OpenAPIを使ってみる(2) Androidアプリ(Kotlin)を作成する

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}")
            }
        }
    }
}

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください