Vo svete mobilného vývoja zohráva výber správnej architektúry aplikácií kľúčovú úlohu pri zabezpečovaní kvality kódu, udržateľnosti a škálovateľnosti. Každý rok prináša nové prístupy, knižnice a rámce navrhnuté tak, aby zjednodušili vývojový proces a štruktúrovali kód.V posledných rokoch získala architektúra MVI (Model-View-Intent) osobitnú popularitu tým, že ponúka elegantné riešenie pre správu stavu aplikácie a organizáciu jednosmerného toku dát.
V tomto článku sa pozrieme na SimpleMVI – ľahké, ale výkonné riešenie na implementáciu vzoru MVI v projektoch s viacerými platformami Kotlin.Budeme skúmať základné komponenty knižnice, jej funkcie a analyzovať praktické príklady, ktoré vám pomôžu pochopiť, ako aplikovať SimpleMVI vo vašich projektoch.
Čo je MVI vzor a jeho základné pojmy
Model-View-Intent (MVI) je architektonický vzor pre vývoj používateľského rozhrania, inšpirovaný funkčným programovaním a reaktívnymi systémami.
-
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.
V architektúre MVI:
- Model predstavuje nemenný stav aplikácie, ktorý úplne opisuje údaje potrebné na zobrazenie rozhrania používateľa.
- Zobrazenie pasívne zobrazuje aktuálny stav a prenáša používateľské akcie ako zámery.
- Úmysel opisuje zámery používateľa alebo systému, ktoré môžu potenciálne zmeniť stav aplikácie.
Okrem týchto základných zložiek MVI často zahŕňa:
- Reduktor – funkcia, ktorá berie aktuálny stav a zámer a vráti nový stav.
- Vedľajšie účinky – vedľajšie účinky, ktoré neovplyvňujú stav, ale vyžadujú interakciu s externými systémami (napr. navigácia, oznámenia, požiadavky API).
Stručná história architektonických vzorov
Architektonické vzory používateľského rozhrania sa v priebehu času výrazne vyvinuli:
MVC (Model View Controller – ovládač zobrazenia modelu)
Jeden z prvých vzorcov, ktorý rozdelil aplikáciu na tri komponenty:
- Model – údaje a obchodná logika
- View – používateľské rozhranie
- Controller – manipulácia s používateľským vstupom
Hlavným problémom MVC je tesné spájanie komponentov a nejasné oddelenie zodpovedností, čo komplikuje testovanie a údržbu.
MVP (modelový prezentátor)
Zlepšenie v porovnaní s MVC, kde:
- Model – údaje a obchodná logika
- View – pasívne používateľské rozhranie
- Presenter – sprostredkovateľ medzi modelom a pohľadom
MVP rieši problém testovateľnosti, ale často vedie k napučaným prezentátorom a tesnému spojeniu medzi prezentátorom a zobrazením.
MVVM (Model-View-ViewModel – modelový pohľad)
Ďalší krok v evolúcii:
- Model – údaje a obchodná logika
- View – používateľské rozhranie
- ViewModel – transformuje údaje z modelu do formátu vhodného pre zobrazenie
MVVM používa koncept viazania údajov, ktorý znižuje množstvo kódu boilerplate, ale môže spôsobiť problémy so sledovaním toku údajov.
MVI (Modelový pohľad na úmysel)
Moderný prístup, ktorý zdôrazňuje:
- Predvídateľnosť – deterministický prístup k štátnemu riadeniu
- Immutabilita – stav sa nemení, ale nahrádza
- Jednosmerný tok údajov – jasná a transparentná sekvencia udalostí
MVI je obzvlášť účinný pre komplexné, dátovo bohaté aplikácie s početnými používateľskými interakciami a asynchrónnymi operáciami.
Prečo bol vytvorený SimpleMVI a jeho miesto medzi ostatnými knižnicami
SimpleMVI bol vyvinutý s cieľom poskytnúť vývojárom jednoduchý, ale výkonný nástroj na implementáciu vzoru MVI v projektoch Kotlin Multiplatform.
- Zameriava sa na logiku domény bez uloženia riešení pre vrstvu rozhrania používateľa
- Dodržiava princíp „jednoduchosti predovšetkým“, poskytuje minimálny súbor potrebných komponentov
- Je optimalizovaný pre Kotlin Multiplatform, zabezpečuje kompatibilitu s rôznymi platformami
- Prísne kontroluje bezpečnosť vlákien, čo zaručuje, že interakcia so stavom sa vyskytuje iba na hlavnom vlákne
- Poskytuje flexibilnú konfiguráciu manipulácie s chybami prostredníctvom systému konfigurácie
Hlavné výhody SimpleMVI oproti alternatívam:
- Menej závislostí a menšia veľkosť knižnice v porovnaní s komplexnejšími riešeniami
- Nižší vstupný prah pre pochopenie a použitie
- Kompletný Kotlin prístup s modernými jazykovými konštrukciami
- Pohodlný DSL pre popis obchodnej logiky
- Jasné rozdelenie zodpovednosti medzi zložkami
SimpleMVI nemá za cieľ vyriešiť všetky problémy architektúry aplikácií, ale poskytuje spoľahlivý základ pre organizáciu obchodnej logiky, ktorá môže byť integrovaná s akýmikoľvek riešeniami pre rozhranie používateľa, navigáciu a ďalšie aspekty aplikácie.
Základné pojmy a komponenty SimpleMVI
SimpleMVI ponúka minimalistický prístup k implementácii architektúry MVI so zameraním na tri kľúčové komponenty: Store, Actor a Middleware.Každá z týchto zložiek má jedinečnú úlohu pri zabezpečovaní jednosmerného toku dát a spravovaní stavu aplikácie.
Skladovanie - ústredný prvok architektúry
Definícia a úloha obchodu
Store je srdcom SimpleMVI – je to kontajner, ktorý uchováva stav aplikácie, spracováva zámery a vytvára vedľajšie účinky.
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()
}
Veľký životný cyklus
Obchod má jasne definovaný životný cyklus:
-
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
Je dôležité pochopiť, že:
- Všetky metódy verejného obchodu musia byť vyvolané iba na hlavnom drôte (označené @MainThread)
- Po zavolaní zničiť(), Obchod nie je možné použiť; pokusy o prístup k zničenému Obchodu povedú k chybe
- Obchod musí byť inicializovaný metódou init() pred použitím
Štátne riadenie
Obchod poskytuje nasledujúce možnosti pre prácu so štátom:
-
Access to the current state via the
state
property -
Observing state changes via the
states
flow -
Processing side effects via the
sideEffects
flow
SimpleMVI používa triedy z Kotlin Coroutines pre implementáciu toku:StateFlow
pre štáty a pravidelnéFlow
pre vedľajšie účinky, zabezpečenie kompatibility so štandardnými prístupmi k reaktívnemu programovaniu v Kotlin.
Pohodlné rozšírenia do obchodu
SimpleMVI poskytuje pohodlné operátory pre prácu s úmyslami:
// Instead of store.accept(intent)
store + MyStore.Intent.LoadData
// Instead of store.accept(intent)
store += MyStore.Intent.LoadData
Poskytovateľ - Implementácia obchodnej logiky
Pracovné princípy aktérov
Actor je komponent zodpovedný za obchodnú logiku v SimpleMVI. Prijíma zámery, spracováva ich a môže produkovať nový stav a vedľajšie účinky. Actor je sprostredkovateľom medzi používateľským rozhraním a dátami aplikácie.
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()
}
Každý účastník má prístup k:
- CoroutineScope - pre spustenie asynchrónnych operácií
- Funkcia aktuálneho stavu (getState)
- Funkcia štátnej redukcie (zníženie)
- Nová funkcia odosielania zámerov (onNewIntent)
- Funkcia odosielania vedľajších efektov (postSideEffect)
pokus o spracovanie
naonIntent(intent: Intent)
metóda je vyzvaná Obchodom pri prijímaní nového zámeru a je hlavným vstupným bodom pre obchodnú logiku.
- Určuje typ prijatého zámeru
- Vykonáva potrebnú obchodnú logiku
- Aktualizácia štátu
- V prípade potreby vedľajšie účinky
DefaultActor a DslActor: Rôzne prístupy k implementácii
SimpleMVI ponúka dva rôzne prístupy k implementácii Actor:
DefaultActor – objektovo orientovaný prístup
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
}
}
Výhody DefaultActor:
- Známy prístup OOP
- Vhodné pre komplexnú obchodnú logiku
- Vhodné pre veľké projekty
DslActor - funkčný prístup s 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
}
}
Výhody DslActor:
- Viac deklaratívny prístup
- Menej boilerplate kód
- Vhodnejšie pre malé a stredné projekty
- Typ Bezpečné úmyselné zaobchádzanie
Oba prístupy poskytujú rovnakú funkčnosť a voľba medzi nimi závisí od preferencií vývojára a špecifiká projektu.
Middleware – rozšírenie funkčnosti
Účel Middleware
Middleware v SimpleMVI pôsobí ako pozorovateľ udalostí v Obchode. Middleware nemôže modifikovať udalosti, ale môže na ne reagovať, čo z neho robí ideálny pre implementáciu cross-funkčnej logiky, ako je logovanie, analýza alebo debugging.
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)
}
Schopnosť skladovania a debugovania
SimpleMVI obsahuje vstavanú Middleware implementáciu pre logovanie –LoggingMiddleware
:
val loggingMiddleware = LoggingMiddleware<MyIntent, MyState, MySideEffect>(
name = "MyStore",
logger = DefaultLogger
)
LoggingMiddleware
zachytáva všetky udalosti v Obchode a odosiela ich do denníka:
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
To je užitočné pre riešenie problémov, pretože vám umožní sledovať celý tok údajov v aplikácii.
Implementácia Custom Middleware
Vytvorenie vlastného Middleware je veľmi jednoduché:
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 možno kombinovať a vytvoriť reťazec manipulátorov:
val store = createStore(
name = storeName<MyStore>(),
initialState = MyState(),
actor = myActor,
middlewares = listOf(
loggingMiddleware,
analyticsMiddleware,
debugMiddleware
)
)
Kľúčové prípady použitia pre 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
Je dôležité si uvedomiť, že Middleware je pasívny pozorovateľ a nemôže modifikovať udalosti, ktoré dostáva.
Spolupráca s knižnicou
Inštalácia a inštalácia
Pridanie závislosti do vášho projektu:
// build.gradle.kts
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")
Vytvorenie vášho prvého obchodu
Najjednoduchší spôsob, ako vytvoriť obchod, je deklarovať triedu implementujúcu rozhranie obchodu:
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
}
Používanie obchodu
// 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()
Podpora viacerých platforiem Kotlin
SimpleMVI podporuje rôzne platformy prostredníctvom Kotlin Multiplatform:
- Androidový
- IOS
- Mačiatko
- Šťava JS
Použitie mechanizmov izolácie kódu špecifických pre platformuexpect/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 je podobne implementovaný pre rôzne platformy:
// 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")
}
Praktický príklad: Counter
Definícia veľkého dátového modelu
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
}
}
Veľká implementácia
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
}
Pripojenie k UI (Android Príklad)
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)
}
}
Pokročilé funkcie
Konfigurácia knižnice
SimpleMVI poskytuje flexibilný systém konfigurácie:
configureSimpleMVI {
// Strict error handling mode (throws exceptions)
strictMode = true
// Logger configuration
logger = object : Logger {
override fun log(message: String) {
// Your logging implementation
}
}
}
Chybné spôsoby konania
- strictMode = true - knižnica pracuje v striktnom režime a hodí výnimky, keď sa zistia chyby
- strictMode = false (predvolené) - knižnica funguje v režime lenivosti a iba zaznamenáva chyby bez prerušenia vykonávania
Chybné konanie
SimpleMVI má špeciálne výnimky:
- NotOnMainThreadException - pri pokuse o volanie metód Store nie z hlavného drôtu
- StoreIsNotInitializedException - pri pokuse o použitie neinicializovaného obchodu
- StoreIsAlreadyDestroyedException - pri pokuse o použitie už zničeného obchodu
Testovanie komponentov
Vďaka čistému oddeleniu zodpovedností je jednoduché otestovať komponenty 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()
}
záver
Keďže mobilný vývoj sa stáva čoraz zložitejší a požiadavky na kvalitu kódu a udržateľnosť aplikácií rastú, výber správnej architektúry sa stáva rozhodujúcim rozhodnutím. SimpleMVI ponúka moderný, elegantný prístup k organizácii kódu založený na princípoch vzorov MVI a prispôsobený pre multiplatformný vývoj s Kotlinom.
Hlavné výhody SimpleMVI
V súhrne možno zdôrazniť nasledujúce silné stránky knižnice:
Minimalizmus a pragmatický prístup
SimpleMVI poskytuje len potrebné komponenty pre implementáciu vzoru MVI, bez zbytočných abstrakcií a zložitostí. knižnica dodržiava princíp "jednoduchosti predovšetkým", čo z neho robí jednoduché pochopiť a používať aj pre vývojárov, ktorí sa práve zoznámili s architektúrou MVI.
Plná podpora Kotlin Multiplatform
Knižnica izoluje kód špecifický pre platformu prostredníctvom mechanizmu expect/real, čím zabezpečuje kompatibilitu s Androidom, iOS, macOS a wasm js.
Predvídateľný štátny manažment
Prísne dodržiavanie zásad štátnej nemennosti a jednosmerného toku dát robí aplikácie postavené na SimpleMVI predvídateľnejšie a menej náchylné na chyby.Každá zmena stavu prebieha prostredníctvom jasne definovaného procesu, ktorý zjednodušuje debugovanie a testovanie.
Vstavaná ochrana proti bežným problémom
Knižnica poskytuje prísnu kontrolu bezpečnosti vlákien, ktorá zaisťuje, že interakcia so stavom sa vyskytuje iba na hlavnom vlákne.Toto zabraňuje mnohým bežným chybám súvisiacim s multithreading, ktoré môžu byť ťažké zistiť a opraviť.
Pohodlný DSL pre deklaratívny logický popis
Vďaka podpore DSL umožňuje SimpleMVI popísať obchodnú logiku v deklaratívnom štýle, čím sa kód stáva čitateľnejším a zrozumiteľnejším.
Flexibilita a rozšíriteľnosť
Napriek minimalistickému prístupu poskytuje SimpleMVI mechanizmy na rozšírenie funkčnosti prostredníctvom systému Middleware.To uľahčuje pridávanie funkcií, ako je logovanie, analýza alebo debugging bez ovplyvnenia základnej obchodnej logiky.
Typické prípady použitia
SimpleMVI je obzvlášť vhodný pre nasledujúce scenáre:
Projekty Kotlin Multiplatform
Ak vyvíjate aplikáciu, ktorá potrebuje pracovať na viacerých platformách (Android a iOS, webové aplikácie), SimpleMVI vám umožňuje používať jediný architektonický prístup a zdieľaný obchodný logický kód.
Aplikácie so zložitými štátnymi a užívateľskými interakciami
Pre aplikácie, ktoré spravujú komplexný stav a zaoberajú sa početnými používateľskými interakciami, prístup MVI poskytuje jasnú štruktúru a predvídateľnosť.
Projekty s dôrazom na testovateľnosť
Vďaka jasnému oddeleniu zodpovednosti medzi komponentmi a predvídateľnému toku údajov sú aplikácie postavené pomocou SimpleMVI ľahko jednotkovo testovateľné.
Migrácia existujúcich projektov do architektúry MVI
SimpleMVI je možné zaviesť postupne, počnúc jednotlivými modulmi alebo funkciami, čím je vhodný pre postupnú migráciu existujúcich projektov do architektúry MVI.
Vzdelávacie projekty a prototypy
Vďaka svojej jednoduchosti a minimalizmu je SimpleMVI vhodný na výučbu princípov MVI a na rýchle prototypovanie.
Zdroje pre ďalšie vzdelávanie
Pre tých, ktorí chcú prehĺbiť svoje vedomosti o architektúre SimpleMVI a MVI vo všeobecnosti, odporúčam nasledujúce zdroje:
- SimpleMVI GitHub repository – zdrojový kód knižnice s príkladmi použitia
- Dokumentácia SimpleMVI – oficiálna dokumentácia s podrobným popisom a odporúčaniami API
Konečné myšlienky
SimpleMVI predstavuje vyvážené riešenie pre organizovanie podnikovej logiky aplikácií pomocou moderných prístupov k architektúre. Knižnica ponúka jasnú štruktúru a predvídateľný tok dát bez toho, aby si vyžadovala zbytočnú zložitosť.
Pri výbere architektúry pre váš projekt nezabudnite, že neexistuje univerzálne riešenie vhodné pre všetky prípady. SimpleMVI môže byť vynikajúcou voľbou pre projekty, kde sa oceňuje jednoduchosť, predvídateľnosť a podpora viacerých platforiem, ale pre niektoré scenáre môžu byť vhodnejšie iné knižnice alebo prístupy.
Experimentujte, skúmajte rôzne architektonické riešenia a vyberte to, čo najlepšie vyhovuje potrebám vášho projektu a tímu.A pamätajte: najlepšia architektúra je tá, ktorá vám pomôže efektívne riešiť úlohy, ktoré sú na dosah ruky, nie tá, ktorá vytvára dodatočnú zložitosť.