この記事はAndroid チュートリアルが終わったくらいの人が対象だよ
まだしてなかったらするのをお勧めするよ!
Android は基本的に公式推奨の作り方をカスタムしたもので作ってて、
この記事ではアプリで重要な部分の、
「ネットからデータをとってきて、画面に反映する」ところを大まかに説明するよ
そのためにポケモンの画像を表示するアプリを作るよ
完成図はこんな感じ
上にポケモンの画像があって、下にボタンがあるよ。
ボタンを押すと、
ポケモンの画像がランダムに切り替わるようにするよ
ざっくりとした内容はこんな感じ
言語 | Kotlin |
アーキテクチャ | MVVM + Repository |
View | ViewBinding |
非同期 | Coroutine |
DI | Hilt |
HTTPクライアント | Retrofit2 |
Jsonライブラリ | Moshi |
イメージローダー | Glide |
完成品はGithubにあげてるよ
プロジェクトの作成
じゃあさっそくプロジェクトを作るよ
AndroidStudio を開いたら
EmptyActivity でプロジェクトを作るよ
プロジェクトの名前は適当でいいよ。
今回は「AboutMvvm」にするね
まずはプロジェクトで使うライブラリの設定をするよ
gradle を開いてこのコミットを参考にして変更してね
変更したら AndroidStudio の上のほうに Sync っていうのが出るから押してね
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
内ではエラーが起きてもアプリはクラッシュしなくなるよ
その代わりrunCatching
はResult
っていうのに値を包んで返すよ
使ってみたほうがわかりやすいから使ってみよう
まずはPokemonRepositoryのgetRandomImageUrlでrunCatching
を使おう
関数全体を囲ってしまえばいい感じだよ
getRandomImageUrlが返す値がString
からResult<String>
になっているから
ViewModelの方で修正が必要だね
Result
は成功したときにonSuccess
に値が流れるよ
なのでonSuccess
でmutableImageUrlLiveData
を更新できればいいね
実際に使ってみるとこんな感じ
じゃあ次は失敗したときの処理を書くよ
まずerrorFlow
をViewModelに宣言するよ
これはLiveData
みたいにFragmentに値を流すために使うよ
なんでLiveData
じゃなくてSharedFlow
を使うのかというと、
LiveData
は値を入れた時とobserveした瞬間に値が流れるんだけど、
エラー処理だとこれはいらなくて、エラーした瞬間だけ値が流れればいいよ
SharedFlow
はobserveしたときに値が流れなくて、
値を入れた時だけ値が流れるから、エラー処理にぴったりだよ
それで、Result
でエラーして失敗したときはonFailure
にエラーが流れるよ
こっちもonFailure
のときにmutableErrorFlow
を更新できればいいね
SharedFlow
を更新するのにはemitをつかうよ
FragmentのほうでerrorFlow
をobserveするよ
といってもFlow
はobserveじゃなくてonEach
とlaunchIn
を使うよ
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
をつけるよ
これで 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を使ってみる