見出し画像

「COMPOSE を用いた ANDROID アプリ開発の基礎」の学習支援⑯ -ユニット5パスウェイ2

皆さん、こんにちは!又はこんばんは!初めての方は初めまして!
Google Codelabsの「COMPOSE を用いた ANDROID アプリ開発の基礎」コースのお手伝いをする「りおん」です。
今回は、ユニット5「インターネットに接続する」のパスウェイ2「インターネットから画像を読み込んで表示する」です。

補足のため、「COMPOSE を用いた ANDROID アプリ開発の基礎」コースで心配になった時、エラーが起きて詰まった時や、分からないことがあった時、軽く復習したい時に見てください!
また、目次を見て自分に必要なところだけ見るのをお勧めします!

この記事を作成するにあたり使用しているAndroid StudioのバージョンはGiraffeです。バージョンによってはUIが違うことがあるのでご了承ください。
また、2024年4月16日現在の「COMPOSE を用いた ANDROID アプリ開発の基礎」コースを参考にしています。


学習内容

②リポジトリと手動依存関係挿入を追加する

このパスウェイで学習したことは以下の3つです。
1.リポジトリ
2.依存関係インジェクション(DI)
3.コンテナ
4.リポジトリ、ViewModelの単体テスト

リポジトリ

リポジトリ(repository)とは、直訳すると「(大量の物の)保管場所」です。(ジーニアス英和辞典第五版より)
リポジトリを実装する理由は関心の分離を実現するためです。関心の分離を行うことでコードを一部変更したときに他の部分に影響を与えることが少なくなるなどメリットが多くあります。

関心の分離を実現する方法の一つとして、Androidの推奨アプリアーキテクチャによれば、アプリにはUIレイヤとデータレイヤがあるべきとされています。

アプリのアーキテクチャ
Codelabsの「Compose での ViewModel と状態」より

UIレイヤの役割はアプリデータを画面に表示することです。UIレイヤの一部であるViewModel(状態ホルダー)の役割はデータの保持、UIへの公開、アプリロジックの処理です。
一方、データレイヤの役割は、アプリのビジネスロジックの処理とアプリのデータの収集と処理です。要はデータに関すること全般です。

リポジトリがない場合はアプリがUIレイヤとデータレイヤに分離できません。理由としてViewModelがデータレイヤの役割であるはずのデータの収集を行っているためです。

リポジトリがない場合のアプリアーキテクチャ

一方、リポジトリがある場合はアプリがUIレイヤとデータレイヤに分離できます。

リポジトリがある場合のアプリアーキテクチャ

以上のことから、リポジトリはUIレイヤとデータレイヤの分離に一役買っている ➡ 関心の分離に役立っていることが分かります。

依存関係インジェクション(DI)

依存関係インジェクション(DI)は、実行時に依存関係を提供することです。DIを実現する方法として、依存するオブジェクトをクラスの外部でインスタンス化してから渡すことが挙げられます。

用語自体の解説をすると、依存関係、インジェクションという2つの単語から成り立っています。
依存関係とは、あるクラスで別のクラスが必要な場合にその必要なクラスは依存関係と言います。
インジェクションとは、直訳すると「注入」です。(ジーニアス英和辞典第五版より)

DIに従うことでコード間のつながりが薄くなる(=疎結合)ため、コードの再利用やリファクタリング、テストが容易になります。

DIに従わない場合とDIに従った場合でコードの変更にどのような影響があるか見てみましょう。
(Codelabsでも同じことをしましたが、ここではViewModelクラスがRepositoryクラスに依存しています)
以下のコードはDIに従わない例です。

interface Repository{
    fun printWord()
}

class Repository1: Repository{
    override fun printWord(){
        println("Repository1_OK")
    }
}

class ViewModel{
    private val repository = Repository1()
    
    fun printWord(){
        repository.printWord()
    }
}

fun main() {
    val viewModel = ViewModel()
    viewModel.printWord()
}

この例において、Repository2が必要な場合において以下のようにviewModelクラスを変更しないといけません。
つまり、コードに柔軟性がありません(=変更が困難)。

interface Repository{
    fun printWord()
}

class Repository1: Repository{
    override fun printWord(){
        println("Repository1_OK")
    }
}

class Repository2: Repository{
    override fun printWord(){
        println("Repository2_OK")
    }
}

class ViewModel{
    private val repository = Repository2()//変更した箇所その1
    
    fun printWord(){
        repository.printWord()//変更した箇所その2
    }
}

fun main() {
    val viewModel = ViewModel()
    viewModel.printWord()
}

一方、DIに従うとコードは以下のようになります。

interface Repository{
    fun printWord()
}

class Repository1: Repository{
    override fun printWord(){
        println("Repository1_OK")
    }
}

class ViewModel(private val repository: Repository){
    fun printWord(){
        repository.printWord()
    }
}

fun main() {
    val repository = Repository1()
    val viewModel = ViewModel(repository)
    viewModel.printWord()
}

この例において、Repository2が必要な場合は実行部(ここではmain関数)を変更するだけで済みます

interface Repository{
    fun printWord()
}

class Repository1: Repository{
    override fun printWord(){
        println("Repository1_OK")
    }
}

class Repository2: Repository{
    override fun printWord(){
        println("Repository2_OK")
    }
}

class ViewModel(private val repository: Repository){
    fun printWord(){
        repository.printWord()
    }
}

fun main() {
    val repository = Repository2()//変更箇所
    val viewModel = ViewModel(repository)
    viewModel.printWord()
}

Repository2クラスをFakeRepository(テスト用のリポジトリ)、fun main()を@Test fun viewModel_printWordと考えればDIは単体テストにも役立つことも分かると思います。

コンテナ

コンテナとは依存関係を含むオブジェクトです。
今回はネットワーク呼び出しを行う際にViewModelはリポジトリに依存しており、この依存関係にDIを適用するためにコンテナを作成しました。

コンテナがなぜ必要なのか説明します。
まず、簡単な例としてDIを使用せずにリポジトリを実装した例を挙げます。
DIを使用していないというのはViewModelクラスの引数にrepositoryがないことからわかると思います。
このコードは、viewModelをテストすることが非常に困難であるため適切とは言えません。

//依存関係インジェクション(DI)を用いない場合 
data class Process(
    val repositoryName: String,
    val message: String
)

interface Repository{
    fun message()
}

class Repository1(private val process: Process): Repository{
    override fun message(){
        println("${process.repositoryName} - ${process.message}")
    }
}

class FakeRepository(): Repository{
    override fun message(){
        println("FakeRepository - OK")
    }
}

class ViewModel(){
    val process = Process("Repository1", "OK")
    val repository = Repository1(process)
    fun message(){
        repository.message()
    }
}

fun main() {
    val viewModel = ViewModel()
    viewModel.message()
    //FakeRepositoryを使ってviewModelをテストしたいが、
    //viewModel自体を変更しないとテストできないので、
    //適切なViewModelのテストを行えない
}

次に上と同じことをコンテナを用いずにDIに沿った実装をした例です。
このコードはDIに従っているためテスト用のFakeRepositoryを用いてViewModelをテストをすることが容易です。

//DIを適用。コンテナ無し
data class Process(
    val repositoryName: String,
    val message: String
)

interface Repository{
    fun message()
}

class Repository1(private val process: Process): Repository{
    override fun message(){
        println("${process.repositoryName} - ${process.message}")
    }
}

class FakeRepository(): Repository{
    override fun message(){
        println("FakeRepository - OK")
    }
}

class ViewModel(private val repository: Repository){
    fun message(){
        repository.message()
    }
}

fun main() {
    val process = Process("Repository1", "OK")
    val repository = Repository1(process)
    val viewModel = ViewModel(repository)
    viewModel.message()
    
    val fakeRepository = FakeRepository()
    val testViewModel = ViewModel(fakeRepository)
    testViewModel.message()
}

最後にコンテナを用いてDIに沿った実装をした例です。
このコードもDIに従っているためテスト用のFakeRepositoryを用いてViewModelをテストをすることが容易です。

//コンテナを使用してDIを適用
data class Process(
    val repositoryName: String,
    val message: String
)

interface Repository{
    fun message()
}

class Repository1(private val process: Process): Repository{
    override fun message(){
        println("${process.repositoryName} - ${process.message}")
    }
}

class Container(){
    val process = Process("Repository1", "OK")
    val repository = Repository1(process)
}

class FakeRepository(): Repository{
    override fun message(){
        println("FakeRepository - OK")
    }
}

class ViewModel(private val repository:Repository){
    fun message(){
        repository.message()
    }
}

fun main() {
    val repository = Container().repository
    val viewModel = ViewModel(repository)
    viewModel.message()
    
    //テスト用
    val fakeRepository = FakeRepository()
    val testViewModel = ViewModel(fakeRepository)
    testViewModel.message()
}

上の3つのコードからわかるようにDIは必須ですが、DIを実現する為だけならコンテナは必要ありません。
しかし、コンテナは依存関係を入れると決めることで関心の分離が行えます。つまり、コードの再利用(コンテナの場合はオブジェクトの再利用)が容易になり、コードの変更も容易になります。

まとめると、DIを実現する方法の最適解として依存関係をコンテナに収めているということです。

次に、今回のCodelabsでコンテナをどのように作成したかを見ていきます。

今回作成したコンテナ
viewModelが依存関係としてリポジトリを受け取るため、MarsPhotosRepositoryプロパティが格納されている

コンテナは全てのアクティビティが使用できる場所に配置する必要があります。そのため、Applicationクラスのサブクラスを作成し、コンテナへの参照を格納しました。また、定義したアプリクラスをアプリで使用するためにはAndroidマニフェストを更新する必要もあります。

今回Applicationクラスのサブクラスとして作成したMarsPhotosApplicationクラス
Androidマニフェストの更新

下の画像はリポジトリを依存関係として受け取るviewModelのFactoryです。
Factoryとは、オブジェクトの作成に使用される作成パターンです。
Android フレームワークでは、ViewModel を作成する際にコンストラクタで値を渡すことができないため、コンストラクタではなくFactoryメソッドを呼び出してオブジェクトを作成します。
コンテナの中に依存関係を書いているため、viewModel.Factoryオブジェクトは下の写真のようになります。

リポジトリ、ViewModelの単体テスト

ViewModel、リポジトリのテストはDIによってそれぞれ簡単に単体テストすることが可能になりました
その方法は、テスト用の簡易なリポジトリ、データソースを作成しこれを用いてそれぞれテストするというものでした。
(CodelabsではFake~クラスで表現されていました)

viewModelのテストの場合、テスト用のFakeDataSourceとFakeRepositoryを使用

③インターネットから画像を読み込んで表示する

このパスウェイではCoilライブラリを使って画像のURLから画像を表示する方法を学習しました。
(LazyVerticalGridはユニット3パスウェイ2の④章で学習しています)

必要なものは以下の2つです。
1.画像のURL(下の画像ではphoto.imgSrc)
2.画像を表示するためのAsyncImageコンポーザブル

AsyncImageコンポーザブルはImageコンポーザブルと同じ引数もサポートしているので使い勝手はほぼ変わりません

さいごに

今回もCodelabsでの学習お疲れさまでした!
今回は前半はリポジトリ、依存関係インジェクションという関心の分離を行うことでコードの再利用やリファクタリング、可読性などを向上させるという内容でした。
後半はCoilライブラリを使用してURLから画像を読み込んで表示させるという内容でした。
このパスウェイを終了した方は是非、自分が作成したアプリにコンテナとリポジトリを実装し、関心の分離によって得られる恩恵を感じてみてください!
次はいよいよデータの永続化です。アプリにとって非常に大事な要素ですので是非学習を続けてください!

この記事が気に入ったらサポートをしてみませんか?