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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View 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 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 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 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 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 Slide

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

    View 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

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

    View 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 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 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View 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 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 Slide

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

    View 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide