U svetu mobilnog razvoja, izbor prave arhitekture aplikacija igra ključnu ulogu u osiguravanju kvaliteta koda, održivosti i skalabilnosti. Svake godine donosi nove pristupe, biblioteke i okvire dizajnirane da pojednostave proces razvoja i učine kod strukturiranijim.
U ovom članku ćemo ispitati SimpleMVI – lagano, ali moćno rješenje za implementaciju MVI uzorka u Kotlin multiplatform projekata. Mi ćemo istražiti osnovne komponente biblioteke, njegove karakteristike i analizirati praktične primjere koji će vam pomoći da razumete kako primijeniti SimpleMVI u svojim projektima.
Šta je MVI obrazac i njegovi osnovni koncepti
Model-View-Intent (MVI) je arhitektonski model za razvoj korisničkog sučelja, inspiriran funkcionalnim programiranjem i reaktivnim sistemima.
-
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.
U MVI arhitekturi:
- Model predstavlja nepromjenjivo stanje aplikacije koje u potpunosti opisuje podatke potrebne za prikaz interfejsa korisnika.
- Vizija pasivno prikazuje trenutno stanje i prenosi korisničke akcije kao namjere.
- Namera opisuje namere korisnika ili sistema koji mogu potencijalno promeniti stanje aplikacije.
Pored ovih osnovnih komponenti, MVI često uključuje:
- Reduktor – funkcija koja uzima trenutno stanje i namjeru i vraća novo stanje.
- Side Effect – nuspojave koje ne utječu na stanje, ali zahtijevaju interakciju s vanjskim sistemima (npr. navigacija, obaveštenja, API zahtevi).
Kratka istorija arhitektonskih uzoraka
UI arhitektonski obrasci su se značajno razvili tijekom vremena:
MVC (Model View Controller) – upravljač za prikaz modela
Jedan od prvih uzoraka koji su podijelili aplikaciju u tri komponente:
- Model - podaci i poslovna logika
- Interfejs korisnika - User Interface
- Kontrolor – rukovanje unosom korisnika
Glavni problem sa MVC-om je tesno povezivanje komponenti i nejasna odvajanja odgovornosti, što komplicira testiranje i održavanje.
MVP (prezentacija modela)
Poboljšanje u odnosu na MVC, gde:
- Model - podaci i poslovna logika
- Pregled - pasivni korisnički interfejs
- Posrednik – posrednik između modela i pogleda
MVP rješava problem testiranosti, ali često dovodi do natečenih Prezentera i uske povezanosti između Prezentera i Prikaza.
MVVM (Model-View-ViewModel) je sistem koji se koristi za
Sljedeći korak u evoluciji:
- Model - podaci i poslovna logika
- Interfejs korisnika - User Interface
- ViewModel – pretvara podatke iz Modela u format pogodan za pregled
MVVM koristi koncept vezanja podataka, koji smanjuje količinu kotelijskog koda, ali može uzrokovati probleme s protokom praćenja podataka.
MVI (Model View – namjera)
Moderan pristup koji naglašava:
- Predvidivost – deterministski pristup državnom upravljanju
- Immutabilnost – stanje se ne menja, već zamjenjuje
- Jednosmerni protok podataka – jasna i transparentna sekvenca događaja
MVI je posebno učinkovit za složene, podatke bogate aplikacije s brojnim interakcijama korisnika i asinkronim operacijama.
Zašto je SimpleMVI stvoren i njegovo mjesto među drugim bibliotekama
SimpleMVI je razvijen kako bi programerima pružio jednostavan, ali moćan alat za implementaciju MVI obrasca u Kotlin Multiplatform projektima.
- Fokusira se na logiku domena, bez nametanja rešenja za sloj UI
- Pristupa principu "jednostavnost iznad svega", osiguravajući minimalni skup potrebnih komponenti
- je optimiziran za Kotlin Multiplatform, osiguravajući kompatibilnost sa različitim platformama
- Strogo kontrolira sigurnost žica, jamčeći da se interakcija sa stanjem javlja samo na glavnoj žici
- Pruža fleksibilnu konfiguraciju upravljanja greškama kroz sistem konfiguracije
Glavne prednosti SimpleMVI u odnosu na alternative:
- Manje ovisnosti i manja veličina biblioteke u poređenju sa složenijim rešenjima
- Niži prag ulaska za razumijevanje i upotrebu
- Kompletni Kotlin pristup koristeći moderne jezične konstrukte
- Praktičan DSL za opisivanje poslovne logike
- Jasno razdvajanje odgovornosti između komponenti
SimpleMVI ne nastoji riješiti sve probleme arhitekture aplikacija, već pruža pouzdanu osnovu za organizaciju poslovne logike koja se može integrirati sa bilo kojim rješenjima za UI, navigaciju i druge aspekte aplikacije.
Osnovni koncepti i komponente SimpleMVI
SimpleMVI nudi minimalistički pristup implementaciji MVI arhitekture, fokusirajući se na tri ključne komponente: Store, Actor i Middleware. Svaka od ovih komponenti ima jedinstvenu ulogu u osiguravanju jednosmernog protoka podataka i upravljanju statusom aplikacije.
Trgovina – centralni element arhitekture
Definicija i uloga trgovine
Store je srce SimpleMVI-a – to je kontejner koji drži stanje aplikacije, obrađuje namere i generira nuspojave. Store inkapsulira svu logiku vezanu za podatke, pružajući jedinstveni izvor istine za korisnički interfejs.
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()
}
Veliki životni ciklus
Trgovina ima jasno definisan životni ciklus:
-
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
Važno je shvatiti da:
- Sve metode javne Trgovine moraju biti pozvane samo na glavnu traku (označeno @MainThread anotacijom)
- Nakon pozivanja uništiti(), Trgovina se ne može koristiti; pokušaji pristupa uništenoj Trgovini će rezultirati greškom
- Trgovina mora biti inicijalizovana metodom init() pre upotrebe
Državno upravljanje
Trgovina pruža sledeće mogućnosti za rad sa državom:
-
Access to the current state via the
state
property -
Observing state changes via the
states
flow -
Processing side effects via the
sideEffects
flow
SimpleMVI koristi klase iz Kotlin Coroutines za implementaciju protoka:StateFlow
Za države članice i redovneFlow
za nuspojave, osiguravajući kompatibilnost sa standardnim pristupima reaktivnom programiranju u Kotlin.
Prikladna proširenja za trgovinu
SimpleMVI pruža praktične operatore za rad s namjerama:
// Instead of store.accept(intent)
store + MyStore.Intent.LoadData
// Instead of store.accept(intent)
store += MyStore.Intent.LoadData
Poslovna logika – implementacija poslovne logike
Načela rada aktera
Actor je komponenta odgovorna za poslovnu logiku u SimpleMVI. Ona prihvaća namjere, obrađuje ih i može proizvesti novi status i nuspojave. Actor je posrednik između korisničkog sučelja i podataka aplikacije.
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()
}
Svaki učesnik ima pristup:
- CoroutineScope - za pokretanje asinkronih operacija
- Funkcija trenutnog stanja (getState)
- Funkcija državnog smanjenja (smanjenje)
- Nova funkcija slanja namera (onNewIntent)
- Funkcija slanja nuspojava (postSideEffect)
Pokušaj obrade
NašionIntent(intent: Intent)
Metoda je pozvana od strane Trgovine kada prima novu namjeru i glavna je ulazna tačka za poslovnu logiku.
- Određuje vrstu primljene namjere
- Obavlja potrebnu poslovnu logiku
- Aktualizirajte državu
- Generira nuspojave ako je potrebno
DefaultActor i DslActor: Različiti pristupi implementaciji
SimpleMVI nudi dva različita pristupa implementaciji Actora:
DefaultActor - Objektno orijentiran pristup
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
}
}
Prednosti DefaultActor:
- Poznati OOP pristup
- Pogodan za kompleksnu poslovnu logiku
- Pogodan za velike projekte
DslActor - funkcionalni pristup sa 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 prednosti:
- Više deklarativni pristup
- Manje boilerplate kod
- Bolje pogodan za male i srednje projekte
- Tip-sigurno namjerno rukovanje
Oba pristupa pružaju istu funkcionalnost, a izbor između njih zavisi od preferencija programera i specifičnosti projekta.
Middleware – proširenje funkcionalnosti
Svrha Middleware
Middleware u SimpleMVI djeluje kao promatrač događaja u Trgovini. Middleware ne može modifikovati događaje, ali može reagovati na njih, što ga čini idealnim za implementaciju interfunkcionalne logike kao što su logging, analitika ili 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)
}
Snabdevanje i debugging sposobnosti
SimpleMVI uključuje ugrađenu Middleware implementaciju za logiranje –LoggingMiddleware
:
val loggingMiddleware = LoggingMiddleware<MyIntent, MyState, MySideEffect>(
name = "MyStore",
logger = DefaultLogger
)
LoggingMiddleware
sakuplja sve događaje u Trgovini i izlazi ih u dnevnik:
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
Ovo je korisno za debugiranje jer vam omogućuje praćenje cijelog toka podataka u aplikaciji.
Uvođenje Custom Middleware
Stvaranje sopstvenog Middleware je vrlo jednostavno:
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 se može kombinovati, stvarajući lanac rukovaoca:
val store = createStore(
name = storeName<MyStore>(),
initialState = MyState(),
actor = myActor,
middlewares = listOf(
loggingMiddleware,
analyticsMiddleware,
debugMiddleware
)
)
Ključni slučajevi upotrebe za 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
Važno je zapamtiti da je Middleware pasivni promatrač i ne može modificirati događaje koje prima.
Rad sa bibliotekom
Instalacija i postavljanje
Dodavanje zavisnosti vašem projektu:
// build.gradle.kts
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")
Kreirajte svoju prvu radnju
Najjednostavniji način za kreiranje trgovine je deklarisanje klase koja implementira interfejs trgovine:
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
}
Korišćenje prodavnice
// 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()
Podrška za više platformi Kotlin
SimpleMVI podržava različite platforme putem Kotlin Multiplatform:
- Android uređaji
- IOS
- Mačevi
- Smeštaj JS
Upotreba mehanizama za izolaciju koda specifičnih za 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 na sličan način implementiran za različite platforme:
// 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")
}
Praktični primjeri: kontrola
Definicija modela velikih podataka
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
}
}
Velika implementacija
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
}
Povezivanje na UI (Android Primjer)
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)
}
}
Napredne karakteristike
Konfiguracija biblioteke
SimpleMVI pruža fleksibilan sistem konfiguracije:
configureSimpleMVI {
// Strict error handling mode (throws exceptions)
strictMode = true
// Logger configuration
logger = object : Logger {
override fun log(message: String) {
// Your logging implementation
}
}
}
Pogrešni načini ponašanja
- strictMode = true - biblioteka radi u strogom načinu rada i baca iznimke kada se otkriju greške
- strictMode = false (podrazumevano) - biblioteka radi u opuštenom načinu rada i samo bilježi greške bez prekida izvršenja
Pogrešno ponašanje
SimpleMVI ima posebne iznimke:
- NotOnMainThreadException - kada pokušavate pozvati Metode trgovine ne iz glavnog trena
- StoreIsNotInitializedIzuzetak - kada pokušavate koristiti neinitializovanu Trgovinu
- StoreIsAlreadyDestroyedIzuzetak - kada pokušavate koristiti već uništenu Trgovinu
Testiranje komponenti
Zahvaljujući čistim odvajanju odgovornosti, SimpleMVI komponente su jednostavne za testiranje:
// 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()
}
Zaključak
Kako razvoj mobilnih uređaja postaje sve složeniji, a zahtjevi za kvalitetom koda i održivosti aplikacija rastu, odabir prave arhitekture postaje ključna odluka. SimpleMVI nudi moderan, elegantan pristup organizaciji koda zasnovan na principima MVI uzoraka i prilagođen za multiplatform razvoj sa Kotlinom.
Ključne prednosti SimpleMVI
Ukratko, mogu se istaknuti sledeće snage biblioteke:
Minimalistički i pragmatični pristup
SimpleMVI pruža samo potrebne komponente za implementaciju MVI obrasca, bez nepotrebnih abstrakcija i složenosti. Knjižnica slijedi princip "jednostavnost iznad svega", čineći ga lako razumjeti i koristiti čak i za programere koji tek upoznaju MVI arhitekturu.
Potpuna podrška za više platformi Kotlin
Izgrađen na Kotlinu od početka, SimpleMVI je optimizovan za razvoj na više platformi. Knjižnica izolira kod specifičan za platformu kroz mehanizam očekivanja / stvarnosti, osiguravajući kompatibilnost sa Androidom, iOS-om, macOS-om i wasm js-om.
Predvidljivo državno upravljanje
Strogo pridržavanje principa nepromjenjivosti stanja i jednosmernog protoka podataka čini aplikacije izgrađene na SimpleMVI predvidljivijim i manje podložnim greškama.
Ugrađena zaštita od uobičajenih problema
Knjižnica pruža strogu bezbednosnu kontrolu niza, osiguravajući da se interakcija sa statusom javlja samo na glavnom nizu.To sprečava mnoge uobičajene greške povezane s multithreading koji mogu biti teško otkriti i popraviti.
Praktičan DSL za deklarativni opis logike
Zahvaljujući DSL podršci, SimpleMVI omogućuje opisivanje poslovne logike u deklarativnom stilu, čineći kod čitljiviji i razumljiviji.
Fleksibilnost i proširenost
Unatoč svom minimalističkom pristupu, SimpleMVI pruža mehanizme za proširenje funkcionalnosti kroz sistem Middleware.
Tipični slučajevi korištenja
SimpleMVI je posebno pogodan za sljedeće scenarije:
Kotlin multiplatformni projekti
Ako razvijate aplikaciju koja treba da radi na više platformi (Android i iOS, web aplikacije), SimpleMVI vam omogućuje da koristite jedan arhitektonski pristup i zajednički kod poslovne logike.
Aplikacije sa složenom državom i interakcijama korisnika
Za aplikacije koje upravljaju složenim stanjem i rukuju brojnim interakcijama korisnika, MVI pristup pruža jasnu strukturu i predvidljivost.
Projekti sa naglaskom na testiranosti
Zahvaljujući jasnom razdvajanju odgovornosti između komponenti i predvidljivom protoku podataka, aplikacije izgrađene pomoću SimpleMVI-a lako se mogu testirati na jedinicu.
Migracija postojećih projekata u MVI arhitekturu
SimpleMVI se može uvesti postupno, počevši od pojedinačnih modula ili funkcija, što ga čini pogodnim za postupnu migraciju postojećih projekata u MVI arhitekturu.
Obrazovni projekti i prototipi
Zbog svoje jednostavnosti i minimalizma, SimpleMVI je dobro pogodan za podučavanje principa MVI i za brzo prototipiranje.
Sredstva za daljnje učenje
Za one koji žele produbiti svoje znanje o SimpleMVI i MVI arhitekturi općenito, preporučujem sledeće resurse:
- SimpleMVI GitHub repozitorij – izvorni kod biblioteke sa primjerima upotrebe
- SimpleMVI dokumentacija – službena dokumentacija sa detaljnim opisom API-ja i preporukama
Završne misli
SimpleMVI predstavlja uravnoteženo rešenje za organizovanje aplikacijske poslovne logike koristeći moderne pristupe arhitekturi. Knjižnica nudi jasnu strukturu i predvidljiv protok podataka bez nametanja nepotrebne kompleksnosti.
Prilikom odabira arhitekture za vaš projekat, imajte na umu da ne postoji univerzalno rješenje pogodno za sve slučajeve. SimpleMVI može biti odličan izbor za projekte u kojima se ceni jednostavnost, predvidljivost i multiplatform podrška, ali za neke scenarije, druge biblioteke ili pristupi mogu biti prikladniji.
Eksperimentirajte, istražite različita arhitektonska rešenja i odaberite ono što najbolje odgovara potrebama vašeg projekta i tima.I zapamtite: najbolja arhitektura je ona koja vam pomaže da efikasno rešite zadatke, a ne ona koja stvara dodatnu složenost.