Android#2 Advent Calendar 2019 21日目の記事です
どうも、iOSエンジニア→Androidエンジニアにコンバートして丸一年が経った@orimomoです。
思えば、ある日突然Androidエンジニア(それも、社内唯一の…)になってから色々ありました。
Android Studioとはなんぞやというところから始まり、iOSとの違いに苦しみ、チョットデキルようになったと思ったらライフサイクルの罠にハマり、あれよあれよという間にAndroid10がリリースされて対応に追われ…。
そんな辛くも楽しい一年を振り返り、実際にハマったり、ベテランエンジニアにレビューで指摘を受けたことを中心にTips集としてまとめてみました
これから本格的にAndroid開発を始める方にとって、少しでもお役に立てば嬉しいです。
ライフサイクルについてのTips
皆さんご存知、この図です。
「iOSのライフサイクルと同じようなものでは?」と思っていた私が躓いた点を挙げておきます。
(ここではFragmentのライフサイクルに絞って話をします)
フラグメント | Android デベロッパー | Android Developers
■ライフサイクルメソッドは対になっている
-
onAttach()
とonDetach()
-
onCreate()
とonDestroy()
-
onCreateView()
とonDestroyView()
のように、ライフサイクルメソッドの中には対になっているものがあります。(裏を返せば対になっていないものもあるのですが、ここでは省きます)
これはつまり、onCreate()
でした設定はonDestroy()
でキャンセルしないといけないし、
onCreateView()
で設定した値はonDestroyView()
でnullを詰めたりして破棄しないといけない、ということです。
間違えて対になっていないメソッドの中で後処理したりすると、「呼ばれない」などの意図しない不具合が発生するので注意が必要です。
これに関しては下記記事がわかりやすいので一読されることをオススメします。
あの日挫折した Android 初心者へ戻るためのショートカット 〜ライフサイクル編〜 - Qiita
■画面遷移でライフサイクルメソッドが呼ばれる順番に注意
画面Aと画面Bがあったとして、A→Bに遷移するとき、ライフサイクルメソッドが呼ばれる順番はどうなるでしょうか?
画面Aで全てのライフサイクルメソッドが呼ばれた後に、画面Bのライフサイクルが始まってくれると一番わかりやすいのですが、
実際は、画面Aのライフサイクルメソッドがまだ残っているうちに、画面Bのライフサイクルがスタートしてしまう場合があります。
私が遭遇したのは、遷移前の画面のonDestroyView()
よりも、遷移後の画面のonViewCreated()
のほうが先に呼ばれてしまうことにより、インスタンスが正常に破棄されないという不具合でした。
この手の不具合はコードレビューでは見つかりづらいので、ログ出力などを使い、意図したタイミングでメソッドに入ってるかを確認しながら実装するのが良いと思います。
レイアウトについてのTips
■ConstraintLayoutで階層を浅くする
RelativeLayout
の中にRelativeLayout
があって、さらにその中にLinearLayout
があって…というようなネストの深いレイアウトは、ビューの描画に時間がかかるため、ユーザーに優しいレイアウトとは言えません。
<RelativeLayout>
<ImageView />
<TextView />
<RelativeLayout>
<TextView />
<LinearLayout>
<TextView />
<RelativeLayout>
<EditText />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
2016年のGoogle I/Oで発表された**ConstraintLayout
を使うことでレイアウトの階層をフラットにすることができるので、可能な限りConstraintLayout
を使う**などして、ネストが深くならないように工夫できると良さそうです。
ConstraintLayout
を使うことのメリットやAndroidの描画処理については下記記事がわかりやすかったです。
Google Developers Japan: ConstraintLayout がもたらすパフォーマンスのメリットを理解する
■ConstraintLayout
内のビューでは、match_parent
ではなく0dp
を使う
ConstraintLayout
では、従来使われていたmatch_parent
が廃止されたため、代わりに**「制約に合致(Match Constraints)」を意味する0dp
を指定する必要があります。**
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/parent_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--layout_widthのmatch_parentを、0dpに変える必要がある-->
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="10dp"
android:background="@color/white"
下記の通り、公式ドキュメントにもしっかり記載されています。
ちなみにmatch_parent
を使っても動くは動くのですが、下記記事にあるように想定外の挙動をする場合があるので、素直に使わないのが吉です。
ConstraintLayoutでMATCH_PARENT利用時の想定外の挙動 - Qiita
ConstraintLayout
はiOSのAuto Layoutに似ており、雰囲気で使ってしまいがちだからこそ注意が必要だなと感じています。
LiveDataについてのTips
■LiveDataをobserveするときは、 this
ではなくviewLifecycleOwner
を使う
下記コードのように、FragmentでLiveDataをobserveするときにthis
を渡す(Fragment自身をLifecycleOwnerとして設定する)のにはリスクがあります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.liveData.observe(this, Observer {
// 処理
})
}
このObserverが破棄されるタイミングは、主にFragmentのonDestroy()
が呼ばれたタイミングです。
しかし、上に貼ったライフサイクルの図を再度見てもらうと分かる通り、
FragmentのonDestroy()
が呼ばれずに、複数回onCreateView()
が呼ばれる(→Fragmentのインスタンスは生き残り、Viewだけが破棄される)ことがあり、そうすると前のObserverが破棄されずに残ってしまいます。
結果、アクティブなObseverの数がどんどん増えていくことになり、メモリリークや意図しない不具合の原因になります。
それを防ぐために、FragmentにはView用のLifecycle、viewLifecycleOwner
が用意されているので、それをLifecycleOwnerに設定してあげると良いです。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.liveData.observe(viewLifecycleOwner, Observer {
// 処理
})
}
これにより、FragmentのViewが破棄されると毎回LiveDataも破棄されるようになるため、複数のObserverが登録されるのを回避できます。
この問題については色々な記事が出ていますが、個人的には下記2つの記事がわかりやすかったです。
Android Architecture Componentsで犯しがちな5つの間違い【翻訳】|ふぉれ$Cooking Programmer|note
FragmentとgetViewLifecycleの話 - stsnブログ
おわりに
今ではAndroid開発と、スマートに書けるKotlinが大好きなので、引き続き深めていきたい気持ちです。
今回書ききれなかったTipsもまだまだあるので、また別の機会に紹介できたらと思います。
来年もモバイル開発、やっていくぞ