APIとAndroidアプリを連携してAPI結果を表示する

## 1.表題
Javaで作成したAPIをAndroidアプリから呼び出して、APIの結果をAndroidアプリに表示させます。

## 2.動作環境
AndroidStudio、Kotlin
※APIは事前にEclipseでBootRun済とする。

## 3.使用するAPI
自作の料理IDに紐づくアレルギー情報API
<http://localhost:8080/api/RecipeAllergyInfo?recipeId=1>
```json
{
    "result": {
        "success": true,
        "info": [
            {
                "allergyId": 1
            },
            {
                "allergyId": 4
            },
            {
                "allergyId": 5
            }
        ]
    }
}
```
<http://localhost:8080/api/RecipeAllergyInfo?recipeId=2>
```json
{
    "result": {
        "success": true,
        "info": [
            {
                "allergyId": 2
            },
            {
                "allergyId": 3
            },
            {
                "allergyId": 6
            }
        ]
    }
}
```
## 4.仕様
（１）入力画面
１.初期表示時
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583781/2f85f208-1696-4942-9858-83b7c78bb1c9.png" width=40%>

2.レシピID=1で入力時
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583781/9779edc9-497f-4f2c-a311-9bd69d1bb25e.png" width=40%>
→ (3.使用するAPIの結果)で返ってきた結果がカンマ区切りで表示されている。

3.レシピID=2で入力時
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583781/b533990b-4ba3-44b9-8c7e-6cc2fc9bdd96.png" width=40%>
→ (3.使用するAPIの結果)で返ってきた結果がカンマ区切りで表示されている

4.登録されていないレシピIDで検索時
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583781/da030cdd-e1d3-47cf-9614-5fe23fe76129.png" width=40%>
→ 何も表示しない

## 5.コーディング
### (1) レイアウト（AndroidManifest.xml）
```xml:AndroidManifest.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp">

    <EditText
        android:id="@+id/idInput"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="アレルギーIDを入力"
        android:inputType="number" />

    <Button
        android:id="@+id/searchButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="検索" />

    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="[アレルギー検索結果]"
        android:textSize="18sp" />
</LinearLayout>
```

### (2) API設定
　戻りのクラス（AllergyResponse.kt）
 ```kotlin:AllergyResponse.kt
package com.example.myfirstapp.api

data class AllergyResponse(
    val result: Result
)

data class Result(
    val success: Boolean,
    val info: List<AllergyInfo>
)

data class AllergyInfo(
    val allergyId: Int
)
 ```

 　APIのサービス設定（ApiService.kt）
```kotlin:ApiService.kt
package com.example.myfirstapp.api

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface ApiService {
    @GET("api/RecipeAllergyInfo")
    fun getAllergyInfo(@Query("recipeId") recipeId: Int): Call<AllergyResponse>
}
 ```

 　Retrofitライブラリを使用してAPI通信を行う際に、Retrofitのインスタンスを生成し、そのインスタンスを管理するためのクラスやオブジェクト（RetrofitInstance.kt）
```kotlin:RetrofitInstance.kt
package com.example.myfirstapp.api

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl("http://10.0.2.2:8080/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

 ```

### (3) kotlin側
```kotlin:MainActivity.kt
package com.example.myfirstapp

import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import com.example.myfirstapp.api.RetrofitInstance
import com.example.myfirstapp.api.AllergyResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {

    private lateinit var resultTextView: TextView
    private lateinit var idInput: EditText
    private lateinit var searchButton: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        resultTextView = findViewById(R.id.resultTextView)
        idInput = findViewById(R.id.idInput)
        searchButton = findViewById(R.id.searchButton)

        searchButton.setOnClickListener {
            val inputText = idInput.text.toString()
            if (inputText.isNotEmpty()) {
                val id = inputText.toInt()
                fetchAllergyInfo(id)
            } else {
                Toast.makeText(this, "アレルギーIDを入力してください", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun fetchAllergyInfo(id: Int) {
        resultTextView.text = "読み込み中..."

        RetrofitInstance.api.getAllergyInfo(id).enqueue(object : Callback<AllergyResponse> {
            override fun onResponse(call: Call<AllergyResponse>, response: Response<AllergyResponse>) {
                if (response.isSuccessful) {
                    val allergyIds = response.body()?.result?.info?.joinToString(", ") { it.allergyId.toString() }
                    resultTextView.text = "Allergy IDs: $allergyIds"
                } else {
                    resultTextView.text = "エラー: ${response.code()}"
                }
            }

            override fun onFailure(call: Call<AllergyResponse>, t: Throwable) {
                resultTextView.text = "通信エラー: ${t.message}"
            }
        })
    }
}
```

### (4) 設定ファイル(gradle.ktsファイル）
```build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.myfirstapp"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.myfirstapp"
        minSdk = 21
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
}

dependencies {

    // Retrofit & Gson
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Coroutine（API呼び出しを非同期で行うため）
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}
```


## 6.終わりに
Andoroid StudioとAPIの連携の調査まで終わったので、今後作成予定の課題にそろそろ実装を着手していこうかなというところです。