Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Copyright 2023 m.coder All Rights Reserved. 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Copyright 2023 m.coder All Rights Reserved. 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Copyright 2023 m.coder All Rights Reserved. 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Copyright 2023 m.coder All Rights Reserved. 68

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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 アクセスが可能にな る

Slide 104

Slide 104 text

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 アノテーションをつけた 型を指定するとその型にパースしてくれる

Slide 105

Slide 105 text

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 リクエスト処理を記述

Slide 106

Slide 106 text

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 クラスはこんな感じ

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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=

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

Copyright 2023 m.coder All Rights Reserved. 129

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

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 を定義する

Slide 136

Slide 136 text

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 へのアクセス処理を書く

Slide 137

Slide 137 text

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 へのアクセス処理を書く

Slide 138

Slide 138 text

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 へのアクセス処理を書く

Slide 139

Slide 139 text

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 へのアクセス処理を書く

Slide 140

Slide 140 text

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 へのアクセス処理を書く

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

Copyright 2023 m.coder All Rights Reserved. 146

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

Copyright 2023 m.coder All Rights Reserved. 154

Slide 155

Slide 155 text

Copyright 2023 m.coder All Rights Reserved. 155

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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