Slide 1

Slide 1 text

©Copyright 2021 BEARTAIL Inc. Time Hack Company 株式会社BEARTAIL 「Jetpack Composeって、Desktopアプリも作れるのよ」 2021.10.01 BEARTAL社内LT会 Expense事業部 坂上晴信/にしこりさぶろ~ J( 'ー`)し

Slide 2

Slide 2 text

©Copyright 2021 BEARTAIL Inc. 話すこと ★Jetpack ComposeでDesktopアプリを作っている話 ➢ そもそもJetpack Composeとは? ➢ Jetpack Compose for Desktopの概要解説 ~Kotlin MPPを添えて~ ➢ 作っているアプリの実装紹介と使い心地レビュー ★Jetpack ComposeでDesktopアプリを実装する楽しさを伝えられれば…😊

Slide 3

Slide 3 text

そもそもJetpack Composeとは?

Slide 4

Slide 4 text

©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク

Slide 5

Slide 5 text

©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML + View activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById(R.id.message) textView.text = "Hello, World!" } } MainActivity.kt

Slide 6

Slide 6 text

©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML + View activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById(R.id.message) textView.text = "Hello, World!" } } MainActivity.kt これから: Jetpack Compose @Composable fun Greeting() { Text("Hello, World!") } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Greeting() } } } MainActivity.kt

Slide 7

Slide 7 text

©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML + View activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById(R.id.message) textView.text = "Hello, World!" } } MainActivity.kt これから: Jetpack Compose AndroidアプリのUIをReactのような文法で Kotlinの高い表現力 + 型安全の恩恵を得つつ 実装できるフレームワーク! 発表: Google I/O 2019 Stable版リリース: 2021.07.28🎉 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Greeting() } } } MainActivity.kt @Composable fun Greeting() { Text("Hello, World!") }

Slide 8

Slide 8 text

©Copyright 2021 BEARTAIL Inc. ★レシートポスト 全ての画面をMVVM化できたら導入したいな…(願望) そもそもJetpack Composeとは? Jetpack Compose採用事例 ↓公式ドキュメントより ★ZOZOTOWN AndroidへのJetpack Compose導入の取り組み - ZOZO Technologies TECH BLOG https://techblog.zozo.com/entry/zozotown-android-jetpack-compose ✓ ViewやViewModelとの相互運用性に かなり配慮されたAPI仕様 ✓ コードで直接UIを表現するため ✓ 読みやすい!状態を分離しやすい! ✓ KotlinでUI実装できる!最高! →今後AndroidにおけるUI実装の デファクトになる(はず)

Slide 9

Slide 9 text

©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? 🤔

Slide 10

Slide 10 text

©Copyright 2021 BEARTAIL Inc. Time Hack Company 株式会社BEARTAIL J( 'ー`)し 「Jetpack Composeって、Desktopアプリも作れるのよ」 2021.10.01 BEARTAL社内LT会 Expense事業部 坂上晴信/にしこりさぶろ~

Slide 11

Slide 11 text

©Copyright 2021 BEARTAIL Inc. Time Hack Company 株式会社BEARTAIL J( 'ー`)し 「Jetpack Composeって、Desktopアプリも作れるのよ」 2021.10.01 BEARTAL社内LT会 Expense事業部 坂上晴信/にしこりさぶろ~ 今日はDesktopアプリを作る話だったはず…🤔 なぜ はさっきからAndroidの話ばっかりしているんだ…?🤔

Slide 12

Slide 12 text

Jetpack Compose for Desktopの概要解説 ~Kotlin MPPを添えて~

Slide 13

Slide 13 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Jetpack Compose for Desktopの説明の前に… ★Kotlin Multiplatform(MPP)について ➢ Kotlinの言語機能を用いたX-Platフレームワーク ➢ 「ビジネスロジックの共通化」へのフォーカスが特徴 ➢ KotlinのコードをJVM/Native/JSコードに変換し、出力 Reference: https://kotlinlang.org/docs/multiplatform.html

Slide 14

Slide 14 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ

Slide 15

Slide 15 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ commonMain: 共通のロジック・データ構造 iosMain: iOS向けの実装 androidMain: Android向けの実装

Slide 16

Slide 16 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ data class User( val id: String, val name: String, val email: String, ) { fun useGmail() = email.endsWith("gmail.com") } プラットフォームで定義・処理が 変化しないデータ構造

Slide 17

Slide 17 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ expect fun randomUUID(): String プラットフォームで引数・返り値が同じ 処理が変化するメソッド (例) ランダムなUUIDをString型で取得

Slide 18

Slide 18 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ expect fun randomUUID(): String プラットフォームで引数・返り値が同じ 処理が変化するメソッド (例) ランダムなUUIDをString型で取得 expect修飾子をつけてメソッド宣言

Slide 19

Slide 19 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ expect fun randomUUID(): String import java.util.UUID actual fun randomUUID() = UUID.randomUUID().toString() import platform.Foundation.NSUUID actual fun randomUUID() = NSUUID().UUIDString()

Slide 20

Slide 20 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ expect fun randomUUID(): String import platform.Foundation.NSUUID actual fun randomUUID() = NSUUID().UUIDString() import java.util.UUID actual fun randomUUID() = UUID.randomUUID().toString() actual修飾子をつけて実装を追加(Javaのメソッドを呼び出す) actual修飾子をつけて実装を追加(Swiftのメソッドを呼び出す)

Slide 21

Slide 21 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★特定のターゲットに対して細かく実装を分けることもできる 例えばターゲットがJVMの時は… 「Android向け」と「Desktop向け」の実装を分け それぞれ成果物を出力することができる! commonMain: 共通のロジック・データ構造 desktopMain: Desktop + JVM向けの実装 androidMain: Android + JVM向けの実装

Slide 22

Slide 22 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★特定のターゲットに対して細かく実装を分けることもできる 例えばターゲットがJVMの時は… 「Android向け」と「Desktop向け」の実装を分け それぞれ成果物を出力することができる! commonMain: 共通のロジック・データ構造 desktopMain: Desktop + JVM向けの実装 androidMain: Android + JVM向けの実装 Jetpack Composeもクラス・メソッドに片っ端からexpectつけて Desktop向けの実装を粛々と追加すれば Androidと同じI/FのフレームワークでDesktopアプリを(理論上は)実装できる!!!

Slide 23

Slide 23 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★いやいやそんな面倒なこと…

Slide 24

Slide 24 text

©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★いやいやそんな面倒なこと…をGoogleとJetBrainsがやってくれました🎉 Reference: https://github.com/JetBrains/compose-jb ※DesktopアプリのレンダリングにはSkiaを利用 ※Alpha版リリース: 2021.08.04

Slide 25

Slide 25 text

作っているアプリの実装紹介と 使い心地レビュー

Slide 26

Slide 26 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの紹介 WING Calculator(仮): subroh0508/WING-Calculator ➢ 某アイドル育成ゲームのダメージ計算機 実装済み • ダメージ値のリアルタイム計算 • 入力したステータスの保存機能 • レスポンシブ(?)対応 これから • ダークテーマ対応 • 入力画面の多機能化

Slide 27

Slide 27 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 ★利用ライブラリ • kotlinx.coroutines: 非同期処理 • kotlinx.datetime: 日付・時刻処理用 • SQLDelight: SQLiteクライアント • Koin: Dependency Injection用 • (Ktor: Httpクライアント) ここに挙げたライブラリは全てKotlin MPP対応! iOS向け/JS向けにも問題なく利用可能!

Slide 28

Slide 28 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 (例) ステータス値入力用のTextField @Composable fun IdolStatusForm( label: String, status: IdolStatus, onStateChange: (IdolStatus) -> Unit, ) { val (_, dispatch) = provideInputFormDispatcher() LaunchedEffect(status) { val (vocal, dance, visual, mental) = status.toInt() dispatch(vo = vocal, da = dance, vi = visual, me = mental) } Row { StatusTextField( status.vocal, label = "Vo", focusedColor = vocalColor, onChangeValue = { onStateChange(status.copy(vocal = it)) }, modifier = Modifier.weight(1F), ) …

Slide 29

Slide 29 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 (例) ステータス値入力用のTextField @Composable fun IdolStatusForm( label: String, status: IdolStatus, onStateChange: (IdolStatus) -> Unit, ) { val (_, dispatch) = provideInputFormDispatcher() LaunchedEffect(status) { val (vocal, dance, visual, mental) = status.toInt() dispatch(vo = vocal, da = dance, vi = visual, me = mental) } Row { StatusTextField( status.vocal, label = "Vo", focusedColor = vocalColor, onChangeValue = { onStateChange(status.copy(vocal = it)) }, modifier = Modifier.weight(1F), ) … @Composeアノテーションをつけた メソッド内にUIを実装していく

Slide 30

Slide 30 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 (例) ステータス値入力用のTextField @Composable fun IdolStatusForm( label: String, status: IdolStatus, onStateChange: (IdolStatus) -> Unit, ) { val (_, dispatch) = provideInputFormDispatcher() LaunchedEffect(status) { val (vocal, dance, visual, mental) = status.toInt() dispatch(vo = vocal, da = dance, vi = visual, me = mental) } Row { StatusTextField( status.vocal, label = "Vo", focusedColor = vocalColor, onChangeValue = { onStateChange(status.copy(vocal = it)) }, modifier = Modifier.weight(1F), ) … TextFieldの実体 コレ

Slide 31

Slide 31 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 (例) ステータス値入力用のTextField @Composable fun IdolStatusForm( label: String, status: IdolStatus, onStateChange: (IdolStatus) -> Unit, ) { val (_, dispatch) = provideInputFormDispatcher() LaunchedEffect(status) { val (vocal, dance, visual, mental) = status.toInt() dispatch(vo = vocal, da = dance, vi = visual, me = mental) } Row { StatusTextField( status.vocal, label = "Vo", focusedColor = vocalColor, onChangeValue = { onStateChange(status.copy(vocal = it)) }, modifier = Modifier.weight(1F), ) … 見た目はModifierで調整する ※Modifier.weight(1) →余白を残さないよう横幅いっぱいに広げる

Slide 32

Slide 32 text

©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 (例) ステータス値入力用のTextField @Composable fun IdolStatusForm( label: String, status: IdolStatus, onStateChange: (IdolStatus) -> Unit, ) { val (_, dispatch) = provideInputFormDispatcher() LaunchedEffect(status) { val (vocal, dance, visual, mental) = status.toInt() dispatch(vo = vocal, da = dance, vi = visual, me = mental) } Row { StatusTextField( status.vocal, label = "Vo", focusedColor = vocalColor, onChangeValue = { onStateChange(status.copy(vocal = it)) }, modifier = Modifier.weight(1F), ) … LaunchedEffect →引数に渡した値が変化したら コールバックを実行 →React HooksのuseEffectと同じ動き ※Jetpack ComposeとReact Hooksの対応 remember { mutableStateOf() } ⇔ useState CompositionLocal ⇔ React.Context

Slide 33

Slide 33 text

©Copyright 2021 BEARTAIL Inc. 使い心地レビュ- ★しあわせなところ ➢ 実装作業中の(脳の)コンテキストスイッチ切り替えが劇的に楽 • AndroidもDesktopも同じ文法 + クラス + メソッドが使える • UIの状態管理を1つのライブラリにまとめられるのはインパクト大 ➢ ReactとI/Fが似通っており、Webフロントの知見を輸入して扱える ➢ 表現力の高いKotlinを使ってUIを実装できる • 具体例は「余談: Kotlin + Jetpack Composeここすき実装」を参照

Slide 34

Slide 34 text

©Copyright 2021 BEARTAIL Inc. 使い心地レビュ- ★つらいところ ➢ たまにAndroidとDesktopでI/Fが違うコンポーネントがある • DropdownMenu等、commonMainからは「存在しない」扱いされる😥 ➢ UIを100%共通化しようとすればするほどつらみが増す • ダイアログ → Android: オーバレイ、Desktop: オーバレイ or 別ウィンドウ • 適度な妥協が必要 そもそもKotlin MPPがUI実装は無理に共通化しない思想だったり🙄 ➢ Desktop向けビルドはふとした時にバグバグしさを感じる • 日本語入力中にUI操作を受け付けなくなるバグが数ヶ月前まで残っていた

Slide 35

Slide 35 text

©Copyright 2021 BEARTAIL Inc. まとめ ➢ alpha版ではあるものの、Jetpack Compose for Desktopはそこそこ使える! ➢ Webエンジニア視点 • Reactのような書き味でAndroid・Desktopアプリ開発に入門できる • Android端末が手元になくてもKotlinでモダンなGUIアプリを体験できる ➢ Androidエンジニア視点 • ネイティブアプリを実装しつつ、Webフロント開発をうっすら体験できる JetBrainsがチュートリアルを用意しているので 興味が湧いたらIntelliJ IDEA CEをDLして今すぐチャレンジ!🚀 Getting Started with Compose Multiplatform: https://github.com/JetBrains/compose-jb/blob/master/tutorials/Getting_Started/README.md

Slide 36

Slide 36 text

©Copyright 2021 BEARTAIL Inc. Have a nice Kotlin!

Slide 37

Slide 37 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); }

Slide 38

Slide 38 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); } レイアウトの種類をenumで定義 →ONE_PANEL_MODAL = 1レーンレイアウト + Drawerはモーダル表示

Slide 39

Slide 39 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); } レイアウトを適用する横幅の範囲をプロパティとして持たせる

Slide 40

Slide 40 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); } KotlinのClass Delegationを使い enumにClosedRangeインターフェースを継承する

Slide 41

Slide 41 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); } ウィンドウ幅(maxWidth)からレイアウトを決定するメソッド fun detectLayout(maxWidth: Dp) = SimpleCalculatorPageConstraints.values().find { maxWidth in it } ?: SimpleCalculatorPageConstraints.ONE_PANEL_MODAL

Slide 42

Slide 42 text

©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange, ) : LayoutConstraints, ClosedRange by range { ONE_PANEL_MODAL(DrawerType.Modal, 0.dp..367.dp)), ONE_PANEL_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 368.dp..687.dp), TWO_PANELS_SHRINKABLE_MODAL(DrawerType.ShrinkableModal, 688.dp..895.dp), TWO_PANELS_SHRINKABLE_PERSIST(DrawerType.ShrinkablePersist, 896.dp..Dp.Infinity), TWO_PANELS_MODAL(DrawerType.Modal, -Dp.Infinity..(-1).dp); } fun detectLayout(maxWidth: Dp) = SimpleCalculatorPageConstraints.values().find { c -> maxWidth in c } ?: SimpleCalculatorPageConstraints.ONE_PANEL_MODAL ウィンドウ幅(maxWidth)からレイアウトを決定するメソッド ClosedRangeに対して利用可能なin演算で ウィンドウ幅が範囲内に含まれるか判定できる! Switch-Case文から👋できる!