Android コードの将来性を保証する、パート 2: 関数型リアクティブ プログラミングの実践
公開: 2022-09-08関数型リアクティブ プログラミング (FRP) は、リアクティブ プログラミングのリアクティブ性と、関数型プログラミングの宣言型関数構成を組み合わせたパラダイムです。 複雑なタスクを簡素化し、洗練されたユーザー インターフェイスを作成し、状態をスムーズに管理します。 これらおよび他の多くの明確な利点により、FRP の使用はモバイルおよび Web 開発の主流になりつつあります。
これは、このプログラミング パラダイムを理解するのが簡単だという意味ではありません。 このチュートリアルのパート 1 では、関数型プログラミングとリアクティブ プログラミングという FRP の基本概念を定義しました。 この記事では、便利なライブラリーの概要と詳細なサンプル実装を使用して、それを適用する準備をします。
この記事は Android 開発者を念頭に置いて書かれていますが、概念は一般的なプログラミング言語の経験を持つすべての開発者に関連し、有益です。
FRP 入門: システム設計
FRP パラダイムは、状態とイベントの無限のサイクルです: State -> Event -> State' -> Event' -> State'' -> …
. (注意として、「プライム」と発音する'
は、同じ変数の新しいバージョンを示します。) すべての FRP プログラムは、受け取るイベントごとに更新される初期状態で開始します。 このプログラムには、リアクティブ プログラムと同じ要素が含まれています。
- 州
- イベント
- 宣言型パイプライン (
FRPViewModel function
として示されています) - 観測可能 (
StateFlow
として示される)
ここでは、一般的なリアクティブ要素を実際の Android コンポーネントとライブラリに置き換えました。
FRP ライブラリとツールの探索
FRP を使い始めるのに役立ち、関数型プログラミングにも関連するさまざまな Android ライブラリとツールがあります。
- Ivy FRP : これは私が作成したライブラリで、このチュートリアルで教育目的で使用されます。 これは、FRP へのアプローチの開始点として意図されていますが、適切なサポートがないため、そのままでは本番環境での使用は意図されていません。 (現在、私はそれを維持している唯一のエンジニアです。)
- 矢印: これは、FP 用の最高かつ最も人気のある Kotlin ライブラリの 1 つであり、サンプル アプリでも使用します。 比較的軽量でありながら、Kotlin で機能するために必要なほとんどすべてを提供します。
- Jetpack Compose : これは、ネイティブ UI を構築するための Android の現在の開発ツールキットであり、今日使用する 3 番目のライブラリです。 これは、現代の Android 開発者にとって不可欠です。まだ習得していない場合は、習得し、UI を移行することをお勧めします。
- Flow : これは Kotlin の非同期リアクティブ データストリーム API です。 このチュートリアルでは使用しませんが、RoomDB、Retrofit、Jetpack などの多くの一般的な Android ライブラリと互換性があります。 フローはコルーチンとシームレスに連携し、反応性を提供します。 たとえば、Flow を RoomDB と併用すると、アプリが常に最新のデータで動作することが保証されます。 テーブルで変更が発生すると、このテーブルに依存するフローはすぐに新しい値を受け取ります。
- Kotest : このテスト プラットフォームは、純粋な FP ドメイン コードに関連するプロパティ ベースのテスト サポートを提供します。
サンプルのフィート/メートル変換アプリの実装
Android アプリでの FRP の使用例を見てみましょう。 メートル (m) とフィート (ft) の間で値を変換する簡単なアプリを作成します。
このチュートリアルでは、FRP を理解するために不可欠なコードの部分のみを取り上げ、簡単にするために完全なコンバーター サンプル アプリから変更しています。 Android Studio を使いたい場合は、Jetpack Compose アクティビティを使用してプロジェクトを作成し、Arrow と Ivy FRP をインストールします。 28 以降のminSdk
バージョンと、Kotlin 1.6 以降の言語バージョンが必要です。
州
アプリの状態を定義することから始めましょう。
// ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )
私たちの状態クラスはかなり一目瞭然です:
-
conversion
: フィートからメートル、またはメートルからフィートの変換対象を表す型。 -
value
: ユーザーが入力する float で、後で変換します。 -
result
: 成功した変換を表すオプションの結果。
次に、ユーザー入力をイベントとして処理する必要があります。
イベント
ユーザー入力を表すシール クラスとしてConvEvent
を定義しました。
// ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }
そのメンバーの目的を調べてみましょう。
-
SetConversionType
: フィートからメートルに変換するか、メートルからフィートに変換するかを選択します。 -
SetValue
: 変換に使用される数値を設定します。 -
Convert
: 変換タイプを使用して入力値の変換を実行します。
それでは、ビューモデルを続けます。
宣言型パイプライン: イベント ハンドラーと関数の構成
ビュー モデルには、イベント ハンドラーと関数構成 (宣言型パイプライン) コードが含まれています。
// ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { companion object { const val METERS_FEET_CONST = 3.28084f } // set initial state override val _state: MutableStateFlow<ConvState> = MutableStateFlow( ConvState( conversion = ConvType.METERS_TO_FEET, value = 1f, result = None ) ) override suspend fun handleEvent(event: ConvEvent): suspend () -> ConvState = when (event) { is ConvEvent.SetConversionType -> event asParamTo ::setConversion then ::convert is ConvEvent.SetValue -> event asParamTo ::setValue is ConvEvent.Convert -> stateVal() asParamTo ::convert } // ... }
実装を分析する前に、Ivy FRP ライブラリに固有のいくつかのオブジェクトを分析してみましょう。
FRPViewModel<S,E>
は、FRP アーキテクチャを実装する抽象ビュー モデル ベースです。 コードでは、次のメソッドを実装する必要があります。
-
val _state
: 状態の初期値を定義します (Ivy FRP は Flow をリアクティブ データ ストリームとして使用しています)。 -
handleEvent(Event): suspend () -> S
:Event
を指定して、非同期で次の状態を生成します。 基礎となる実装は、イベントごとに新しいコルーチンを起動します。 -
stateVal(): S
: 現在の状態を返します。 -
updateState((S) -> S): S
ViewModel
の状態を更新します。
それでは、関数合成に関連するいくつかのメソッドを見てみましょう。
-
then
: 2 つの関数を一緒に構成します。 -
asParamTo
:f(T)
と値t
(型T
) から関数g() = f(t)
を生成します。 -
thenInvokeAfter
: 2 つの関数を合成してから呼び出します。
updateState
とthenInvokeAfter
は、次のコード スニペットに示されているヘルパー メソッドです。 これらは、残りのビュー モデル コードで使用されます。
宣言型パイプライン: 追加の関数の実装
ビュー モデルには、変換の種類と値を設定し、実際の変換を実行し、最終結果をフォーマットするための関数の実装も含まれています。
// ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { // ... private suspend fun setConversion(event: ConvEvent.SetConversionType) = updateState { it.copy(conversion = event.conversion) } private suspend fun setValue(event: ConvEvent.SetValue) = updateState { it.copy(value = event.value) } private suspend fun convert( state: ConvState ) = state.value asParamTo when (stateVal().conversion) { ConvType.METERS_TO_FEET -> ::convertMetersToFeet ConvType.FEET_TO_METERS -> ::convertFeetToMeters } then ::formatResult thenInvokeAfter { result -> updateState { it.copy(result = Some(result)) } } private fun convertMetersToFeet(meters: Float): Float = meters * METERS_FEET_CONST private fun convertFeetToMeters(ft: Float): Float = ft / METERS_FEET_CONST private fun formatResult(result: Float): String = DecimalFormat("###,###.##").format(result) }
Ivy FRP ヘルパー関数を理解したら、コードを分析する準備が整いました。 コア機能から始めましょう: convert
。 convert
は状態 ( ConvState
) を入力として受け取り、変換された入力の結果を含む新しい状態を出力する関数を生成します。 擬似コードでは、 State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>)
のように要約できます。
Event.SetValue
イベントの処理は簡単です。 イベントからの値で状態を更新するだけです (つまり、ユーザーが変換する数値を入力します)。 ただし、 Event.SetConversionType
イベントの処理は、次の 2 つのことを行うため、もう少し興味深いものです。
- 選択した変換タイプ (
ConvType
) で状態を更新します。 -
convert
を使用して、選択した変換タイプに基づいて現在の値を変換します。
コンポジションの力を利用して、 convert: State -> State
関数を他のコンポジションの入力として使用できます。 上記のコードが純粋ではないことに気付いたかもしれません: FRPViewModel でprotected abstract val _state: MutableStateFlow<S>
をFRPViewModel
しているため、 updateState {}
を使用するたびに副作用が発生します。 Kotlin で Android 用の完全に純粋な FP コードを作成することは現実的ではありません。
純粋ではない関数を合成すると予測できない結果が生じる可能性があるため、ハイブリッド アプローチが最も実用的です。ほとんどの部分で純粋な関数を使用し、不純な関数の副作用が制御されていることを確認します。 これはまさに私たちが上で行ったことです。
オブザーバブルと UI
最後のステップは、アプリの UI を定義し、コンバーターに命を吹き込むことです。
アプリの UI は少し「醜い」ものになりますが、この例の目的は FRP を示すことであり、Jetpack Compose を使用して美しいデザインを構築することではありません。
// ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }
私たちの UI コードは、できるだけ少ないコード行で基本的な Jetpack Compose 原則を使用しています。 ただし、言及する価値のある興味深い関数が 1 つあります。 FRP<ConvState, ConvEvent, ConverterViewModel>
です。 FRP
は Ivy FRP フレームワークからの構成可能な関数であり、いくつかのことを行います。
-
@HiltViewModel
を使用してビュー モデルをインスタンス化します。 - フローを使用してビュー モデルの
State
を監視します。 - コード
onEvent: (Event) -> Unit)
を使用して、ViewModel
にイベントを伝達します。 - イベントの伝播を行い、最新の状態を受け取る
@Composable
高階関数を提供します。 - 必要に応じて、アプリの起動時に呼び出される
initialEvent
を渡す方法を提供します。
Ivy FRP ライブラリでFRP
関数を実装する方法は次のとおりです。
@Composable inline fun <S, E, reified VM : FRPViewModel<S, E>> BoxWithConstraintsScope.FRP( initialEvent: E? = null, UI: @Composable BoxWithConstraintsScope.( state: S, onEvent: (E) -> Unit ) -> Unit ) { val viewModel: VM = viewModel() val state by viewModel.state().collectAsState() if (initialEvent != null) { onScreenStart { viewModel.onEvent(initialEvent) } } UI(state, viewModel::onEvent) }
コンバーターの例の完全なコードは GitHub で見つけることができ、UI コード全体はConverterScreen.kt
ファイルのUI
関数で見つけることができます。 アプリまたはコードを試してみたい場合は、Ivy FRP リポジトリのクローンを作成し、Android Studio でsample
アプリを実行できます。 アプリを実行するには、エミュレータでストレージを増やす必要がある場合があります。
FRP を使用したよりクリーンな Android アーキテクチャ
関数型プログラミング、リアクティブ プログラミング、そして最後に関数型リアクティブ プログラミングの基礎をしっかりと理解することで、FRP の利点を享受し、よりクリーンで保守しやすい Android アーキテクチャを構築する準備が整います。
Toptal Engineering Blog は、この記事で紹介したコード サンプルをレビューしてくれた Tarun Goyal に感謝の意を表します。