230 lesings

Hierdie klein Kotlin-biblioteek kan die skoonste manier wees om cross-platform-apps te bou

deur Android Insights19m2025/05/07
Read on Terminal Reader

Te lank; Om te lees

SimpleMVI is 'n ligte maar kragtige oplossing vir die implementering van die MVI patroon in Kotlin multiplatform projekte.
featured image - Hierdie klein Kotlin-biblioteek kan die skoonste manier wees om cross-platform-apps te bou
Android Insights HackerNoon profile picture
0-item

In die wêreld van mobiele ontwikkeling speel die keuse van die regte toepassingsargitektuur 'n kritieke rol in die versekering van kode kwaliteit, onderhoubaarheid en skaalbaarheid. Elke jaar bring dit nuwe benaderings, biblioteke en raamwerke wat ontwerp is om die ontwikkelingsproces te vereenvoudig en kode meer gestruktureer.


In hierdie artikel sal ons SimpleMVI ondersoek—’n ligte maar kragtige oplossing vir die implementering van die MVI patroon in Kotlin multiplatform projekte.

Wat is die MVI Pattern en sy kernkonsepte

Model-View-Intent (MVI) is 'n argitektuurpatroon vir gebruikersinterface-ontwikkeling, geïnspireer deur funksionele programmering en reaktiewe stelsels.

  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.


In die MVI architektuur:

  • Model verteenwoordig die onveranderlike toepassingstoestand wat die data wat nodig is om die UI te wys, ten volle beskryf.
  • View vertoon passief die huidige toestand en stuur gebruikersaktiwiteite as Intensies.
  • Intent beskryf die bedoelings van die gebruiker of stelsel wat moontlik die toestand van die aansoek kan verander.


Benewens hierdie kernkomponente sluit MVI dikwels in:

  • Reduser - 'n funksie wat die huidige toestand en voorneme neem en 'n nuwe toestand retourneer.
  • Side-effek - newe-effekte wat nie die toestand beïnvloed nie, maar interaksie met eksterne stelsels vereis (bv. navigeer, kennisgewing, API-versoek).

'N Korte geskiedenis van argitektuurpatrone

UI-architektiese patrone het in die loop van die tyd aansienlik ontwikkel:

Die MVC (Model-View-Controller)

Een van die eerste patrone wat die aansoek in drie komponente verdeel het:

  • Model - data en besigheidslogika
  • Kyk - gebruikersinterface
  • Controller - gebruikersinvoer hanteer


Die hoofprobleem met MVC is die stewige koppeling tussen komponente en onduidelike skeiding van verantwoordelikhede, wat toetsing en onderhoud bemoeilik.

Die MVP (Model-View-Presenter)

'N Verbetering ten opsigte van MVC, waar:

  • Model - data en besigheidslogika
  • View - passiewe gebruikersinterface
  • Presenter - bemiddelaar tussen Model en View


MVP los die toetsbaarheidsprobleem op, maar lei dikwels tot opgeblaasde Presenters en noue koppeling tussen Presenter en View.

Die MVVM (Model-View-ViewModel)

Die volgende stap in evolusie:

  • Model - data en besigheidslogika
  • Kyk - gebruikersinterface
  • ViewModel – transformeer data van Model in 'n formaat wat gerieflik is vir View


MVVM gebruik die konsep van data binding, wat die hoeveelheid boilerplate kode verminder, maar probleme met die opsporing van data vloei kan veroorsaak.

MVI (Model-View-Intensiwiteit)

'N Moderne benadering wat beklemtoon:

  • Voorspelbaarheid - 'n deterministiese benadering tot staatsbestuur
  • Immutabiliteit - toestand word nie verander nie, maar vervang
  • Unidirectionele data vloei - duidelike en transparante volgorde van gebeure


MVI is veral effektief vir komplekse, data-ryk toepassings met talle gebruikersinteraksies en asynchrone bedrywighede.

Waarom SimpleMVI geskep is en sy plek onder ander biblioteke

SimpleMVI is ontwikkel om ontwikkelaars 'n eenvoudige maar kragtige hulpmiddel te verskaf vir die implementering van die MVI-patroon in Kotlin Multiplatform-projekte.

  1. Fokus op domeinlogika, sonder om oplossings vir die UI-laag op te leg
  2. Volg die beginsel van "eenvoudighede voor alles", wat 'n minimale stel nodige komponente verskaf
  3. Is geoptimaliseer vir Kotlin Multiplatform, wat verseker kompatibiliteit met verskeie platforms
  4. Streng beheer draad veiligheid, waarborg dat die interaksie met die staat plaasvind slegs op die hoof draad
  5. Verskaf fleksibele foutbeheer konfigurasie deur die konfigurasie stelsel


Die belangrikste voordele van SimpleMVI in vergelyking met alternatiewe:

  • Minder afhanklikhede en kleiner biblioteekgrootte in vergelyking met meer komplekse oplossings
  • Laer toegangsdrempel vir begrip en gebruik
  • Volledige Kotlin benadering met behulp van moderne taalkonstruksie
  • Gerieflike DSL vir die beskrywing van besigheidslogika
  • Duidelike skeiding van verantwoordelikhede tussen komponente


SimpleMVI is nie daarop gemik om alle toepassingsargitektuurprobleme op te los nie, maar bied 'n betroubare basis vir die organisering van besigheidslogika wat met enige oplossings vir UI, navigasie en ander aspekte van die aansoek geïntegreer kan word.

Kernkonsepte en komponente van SimpleMVI

SimpleMVI bied 'n minimalistiese benadering tot die implementering van MVI-argitektuur, wat fokus op drie sleutelkomponente: Store, Actor en Middleware. Elkeen van hierdie komponente het 'n unieke rol in die versekering van eenrigtingsdata vloei en die bestuur van aansoekstoestand.

Store - die sentrale element van die argitektuur

Definisie en rol van die winkel

Store is die hart van SimpleMVI - dit is 'n container wat die toepassingstoestand hou, bedoelings verwerk en newe-effekte genereer.

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

Groot lewenscyklus

Die winkel het 'n duidelike lewenscyklus:

  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


Dit is belangrik om te verstaan dat:

  • Alle openbare Store-metodes moet slegs op die hoofdraad geroep word (gemerk met die @MainThread-aanmerking)
  • Na die oproep vernietig(), kan die Store nie gebruik word nie; pogings om toegang tot 'n vernietigde Store sal lei tot 'n fout
  • Die Store moet met die init() metode geïmplementeer word voordat dit gebruik word.

Staatsbestuur

Store bied die volgende vermoëns vir die werk met die staat:

  • Access to the current state via the state property

  • Observing state changes via the states flow

  • Processing side effects via the sideEffects flow


SimpleMVI gebruik klasse van Kotlin Coroutines vir vloeiimplementasie:StateFlowvir staats- en gereeldeFlowvir newe-effekte, die versekering van verenigbaarheid met standaard benaderings tot reaktiewe programmering in Kotlin.

Gerieflike uitbreidings vir die winkel

SimpleMVI bied gerieflike bedieners vir die werk met bedoelings:

// Instead of store.accept(intent)
store + MyStore.Intent.LoadData

// Instead of store.accept(intent)
store += MyStore.Intent.LoadData

Akteur - Business Logic Implementation

Akteurs se werkprincipe

Actor is die komponent wat verantwoordelik is vir besigheidslogika in SimpleMVI. Dit aanvaar bedoelings, verwerk dit en kan 'n nuwe toestand en newe-effekte produseer. Actor is die bemiddelaar tussen die gebruikersinterface en aansoek data.

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

Elke akteur het toegang tot:

  • CoroutineScope - vir die begin van asynchrone bedrywighede
  • Die huidige staat getter funksie (getState)
  • Staatlike verminderingsfunksie (verminder)
  • Nuwe bedoeling stuur funksie (onNewIntent)
  • Funksie vir die stuur van side-effekte (postSideEffect)

Versoek om te verwerk

dieonIntent(intent: Intent)Die metode word deur die Store genoem wanneer 'n nuwe bedoeling ontvang word en is die belangrikste ingangspunt vir besigheidslogika.

  1. Bepaal die tipe van die ontvangte bedoeling
  2. Voer die nodige besigheidslogika uit
  3. Opdatering van die staat
  4. Veroorsaak newe-effekte indien nodig

DefaultActor en DslActor: verskillende implementeringsbenaderings

SimpleMVI bied twee verskillende benaderings vir Actor-implementasie:

DefaultActor - Object-Oriented benadering

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 voordele:

  • Bekende OOP benadering
  • Gerieflik vir komplekse besigheidslogika
  • Goed geskik vir groot projekte

DslActor - Funksionele benadering met 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
    }
}

Die voordele van DslActor:

  • Meer deklaratiewe benadering
  • Minder boilerplate kode
  • Meer geskik vir klein en middelgrote projekte
  • Type-safe bedoel om te bestuur


Beide benaderings bied dieselfde funksionaliteit, en die keuse tussen hulle hang af van die ontwikkelaar se voorkeure en projekspesifikasies.

Middleware - Uitbreiding van funksionaliteit

Die doel van Middleware

Middleware in SimpleMVI optree as 'n waarnemer van gebeure in die Store. Middleware kan gebeurtenisse nie verander nie, maar kan daarop reageer, wat dit ideaal maak vir die implementering van kruisfunksionele logika soos logging, analytics, of 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)
}

Vervaardiging en debugging vermoëns

SimpleMVI bevat 'n ingeboude Middleware implementasie vir logging -LoggingMiddleware:

val loggingMiddleware = LoggingMiddleware<MyIntent, MyState, MySideEffect>(
    name = "MyStore",
    logger = DefaultLogger
)


LoggingMiddlewarevang al die gebeure in die Store en outputs hulle na die log:


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

Dit is nuttig vir debugging aangesien dit jou toelaat om die hele data vloei in die aansoek te volg.

Uitvoering van Custom Middleware

Die skep van jou eie Middleware is baie eenvoudig:

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 kan gekombineer word, wat 'n ketting van hanteerders skep:

val store = createStore(
    name = storeName<MyStore>(),
    initialState = MyState(),
    actor = myActor,
    middlewares = listOf(
        loggingMiddleware,
        analyticsMiddleware,
        debugMiddleware
    )
)

Belangrike gebruik gevalle vir 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


Dit is belangrik om te onthou dat Middleware 'n passiewe waarnemer is en nie die gebeure wat dit ontvang, kan verander nie.

Werk met die biblioteek

Installasie en opstel

Voeg die afhanklikheid by jou projek:

// build.gradle.kts
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")

Maak jou eerste winkel

Die eenvoudigste manier om 'n Store te skep, is om 'n klas wat die Store-interface implementeer, te verklaar:

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
}

Gebruik die winkel

// 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 multiplatform ondersteuning

SimpleMVI ondersteun verskeie platforms deur middel van Kotlin Multiplatform:

  • Die Android
  • die
  • Maatjies
  • Die JS

Platform-spesifieke kode isolasie meganismes gebruikexpect/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 word soortgelyk geïmplementeer vir verskillende platforms:

// 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")
}

Praktyke voorbeelde: Counter

Groot data model definisie

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

Groot implementasie

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
}

Verbinding met die UI (Android Voorbeeld)

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

gevorderde kenmerke

Konfigurasie biblioteek

SimpleMVI bied 'n fleksibele konfigurasie stelsel:

configureSimpleMVI {
    // Strict error handling mode (throws exceptions)
    strictMode = true
    
    // Logger configuration
    logger = object : Logger {
        override fun log(message: String) {
            // Your logging implementation
        }
    }
}

verkeerde manier van optree

  • strictMode = waar - die biblioteek werk in streng modus en gooi uitsonderings wanneer foute waargeneem word
  • strictMode = vals (default) - die biblioteek werk in lae modus en log slegs foute sonder om uitvoering te onderbreek

verkeerde optrede

SimpleMVI het spesiale uitsonderings:

  • NotOnMainThreadException - wanneer jy probeer om Store-metodes te bel nie van die hoofdraad
  • StoreIsNotInitializedException - wanneer jy probeer om 'n oninitialized Store te gebruik
  • StoreIsAlreadyDestroyedException - wanneer jy probeer om 'n reeds vernietigde Store te gebruik

Testeer komponente

Dankie aan skoon skeiding van verantwoordelikhede, SimpleMVI komponente is maklik om te toets:

// 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()
}

Konklusie

Aangesien mobiele ontwikkeling steeds meer kompleks word en die vereistes vir kode kwaliteit en toepassingsonderhoudbaarheid groei, word die keuse van die regte argitektuur 'n kritieke besluit. SimpleMVI bied 'n moderne, elegante benadering tot kode-organisasie gebaseer op MVI-patroonbeginsels en aangepas vir multiplatform ontwikkeling met Kotlin.

Belangrike voordele van SimpleMVI

Kortom, die volgende sterkte van die biblioteek kan beklemtoon word:

Minimalistiese en pragmatiese benadering

SimpleMVI bied slegs die nodige komponente vir die implementering van die MVI-patroon, sonder onnodige abstraksies en kompleksiteite. Die biblioteek volg die "eenvoudighede boven alles" beginsel, wat dit maklik maak om te verstaan en te gebruik, selfs vir ontwikkelaars wat net vertroud raak met MVI-argitektuur.

Volledige Kotlin multiplatform ondersteuning

Gebou op Kotlin van die grond af, is SimpleMVI geoptimaliseer vir multiplatform ontwikkeling. Die biblioteek isoleer platform-spesifieke kode deur die verwag / werklike meganisme, wat verseker kompatibiliteit met Android, iOS, macOS en wasm js.

Voorspelbare staatsbestuur

Strikte nalewing aan die beginsels van state-onveranderlikheid en eenrigtingsdata vloei maak toepassings wat op SimpleMVI gebou is, meer voorspelbaar en minder foutgevoelig.

Ingebouwde beskerming teen algemene probleme

Die biblioteek bied streng draadveiligheidsbeheer, wat verseker dat interaksie met toestand slegs op die hoofdraad plaasvind.

Gerieflike DSL vir Deklaratiewe Logiese Beskrywing

Dankie aan DSL ondersteuning, SimpleMVI kan beskryf besigheid logika in 'n deklaratiewe styl, maak die kode meer leesbaar en verstaanbaar.

Flexibiliteit en uitbreidbaarheid

Ten spyte van sy minimalistiese benadering, bied SimpleMVI meganismes vir die uitbreiding van funksionaliteit deur middel van die Middleware-stelsel.

Tipiese gebruik gevalle

SimpleMVI is veral goed geskik vir die volgende scenario's:

1.Kotlin multiplatform projekte

As jy 'n aansoek ontwikkel wat op verskeie platforms (Android en iOS, webtoepassings) moet werk, laat SimpleMVI jou toe om 'n enkele argitekturiese benadering en gedeelde besigheidslogiese kode te gebruik.

Toepassings met komplekse staat- en gebruikersinteraksies

Vir programme wat komplekse toestande bestuur en baie gebruikersinteraksies hanteer, bied die MVI-benadering 'n duidelike struktuur en voorspelbaarheid.

Projekte met 'n klem op toetsbaarheid

Deur die duidelike skeiding van verantwoordelikhede tussen komponente en voorspelbare data vloei, is toepassings wat met SimpleMVI gebou is, maklik eenheidstestbaar.

Migrasie van bestaande projekte na MVI-argitektuur

SimpleMVI kan geleidelik geïntroduceer word, begin met individuele modules of funksies, wat dit geskik maak vir geleidelike migrasie van bestaande projekte na MVI-argitektuur.

Onderwysprojekte en prototipes

As gevolg van sy eenvoud en minimalisme, is SimpleMVI goed geskik vir die onderrig van MVI-beginsels en vir vinnige prototyping.

Resources vir verdere leer

Vir diegene wat hul kennis van SimpleMVI en MVI-architektuur in die algemeen wil verdiep, beveel ek die volgende hulpbronne aan:

  • SimpleMVI GitHub repository — bron kode van die biblioteek met voorbeelde van gebruik
  • SimpleMVI Dokumentasie – amptelike dokumentasie met gedetailleerde API beskrywing en aanbevelings

Laaste gedagtes

SimpleMVI verteenwoordig 'n gebalanseerde oplossing vir die organisering van toepassing besigheid logika met behulp van moderne benaderings tot argitektuur. Die biblioteek bied 'n duidelike struktuur en voorspelbare data vloei sonder om onnodige kompleksiteit op te leg.


Wanneer jy 'n argitektuur vir jou projek kies, onthou dat daar geen universele oplossing is wat geskik is vir alle gevalle nie. SimpleMVI kan 'n uitstekende keuse wees vir projekte waar eenvoud, voorspelbaarheid en multiplatform ondersteuning waardeer word, maar vir sommige scenario's kan ander biblioteke of benaderings meer geskik wees.


Eksperimenteer, ondersoek verskillende argitektuuroplossings en kies wat die beste pas by die behoeftes van jou projek en span. En onthou: die beste argitektuur is een wat jou help om die take effektief op te los, nie een wat bykomende kompleksiteit skep nie.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks