Android
 Computer >> コンピューター >  >> システム >> Android

【Jetpack Compose入門】Sealed Classとラムダ式でUIイベントをスマートに処理する方法

本記事では、Jetpack ComposeにおけるUIイベントの処理方法について、実践的かつ簡潔に解説します。

従来のViewシステムでは、OnClickListenerなどのインターフェースを使ってイベントを処理していました。一方、ComposeではKotlinのSealed Class(シールドクラス)関数型ラムダ式を最大限に活用できます。

コンポーザブル(composable)が何かわからないという方は、まず基礎を解説した記事を読んでおくことをおすすめします。

本記事の内容は3分以内の動画にもまとめられています。

Sealed ClassでUIイベントをモデリングする方法

まずは、UIイベントとは何か、そしてそれをSealed Classでどうモデル化するのかを学びましょう。

筆者は以前、JavaとKotlin(旧Viewシステム)を使った同じ手法について記事にしていますので、ここでは要点のみを簡単に説明します。

手順

UIの各画面・サブ画面ごとに、次の問いを自分に投げかけてください。「ユーザーはこの画面と、どんな方法でインタラクションできるだろうか?」

例として、筆者がComposeだけで完全に構築した初めてのアプリ「Graph Sudoku」を挙げます。

【Jetpack Compose入門】Sealed Classとラムダ式でUIイベントをスマートに処理する方法
数独Androidアプリのスクリーンショット

この画面のUI操作を表すために筆者が使用しているSealed Classは以下の通りです。

sealed class ActiveGameEvent {
    data class OnInput(val input: Int) : ActiveGameEvent()
    data class OnTileFocused(val x: Int, 
    val y: Int) : ActiveGameEvent()
    object OnNewGameClicked : ActiveGameEvent()
    object OnStart : ActiveGameEvent()
    object OnStop : ActiveGameEvent()
}

簡単に説明すると:

  • OnInput:ユーザーが入力ボタン(0、1、2、3、4など)をタップしたことを表します
  • OnTileFocused:ユーザーがマス(アンバー色でハイライトされたセルなど)を選択したことを表します
  • OnNewGameClicked:名前の通り、「新規ゲーム」ボタンがクリックされたことを表します
  • OnStart / OnStop:ライフサイクルイベントです。コンポーザブル自身は関知しませんが、コンポーザブルのコンテナとして機能するActivityで使用されます

Sealed Classを定義できたら、あとは単一のイベントハンドラ関数でさまざまなイベントを処理できます。ただし、場合によっては複数のハンドラ関数に分割した方が理にかなうこともあります。このアプローチはプロジェクト固有の要件に合わせて柔軟に調整することが重要です。

ソフトウェアアーキテクチャとの接続方法

これらのイベントを何が処理するかは完全に自由です。MVVMこそがソフトウェアアーキテクチャの黄金標準だと考える人もいますが、あらゆる状況に最適な単一のアーキテクチャなど存在しないと気づき始めた人が増えています。

Composeを使うAndroid開発において、筆者の現在のアプローチは、サードパーティ製ライブラリにほぼ依存しないミニマリスト的なもので、各機能(画面)には通常以下の要素を含めます。

  • イベントハンドラとしての(プレゼンテーション)ロジッククラス
  • Viewの描画に必要なデータを保持するViewModel(その名の通り)
  • コンテナとして機能するActivity(godオブジェクトではない)
  • Viewを構成するコンポーザブル群
【Jetpack Compose入門】Sealed Classとラムダ式でUIイベントをスマートに処理する方法
Model-View-Whatever

関心の分離(separation of concerns)が守られている限り、何を採用しても構いません。筆者は「何を同じクラスにまとめるべきで、何をまとめるべきでないか」というシンプルな問いから、このアーキテクチャにたどり着きました。

ViewModel、Fragment、Activityのどれをイベントハンドラにするかにかかわらず、セットアップの仕方はすべて同じです。関数型(Function Types)を使います!

選択したクラス内に、Sealed Classを引数として受け取るイベントハンドラ関数を定義します。

class ActiveGameLogic(
    private val container: ActiveGameContainer?,
    private val viewModel: ActiveGameViewModel,
    private val gameRepo: IGameRepository,
    private val statsRepo: IStatisticsRepository,
    dispatcher: DispatcherProvider
) : BaseLogic<ActiveGameEvent>(dispatcher),
    CoroutineScope {
    //...
    override fun onEvent(event: ActiveGameEvent) {
        when (event) {
            is ActiveGameEvent.OnInput -> onInput(
                event.input,
                viewModel.timerState
            )
            ActiveGameEvent.OnNewGameClicked -> onNewGameClicked()
            ActiveGameEvent.OnStart -> onStart()
            ActiveGameEvent.OnStop -> onStop()
            is ActiveGameEvent.OnTileFocused -> onTileFocused(event.x, event.y)
        }
    }
    //...
}

このアプローチは非常に整理されており、サードパーティライブラリ非依存のこのクラスのすべてのユニットを、単一のエントリポイントを通じてテストしやすくなります。

しかし、まだ作業は終わりません。当然ながら、このイベントハンドラ関数onEventへの参照をコンポーザブル側に渡す方法が必要です。これは関数参照(function reference)を使えば実現できます。

class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer {
    private lateinit var logic: ActiveGameLogic

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ActiveGameViewModel()

        setContent {
            ActiveGameScreen(
                onEventHandler = logic::onEvent,
                viewModel
            )
        }

        logic = buildActiveGameLogic(this, viewModel, applicationContext)
    }

  	//...
}

「なぜActivityを使うのか」と疑問に思う方もいるでしょう。詳細な回答は、いつかライブ配信のQ&Aでお答えします。

端的に言えば、筆者のアーキテクチャでは(Jetpack Navigationを使用しないため)、FragmentはComposeにおいてやや存在意義が薄く、機能ごとのコンテナとしてActivityを使うこと自体に何の問題もありません。要は、巨大なgod Activityを書くことさえ避ければいいのです。

Kotlinで関数参照を作る方法を具体的に説明すると、クラス名/インターフェース名(トップレベル関数の場合は省略可)+ コロン2つ(::)+ 引数や括弧を付けない関数名という形式になります。

onEventHandler = logic::onEvent

OnClickListenerをJetpack ComposeのonClick修飾子で置き換える方法

準備が整ったところで、コンポーザブル内部での動作を見ていきましょう。当然ながら、ルートのコンポーザブルはイベントハンドラ関数をパラメータとして受け取る必要があります。

@Composable
fun ActiveGameScreen(
    onEventHandler: (ActiveGameEvent) -> Unit,
    viewModel: ActiveGameViewModel
) {
//...
}

関数型の構文は少し癖があり戸惑うかもしれませんが、これがクラス参照と大差ない「関数への参照」であると理解すれば大丈夫です。

godオブジェクトを作ってはいけないのと同じように、巨大なコンポーザブルも作るべきではありません。

  1. UIを可能な限り小さな合理的な部品に分解する
  2. それぞれをコンポーザブル関数でラップする
  3. UIインタラクションを持つ各コンポーザブルには、必ずイベントハンドラ関数への参照を渡す

以下は数独アプリの入力ボタンを表すコンポーザブルで、イベントハンドラが参照として渡されています。

@Composable
fun SudokuInputButton(
    onEventHandler: (ActiveGameEvent) -> Unit,
    number: Int
) {
    Button(
        onClick = { onEventHandler.invoke(ActiveGameEvent.OnInput(number)) },
        modifier = Modifier
            .requiredSize(56.dp)
            .padding(2.dp)
    ) {
        Text(
            text = number.toString(),
            style = inputButton.copy(color = MaterialTheme.colors.onPrimary),
            modifier = Modifier.fillMaxSize()
        )
    }
}

実際にイベントをロジッククラスへ渡すには、invoke関数を使用します。この関数は、関数型の定義に従って引数を受け取ります(この場合はActiveGameEvent)。

ここまで来れば、この美しくモダンなプログラミング言語Kotlinの力を最大限に活かして、Composeの有無にかかわらずUIインタラクションイベントを処理できるようになっています。

この記事が役に立ったら、ぜひSNSでシェアしてください。また、以下のリソースもチェックして、独立系プログラマー兼コンテンツクリエイターを応援していただけると嬉しいです。

SNS

Instagramはこちら、Twitterはこちらからフォローできます。

チュートリアル&コース一覧

https://youtube.com/wiseass    https://www.freecodecamp.org/news/author/ryan-michael-kay/ https://skl.sh/35IdKsj(Android StudioによるAndroid入門)

  1. Androidでシングルトンクラスを使う方法をサンプルコード付きで解説

    シングルトンデザインパターンとは?実装例に入る前に、まず「シングルトンデザインパターン」についておさらいしておきましょう。シングルトン(Singleton)とは、あるクラスのインスタンス生成をアプリケーション全体で1つだけに制限するデザインパターンです。主な用途としては、同時実行(並行処理)の制御や、アプリケーションがデータストアへアクセスするための中心的なアクセスポイントの作成などが挙げられます。この記事では、Androidアプリでシングルトンクラスを実際に使う方法を、ステップごとのサンプルコードとともに解説します。手順1:新規プロジェクトを作成するAndroid StudioでFile →

  2. Androidで画面の向きの変更(回転)を処理する方法【サンプルコード付き】

    Androidアプリ開発では、端末を回転させたときの画面の向き(縦・横)の変化を適切に処理することが重要です。本記事では、Android Studioを使用して、画面の向きが変更されたタイミングを検出し、その結果をTextViewに表示するサンプルアプリの作成手順を解説します。 ステップ1:新規プロジェクトを作成する Android Studioで新しいプロジェクトを作成します。メニューから「File」→「New Project」を選択し、必要な項目を入力してプロジェクトを作成してください。 ステップ2:レイアウトファイル(activity_main.xml)を編集する res/layou