Slide 1

Slide 1 text

Compose for Deskopで始める Android開発効率化ツールの作成 DroidKaigi 2022 Day01 Yusuke Katsuragawa / YUMEMI Inc. 1

Slide 2

Slide 2 text

自己紹介 2

Slide 3

Slide 3 text

自己紹介 - 桂川 祐介 - 株式会社ゆめみ - Androidエンジニア - Twitter : @kaleidot725 - GitHub : kaleidot725 3

Slide 4

Slide 4 text

アジェンダ 4

Slide 5

Slide 5 text

アジェンダ ● Compose for Desktopとは ● Compose for Desktopでなぜツールを作るのか ● Compose for Desktopでどのようにツールを作るか a. 作成したツール b. 開発環境 c. プロジェクトのセットアップ d. 依存関係のセットアップ e. 機能実装 ● Compose for Desktopでツールを使ってみて 5

Slide 6

Slide 6 text

Compose for Desktopとは 6

Slide 7

Slide 7 text

Compose for Desktopとは? ● Compose MultiplatformはJetBrainsが開発しているデスクトップ&Web向 けUIフレームワーク ● Compose MultiplatformはJetpack Composeをベースにして作られている ため、Jetpack Composeと同じ宣言型でUIを構築できる ● Compose Multiplatformのデスクトップ向けをCompose for Desktop、Web 向けをCompose for Webと呼ぶ ● Compose for DesktopはJetpack Composeと同じ書き方でデスクトップア プリを作れるUIフレームワークということ 7

Slide 8

Slide 8 text

Compose for Desktopの特徴 8

Slide 9

Slide 9 text

Compose for Desktopの特徴 複数のデスクトッププラットフォームに対応 Windows macOS Linux 9

Slide 10

Slide 10 text

Compose for Desktopの特徴 Jetpack Composeと互換性のあるUIコンポーネントの提供 Row Column OutlineButton Button 10

Slide 11

Slide 11 text

Compose for Desktopの特徴 デスクトップアプリ向けの機能の提供 メニューバー メニュー トレイ コンテキスト メニュー 通知 11

Slide 12

Slide 12 text

Compose for Desktopの特徴 Kotlin Multiplatformを利用して、Jetpack Composeとコード共有可能 Android デスクトップ ※ Jetpack Composeとコード共有したサンプルアプリ例 : https://bit.ly/3BgSV7g 12

Slide 13

Slide 13 text

Compose for Desktopで なぜツールを作ろうと思ったのか 13

Slide 14

Slide 14 text

Compose for Desktopでなぜツールを作ろうと思ったのか Androidエンジニアが日々の開発で繰り返しやる作業の手間を減らしたい ● 開発者オプションの設定変更 ● ネットワークエラー時の動作確認 ● ダークテーマ・ライトテーマでの動作確認 ● テキスト入力での動作確認 これらを補助するためのツールを作りたいが どのような言語・フレームワークで作るのがよいか? 14

Slide 15

Slide 15 text

Compose for Desktopでなぜツールを作ろうと思ったのか Compose for DesktopとAndroidエンジニアの相性は良さそう ● Jetpack Composeと同じ書き方でデスクトップアプリを開発できる - Jetpack Composeが扱える人の学習コストが低い ● Compose for Desktopを学ぶことが、Jetpack Composeで活かせる - Compose for Desktopで学んだことが無駄になりにくい ● Compose for Desktopでのツール開発が属人化しにくそう - Jetpack Composeを書ける人であればメンテできるため Compose for Desktopでツールを作ってみることに 15

Slide 16

Slide 16 text

Compose for Desktopでどのようにツールを作るか 16

Slide 17

Slide 17 text

作成したツール 17

Slide 18

Slide 18 text

作成したツール 端末選択 メニューリスト メニュー画面 ※ 作成したツールのリポジトリ : https://github.com/kaleidot725/AdbPad/tree/release/droidkaigi2022 18

Slide 19

Slide 19 text

作成したツール メニュー画面 コマンド ● 開発者オプション設定 ● ダークテーマON / OFF ● Wi-Fi ON / OFF ● データ通信 ON / OFF 19

Slide 20

Slide 20 text

作成したツール メニュー画面 テキスト入力 ● 入力テキストの保存/削除 ● 入力テキストの送信 20

Slide 21

Slide 21 text

作成したツール メニュー画面 スクリーンショット ● 現在の画面の撮影 ● テーマ毎に現在の画面の撮影 21

Slide 22

Slide 22 text

今回は作成したツールの実装を解説するのは難しいので どのように開発したかサンプルを用いて解説します 22

Slide 23

Slide 23 text

開発環境 23

Slide 24

Slide 24 text

開発環境 IntelliJ IDEA Community 2022.2 ● インストールするだけでCompose for Desktopで開発できる ● JetBrainsが開発しているのでAndroid Studioと操作感に違いはない 24

Slide 25

Slide 25 text

開発環境 Compose Multiplatform IDE Support ● Compose for Desktopでプレビューを可能にするIDEAプラグイン ● Stable版は1.1.1ですがIDEA 2022.2と互換性が無いのでAlpha版の1.2.0を利用 25

Slide 26

Slide 26 text

プロジェクトのセットアップ 26

Slide 27

Slide 27 text

プロジェクトのセットアップ 「New Project」からプロジェクトを新規作成を開始する 27

Slide 28

Slide 28 text

プロジェクトのセットアップ 「Compose Multiplatform」を選択し「Create」でプロジェクトを作成する デスクトップ向けなのでConfigurationをSingle platform, PlatformをDesktopにする Compose for DesktopはJDK11以上をサポートし ているのでJDK11以上を設定する必要がある 28

Slide 29

Slide 29 text

プロジェクトのセットアップ Gradleプロジェクト作成され、Gradleタスクからアプリが起動する Compose for Desktopのタスク一覧が表 示される。runを実行するとアプリケーション のビルド&実行ができる 29

Slide 30

Slide 30 text

依存関係のセットアップ 30

Slide 31

Slide 31 text

依存関係のセットアップ プロジェクトで利用する以下の依存関係を追加する 依存関係 バージョン 備考 Compose for Desktop v1.1.1 ● Compose for Desktopのv1.2.0はアルファ版なのでv1.1.1を利用す る Kotlin v1.6.10 ● Compose for Desktopに合わせv1.6.10を利用する Kotlin Coroutines v1.6.0 Kotlin Serialization v1.3.1 ● アプリの設定ファイル(json)の書き込みに利用する Adam v0.4.5 ● ADBのヘルパーライブラリ 31

Slide 32

Slide 32 text

依存関係のセットアップ 今回のプロジェクトでは以下の方針で依存関係を整理&管理する ● GradleプロジェクトのVersion Catalogを利用する ● Gradleファイルに依存関係の情報を記載し整理する Gradleファイル 記載内容 gradle.properties 依存関係のバージョン情報 setting.gradle.kts 依存関係のアーティファクト情報 build.gradle.kts アプリのモジュールで扱うアーティファクト情報 32

Slide 33

Slide 33 text

kotlin.code.style =official kotlin.version =1.6.10 kotlin.coroutines =1.6.0 kotlin.serialization =1.3.1 agp.version=4.2.2 compose.version =1.1.1 library.ktlint.plugin =10.3.0 library.adam=0.4.5 library.junit=5.9.0 アーティファクトのバージョンを定義する 依存関係のセットアップ gradle.properties 33

Slide 34

Slide 34 text

dependencyResolutionManagement { versionCatalogs { create("libs") { val adamVer = extra["library.adam" ] as String library( "adam", "com.malinskiy.adam:adam: $adamVer") val coroutinesVer = extra["kotlin.coroutines" ] as String library( "kotlin-coroutines" , "org.jetbrains.kotlinx:kotlinx-coroutines-core: $coroutinesVer") val serializationVer = extra["kotlin.serialization" ] as String library( "kotlin-serialization" , "org.jetbrains.kotlinx:kotlinx-serialization-json: $serializationVer ") val junitVer = extra["library.junit" ] as String library( "junit5", "org.junit.jupiter:junit-jupiter: $junitVer") } } } pluginManagement { repositories { … } plugins { kotlin("multiplatform" ).version(extra["kotlin.version" ] as String) id("org.jetbrains.compose" ).version(extra["compose.version" ] as String) kotlin("plugin.serialization" ).version(extra["kotlin.version" ] as String) id("org.jlleitschuh.gradle.ktlint" ).version(extra["library.ktlint.plugin" ] as String) } } 利用するプラグインを宣言する 依存関係のセットアップ setting.gradle.kts 34

Slide 35

Slide 35 text

dependencyResolutionManagement { versionCatalogs { create("libs") { val adamVer = extra["library.adam" ] as String library( "adam", "com.malinskiy.adam:adam: $adamVer") val coroutinesVer = extra["kotlin.coroutines" ] as String library( "kotlin-coroutines" , "org.jetbrains.kotlinx:kotlinx-coroutines-core: $coroutinesVer") val serializationVer = extra["kotlin.serialization" ] as String library( "kotlin-serialization" , "org.jetbrains.kotlinx:kotlinx-serialization-json: $serializationVer ") val junitVer = extra["library.junit" ] as String library( "junit5", "org.junit.jupiter:junit-jupiter: $junitVer") } } } pluginManagement { repositories { … } plugins { kotlin("multiplatform" ).version(extra["kotlin.version" ] as String) id("org.jetbrains.compose" ).version(extra["compose.version" ] as String) kotlin("plugin.serialization" ).version(extra["kotlin.version" ] as String) id("org.jlleitschuh.gradle.ktlint" ).version(extra["library.ktlint.plugin" ] as String) } } 依存関係のセットアップ setting.gradle.kts Versioni Catalogを使ってライブラリのアーティファクトを宣言する 35

Slide 36

Slide 36 text

kotlin { sourceSets { val jvmMain by getting { dependencies { implementation( compose.desktop.currentOs) implementation( compose.material) implementation( compose.materialIconsExtended ) implementation( libs.adam) implementation( libs.kotlin.coroutines) implementation( libs.kotlin.serialization) } } } } plugins { kotlin("multiplatform" ) id("org.jetbrains.compose" ) kotlin("plugin.serialization" ) id("org.jlleitschuh.gradle.ktlint" ) } setting.gradle.ktsに定義したアーティファクトを参照する 依存関係のセットアップ build.gradle.kts 36

Slide 37

Slide 37 text

kotlin { sourceSets { val jvmMain by getting { dependencies { implementation( compose.desktop.currentOs) implementation( compose.material) implementation( compose.materialIconsExtended ) implementation( libs.adam) implementation( libs.kotlin.coroutines) implementation( libs.kotlin.serialization) } } } } plugins { kotlin("multiplatform" ) id("org.jetbrains.compose" ) kotlin("plugin.serialization" ) id("org.jlleitschuh.gradle.ktlint" ) } setting.gradle.ktsに定義したアーティファクトを参照する 依存関係のセットアップ build.gradle.kts 37

Slide 38

Slide 38 text

kotlin { sourceSets { val jvmMain by getting { dependencies { implementation( compose.desktop.currentOs) implementation( compose.material) implementation( compose.materialIconsExtended ) implementation( libs.adam) implementation( libs.kotlin.coroutines) implementation( libs.kotlin.serialization) } } } } plugins { kotlin("multiplatform" ) id("org.jetbrains.compose" ) kotlin("plugin.serialization" ) id("org.jlleitschuh.gradle.ktlint" ) } 依存関係のセットアップ build.gradle.kts あらかじめCompose Pluginが予め依存関係の定義 を用意してくれています。このように記述すると local.propertiesに定義したバージョンで依存関係を セットアップされる。 ※ Compose for Desktopの依存関係の定義 : https://bit.ly/3L2wnKN 38

Slide 39

Slide 39 text

機能実装 - ADBと連携する 39

Slide 40

Slide 40 text

機能実装 - ADBと連携する ADBとは ● Android Debug Bridge(adb)は、デバイスと通信するための多用途のコマンドライ ン ツール ● ADBのshellコマンドを利用することでAndroid OSのシェルを通して端末を操作でき る ● その他にもアプリのインストールやスクリーンキャプチャなどの操作が実行できる 40

Slide 41

Slide 41 text

機能実装 - ADBと連携する Adamとは ● AdamはKotlinで作られたADBのヘルパーライブラリ ● AdamからADBを操作し、端末一覧取得・シェル実行・スクリーンショット取得などが できる ● Kotlin Coroutinesにも対応しているので簡単に非同期処理を実行できる ● 今回はこのAdamというライブラリを利用しADBを実行します 41

Slide 42

Slide 42 text

機能実装 - ADBと連携する Adamの使い方 StartAdbInteractor().execute() val adb = AndroidDebugBridgeClientFactory().build() val output = adb.execute( request = ShellCommandRequest( "echo hello"), serial = "emulator-5554" ) ①ADBサーバーを開始する 42

Slide 43

Slide 43 text

機能実装 - ADBと連携する Adamの使い方 StartAdbInteractor().execute() val adb = AndroidDebugBridgeClientFactory().build() val output = adb.execute( request = ShellCommandRequest( "echo hello"), serial = "emulator-5554" ) ②ADBクライアントを作成する 43

Slide 44

Slide 44 text

機能実装 - ADBと連携する Adamの使い方 StartAdbInteractor().execute() val adb = AndroidDebugBridgeClientFactory().build() val output = adb.execute( request = ShellCommandRequest( "echo hello"), serial = "emulator-5554" ) ③ADBクライアントからリクエストを送信する 44

Slide 45

Slide 45 text

機能実装 - 端末一覧をADBから取得し表示する 端末リスト表示 ※ 端末リスト表示のサンプルコード : https://github.com/kaleidot725/AdbPad/tree/release/droidkaigi2022-sample1 45

Slide 46

Slide 46 text

機能実装 - 端末一覧をADBから取得し表示する レイヤ 役割 説明 Model Data ● 機能を実現するデータ UseCase ● 機能を実現するための処理 View UI Component ● UIコンポーネントの定義 State ● UIコンポーネントの状態 StateHolder ● UI ComponentとStateをつなぎ合わせる ● UI ComponentにStateを通知する ● UI ComponentのEventに反応して処理する ViewとModelの2つのレイヤに大きく分けて実装を進める 46

Slide 47

Slide 47 text

data class Device(val serial: String, val state: DeviceState) 端末情報についてはAdamで定義されているデータクラスをそのまま使う 機能実装 - 端末一覧をADBから取得し表示する Data 47

Slide 48

Slide 48 text

機能実装 - 端末一覧をADBから取得し表示する class GetDevicesFlowUseCase { operator fun invoke(coroutineScope: CoroutineScope): Flow> { val adbClient = AndroidDebugBridgeClientFactory().build() val receiveChannel = adbClient.execute( request = AsyncDeviceMonitorRequest() , scope = coroutineScope ) return receiveChannel. receiveAsFlow() } } UseCase 48 端末一覧を購読するためのFlowを作成する

Slide 49

Slide 49 text

機能実装 - 端末一覧をADBから取得し表示する class GetDevicesFlowUseCase { operator fun invoke(coroutineScope: CoroutineScope): Flow> { val adbClient = AndroidDebugBridgeClientFactory().build() val receiveChannel = adbClient.execute( request = AsyncDeviceMonitorRequest() , scope = coroutineScope ) return receiveChannel. receiveAsFlow() } } UseCase AdbClientを生成する 49

Slide 50

Slide 50 text

機能実装 - 端末一覧をADBから取得し表示する class GetDevicesFlowUseCase { operator fun invoke(coroutineScope: CoroutineScope): Flow> { val adbClient = AndroidDebugBridgeClientFactory().build() val receiveChannel = adbClient.execute( request = AsyncDeviceMonitorRequest() , scope = coroutineScope ) return receiveChannel. receiveAsFlow() } } UseCase AsyncDeviceMonitorRequestで端末一覧を通知してくれるChannelを作成する 50

Slide 51

Slide 51 text

機能実装 - 端末一覧をADBから取得し表示する class GetDevicesFlowUseCase { operator fun invoke(coroutineScope: CoroutineScope): Flow> { val adbClient = AndroidDebugBridgeClientFactory().build() val receiveChannel = adbClient.execute( request = AsyncDeviceMonitorRequest() , scope = coroutineScope ) return receiveChannel. receiveAsFlow() } } UseCase ChannelよりもFlowのほうが扱いやすいのでreceiveAsFlowで変換する 51

Slide 52

Slide 52 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { // 選択中のデバイス表示 Box(...) { ... } // デバイス一覧表示 DropdownMenu(...) { ... } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 選択している端末、接続している端末一覧を表示できるようにする 52

Slide 53

Slide 53 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box( modifier = Modifier.clickable { if (!expanded && devices. isNotEmpty()) expanded = true } ) { Text(...) Icon(...) } DropdownMenu(...) { ... } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 53 選択された端末をクリックで DropdownMenuを表示できるようにする

Slide 54

Slide 54 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box(...) { Text( text = selectedDevice?. serial ?: StringRes.NOT_FOUND_DEVICE , modifier = Modifier.fillMaxWidth() ) Icon(...) } DropdownMenu(...) { ... } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 54 選択された端末名を表示する

Slide 55

Slide 55 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box(...) { Text(...) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = "Device DropDown Icon" , modifier = Modifier.align(Alignment.CenterEnd) ) } DropdownMenu(...) { ... } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 55 選択できることがわかるようにアイコンを表示する

Slide 56

Slide 56 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box(...) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier.fillMaxWidth() ) { ... } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 56 接続している端末リストを表示するためにDropdownMenuを定義する。 expandedを渡して開閉状態を同期してやる。

Slide 57

Slide 57 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box(...) DropdownMenu(...) { devices.forEach { device -> DropdownMenuItem(...) { Text(text = device.serial) } } } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 57 接続している端末の名称を表示する

Slide 58

Slide 58 text

@Composable fun DropDownDeviceMenu ( selectedDevice: Device? , devices: List , onSelectDevice: (Device) -> Unit , modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } Box(modifier) { Box(...) DropdownMenu(...) { devices.forEach { device -> DropdownMenuItem( onClick = { onSelectDevice(device) expanded = false } ) {...} } } } } 機能実装 - 端末一覧をADBから取得し表示する UI Component 58 接続している端末リストを選択する処理を実装する

Slide 59

Slide 59 text

data class MainState( val devices: List = emptyList(), val selectedDevice : Device? = null, ) リストに渡すための情報をまとめる 機能実装 - 端末一覧をADBから取得し表示する State 59

Slide 60

Slide 60 text

機能実装 - 端末一覧をADBから取得し表示する StateHolder class MainStateHolder( val getDevicesFlow : GetDevicesFlowUseCase = GetDevicesFlowUseCase() ) { ︙ val state: StateFlow = combine(devices, selectedDevice ) { devices, selected -> MainState(devices = devices, selectedDevice = selected) }.stateIn(coroutineScope , SharingStarted.WhileSubscribed() , MainState()) ︙ fun setup() {...} fun selectDevice(device: Device) { ... } fun dispose() { ... } } Stateの変化をUI Componentに通知する、また画面表示時、コマンド実行時、画面非表示時 に処理を実行する、というのをできるようにするためのStateHolderを作成する 60

Slide 61

Slide 61 text

機能実装 - 端末一覧をADBから取得し表示する StateHolder class MainStateHolder( val getDevicesFlow : GetDevicesFlowUseCase = GetDevicesFlowUseCase() ) { private val coroutineContext get() = SupervisorJob() + Dispatchers. Main + Dispatchers. IO private val coroutineScope : CoroutineScope = CoroutineScope(coroutineContext ) private val devices: MutableStateFlow> = MutableStateFlow(emptyList()) private val selectedDevice : MutableStateFlow = MutableStateFlow(null) val state: StateFlow = combine(devices, selectedDevice ) { devices, selected -> MainState(devices = devices, selectedDevice = selected) }.stateIn(coroutineScope , SharingStarted.WhileSubscribed() , MainState()) fun setup() {...} fun selectDevice(device: Device) { ... } fun dispose() { ... } } StateをUI Componentに通知する仕組みを実装する。 端末一覧と選択端末が更新されたらStateを再生成して通知する 61

Slide 62

Slide 62 text

機能実装 - 端末一覧をADBから取得し表示する StateHolder class MainStateHolder( val getDevicesFlow : GetDevicesFlowUseCase = GetDevicesFlowUseCase() ) { ︙ private val devices: MutableStateFlow> = MutableStateFlow(emptyList()) private val selectedDevice : MutableStateFlow = MutableStateFlow(null) ︙ fun setup() { coroutineScope .launch { StartAdbInteractor().execute() getAndroidDevicesFlowUseCase (coroutineScope ).collect { devices.value = it val hasNotValue = !it.contains(selectedDevice.value) if (hasNotValue) selectedDevice.value = it.firstOrNull() } } } fun selectDevice(device: Device) { ... } fun dispose() { ... } } 画面表示時の処理を実装する。 adbサーバーを起動しFlowをcollectして端末一覧を更新する 62

Slide 63

Slide 63 text

機能実装 - 端末一覧をADBから取得し表示する StateHolder class MainStateHolder( val getDevicesFlow : GetDevicesFlowUseCase = GetDevicesFlowUseCase() ) { ︙   private val selectedDevice : MutableStateFlow = MutableStateFlow(null) ︙ fun setup() { ... } fun selectDevice(device: Device) { selectedDevice .value = device } fun dispose() { ... } } 選択時にデバイス選択状況を更新する 63

Slide 64

Slide 64 text

機能実装 - 端末一覧をADBから取得し表示する StateHolder class MainStateHolder( val getDevicesFlow : GetDevicesFlowUseCase = GetDevicesFlowUseCase() ) { ︙ private val coroutineContext get() = SupervisorJob() + Dispatchers. Main + Dispatchers. IO private val coroutineScope : CoroutineScope = CoroutineScope(coroutineContext ) ︙ fun setup() {...} fun selectDevice(device: Device) { ... } fun dispose() { coroutineScope .cancel() } } 画面非表示時に実行されている コルーチンをキャンセルする 64

Slide 65

Slide 65 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() DropDownDeviceMenu( devices = state.devices, selectedDevice = state.selectedDevice , onSelectDevice = { stateHolder.selectDevice( it) }, modifier = Modifier.wrapContentSize() ) } } 機能実装 - 端末一覧をADBから取得し表示する main Compose for Desktopのメイン関数で、今まで作成したクラスを使ってアプリを組み立てる 65

Slide 66

Slide 66 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() DropDownDeviceMenu( devices = state.devices, selectedDevice = state.selectedDevice , onSelectDevice = { stateHolder.selectDevice( it) }, modifier = Modifier.wrapContentSize() ) } } 機能実装 - 端末一覧をADBから取得し表示する main 画面表示時にsetupを実行する また画面非表示にdisposeを実行する 66

Slide 67

Slide 67 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() DropDownDeviceMenu( devices = state.devices, selectedDevice = state.selectedDevice , onSelectDevice = { stateHolder.selectDevice( it) }, modifier = Modifier.wrapContentSize() ) } } 機能実装 - 端末一覧をADBから取得し表示する main stateをcollectしてDropDownDeviceMenuに渡す、 DropDownDeviceMenuからイベントをstateHolderに伝 える 67

Slide 68

Slide 68 text

ADBと連携する - 端末一覧を取得し表示する 68

Slide 69

Slide 69 text

機能実装 - ADBでコマンドを実行し端末を操作する 69

Slide 70

Slide 70 text

機能実装 - ADBでコマンドを実行し端末を操作する コマンド実行画面 ※ コマンド実行画面のサンプルコード : https://github.com/kaleidot725/AdbPad/tree/release/droidkaigi2022-sample2 70

Slide 71

Slide 71 text

機能実装 - ADBでコマンドを実行し端末を操作する 分類 役割 説明 Model Data ● 機能を実現するデータ UseCase ● 機能を実現するための処理 View UI Component ● UIコンポーネントの定義 State ● UIコンポーネントの状態 StateHolder ● UI ComponentとStateをつなぎ合わせる 同じくこの構造を用いて作成を進める 71

Slide 72

Slide 72 text

sealed class Command( val title: String, val details: String, val requests: List ) { object DarkThemeOn : Command( "ダークテーマON", "端末のダークテーマ設定を ONにします", listOf(ShellCommandRequest( "cmd uimode night yes" )) ) object WifiAndDataOn : Command( "Wi-Fi&データ通信 ON", "端末のWi-Fi設定とデータ通信設定の両方を ONにします", listOf( ShellCommandRequest( "svc wifi enable" ), ShellCommandRequest( "svc data enable" ) ) ) ︙ } 機能実装 - ADBでコマンドを実行し端末を操作する Data コマンドのタイトルと説明を定義しておく。 またこのコマンドで実行したいAdamのリ クエストにして定義しておく。 72

Slide 73

Slide 73 text

sealed class Command( val title: String, val details: String, val requests: List ) { object DarkThemeOn : Command( "ダークテーマON", "端末のダークテーマ設定を ONにします", listOf(ShellCommandRequest( "cmd uimode night yes" )) ) object WifiAndDataOn : Command( "Wi-Fi&データ通信 ON", "端末のWi-Fi設定とデータ通信設定の両方を ONにします", listOf( ShellCommandRequest( "svc wifi enable" ), ShellCommandRequest( "svc data enable" ) ) ) ︙ } 機能実装 - ADBでコマンドを実行し端末を操作する Data ダークテーマONにする ためのコマンド 73

Slide 74

Slide 74 text

sealed class Command( val title: String, val details: String, val requests: List ) { object DarkThemeOn : Command( "ダークテーマON", "端末のダークテーマ設定を ONにします", listOf(ShellCommandRequest( "cmd uimode night yes" )) ) object WifiAndDataOn : Command( "Wi-Fi&データ通信 ON", "端末のWi-Fi設定とデータ通信設定の両方を ONにします", listOf( ShellCommandRequest( "svc wifi enable" ), ShellCommandRequest( "svc data enable" ) ) ) ︙ } 機能実装 - ADBでコマンドを実行し端末を操作する Data もし複数リクエストがある場合 にこのように2つのリクエスト を定義する 74

Slide 75

Slide 75 text

機能実装 - ADBでコマンドを実行し端末を操作する class GetCommandListUseCase { operator fun invoke(): List { return listOf(Command.DarkThemeOn ,Command.WifiAndDataOn) } } UseCase 定義したコマンド情報をリストで返す 75

Slide 76

Slide 76 text

機能実装 - ADBでコマンドを実行し端末を操作する class ExecuteCommandUseCase { suspend operator fun invoke(serial: String? , command: Command): Boolean { return withContext(Dispatchers. IO) { val adbClient = AndroidDebugBridgeClientFactory().build() command. requests.forEach { request -> val result = adbClient.execute(request , serial) if (result.exitCode != 0) return@withContext false } return@withContext true } } } UseCase 定義したコマンドをAdamで実行する 76

Slide 77

Slide 77 text

機能実装 - ADBでコマンドを実行し端末を操作する class ExecuteCommandUseCase { suspend operator fun invoke(serial: String? , command: Command): Boolean { return withContext(Dispatchers. IO) { val adbClient = AndroidDebugBridgeClientFactory().build() command. requests.forEach { request -> val result = adbClient.execute(request , serial) if (result.exitCode != 0) return@withContext false } return@withContext true } } } UseCase AdbClientを生成する 77

Slide 78

Slide 78 text

機能実装 - ADBでコマンドを実行し端末を操作する class ExecuteCommandUseCase { suspend operator fun invoke(serial: String? , command: Command): Boolean { return withContext(Dispatchers. IO) { val adbClient = AndroidDebugBridgeClientFactory().build() command. requests.forEach { request -> val result = adbClient.execute(request , serial) if (result.exitCode != 0) return@withContext false } return@withContext true } } } UseCase リクエストを順番に実行し、途中で失敗したらfalse、 そうでなければtrueを返すようにする 78

Slide 79

Slide 79 text

機能実装 - ADBでコマンドを実行し端末を操作する UI Component @Composable fun CommandList( commands: List , onExecute: (Command) -> Unit , modifier: Modifier = Modifier ) { Box(modifier = modifier) { Column(...) { commands.forEach { command -> Card(...) { ... } } } } } 受け取ったコマンドをリストで表示する、 また選択したコマンドを実行できるようにする 79

Slide 80

Slide 80 text

機能実装 - ADBでコマンドを実行し端末を操作する UI Component @Composable fun CommandList( commands: List , onExecute: (Command) -> Unit , modifier: Modifier = Modifier ) { Box(modifier = modifier) { Column(...) { commands.forEach { command -> Card(...) { ... } } } } } ColumnとCardを利用してコマンドリストを作成する 80

Slide 81

Slide 81 text

@Composable fun CommandList( commands: List , onExecute: (Command) -> Unit , modifier: Modifier = Modifier ) { Box(modifier = modifier) { Column(...) { commands.forEach { command -> Card(...) { Row(...) { Column(modifier = Modifier.weight(0.9f, true)) { Text(text = command.title) Text(text = command.details) } Button(...) { ... } } } } } } } 機能実装 - ADBでコマンドを実行し端末を操作する UI Component コマンドのタイトルと説明を並べて表示する 81

Slide 82

Slide 82 text

@Composable fun CommandList( commands: List , onExecute: (Command) -> Unit , modifier: Modifier = Modifier ) { Box(modifier = modifier) { Column(...) { commands.forEach { command -> Card(...) { Row(...) { Column(...) { ... } Button(onClick = { onExecute(command) }) { Text(text = StringRes.EXECUTE) } } } } } } } 機能実装 - ADBでコマンドを実行し端末を操作する UI Component StateHolderにコマンド実行を依頼できるようにする 82

Slide 83

Slide 83 text

data class MainState( val commands: List = emptyList(), ) 機能実装 - ADBでコマンドを実行し端末を操作する State リストに渡すための情報をまとめる 83

Slide 84

Slide 84 text

機能実装 - ADBでコマンドを実行し端末を操作する StateHolder class MainStateHolder( val getCommandListUseCase : GetCommandListUseCase = GetCommandListUseCase() , val executeCommandUseCase : ExecuteCommandUseCase = ExecuteCommandUseCase() , ) { ︙ val state: StateFlow = commands.map { MainState(commands = it) }.stateIn(coroutineScope , SharingStarted.WhileSubscribed() , MainState()) ︙ fun setup() {...} fun executeCommand (command: Command) { ... } fun dispose() { ... } } Stateの変化をUI Componentに通知する、また画面表示時、コマンド実行時、画面非表示時 に処理を実行する、というのをできるようにするためのStateHolderを作成する 84

Slide 85

Slide 85 text

機能実装 - ADBでコマンドを実行し端末を操作する StateHolder class MainStateHolder( val getCommandListUseCase : GetCommandListUseCase = GetCommandListUseCase() , val executeCommandUseCase : ExecuteCommandUseCase = ExecuteCommandUseCase() , ) { private val coroutineContext get() = SupervisorJob() + Dispatchers. Main + Dispatchers. IO private val coroutineScope : CoroutineScope = CoroutineScope(coroutineContext ) private val commands: MutableStateFlow> = MutableStateFlow(emptyList()) val state: StateFlow = commands.map { MainState(commands = it) }.stateIn(coroutineScope , SharingStarted.WhileSubscribed() , MainState()) fun setup() {...} fun executeCommand (command: Command) { ... } fun dispose() { ... } } StateをUI Componentに通知する仕組みを実装する。 コマンド一覧が更新されたらStateを再生成して通知する 85

Slide 86

Slide 86 text

機能実装 - ADBでコマンドを実行し端末を操作する StateHolder class MainStateHolder( val getCommandListUseCase : GetCommandListUseCase = GetCommandListUseCase() , val executeCommandUseCase : ExecuteCommandUseCase = ExecuteCommandUseCase() , ) { ︙ private val commands: MutableStateFlow> = MutableStateFlow(emptyList()) ︙ fun setup() { coroutineScope .launch { StartAdbInteractor().execute() } commands.value = getCommandListUseCase () } fun executeCommand (command: Command) { ... } fun dispose() { ... } } 画面表示時の処理を実装する。 コマンド実行に備えて、 adbサーバーを起動する。 そのあとコマンドの一覧を更新する。 86

Slide 87

Slide 87 text

機能実装 - ADBでコマンドを実行し端末を操作する StateHolder class MainStateHolder( val getCommandListUseCase : GetCommandListUseCase = GetCommandListUseCase() , val executeCommandUseCase : ExecuteCommandUseCase = ExecuteCommandUseCase() , ) { ︙ fun setup() { ... } fun executeCommand (command: Command) { coroutineScope .launch { executeCommandUseCase ("XXX", command) } } fun dispose() { ... } } 選択したコマンドを実行する。今回 はシリアル番号は仮で固定値を入 れている。 87

Slide 88

Slide 88 text

機能実装 - ADBでコマンドを実行し端末を操作する StateHolder class MainStateHolder( val getCommandListUseCase : GetCommandListUseCase = GetCommandListUseCase() , val executeCommandUseCase : ExecuteCommandUseCase = ExecuteCommandUseCase() , ) { ︙ fun setup() { ... } fun executeCommand (command: Command) { ... } fun dispose() { coroutineScope .cancel() } } 画面非表示時に実行されている コルーチンをキャンセルする 88

Slide 89

Slide 89 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() CommandList( commands = state.commands, onExecute = { stateHolder.executeCommand( it) }, modifier = Modifier.fillMaxSize() ) } } 機能実装 - ADBでコマンドを実行し端末を操作する main 89 Compose for Desktopのメイン関数で、今まで作成したクラスを使ってアプリを組み立てる

Slide 90

Slide 90 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() CommandList( commands = state.commands, onExecute = { stateHolder.executeCommand( it) }, modifier = Modifier.fillMaxSize() ) } } 機能実装 - ADBでコマンドを実行し端末を操作する main 画面表示時にsetupを実行する。 また画面非表示にdisposeを実行 する。 90

Slide 91

Slide 91 text

fun main() = application { Window(title = StringRes.WINDOW_TITLE, onCloseRequest = ::exitApplication) { val stateHolder by remember { mutableStateOf(MainStateHolder()) } DisposableEffect(stateHolder) { stateHolder.setup() onDispose { stateHolder.dispose() } } val state by stateHolder.state.collectAsState() CommandList( commands = state.commands, onExecute = { stateHolder.executeCommand( it) }, modifier = Modifier.fillMaxSize() ) } } 機能実装 - ADBでコマンドを実行し端末を操作する main stateをcollectしてCommandListに渡す、 CommandListからイベントをstateHolderに伝 える 91

Slide 92

Slide 92 text

ADBと連携する - ADBでコマンドを実行し端末を操作する 92

Slide 93

Slide 93 text

Demo 93

Slide 94

Slide 94 text

Compose for Desktopを使ってみて 94

Slide 95

Slide 95 text

Compose for Desktopを使ってみて ● Jetpack Composeと同じ仕組みでUIを作れる - Jetpack Composeが使えればCompose for Destkopも簡単に使える ● Androidのアプリ開発で得た知識を流用しやすい - IDE - IntelliJ IDEAとAndroid Studioは大きな違いはなく使いやすい - Coroutines・Flow - CoroutinesとFlowを使えるので非同期処理などの実装がかなり楽 - アーキテクチャ - Androidアーキテクチャガイドを参考にできる 良かったところ 95

Slide 96

Slide 96 text

● Compose for DesktopのリリースはJetpack Composeの後追い - Jetpack Composeの最新版で使えるようになってもCompose for Desktopで 使えない ● プレビュー機能が弱い - プレビューの実行&更新は手動なので少々手間がかかる ● ドキュメントが少なめ - 困ったらGitHubのIssueで解決策を調べることが多い Compose for Desktopを使ってみて 辛かったところ 96

Slide 97

Slide 97 text

まとめ ● Compose for Desktop × Adamで気軽にadbを利用したツールを開発できる ● Androidのアプリ開発で得た知識を流用できるので、学び直しが少なく効率的に開 発ができる ● Jetpack Composeと比べると開発体験は少し悪いところがある - リリース・ドキュメント・プレビューなど - Compose for Desktop 1.2.0の開発が続いているので改善に期待 97

Slide 98

Slide 98 text

おわり 98