【Android】Androidにおける実装の負の遺産を最新にリファクタする

#はじめに
Android OS が上陸して10年以上経った
時代とともに開発のベストプラクティスも変化してきた
その為、昔からあるアプリのプロジェクトでは負の遺産と呼ばれる実装が残ってしまっている場合がある
Android 開発の情報が最新にアップデートできていないという人が実装するとそれがすぐにバレてしまう
また、初心者であれば実装にレベル差を感じられてしまう
今回は昔ながらの実装を最新にリファクタしていく例を紹介してみたいと思う

#サンプルアプリの仕様
今回説明するサンプルアプリの仕様を考えてみる
説明しやすいようにシンプルな仕様とする

* 画面にはボタンと数字表示用テキスト
* ボタンを押す度に数字が1つずつカウントアップしていく
* ホーム等に遷移して再度アプリに戻る（onResume()になる）とカウントリセットする

androidx、navigation、constraintlayout は利用されている前提とした

#昔の実装方法でアプリを作る
Fragment で findViewById をし、クリック処理をセットするような実装となる
実装例は以下に記載した

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("MainActivity", "_onCreate")
        setContentView(R.layout.activity_main)
    }
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mobile_navigation"
    app:startDestination="@+id/navigation_main">

    <fragment
        android:id="@+id/navigation_main"
        android:name="package名.MainFragment"
        android:label="@string/title_main"
        tools:layout="@layout/fragment_main" />
</navigation>
```

```kotlin:MainFragment.kt
class MainFragment : Fragment() {

    private var number = 0
    private lateinit var textView: TextView

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.fragment_main, container, false)
        textView = root.findViewById(R.id.text_main)
        textView.setText(number.toString())

        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            // カウントアップして表示更新
            number++
            textView.setText(number.toString())
        }

        return root
    }

    override fun onResume() {
        super.onResume()

        // カウントリセットして表示更新
        number = 0
        textView.setText(number.toString())
    }
}
```

```xml:fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="package名.MainFragment">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/text_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:gravity="center_horizontal"
        android:textAlignment="center"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

```xml:strings.xml
<resources>
    <string name="app_name">My Application</string>
    <string name="title_main">Main</string>
</resources>
```

これでアプリを実行すると仕様通りに動く
![スクリーンショット 2020-11-08 17.17.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/78eba68c-f768-f387-8678-bbeecea13888.png)

#何が負の遺産なのか？
上記の実装でも十分に仕様通り動く
昔のAndroidの教材ではこのような実装例が多かった
しかし、この実装をやってしまうと問題が出てくる
何が問題となるのか？
それは、View（Activity や Fragment）に処理が集中してしまうことである
今回のサンプルではカウントアップの実装しかないが、このまま徐々に機能が増えていくと何千行もある肥大化した Fragment のクラスになってしまう

では解決方は？

#MVVM を使う
View（Activity や Fragment）に処理が集中してしまわないように様々なアーキテクチャが登場した
そして MVVM での実装が今ではスタンダードとなっている
ViewModel に処理を委譲することで View が肥大化することなくなる
一時期 Presenter に委譲する方法も流行ったが今は ViewModel を利用するのが当たり前になってきている
また、値を監視し View 側にオブザーブするために LiveData が使われる
ViewModel + LiveData で監視することでメモリリークを減らすこともできるので使いこなしたい

