Во светот на мобилниот развој, изборот на вистинската апликациска архитектура игра критична улога во обезбедувањето на квалитетот на кодот, одржливоста и скалабилноста. Секоја година носи нови пристапи, библиотеки и рамки дизајнирани да го поедностават процесот на развој и да го направат кодот поструктуриран. Во последниве години, MVI архитектурата (Model-View-Intent) добила посебна популарност, нудејќи елегантно решение за управување со состојбата на апликацијата и организирање на еднонасочен проток на податоци.
Во оваа статија, ние ќе ги испитаме SimpleMVI – лесен, но моќно решение за имплементација на MVI модел во Kotlin мултиплатформен проекти. Ние ќе ги истражуваат основните компоненти на библиотеката, неговите карактеристики, и да се анализира практични примери кои ќе ви помогне да се разбере како да се примени SimpleMVI во вашите проекти.
Што е MVI модел и неговите основни концепти
Model-View-Intent (MVI) е архитектонски модел за развој на кориснички интерфејс, инспириран од функционално програмирање и реактивни системи.
-
Unidirectional Data Flow — data moves in one direction, forming a cycle: from user action to model change, then to view update.
-
Immutable State — the application state is not changed directly; instead, a new state is created based on the previous one.
-
Determinism — the same user actions with the same initial state always lead to the same result.
Во MVI архитектура:
- Моделот претставува непроменлива состојба на апликацијата која целосно ги опишува податоците потребни за прикажување на интерфејсот на корисникот.
- Преглед пасивно ја прикажува тековната состојба и ги пренесува акциите на корисникот како Намера.
- Намерата ги опишува намерите на корисникот или системот кои потенцијално можат да ја променат состојбата на апликацијата.
Во прилог на овие основни компоненти, MVI често вклучува:
- Редуктор – функција која ја зема тековната состојба и намерата и враќа нова состојба.
- Несакани ефекти – несакани ефекти кои не влијаат на состојбата, но бараат интеракција со надворешни системи (на пример, навигација, известувања, API барања).
Кратка историја на архитектонски модели
Архитектурните модели на корисничкиот интерфејс значително се развиле со текот на времето:
MVC (модел за гледање на контролер)
Еден од првите модели кои ја поделија апликацијата на три компоненти:
- Модел - податоци и деловна логика
- Преглед - кориснички интерфејс
- Контролер – ракување со кориснички влез
Главниот проблем со MVC е тесното спојување помеѓу компонентите и нејасното одвојување на одговорностите, што го комплицира тестирањето и одржувањето.
MVP (презентација на моделот)
Подобрување во однос на MVC, каде:
- Модел - податоци и деловна логика
- Преглед – пасивен кориснички интерфејс
- Презентатор – посредник помеѓу Модел и Вид
MVP го решава проблемот со тестирањето, но често доведува до надуеност на презентаторите и тесно поврзување помеѓу Презентаторот и Прегледот.
MVVM (Model-View-ViewModel – Модел за гледање)
Следниот чекор во еволуцијата:
- Модел - податоци и деловна логика
- Преглед - кориснички интерфејс
- ViewModel – ги трансформира податоците од Модел во формат погоден за Преглед
MVVM го користи концептот на врзување на податоци, кој го намалува количината на кодот на котлонот, но може да предизвика проблеми со протокот на податоци за следење.
MVI (Model-View-Intent) – Модел за гледање на намерата
Модерен пристап кој нагласува:
- Предвидливост – детерминистички пристап кон државното управување
- Непроменливост – состојбата не се менува, туку се заменува
- Еднонасочен проток на податоци – јасна и транспарентна секвенца на настани
MVI е особено ефикасен за сложени, богати со податоци апликации со бројни кориснички интеракции и асинхрони операции.
Зошто е создаден SimpleMVI и неговото место меѓу другите библиотеки
SimpleMVI е развиен за да им обезбеди на програмерите едноставна, но моќна алатка за имплементација на MVI шаблонот во Kotlin Multiplatform проекти.
- Се фокусира на логиката на доменот, без да наметнува решенија за слојот на корисничкиот интерфејс
- Се придржува до принципот на "једноставност пред сè", обезбедувајќи минимален сет на потребни компоненти
- Оптимизиран за Kotlin Multiplatform, обезбедувајќи компатибилност со различни платформи
- Строго ја контролира безбедноста на нишката, гарантирајќи дека интеракцијата со состојбата се случува само на главната нишка
- Обезбедува флексибилна конфигурација за справување со грешки преку системот за конфигурирање
Главните предности на SimpleMVI во споредба со алтернативите:
- Помалку зависности и помала големина на библиотеката во споредба со повеќе комплексни решенија
- Низок праг на влез за разбирање и употреба
- Целосниот пристап на Kotlin со користење на модерни јазични конструкции
- Удобен DSL за опишување на деловната логика
- Јасна поделба на одговорностите меѓу компонентите
SimpleMVI не има за цел да ги реши сите проблеми со апликациската архитектура, но обезбедува сигурна основа за организирање на деловната логика која може да се интегрира со какви било решенија за корисничкиот интерфејс, навигација и други аспекти на апликацијата.
Основни концепти и компоненти на SimpleMVI
SimpleMVI нуди минималистички пристап кон имплементацијата на MVI архитектурата, фокусирајќи се на три клучни компоненти: Store, Actor и Middleware. Секоја од овие компоненти има уникатна улога во обезбедувањето на еднонасочен проток на податоци и управување со состојбата на апликацијата.
Магазин - централен елемент на архитектурата
Дефиниција и улога на продавницата
Складиштето е срцето на 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()
}
Голем животен циклус
Магазинот има јасно дефиниран животен циклус:
-
Creation - instantiating the Store object with necessary dependencies
-
Initialization - calling the
init()
method, preparing internal components -
Active use - processing intents through the
accept(intent)
method -
Destruction - calling the
destroy()
method, releasing resources
Важно е да се разбере дека:
- Сите методи за јавна продавница мора да се повикуваат само на главната низа (означена со @MainThread анотација)
- По повикувањето уништи(), продавницата не може да се користи; обидите за пристап до уништена продавница ќе резултираат со грешка
- Магазинот мора да биде инициализиран со методот init() пред употреба
Државно управување
Магазин обезбедува следниве можности за работа со државата:
-
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 обезбедува погодни оператори за работа со намера:
// Instead of store.accept(intent)
store + MyStore.Intent.LoadData
// Instead of store.accept(intent)
store += MyStore.Intent.LoadData
Актер - имплементација на деловната логика
Принципи на работа на актерите
Actor е компонента одговорна за деловната логика во SimpleMVI. Таа ги прифаќа намерите, ги обработува и може да произведе нова состојба и несакани ефекти. 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 - за лансирање на асинхрони операции
- Функција за актуелен статус (getState)
- Државна функција за намалување (редукција)
- Нова функција за испраќање на намера (onNewIntent)
- Функција за испраќање на странични ефекти (postSideEffect)
Обид за обработка
наonIntent(intent: Intent)
методот е повикан од страна на продавницата при примање на нова намера и е главната влезна точка за деловната логика.
- Одредува видот на добиената намера
- Извршува потребната деловна логика
- Ажурирање на државата
- генерира несакани ефекти ако е потребно
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
Креирањето на сопствен 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")
}
}
Middleware може да се комбинира, создавајќи синџир на раководители:
val store = createStore(
name = storeName<MyStore>(),
initialState = MyState(),
actor = myActor,
middlewares = listOf(
loggingMiddleware,
analyticsMiddleware,
debugMiddleware
)
)
Клучни случаи на употреба за Middleware
-
Logging — recording all events for debugging
-
Analytics — tracking user actions
-
Performance metrics — measuring intent processing time
-
Debugging — visualizing data flow through UI
-
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
}
Логирањето е слично имплементирано за различни платформи:
// 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 (Андроид пример)
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 не од главната нишка
- StoreIsNotInitializedИсклучок - кога се обидувате да користите неинициализирана продавница
- StoreIsAlreadyDestroyedИсклучок - кога се обидувате да користите продавница која веќе е уништена
Тестирање на компоненти
Благодарение на чистата поделба на одговорностите, компонентите на 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
Накратко, може да се истакнат следниве предности на библиотеката:
Минималистички и прагматичен пристап
SimpleMVI обезбедува само потребните компоненти за имплементација на MVI шаблонот, без непотребни апстракции и комплексности.
Потполна поддршка за повеќеплатформата на Kotlin
Изградена на Kotlin од нула, SimpleMVI е оптимизирана за развој на повеќе платформи. библиотеката го изолира кодот специфичен за платформата преку механизмот за очекување / реалност, обезбедувајќи компатибилност со Android, iOS, macOS и wasm js.
Предвидливо државно управување
Строго придржување кон принципите на состојба непроменливост и еднонасочен проток на податоци прави апликации изградени на SimpleMVI повеќе предвидливи и помалку склони кон грешки.
Вградена заштита од заеднички проблеми
Библиотеката обезбедува строга контрола на безбедноста на нишките, обезбедувајќи дека интеракцијата со состојбата се случува само на главната нишка.
Удобен DSL за декларативен опис на логиката
Благодарение на поддршката за DSL, SimpleMVI ви овозможува да ја опишете деловната логика во декларативен стил, што го прави кодот повеќе читлив и разбирлив.
6. флексибилност и екстензибилност
И покрај минималистичкиот пристап, SimpleMVI обезбедува механизми за проширување на функционалноста преку системот Middleware.
Типични случаи на употреба
SimpleMVI е особено погоден за следниве сценарија:
Котлин мултиплатформени проекти
Ако развивате апликација која треба да работи на повеќе платформи (Android и iOS, веб апликации), SimpleMVI ви овозможува да користите еден архитектурен пристап и споделен бизнис логика код.
Апликации со комплексни состојби и кориснички интеракции
За апликации кои управуваат со сложени состојби и управуваат со бројни кориснички интеракции, пристапот MVI обезбедува јасна структура и предвидливост.
Проекти со акцент на тестираност
Благодарение на јасната поделба на одговорностите меѓу компонентите и предвидливиот проток на податоци, апликациите изградени со SimpleMVI лесно се тестираат на единица.
Миграција на постоечките проекти во MVI архитектура
SimpleMVI може да се воведе постепено, почнувајќи со поединечни модули или карактеристики, што го прави погоден за постепена миграција на постоечките проекти во MVI архитектурата.
Образовни проекти и прототипи
Поради својата едноставност и минимализам, SimpleMVI е добро погоден за учење на принципите на MVI и за брзо прототипирање.
Ресурси за понатамошно учење
За оние кои сакаат да ги продлабочат своите знаења за архитектурата SimpleMVI и MVI воопшто, ги препорачувам следниве ресурси:
- SimpleMVI GitHub репозиториум – изворниот код на библиотеката со примери за употреба
- SimpleMVI Документација – официјална документација со детален опис на API и препораки
Завршни мисли
SimpleMVI претставува избалансирано решение за организирање на апликациската деловна логика со користење на современи пристапи кон архитектурата.
При изборот на архитектура за вашиот проект, запомнете дека не постои универзално решение погодно за сите случаи. SimpleMVI може да биде одличен избор за проекти каде што се вреднуваат едноставноста, предвидливоста и поддршката за повеќе платформи, но за некои сценарија, други библиотеки или пристапи може да бидат повеќе соодветни.
Експериментирајте, истражувајте различни архитектонски решенија и изберете што најдобро одговара на потребите на вашиот проект и тим.И запомнете: Најдобрата архитектура е онаа која ви помага ефикасно да ги решите задачите на рака, а не онаа која создава дополнителна сложеност.