230 讀數

这个小小的Kotlin图书馆可能是构建跨平台应用程序的最干净方法

经过 Android Insights19m2025/05/07
Read on Terminal Reader

太長; 讀書

SimpleMVI 是一种轻量级但强大的解决方案,用于在 Kotlin 多平台项目中实现 MVI 模式。
featured image - 这个小小的Kotlin图书馆可能是构建跨平台应用程序的最干净方法
Android Insights HackerNoon profile picture
0-item

在移动开发领域,选择合适的应用架构在确保代码质量、可维护性和可扩展性方面起着至关重要的作用,每年都会带来新的方法、库和框架,旨在简化开发过程并使代码更加结构化。


在本文中,我们将探讨SimpleMVI,一个轻量级但强大的解决方案,用于在Kotlin多平台项目中实现MVI模式,我们将探索图书馆的核心组件,其功能,并分析实用示例,以帮助您了解如何在您的项目中应用SimpleMVI。

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架构中:

  • 模型代表了不可变的应用程序状态,它完全描述了显示用户界面所需的数据。
  • 视图被动地显示当前状态,并以意图形式传输用户操作。
  • 意图描述了用户或系统可能改变应用程序状态的意图。


除了这些核心组件外,MVI通常包括:

  • Reducer - 一个函数,它采取当前状态和意图,并返回一个新的状态。
  • 副作用 - 不会影响状态,但需要与外部系统(例如导航,通知,API请求)的副作用。

建筑模式的简短历史

用户界面架构模式随着时间的推移而显著发展:

MVC(模型视图控制器)

其中一个最初的模式将应用程序分为三个组件:

  • 模型 - 数据和商业逻辑
  • 查看 - 用户界面
  • 控制器 - 处理用户输入


MVC的主要问题是组件之间的紧密连接和不明确的责任分离,这使测试和维护变得复杂。

MVP(模型视图演示)

对MVC的改进,其中:

  • 模型 - 数据和商业逻辑
  • 视图 - 被动用户界面
  • 演示者 - 模型和视觉之间的调解者


MVP 解決了可測試性問題,但經常導致演講者膨脹,並且在演講者和視圖之間緊密連接。

MVVM(模型视图模型)

进化中的下一步:

  • 模型 - 数据和商业逻辑
  • 查看 - 用户界面
  • ViewModel — 将模型中的数据转换为易于查看的格式


MVVM使用数据绑定的概念,这减少了锅炉板代码的数量,但可能会导致跟踪数据流的问题。

MVI(模型视野意图)

一个现代的方法,强调:

  • 可预测性 - 对国家管理的决定性方法
  • 不可变 - 状态不会改变,而是被取代
  • 单向数据流 — 事件的清晰透明序列


MVI特别有效于复杂的数据丰富的应用程序,具有众多用户交互和非同步操作。

为什么创建了SimpleMVI及其在其他图书馆中的位置

SimpleMVI 旨在为开发人员提供一个简单但强大的工具,用于在 Kotlin Multiplatform 项目中实现 MVI 模式。

  1. 专注于域逻辑,而不强加对UI层的解决方案
  2. 坚持“简单优先”原则,提供最少的必要组件
  3. 优化为Kotlin Multiplatform,确保与各种平台兼容
  4. 严格控制线程安全,保证与状态的交互只发生在主线上
  5. 通过配置系统提供灵活的错误处理配置


与替代品相比,SimpleMVI的主要优势:

  • 与更复杂的解决方案相比,较少的依赖性和较小的图书馆大小
  • 理解和使用的低入门门槛
  • 使用现代语言构造的完整Kotlin方法
  • 方便的DSL描述业务逻辑
  • 组件之间明确的责任分离


SimpleMVI 不是旨在解决所有应用程序架构问题,而是为组织业务逻辑提供可靠的基础,可以与用户界面、导航和应用程序其他方面的任何解决方案集成。

SimpleMVI的核心概念和组件

SimpleMVI 提供了实施 MVI 架构的最小化方法,专注于三个关键组件: Store、Actor 和 Middleware. 每个组件在确保单向数据流和管理应用程序状态方面都有独特的作用。

商店 - 建筑的核心元素

商店的定义和作用

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


重要的是要理解:

  • 所有公共商店方法只能在主线上调用(标记为 @MainThread 注释)
  • 调用“破坏()”后,商店无法使用;试图访问破坏的商店会导致错误
  • 使用前必须使用 init() 方法初始化 Store。

国家管理

商店提供与国家合作的下列功能:

  • Access to the current state via the state property

  • Observing state changes via the states flow

  • Processing side effects via the sideEffects flow


SimpleMVI 使用 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

演员 - 商业逻辑实施

演员工作原则

Actor 是 SimpleMVI 业务逻辑的组件,它接受意图,处理它们,并可以产生新的状态和副作用。

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 - 用于启动非同步操作
  • 当前状态接口函数(getState)
  • 国家减少功能(减少)
  • 新意图发送函数(onNewIntent)
  • 副作用发送函数(postSideEffect)

企图处理

onIntent(intent: Intent)商店在收到新意图时称之为方法,是商业逻辑的主要入口点。

  1. 确定收到的意图的类型
  2. 执行必要的商业逻辑
  3. 更新国家
  4. 必要时产生副作用

DefaultActor 和 DslActor:不同的实施方法

SimpleMVI为 Actor 实现提供了两种不同的方法:

DefaultActor - 面向对象的方法

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 的优点:

  • 更具宣言性的做法
  • 更少的代码
  • 更适合中小型项目
  • 安全意图处理


两种方法都提供相同的功能,它们之间的选择取决于开发者的偏好和项目的具体性。

Middleware - 扩展功能

Middleware 的用途

Middleware 在 SimpleMVI 中作为商店中的事件的观察者。 Middleware 不能修改事件,但可以对它们做出反应,这使得它非常适合执行跨功能逻辑,如日志,分析或调试。

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

中间件的关键用例

  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 支持各种平台:

  • 安卓
  • 马克思
  • 华为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 - 当尝试调用 Store 方法而不是主线时
  • StoreIsNotInitializedException - 当尝试使用非初始化商店时
  • 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多平台

基于Kotlin从头开始,SimpleMVI针对多平台开发进行了优化,该库通过预期/实际机制隔离了平台特定的代码,确保了与Android、iOS、macOS和wasm js的兼容性。

3、可预测的国家管理

严格遵守状态不变和单向数据流的原则,使基于SimpleMVI构建的应用程序更具预测性和更少容易出现错误。

4、针对常见问题内置保护

该库提供严格的线程安全控制,确保与状态的交互仅发生在主线上,从而防止许多与多线程相关的常见错误,这些错误很难检测和修复。

5. 方便的 DSL 用于声明逻辑描述

由于 DSL 支持,SimpleMVI 允许以声明式的方式描述业务逻辑,使代码更易于阅读和理解。

6、灵活性和可扩展性

尽管采用了最小化的方法,但SimpleMVI提供了通过中间软件系统扩展功能的机制,这使您可以轻松地添加记录、分析或调试等功能,而不会影响核心业务逻辑。

典型的使用案例

SimpleMVI特别适合以下场景:

1. Kotlin 多平台项目

如果您正在开发一个需要在多个平台上工作的应用程序(Android和iOS,Web应用程序),SimpleMVI允许您使用单一的架构方法和共享业务逻辑代码。

2、具有复杂状态和用户交互的应用程序

对于管理复杂状态和处理众多用户交互的应用程序,MVI 方法提供了清晰的结构和可预测性。

3、强调可测试性的项目

由于组件之间的责任和可预测的数据流的清晰分离,使用SimpleMVI构建的应用程序很容易单元测试,这使得库成为优先考虑代码质量和可测试的项目的绝佳选择。

将现有项目迁移到MVI架构

SimpleMVI可以逐步引入,从单个模块或功能开始,使其适合逐步迁移现有项目到MVI架构。

5、教育项目和原型

由于其简单性和最小化,SimpleMVI非常适合教学MVI原则和快速原型。

进一步学习的资源

对于那些想要深入了解SimpleMVI和MVI架构的人来说,我建议以下资源:

  • SimpleMVI GitHub 存储库 — 图书馆的源代码与使用示例
  • SimpleMVI 文档 — 官方文档,详细的 API 描述和建议

最后的想法

SimpleMVI 代表了使用现代建筑方法来组织应用程序业务逻辑的均衡解决方案. 图书馆提供了一个清晰的结构和可预测的数据流,而不强加不必要的复杂性。


在为您的项目选择架构时,请记住,没有适用于所有情况的通用解决方案。SimpleMVI 可以是优良的选择,对于那些重视简单、可预测和多平台支持的项目,但对于某些场景,其他库或方法可能更合适。


尝试,探索不同的架构解决方案,并选择最适合您的项目和团队的需求,并记住:最好的架构是帮助您有效地解决当前任务的架构,而不是创造额外的复杂性。

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks