Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. 1
    Jetpack Compose で Android/iOS アプリを作る
    m.coder

    View full-size slide

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

    View full-size slide

  3. Copyright 2023 m.coder All Rights Reserved. 3
    普段 iPhone 使いの人〜? 

    View full-size slide

  4. Copyright 2023 m.coder All Rights Reserved. 4
    普段 iPhone 使いです

    View full-size slide

  5. Copyright 2023 m.coder All Rights Reserved. 5
    自分で作ったアプリを
    普段の端末で動かせないのが少しさみしい

    View full-size slide

  6. Copyright 2023 m.coder All Rights Reserved. 6
    そこで
    Compose for iOS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  9. Copyright 2023 m.coder All Rights Reserved. 9
    Compose Multiplatform ?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. Copyright 2023 m.coder All Rights Reserved. 12
    UI構築は Compose
    →ビジネスロジックは?

    View full-size slide

  13. Copyright 2023 m.coder All Rights Reserved. 13
    KMP(Kotlin Multiplatform)
    を使う

    View full-size slide

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

    View full-size slide

  15. Copyright 2023 m.coder All Rights Reserved. 15
    Compose Multiplatform + KMP
    = ぜんぶ Kotlin で書ける!

    View full-size slide

  16. Copyright 2023 m.coder All Rights Reserved. 16
    Jetpack Compose で
    Android/iOS アプリを作る

    View full-size slide

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

    View full-size slide

  18. Copyright 2023 m.coder All Rights Reserved. 18
    つまり、こう
    Jetpack Compose → Android用
    Compose for iOS → iOS用

    View full-size slide

  19. Copyright 2023 m.coder All Rights Reserved. 19
    Jetpack Compose で
    Android/iOS アプリを作る

    View full-size slide

  20. 01 | Compose Multiplatform のセットアップ
    02 | サンプルアプリをいじってみる
    03 | API通信するアプリを構築してみる
    アジェンダ
    Copyright 2023 m.coder All Rights Reserved.
    アジェンダ
    20

    View full-size slide

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

    View full-size slide

  22. Copyright 2023 m.coder All Rights Reserved. 22
    注意事項
    Compose for iOS is in Alpha.
    →破壊的な変更が入る可能性が
    大いにあります

    View full-size slide

  23. Copyright 2023 m.coder All Rights Reserved. 23
    Compose Multiplatformの
    セットアップ

    View full-size slide

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

    View full-size slide

  25. Copyright 2023 m.coder All Rights Reserved. 25

    View full-size slide

  26. Copyright 2023 m.coder All Rights Reserved. 26
    今回はテンプレートリポジトリを使います
    https://github.com/JetBrains/compose-multiplatform-ios-an
    droid-template

    View full-size slide

  27. Copyright 2023 m.coder All Rights Reserved. 27

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 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
    環境構築
    環境が整っていれば全てに ✅がつく

    View full-size slide

  31. 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

    View full-size slide

  32. 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

    View full-size slide

  33. 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` を入力しセットアップ

    View full-size slide

  34. Copyright 2023 m.coder All Rights Reserved. 34
    サンプルアプリをいじってみる

    View full-size slide

  35. Copyright 2023 m.coder All Rights Reserved. 35
    ひとまずRunしてみる

    View full-size slide

  36. Copyright 2023 m.coder All Rights Reserved. 36

    View full-size slide

  37. Copyright 2023 m.coder All Rights Reserved. 37
    Android iOS

    View full-size slide

  38. Copyright 2023 m.coder All Rights Reserved. 38
    サンプルアプリをいじってみる
    ・shared.commonMain …共通コード
    ・shared.androidMain …Android用の Kotlin コー

    ・shared.iosMain …iOS用の Kotlin コード

    View full-size slide

  39. 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 の実装コード

    View full-size slide

  40. 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 の実装コード

    View full-size slide

  41. 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 の実装コード

    View full-size slide

  42. 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 …各プラットフォームでの実際の関数の
    動作

    View full-size slide

  43. 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 …各プラットフォームでの実際の関数の
    動作

    View full-size slide

  44. Copyright 2023 m.coder All Rights Reserved. 44
    サンプルアプリをいじってみる
    expect fun と actual fun を使って
    Android と iOS で
    違う画像を表示してみよう!
    android.png ios.png

    View full-size slide

  45. 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 に実装を書く

    View full-size slide

  46. 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() に差し替える

    View full-size slide

  47. Copyright 2023 m.coder All Rights Reserved. 47
    ビルドしてみる

    View full-size slide

  48. Copyright 2023 m.coder All Rights Reserved. 48
    Android iOS

    View full-size slide

  49. Copyright 2023 m.coder All Rights Reserved. 49
    Android iOS
    リソースが見つからない

    View full-size slide

  50. Copyright 2023 m.coder All Rights Reserved. 50
    https://github.com/JetBrains/compose-multiplatform
    ↓のサンプルプロジェクトだと
    動いてるのでなにもわからない
    誰か教えてください

    View full-size slide

  51. Copyright 2023 m.coder All Rights Reserved. 51
    気を取り直して、
    リソース管理方法を変えてみる
    (文字列の直指定やめたい)

    View full-size slide

  52. Copyright 2023 m.coder All Rights Reserved. 52
    moko-resources を導入する
    https://github.com/icerockdev/moko-resources

    View full-size slide

  53. 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 を追加する

    View full-size slide

  54. 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
    の依存を追加する

    View full-size slide

  55. 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 にリソース読み
    込み時に指定するパッケージ名を入力する

    View full-size slide

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

    View full-size slide

  57. Copyright 2023 m.coder All Rights Reserved. 57
    moko-resources を導入する
    Xcodeの Build Phases で New Run Script Phase
    を選ぶ

    View full-size slide

  58. Copyright 2023 m.coder All Rights Reserved. 58
    moko-resources を導入する
    リソースコピー用の Shell Script を記述する
    https://github.com/icerockdev/moko-resources
    #with-orgjetbrainskotlinnativecocoapods

    View full-size slide

  59. 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.{ファイル名} でリソース
    ファイルにアクセス可能になる

    View full-size slide

  60. 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 リソースを
    読み込める

    View full-size slide

  61. 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 に実装を書く

    View full-size slide

  62. 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 型を返す関数に書き換える

    View full-size slide

  63. 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 のものに差し替える

    View full-size slide

  64. Copyright 2023 m.coder All Rights Reserved. 64
    Android iOS

    View full-size slide

  65. Copyright 2023 m.coder All Rights Reserved. 65
    Android iOS

    View full-size slide

  66. Copyright 2023 m.coder All Rights Reserved. 66
    API通信するアプリを
    構築してみる

    View full-size slide

  67. Copyright 2023 m.coder All Rights Reserved. 67
    サンプルリポジトリ
    https://github.com/nanaten/compose-for-ios-sample

    View full-size slide

  68. Copyright 2023 m.coder All Rights Reserved. 68

    View full-size slide

  69. Copyright 2023 m.coder All Rights Reserved. 69
    Cat API 🐱
    https://thecatapi.com/

    View full-size slide

  70. Copyright 2023 m.coder All Rights Reserved. 70
    Dog API 🐶
    https://dog.ceo/dog-api/

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  73. Copyright 2023 m.coder All Rights Reserved. 73
    Kamelを使う
    https://github.com/Kamel-Media/Kamel

    View full-size slide

  74. Copyright 2023 m.coder All Rights Reserved.
    KamelImage(
    resource = asyncPainterResource("{画像URL}"),
    contentDescription = null,
    )
    74
    ネットワーク上の画像を読み込めるようにする
    使い方例

    View full-size slide

  75. Copyright 2023 m.coder All Rights Reserved. 75
    Ktor に依存しているので、
    Ktor のセットアップも必要
    https://ktor.io/

    View full-size slide

  76. Copyright 2023 m.coder All Rights Reserved. 76
    Ktor = ネットワーク通信用のライブラリ
    100% Kotlin 製なので KMP で使われる

    View full-size slide

  77. 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 の依存関係を追

    View full-size slide

  78. 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 クライアントとプラットフォームごとのエンジン
    を依存関係に追加する

    View full-size slide

  79. 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

    View full-size slide

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

    View full-size slide

  81. 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を指定する

    View full-size slide

  82. Copyright 2023 m.coder All Rights Reserved.
    // マニフェストの設定を忘れずに!
    />
    82
    ネットワーク上の画像を読み込めるようにする
    Android の方は AndroidManifest の INTERNET
    パーミッションをちゃんと設定してあげる
    (KMPでも必要なんだね)

    View full-size slide

  83. Copyright 2023 m.coder All Rights Reserved. 83
    Android iOS

    View full-size slide

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

    View full-size slide

  85. Copyright 2023 m.coder All Rights Reserved. 85
    さっきセットアップした
    Ktor を使えるから楽々!
    (フラグ)

    View full-size slide

  86. 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 リクエスト方法

    View full-size slide

  87. 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 インスタンス生成

    View full-size slide

  88. 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 でリクエスト

    View full-size slide

  89. 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 情報を指定

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  93. 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 と併せて使う

    View full-size slide

  94. 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 をセッ
    トアップ

    View full-size slide

  95. 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 を
    インストールする

    View full-size slide

  96. 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 に渡す

    View full-size slide

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

    View full-size slide

  98. 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 エンドポイント
    こんな感じのものを作ります

    View full-size slide

  99. 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 プラグインの依存関係を追加

    View full-size slide

  100. 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 アノテーションを付与したクラスを作成
    する

    View full-size slide

  101. 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 プラグインをインストール

    View full-size slide

  102. 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 でホスト名やプロトコルなどを指定
    しておくと毎回ホスト名を入力しなくていい

    View full-size slide

  103. 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()
    103
    Resource の導入
    Resource プラグインをインストール
    defaultRequest でホスト名やプロトコルなどを指定
    しておくと毎回ホスト名を入力しなくていい
    client.get(Search(...)) で API アクセスが可能にな

    View full-size slide

  104. 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()
    104
    Resource の導入
    Resource プラグインをインストール
    defaultRequest でホスト名やプロトコルなどを指定
    しておくと毎回ホスト名を入力しなくていい
    client.get(Search(...)) で API アクセスが可能にな

    .body で @Serializable アノテーションをつけた
    型を指定するとその型にパースしてくれる

    View full-size slide

  105. Copyright 2023 m.coder All Rights Reserved.
    class AppRepository {
    private val client: HttpClient = ApiClient().create()
    suspend fun fetchCats(): Result> =
    runCatching {
    client.get(Search()).body().parse()
    }
    // ↓このへんでいい感じに
    Catクラスへパース

    }
    105
    Repository の実装
    今までの処理を整理して、 HttpClient 生成処理は
    ApiClient クラスに切り出し
    Repository に API リクエスト処理を記述

    View full-size slide

  106. Copyright 2023 m.coder All Rights Reserved.
    class AppRepository {
    private val client: HttpClient = ApiClient().create()
    suspend fun fetchCats(): Result> =
    runCatching {
    client.get(Search()).body().parse()
    }
    // ↓このへんでいい感じに
    Catクラスへパース

    }
    data class Cat(
    val name: String,
    val imageUrl: String,
    val description: String,
    )
    106
    Repository の実装
    今までの処理を整理して、 HttpClient 生成処理は
    ApiClient クラスに切り出し
    Repository に API リクエスト処理を記述
    Cat クラスはこんな感じ

    View full-size slide

  107. Copyright 2023 m.coder All Rights Reserved. 107
    地味な罠

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  110. 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=

    View full-size slide

  111. 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=
    ホスト名が見つからなくて通信エラーになる

    View full-size slide

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

    View full-size slide

  113. 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

    View full-size slide

  114. 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

    View full-size slide

  115. 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

    View full-size slide

  116. 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

    View full-size slide

  117. 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

    View full-size slide

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

    View full-size slide

  119. Copyright 2023 m.coder All Rights Reserved. 119
    画面遷移用の
    公式ライブラリはまだない

    View full-size slide

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

    View full-size slide

  121. Copyright 2023 m.coder All Rights Reserved. 121
    API通信するアプリを構築してみる
    ● 画面遷移処理の実装方法
    ○ 各プラットフォームごとにネイティブで画面遷移処理を実装する
    ○ 画面遷移用の外部ライブラリを使う
    ■ Voyager を利用
    https://github.com/adrielcafe/voyager

    View full-size slide

  122. 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 に依存関係を追加

    View full-size slide

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

    View full-size slide

  124. 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を
    記述する

    View full-size slide

  125. 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クラス}) で画面遷移
    させる

    View full-size slide

  126. 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 のインスタンスを取得する

    View full-size slide

  127. 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 を継承さ
    せておく

    View full-size slide

  128. 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クラス})でバックスタックを
    積みつつ次画面へ遷移が可能

    View full-size slide

  129. Copyright 2023 m.coder All Rights Reserved. 129

    View full-size slide

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

    View full-size slide

  131. Copyright 2023 m.coder All Rights Reserved. 131
    先ほどの Voyager を使う 

    View full-size slide

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

    View full-size slide

  133. 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 を扱える

    View full-size slide

  134. 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関数から
    呼び出し可能

    View full-size slide

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

    View full-size slide

  136. Copyright 2023 m.coder All Rights Reserved.
    class CatListScreenModel : ScreenModel {
    private val repository: AppRepository = AppRepository()
    val state: StateFlow get() = _state
    private val _state: MutableStateFlow =
    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 へのアクセス処理を書く

    View full-size slide

  137. Copyright 2023 m.coder All Rights Reserved.
    class CatListScreenModel : ScreenModel {
    private val repository: AppRepository = AppRepository()
    val state: StateFlow get() = _state
    private val _state: MutableStateFlow =
    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 へのアクセス処理を書く

    View full-size slide

  138. Copyright 2023 m.coder All Rights Reserved.
    class CatListScreenModel : ScreenModel {
    private val repository: AppRepository = AppRepository()
    val state: StateFlow get() = _state
    private val _state: MutableStateFlow =
    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 へのアクセス処理を書く

    View full-size slide

  139. Copyright 2023 m.coder All Rights Reserved.
    class CatListScreenModel : ScreenModel {
    private val repository: AppRepository = AppRepository()
    val state: StateFlow get() = _state
    private val _state: MutableStateFlow =
    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 へのアクセス処理を書く

    View full-size slide

  140. Copyright 2023 m.coder All Rights Reserved.
    class CatListScreenModel : ScreenModel {
    private val repository: AppRepository = AppRepository()
    val state: StateFlow get() = _state
    private val _state: MutableStateFlow =
    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 へのアクセス処理を書く

    View full-size slide

  141. 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 クラス側で状態を監視する

    View full-size slide

  142. 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 クラス側で状態を監視する

    View full-size slide

  143. 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を出し分ける

    View full-size slide

  144. 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を出し分ける

    View full-size slide

  145. 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を出し分ける

    View full-size slide

  146. Copyright 2023 m.coder All Rights Reserved. 146

    View full-size slide

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

    View full-size slide

  148. 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)を作る

    View full-size slide

  149. 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}) に指定する

    View full-size slide

  150. 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}) 以下のコンテンツスロットにレ
    イアウトを構築する

    View full-size slide

  151. 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を構築する

    View full-size slide

  152. 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を構築する

    View full-size slide

  153. 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
    を呼び出せる

    View full-size slide

  154. Copyright 2023 m.coder All Rights Reserved. 154

    View full-size slide

  155. Copyright 2023 m.coder All Rights Reserved. 155

    View full-size slide

  156. Copyright 2023 m.coder All Rights Reserved. 156
    まとめ
    ● 嬉しいこと
    ○ ほとんどのコードを Kotlin で書ける
    ○ UI構築の時間が短縮できる
    ● つらみ
    ○ 外部ライブラリに頼らざるをえない
    ○ 型安全なアクセスをするのに一工夫いる
    ○ commonMain で Preview が使えない
    ■ Compose for Desktop 用のプラグインはあるみたいなので
    今後に期待

    View full-size slide

  157. Copyright 2023 m.coder All Rights Reserved. 157
    Compose で書いたアプリが
    iOS で動くのは楽しい!

    View full-size slide

  158. Copyright 2023 m.coder All Rights Reserved. 158
    おわり

    View full-size slide