Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose for Deskopで始めるAndroid開発効率化ツールの作成

Compose for Deskopで始めるAndroid開発効率化ツールの作成

DroidKaigi2022で発表しました。Compose for Deskopで始めるAndroid開発効率化ツールの作成の資料になります。

https://droidkaigi.jp/2022/timetable/364461

Yusuke Katsuragawa

October 05, 2022
Tweet

More Decks by Yusuke Katsuragawa

Other Decks in Programming

Transcript

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

    View Slide

  2. 自己紹介
    2

    View Slide

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

    View Slide

  4. アジェンダ
    4

    View Slide

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

    View Slide

  6. Compose for Desktopとは
    6

    View Slide

  7. 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

    View Slide

  8. Compose for Desktopの特徴
    8

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. 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

    View Slide

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

    View Slide

  17. 作成したツール
    17

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. 開発環境
    23

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. 依存関係のセットアップ
    プロジェクトで利用する以下の依存関係を追加する
    依存関係 バージョン 備考
    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

    View Slide

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

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. 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

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

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

    View Slide

  40. 機能実装 - ADBと連携する
    ADBとは
    ● Android Debug Bridge(adb)は、デバイスと通信するための多用途のコマンドライ
    ン ツール
    ● ADBのshellコマンドを利用することでAndroid OSのシェルを通して端末を操作でき

    ● その他にもアプリのインストールやスクリーンキャプチャなどの操作が実行できる
    40

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. 機能実装 - 端末一覧を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を作成する

    View Slide

  49. 機能実装 - 端末一覧を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

    View Slide

  50. 機能実装 - 端末一覧を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

    View Slide

  51. 機能実装 - 端末一覧を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

    View Slide

  52. @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

    View Slide

  53. @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を表示できるようにする

    View Slide

  54. @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
    選択された端末名を表示する

    View Slide

  55. @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
    選択できることがわかるようにアイコンを表示する

    View Slide

  56. @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を渡して開閉状態を同期してやる。

    View Slide

  57. @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
    接続している端末の名称を表示する

    View Slide

  58. @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
    接続している端末リストを選択する処理を実装する

    View Slide

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

    View Slide

  60. 機能実装 - 端末一覧を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

    View Slide

  61. 機能実装 - 端末一覧を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

    View Slide

  62. 機能実装 - 端末一覧を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

    View Slide

  63. 機能実装 - 端末一覧を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

    View Slide

  64. 機能実装 - 端末一覧を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

    View Slide

  65. 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

    View Slide

  66. 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

    View Slide

  67. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  72. 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

    View Slide

  73. 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

    View Slide

  74. 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

    View Slide

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

    View Slide

  76. 機能実装 - 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

    View Slide

  77. 機能実装 - 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

    View Slide

  78. 機能実装 - 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

    View Slide

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

    View Slide

  80. 機能実装 - 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

    View Slide

  81. @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

    View Slide

  82. @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

    View Slide

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

    View Slide

  84. 機能実装 - 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

    View Slide

  85. 機能実装 - 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

    View Slide

  86. 機能実装 - 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

    View Slide

  87. 機能実装 - 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

    View Slide

  88. 機能実装 - ADBでコマンドを実行し端末を操作する
    StateHolder
    class MainStateHolder(
    val getCommandListUseCase
    : GetCommandListUseCase = GetCommandListUseCase()
    ,
    val executeCommandUseCase
    : ExecuteCommandUseCase = ExecuteCommandUseCase()
    ,
    ) {

    fun setup() { ... }
    fun executeCommand
    (command: Command) { ... }
    fun dispose() {
    coroutineScope
    .cancel()
    }
    }
    画面非表示時に実行されている
    コルーチンをキャンセルする
    88

    View Slide

  89. 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のメイン関数で、今まで作成したクラスを使ってアプリを組み立てる

    View Slide

  90. 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

    View Slide

  91. 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

    View Slide

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

    View Slide

  93. Demo
    93

    View Slide

  94. Compose for Desktopを使ってみて
    94

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  98. おわり
    98

    View Slide