OpenAPIを使って、Androidアプリ(Kotlin)を作成する。
「OpenAPIを使ってみる(1) Pythonでテストサーバーを立ち上げる」の続き。
- 環境
- macOS Monterey
- openJDK 11(「brew install openjdk@11」でインストール)
- openapi-generator-cli 6.0.1(「brew install openapi-generator」でインストール)
前回に作成したOpenAPIのAPIドキュメントとテストサーバーを使って、Androidアプリを作成する。
openapi_sampleフォルダーの下にAndroidアプリを作成する。
openapi_sample/
openapi/
openapi.yml
android_app/
OpenAPI Generatorでソースコードを生成する
openapiフォルダーで以下のコマンドを実行し、Androidのソースコードを生成する。
使用できるオプションを確認する。
openapi-generator config-help -g kotlin
作成するアプリケーショーンが対応するAndroidのバージョンを考慮して、使用するライブラリを選定する。
Retrofitを使用する。
--additional-properties=library=jvm-retrofit2
ソースコードの生成されるフォルダーをsrc/main/kotlinでなく、src/main/javaにする。
--additional-properties=src/main/java
kotlinフォルダーに出力する。
-o kotlin
openapiフォルダーで上記のオプションを指定して、ソースコードを生成する。
openapi-generator generate -i openapi.yml -g kotlin -o kotlin --additional-properties=jvm-retrofit2,sourceFolder=src/main/java
kotlin/settings.gradleを編集して、パッケージ名を設定する。
rootProject.name = 'openapi-client'
パッケージを作成する。
cd kotlin
chmod +x gradlew
./gradlew check assemble
以下の場所にファイルが生成される。
kotlin/build/libs/openapi-client-1.0.0.jar
このファイルをAndroidプロジェクトにコピーする。
cp build/libs/openapi-client-1.0.0.jar ../../android_app/app/libs
Android Studioで/libs/openapi-client-1.0.0.jarを右クリックして、「Add As Library…」を選択する。
build.gradle(:app)に追加されるので、「Sync Project with Gradle Files」を実行する。
APIを叩く
ApiClientのインスタンスを生成する。
クラス名はopenapi.ymlのtagsで指定した名前+APIになる。
引数でAPIのベースのURLを指定する。
val usersApi = UsersApi("http://192.168.10.109:8000/v1")
UsersApiのメソッドを呼ぶことでAPIを叩ける。
// ユーザー一覧を取得する
val users = usersApi.listUsers()
// ユーザーを取得する
val user = usersApi.getUserById(id ?: 0)
HTTPアクセスを行うために、AndroidManifest.xmlを編集する。
<manifest
...>
<uses-permission android:name="android.permission.INTERNET"/> //追加
<application
...
android:usesCleartextTraffic="true" //追加
>
サンプルプログラム
「Load Users」ボタンを押すとユーザー一覧を取得し、ユーザーをタップすると詳細を表示する。
package jp.gesource.example.android_app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import jp.gesource.example.android_app.ui.theme.Android_appTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.openapitools.client.apis.UsersApi
import org.openapitools.client.models.SimpleUser
import org.openapitools.client.models.User
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MainScreenViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_appTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainScreen(viewModel)
}
}
}
}
}
class MainScreenViewModel : ViewModel() {
private var _loading = MutableStateFlow(false)
val loading: StateFlow<Boolean> = _loading
private var _users: MutableStateFlow<List<SimpleUser>> = MutableStateFlow(emptyList())
val users: StateFlow<List<SimpleUser>> = _users
private var _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user
private val usersApi = UsersApi("http://192.168.10.109:8080/v1")
fun listUsers() {
_loading.value = true
viewModelScope.launch(Dispatchers.IO) {
try {
delay(1000L)
val users = usersApi.listUsers()
_users.value = users
} catch (e: Exception) {
println(e.message)
_users.value = emptyList()
}
_loading.value = false
}
}
fun getUserById(id: Int?) {
_loading.value = true
viewModelScope.launch(Dispatchers.IO) {
try {
delay(1000L)
val user = usersApi.getUserById(id ?: 0)
_user.value = user
} catch (e: Exception) {
_user.value = null
}
_loading.value = false
}
}
fun resetUser() {
_user.value = null
}
}
@Composable
fun MainScreen(viewModel: MainScreenViewModel = MainScreenViewModel()) {
val loading = viewModel.loading.collectAsState()
val users = viewModel.users.collectAsState()
val user = viewModel.user.collectAsState()
val listState = rememberLazyListState()
val selectedIndex by remember { mutableStateOf(-1) }
if (loading.value) {
Box(modifier = Modifier.fillMaxSize()) {
Text(text = "Loading", modifier = Modifier.align(alignment = Alignment.Center))
}
} else {
if (user.value == null) {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { viewModel.listUsers() }) {
Text(text = "Load Users")
}
LazyColumn(state = listState) {
items(users.value) { user: SimpleUser ->
Text(
text = "${user.id}: ${user.name}",
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = user.id == selectedIndex,
onClick = { viewModel.getUserById(user.id) }
)
)
}
}
}
} else {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { viewModel.resetUser() }) {
Text(text = "Close")
}
Text(text = "id : ${user.value?.id}")
Text(text = "name : ${user.value?.name}")
Text(text = "birthday : ${user.value?.birthday}")
}
}
}
}