Androidアプリで楽天レシピAPIを利用して人気レシピの情報を取得する方法

# 初めに
この記事では、Androidアプリ開発において楽天レシピAPIをたたいて人気レシピの情報を取得する方法について解説します。
なお、Android開発初学者である自分用にRetrofitを利用してインターネットに接続し、データを取得する過程についても詳しくまとめています。
既にデータ通信の方法についてご存じの方にとってはやや冗長な内容となっていますが、その点はご了承ください。
# 目次
- 今回参考にした資料
- 楽天レシピAPIの概要
- API通信の流れ
- Retrofitとは
- 依存関係
- permissonの追加
- 実装
    - データクラスの定義
    - APIクライアントクラスの定義
    - インタフェースの定義
    - ViewModelの定義
    - コンポーザブル関数の定義

- 実行結果
- まとめ
# 今回参考にした資料
今回はこちらのCodelabを参考にしました。
https://developer.android.com/codelabs/basic-android-kotlin-compose-getting-data-internet?hl=ja&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-5-pathway-1%3Fhl%3Dja%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-getting-data-internet#0)
# 楽天レシピAPIの概要
楽天レシピカテゴリ別ランキングAPI
https://webservice.rakuten.co.jp/documentation/recipe-category-ranking

このAPIはアプリケーションIDを指定することで楽天レシピのカテゴリ別のランキングをJSON形式で返すAPIです。
# API通信の大まかな流れ
まず、API通信で情報を取得し、アプリで利用できるようになるまでの流れを以下に示します。
1. クライアントでRetrofitライブラリを用いてHTTPリクエストを作成します
2. リクエストを受信したサーバーはそのリクエストに基づいた処理を行い、データをJSONなどの形式で返します
3. サーバーから返されたJSONデータをそのままの状態でアプリ内で使用することはできないため、Gsonというライブラリを用いてKotlinのオブジェクトに変換します
4. GsonによってKotlinオブジェクトに変換されたデータはアプリ内で直接操作できるようになります

なお、本記事ではAPIの通信に`Retrofit`を、JSONデータの変換に`Gson`を利用していますが、それぞれ他のライブラリなどで代用することも可能です。
# Retrofitとは
Retrofitは、Androidアプリ開発において使用されるHTTPクライアントライブラリで、特にAPIとの通信をシンプルかつ効率的に行うことができます。Retrofitの大きな特徴として**インタフェースを使ってAPIのリクエストを定義し、本来複雑なHTTPリクエストの処理をブラックボックスとすることができる**ということが挙げられます。
# 依存関係
まず、依存関係の宣言を行います。
なお、今回はRetrofitに加え、JSONデータの変換にGson、URLを用いた画像の表示にCoilというライブラリを使用するほか、ViewModelも使用するため、app/build.gradleに以下のような記述を追加します。
・build.gradle(app)
```gradle
dependencies{
    ...
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.11.0")

    //Gson Converter
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Gson
    implementation("com.google.code.gson:gson:2.10.1")

    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")

    // Coil
    implementation("io.coil-kt:coil-compose:2.4.0")
    ...
}
```
# permissonの追加
インターネットに接続するためには、そのためのpermissonをAndroidManifest.xmlに記述する必要があります。
・AndroidManifest.xml
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    ...
</manifest>
```
# 実装
### データクラスの定義
まず、APIが返したJSONデータをアプリ内で使用するためにデータクラスを定義します。今回はAPIが返すJSONデータのうち、アプリ内で使用するデータは以下の4つとなっています。
- `recipeTitle`(レシピタイトル)
- `foodImageUrl`(料理画像のURL)
- `recipeIndication`(調理時間目安)
- `recipeCost`(費用の目安)
これらをアプリ内で利用するためのデータクラスの定義は以下の通りです。
・Result.kt
```Kotlin
data class Result(
    @SerializedName("recipeTitle")
    val Title: String,
    @SerializedName("foodImageUrl")
    val foodimage:String,
    @SerializedName("recipeCost")
    val cost:String,
    @SerializedName("recipeIndication")
    val indication:String
)

data class ResultResponse(
    @SerializedName("result")
    val result: List<Result>
)
```
アプリ内では4つの変数を持ったデータクラス`Result`のインスタンスがリスト形式となった`ResultResponse`のインスタンスをJSONデータから取得することになります。
なお、JSONデータにおける変数名とアプリ内で使用する変数名が異なっている場合には上に示したように`SerializedName`アノテーションを使用する必要があります。例えば、今回はJSONデータ内のrecipeTitle,foodImageUrl,recipeCost,recipeIndicationをそれぞれアプリ内では異なる名前で使いたいので、`SerializedName`アノテーションを用いてJSONデータ内の変数名を宣言し、その下でアプリ内のデータクラスで用いる変数名を宣言します。

### APIClientクラスの定義
・RecipeApiService.kt
```Kotlin
class ApiClient {
    private companion object {
        private const val BASE_URL = "https://app.rakuten.co.jp/services/api/"
        private const val APPLICATION_ID = //ここにアプリケーションIDを記述
    }

    //Gsonインスタンスの生成
    private val gson = GsonBuilder()
        .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
        .create()

    //Retrofitインスタンスの生成
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build()

    //APIサービスのインスタンス化
    private val apiService : ApiService by lazy{
        retrofit.create(ApiService::class.java)
    }

    //結果を取得する関数を定義
    suspend fun fetchResults(): Response<ResultResponse> {
        return apiService.fetchResults(APPLICATION_ID)
    }
}
```
まず、companion object内でベースとなるURLとアプリケーションIDを変数に格納しておきます。
次にGsonのインスタンスを作成します。
Gsonのインスタンスは`GsonBuiilder().create()`で作成することができます。
`.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)`でJSONフィールドとKotlinフィールドの名前をそのままにすることを設定しています。

その次にRetrofitのインスタンスを作成します。
Retrofitのインスタンスは
`Retrofit.Builder()`で作成を開始し、
`baseUrl()`でベースURLの指定、
`addConverterFactory()`でGsonのインスタンスを引数とし、JSONデータを自動的にオブジェクトに変換するGsonを設定、
`build()`でインスタンスの作成を完了しています。

さらに結果を取得する`fetchResults()`メソッドを定義します。
fetchResult()メソッドでは、ApiServiceのfetchResults()メソッドを呼び出し、APIにリクエストを送ることでレシピ情報のリストを取得しています。
このメソッドはsuspendであるため、非同期処理として実行されます。
### インタフェースの定義
```Kotlin
interface ApiService {
    @GET("Recipe/CategoryRanking/20170426")
    suspend fun fetchResults(
        @Query("applicationId") applicationId: String,
        @Query("categoryId") categoryId: Int = 30,
    ): Response<ResultResponse>
}
```
先程インスタンスを作成したApiServiceのインタフェースを定義し、その中でfetchResultsメソッドを定義しています。

`GETアノテーション`はHTTPのGETリクエストを指定しており、BASE_URL以降に続くAPIエンドポイントへのパスとなっています。

`Queryアノテーション`はクエリパラメータを指定するためのものであり、ここでは`applicationId`(必須)と`categoryId`をそれぞれ指定しています。なお、categoryIdについては楽天レシピカテゴリ一覧APIという別のAPIで取得することができ、今回情報を取得したい「人気メニュー」カテゴリのcategoryIdは30であることがあらかじめ分かっているため、ここで直接指定しています。

### ViewModelの定義
・RecipeViewModel.kt
```Kotlin
sealed interface RecipeUiState {
    data class Success(var recipes: List<Result>) : RecipeUiState
    object Error : RecipeUiState
    object Loading : RecipeUiState
}

class RecipeViewModel: ViewModel() {
    var recipeUiState: RecipeUiState by mutableStateOf(RecipeUiState.Loading)
        private set

    init {
        getRecipeList()
    }
    
    fun getRecipeList(){
        viewModelScope.launch {
            recipeUiState = RecipeUiState.Loading
            recipeUiState = try {
                val listResult = ApiClient().fetchResults()
                if (listResult.isSuccessful) { 
                    val recipeResponse = listResult.body()
                    if (recipeResponse != null ) {
                        RecipeUiState.Success(recipeResponse.result)
                    } else {
                        RecipeUiState.Error
                    }
                } else {
                    RecipeUiState.Error
                }
            } catch (e: IOException) {
                RecipeUiState.Error 
            } catch (e: HttpException) {
                RecipeUiState.Error
            }
        }
    }
}
```
まず、取得したレシピのUI状態である`RecipeUiState`を定義しています。
これは3つの状態をもっており、
RecipeViewModelクラスはUI状態を管理するためのクラスです。
`getLecipeList()`メソッドでは、`viewModelScope`を用いてコルーチンを開始し、非同期で以下に続く処理を実行します。
まずはRecipeUiStateをLoading(ロード中)にした状態でApiClientクラスのfetchResult()メソッドを用いてAPIリクエストを実行し、データの取得を試みます。
データの取得に成功した場合(isSuccessfulの場合)はその取得したデータがnullでないことを確認し、取得したrecipeResponseのデータによりRecipeUiStateをSuccess状態に更新します。
なお、もしデータを適切に取得できなかった場合にはRecipeUiStateの状態がErrorに設定されます。

### コンポーザブル関数の定義
・HomeScreen.kt
```Kotlin
@Composable
fun HomeScreen(
    recipeUiState: RecipeUiState,
    modifier: Modifier = Modifier
){
    when (recipeUiState) {
        is RecipeUiState.Success -> {
            RecipeList(recipes = recipeUiState.recipes, modifier = modifier)
        }
        is RecipeUiState.Error -> {
            Text(text = "Error loading recipe")
        }
        is RecipeUiState.Loading -> {
            Text(text = "Loading...")
        }
    }
}

@Composable
fun RecipeList(
    recipes:List<Result>,
    modifier: Modifier = Modifier
){
        LazyColumn(
            modifier = modifier
        ) {
            items(recipes) { recipe ->
                RecipeCard(result = recipe)
            }
        }
}


@Composable
fun RecipeCard(
    result: Result,
    modifier: Modifier = Modifier
){
    Card(
        colors = CardDefaults.cardColors(),
        modifier = modifier.fillMaxWidth().padding(16.dp)
    ) {
        Column {
            Text(
                text = result.Title,
                modifier = Modifier.padding(16.dp),
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold
            )
            Row {
                AsyncImage(
                    model = result.foodimage,
                    contentDescription = null,
                    modifier = Modifier
                        .width(160.dp)
                        .height(120.dp)
                )
                Column {
                    Text(
                        text="コスト: ${result.cost}",
                        modifier = Modifier.padding(12.dp),
                        fontSize = 16.sp
                    )
                    Text(
                        text="所要時間: ${result.indication}",
                        modifier = Modifier.padding(12.dp),
                        fontSize = 16.sp
                    )
                }
            }
        }
    }
}
```
`HomeScreen()`メソッドではRecipeUiStateを引数とし、その状態に応じてUIを変えます。
RecipeUiState.Successの場合はRecipeListメソッドを呼び出し、それ以外の状態では対応するテキストを表示します。

`RecipeList()`メソッドは、レシピのリストをスクロール可能なリストとして表示します。Resultのリストであるrecipesから要素を1つづつ取り出し、個々のレシピのUIであるRecipeCardを表示しています。

`RecipeCard()`メソッドは、個々のレシピ情報をカード形式で表示します。

・MainActivity.kt
```Kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            RakutenSampleTheme {
                Surface(
                    modifier=Modifier.fillMaxSize(),
                    color= MaterialTheme.colorScheme.background
                ) {
                    val recipeViewModel: RecipeViewModel = viewModel()
                    HomeScreen(
                        recipeUiState = recipeViewModel.recipeUiState,
                        modifier = Modifier.fillMaxSize()
                    )
                }
            }
        }
    }
}
```
MainActivityでは、viewmodelのインスタンスを生成し、HomeScreen関数を呼び出しています。
# 実行結果
実行結果は以下のようになります。
![スクリーンショット 2024-10-02 123831.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796037/5502a44d-40fe-7faf-a2a6-f0bada9b8709.png)
人気レシピの情報を取得し、リスト形式で画面上に表示できていることがわかります。

# まとめ
この記事では、楽天レシピAPIを利用してAndroidアプリ内で人気レシピの情報を取得する方法を解説しました。HTTPリクエストを通じたデータ取得、そしてそのデータをアプリ内で表示する流れを具体的に見てきました。
この記事を参考に、実際のアプリ開発に活用してみてください。