AndroidのUI状態のモデリング


Androidの開発のためのGoogleから推奨されたアプローチはViewModel そしてView 観察する.それを達成するためにLiveData , StateFlow , RxJava または同様のツールです.しかし、どのようにUIの状態をモデル化するには?データクラスまたは密封クラスを使用しますか?つの観測可能なプロパティまたは多くを使用しますか?私はアプローチの間のトレードオフを記述し、使用する方法を決定するためのツールを提示します.この記事は大いに感激しているTypes as Sets エルムガイドから.
写真でMarc-Olivier Jodoin on Unsplash

集合としての型


コード内の可能な値が実際の有効な値と正確に一致することを確認できます.これを行うと、無効なデータに関連するバグのクラス全体を避けることができます.そのためには,まず,型と集合の関係を理解する必要がある.
値の集合として型を考えることができます.例えば、
  • Nothing - 空の集合には要素が含まれません.
  • Unit - singleton set 1要素を含むUnit
  • Boolean - 要素を含むtrue and false
  • Int - 要素を含みます.-2 , -1 , 0 , 1 , 2
  • Float - 要素を含みます:0.1 , 0.01 , 1.0 ….
  • String - 要素を含みます:"" , "a" , "b" , "Kotlin" , "Android" , "Hello world!"
  • それで書くときval x: Boolean it means x 集合に属するBoolean 値を指定できます.true or false .

    枢機卿


    数学ではCardinality セットの「要素数」の尺度です.例えば、Boolean 要素を含む[true, false] したがって、それはcardinality = 2を持ちます.
    上で述べたセットの枢機卿を見てみましょう.
  • Nothing - 0
  • Unit - 1
  • Boolean - 2
  • Short - 65535
  • Int - ∞
  • Float - ∞
  • String - ∞
  • 注意:Int and Float 正確に無限ではありません、しかし、それは2 ^ 32です、しかし、それは巨大な数です.
    アプリケーションを構築するときは、組み込みの型を使用して、カスタムクラスとデータクラスのような構造体を使用して、カスタムクラスを作成します.

    製品の種類(*)


    小林の製品タイプの1つの味は、そうですPair and Triple . その枢機卿を見てみましょう.
  • Pair<Unit, Boolean> - cardinality (単位)* cardinality ( boolean )= 1 * 2 = 2
  • Pair<Boolean, Boolean> - 2 * 2 = 4
  • Pair<Unit, Boolean> 要素を含みます:
  • Pair(Unit, false)
  • Pair(Unit, true)
  • その他の例
  • Triple<Unit, Boolean, Boolean > = 1 * 2 * 2 = 4
  • Pair<Int, Int> = cardinality ( int )* cardinality ( int )=∞ * ∞ = ∞
  • さて、それはすぐにエスカレートしました.
    型を組み合わせた場合Pair/Triple 彼らのcardinalityは掛け算する(したがって、名前積タイプ).Pair/Triple は、それぞれ2と3のプロパティを持つデータクラスのジェネリックバージョンです.data class User(val emailVerified: Boolean, val isAdmin: Boolean) と同じ濃度Pair<Boolean, Boolean> , 4 .要素
  • User(false, false)
  • User(false, true)
  • User(true, false)
  • User(true, true) .
  • 和型(+)


    Kotlinでは、sum型を実装するために密閉クラスを使用します.密封されたクラスを使用している型を結合するとき、総枢機卿はメンバーの枢機卿の合計と等しいです.以下に例を示します:
    sealed class NotificationSetting
    object Disabled : NotificationSettings() // an object has one element -> cardinality = 1
    data class Enabled(val pushEnabled: Boolean, val emailEnabled: Boolean) : NotificationSettings()
    
    // cardinality = cardinality (Disabled) + cardinality(Enabled)
    // cardinality = 1 + (2 * 2)
    // cardinality = 1 + 4 = 5
    
    sealed class Location
    object Unknown : Location()
    data class Somewhere(val lat: Float, val lng: Float) : Location()
    
    // cardinality = cardinality (Unknown) + cardinality(Somewhere)
    // cardinality = 1 + (∞ * ∞)
    // cardinality = 1 + ∞ = ∞
    
    

    許容可能な型


    モデルのもう一つの方法Location typeはnull可能な型を使用していますdata class Location(val lat: Float, val lng: Float) を返します.val location: Location? . このシナリオではnull 場所が不明です.これらの2つの表現は同じcardinalityを持ちます、そして、我々はどんな情報損失なしででも彼らの間で変わることができます.さらにいくつかの例を示します.
  • Unit? - 枢機卿= 2(1 +枢機卿)
  • Boolean? - 3 ( 1 + cardinality ( boolean ))
  • null可能な型の要素A? are null + 原型の要素A . 許容可能なタイプのcardinalityは、1 +オリジナルのcardinalityです.

    会館


    閻魔は、琴の和を表現する別の方法である.
    enum class Color { RED, YELLOW, GREEN }
    
    
    の枢機卿Color はこの場合、要素数に等しい.密閉クラスを使用した別の表現は以下の通りです.
    sealed class Color
    object Red : Color()
    object Yellow : Color()
    object Green : Color()
    
    
    そして、それは同じ枢機卿- 3です.

    なぜそれが問題か


    型とそのcardinalityとしてタイプについて考えることは、無効なデータに関連するバグの全部のクラスを避けるためにデータモデリングに役立ちます.我々が交通信号をモデル化していると言いましょう.可能な色は:赤、黄色、緑です.コードで表現するには、次のようになります.

  • エーString どこ"red" , "yellow" and "green" 有効なオプションはすべて無効です.しかし、ユーザータイプ"rad" 赤の代わりに、我々には問題があります.Or "yelow" or "RED" . すべての関数は引数を検証しますか?すべての関数がテストを行う必要がありますか?ここでの問題の根本的原因はcardinalityです.文字列には∞ 一方、我々の問題は3です.がある∞ - 3可能な無効値.

  • データクラスdata class Color(val isRed: Boolean, val isYellow: Boolean, val isGreen: Boolean) - ヒアColor(true, false, false) 赤を表す.しかし、これはまだ無効なデータのための部屋を残します.Color(true, true, true) . もう一度、あなたは値が有効であることを確実にするためにチェックとテストを必要とするでしょう.データクラスの基数Color は8で、8 - 3 = 5の不正値です.それよりずっと良いString , しかし、我々はまだそれを改善することができます.

  • enum -enum class Color { RED, YELLOW, GREEN } - これは枢機卿=3である.それは正確に問題の有効な値と一致します.不正な値は不可能ですので、テストのデータ妥当性をチェックする必要はありません.
    不正な値を除外する方法でデータをモデリングすることによって、結果として生じるコードも短くなり、より明確でテストが容易になります.

    Make sure the set of possible values in code match the set of valid values of the problem and a lot of issues disappear.


    ビューモデルから状態を公開する


    課題:
    トラフィックライトエンドポイントを呼び出すと、信号光(赤、黄色または緑)の色を示すアプリをビルドします.ネットワークコールショー中ProgressBar . 成功するとView 色とエラーの場合はTextView 一般的なテキストで.つのビューだけが同時に表示され、エラーを再試行する可能性はありません.
    class TrafficLightViewModel : ViewModel() {
    
        val state: LiveData<TrafficLightState> = TODO()
    }
    
    
    色をenumとして3つの値で表現します.タイプはどうTrafficLightState ですね.
    data class TrafficLightState(
        val isLoading: Boolean, 
        val isError: Boolean, 
        val color: Color?
    )
    
    
    つの方法は3つのプロパティを持つデータクラスです.しかし、これには無効な状態があります.TrafficLightState(true, true, Color.RED) . エラーと読み込みの両方がtrueです.両方を示すProgressBar とエラーTextView UIでは使用できません.また、エラーの場合には不可能な色もあります.の枢機卿TrafficLightState は2 ( boolean )* 2 ( boolean )* 4 ( color ?)=16章
    多くの観測可能なプロパティを使用するか?
    class TrafficLightViewModel : ViewModel() {
    
        val loading: LiveData<Boolean> = TODO()
    
        val error: LiveData<Boolean> = TODO()
    
        val color: LiveData<Color?> = TODO()
    }
    
    
    これは2 ( boolean ) * 2 ( boolean )* 4 (カラー?)=の基数です16 .データクラスアプローチと同じcardinalityを持ち、不正な値を有効にします.
    別のアプローチは、密封されたクラスを使用します.
    sealed class TrafficLightState
    object Loading : TrafficLightState()
    object Error : TrafficLightState()
    data class Success(val color: Color) : TrafficLightState()
    
    
    これには、1 (読み込み)+ 1 ( error )+ 3 (成功)= 5の値があります.
  • ネットワークコール中Loading そして、ProgressBar
  • ネットワークコールが失敗した場合:Error で、TextView
  • 成功するとSuccess 対応する色でビューを表示する
  • これらはすべて有効なアプローチであり、あなたのUI状態を設計するときです.しかし、それらのうちの1つだけが問題の正確な有効な状態にマッチします.バグを取り除くために、コードを単純化して、テストの数を減らすために、それを使ってください.

    結論


    コードの問題をモデル化するには、次のように組み込み型を使用します.Boolean , Int , String … また、Kotlinでは、データクラスや密閉クラスのような構造体を使用してカスタム型を作成します.異なる言語構成は、モデルのcardinalityに、異なる影響を及ぼします.正しい組み合わせを選ぶことで無効な状態の数を減らします.それは、より簡単でより堅牢なコードに終わります.
    レビューのおかげで.