ネモラムネ

よくあるAndroidMVVM

#Android #Kotlin

2022-12-14

この記事はAndroid チュートリアルが終わったくらいの人が対象だよ
まだしてなかったらするのをお勧めするよ!

Android は基本的に公式推奨の作り方をカスタムしたもので作ってて、
この記事ではアプリで重要な部分の、
「ネットからデータをとってきて、画面に反映する」ところを大まかに説明するよ

そのためにポケモンの画像を表示するアプリを作るよ
完成図はこんな感じ
上にポケモンの画像があって、下にボタンがあるよ。
ボタンを押すと、
ポケモンの画像がランダムに切り替わるようにするよ
ざっくりとした内容はこんな感じ

言語Kotlin
アーキテクチャMVVM + Repository
ViewViewBinding
非同期Coroutine
DIHilt
HTTPクライアントRetrofit2
JsonライブラリMoshi
イメージローダーGlide

完成品はGithubにあげてるよ

プロジェクトの作成

じゃあさっそくプロジェクトを作るよ

AndroidStudio を開いたら
EmptyActivity でプロジェクトを作るよ

プロジェクトの名前は適当でいいよ。
今回は「AboutMvvm」にするね

まずはプロジェクトで使うライブラリの設定をするよ
gradle を開いてこのコミットを参考にして変更してね
変更したら AndroidStudio の上のほうに Sync っていうのが出るから押してね

https://github.com/nemoramune/AboutAndroidMVVM/commit/aba664e45ea1e0aaa7fc3f6125d91601906f8f4b

Fragment の追加

Android チュートリアルで
Activity と Fragment を使ってレイアウトを作ったと思うよ

このアプリでも同じように作っていくよ
基本的どのアプリでも Activity をベースとして、
実際のレイアウトは Fragment を使って表示するよ

なのでアプリに Activity が一つしかないことも多いね
これを SingleActivityApplication と言ったりするよ
ライフサイクルとかナビゲーションとかいろいろな利点があるから気になったら調べてみてね

まず表示したい Fragment を作るよ

レイアウトはこんな感じになってて、
完成図と同じように画像とボタンがあるね

Fragment のコンストラクターに
レイアウトリソースを渡すと、
onCreateView をオーバーライドしなくても、
レイアウトが適応されるよ。

Activity でも同じことができるから
試してみるのも面白いよ

Fragment ができたから、
Activity で Fragment を表示したいね
チュートリアルで使ったNavigationを使う方法も主流だけど
今回はFragmentManagerを使って表示するよ

そのためには Fragment の入れ物の
FragmentContainerView というのを Activity のレイアウトに追加するよ

FragmentContainerView に Fragment を入れるには、
supportFragmentManager を使うよ

エミュレーターでアプリを起動してみてね
プレビューで設定してるドロイド君の画像は
プレビューのみで表示されるようになってるから、
エミュレーターではボタンだけ表示されると思うんだけど、
Fragment 自体は表示できてるから問題ないよ

なんでエミュレーターで
ドロイド君の画像が表示されないかというと
imageView の画像の指定に
tools:srcCompatというのを使ってるからだよ
tools というのはプレビューにしか
反映されない項目なんだね

Glide で画像読み込み

それじゃあ Fragment で画像を表示してみよう
インターネット上の画像を表示するライブラリはたくさんあるんだけど、
今回は Glide を使ってみるよ

Fragment を表示したときに画像を読み込ませたいから
onViewCreated で Glide を使うよ

onViewCreated 内で View を Binding にしてるのは、View を扱いやすくするためだよ

Binding を使わない場合、findViewByIdとかを使うんだけど、
これは結構重かったりするからあんまり使わないよ
だから View を扱うときは基本的に Binding にしてから使うよ

エミュレーター実行してみても画像が表示されないね

これはアプリにインターネットを使う権限がないからだよ
なので権限をついかしてみよう

権限を追加するにはAndroidManifestにuses-permissionっていうのを追加するよ
それでnameのところに欲しい権限を入れるよ
今回はインターネットだから、android.permission.INTERNETだよ

実行してみるとこんな感じだよ

ボタンに何も設定してないから押しても何も起こらないよ

ボタンについては後々設定することにして
次は ViewModel を作ってみるよ

ViewModel の追加

今のところ画像の Url は TopFragment に書いてあるんだけど、
この画像 Url は Api から取得するつもりだよ

Api っていうのは簡単に言うとインターネット上のサービスと通信するための仕組みのことだよ
つまり画像 Url 自体もインターネットから取得するってことだね
あと、Api と通信する処理とかを非同期処理っていうよ

Android チュートリアルでもしたと思うけど、
非同期で取得した View 用のデータは ViewModel の中に LiveData として保持するよ
とりあえず画像 Url を ViewModel に 移動してみよう

LiveData を宣言するうえで気を付けておきたいのは
MutableLiveData は private で宣言するということだよ
こうすることで Fragment とかから間違って値を変更することがなくなるから安心だよ

あと、Fragment が ViewModel に画像取得してってに言えるように fetch 関数も書いておいたよ
この fetch 関数を呼び出せば LiveData が更新されるんだね

ViewModel を Fragment で使ってみよう

まず Fragment で ViewModel を生成するのには
コンストラクターじゃなくて viewModels っていうのから取得するよ
書き方がちょっと変わってて
委譲プロパティってやつだけどあんまりわからなくても大丈夫だよ

Fragment で ViewModel から画像 Url を取得するために
observe を使って imageUrlLiveData を監視するよ

observe を設定した後に fetch を呼び出すことで
imageUrlLiveData を更新されて、Glide で画像が読み込まれるよ

observe に渡す LifecycleOwner は this じゃなくて viewLifecycleOwner にしてね
this は Fragment のライフサイクルだから、
View とライフサイクルが違うせいでバグの原因になりがちだよ。

これでエミュレータを起動するとさっきと変わらずに画像が表示されると思うよ

Repository の追加

Repository っていうのはデータのやり取りを簡単にするために作るものだよ
インターネット とかデータベースからのデータ取得するときは、
ViewModel から直接するんじゃなくて、Repository を通して取得するよ

Repository って名前付いてるから、
ViewModel みたいに Android 公式の親クラスとかありそうだけどなくて、普通のクラスだよ

とりあえず画像 Url を返してくれる Repository を作って
これを ViewModel で直接生成して使ってみるよ

これで最初 Fragment に直接書いてた
画像 Url を Repository に移せたね

MVVM はこんな感じでレイヤー状になってて
画像では黒の矢印が参照の方向で、
青の矢印がデータの流れを表してるよ

下のレイヤーが上のレイヤーを
参照してないことがポイントだね

MVVM で注意するところは、
それぞれのレイヤーにあったコードを書くことだよ
たとえばデータの取得やロジックなんかは Model の部分で書くよ

詳しくは、公式のアプリ アーキテクチャ ガイドとか調べてみてね

Api通信

ポケモンの画像の取得にはPokeApiっていう
非公式のポケモンApiを使うよ

このApiは例えばhttps://pokeapi.co/api/v2/pokemon/1のUrlにアクセスすると
図鑑で1番目のフシギダネの情報が取得できるよ!
最後の数字の部分が図鑑の番号になってて、
25にしてみるとピカチュウの情報が取得できるんだね!

それで、Apiから帰ってくる情報つまりレスポンスは
ポケモンについていろいろな情報が含まれてるんだけど、
今回のアプリでほしいのはポケモンの正面の画像Urlだけだよ

レスポンスはJsonっていう文字列でデータを表すものになってて、
ポケモンの正面の画像のUrlだけをみるとこんな感じ
front_defaultっていうのがポケモンの正面の画像のUrlだよ

{
  "sprite": {
    "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
  }
}

これをKotlinのDataClassで表すとこんな感じになるよ
front_defaultをKotlinらしくfrontDefaultって名前にしたいから
@Jsonをつけて名前を変えてるよ

PokemonResponseの中にPokemonSpritesがあってその中にfrontDefaultがあるのがわかるかな?
なんとなくイメージとしてはこんな感じだよ
Jsonと同じような階層になってるんだね

PokemonResponse: {
  PokemonSprites: {
    frontDefault: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
  }
}

今回はApiと通信するためにRetrofitというライブラリを使うよ
Retrofitを使うとApi通信がとても簡単にできるよ

まずは通信したいApi用のinterfaceを作るよ

アクセスしたいApiのUrlはhttps://pokeapi.co/api/v2/pokemon/1なんだけど、
https://pokeapi.co/api/v2/までがベースUrlとなってるよ
つまりpokemon/1の部分が変更できる箇所ってことだね

それで、今回はランダムなポケモンの画像が欲しいから、
変更したい部分としてはポケモンの図鑑番号、
つまりpokemon/1の1の部分を変れるようにしたいよ
これをRetrofit用に表すと、こんな感じになるよ

@GETでベースUrlから先のUrlを表してて、
Urlで変更したい部分は@GETのなかで{変数名}って書くと、
そこに関数の引数の@Path("変数名")の値が入ってくれるようになるよ

つまりgetPokemonDataだとidがUrlの中に入る感じになってるね

それでもう一つ注目しておきたい部分があって、
それは関数の前についてるsuspendだよ
suspendがついている関数のことをサスペンド関数っていうよ
サスペンド関数は簡単に言うと非同期処理をいい感じに使える関数だよ
その分呼び出し方が少し特殊になってるんだけど
あとでViewModelから呼び出すから今はあまり気にしなくてもいいよ

ApiClientの作成

Api通信するためのPokemonApiを作れたね
これを使うのにはPokemonApiをインスタンス化する必要があると思うんだけど、
それにはRetrofitを使うよ。

Retrofitはクライアントを生成する必要があるよ
Retrofitクライアントは毎回生成するんじゃなくて、アプリ内で使いまわすのが良いらしいよ
なのでApiClientってobjectを作ってその中でRetrofitクライアントを保持するよ

ApiのレスポンスのJsonをKotlinオブジェクトに変換するためには
Moshiっていうライブラリを使うよ
これをRetrofitに設定すればApiレスポンスをKotlinオブジェクトに変えてくれるよ

それで、ここまでがRetrofitのクライアントの準備で、
実際にPokemonApiを生成するのにはRetrofitのcreate関数を使うと生成できるよ
これでPokemonApiを使えるようになったよ!

実際にRepositoryでPokemonApiを使ってみよう!
ApiClientはobjectだからどこからでもアクセスできるようになってるから
PokemonRepositoryから直接参照してPokemonApiを使うよ!

PokemonRepository のgetRandomImageUrlもサスペンド関数になってることにも注目だよ!
これをTopViewModelで呼び出してみるね

基本的にサスペンド関数は、
サスペンド関数かCoroutineScopeっていうのからしか呼び出せないよ
ViewModelにはviewModelScopeっていう専用のCoroutineScopeがあるからこれを使うよ
これを使えばViewModelのライフサイクルを気にせずに非同期処理を使えるよ
サスペンド関数とかCoroutineScopeとかは
KotlinのCoroutineってやつだよ気になったら調べてみてね

これで、起動するとフシギダネの画像が表示されると思うよ

今のところ画像はフシギダネ固定になってるね
これをランダムにしたいんだけど、Apiはランダムなポケモンを返してくれるわけじゃないから、
アプリ側で図鑑番号をランダムに指定することにするね
KotlinではRandom.nextIntをつかうとランダムな整数が取得できるよ
引数を渡すと範囲指定できるから、わかりやすいように1~151までを指定するよ

これで起動すると初代のポケモンがランダムで表示されると思うよ

ボタンに何も設定してないからボタンを押しても何も反応しないね
ボタンを押したらポケモンの情報を新しく取得するようにするよ
といってもボタンを押したらviewModel.fetchを呼び出すだけだね

これでボタンを押すたびにランダムなポケモンの画像が表示されると思うよ

エラーハンドリング

Apiから画像のUrlを取得できたからアプリとしてはこれで十分ではあるんだけど、
いくつか細かい点を改善していくよ!

まずはApi通信にエラーハンドリングを追加するよ
今のままだと機内モードにした状態でアプリを起動するとクラッシュしちゃうよ
これはApiに接続できないからエラーしてて、
Kotlinではエラーを処理しないとクラッシュしちゃうんだね

簡単な方法としてはRepositoryでApi通信の部分をrunCatchingで囲うことだよ

runCatching内ではエラーが起きてもアプリはクラッシュしなくなるよ
その代わりrunCatchingResultっていうのに値を包んで返すよ
使ってみたほうがわかりやすいから使ってみよう

まずはPokemonRepositoryのgetRandomImageUrlでrunCatchingを使おう
関数全体を囲ってしまえばいい感じだよ

getRandomImageUrlが返す値がStringからResult<String>になっているから
ViewModelの方で修正が必要だね

Resultは成功したときにonSuccessに値が流れるよ
なのでonSuccessmutableImageUrlLiveDataを更新できればいいね
実際に使ってみるとこんな感じ

じゃあ次は失敗したときの処理を書くよ
まずerrorFlowをViewModelに宣言するよ
これはLiveDataみたいにFragmentに値を流すために使うよ

なんでLiveDataじゃなくてSharedFlowを使うのかというと、
LiveDataは値を入れた時とobserveした瞬間に値が流れるんだけど、
エラー処理だとこれはいらなくて、エラーした瞬間だけ値が流れればいいよ

SharedFlowはobserveしたときに値が流れなくて、
値を入れた時だけ値が流れるから、エラー処理にぴったりだよ

それで、Resultでエラーして失敗したときはonFailureにエラーが流れるよ
こっちもonFailureのときにmutableErrorFlowを更新できればいいね
SharedFlowを更新するのにはemitをつかうよ

FragmentのほうでerrorFlowをobserveするよ
といってもFlowはobserveじゃなくてonEachlaunchInを使うよ

onEachの中に値が流れてきたときの処理を書けば良くて、
今回はスナックバーでエラーしたよって表示することにするね

これで機内モードで起動したときに
アプリがクラッシュせずにスナックバーが表示されると思うよ

Resultの詳しい使い方はKotlinのドキュメントを見てね

Flowは、Kotlinのドキュメントとか
Android公式の解説とかあるけどちょっと難しいよ

FlowはCoroutineの仲間だから一緒に調べるといいよ
SharedFlowのほかにもStateFlowっていう
LiveDataと同じように値を保持できる便利なFlowもあるから
気になったら調べてみてね

Hilt の導入

Hilt っていうのは Android 用の DI ライブラリだよ
DI(Dependency Injection)は日本語では依存性注入っていうよ
日本語にしてもわかりにくいことで有名だよ

でもやってること自体は単純だよ
ちょっと例を出してみるね

class Item
class Container(private val item: Item)

val item = Item()
val container = Container(item)

こうやってよく、クラスに別のクラスのインスタンスを入れることってあるよね
簡単に言うとこれが DI だよ

じゃあ DI ライブラリは何をするのかというと
Item のインスタンス化と Container に入れるとこまでをいい感じにしてくれるよ
中身の仕組みは難しいんだけど使うのは簡単だから実際に Hilt を使ってみよう

まずは Application を継承したクラスを作るよ
つぎにそれを AndroidManifest に登録するよ
AndroidManifest の application に android:name というのをつけると登録できるよ

Hilt を使うには
アノテーションていう@から始まるタグみたいなやつをつけていくよ
Application には@HiltAndroidApp
MainActivity とか Fragment には@AndroidEntryPoint
ViewModel には@HiltViewModelをつけるよ

https://github.com/nemoramune/AboutAndroidMVVM/commit/a738bb1c73f85c7c8349fbcf058d9c882da8bc83

これで Hilt を使う準備ができたから
ViewModel に Repository を注入してみよう!

PokemonRepository のコンストラクターに@Injectをつけると
Hilt に PokemonRepository の作り方を教えることができるよ
今回は引数がなくても PokemonRepository ができるってことを Hilt に教えてるよ

同じように TopViewModel のコンストラクターに@Injectをつけて
TopViewModel の作り方を Hilt に教えるよ
TopViewModel は PokemonRepository が必要だからコンストラクターに入れようね
こうすると Hilt に TopViewModel を作るのには PokemonRepository が必要なんだって教えれるよ
Hilt はもう PokemonRepository の作り方を知ってるから
TopViewModel を作ってくれるってことだね

Fragment のほうは何もいじらなくても大丈夫だよ
Hilt と viewModels が連携していい感じにしてくれてるよ

起動してみるとアプリが問題なく動くと思うよ

DI をするとどういういいことがあるかというと
疎結合になるってことが大きいと思うよ

Hilt が Repository の作り方を知ってるから
ViewModel は Repository の作り方を知らなくても良くなったよ
こうなると Repository の作り方が変わったときに
Hilt のほうだけ変更すれば ViewModel を変更しなくてもよくなるよ

ほかにもいろいろメリットがあるから基本的に DI ライブラリを入れることが多いよ
気になったら調べてみてね

ApiClientの注入

もう一つHiltを試してみよう
今はPokemonRepositoryがApiClientを使ってるんだけど、
これも注入してみるよ

PokemonRepositoryのコードを見てみると、
必要なのはApiClientじゃなくてPokemonApiで十分だよ
だからPokemonApiの作り方をHiltに教えてあげればいいね

さらにApiClientのコードを見てみると
書いてあるのはPokemonApiの作り方だけだよ
だからこれを丸ごとHiltに教えてあげればいい感じだね

いろいろやり方はあると思うんだけど、
まず、ApiClientに@Module@InstallInっていうアノテーションをつけるよ

@Moduleっていうのは
このオブジェクトはインスタンスの作り方をHiltに教えるものっていうアノテーションだよ

@InstallInっていうのは
簡単に言うとそのオブジェクトを使う範囲を示しているよ
SingletonComponentだとobjectと同じ範囲つまり、アプリ全体で使用できるっていう意味だよ
他にもActivityで使うっていう意味のActivityComponent
Fragment用のFragmentComponent、ViewModel用のViewModelComponentとかがあるよ
詳しくはHiltの公式ドキュメントを読んでね

@Module@InstallInをつけるだけじゃだめで、
インスタンスの作り方の関数を作って、@Providesをつける必要があるよ
すでにインスタンスの作り方は書いてあるから、関数にして@Providesをつけるよ

関数にもう一つ@Singletonもつけるよ
これはこのインスタンスは使いまわすよっていうアノテーションだよ
これがないと呼び出されるたびにインスタンスを生成しちゃうよ
例えばRetrofitを使うたびにRetrofitを生成しちゃうってことだね
だから@Singletonをつけて一つのインスタンスを使いまわすようにするよ

細かいところなんだけど、
ApiClientはもうクライアントじゃなくてモジュールになってるから、
ApiModuleに名前を変えておくよ

これでPokemonApiの作り方をHiltに教えることができたよ
これをPokemonRepositoryのコンストラクターに入れて使ってみよう!

これでアプリを起動すると問題なく動くと思うよ!

HiltとかのDIライブラリは仕組みは難しいんだけど、
使うのは簡単になってることが多いから
複雑なDIをするようになったときに調べるのでも遅くないと思うよ

Hiltの公式ドキュメント
Android公式のHiltの記事

おわりに

「ネットからデータをとってきて、画面に反映する」流れについて説明したつもりだよ
大体のAndroidアプリはこんな雰囲気で作ってると思うよ

もちろん例外もあると思うよ、
最近はViewBindingじゃなくてJetpackComposeを使うのが流行ってたりもするね

まとめとしては
MVVMはレイヤー状になってて
下のレイヤーが上のレイヤーを参照してないことがポイントだよ

最初のうちは
コードをどこに書けばいいのかよくわからないと思うけど、
このコードはどこに書くんだろうって考えることが大切だと思うよ

ある程度慣れてきてやることがなくなったらするおすすめの項目を挙げておくね

  • テストの記述してみる
  • ViewBindingのメモリリークについて調べてみる
  • LiveDataの代わりにStateFlowを使ってみる
  • RecyclerViewをつかったリストを作ってみる
  • JetpackComposeを使ったレイアウトを作ってみる
  • Paging3を使って無限スクロールリストを作ってみる
  • Roomを使ってみるApiのレスポンスをキャッシュとか
  • UseCaseを使ってみる