Androidエンジニアにコンバートして学んだ、厳選Tips集

[Android#2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/android-2) 21日目の記事です:christmas_tree:

どうも、iOSエンジニア→Androidエンジニアにコンバートして丸一年が経った[@orimomo](https://twitter.com/orimomo_147cm)です。

思えば、ある日突然Androidエンジニア（それも、社内唯一の…）になってから色々ありました。
Android Studioとはなんぞやというところから始まり、iOSとの違いに苦しみ、チョットデキルようになったと思ったらライフサイクルの罠にハマり、あれよあれよという間にAndroid10がリリースされて対応に追われ…。

そんな辛くも楽しい一年を振り返り、**実際にハマったり、ベテランエンジニアにレビューで指摘を受けたこと**を中心にTips集としてまとめてみました:slight_smile:

これから本格的にAndroid開発を始める方にとって、少しでもお役に立てば嬉しいです。

# ライフサイクルについてのTips
皆さんご存知、この図です。
「iOSのライフサイクルと同じようなものでは？」と思っていた私が躓いた点を挙げておきます。
（ここではFragmentのライフサイクルに絞って話をします）
![fragment_lifecycle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/db63f316-2fef-357d-8320-00e708b43b1d.png)
[フラグメント  |  Android デベロッパー  |  Android Developers](https://developer.android.com/guide/components/fragments?hl=ja#Creating)

## ■ライフサイクルメソッドは対になっている
* `onAttach()`と`onDetach()`
* `onCreate()`と`onDestroy()`
* `onCreateView()`と`onDestroyView()`

のように、**ライフサイクルメソッドの中には対になっているものがあります。**（裏を返せば対になっていないものもあるのですが、ここでは省きます）

これはつまり、`onCreate()`でした設定は`onDestroy()`でキャンセルしないといけないし、
`onCreateView()`で設定した値は`onDestroyView()`でnullを詰めたりして破棄しないといけない、ということです。

**間違えて対になっていないメソッドの中で後処理したりすると、「呼ばれない」などの意図しない不具合が発生するので注意が必要**です。

これに関しては下記記事がわかりやすいので一読されることをオススメします。
[あの日挫折した Android 初心者へ戻るためのショートカット 〜ライフサイクル編〜 - Qiita](https://qiita.com/akitaika_/items/150857170903f38d3d1c)

## ■画面遷移でライフサイクルメソッドが呼ばれる順番に注意
画面Aと画面Bがあったとして、A→Bに遷移するとき、ライフサイクルメソッドが呼ばれる順番はどうなるでしょうか？

画面Aで全てのライフサイクルメソッドが呼ばれた後に、画面Bのライフサイクルが始まってくれると一番わかりやすいのですが、
**実際は、画面Aのライフサイクルメソッドがまだ残っているうちに、画面Bのライフサイクルがスタートしてしまう場合があります。**

私が遭遇したのは、遷移前の画面の`onDestroyView()`よりも、遷移後の画面の`onViewCreated()`のほうが先に呼ばれてしまうことにより、インスタンスが正常に破棄されないという不具合でした。
この手の不具合はコードレビューでは見つかりづらいので、ログ出力などを使い、意図したタイミングでメソッドに入ってるかを確認しながら実装するのが良いと思います。

# レイアウトについてのTips
## ■ConstraintLayoutで階層を浅くする
`RelativeLayout`の中に`RelativeLayout`があって、さらにその中に`LinearLayout`があって…というような**ネストの深いレイアウトは、ビューの描画に時間がかかるため、ユーザーに優しいレイアウトとは言えません。**


```xml:イマイチな例
<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 がもたらすパフォーマンスのメリットを理解する](https://developers-jp.googleblog.com/2017/09/understanding-performance-benefits-of.html)

## ■`ConstraintLayout`内のビューでは、`match_parent`ではなく`0dp`を使う
`ConstraintLayout`では、従来使われていた`match_parent`が廃止されたため、代わりに**「制約に合致（Match Constraints）」を意味する`0dp`を指定する必要があります。**

```xml:悪い例
<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"
```


下記の通り、[公式ドキュメント](https://developer.android.com/training/constraint-layout?hl=ja)にもしっかり記載されています。
<img width="876" alt="スクリーンショット 2019-12-15 12.17.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/1d3692ee-16df-e393-d6f0-d77c2b6f73de.png">
ちなみに`match_parent`を使っても動くは動くのですが、下記記事にあるように想定外の挙動をする場合があるので、素直に使わないのが吉です。
[ConstraintLayoutでMATCH_PARENT利用時の想定外の挙動 - Qiita](https://qiita.com/ara_tack/items/68c07529c1477c56997f)

`ConstraintLayout`はiOSのAuto Layoutに似ており、雰囲気で使ってしまいがちだからこそ注意が必要だなと感じています。

# LiveDataについてのTips
## ■LiveDataをobserveするときは、 `this`ではなく`viewLifecycleOwner`を使う
下記コードのように、FragmentでLiveDataをobserveするときに`this`を渡す（Fragment自身をLifecycleOwnerとして設定する）のにはリスクがあります。

```kotlin:悪い例
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に設定してあげると良いです。**

```kotlin:良い例
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](https://note.com/d_forest/n/n9c2dcf155f34)
[FragmentとgetViewLifecycleの話  - stsnブログ](https://satoshun.github.io/2018/12/view_lifecycle/)

# おわりに
今ではAndroid開発と、スマートに書けるKotlinが大好きなので、引き続き深めていきたい気持ちです。
今回書ききれなかったTipsもまだまだあるので、また別の機会に紹介できたらと思います。

来年もモバイル開発、やっていくぞ:star2: