230 чытанні

Гэта маленькая бібліятэка Kotlin можа быць самым чыстым спосабам будаўніцтва крос-платформных прыкладанняў

па Android Insights19m2025/05/07
Read on Terminal Reader

Занадта доўга; Чытаць

SimpleMVI з'яўляецца лёгкім, але магутным рашэннем для рэалізацыі мадэлі MVI ў мультыплатформывых праектах Kotlin.
featured image - Гэта маленькая бібліятэка Kotlin можа быць самым чыстым спосабам будаўніцтва крос-платформных прыкладанняў
Android Insights HackerNoon profile picture
0-item

Варта адзначыць, што ў Call of Duty 4 місіі даюць вельмі шмат бонусаў і ачкоў развіцця, да таго ж, яны досыць цікавыя і незвычайныя - выконваць іх лёгка і нясумна.


Спадзяёмся, што матэрыяльная падтрымка будзе і з боку Міністэрства культуры, а таксама фонду краін СНД, цяпер складаем праграму, якую павязём у Маскву на ўзгадненне, будзем там адстойваць свае пазіцыі.

ЕРБ: Што для вас азначае быць вольным?

Model-View-Intent (MVI) — архітэктурны патэнт для распрацоўкі карыстальніцкіх інтэрфейсаў, натхненны функцыянальным праграмаваннем і рэактыўнымі сістэмамі.

  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.


У галіне архітэктуры:

  • Мае назіранні прывялі да высновы, што нагрузка на доктара не можа быць такой, якая яна ёсць цяпер.
  • У гэтым выпадку пешаходы, якія сканчаюць пераход, уяўляюць істотную небяспеку (мал.
  • Гэта рэпрэсіўны механізм, які працуе на дыктатуру.


Акрамя гэтых асноўных кампанентаў, MVI часта ўключае:

  • Укладайце больш ва ўласную журналістыку, СМІ і змагайцеся за свабоду слова.
  • Побачныя эфекты — пабочныя эфекты, якія не ўплываюць на стан, але патрабуюць ўзаемадзеяння з вонкавымі сістэмамі (напрыклад, навігацыя, паведамленні, запыты API).

Краткая гісторыя архітэктурных мадэляў

Архітэктурныя мадэлі UI развіваліся значна з часам:

MVC (Model View Controller)

Адзін з першых мадэляў, які падзяліў прыкладанне на тры кампаненты:

  • Мадэль — дадзеныя і бізнес-логіка
  • Вынікі пошуку - user interface
  • Загрузіць: User Input


Найлепшая траекторыя руху пры змене паласы - гэта вельмі плыўная, але не занадта расцягнутая па даўжыні крывая.

MVP (Model View Presentation) — прэзентацыя мадэлі

Узнагароджанне з MVC, дзе:

  • Мадэль — дадзеныя і бізнес-логіка
  • View — пасіўны інтэрфейс
  • Прэзентацыя — медыятар паміж мадэлью і поглядам


У нас функцыяну­юць таварыствы “Разумнікі і разумніцы”, “Даследчык”, а таксама адзіная ў Магілёўскай вобласці астранамічная пляцоўка.

MVVM (Model-View-ViewModel — мадэль паглядзець)

Наступны этап эвалюцыі:

  • Мадэль — дадзеныя і бізнес-логіка
  • Вынікі пошуку - user interface
  • ViewModel — пераўтварае дадзеныя з Model у формат, зручны для View


MVVM выкарыстоўвае канцэпцыю абвяшчэння дадзеных, якая зніжае колькасць кода, але можа выклікаць праблемы з даследаваннем патоку дадзеных.

MVI (Model-View-Intent) — мадэль наведвання

Сучасны падыход, які падкрэслівае:

  • Прагнозаванне — дэтэнтыстычны падыход да дзяржаўнага кіравання
  • Незмяненне — стан не змяняецца, але замяняецца
  • Unidirectional data flow — ясная і празрачная сцэна падзеяў


Афары́зм (па-грэцку: αφορισμός — выказваньне) — выслоўе, у якім у трапнай, ляканічнай і звычайна вобразнай форме выказаная арыгінальная думка.

Чаму SimpleMVI быў створаны і яго месца сярод іншых бібліятэк

SimpleMVI быў распрацаваны, каб прапанаваць распрацоўшчыкам просты, але магутны інструмент для рэалізацыі мадэлі MVI ў праектах Kotlin Multiplatform.

  1. Засяроджанымі на тых пытаннях, якія недастаткова асветлены і/або з'яўляюцца адпрэчанымі.
  2. Побач з млынам – хатка завозніка (так называлі тых, хто прывозіў малоць жыта).
  3. Оптымізавана для Kotlin Multiplatform, забяспечваючы сумяшчальнасць з розных платформамі
  4. Разумею, што фатаграфія сёння змянілася, але, на маю думку, мы ставімся да яе занадта легкадумна...
  5. Галівудская кінаактрыса Джэніфер Эністан заўсёды выглядае ўзрушаюча і эфектна, і гэта ў 45 гадоў.


Галоўныя перавагі SimpleMVI у параўнанні з альтэрнатывамі:

  • Медыцынскія фактары не мелі дачынення да таго, што цяпер адбываецца.
  • Нізкая прагаласаванне ўступлення для разумення і выкарыстання
  • Поўны котлінскі падыход з выкарыстаннем сучасных моўных канструкцый
  • Зручны DSL для апісання бізнес-логікі
  • Чыстае размяшчэнне адказнасці між членамі


SimpleMVI не марыць вырашаць усе праблемы архітэктуры прыкладанняў, але забяспечвае надзейную аснову для арганізацыі бізнес-логікі, якая можа быць інтэграваная з любымі рашэннямі для UI, навігацыі і іншых аспектаў прыкладання.

Ключавыя концепты і кампаненты 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()
}

Вялікі жизненны цикл

У гэтага паняцця ёсць і іншыя значэнні, гл. Зборная Чылі:

  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


Важна разумець, што:

  • Усе публічныя метады Магазіну павінны быць выкліканы толькі на асноўным трэдзе (пазначаны анотацыяй @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

Акцёр — Business Logic Implementation

Актыўны працэс работы

Афарызм (гр. aphorismos - выказванне) - выслоўе, у якім у трапнай, лаканічнай форме выказана значная і арыгінальная думка.

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 - для запуску асінхронных аперацый
  • Функцыя Current State Getter (getState)
  • Функцыя дзяржаўнай рэдукцыі (рэдукцыя)
  • Новая функцыя адпраўкі намераў (onNewIntent)
  • Функцыя адпраўкі бокавых эфектаў (postSideEffect)

Пытанні працэсу

ІonIntent(intent: Intent)Для будаўніцтва "калоніі ўжанднічай" у Лідзе быў куплены пляц на так званым "выгане", у той час за горадам.

  1. Вызначыўся з працай, які купіў.
  2. Выконвае неабходную бізнес-логіку
  3. Загрузіць State
  4. Адчынім дзьверы на хрантох.

DefaultActor і DslActor: розныя падыходы

SimpleMVI прапануе два розныя падыходы да рэалізацыі Actor:

Асноўны артыкул: Object Oriented Approach

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:

  • Знакаміты падыход
  • Зручна для складанай бізнес-логікі
  • Вельмі зручны для вялікіх праектаў

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:

  • Больш падрабязная заява
  • Меньшая колькасць кодаў
  • Лепшы для малых і сярэдніх праектаў
  • Вынікі пошуку - Safe Intention


Рымская імперыя распалася на 2 самастойныя часткі, кожная са сваім імператарам – Заходнюю і Усходнюю.

Middleware — пашырэнне функцыянальнасці

Задачы Middleware

Адкрыццё было спрэчным, і канчатковы доказ прыйшоў толькі ў 1999 г. пасля эксперыментаў KTeV ў Фермілабе і эксперыментаў NA48 ў ЦЕРН.

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

  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


Важна памятаць, што 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:

  • Андрэй
  • І
  • Макіяж
  • Сцяг Дж.

Платформа-спецыфічны механізм ізаляцыі кода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")
}

Асноўны артыкул: Counter

Дадатковая мадэль Define Data Model

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 (Android Example)

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 не з асноўнага дроту
  • StoreIsNotInitializedException — калі вы спрабуеце выкарыстоўваць Uninitialized Store
  • StoreIsAlreadyDestroyedException - пры справе выкарыстання ўжо знішчанага Магазіну

Выпрабаванне элементаў

Дзякуючы чыстым распаўсюджванню адказнасці, 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()
}

Высновы

Сярод версій гульняў онлайн call of duty можна знайсці мноства займальных і дасціпных сюжэтаў, а апошняй навінкай, выпушчанай у канцы восені гэтага года, стала гульня Call of Duty: Ghost.

Ключавыя перавагі SimpleMVI

Напрыклад, можна вызначыць наступныя сілы бібліятэкі:

Мінімалістычны і прагматычны погляд

Напэўна, мы не з’яўляемся нейкімі паддоследнымі суб’ектамі, на якіх Бог эксперыментуе, спасылаючы нейкія цяжкасці і выпрабаванні.

Поўная падтрымка мультыплатформы Kotlin

Будаваны на Kotlin з нуля, SimpleMVI апынуўся для развіцця на некалькіх платформах. Бібліятэка ізалявае платформы-спецыфічны код праз механізм чакаць / рэальны, забяспечваючы сумяшчальнасць з Android, iOS, macOS і wasm js.

3.Предсказальнае дзяржаўнае кіраванне

Варта адзначыць, што ў Call of Duty 4 місіі даюць вельмі шмат бонусаў і ачкоў развіцця, да таго ж, яны досыць цікавыя і незвычайныя - выконваць іх лёгка і нясумна.

Галоўная / Заявы і абмоўкі пра абмежаванне адказнасці

Варта адзначыць, што ў Call of Duty 4 місіі даюць вельмі шмат бонусаў і ачкоў развіцця, да таго ж, яны досыць цікавыя і незвычайныя - выконваць іх лёгка і нясумна.

Зручны DSL для дэкларацыйнага логічнага апісання

Афары́зм (па-грэцку: αφορισμός — выказваньне) — выслоўе, у якім у трапнай, ляканічнай і звычайна вобразнай форме выказаная арыгінальная думка.

Флексіфікацыя і экспансійнасць

Не палічыце сарказмам, але тое, што адбываецца цяпер, вельмі нагадвае прымітыўную спробу даказаць: у «рускім свеце» усё самае лепшае.

Класічныя выпадкі выкарыстання

SimpleMVI вельмі падыходзіць для наступных сцэнарый:

Колькасць мультыплатформных праектаў

Калі вы распрацоўваеце прыкладанне, якое трэба працаваць на некалькіх платформах (Android і iOS, вэб-прыкладанняў), SimpleMVI дазваляе выкарыстоўваць адзін архітэктурны падыход і сумесны бізнес-логічны код.

Заявы і абмоўкі пра абмежаванне адказнасці

Варта адзначыць, што ў Call of Duty 4 місіі даюць вельмі шмат бонусаў і ачкоў развіцця, да таго ж, яны досыць цікавыя і незвычайныя - выконваць іх лёгка і нясумна.

Праекты з акцэнтам на тэставальнасць

Варта адзначыць, што для ажыццяўлення работ па стварэнні Нацыянальнага інвентара нематэрыяльнай культурнай спадчыны наша краіна летась атрымала грант з адпаведнага фонду UNESCO.

Міграцыя існуючых праектаў у архітэктуру MVI

SimpleMVI можа быць уведзены паступова, пачынаючы з асобных модуляў або функцый, што робіць яго прыдатным для паступовай міграцыі існуючых праектаў у архітэктуру MVI.

5 Навуковыя праекты і пратэсты

Дзякуючы сваёй простыні і мінімалізму, SimpleMVI добра падыходзіць для выкладання МVI-прынцыпаў і для хуткага прататыпавання.

Задачы для далейшага навучання

Для тых, хто хоча падымаць свае веды аб SimpleMVI і MVI архітэктуры ў цэлым, я рэкамендую наступныя рэсурсы:

  • SimpleMVI GitHub repository — крыніцавы код бібліятэкі з прыкладамі выкарыстання
  • SimpleMVI Documentation — афіцыйная дакументацыя з падрабязным апісаннем API і рэкамендацыямі

Заключныя думкі

SimpleMVI з'яўляецца збалансаваным рашэннем для арганізацыі прыкладання бізнес-логікі з выкарыстаннем сучасных падыходаў да архітэктуры.


Але серыя онлайн цацак Assassins creed 3 і Assassins creed 4, а таксама астатнія часткі забавы таксама заслугоўваюць увагі і вывучэння.


Размова сапраўды вельмі важная — мы ж усе цудоўна разумеем, што любы, самы лепшы дэкрэт можна звесці на нішто практыкай прымянення.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks