230 測定値

この小さなKotlin図書館は、クロスプラットフォームアプリを構築する最もクリーンな方法かもしれません。

Android Insights19m2025/05/07
Read on Terminal Reader

長すぎる; 読むには

SimpleMVI は、Kotlin マルチプラットフォームプロジェクトにおける MVI パターンを実装するための軽量で強力なソリューションです。
featured image - この小さなKotlin図書館は、クロスプラットフォームアプリを構築する最もクリーンな方法かもしれません。
Android Insights HackerNoon profile picture
0-item

モバイル開発の世界では、適切なアプリケーションアーキテクチャを選択することは、コードの品質、維持性、スケーラビリティを確保する上で重要な役割を果たします。毎年、開発プロセスを簡素化し、コードをより構造化するように設計された新しいアプローチ、ライブラリ、フレームワークを導入しています。


この記事では、MVI パターンを Kotlin マルチプラットフォームプロジェクトで実装するための軽量で強力なソリューションである SimpleMVI を検討します.We will explore the library's core components, its features, and analyze practical examples that will help you understand how to apply SimpleMVI in your projects.

MVIパターンとそのコアコンセプトとは?

Model-View-Intent(MVI)は、機能プログラミングと反応システムにインスピレーションを与えたユーザインターフェイス開発の建築パターンです。

  1. Unidirectional Data Flow — data moves in one direction, forming a cycle: from user action to model change, then to view update.

  2. Immutable State — the application state is not changed directly; instead, a new state is created based on the previous one.

  3. Determinism — the same user actions with the same initial state always lead to the same result.


MVIアーキテクチャ:

  • モデルは、UI を表示するために必要なデータを完全に記述する不変のアプリケーション状態を表します。
  • View は、現在の状態を被動的に表示し、ユーザーのアクションを Intents として送信します。
  • Intent は、アプリケーションの状態を変える可能性のあるユーザーまたはシステムの意図を記述します。


これらのコアコンポーネントに加えて、MVIはしばしば含まれます:

  • Reducer - 現在の状態と意図を取り、新しい状態を返します。
  • Side Effect - 状態に影響を与えないが、外部システムとの相互作用を必要とする副作用(たとえば、ナビゲーション、通知、APIリクエスト)

建築パターンの短い歴史

UIアーキテクチャのパターンは、時間とともに大きく進化しました:

MVC(モデル・ビュー・コントローラー)

アプリケーションを3つのコンポーネントに分ける最初のパターンの一つ:

  • モデル - データとビジネス論理
  • ユーザインタフェース - User Interface
  • コントローラー - User Input


MVCの主な問題は、部品の緊密な接続と責任の不明確な分離で、テストとメンテナンスを複雑にします。

MVP(モデル・ビュー・プレゼンター)

MVCに比べて、以下のような改善があります。

  • モデル - データとビジネス論理
  • View - Passive User Interface(パシブユーザーインターフェイス)
  • Presenter - モデルとビューの間の仲介者


MVP はテスト性の問題を解決しますが、しばしばプレゼンターが膨らみ、プレゼンターとビューの間の緊密な接続につながります。

MVVM(モデル・ビュー・ビュー・モデル)

進化の次のステップ:

  • モデル - データとビジネス論理
  • ユーザインタフェース - User Interface
  • ViewModel — モデルからデータを View に便利な形式に変換します。


MVVMはデータ結合のコンセプトを使用し、ボイラープレートコードの量を減らすが、トラッキングデータフローの問題を引き起こす可能性がある。

MVI(モデル・ビュー・インテント)

強調する現代的なアプローチ:

  • 予測性 - 国家管理に対する決定主義的アプローチ
  • Immutability - state is not changed but replaced. 状態は変更されず、置き換えられます。
  • Unidirectional data flow — clear and transparent sequence of events (単方向データフロー — イベントの明確で透明な順序)


MVIは、複雑でデータ豊富なアプリケーションで、多数のユーザーインタラクションと非同期操作に特に効果的です。

なぜSimpleMVIが創設され、その他の図書館の間でその位置を占めるのか

SimpleMVI は、開発者に Kotlin Multiplatform プロジェクトで MVI パターンを実装するためのシンプルで強力なツールを提供するために開発されました。

  1. ドメイン論理に焦点を当て、UIレイヤーのソリューションを強要することなく
  2. 最低限必要なコンポーネントのセットを提供する「シンプルさ第一」の原則に従う
  3. Kotlin Multiplatformに最適化され、さまざまなプラットフォームとの互換性を確保
  4. トレイドのセキュリティを厳しく制御し、ステータスとの相互作用がメイントレイドでのみ発生することを保証します。
  5. 構成システムを通じて柔軟なエラー処理構成を提供


代替品に比べてSimpleMVIの主な利点:

  • より複雑なソリューションに比べて、より少ない依存性と小さなライブラリサイズ
  • 理解と使用のための低い入力値
  • 現代言語構造を用いた完全なKotlinアプローチ
  • ビジネス論理を説明するための便利なDSL
  • 部品間の責任の明確な分離


SimpleMVI は、すべてのアプリケーションアーキテクチャの問題を解決することを目的としているのではなく、UI、ナビゲーション、およびアプリケーションの他の側面に関するあらゆるソリューションと統合できるビジネス論理を組織するための信頼できる基盤を提供します。

SimpleMVIのコアコンセプトとコンポーネント

SimpleMVI は、MVI アーキテクチャを実装するためのミニマリズム的なアプローチを提供し、ストア、Actor、ミドルウェアの 3 つの主要なコンポーネントに焦点を当てています. Each of these components has a unique role in ensuring unidirectional data flow and managing application state.

ストア - 建築の中心的な要素

店舗の定義と役割

Store は SimpleMVI の中心であり、アプリケーションの状態を保持し、意図を処理し、副作用を生成するコンテナです。

public interface Store<in Intent : Any, out State : Any, out SideEffect : Any> {
    // Current state
    public val state: State
    
    // State flow
    public val states: StateFlow<State>
    
    // Side effects flow
    public val sideEffects: Flow<SideEffect>

    // Store initialization
    @MainThread
    public fun init()

    // Intent processing
    @MainThread
    public fun accept(intent: Intent)

    // Store destruction
    @MainThread
    public fun destroy()
}

大寿命サイクル

ストアには明確に定義されたライフサイクルがあります:

  1. Creation - instantiating the Store object with necessary dependencies

  2. Initialization - calling the init() method, preparing internal components

  3. Active use - processing intents through the accept(intent) method

  4. Destruction - calling the destroy() method, releasing resources


それを理解することが重要である:

  • All public Store methods must be called only on the main thread (marked with the @MainThread annotation)
  • 破壊()を呼び出した後、ストアは使用できません;破壊されたストアにアクセスしようとすると、エラーが発生します。
  • Store は、使用前に init() メソッドで初期化する必要があります。

国家管理

Store は、State で作業するための次の機能を提供しています。

  • Access to the current state via the state property

  • Observing state changes via the states flow

  • Processing side effects via the sideEffects flow


SimpleMVI は、Flow Implementation に Kotlin Coroutines のクラスを使用します。StateFlow州および定期Flow副作用のために、Kotlinの反応プログラミングの標準アプローチとの互換性を確保する。

商店への便利な拡張

SimpleMVI は、 intents で作業するための便利なオペレーターを提供します。

// Instead of store.accept(intent)
store + MyStore.Intent.LoadData

// Instead of store.accept(intent)
store += MyStore.Intent.LoadData

アーティスト - Business Logic Implementation

俳優活動の原則

Actor は SimpleMVI のビジネス ロジックを担当するコンポーネントです. It accepts intentions, processes them, and can produce a new state and side effects. Actor は、ユーザー インターフェイスとアプリケーション データの間の仲介者です.

public interface Actor<Intent : Any, State : Any, out SideEffect : Any> {
    @MainThread
    public fun init(
        scope: CoroutineScope,
        getState: () -> State,
        reduce: (State.() -> State) -> Unit,
        onNewIntent: (Intent) -> Unit,
        postSideEffect: (sideEffect: SideEffect) -> Unit,
    )

    @MainThread
    public fun onIntent(intent: Intent)

    @MainThread
    public fun destroy()
}

各プレイヤーは、以下にアクセスできます:

  • CoroutineScope - asynchronous operations を起動するために
  • 現在の状態ゲッター機能(getState)
  • 国家減少機能(減少)
  • New intention sending function (onNewIntent) (新意図送信機能)
  • 副作用の送信機能(postSideEffect)

企画加工

THEonIntent(intent: Intent)この方法は、新しい意図を受け取るときにストアによって呼ばれ、ビジネス論理の主な入り口である。

  1. 受信した意図の種類を決定する
  2. 必要なビジネス論理を実行する
  3. 国を更新
  4. 必要に応じて副作用を引き起こす

DefaultActor と DslActor: 異なる実装アプローチ

SimpleMVI は Actor の実装に 2 つの異なるアプローチを提供しています。

DefaultActor - Object-Oriented Approach オブジェクト指向アプローチ

class CounterActor : DefaultActor<CounterIntent, CounterState, CounterSideEffect>() {
    override fun handleIntent(intent: CounterIntent) {
        when (intent) {
            is CounterIntent.Increment -> {
                reduce { copy(count = count + 1) }
            }
            is CounterIntent.Decrement -> {
                reduce { copy(count = count - 1) }
            }
            is CounterIntent.Reset -> {
                reduce { CounterState() }
                sideEffect(CounterSideEffect.CounterReset)
            }
        }
    }
    
    override fun onInit() {
        // Initialization code
    }
    
    override fun onDestroy() {
        // Cleanup code
    }
}

DefaultActorの利点:

  • OOPアプローチ
  • 複雑なビジネス論理に適しています。
  • 大規模なプロジェクトに適した

DslActor - DSLによる機能的なアプローチ

val counterActor = actorDsl<CounterIntent, CounterState, CounterSideEffect> {
    onInit {
        // Initialization code
    }
    
    onIntent<CounterIntent.Increment> {
        reduce { copy(count = count + 1) }
    }
    
    onIntent<CounterIntent.Decrement> {
        reduce { copy(count = count - 1) }
    }
    
    onIntent<CounterIntent.Reset> {
        reduce { CounterState() }
        sideEffect(CounterSideEffect.CounterReset)
    }
    
    onDestroy {
        // Cleanup code
    }
}

DslActorの利点:

  • より宣言的なアプローチ
  • ボイラープレートコード
  • 小規模・中規模プロジェクトに最適
  • タイトル: Safe Intention


両方のアプローチは、同じ機能性を提供し、それらの間の選択は、開発者の好みとプロジェクトの特性に依存します。

Middleware - 機能の拡張

ミドルウェアの目的

SimpleMVIのミドルウェアは、ストア内のイベントの観察者として機能します。ミドルウェアは、イベントを変更することはできませんが、それらに反応することができ、ログアップ、分析、デバッグなどのクロス機能論理を実装するのに理想的です。

public interface Middleware<Intent : Any, State : Any, SideEffect : Any> {
    // Called when Store is initialized
    public fun onInit(state: State)
    
    // Called when a new intent is received
    public fun onIntent(intent: Intent, state: State)
    
    // Called when state changes
    public fun onStateChanged(oldState: State, newState: State)
    
    // Called when a side effect is generated
    public fun onSideEffect(sideEffect: SideEffect, state: State)
    
    // Called when Store is destroyed
    public fun onDestroy(state: State)
}

ロッジング&デバッグ能力

SimpleMVI には、ログアップのための内蔵の Middleware 実装が含まれています。LoggingMiddleware:

val loggingMiddleware = LoggingMiddleware<MyIntent, MyState, MySideEffect>(
    name = "MyStore",
    logger = DefaultLogger
)


LoggingMiddlewareストア内のすべてのイベントをキャプチャし、ログに出力します:


MyStore | Initialization
MyStore | Intent | LoadData
MyStore | Old state | State(isLoading=false, data=null)
MyStore | New state | State(isLoading=true, data=null)
MyStore | SideEffect | ShowLoading
MyStore | Destroying

これは、アプリケーション内のすべてのデータフローを追跡することを可能にするため、デバッグのための有用です。

「Custom Middleware」

あなた自身のミドルウェアを作成することは非常に簡単です:

class AnalyticsMiddleware<Intent : Any, State : Any, SideEffect : Any>(
    private val analytics: AnalyticsService
) : Middleware<Intent, State, SideEffect> {
    
    override fun onInit(state: State) {
        analytics.logEvent("store_initialized")
    }
    
    override fun onIntent(intent: Intent, state: State) {
        analytics.logEvent("intent_received", mapOf("intent" to intent.toString()))
    }
    
    override fun onStateChanged(oldState: State, newState: State) {
        analytics.logEvent("state_changed")
    }
    
    override fun onSideEffect(sideEffect: SideEffect, state: State) {
        analytics.logEvent("side_effect", mapOf("effect" to sideEffect.toString()))
    }
    
    override fun onDestroy(state: State) {
        analytics.logEvent("store_destroyed")
    }
}


ミドルウェアは組み合わせることができ、ハンドラーの連鎖を作成することができます:

val store = createStore(
    name = storeName<MyStore>(),
    initialState = MyState(),
    actor = myActor,
    middlewares = listOf(
        loggingMiddleware,
        analyticsMiddleware,
        debugMiddleware
    )
)

Key Use Cases for Middleware について

  1. Logging — recording all events for debugging

  2. Analytics — tracking user actions

  3. Performance metrics — measuring intent processing time

  4. Debugging — visualizing data flow through UI

  5. Testing — verifying the correctness of event sequences


Middleware は被動的な観察者であり、受信するイベントを変更することはできません。

図書館との協力

インストール&セットアップ

プロジェクトに依存性を追加する:

// build.gradle.kts
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")

あなたの最初の店を作る

ストアを作成する最も簡単な方法は、ストアインターフェイスを実装するクラスを宣言することです。

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> by createStore(
    name = storeName<CounterStore>(),
    initialState = State(),
    actor = actorDsl {
        onIntent<Intent.Increment> {
            reduce { copy(count = count + 1) }
        }
        
        onIntent<Intent.Decrement> {
            reduce { copy(count = count - 1) }
        }
    }
) {
    sealed interface Intent {
        data object Increment : Intent
        data object Decrement : Intent
    }
    
    data class State(val count: Int = 0)
    
    sealed interface SideEffect
}

店の利用

// Creating an instance
val counterStore = CounterStore()

// Initialization
counterStore.init()

// Sending intents
counterStore.accept(CounterStore.Intent.Increment)
// or using operators
counterStore + CounterStore.Intent.Increment
counterStore += CounterStore.Intent.Decrement

// Getting the current state
val currentState = counterStore.state

// Subscribing to the state flow
val statesJob = launch {
    counterStore.states.collect { state ->
        // Useful work
    }
}

// Subscribing to side effects
val sideEffectsJob = launch {
    counterStore.sideEffects.collect { sideEffect ->
        // Processing side effects
    }
}

// Releasing resources
counterStore.destroy()

Kotlinマルチプラットフォームサポート

SimpleMVI は Kotlin Multiplatform を通じてさまざまなプラットフォームをサポートしています。

  • Androidの
  • IOS
  • マコス
  • ワーム JS

プラットフォーム特定のコード隔離メカニズムの使用expect/actual:

// Common code
public expect fun isMainThread(): Boolean

// Android implementation
public actual fun isMainThread(): Boolean {
    return Looper.getMainLooper() == Looper.myLooper()
}

// iOS implementation
public actual fun isMainThread(): Boolean {
    return NSThread.isMainThread
}

// wasm js implementation
public actual fun isMainThread(): Boolean {
    return true // JavaScript is single-threaded
}

Loggingは、異なるプラットフォームで同様に実装されています:

// Common code
public expect fun logV(tag: String, message: String)

// Android implementation
public actual fun logV(tag: String, message: String) {
    Log.v(tag, message)
}

// iOS/wasm js implementation
public actual fun logV(tag: String, message: String) {
    println("$tag: $message")
}

具体例:カウンター

大規模データモデルの定義

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> {
    // Intents - user actions
    sealed interface Intent {
        data object Increment : Intent
        data object Decrement : Intent
        data object Reset : Intent
    }
    
    // State
    data class State(
        val count: Int = 0,
        val isPositive: Boolean = true
    )
    
    // Side effects - one-time events
    sealed interface SideEffect {
        data object CounterReset : SideEffect
    }
}

大規模実施

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> by createStore(
    name = storeName<CounterStore>(),
    initialState = State(),
    actor = actorDsl {
        onIntent<Intent.Increment> {
            reduce { 
                copy(
                    count = count + 1,
                    isPositive = count + 1 >= 0
                ) 
            }
        }
        
        onIntent<Intent.Decrement> {
            reduce { 
                copy(
                    count = count - 1,
                    isPositive = count - 1 >= 0
                ) 
            }
        }
        
        onIntent<Intent.Reset> {
            reduce { State() }
            sideEffect(SideEffect.CounterReset)
        }
    }
) {
    // Data model defined above
}

UI への接続(Android 例)

class CounterViewModel : ViewModel() {
    private val store = CounterStore()
    
    init {
        // Built-in extension for automatic lifecycle management
        attachStore(store)
    }
    
    val state = store.states.stateIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly,
        initialValue = store.state
    )
    
    val sideEffects = store.sideEffects
    
    fun increment() {
        store.accept(CounterStore.Intent.Increment)
    }
    
    fun decrement() {
        store.accept(CounterStore.Intent.Decrement)
    }
    
    fun reset() {
        store.accept(CounterStore.Intent.Reset)
    }
}

高度な特徴

図書館構成

SimpleMVI は、柔軟な構成システムを提供します。

configureSimpleMVI {
    // Strict error handling mode (throws exceptions)
    strictMode = true
    
    // Logger configuration
    logger = object : Logger {
        override fun log(message: String) {
            // Your logging implementation
        }
    }
}

間違った操作方法

  • strictMode = true - ライブラリは厳格なモードで動作し、エラーが検出された場合に例外を投げます。
  • strictMode = false(デフォルト) - ライブラリは緩やかなモードで動作し、実行を中断することなくエラーのみをログします。

ミス行動

SimpleMVIには特別な例外があります:

  • NotOnMainThreadException - メイントレードからではなくストアメソッドを呼び出そうとすると
  • StoreIsNotInitializedException - uninitialized Store を使用しようとすると
  • StoreIsAlreadyDestroyedException - 既に破壊されたストアを使用しようとすると

コンポーネントテスト

シンプルな責任分離のおかげで、SimpleMVI コンポーネントは簡単にテストできます。

// Example of Store testing
@Test
fun `increment should increase counter by 1`() {
    // Arrange
    val store = CounterStore()
    store.init()
    
    // Act
    store.accept(CounterStore.Intent.Increment)
    
    // Assert
    assertEquals(1, store.state.count)
    assertTrue(store.state.isPositive)
    
    // Cleanup
    store.destroy()
}

結論

モバイル開発がますます複雑になり、コードの品質とアプリケーションの維持可能性に対する要件が増加するにつれて、適切なアーキテクチャを選択することは重要な決定となります。

SimpleMVIの主な利点

図書館の強みを以下に挙げる。

1.ミニマリズムと実践的アプローチ

SimpleMVI は、MVI パターンを実装するために必要なコンポーネントのみを提供し、不要な抽象や複雑さがなく、MVI パターンを実装するために必要なコンポーネントのみを提供します。

2. 完全な Kotlin Multiplatform サポート

Kotlin に基づいて最初から構築された SimpleMVI は、マルチプラットフォーム開発に最適化されています. The library isolates platform-specific code through the expect/real mechanism, ensuring compatibility with Android, iOS, macOS, and wasm js.

3.予測可能な国家管理

ステータス不変性と単方向データフローの原則の厳格な遵守により、SimpleMVIに構築されたアプリケーションはより予測可能で、エラーの可能性が低くなります。

4. 共通の問題に対する内蔵保護

ライブラリは、ステータスとの相互作用がメイン・トレードのみで発生することを保証する厳格なトレードセキュリティコントロールを提供し、複数のトレードに関連する多くの一般的なエラーを防ぐことができます。

DSL for Declarative Logic Description(デカラティブロジック説明)

DSL サポートのおかげで、SimpleMVI はビジネス論理を宣言的なスタイルで説明することを可能にし、コードをより読みやすく理解できるようにします。

6. 柔軟性と拡張性

ミニマリスト的なアプローチにもかかわらず、SimpleMVIは、ミドルウェアシステムを通じて機能を拡張するためのメカニズムを提供します. This makes it easy to add capabilities such as logging, analytics, or debugging without affecting the core business logic.

典型的な使用例

SimpleMVI は、以下のシナリオに特に適しています。

1. Kotlin マルチプラットフォームプロジェクト

複数のプラットフォーム(AndroidとiOS、Webアプリケーション)で動作する必要があるアプリケーションを開発している場合、SimpleMVIは、単一のアーキテクチャアプローチと共有ビジネス論理コードを使用することを可能にします。

2.複雑なステータスおよびユーザーインタラクションを持つアプリケーション

複雑な状態を管理し、多数のユーザーインタラクションを処理するアプリケーションの場合、MVIアプローチは明確な構造と予測性を提供します。

3.Testabilityに重点を置くプロジェクト

コンポーネント間の責任の明確な分離と予測可能なデータフローのおかげで、SimpleMVI で構築されたアプリケーションは単位テストが容易になります。

4. MVI Architecture への既存プロジェクトの移行

SimpleMVI は、個々のモジュールや機能から始めて段階的に導入することができ、既存のプロジェクトを MVI アーキテクチャに段階的に移行するのに適しています。

5.教育プロジェクトとプロトタイプ

シンプルさとミニマリズムのおかげで、SimpleMVIはMVIの原則を教えるのに適しています。

さらなる学習のための資源

SimpleMVIおよびMVIアーキテクチャの知識を深めたい方は、以下のリソースをお勧めします。

  • SimpleMVI GitHub repository — 使用例を含むライブラリのソースコード
  • SimpleMVI ドキュメント — 詳細な API 説明と推奨事項を含む公式ドキュメント

最終思考

SimpleMVI は、アーキテクチャに近代的なアプローチを用いてアプリケーションビジネス論理を組織するためのバランスの取れたソリューションを提供しています.The library offers a clear structure and predictable data flow without imposing unnecessary complexity.


プロジェクトのアーキテクチャを選択する際には、すべてのケースに適した普遍的なソリューションがないことを覚えておいてください. SimpleMVI は、シンプルさ、予測性、およびマルチプラットフォームサポートが評価されているプロジェクトのための優れた選択肢ですが、いくつかのシナリオでは、他のライブラリやアプローチがより適切かもしれません。


実験し、さまざまなアーキテクチャーソリューションを探索し、プロジェクトやチームのニーズに最適なものを選択してください. And remember: the best architecture is one that helps you effectively solve the tasks at hand, not one that creates additional complexity.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks