Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

J( 'ー`)し「Jetpack Composeって、Desktopアプリも作れるのよ」

subroh_0508
September 30, 2021

J( 'ー`)し「Jetpack Composeって、Desktopアプリも作れるのよ」

社内LT会の発表資料です

subroh_0508

September 30, 2021
Tweet

More Decks by subroh_0508

Other Decks in Programming

Transcript

  1. ©Copyright 2021 BEARTAIL Inc. Time Hack Company 株式会社BEARTAIL 「Jetpack Composeって、Desktopアプリも作れるのよ」

    2021.10.01 BEARTAL社内LT会 Expense事業部 坂上晴信/にしこりさぶろ~ J( 'ー`)し
  2. ©Copyright 2021 BEARTAIL Inc. 話すこと ★Jetpack ComposeでDesktopアプリを作っている話 ➢ そもそもJetpack Composeとは?

    ➢ Jetpack Compose for Desktopの概要解説 ~Kotlin MPPを添えて~ ➢ 作っているアプリの実装紹介と使い心地レビュー ★Jetpack ComposeでDesktopアプリを実装する楽しさを伝えられれば…😊
  3. ©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML +

    View <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById<TextView>(R.id.message) textView.text = "Hello, World!" } } MainActivity.kt
  4. ©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML +

    View <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById<TextView>(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
  5. ©Copyright 2021 BEARTAIL Inc. そもそもJetpack Composeとは? ★ネイティブのAndroidアプリUIの実装に用いるKotlin製宣言型UIフレームワーク これまで: XML +

    View <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> activity_main.xml class MainActivity : AppCompatActivity(R.layout.activity_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val textView = findViewById<TextView>(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!") }
  6. ©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実装の デファクトになる(はず)
  7. ©Copyright 2021 BEARTAIL Inc. Time Hack Company 株式会社BEARTAIL J( 'ー`)し

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

    「Jetpack Composeって、Desktopアプリも作れるのよ」 2021.10.01 BEARTAL社内LT会 Expense事業部 坂上晴信/にしこりさぶろ~ 今日はDesktopアプリを作る話だったはず…🤔 なぜ はさっきからAndroidの話ばっかりしているんだ…?🤔
  9. ©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
  10. ©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ

    commonMain: 共通のロジック・データ構造 iosMain: iOS向けの実装 androidMain: Android向けの実装
  11. ©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") } プラットフォームで定義・処理が 変化しないデータ構造
  12. ©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 Kotlin MPPに対応したプロジェクトのディレクトリ

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

    expect fun randomUUID(): String プラットフォームで引数・返り値が同じ 処理が変化するメソッド (例) ランダムなUUIDをString型で取得 expect修飾子をつけてメソッド宣言
  14. ©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()
  15. ©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のメソッドを呼び出す)
  16. ©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★特定のターゲットに対して細かく実装を分けることもできる 例えばターゲットがJVMの時は…

    「Android向け」と「Desktop向け」の実装を分け それぞれ成果物を出力することができる! commonMain: 共通のロジック・データ構造 desktopMain: Desktop + JVM向けの実装 androidMain: Android + JVM向けの実装
  17. ©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アプリを(理論上は)実装できる!!!
  18. ©Copyright 2021 BEARTAIL Inc. Jetpack Compose for Desktopの概要解説 ★いやいやそんな面倒なこと…をGoogleとJetBrainsがやってくれました🎉 Reference:

    https://github.com/JetBrains/compose-jb ※DesktopアプリのレンダリングにはSkiaを利用 ※Alpha版リリース: 2021.08.04
  19. ©Copyright 2021 BEARTAIL Inc. 作っているアプリの紹介 WING Calculator(仮): subroh0508/WING-Calculator ➢ 某アイドル育成ゲームのダメージ計算機

    実装済み • ダメージ値のリアルタイム計算 • 入力したステータスの保存機能 • レスポンシブ(?)対応 これから • ダークテーマ対応 • 入力画面の多機能化
  20. ©Copyright 2021 BEARTAIL Inc. 作っているアプリの実装紹介 ★利用ライブラリ • kotlinx.coroutines: 非同期処理 •

    kotlinx.datetime: 日付・時刻処理用 • SQLDelight: SQLiteクライアント • Koin: Dependency Injection用 • (Ktor: Httpクライアント) ここに挙げたライブラリは全てKotlin MPP対応! iOS向け/JS向けにも問題なく利用可能!
  21. ©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), ) …
  22. ©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を実装していく
  23. ©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の実体 コレ
  24. ©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) →余白を残さないよう横幅いっぱいに広げる
  25. ©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
  26. ©Copyright 2021 BEARTAIL Inc. 使い心地レビュ- ★しあわせなところ ➢ 実装作業中の(脳の)コンテキストスイッチ切り替えが劇的に楽 • AndroidもDesktopも同じ文法

    + クラス + メソッドが使える • UIの状態管理を1つのライブラリにまとめられるのはインパクト大 ➢ ReactとI/Fが似通っており、Webフロントの知見を輸入して扱える ➢ 表現力の高いKotlinを使ってUIを実装できる • 具体例は「余談: Kotlin + Jetpack Composeここすき実装」を参照
  27. ©Copyright 2021 BEARTAIL Inc. 使い心地レビュ- ★つらいところ ➢ たまにAndroidとDesktopでI/Fが違うコンポーネントがある • DropdownMenu等、commonMainからは「存在しない」扱いされる😥

    ➢ UIを100%共通化しようとすればするほどつらみが増す • ダイアログ → Android: オーバレイ、Desktop: オーバレイ or 別ウィンドウ • 適度な妥協が必要 そもそもKotlin MPPがUI実装は無理に共通化しない思想だったり🙄 ➢ Desktop向けビルドはふとした時にバグバグしさを感じる • 日本語入力中にUI操作を受け付けなくなるバグが数ヶ月前まで残っていた
  28. ©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
  29. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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); }
  30. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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はモーダル表示
  31. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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); } レイアウトを適用する横幅の範囲をプロパティとして持たせる
  32. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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<Dp>インターフェースを継承する
  33. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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
  34. ©Copyright 2021 BEARTAIL Inc. 余談: Kotlin + Jetpack Composeここすき実装 ★ウィンドウ幅に合わせてレイアウトを変更するロジック

    actual enum class SimpleCalculatorPageConstraints( override val drawer: DrawerType, private val range: ClosedRange<Dp>, ) : LayoutConstraints, ClosedRange<Dp> 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文から👋できる!