$30 off During Our Annual Pro Sale. View Details »

Compose で Android/iOS アプリを作る

m.coder
September 15, 2023

Compose で Android/iOS アプリを作る

DroidKaigi2023 Day.3 15:00-15:40 の発表スライドです。

m.coder

September 15, 2023
Tweet

More Decks by m.coder

Other Decks in Programming

Transcript

  1. Copyright 2023 m.coder All Rights Reserved. 2 自己紹介 m.coder (

    @_m_coder ) フラー株式会社所属 Androidテックリード DroidKaigi 2021, 2022 に続き3回目の登壇ですが、 オフライン登壇は初めてです 今年は DroidKaigi ボランティアスタッフにも 参加しています
  2. Copyright 2023 m.coder All Rights Reserved. 7 Compose を使って Android

    と iOS 両方のアプリを作ってみよう!
  3. Copyright 2023 m.coder All Rights Reserved. 8 Compose for iOS

    とは? • Compose Multiplatform の中のフレームワークの一つ • Compose を使って iOS の UI を構築できる
  4. Copyright 2023 m.coder All Rights Reserved. 10 Compose Multiplatform Compose

    で様々なプラットフォームの UIを構築しようという試み https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/
  5. Copyright 2023 m.coder All Rights Reserved. 11 Compose Multiplatform Compose

    で様々なプラットフォームの UIを構築しようという試み https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/
  6. Copyright 2023 m.coder All Rights Reserved. 14 KMP とは? •

    Kotlin Multiplatform の略称 • Kotlin で様々なプラットフォームのロジックを共通化しよう という取り組み • 【余談】KMM (Kotlin Multiplatform Mobile)という呼び方 もあったが、公式がKMPを推奨し始めた
  7. Copyright 2023 m.coder All Rights Reserved. 17 Compose Multiplatform Compose

    で様々なプラットフォームの UIを構築しようという試み https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/
  8. 01 | Compose Multiplatform のセットアップ 02 | サンプルアプリをいじってみる 03 |

    API通信するアプリを構築してみる アジェンダ Copyright 2023 m.coder All Rights Reserved. アジェンダ 20
  9. Copyright 2023 m.coder All Rights Reserved. 21 話すこと・話さないこと • 話すこと

    ◦ Compose を使った Android/iOS のアプリ作成の始め方 ◦ Compose Multiplatform + KMP を使った簡単なアプリ作り ◦ Compose for iOS でハマった部分 • 話さないこと ◦ 細かいアーキテクチャや状態管理の設計などの話 ◦ Compose for Desktop / Compose for Web の話
  10. Copyright 2023 m.coder All Rights Reserved. 22 注意事項 Compose for

    iOS is in Alpha. →破壊的な変更が入る可能性が 大いにあります
  11. Copyright 2023 m.coder All Rights Reserved. 24 Compose Multiplatform Wizard

    というサイトがあります https://terrakok.github.io/Compose-Multiplatform-Wizard/
  12. Copyright 2023 m.coder All Rights Reserved. % brew install kdoctor

    28 環境構築 Homebrew を使って kdoctor を入れる
  13. Copyright 2023 m.coder All Rights Reserved. % brew install kdoctor

    % kdoctor 29 環境構築 Homebrew を使って kdoctor を入れる ターミナルで kdoctor を実行する
  14. Copyright 2023 m.coder All Rights Reserved. Environment diagnose (to see

    all details, use -v option): [✓] Operation System [✓] Java [✓] Android Studio [✓] Xcode [✓] Cocoapods Conclusion: ✓ Your system is ready for Kotlin Multiplatform Mobile development! 30 環境構築 環境が整っていれば全てに ✅がつく
  15. Copyright 2023 m.coder All Rights Reserved. Environment diagnose (to see

    all details, use -v option): [✓] Operation System [✓] Java [✓] Android Studio [✓] Xcode [✓] Cocoapods Conclusion: ✓ Your system is ready for Kotlin Multiplatform Mobile development! 31 環境構築 環境が整っていれば全てに ✅がつく • 比較的新しい MacOS マシン ◦ Venturaならいける • JDK ◦ 11以上ならおそらくOK • Android Studio ◦ 最新の安定版 ◦ Kotlin Multiplatform Mobile プラグイ ンのインストール • Xcode ◦ 最新の安定版ならおそらく OK • CocoaPods ◦ 最新ならおそらくOK
  16. Copyright 2023 m.coder All Rights Reserved. Environment diagnose (to see

    all details, use -v option): [✓] Operation System [✓] Java [✓] Android Studio [✓] Xcode [✓] Cocoapods Conclusion: ✓ Your system is ready for Kotlin Multiplatform Mobile development! 32 環境構築 環境が整っていれば全てに ✅がつく • 比較的新しい MacOS マシン ◦ Venturaならいける • JDK ◦ 11以上ならおそらくOK • Android Studio ◦ 最新の安定版 ◦ Kotlin Multiplatform Mobile プラグイ ンのインストール • Xcode ◦ 最新の安定版ならおそらく OK • CocoaPods ◦ 最新ならおそらくOK
  17. Copyright 2023 m.coder All Rights Reserved. 33 環境構築 • Kotlin

    Multiplatform Mobile プラグインのインストール ◦ Android Studio の Settings > Plugins > Marketplace からサーチバーに `Kotlin Multiplatform Mobile` と入力して検索 • Xcodeのインストール ◦ https://developer.apple.com/download/applications/ からダウンロード ◦ 14.3.1が安定版 (2023/08/16時点) • CocoaPodsのインストール ◦ Rubyをインストール ◦ ターミナルで `sudo gem install cocoapods` を入力しセットアップ
  18. Copyright 2023 m.coder All Rights Reserved. 38 サンプルアプリをいじってみる ・shared.commonMain …共通コード

    ・shared.androidMain …Android用の Kotlin コー ド ・shared.iosMain …iOS用の Kotlin コード
  19. Copyright 2023 m.coder All Rights Reserved. @OptIn(ExperimentalResourceApi::class) @Composable fun App()

    { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { Image( painterResource("compose-multiplatform.xml"), null ) } } } } expect fun getPlatformName(): String 39 サンプルアプリをいじってみる App.kt の実装コード
  20. Copyright 2023 m.coder All Rights Reserved. @OptIn(ExperimentalResourceApi::class) @Composable fun App()

    { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { Image( painterResource("compose-multiplatform.xml"), null ) } } } } expect fun getPlatformName(): String 40 サンプルアプリをいじってみる App.kt の実装コード
  21. Copyright 2023 m.coder All Rights Reserved. @OptIn(ExperimentalResourceApi::class) @Composable fun App()

    { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { Image( painterResource("compose-multiplatform.xml"), null ) } } } } expect fun getPlatformName(): String 41 サンプルアプリをいじってみる App.kt の実装コード
  22. Copyright 2023 m.coder All Rights Reserved. ---- App.kt expect fun

    getPlatformName(): String ---- main.android.kt actual fun getPlatformName(): String = "Android" ---- main.ios.kt actual fun getPlatformName(): String = "iOS" 42 サンプルアプリをいじってみる expect fun…複数プラットフォームで共通で使用す る関数の宣言 actual fun …各プラットフォームでの実際の関数の 動作
  23. Copyright 2023 m.coder All Rights Reserved. ---- App.kt expect fun

    getPlatformName(): String ---- main.android.kt actual fun getPlatformName(): String = "Android" ---- main.ios.kt actual fun getPlatformName(): String = "iOS" 43 サンプルアプリをいじってみる expect fun…複数プラットフォームで共通で使用す る関数の宣言 actual fun …各プラットフォームでの実際の関数の 動作
  24. Copyright 2023 m.coder All Rights Reserved. 44 サンプルアプリをいじってみる expect fun

    と actual fun を使って Android と iOS で 違う画像を表示してみよう! android.png ios.png
  25. Copyright 2023 m.coder All Rights Reserved. ---- App.kt expect fun

    getImageResource(): String ---- main.android.k t actual fun getImageResource(): String = "android.png" ---- main.ios.kt actual fun getImageResource(): String = "ios.png" 45 サンプルアプリをいじってみる expect fun getImageResource() を宣言し、actual fun に実装を書く
  26. Copyright 2023 m.coder All Rights Reserved. @OptIn(ExperimentalResourceApi::class) @Composable fun App()

    { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { Image( painterResource(getImageResource()), null ) } } } } expect fun getPlatformName(): String expect fun getImageResource(): String 46 サンプルアプリをいじってみる Image に指定しているファイルを getImageResource() に差し替える
  27. Copyright 2023 m.coder All Rights Reserved. buildscript { repositories {

    gradlePluginPortal() } dependencies { classpath("dev.icerock.moko:resources-generator:0.23.0") } } 53 moko-resources を導入する プロジェクトルートの build.gradle.kts に moko-resources を追加する
  28. Copyright 2023 m.coder All Rights Reserved. plugins { … id("dev.icerock.mobile.multiplatform-resources")

    } kotlin { … sourceSets { val commonMain by getting { dependencies { api("dev.icerock.moko:resources:0.23.0") api("dev.icerock.moko:resources-compose:0.23.0") } } multiplatformResources { multiplatformResourcesPackage = "com.myapplication.common" } 54 moko-resources を導入する shared 内の build.gradle.kts に moko-resources の依存を追加する
  29. Copyright 2023 m.coder All Rights Reserved. plugins { … id("dev.icerock.mobile.multiplatform-resources")

    } kotlin { … sourceSets { val commonMain by getting { dependencies { api("dev.icerock.moko:resources:0.23.0") api("dev.icerock.moko:resources-compose:0.23.0") } } multiplatformResources { multiplatformResourcesPackage = "com.myapplication.common" } 55 moko-resources を導入する shared 内の build.gradle.kts に moko-resources の依存を追加する multiplatformResourcesPackage にリソース読み 込み時に指定するパッケージ名を入力する
  30. Copyright 2023 m.coder All Rights Reserved. 56 moko-resources を導入する commonMain.resources

    フォルダ内に MR.images フォルダを作成する 画像サイズごとに{ファイル名}@1x.png のように命 名する(iOS方式)
  31. Copyright 2023 m.coder All Rights Reserved. 58 moko-resources を導入する リソースコピー用の

    Shell Script を記述する https://github.com/icerockdev/moko-resources #with-orgjetbrainskotlinnativecocoapods
  32. Copyright 2023 m.coder All Rights Reserved. import com.myapplication.common.MR import dev.icerock.moko.resources.compose.painterResource

    Image( painterResource(MR.images.android), null ) 59 moko-resources を導入する ビルドすると MR.images.{ファイル名} でリソース ファイルにアクセス可能になる
  33. Copyright 2023 m.coder All Rights Reserved. import com.myapplication.common.MR import dev.icerock.moko.resources.compose.painterResource

    Image( painterResource(MR.images.android), null ) 60 moko-resources を導入する ビルドすると MR.images.{ファイル名} でリソース ファイルにアクセス可能になる moko-resource-compose プラグインの painterResource を使うと MR.images リソースを 読み込める
  34. Copyright 2023 m.coder All Rights Reserved. ---- App.kt expect fun

    getImageResource(): ImageResource ---- main.android.kt import com.myapplication.common.MR actual fun getImageResource(): ImageResource = MR.images.android ---- main.ios.kt import com.myapplication.common.MR actual fun getImageResource(): ImageResource = MR.images.ios 61 サンプルアプリをいじってみる expect fun getImageResource() を宣言し、actual fun に実装を書く
  35. Copyright 2023 m.coder All Rights Reserved. ---- App.kt expect fun

    getImageResource(): ImageResource ---- main.android.kt import com.myapplication.common.MR actual fun getImageResource(): ImageResource = MR.images.android ---- main.ios.kt import com.myapplication.common.MR actual fun getImageResource(): ImageResource = MR.images.ios 62 サンプルアプリをいじってみる expect fun getImageResource() を宣言し、actual fun に実装を書く ImageResource 型を返す関数に書き換える
  36. Copyright 2023 m.coder All Rights Reserved. import dev.icerock.moko.resources.compose.painterResource @OptIn(ExperimentalResourceApi::class) @Composable

    fun App() { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { Image( painterResource(getImageResource()), null ) } } } } expect fun getPlatformName(): String expect fun getImageResource(): String 63 サンプルアプリをいじってみる painterResource を標準の compose のものから moko-resource のものに差し替える
  37. Copyright 2023 m.coder All Rights Reserved. 71 API通信するアプリを構築してみる • 仕組みづくり

    ◦ ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  38. Copyright 2023 m.coder All Rights Reserved. 72 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  39. Copyright 2023 m.coder All Rights Reserved. KamelImage( resource = asyncPainterResource("{画像URL}"),

    contentDescription = null, ) 74 ネットワーク上の画像を読み込めるようにする 使い方例
  40. Copyright 2023 m.coder All Rights Reserved. sourceSets { val commonMain

    by getting { dependencies { implementation("media.kamel:kamel-image:0.7.1") } } } 77 Kamelのセットアップ shared の build.gradle に Kamel の依存関係を追 加
  41. Copyright 2023 m.coder All Rights Reserved. sourceSets { val commonMain

    by getting { dependencies { … implementation("io.ktor:ktor-client-core:$ktorVersion") } } val androidMain by getting { dependencies { … api("io.ktor:ktor-client-okhttp:$ktorVersion") } } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosMain by creating { dependencies { … implementation("io.ktor:ktor-client-darwin:$ktorVersion") } } } 78 Ktorのセットアップ core クライアントとプラットフォームごとのエンジン を依存関係に追加する
  42. Copyright 2023 m.coder All Rights Reserved. sourceSets { val commonMain

    by getting { dependencies { … implementation("io.ktor:ktor-client-core:$ktorVersion") } } val androidMain by getting { dependencies { … api("io.ktor:ktor-client-okhttp:$ktorVersion") } } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosMain by creating { dependencies { … implementation("io.ktor:ktor-client-darwin:$ktorVersion") } } } 79 Ktorのセットアップ core クライアントとプラットフォームごとのエンジン を依存関係に追加する Android…OkHttp iOS…Darwin
  43. Copyright 2023 m.coder All Rights Reserved. // エンジン指定(指定したエンジンを使用) val client

    = HttpClient(OkHttp) // エンジン指定なし(各プラットフォームに指定したエンジンを使用) val client = HttpClient() 80 Ktorのセットアップ HttpClient 生成時にコンストラクタを指定しない場 合、各プラットフォームに設定したエンジンがデフォ ルトで使われる
  44. Copyright 2023 m.coder All Rights Reserved. @OptIn(ExperimentalResourceApi::class) @Composable fun App()

    { MaterialTheme { var greetingText by remember { mutableStateOf("Hello, World!") } var showImage by remember { mutableStateOf(false) } Column(...) { Button(onClick = { greetingText = "Hello, ${getPlatformName()}" showImage = !showImage }) { Text(greetingText) } AnimatedVisibility(showImage) { KamelImage( asyncPainterResource("https://placehold.jp/150x150.png"), null, ) } } } } 81 ネットワーク上の画像を読み込めるようにする Image を KamelImage に差し替え、読み込みたい 画像のURLを指定する
  45. Copyright 2023 m.coder All Rights Reserved. // マニフェストの設定を忘れずに! <uses-permission android:name="android.permission.INTERNET"

    /> 82 ネットワーク上の画像を読み込めるようにする Android の方は AndroidManifest の INTERNET パーミッションをちゃんと設定してあげる (KMPでも必要なんだね)
  46. Copyright 2023 m.coder All Rights Reserved. 84 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  47. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient()

    val response = client.get { url { protocol = URLProtocol.HTTPS host = "api.thecatapi.com" path("v1/images/search") parameters.append("api_key", "{YOUR_API_KEY}") parameters.append("has_breed", "1") parameters.append("limit", "20") } } 86 APIClient の実装 Ktor の API リクエスト方法
  48. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient()

    val response = client.get { url { protocol = URLProtocol.HTTPS host = "api.thecatapi.com" path("v1/images/search") parameters.append("api_key", "{YOUR_API_KEY}") parameters.append("has_breed", "1") parameters.append("limit", "20") } } 87 APIClient の実装 Ktor の API リクエスト方法 1. HttpClient インスタンス生成
  49. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient()

    val response = client.get { url { protocol = URLProtocol.HTTPS host = "api.thecatapi.com" path("v1/images/search") parameters.append("api_key", "{YOUR_API_KEY}") parameters.append("has_breed", "1") parameters.append("limit", "20") } } 88 APIClient の実装 Ktor の API リクエスト方法 1. HttpClient インスタンス生成 2. client.get でリクエスト
  50. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient()

    val response = client.get { url { protocol = URLProtocol.HTTPS host = "api.thecatapi.com" path("v1/images/search") parameters.append("api_key", "{YOUR_API_KEY}") parameters.append("has_breed", "1") parameters.append("limit", "20") } } 89 APIClient の実装 Ktor の API リクエスト方法 1. HttpClient インスタンス生成 2. client.get でリクエスト 3. アクセス先の url 情報を指定
  51. Copyright 2023 m.coder All Rights Reserved. 90 API通信するアプリを構築してみる • 問題点がいろいろ

    ◦ JSONのパースをしてくれない ◦ リクエスト時に毎回ホスト名やパスの文字列を手打ち
  52. Copyright 2023 m.coder All Rights Reserved. 91 API通信するアプリを構築してみる • 問題点がいろいろ

    ◦ JSONのパースをしてくれない ▪ ContentNegotiation プラグインを使う ◦ リクエスト時に毎回ホスト名やパスの文字列を手打ち ▪ Resource プラグインを使う
  53. Copyright 2023 m.coder All Rights Reserved. 92 API通信するアプリを構築してみる • 問題点がいろいろ

    ◦ JSONのパースをしてくれない ▪ ContentNegotiation プラグインを使う ◦ リクエスト時に毎回ホスト名やパスの文字列を手打ち ▪ Resource プラグインを使う
  54. Copyright 2023 m.coder All Rights Reserved. ---- projectRoot の build.gradle.kts

    plugins { kotlin("multiplatform").apply(false) kotlin("plugin.serialization") version "1.8.20" } buildscript { dependencies { classpath(kotlin("serialization", version = kotlinVersion)) } } 93 ContentNegotiation の導入 ContentNegotiation = シリアライズ・デシリアライズ をサポートするプラグイン https://ktor.io/docs/serialization-client.html 今回は kotlinx.serialization と併せて使う
  55. Copyright 2023 m.coder All Rights Reserved. ---- shared の build.gradle.kts

    plugins { id("kotlinx-serialization") } sourceSets { val commonMain by getting { implementation("io.ktor:ktor-client-content-negotiation:$kt orVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$kt orVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization -json:1.5.1") // kotlinx.serialization } } 94 ContentNegotiation の導入 ContentNegotiation = シリアライズ・デシリアライズ をサポートするプラグイン https://ktor.io/docs/serialization-client.html 今回は kotlinx.serialization と併せて使う ContentNegotiation と kotlinx.serialization をセッ トアップ
  56. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } } 95 ContentNegotiation の導入 Ktor の Httpクライアントに ContentNegotiation を インストールする
  57. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } } 96 ContentNegotiation の導入 Ktor の Httpクライアントに ContentNegotiation を インストールする kotlinx.serialization の Json クラスを ContentNegotiation に渡す
  58. Copyright 2023 m.coder All Rights Reserved. 97 API通信するアプリを構築してみる • 問題点がいろいろ

    ◦ JSONのパースをしてくれない ▪ ContentNegotiation プラグインを使う ◦ リクエスト時に毎回ホスト名やパスの文字列を手打ち ▪ Resource プラグインを使う
  59. Copyright 2023 m.coder All Rights Reserved. interface API { @GET("/endpoint")

    suspend fun fetch( @Query("query") query: String = QUERY_PARAM, ): SampleResponse } 98 Resource の導入 例:Retrofit の API エンドポイント こんな感じのものを作ります
  60. Copyright 2023 m.coder All Rights Reserved. ---- shared の build.gradle.kts

    sourceSets { val commonMain by getting { implementation("io.ktor:ktor-client-resources:$ktorVersion" ) } } 99 Resource の導入 Resource プラグインの依存関係を追加
  61. Copyright 2023 m.coder All Rights Reserved. @Resource("v1/images/search") class Search( val

    api_key: String? = "{YOUR_API_KEY}", val has_breeds: String?, val limit: String? ) 100 Resource の導入 @Resource アノテーションを付与したクラスを作成 する
  62. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } install(Resources) } 101 Resource の導入 Resource プラグインをインストール
  63. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } install(Resources) defaultRequest { host = "api.thecatapi.com" url { protocol = URLProtocol.HTTPS } } } 102 Resource の導入 Resource プラグインをインストール defaultRequest でホスト名やプロトコルなどを指定 しておくと毎回ホスト名を入力しなくていい
  64. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } install(Resources) defaultRequest { host = "api.thecatapi.com" url { protocol = URLProtocol.HTTPS } } } val response = client.get( Search(has_breeds = "1", limit = "20") ).body<JsonArray>() 103 Resource の導入 Resource プラグインをインストール defaultRequest でホスト名やプロトコルなどを指定 しておくと毎回ホスト名を入力しなくていい client.get(Search(...)) で API アクセスが可能にな る
  65. Copyright 2023 m.coder All Rights Reserved. val client = HttpClient

    { install(ContentNegotiation) { json( Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true } ) } install(Resources) defaultRequest { host = "api.thecatapi.com" url { protocol = URLProtocol.HTTPS } } } val response = client.get( Search(has_breeds = "1", limit = "20") ).body<JsonArray>() 104 Resource の導入 Resource プラグインをインストール defaultRequest でホスト名やプロトコルなどを指定 しておくと毎回ホスト名を入力しなくていい client.get(Search(...)) で API アクセスが可能にな る .body<T> で @Serializable アノテーションをつけた 型を指定するとその型にパースしてくれる
  66. Copyright 2023 m.coder All Rights Reserved. class AppRepository { private

    val client: HttpClient = ApiClient().create() suspend fun fetchCats(): Result<List<Cat>> = runCatching { client.get(Search()).body<JsonArray>().parse() } // ↓このへんでいい感じに Catクラスへパース … } 105 Repository の実装 今までの処理を整理して、 HttpClient 生成処理は ApiClient クラスに切り出し Repository に API リクエスト処理を記述
  67. Copyright 2023 m.coder All Rights Reserved. class AppRepository { private

    val client: HttpClient = ApiClient().create() suspend fun fetchCats(): Result<List<Cat>> = runCatching { client.get(Search()).body<JsonArray>().parse() } // ↓このへんでいい感じに Catクラスへパース … } data class Cat( val name: String, val imageUrl: String, val description: String, ) 106 Repository の実装 今までの処理を整理して、 HttpClient 生成処理は ApiClient クラスに切り出し Repository に API リクエスト処理を記述 Cat クラスはこんな感じ
  68. Copyright 2023 m.coder All Rights Reserved. defaultRequest { host =

    "api.thecatapi.com/v1" url { protocol = URLProtocol.HTTPS } } 108 地味な罠 host名指定
  69. Copyright 2023 m.coder All Rights Reserved. defaultRequest { host =

    "api.thecatapi.com/v1" url { protocol = URLProtocol.HTTPS } } 109 地味な罠 host名指定 host以外の部分も指定すると …
  70. Copyright 2023 m.coder All Rights Reserved. defaultRequest { host =

    "api.thecatapi.com/v1" url { protocol = URLProtocol.HTTPS } } 110 地味な罠 host名指定 host以外の部分も指定すると … A server with the specified hostname could not be found., NSErrorFailingURLStringKey=
  71. Copyright 2023 m.coder All Rights Reserved. defaultRequest { host =

    "api.thecatapi.com/v1" url { protocol = URLProtocol.HTTPS } } 111 地味な罠 host名指定 host以外の部分も指定すると … A server with the specified hostname could not be found., NSErrorFailingURLStringKey= ホスト名が見つからなくて通信エラーになる
  72. Copyright 2023 m.coder All Rights Reserved. 112 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  73. Copyright 2023 m.coder All Rights Reserved. @Composable fun CatListScreen() {

    val items = remember { runBlocking { AppRepository().fetchCats() }.getOrNull() } Scaffold( modifier = Modifier .fillMaxWidth() ) { items?.let { LazyColumn { items(it) { cat -> CatListItem(cat) } } } } } 113
  74. Copyright 2023 m.coder All Rights Reserved. @Composable fun CatListScreen() {

    val items = remember { runBlocking { AppRepository().fetchCats() }.getOrNull() } Scaffold( modifier = Modifier .fillMaxWidth() ) { items?.let { LazyColumn { items(it) { cat -> CatListItem(cat) } } } } } 114
  75. Copyright 2023 m.coder All Rights Reserved. @Composable fun CatListItem( item:

    Cat, ) { Card(...) { Row( verticalAlignment = Alignment.CenterVertically ) { KamelImage( modifier = Modifier.fillMaxHeight() .aspectRatio(1.0f), contentScale = ContentScale.Crop, resource = asyncPainterResource(item.imageUrl), contentDescription = null, ) Text( modifier = Modifier.padding(horizontal = 16.dp), text = item.name ) } } 115
  76. Copyright 2023 m.coder All Rights Reserved. @Composable fun CatListItem( item:

    Cat, ) { Card(...) { Row( verticalAlignment = Alignment.CenterVertically ) { KamelImage( modifier = Modifier.fillMaxHeight() .aspectRatio(1.0f), contentScale = ContentScale.Crop, resource = asyncPainterResource(item.imageUrl), contentDescription = null, ) Text( modifier = Modifier.padding(horizontal = 16.dp), text = item.name ) } } 116
  77. Copyright 2023 m.coder All Rights Reserved. @Composable fun CatDetailScreen(cat: Cat)

    { Scaffold { Column( modifier = Modifier .fillMaxWidth() .padding(32.dp) ) { KamelImage( modifier = Modifier .fillMaxWidth() .aspectRatio(0.9f) .clip(RoundedCornerShape(16.dp)), contentScale = ContentScale.Crop, resource = asyncPainterResource(cat.imageUrl), contentDescription = null, ) Spacer(Modifier.height(16.dp)) Text(text = cat.name) Spacer(Modifier.height(16.dp)) Text(text = cat.description) } } 117
  78. Copyright 2023 m.coder All Rights Reserved. 118 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  79. Copyright 2023 m.coder All Rights Reserved. 120 API通信するアプリを構築してみる • 画面遷移処理の実装方法

    ◦ 各プラットフォームごとにネイティブで画面遷移処理を実装する ◦ 画面遷移用の外部ライブラリを使う
  80. Copyright 2023 m.coder All Rights Reserved. 121 API通信するアプリを構築してみる • 画面遷移処理の実装方法

    ◦ 各プラットフォームごとにネイティブで画面遷移処理を実装する ◦ 画面遷移用の外部ライブラリを使う ▪ Voyager を利用 https://github.com/adrielcafe/voyager
  81. Copyright 2023 m.coder All Rights Reserved. ---- shared の build.gradle.kts

    sourceSets { val commonMain by getting { implementation("cafe.adriel.voyager:voyager-navigator$voyag erVersion") } } 122 Voyager の導入 Voyager = 画面遷移・状態管理の仕組みを提供し てくれるライブラリ shared の build.gradle に依存関係を追加
  82. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { // CatListScreen の実装 ... } } @Composable fun App() { MaterialTheme { Navigator(CatListScreen()) } } 123 Voyager の導入 Screen クラスを継承したクラスを用意する
  83. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { // CatListScreen の実装 ... } } @Composable fun App() { MaterialTheme { Navigator(CatListScreen()) } } 124 Voyager の導入 Screen クラスを継承したクラスを用意する override Content() の中に今までどおり画面の UIを 記述する
  84. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { // CatListScreen の実装 ... } } @Composable fun App() { MaterialTheme { Navigator(CatListScreen()) } } 125 Voyager の導入 Screen クラスを継承したクラスを用意する override Content() の中に今までどおり画面の UIを 記述する 遷移元から Navigator({Screenクラス}) で画面遷移 させる
  85. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Scaffold(...) { items?.let { LazyColumn { items(it) { cat -> CatListItem(cat) { navigator.push(CatDetailScreen(it)) } } } } } } } class CatDetailScreen(private val cat: Cat) : Screen { @Composable override fun Content() { // CatDetailScreenの実装 ... } } 126 Voyager の導入 LocalNavigator.currentOrThrow を使って Navigator のインスタンスを取得する
  86. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Scaffold(...) { items?.let { LazyColumn { items(it) { cat -> CatListItem(cat) { navigator.push(CatDetailScreen(it)) } } } } } } } class CatDetailScreen(private val cat: Cat) : Screen { @Composable override fun Content() { // CatDetailScreenの実装 ... } } 127 Voyager の導入 LocalNavigator.currentOrThrow を使って Navigator のインスタンスを取得する CatDetailScreen もクラス化して Screen を継承さ せておく
  87. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Scaffold(...) { items?.let { LazyColumn { items(it) { cat -> CatListItem(cat) { navigator.push(CatDetailScreen(it)) } } } } } } } class CatDetailScreen(private val cat: Cat) : Screen { @Composable override fun Content() { // CatDetailScreenの実装 ... } } 128 Voyager の導入 LocalNavigator.currentOrThrow を使って Navigator のインスタンスを取得する CatDetailScreen もクラス化して Screen を継承さ せておく navigator.push({Screenクラス})でバックスタックを 積みつつ次画面へ遷移が可能
  88. Copyright 2023 m.coder All Rights Reserved. 130 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  89. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { } 132 状態ごとにUIを切り替える ScreenModel = Jetpack の ViewModel 的な役割 ライフサイクルに沿った動作や画面回転後のデー タ保持などを提供してくれる
  90. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { fun fetch() { coroutineScope.launch { repository.fetchCats() ... } } } 133 状態ごとにUIを切り替える ScreenModel = Jetpack の ViewModel 的な役割 ライフサイクルに沿った動作や画面回転後のデー タ保持などを提供してくれる coroutineScope を使うと viewModelScope のよう にScreenModel のスコープで Coroutines を扱える
  91. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } ... } } 134 状態ごとにUIを切り替える ScreenModel = Jetpack の ViewModel 的な役割 ライフサイクルに沿った動作や画面回転後のデー タ保持などを提供してくれる coroutineScope を使うと viewModelScope のよう にScreenModel のスコープで Coroutines を扱える rememberScreenModel で Composable関数から 呼び出し可能
  92. Copyright 2023 m.coder All Rights Reserved. sealed class State {

    object Initial : State() object Loading : State() data class OnReady(val cats: List<Cat>) : State() data class Error(val reason: Throwable) : State() } 135 状態ごとにUIを切り替える 状態管理用の State を定義する
  93. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { private val repository: AppRepository = AppRepository() val state: StateFlow<State> get() = _state private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial) init { fetch() } fun fetch() { coroutineScope.launch { _ state.update { State.Loading } repository.fetchCats() .fold( onSuccess = { response -> _state.update { State.OnReady(response)} }, onFailure = { throwable -> _state.update { State.Error(throwable) } } ) } } } 136 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く
  94. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { private val repository: AppRepository = AppRepository() val state: StateFlow<State> get() = _state private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial) init { fetch() } fun fetch() { coroutineScope.launch { _ state.update { State.Loading } repository.fetchCats() .fold( onSuccess = { response -> _state.update { State.OnReady(response)} }, onFailure = { throwable -> _state.update { State.Error(throwable) } } ) } } } 137 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く
  95. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { private val repository: AppRepository = AppRepository() val state: StateFlow<State> get() = _state private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial) init { fetch() } fun fetch() { coroutineScope.launch { _ state.update { State.Loading } repository.fetchCats() .fold( onSuccess = { response -> _state.update { State.OnReady(response)} }, onFailure = { throwable -> _state.update { State.Error(throwable) } } ) } } } 138 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く
  96. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { private val repository: AppRepository = AppRepository() val state: StateFlow<State> get() = _state private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial) init { fetch() } fun fetch() { coroutineScope.launch { _ state.update { State.Loading } repository.fetchCats() .fold( onSuccess = { response -> _state.update { State.OnReady(response)} }, onFailure = { throwable -> _state.update { State.Error(throwable) } } ) } } } 139 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く
  97. Copyright 2023 m.coder All Rights Reserved. class CatListScreenModel : ScreenModel

    { private val repository: AppRepository = AppRepository() val state: StateFlow<State> get() = _state private val _state: MutableStateFlow<State> = MutableStateFlow(State.Initial) init { fetch() } fun fetch() { coroutineScope.launch { _ state.update { State.Loading } repository.fetchCats() .fold( onSuccess = { response -> _state.update { State.OnReady(response)} }, onFailure = { throwable -> _state.update { State.Error(throwable) } } ) } } } 140 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く
  98. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } val state by screenModel.state.collectAsState() when (val result = state) { is State.Initial, is State.Loading -> { LoadingScreen() } is State.OnReady -> { ListView( items = result.cats, onItemClicked = { navigator.push(CatDetailScreen(it)) } ) } is State.Error -> { ErrorScreen( onRetryClicked = screenModel::fetch ) } } } } 141 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く Screen クラス側で状態を監視する
  99. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } val state by screenModel.state.collectAsState() when (val result = state) { is State.Initial, is State.Loading -> { LoadingScreen() } is State.OnReady -> { ListView( items = result.cats, onItemClicked = { navigator.push(CatDetailScreen(it)) } ) } is State.Error -> { ErrorScreen( onRetryClicked = screenModel::fetch ) } } } } 142 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く Screen クラス側で状態を監視する
  100. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } val state by screenModel.state.collectAsState() when (val result = state) { is State.Initial, is State.Loading -> { LoadingScreen() } is State.OnReady -> { ListView( items = result.cats, onItemClicked = { navigator.push(CatDetailScreen(it)) } ) } is State.Error -> { ErrorScreen( onRetryClicked = screenModel::fetch ) } } } } 143 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く Screen クラス側で状態を監視する 状態によってScreenを出し分ける
  101. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } val state by screenModel.state.collectAsState() when (val result = state) { is State.Initial, is State.Loading -> { LoadingScreen() } is State.OnReady -> { ListView( items = result.cats, onItemClicked = { navigator.push(CatDetailScreen(it)) } ) } is State.Error -> { ErrorScreen( onRetryClicked = screenModel::fetch ) } } } } 144 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く Screen クラス側で状態を監視する 状態によってScreenを出し分ける
  102. Copyright 2023 m.coder All Rights Reserved. class CatListScreen : Screen

    { @Composable override fun Content() { val screenModel = rememberScreenModel { CatListScreenModel() } val state by screenModel.state.collectAsState() when (val result = state) { is State.Initial, is State.Loading -> { LoadingScreen() } is State.OnReady -> { ListView( items = result.cats, onItemClicked = { navigator.push(CatDetailScreen(it)) } ) } is State.Error -> { ErrorScreen( onRetryClicked = screenModel::fetch ) } } } } 145 状態ごとにUIを切り替える 状態管理用の State を定義する ScreenModel に API へのアクセス処理を書く Screen クラス側で状態を監視する 状態によってScreenを出し分ける
  103. Copyright 2023 m.coder All Rights Reserved. 147 • 仕組みづくり ◦

    ネットワーク上の画像を読み込めるようにする ◦ API通信部分を構築する • UI構築 ◦ UIを作成する ◦ 画面遷移処理を実装する ◦ 状態ごとにUIを切り替える ◦ AppBarを追加する
  104. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 148 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る
  105. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 149 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る 親スクリーンに初期表示させたいスクリーンを Navigator({Screen}) に指定する
  106. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 150 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る 親スクリーンに初期表示させたいスクリーンを Navigator({Screen}) に指定する Navigator({Screen}) 以下のコンテンツスロットにレ イアウトを構築する
  107. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 151 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る 親スクリーンに初期表示させたいスクリーンを Navigator({Screen}) に指定する Navigator({Screen}) 以下のコンテンツスロットにレ イアウトを構築する AppBarを構築する
  108. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 152 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る 親スクリーンに初期表示させたいスクリーンを Navigator({Screen}) に指定する Navigator({Screen}) 以下のコンテンツスロットにレ イアウトを構築する AppBarを構築する
  109. Copyright 2023 m.coder All Rights Reserved. class MainScreen : Screen

    { @Composable override fun Content() { Navigator(CatListScreen()) { navigator -> val navigationState = remember { NavigationState(navigator)} Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text("Cat App") }, navigationIcon = { if (navigationState.shouldShowBack){ IconButton( onClick = navigator::pop, ) { Icon(...) } } }, ) }) { CurrentScreen() } } } } 153 AppBarを追加する AppBar 表示用の親スクリーン (MainScreen)を作る 親スクリーンに初期表示させたいスクリーンを Navigator({Screen}) に指定する Navigator({Screen}) 以下のコンテンツスロットにレ イアウトを構築する AppBarを構築する CurrentScreen() で Navigator に指定した Screen を呼び出せる
  110. Copyright 2023 m.coder All Rights Reserved. 156 まとめ • 嬉しいこと

    ◦ ほとんどのコードを Kotlin で書ける ◦ UI構築の時間が短縮できる • つらみ ◦ 外部ライブラリに頼らざるをえない ◦ 型安全なアクセスをするのに一工夫いる ◦ commonMain で Preview が使えない ▪ Compose for Desktop 用のプラグインはあるみたいなので 今後に期待