08. MVI va Redux — Unidirectional Data Flow
Arxitektura
iOS Arxitektura 8: MVI va Redux — Unidirectional Data Flow va State Management
Holat: Ushbu dars 2026-yil oktabr oyi holatiga ko'ra zamonaviy iOS (Swift 6, iOS 17+) va SwiftUI texnologiyalariga asoslangan.
Mundarija
- Unidirectional Data Flow (UDF) nima?
- MVI (Model-View-Intent) Arxitekturasi
- Redux: Single Source of Truth
- Amaliy loyiha: To-Do App (MVI Pattern)
- Middleware va Side Effects
- Time Travel Debugging
- ReSwift: iOS uchun Redux
- MVI vs MVVM vs TCA
- MVI/Redux qachon ishlatiladi?
- Xulosa
- Manbalar
1. Unidirectional Data Flow (UDF) nima? {#udf-nima}
Unidirectional Data Flow (UDF) — ma'lumotlar ilova bo'ylab faqat BIR YO'NALISHDA oqishini ta'minlaydigan dizayn patterni. Boshqa arxitekturalarda (masalan, MVVM yoki VIPER) ma'lumotlar ikki yo'nalishda (bidirectional) harakatlanishi mumkin, UDF'da esa harakat doim siklik bo'ladi.
MVI & Redux Unidirectional Data Flow Sikli
UDF ning afzalliklari:
- Predictability (Bashorat qilish qulayligi): Har bir State o'zgarishi aniq bir Action (Intent) orqali sodir bo'ladi.
- Debugging: State o'zgarishlarini kuzatish juda oson. Xato qachon va qaysi Action natijasida yuz berganini bilish mumkin.
- Testing: Reducer - bu sof funksiya (pure function). Uni test qilish uchun mock objectlar kerak emas.
2. MVI (Model-View-Intent) Arxitekturasi {#mvi-arxitekturasi}
MVI dastlab Android ekotizimida ommalashgan, lekin UDF prinsiplari iOS'da ham juda samarali.
- Model (State): UI qanday ko'rinishi kerakligini belgilaydigan ma'lumotlar strukturasi. Bu har doim immutable (o'zgarmas) bo'lishi kerak.
- View: State'ni qabul qilib, uni render qiluvchi (ekranga chiqaruvchi) qism.
- Intent (Action): Foydalanuvchining harakati (masalan: tugmani bosish, text kiritish) yoki tizim hodisasi (API javobi). Bu View'dan Store'ga boradigan xabar.
- Reducer: State va Intent'ni qabul qilib, Yangi State qaytaruvchi sof funksiya.
- Side Effect: API so'rovlari, ma'lumotlar bazasiga yozish kabi asinxron amallar. Ular Reducer'dan tashqarida bajarilishi kerak.
3. Redux: Single Source of Truth {#redux}
Redux asosan JavaScript/React dunyosidan kelgan bo'lib, butun ilova uchun yagona Store (single source of truth) ishlatishga asoslangan.
Redux asoslari:
- Store: Ilovaning barcha State'ini o'zida saqlaydi.
- Action: Nima sodir bo'lganini ifodalovchi oddiy obyekt (enum).
- Reducer: Sof funksiya bo'lib,
(CurrentState, Action) -> NewStatetarzida ishlaydi. - Middleware: Side effect'larni boshqarish uchun. Action Reducer'ga yetib borishidan oldin tutib olinib, asinxron ishlar qilinadi va yangi Action dispatch qilinadi.
Redux Store, Middleware va Single Source of Truth
4. Amaliy loyiha: To-Do App (MVI Pattern) {#amaliy-loyiha}
Quyida iOS 17+ (Swift 6 va @Observable makrosi) yordamida yaratilgan to'liq ishlaydigan To-Do ilovasi.
import SwiftUI
import Observation
// MARK: - Model (Domain)
struct Todo: Identifiable, Equatable {
let id: UUID
var title: String
var isCompleted: Bool
init(id: UUID = UUID(), title: String, isCompleted: Bool = false) {
self.id = id
self.title = title
self.isCompleted = isCompleted
}
}
enum FilterType: Equatable {
case all, active, completed
}
// MARK: - State (Model in MVI)
struct TodoState: Equatable {
var todos: [Todo] = []
var filter: FilterType = .all
var isLoading: Bool = false
// View uchun hisoblangan property'lar
var filteredTodos: [Todo] {
switch filter {
case .all: return todos
case .active: return todos.filter { !$0.isCompleted }
case .completed: return todos.filter { $0.isCompleted }
}
}
}
// MARK: - Intent (Action)
enum TodoIntent {
case addTodo(String)
case toggleTodo(UUID)
case deleteTodo(IndexSet)
case setFilter(FilterType)
case setLoading(Bool)
}
// MARK: - Reducer
struct TodoReducer {
// Pure function: State + Intent -> Yangi State
static func reduce(state: inout TodoState, intent: TodoIntent) {
switch intent {
case .addTodo(let title):
guard !title.isEmpty else { return }
let newTodo = Todo(title: title)
state.todos.append(newTodo)
case .toggleTodo(let id):
if let index = state.todos.firstIndex(where: { $0.id == id }) {
state.todos[index].isCompleted.toggle()
}
case .deleteTodo(let indexSet):
let idsToDelete = indexSet.map { state.filteredTodos[$0].id }
state.todos.removeAll(where: { idsToDelete.contains($0.id) })
case .setFilter(let filter):
state.filter = filter
case .setLoading(let isLoading):
state.isLoading = isLoading
}
}
}
// MARK: - Store
@Observable
@MainActor
final class TodoStore {
private(set) var state: TodoState
init(initialState: TodoState = TodoState()) {
self.state = initialState
}
// View'dan Intent qabul qilish
func dispatch(_ intent: TodoIntent) {
// Reducer'ga state'ni reference (inout) qilib beramiz
TodoReducer.reduce(state: &state, intent: intent)
}
}
// MARK: - View
struct TodoView: View {
@State private var store = TodoStore()
@State private var newTodoTitle = ""
var body: some View {
NavigationView {
VStack {
// Filter picker
Picker("Filter", selection: Binding(
get: { store.state.filter },
set: { store.dispatch(.setFilter($0)) }
)) {
Text("Barchasi").tag(FilterType.all)
Text("Faol").tag(FilterType.active)
Text("Tugallangan").tag(FilterType.completed)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
// Add Todo
HStack {
TextField("Yangi vazifa", text: $newTodoTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onSubmit {
store.dispatch(.addTodo(newTodoTitle))
newTodoTitle = ""
}
Button("Qo'shish") {
store.dispatch(.addTodo(newTodoTitle))
newTodoTitle = ""
}
.disabled(newTodoTitle.isEmpty)
}
.padding(.horizontal)
// Todo List
List {
ForEach(store.state.filteredTodos) { todo in
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundColor(todo.isCompleted ? .green : .gray)
.onTapGesture {
store.dispatch(.toggleTodo(todo.id))
}
Text(todo.title)
.strikethrough(todo.isCompleted)
}
}
.onDelete { indexSet in
store.dispatch(.deleteTodo(indexSet))
}
}
}
.navigationTitle("Vazifalar (MVI)")
}
}
}
#Preview {
TodoView()
}
Senior eslatmasi: E'tibor bering, View hech qachon
state.todos.append()qilib state'ni to'g'ridan to'g'ri o'zgartirmaydi. U doimdispatch(.addTodo(...))chaqiradi. Ma'lumot faqat bir yo'nalishda oqadi.
5. Middleware va Side Effects {#middleware-side-effects}
UDF arxitekturasining eng katta muammosi — API so'rovlari kabi Side Effect'larni qayerda qilish. Reducer'lar mutlaqo sof (pure) bo'lishi kerak. Bu yerda Middleware yordamga keladi.
Middleware Action (Intent) ni ushlab qoladi, asinxron operatsiyani bajaradi va natijani yangi Action sifatida Store'ga yuboradi.
// Middleware Type
typealias Middleware<State, Intent> = (State, Intent, @escaping (Intent) -> Void) -> Void
// Analytics Middleware misoli
func analyticsMiddleware() -> Middleware<TodoState, TodoIntent> {
return { state, intent, dispatch in
switch intent {
case .addTodo(let title):
print("Analytics: Vazifa qo'shildi - '\(title)'")
case .deleteTodo:
print("Analytics: Vazifa o'chirildi")
default:
break
}
}
}
Store'ni o'zgartirib, middleware ishlata oladigan qilishimiz mumkin:
@Observable
@MainActor
final class StoreWithMiddleware {
private(set) var state: TodoState
private let middlewares: [Middleware<TodoState, TodoIntent>]
init(initialState: TodoState = TodoState(), middlewares: [Middleware<TodoState, TodoIntent>] = []) {
self.state = initialState
self.middlewares = middlewares
}
func dispatch(_ intent: TodoIntent) {
// 1. Reducer orqali State ni yangilash
TodoReducer.reduce(state: &state, intent: intent)
// 2. Middleware'larni ishga tushirish (yangi dispatch function bilan)
for middleware in middlewares {
middleware(state, intent, { [weak self] newIntent in
// Asinxron kelgan yangi intentlarni yana dispatch qilish
Task { @MainActor in
self?.dispatch(newIntent)
}
})
}
}
}
6. Time Travel Debugging {#time-travel-debugging}
MVI va Redux'ning mo'jizalaridan biri — Time Travel Debugging. Har bir state o'zgarishi bitta Action bilan sodir bo'lgani va state immutable (valuelar to'plami) bo'lgani uchun, biz oldingi statelarni saqlab qolib, orqaga (undo) qaytishimiz mumkin.
@Observable
@MainActor
final class HistoryStore {
private(set) var state: TodoState
private var pastStates: [TodoState] = []
init(initialState: TodoState = TodoState()) {
self.state = initialState
}
func dispatch(_ intent: TodoIntent) {
// Joriy state'ni tarixga saqlash
pastStates.append(state)
// Yangilash
TodoReducer.reduce(state: &state, intent: intent)
}
func undo() {
guard let previousState = pastStates.popLast() else { return }
self.state = previousState
}
}
Muhim: Bu xususiyat chizmachilik, photo editor yoki kiritilgan ma'lumotlarni undo/redo qilish o'ta muhim bo'lgan kompleks ilovalarda bebaho hisoblanadi.
7. ReSwift: iOS uchun Redux {#reswift}
Agar siz loyihangizda to'liq Redux pattern'ini ishlatmoqchi bo'lsangiz, ReSwift mashhur kutubxona hisoblanadi (garchi SwiftUI+TCA hozir trendda bo'lsa ham, UIKit loyihalarida hali ham ko'p ishlatiladi).
ReSwift arxitekturasi:
import ReSwift
// 1. State
struct AppState {
var counter: Int = 0
}
// 2. Action
struct CounterActionIncrease: Action {}
struct CounterActionDecrease: Action {}
// 3. Reducer
func counterReducer(action: Action, state: AppState?) -> AppState {
var state = state ?? AppState()
switch action {
case _ as CounterActionIncrease:
state.counter += 1
case _ as CounterActionDecrease:
state.counter -= 1
default:
break
}
return state
}
// 4. Store yaratish
let mainStore = Store<AppState>(
reducer: counterReducer,
state: nil
)
// 5. View (UIKit misolida)
class CounterViewController: UIViewController, StoreSubscriber {
typealias StoreSubscriberStateType = AppState
let counterLabel = UILabel()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
mainStore.subscribe(self) // Obuna bo'lish
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
mainStore.unsubscribe(self)
}
func newState(state: AppState) {
counterLabel.text = "\(state.counter)"
}
@objc func increaseTapped() {
mainStore.dispatch(CounterActionIncrease())
}
}
8. MVI vs MVVM vs TCA {#taqqoslash}
| Xususiyat | MVVM | MVI / Redux | TCA (The Composable Architecture) |
|---|---|---|---|
| Data Flow | Bidirectional yoki UDF (ixtiyoriy) | Doim Unidirectional (UDF) | Doim Unidirectional (UDF) |
| State | Multiple sources (tarqoq bo'lishi mumkin) | Single Source of Truth (qatiy State struct) | Single Source of Truth (qatiy State struct) |
| Side Effects | ViewModel ichida (tartibsiz bo'lishi mumkin) | Middleware orqali | Effect orqali (juda tartibli) |
| Testing | Mock objectlar va asinxron testlar kerak | Oson (sof reducer funksiyalarni testlash) | Eng oson (Time-based scheduling o'rnatilgan) |
| Learning Curve | Oson | O'rta | Qiyin (katta boilerplate) |
| Boilerplate | Kam | O'rta | Ko'p |
9. MVI/Redux qachon ishlatiladi? {#qachon-ishlatiladi}
MVI va Redux (yoki umumiy holatda UDF) quyidagi vaziyatlarda eng to'g'ri tanlovdir:
- Complex State: Ekranda ko'plab qismlar bir-biriga bog'liq bo'lsa (masalan: Chat ilovalari, Real-time dashboard'lar).
- Undo/Redo talabi: Har bir foydalanuvchi qadami orqaga qaytarilishi kerak bo'lsa.
- Katta jamoalar: Predictable (bashorat qilinadigan) flow sababli qaysi kod qayerda ishlashini hamma aniq biladi.
- Qat'iy bug-tracking: State'larni log qilib borish orqali crash yuz berganda foydalanuvchi qanday qadamlar qilganini aniq ko'rish mumkin.
Xulosa {#xulosa}
Unidirectional Data Flow (MVI/Redux) ilovaning holatini (State) boshqarishni juda tartibli va ishonchli qiladi. Swift 6 va @Observable bilan bu arxitekturani qurish har qachongidan ham osonlashdi. Garchi oddiy ilovalar uchun MVVM yetarli bo'lsa-da, katta korporativ loyihalarda MVI yoki shunga asoslangan TCA kabi arxitekturalar o'zini to'liq oqlaydi.