araki tech

for developers including me

Android Kotlin日本語チュートリアル-④Android推奨アーキテクチャを取り入れる

Android Kotlin日本語チュートリアル

Android Kotlin日本語チュートリアル

本連載記事はこれからAndroidアプリ開発を始める人に向けたチュートリアルです。

コンセプトは

  • プログラミングをあまり知らない人でも完走できる
  • プログラミングにある程度詳しい人にも満足できる
  • 実用的な知識を提供する
  • とにかくわかりやすく

で、全9回と長めですが頑張っていきましょう。

このチュートリアルを終える頃には、Android開発の土台が形成されているだけでなくアプリケーションアーキテクチャの知識が出来上がっているはずです。

作成するのは以下のようなメモアプリです。

Android Kotlin日本語チュートリアル

完成品は HiroshiARAKI/AndroidKotlinTutrialで公開していますので適宜参考にしてください。

第4回 : Android推奨アーキテクチャを取り入れる

第4回は、タイトル通りAndroid推奨アーキテクチャについての話です。

今回も解説部分が多くなるところもありますが、結構大事なのでできるだけ飛ばさずに進めてください。

ソフトウェアアーキテクチャについて

ソフトウェア開発、アプリケーション開発ではアーキテクチャという概念は大切です。

有名で最も古いアーキテクチャのMVC (Model View Controller) をはじめ、最近ではMVP (Model View Presenter) や MVVM (Model View ViewModel)、そしてClean Architectureなどが人気です。

このようなソフトウェアアーキテクチャ (アプリケーションアーキテクチャ) で共通している大事な概念は、依存関係を単純に、そして一方通行にすることです。

「何言ってるかわからん」という方もいるかと思いますが、このまま読み進めてください。

Android推奨アーキテクチャ

アプリ アーキテクチャ ガイド | Android developerによると、Androidでは以下のようなアーキテクチャでアプリを設計することが推奨されています。

Android Kotlin日本語チュートリアルAndroid Kotlin日本語チュートリアル

MVVMと少し似ていますが厳密には違います。

大事なのは、各要素でしっかりと責務を分けることです。

ActivityとFragmentはわかると思いますが、他の要素はまだ紹介していないのでこれから紹介します。

  • Activity / Fragment … Viewに関する簡単な操作のみを扱う。
  • ViewModel … View生成及び操作に必要なロジックや、LiveDataを管理する。基本的に各Activity/Fragmentに対して一つだけ存在する。
  • Repository … データとの境界面であり、データ取得に必要なロジックを管理する。

Modelの層は一旦置いておいて、このような役割を各要素に持たせるようにします。

もっと噛み砕いて言うと、「Repositoryは上位のViewModelやActivityの存在を知らなくても動作するべき」または「ViewModelはActivityを知らなくても動作するべき」と表現できます。

もう一つ具体的な例を挙げると、「ActivityはViewModelのインスタンス(参照)を持っていても良いが、ViewModelはActivityのインスタンス(参照)を持つべきではない」とも言えます。

少しややこしいですかね。

今はわからなくてもこれを意識してプログラミングすることで、アプリの管理が何倍にもしやすくなります。




ViewModelを作ってみる

それでは早速ViewModelを作ってみましょう。

新たに viewmodel/ パッケージを作成し、MainActivityのViewModelとして、MainViewModel.ktを作成します。

import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel() {
}

とりあえず、ViewModel() を継承させれば、それだけでViewModelにはなります。

そうしたら、ViewModelを簡単に扱うために新しく Android Jetpackの必要となるライブラリをGradleに追加しましょう。

build.gradle (Module)を開いて以下を加筆してください。


dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.fragment:fragment-ktx:1.3.6'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

これでViewModelの扱いが少しだけ楽になります。

この拡張ライブラリで以下のようにViewModelの初期化をシンプルに記述可能です。

class MainActivity : AppCompatActivity() {
    
    private val viewModel: MainViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
    }
}

これからは複雑なロジックを含む操作は全てViewModelに任せる、という風にしましょう。

LiveDataを使ってデータを監視する

AndroidアーキテクチャやMVVMで扱うViewModelは、大きな責務のひとつとしてData Bindingがあります

簡単に説明すると、「データを監視し続け、データ変更の変更をトリガーになにかしらの処理を開始する」のがData Bindingです。

実際に実装しながら、その様子を確認しましょう。

まずはMainViewModelに、RecyclerViewのデータを管理するLiveDataを作ります。

class MainViewModel : ViewModel() {
    
    /** ViewModel内で扱うミュータブルなLiveData */
    private val _memoItems = MutableLiveData<List<Memo>>()
    
    /** 外部公開用のイミュータブルなLiveData */
    val memoItems: LiveData<List<Memo>> = _memoItems
}

ちなみにミュータブル (Mutable)/イミュータブル (Immutable)について補足が必要であれば KotlinのMutableとImmutable【初学者向け】 を参照してください。

そうしたらMainActivity側で、このLiveDataを監視し、変更があったら何をするかを決めましょう

class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val addButton: Button = findViewById(R.id.add_button)

        val recyclerView: RecyclerView = findViewById(R.id.main_list)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = MainAdapter(Memo.createFakes())
        recyclerView.addItemDecoration(
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        )

        addButton.setOnClickListener {
            supportFragmentManager.beginTransaction().run {
                add(R.id.main_container, NewMemoFragment())
                addToBackStack(null)
                commit()
            }
        }

        viewModel.memoItems.observe(this) {
            // ここでRecyclerViewのデータをアップデートしたい
        }
    }
}

LiveDataのObserve処理を実装する

アプリの理想としては、起動からUI表示は迅速にすべきです。

現在の実装だと、MainAdapter(Memo.createFakes())でMemoのリスト生成 (最終的にはデータベースやキャッシュからの取得)に時間がかかれば、UIがユーザの目の前に現れるまで時間を要してしまいます。

これは避けるべきで、私たちがやるべきことは一旦空のリストでUIを描画してデータが定まり次第、再描画をかけることです。

早速それをやってみます。

MainAdapter.kt
class MainAdapter : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {

    private var memoItems: List<Memo> = emptyList()

    // ...
    
    /** データの変更とデータ変更通知 */
    fun setMemoItems(items: List) {
        memoItems = items
        notifyDataSetChanged()
    }
}
MainActivity.kt
        val recyclerView: RecyclerView = findViewById(R.id.main_list)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = MainAdapter()
        recyclerView.addItemDecoration(
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        )

これで空リストでのUI描画がまずは行われます。

そうしたらViewModel側でデータの読み込みを行います。

MainViewModel.kt
class MainViewModel : ViewModel() {

    /** ViewModel内で扱うミュータブルなLiveData */
    private val _memoItems = MutableLiveData<List<Memo>>()

    /** 外部公開用のイミュータブルなLiveData */
    val memoItems: LiveData<List<Memo>> = _memoItems
    
    /** メモリストを読み込む */
    fun loadMemoItems() {
        // データの取得は非同期で
        viewModelScope.launch(Dispatchers.IO) {  // データ取得はIOスレッドで
            // LiveDataのメインスレッド以外での変更は、postValue()を使う
            // メインスレッドならば、_memoItems.value = でOK
            _memoItems.postValue(Memo.createFakes())  // 本来はDBやCacheから取得
        }
    }
}

あとは、MainActivityでデータの読み込みと、データ変更時の処理を追記するだけです。

MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val addButton: Button = findViewById(R.id.add_button)

        val recyclerView: RecyclerView = findViewById(R.id.main_list)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = MainAdapter()
        recyclerView.addItemDecoration(
            DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        )

        addButton.setOnClickListener {
            supportFragmentManager.beginTransaction().run {
                add(R.id.main_container, NewMemoFragment())
                addToBackStack(null)
                commit()
            }
        }

        viewModel.memoItems.observe(this) {
            (recyclerView.adapter as MainAdapter).setMemoItems(it)
        }
        viewModel.loadMemoItems()
    }

おそらく、エミュレータでの動作自体はほとんど変わらないはずですが、空リストでの生成→データ読み込み→再描画が行われているはずです。

試しにMainViewModel#loadMemoItems()を以下のように変更してみると、3秒後に再描画が行われ、この処理がわかりやすくなるでしょう。

/** メモリストを読み込む */
fun loadMemoItems() {
    viewModelScope.launch(Dispatchers.IO) {
        delay(3000)  // 3秒 = 3000ms待機
        _memoItems.postValue(Memo.createFakes())
    }
}

補足: launchによる非同期処理

Kotlinでは、誰かのLifecycleScopeを基準に非同期処理を開始 (launch) できます。

Android開発では以下のことを守りましょう。

  • GlobalScopeは基本使用しない
  • データの取得はDispatchers.IOを指定する

これらを守るだけでも十分良いAndroid開発ができます。

詳しくはAndroid Kotlinにおける非同期処理Tipsを参照してください。

第4回のまとめ

  • Android推奨アーキテクチャはMVVMをベースにしたもの
    • 各クラスで責務を明確に分ける
    • 下層クラスは上層クラスの存在に依存しないようにする
  • ViewModelではLiveDataや比較的複雑なロジックを管理する
    • ViewModelを管理するActivityやFragmentでLiveDataを監視 (Observe)し、データ変更時の処理を記述できる
  • アプリ起動からUI描画は迅速に行うようにする

おわりに

第4回はここで終わりになります。

今回も盛りだくさんな内容でしたが、一番大事なのは依存関係を簡易にすることで、Activity (Fragment)→ViewModelという単一方向の依存関係を守りましょう。

そしてViewModelからActivity (Fragment)にデータ変更を通知するには、Activityのメソッドを呼ぶのではなく、LiveDataの監視によって実現することで、双方向の依存では無くすことができます

まだいまいち理解が追いついていない方もいるかもしれませんが、とにかくViewModelで管理側ActivityやFragmentの参照 (インスタンス) を持ったらNGということだけ頭に入れておいてください。

これは次に解説する、ViewModel→Repositoryでも同様です。

Android Kotlin日本語チュートリアル