[MVVM（公式）](https://developer.android.com/jetpack/docs/guide?hl=ja)
[ViewModel（公式）](https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja)
[LiveData（公式）](https://developer.android.com/topic/libraries/architecture/livedata?hl=ja)

#ViewModel + LiveData を使ったサンプル
サンプルをViewModelを使った実装にリファクタしてみる
まず、準備が必要なので gradle に手を入れる

```gradle:build.gradle[app]
dependencies {
    // 最新バージョンを利用してください
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
}
```

主な変更点は以下となる
* ViewModel クラスを作成する
* LifecycleObserver インターフェイスを利用しライフサイクルを委譲する
* View 側にデータをオブザーブするための MutableLiveData 定義追加

```kotlin:MainViewModel.kt
class MainViewModel : ViewModel(), LifecycleObserver {

    private var number = 0

    val numberText: MutableLiveData<String> by lazy {
        MutableLiveData<String>()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate() {
        Log.d("MainViewModel", "ON_CREATE")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        Log.d("MainViewModel", "ON_RESUME")
        number = 0
        numberText.value = number.toString()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        Log.d("MainViewModel", "ON_PAUSE")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestory() {
        Log.d("MainViewModel", "ON_DESTROY")
    }

    fun clickButton() {
        // カウントアップ
        number++
        // View に値をオブザーブ
        numberText.value = number.toString()
    }

    init {
        numberText.value = number.toString()
    }
}
```

```kotlin:MainFragment.kt
class MainFragment : Fragment() {

    private lateinit var textView: TextView

    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.fragment_main, container, false)
        textView = root.findViewById(R.id.text_main)
        textView.setText(number.toString())

        // LifecycleObserver でライフサイクルが管理できる
        lifecycle.addObserver(mainViewModel)
        // LiveData を監視するオブザーバーとする定義
        mainViewModel.numberText.observe(viewLifecycleOwner, Observer {
            // ViewModel から渡ってきたデータで TextView 更新
            textView.setText(it)
        })

        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            // ViewModel でクリック処理をする
            mainViewModel.clickButton()
        }

        return root
    }
}
```

これで ViewModel を利用した実装が完成!!
アプリの動作はリファクタ前と変わらない
今後の機能追加は ViewModel にすることになるので Fragment は肥大化しにくい

ここからさらにリファクタしていきたい

#DataBinding を使う
DataBinding はレイアウトのViewの要素とオブジェクトのデータをBindしてくれる機能である
DataBinding を使えば 値の変更が View に自動で反映される
歴史的には DataBinding の方が ViewModel より古くから登場している機能である

[DataBinding（公式）](https://developer.android.com/topic/libraries/data-binding?hl=ja)

MVVM + LiveData + DataBinding
のつくりが Android ではスタンダードなっている

早速、DataBinding を利用した実装にリファクタしてみる
こちらも gradle の準備が必要

```gradle:build.gradle[app]
android {
    // 省略
    dataBinding {
        enabled = true
    }
}
```

MainFragment と fragment_main のファイルを主に変更する

レイアウトファイル（fragment_main）にdata要素を記述する
layoutタグ -> data要素 -> Viewのroot要素
nameにはxml上で扱う変数名、typeにはバインド対象のオブジェクトを指定する

View から MainViewModel オブジェクトのプロパティにアクセスするためにandroid:text=@{viewModel.number} のようにする
クリック処理も定義可能
android:onClick="@{() -> viewmodel.clickButton()}"

```kotlin:MainViewModel.kt
class MainViewModel : ViewModel(), LifecycleObserver {

    val number: MutableLiveData<Int> by lazy {
        MutableLiveData(0)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate() {
        Log.d("HomeViewModel", "ON_CREATE")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        Log.d("HomeViewModel", "ON_RESUME")
        number.value = 0
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        Log.d("HomeViewModel", "ON_PAUSE")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestory() {
        Log.d("HomeViewModel", "ON_DESTROY")
    }

    fun clickButton() {
        number.value = (number.value ?: 0) + 1
    }

    init {
        number.value = number.value ?: 0
    }
}
```

```kotlin:MainFragment.kt
class MainFragment : Fragment() {

    private lateinit var binding: FragmentMainBinding

    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        binding = FragmentHomeBinding.inflate(layoutInflater, container, false)
        // or
        //  binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)

        // ViewModel をセット
        binding.viewmodel = mainViewModel

        // ※これを設定しないとViewModleのLiveDataでViewの値が更新されない
        // 意外とここでハマる人が多い
        binding.lifecycleOwner = viewLifecycleOwner
        // Activity であれば viewLifecycleOwner → this

        lifecycle.addObserver(mainViewModel)

        return binding.getRoot()
    }

}
```

```xml:fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewmodel"
            type="package名.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="package名.MainFragment">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            android:onClick="@{() -> viewmodel.clickButton()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/text_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:gravity="center_horizontal"
            android:text="@{String.valueOf(viewmodel.number)}"
            android:textAlignment="center"
            android:textSize="20sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

これで Fragment の実装がかなりスッキリする!
findViewById の記述がいらなくなった

#おまけ
ある条件の時に特別な処理をしないといけない場合があると思う
Transformations.map() という便利なAPIがある
データに変更があったらそのデータを加工して違うデータにすることが簡単にできる

```kotlin:MainViewModel.kt
class MainViewModel : ViewModel(), LifecycleObserver {

    val number: MutableLiveData<Int> by lazy {
        MutableLiveData(0)
    }

    val numberText = Transformations.map(number) {
        when {
            it > 20 -> "OVER 20"
            it > 4 && it < 9 -> "$it is OVER 4 UNDER 9"
            else -> number.value.toString()
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        Log.d("HomeViewModel", "ON_RESUME")
        number.value = 0
    }

    fun clickButton() {
        number.value = (number.value ?: 0) + 1
    }

    init {
        number.value = number.value ?: 0
    }
}
```

```xml:fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewmodel"
            type="package名.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="package名.MainFragment">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            android:onClick="@{() -> viewmodel.clickButton()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/text_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:gravity="center_horizontal"
            android:text="@{viewmodel.numberText}"
            android:textAlignment="center"
            android:textSize="20sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

![スクリーンショット 2020-11-08 17.18.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/2f67f1cb-1228-7f33-9f65-48e29988efb4.png)

#最後に
Android 開発手法は進化しているので情報のアップデートをみんなで頑張りましょう
また、求人情報を見ていて↓のような要件があったら、これのことなんだとこの記事の内容を思い出してほしい

◆必須スキル/経験 
 ・Androidネイティブアプリ開発において、以下の実務経験がある方 
・Kotlinでのアプリ実装 
・MVVMでの実装 
・AndroidXコンポーネントを用いた実装