Skip to content

Commit d86cc19

Browse files
authored
Update mvvm.md
LiveData заменена на StateFlow. Удалена информация об EventDispatcher, добавлена информация о Channel
1 parent 7387545 commit d86cc19

1 file changed

Lines changed: 56 additions & 98 deletions

File tree

university/4-icerock-basics/mvvm.md

Lines changed: 56 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -26,51 +26,51 @@ sidebar_position: 6
2626

2727
## moko-mvvm
2828

29-
Для использования MVVM мы реализовали библиотеку [moko-mvvm](https://github.com/icerockdev/moko-mvvm). Главное, что мы стремились достичь при ее реализации, это использование оригинальных классов JetPack `ViewModel` и `LiveData` со стороны Android, чтобы продолжить использовать существующие в Android интеграции с данными классами (включая логику хранения `ViewModel` в `ViewModelStore` чтобы переживать смену конфигурации). Для iOS стороны (и других платформ тоже) классы `ViewModel` и `LiveData` были реализованы нами, в более простом виде чем в Android (так как только в Android есть сложный жизненный цикл компонентов с пересозданием). По сути классы `ViewModel` и `LiveData` являются expect классами с разными actual реализациями на платформах.
29+
Для использования MVVM мы реализовали библиотеку [moko-mvvm](https://github.com/icerockdev/moko-mvvm). Главное, что мы стремились достичь при ее реализации, это использование оригинальных классов JetPack `ViewModel` и `StateFlow` со стороны Android, чтобы продолжить использовать существующие в Android интеграции с данными классами (включая логику хранения `ViewModel` в `ViewModelStore` чтобы переживать смену конфигурации). Для iOS стороны (и других платформ тоже) классы `ViewModel` и `StateFlow` были реализованы нами, в более простом виде чем в Android (так как только в Android есть сложный жизненный цикл компонентов с пересозданием). По сути классы `ViewModel` и `StateFlow` являются expect классами с разными actual реализациями на платформах.
3030

3131
Для знакомства с библиотекой посмотрите материалы на странице в базе знаний - [moko-mvvm](../../learning/libraries/moko/moko-mvvm).
3232

33-
### Привязка LiveData к UI
33+
### Привязка StateFlow к UI
3434

35-
В библиотеке также содержатся готовые методы для привязки `LiveData` к UI элементам, по аналогии с методами, которые были использованы нами в [статье про State](../../learning/state). Данные методы доступны и для Android и для iOS, а поэтому в большинстве случаев вам не потребуется писать вручную привязку каждого типа данных к каждому UI элементу.
35+
В библиотеке также содержатся готовые методы для привязки `StateFlow` к UI элементам, по аналогии с методами, которые были использованы нами в [статье про State](../../learning/state). Данные методы доступны и для Android и для iOS, а поэтому в большинстве случаев вам не потребуется писать вручную привязку каждого типа данных к каждому UI элементу.
3636

37-
Привязкой UI к `LiveData` называется binding, и основано на использовании метода `bind`:
38-
- [для Android](https://github.com/icerockdev/moko-mvvm/blob/master/mvvm-livedata/src/androidMain/kotlin/dev/icerock/moko/mvvm/utils/LiveDataExt.kt)
39-
- [для iOS](https://github.com/icerockdev/moko-mvvm/blob/master/mvvm-livedata/src/iosMain/kotlin/dev/icerock/moko/mvvm/utils/LiveDataExt.kt)
37+
Привязкой UI к `StateFlow` называется binding, и основано на использовании метода `bind`:
38+
- [для Android](https://github.com/icerockdev/moko-mvvm/blob/master/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt)
39+
- [для iOS](https://github.com/icerockdev/moko-mvvm/blob/master/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt)
4040

4141
Для Android нам доступны например:
4242
```kotlin
4343
fun EditText.bindTextTwoWay(
4444
lifecycleOwner: LifecycleOwner,
45-
liveData: MutableLiveData<String>
46-
): Closeable
45+
flow: MutableStateFlow<String>
46+
): DisposableHandle
4747

4848
fun TextView.bindText(
4949
lifecycleOwner: LifecycleOwner,
50-
liveData: LiveData<String>
51-
): Closeable
50+
flow: StateFlow<String>
51+
): DisposableHandle
5252

5353
fun View.bindVisibleOrGone(
5454
lifecycleOwner: LifecycleOwner,
55-
liveData: LiveData<Boolean>
56-
): Closeable
55+
flow: StateFlow<Boolean>
56+
): DisposableHandle
5757
```
5858

5959
И для iOS соответственно:
6060
```swift
6161
extension UITextField {
6262
@discardableResult
63-
func bindTextTwoWay(liveData: MutableLiveData<NSString>) -> Closeable
63+
func bindTextTwoWay(flow: CMutableStateFlow<String>) -> DisposableHandle
6464
}
6565

6666
extension UILabel {
6767
@discardableResult
68-
func bindText<T : NSString>(liveData: LiveData<T>) -> Closeable
68+
func bindText<T : String>(flow: CStateFlow<T>) -> DisposableHandle
6969
}
7070

7171
extension UIView {
7272
@discardableResult
73-
func bindHidden(liveData: LiveData<KotlinBoolean>) -> Closeable
73+
func bindHidden(flow: CStateFlow<Boolean>) -> DisposableHandle
7474
}
7575
```
7676

@@ -79,8 +79,8 @@ extension UIView {
7979
shared code:
8080
```kotlin
8181
class SimpleViewModel : ViewModel() {
82-
private val _counter: MutableLiveData<Int> = MutableLiveData(0)
83-
val counter: LiveData<String> = _counter.map { it.toString() }
82+
private val _counter: MutableStateFlow<Int> = MutableStateFlow(0)
83+
val counter: CStateFlow<String> = _counter.map { it.toString() }.cStateFlow()
8484

8585
fun onCounterButtonPressed() {
8686
_counter.value += 1
@@ -115,7 +115,7 @@ class SimpleViewController: UIViewController {
115115

116116
viewModel = SimpleViewModel()
117117

118-
counterLabel.bindText(liveData: viewModel.counter)
118+
counterLabel.bindText(flow: viewModel.counter)
119119
}
120120

121121
@IBAction func onCounterButtonPressed() {
@@ -126,33 +126,33 @@ class SimpleViewController: UIViewController {
126126

127127
#### Добавление своих расширений
128128

129-
Если в `moko-mvvm` не оказалось нужной вам функции биндинга для `iOS` или `Android`, вы можете добавить свой `extension` к `LiveData`.
130-
Например, добавим функцию `bindToMenuItemVisible` для связи `LiveData<Boolean>` и `MenuItem` на `Android`:
129+
Если в `moko-mvvm` не оказалось нужной вам функции биндинга для `iOS` или `Android`, вы можете добавить свой `extension` к `CStateFlow`.
130+
Например, добавим функцию `bindToMenuItemVisible` для связи `CStateFlow<Boolean>` и `MenuItem` на `Android`:
131131
```kotlin
132-
internal fun LiveData<Boolean>.bindToMenuItemVisible(
132+
internal fun CStateFlow<Boolean>.bindToMenuItemVisible(
133133
lifecycleOwner: LifecycleOwner,
134134
menuItem: MenuItem
135-
): Closeable {
136-
return bindNotNull(lifecycleOwner) { value ->
135+
): DisposableHandle {
136+
return bind(lifecycleOwner) { value ->
137137
menuItem.isVisible = value
138138
}
139139
}
140140
```
141141

142-
Для `iOS` добавим функцию `bindToUIToolbarVisible` для связи `UIToolbar` c `LiveData<KotlinBoolean>` (на `iOS` из общего кода вместо `Boolean` приходит `KotlinBoolean`) вот как это будет выглядеть:
142+
Для `iOS` добавим функцию `bindToUIToolbarVisible` для связи `UIToolbar` c `CStateFlow<KotlinBoolean>` (на `iOS` из общего кода вместо `Boolean` приходит `KotlinBoolean`) вот как это будет выглядеть:
143143
```swift
144144
extension UIToolbar {
145-
func bindToUIToolbarVisible(liveData: LiveData<KotlinBoolean>) -> Closeable {
146-
return liveData.addCloseableObserver { [weak self] value in
145+
func bindToUIToolbarVisible(flow: CStateFlow<KotlinBoolean>) -> DisposableHandle {
146+
return flow.subscribe { [weak self] value in
147147
let kotlinBool = value as! KotlinBoolean
148148
self?.isHidden = kotlinBool.boolValue
149149
}
150150
}
151151
}
152152
```
153153

154-
Важно, в методах биндинга должна быть только привязка `liveData` к объекту `UI`, никакой логики быть не должно!
155-
Вся логика должна быть во `ViewModel`, если нужно как-то преобразовать значение `liveData`, делайте это там.
154+
Важно, в методах биндинга должна быть только привязка `flow` к объекту `UI`, никакой логики быть не должно!
155+
Вся логика должна быть во `ViewModel`, если нужно как-то преобразовать значение `flow`, делайте это там.
156156

157157
### MvvmActivity и MvvmFragment
158158
В moko-mvvm реализованы абстрактные классы [MvvmFragment](https://github.com/icerockdev/moko-mvvm/blob/b4b2ed1a86451bd303aa0733ecd776be96c6f455/mvvm-viewbinding/src/main/kotlin/dev/icerock/moko/mvvm/viewbinding/MvvmEventsFragment.kt) и [MvvmActivity](https://github.com/icerockdev/moko-mvvm/blob/b6f2630df03bbd405e5659d85ea7df03f38e5dc7/mvvm-viewbinding/src/main/kotlin/dev/icerock/moko/mvvm/viewbinding/MvvmActivity.kt), наследуясь от которых вы:
@@ -235,7 +235,6 @@ class TestFragment : MvvmFragment<TestFragmentBinding, AuthViewModel>() {
235235

236236
Разберем несколько подходов для передачи событий от `ViewModel` на UI:
237237
- используя `Flow`
238-
- используя `EventsDispatcher` из [moko-mvvm](https://github.com/icerockdev/moko-mvvm)
239238
- используя `Flow` вместе с [moko-kswift](https://github.com/icerockdev/moko-kswift)
240239

241240
#### Flow
@@ -266,90 +265,49 @@ sealed interface Action {
266265
В Kotlin-мире мы получим ошибку при компиляции, надо будет добавить в `when` обработку еще одного объекта - нового, который только что добавили во `ViewModel`.
267266
А на iOS компилятор нам ничего не подскажет, потому что новый объект будет обрабатываться в ветке `else`. Из-за этого, логика перехода на iOS нарушится. Поиск ошибки может занять некоторое время, в зависимости от знаний разработчика.
268267

269-
Чтобы не сталкиваться с этим на практике мы долгое время использовали другой подход - с помощью `EventsDispatcher` из [moko-mvvm](https://github.com/icerockdev/moko-mvvm). Разберемся, как он работает.
268+
Чтобы не сталкиваться с этим на практике мы используем другой подход - с помощью `Channel`. Разберемся, как он работает
270269

271-
#### EventsDispatcher
270+
### Передача Action с помощью Channel
272271

273-
С этим подходом нужно разобраться, потому что на многих наших проектах сейчас используется именно он.
274-
`EventDispatcher` - это класс с одной единственной задачей - гарантировать доставку события и вызов его обработчика на UI, после сигнала от `ViewModel`.
275-
276-
Во `ViewModel` объявляется интерфейс с методами, реализация которых ей нужна на платформе, например, метод для перехода на какой-нибудь экран:
277-
278-
```kotlin
279-
interface EventsListener {
280-
fun routeToMainPage()
281-
}
282-
```
283-
284-
Далее, все что остается сделать, чтобы вызвать событие на `UI` - это получить во `ViewModel` объект `eventsDispatcher` и, когда пора переходить на главный экран, послать платформе это событие простым вызовом метода:
272+
Во view model добавляем канал и события для передачи:
285273
```kotlin
286-
class EventsViewModel(
287-
val eventsDispatcher: EventsDispatcher<EventsListener>
288-
) : ViewModel() {
289-
290-
fun onButtonPressed() {
291-
eventsDispatcher.dispatchEvent { routeToMainPage() }
292-
}
293-
294-
interface EventsListener {
295-
fun routeToMainPage()
296-
}
274+
private val _actions: Channel<Actions> = Channel()
275+
val actions: CFlow<Actions> = _actions.receiveAsFlow().cFlow()
276+
...
277+
sealed interface Actions {
278+
data class ShowMessage(val messageText: StringDesc) : Actions
279+
data object RouteToBack : Actions
297280
}
298281
```
299-
300-
На платформах `Fragment` и `UIViewController` реализуют этот интерфейс.
301-
Пример реализации на Android:
302-
282+
В Android подписываемся на события. В экране на Compose UI это выглядит так:
303283
```kotlin
304-
class EventsFragment: Fragment(R.layout.fragment_simple), EventsViewModel.EventsListener {
305-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
306-
super.onViewCreated(view, savedInstanceState)
307-
308-
val viewModel: EventsViewModel = getViewModel {
309-
EventsViewModel(eventsDispatcherOnMain())
284+
viewModel.actions.observeAsActions { action ->
285+
when (action) {
286+
is Actions.ShowMessage -> {
287+
...
310288
}
311289

312-
viewModel.eventsDispatcher.bind(lifecycleOwner = this, listener = this)
313-
}
314-
315-
override fun routeToMainPage() {
316-
TODO("some routing")
290+
Actions.RouteToBack -> {
291+
...
292+
}
317293
}
318294
}
319295
```
320-
321-
Пример на iOS:
322-
296+
В iOS подписка выглядит так:
323297
```swift
324-
class EventsViewController: UIViewController {
325-
private var viewModel: EventsViewModel!
326-
327-
override func viewDidLoad() {
328-
super.viewDidLoad()
329-
330-
viewModel = EventsViewModel(
331-
eventsDispatcher: EventsDispatcher(listener: self)
332-
)
333-
}
334-
}
335-
336-
extension EventsViewController: EventsViewModelEventsListener {
337-
func routeToMainPage() {
338-
fatalError("some routing")
298+
viewModel.actions.subscribe { [weak self] action in
299+
guard let self = self,
300+
let action = action else { return }
301+
let actionKs = SimpleViewModelActionKs(action)
302+
switch actionKs {
303+
case .showMessage(let data):
304+
...
305+
case .routeToBack:
306+
...
339307
}
340308
}
341309
```
342310

343-
За счет интерфейса обе платформы знают, какой набор действий должны поддерживать.
344-
Если во `ViewModel` нужно будет добавить еще одно событие, и мы забудем реализовать его на какой-нибудь из платформ, компилятор выделит, что отсутствует реализация метода интерфейса.
345-
346-
:::warning
347-
348-
В `dispatchEvent` нельзя передавать лямбду из общего кода, например, для установки действия по кнопке в [AlertDialog](https://developer.android.com/reference/android/app/AlertDialog). Нельзя этого делать потому, что на Android мы не сможем ее никуда сохранить, поэтому при пересоздании экрана она пропадет.
349-
Если вам нужно установить чему-либо на платформе действие - делайте соответствующий метод во `ViewModel`.
350-
351-
:::
352-
353311
#### Flow c moko-kswift
354312
Мы уже рассмотрели, с какими проблемами мы столкнулись бы, если бы использовали `Flow` в общем коде.
355313
Разберем теперь, как можно решить эти проблемы, начнем с отсутствия типов у `Flow` на iOS.
@@ -367,7 +325,7 @@ extension EventsViewController: EventsViewModelEventsListener {
367325

368326
## Удобное public api общего кода
369327

370-
Благодаря переносу всей логики приложения в общий код мы получаем более удобное и простое API библиотеки для интеграции на платформы. Мы знаем что есть, например, ряд `ViewModel`-ей, в которых есть `LiveData` на которые нужно подписаться и `EventsDispatcher` события от которого нужно обрабатывать. Все передаваемые на UI данные уже подготовлены к отображению и не требуют дополнительной обработки.
328+
Благодаря переносу всей логики приложения в общий код мы получаем более удобное и простое API библиотеки для интеграции на платформы. Мы знаем что есть, например, ряд `ViewModel`-ей, в которых есть `StateFlow` на которые нужно подписаться и события, которые нужно обрабатывать. Все передаваемые на UI данные уже подготовлены к отображению и не требуют дополнительной обработки.
371329

372330
Вот некоторый список преимуществ, которые мы получаем за счет использования `ViewModel`-ей в общем коде:
373331

0 commit comments

Comments
 (0)