【API通信編】 2022年だしそろそろAndroidアプリ開発を始めてみる (3/4)

この記事はPart3です。Part2(UI実装編)は[こちら](https://qiita.com/tkhskt/items/f354a7da6c6270107f62)をご覧ください。
Part3では、Web APIからデータを取得してアプリに反映させます。楽しくなってきた。

## Web API通信実装の準備

Web APIからデータを取得するため、いくつかのライブラリを導入します。
`app/build.gradle`にライブラリの設定を追加します。

```diff_gradle:build.gradle
...

dependencies {
    ....
    
+    // REST APIを使うためのライブラリ
+    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+    
+    // JSONをKotlinのオブジェクトに変換するためのライブラリ
+    implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
+    implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
+
+    // ViewModelやActivityのKotlin拡張ライブラリ
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
+    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
+    implementation "androidx.activity:activity-ktx:1.4.0"
}
```

## データ取得処理の実装

Web API通信関連の処理は`app/src/main/java/com/example/wikiclient/data`ディレクトリ(パッケージ)に実装します。
ここで作成するクラス/インターフェースは、下記になります。

- APIレスポンスの内容を保存するクラス
- ApiClient
- Repository

### APIレスポンスの内容を保存するクラス

まずは、`app/src/main/java/com/example/wikiclient/data/model`に`GetArticlesResponse`を作成しましょう。

```kotlin:GetArticlesResponse.kt
data class GetArticlesResponse(
    val query: Query,
) {
    data class Query(
        val pages: List<Page>,
    ) {
        data class Page(
            val index: Int,
            val ns: Int,
            val pageid: Int,
            val terms: Terms?,
            val thumbnail: Thumbnail?,
            val title: String,
        ) {
            data class Terms(
                val description: List<String>,
            )

            data class Thumbnail(
                val height: Int,
                val source: String,
                val width: Int,
            )
        }
    }
}
```

Web APIから取得したデータはこのクラスに格納されます。

### ApiClient

次に、`app/src/main/java/com/example/wikiclient/data/remote`に`WikiApiClient`を作成しましょう。
これはretrofitを使ったWeb APIへのリクエストをする際に必要なインターフェースです。

```kotlin:WikiApiClient.kt
import com.example.wikiclient.data.model.GetArticlesResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface WikiApiClient {

    @GET("w/api.php?action=query&format=json&formatversion=2&generator=prefixsearch&prop=pageimages%7Cpageterms&piprop=thumbnail&pithumbsize=50&pilimit=10&wbptterms=description")
    suspend fun getArticleList(
        @Query("gpssearch") gpssearch: String, // キーワード
        @Query("gpslimit") gpslimit: Int, // 一回のリクエストで取得する最大件数
    ): Response<GetArticlesResponse>
}
```

このインターフェースでは、リクエストを行うAPIのエンドポイントと、そこに渡すクエリパラメータの設定を記述したメソッドを定義ます。

### Repository

最後に、`app/src/main/java/com/example/wikiclient/data`に`WikiRepository`を作成します。

このクラスを作成しなくてもWeb APIからのデータ取得はできるのですが、`Repository`クラスを作成することはGoogleが推奨しているため今回はそれに従います。

```kotlin:WikiRepository.kt
import com.example.wikiclient.data.model.GetArticlesResponse
import com.example.wikiclient.data.remote.WikiApiClient
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

interface WikiRepository {
    suspend fun getArticles(keyword: String, limit: Int): GetArticlesResponse?
}

class WikiRepositoryImpl : WikiRepository {

    // JSONからKotlinのクラスに変換するためのライブラリの設定
    private val moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

    // REST APIを利用するためのライブラリの設定
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://ja.wikipedia.org/") // 今回利用するWeb APIのURL
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()

    // WikiApiClientに定義したメソッドを呼び出すための設定
    private val wikiService = retrofit.create(WikiApiClient::class.java)

    // Web APIからデータを取得するメソッド
    override suspend fun getArticles(keyword: String, limit: Int): GetArticlesResponse? {
        return wikiService.getArticleList(keyword, limit).body()
    }
}
```

このクラスでは、Web APIにリクエストを行うためのライブラリの初期化と、実際にWeb APIからデータを取得するメソッドを定義しています。

> retrofitやmoshiのインスタンス生成について
> 今回の実装では、Repository中でmoshiやretrofitなどのライブラリのインスタンスを作成していますが、これは本当はあまり良くないです。(主にパフォーマンス面で)
> 一般的にはretrofitのインスタンスはシングルトンで生成し、Repositoryのコンストラクタに渡して利用することが多いです。

> Repositoryクラスを作成する理由
> ここで説明すると長くなりそうなので興味のある方は下記のリンク先をご参照ください。
> https://developer.android.com/jetpack/guide/data-layer?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-architecture%23article-https%3A%2F%2Fdeveloper.android.com%2Fjetpack%2Fguide%2Fdata-layer

## UIへのデータ反映の実装

ここまでの実装で、Web APIからのデータ取得ができるようになりました。
ここからは、実際にWeb APIからデータを取得して、データをUIに反映させましょう。

### MainViewModel

`app/src/main/java/com/example/wikiclient/ui`に`MainViewModel`を作成します。

最近のAndroid開発では、`ViewModel`というクラスを作成することが多いです。
Androidアプリは、画面回転などを行うと、Activityなどで保持している変数などが破棄されてしまうため、なんとか保持する必要があります。
`ViewModel`はそういったデータを保持するために作成するクラスです。

> ViewModelはAndroidアプリのデータ保持以外にも、アーキテクチャ上の様々な役割があります。

```kotlin:MainViewModel.kt
class MainViewModel(
    private val repository: WikiRepository = WikiRepositoryImpl()
) : ViewModel() {

    // 取得したデータを別クラスに通知するためのクラス
    private val _articles = MutableStateFlow<List<Article>>(emptyList())
    val articles: StateFlow<List<Article>> = _articles

    // Repositoryからデータ取得のメソッドを呼び出す
    fun fetchArticles(keyword: String, limit: Int) {
        viewModelScope.launch(Dispatchers.IO) { 
            runCatching {
                repository.getArticles(keyword, limit)
            }.onSuccess { response ->
                // データ取得成功時
                if (response != null) postArticles(response)
            }.onFailure {
                // データ取得失敗時
                Log.e("Fetch Failure", it.toString())
            }
        }
    }

    // 取得したデータをMainActivityに通知する
    private fun postArticles(response: GetArticlesResponse) {
        _articles.value = response.query.pages.map {
            Article(
                id = it.pageid,
                title = it.title,
                thumbnailUrl = it.thumbnail?.source ?: "",
            )
        }
    }
}
```

`MainViewModel`のコンストラクタに、先ほど作成した`WikiRepository`を渡します。
 L10の`fetchArticles`メソッドでは、`WikiRepository`のメソッドを呼び出し、Web APIからデータを取得します。
 `viewModelScope.launch(Dispatchers.IO)`は非同期処理のための記述です。
 
 > ネットワーク通信は、UIの描画などと異なるスレッドで行います。
 
 取得したデータは、`postArticles`メソッドで`MainActivity`に通知します。

> L5,6で使用しているStateFlowとは、あるクラスから別のクラスにデータを通知するためのクラスです。
> MutableStateFlowは値を変更可能で、StateFlowは値の変更ができません。
> 一般的なAndroid開発では、L5, 6のように、内部で値を更新するためのMutableStateFlowと、別クラスに公開する読み取り専用のStateFlowをそれぞれ定義することが多いです。
> https://developer.android.com/kotlin/flow?hl=ja
> https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja

## MainActivity


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

+    // ViewModelを作成
+    private val viewModel: MainViewModel by viewModels()

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        initArticleList()
-       updateArticleList()
+       observeArticles()
        
+        // Web APIからのデータ取得開始("Android"というキーワードで20件取得)
+        viewModel.fetchArticles("Android", 20)
    }

    ...

+    private fun observeArticles() {
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.articles.collect { articles ->
+                    // Web APIからのデータ取得に成功すると、ここに流れてくる
+                    updateArticleList(articles)
+                }
+            }
+        }
+    }
    
+    // データをUIに反映
+     private fun updateArticleList(articles: List<Article>) {
+        articleListAdapter.submitList(articles)
+    }
-    // RecycleViewにデータを反映
-    private fun updateArticleList() {
-        // リストに表示するデータをリスト形式で作成
-        val articles: List<Article> = (0..10).map { number ->
-            Article(number, "記事$number", "https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/155135/0c2db45b0bd4b1aa023f5a7da835b76c2d191bd4/x_large.png?1585895165")
}
```

`MainActivity`では、先ほど作成した`MainViewModel`のインスタンスを作成し、データの取得を開始します。
`observeArticles`メソッドを作成し、データの取得が完了した際にViewModelからデータを受け取るための処理を記述します。

## 動かしてみる

ここまでできたら、例の再生ボタンを押してアプリを起動します。

![Screenshot_20220330_164136.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155135/0b27f1e6-4ea9-9538-6ec9-8535c47bd134.png)

画像がない記事も結構ありますが...これでデータ反映ができました。よしっ
[次のパート](https://qiita.com/tkhskt/items/67a5904f11fdba91a38c)ではテキストの入力とボタンタップ時の処理を、ここまでのおさらいも兼ねてやってみます。次回、感動の最終回。