230 čitanja

Ova mala Kotlin knjižnica može biti najčišći način izgradnje cross-platform aplikacija

by Android Insights19m2025/05/07
Read on Terminal Reader

Predugo; Citati

SimpleMVI je lagano, ali moćno rešenje za implementaciju MVI obrasca u Kotlin multiplatform projektima.
featured image - Ova mala Kotlin knjižnica može biti najčišći način izgradnje cross-platform aplikacija
Android Insights HackerNoon profile picture
0-item

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.

  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.


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.

  1. Fokusira se na logiku domena, bez nametanja rešenja za sloj UI
  2. Pristupa principu "jednostavnost iznad svega", osiguravajući minimalni skup potrebnih komponenti
  3. je optimiziran za Kotlin Multiplatform, osiguravajući kompatibilnost sa različitim platformama
  4. Strogo kontrolira sigurnost žica, jamčeći da se interakcija sa stanjem javlja samo na glavnoj žici
  5. 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:

  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


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:StateFlowZa države članice i redovneFlowza 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.

  1. Određuje vrstu primljene namjere
  2. Obavlja potrebnu poslovnu logiku
  3. Aktualizirajte državu
  4. 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
)


LoggingMiddlewaresakuplja 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

  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


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.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks