-
Notifications
You must be signed in to change notification settings - Fork 4
Как правильно соединить Store с View
Самая первая задача которую необходимо решить при использовании библиотеки это каким образом правильно соединить Store со View. Для это нужно:
- Создать BindingRulesFactory
- Унаследовать у Activity/Fragment интерфейс StoreView
- Вызывать метод bind у StoreViewBinding в методе onCreate Activity/Fragment
Для примера используем простой Store:
class CounterStore @Inject constructor() : ReducerStore<Action, State, Nothing>(
initialState = State(),
reducer = ReducerImpl()
) {
sealed class Action {
object Increment : Action()
object Decrement : Action()
}
data class State(
val count: Int = 0
)
class ReducerImpl : Reducer<State, Action> {
override fun reduce(state: State, effect: Action) = when (effect) {
is Action.Increment -> state.copy(count = state.count + 1)
is Action.Decrement -> state.copy(count = state.count - 1)
}
}
}
Далее нужно создать BindingRulesFactory:
class CounterBindingFactory @Inject constructor(
private val store: CounterStore
) : DelegateBindingRulesFactory<CounterFragment>() {
override val bindingRulesFactory: BindingRulesFactory<CounterFragment> = bindingRulesFactory { view ->
baseRule { store to view }
autoCancel { store } // важная строчка, чтобы Store сам отписался после утичтожения View
}
}
Что конкретно тут происходит. Мы прописываем при помощи DSL правила по которым у нас будет соединятся Store и View.
В данном примере мы соeдиняем Store со View при помощи метода baseRule, в который передаем пару Pair<Store, View>
.
Этот DSL типобезопасный, поэтому если где-то произошла ошибка в типах, компилятор заругается, что позволяет быстро находить ошибки.
Мы унаследываемся от DelegateBindingRulesFactory который обязывает нас переопределить одно поле bindingRulesFactory. Дженерик для DelegateBindingRulesFactory всегда используем тот Activity/Fragment который унаследован от StoreView, т.е в данном примере это CounterFragment.
Если забыть унаследовать у Activity/Fragment StoreView то компилятор будет ругаться в DSL
Далее важная строчка тут это autoCancel, куда мы передаем Store, она добавляет функциональность которая отпишет Store во время убийства View.
Если же нужно чтобы Store жил дольше view (хотя такой кейс сложно предствить) то можно убрать эту строчку, но тогда клиенту нужно будет самому вызвать метод cancel у Store когда он больше не будет нужен.
Также можно соединять Store со View по другому, как приведено ниже (тут используем уже другой Store для примера):
class SearchFactory @Inject constructor(
private val store: SearchStore,
private val analytic: Analytic
) : DelegateBindingRulesFactory<SearchFragment>() {
override val bindingRulesFactory: BindingRulesFactory<SearchFragment> = bindingRulesFactory { view ->
rule { store bindStateTo view }
rule { view bindActionTo analytic }
rule { view bindActionTo store }
rule { store bindEventTo view }
autoCancel { store } // magic is here )))
}
}
В данном примере мы явно направляем потоки сущностей (Action, State, Event) куда нужно, это может понадобится если допустим нужно перенаправлять Action не только в Store, но и в какую-нибудь аналитику.
Также тут нужно прописывать правила по которым мы направляем Event на View, подробнее об этом написано тут. Помимо этого DSL позволяет прописывать сложные правила как например соединения двух Store и более.
Тут все достаточно тривиально, нужно просто переопределить интерфейс StoreView, в дженерики корого передаем Action и State соответсвующего Store.
class CounterFragment : Fragment(R.layout.fragment_counter), StoreView<Action, State> {
protected val broadcastChannel = BroadcastChannel<Action>(Channel.BUFFERED)
@Inject
lateinit var factory: Provider<CounterBindingFactory>
private var _binding: FragmentCounterBinding? = null
private val binding get() = _binding!!
override val actionFlow: Flow<Action> = broadcastChannel.asFlow()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentCounterBinding.bind(view)
binding.increaseButton.setOnClickListener { postAction(Action.Increment) }
binding.decreaseButton.setOnClickListener { postAction(Action.Increment) }
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun accept(value: State) {
binding.counterTextView.text = getString(R.string.fragment_counter_amount_format, value.count.toString())
}
private fun postAction(action: Action) = broadcastChannel.offer(action)
companion object {
fun newInstance() = CounterFragment()
}
}
Best practive является создание своего BaseFragment с инкапсуляцией логики по работе с Flow. Библиотека не предоставляет своих Fragment/Activity/View для гибкости использования.
Базовый фрагмент может выглядеть следующим образом:
abstract class PublisherFragment<Action : Any, ViewState : Any> @JvmOverloads constructor(
@LayoutRes layoutRes: Int = 0
) : Fragment(layoutRes), StoreView<Action, ViewState> {
protected val broadcastChannel = BroadcastChannel<Action>(Channel.BUFFERED)
fun postAction(action: Action) = broadcastChannel.offer(action)
abstract fun onViewStateChanged(viewState: ViewState)
override val actionFlow: Flow<Action> = broadcastChannel.asFlow()
override fun accept(value: ViewState) {
onViewStateChanged(value)
}
}
Рекомендуется создавать такие для DialogFragment, BottomSheetDialogFragment и т.д.
Последнее что нужно сделать это создать нашу фабрику CounterBindingFactory при помощи DI контейнера или вручную, не имеет значение какой способ используется и передать CounterBindingFactory во фрагмент.
Важный момент если используется DI фреймворк вроде Dagger/Toothpeek/Koin то желательно инжектить в Activity/Fragment не экземпляр CounterBindingFactory, а его провайдер т.е Provider<CounterBindingFactory>
, как показано в примере с CounterFragment.
Это нужно потому как, библиотека сама решает когда нужно создать экземпляр CounterBindingFactory, и сама сохранит его м/у переворотами. А если инжектить просто как поле, то тогда будут создаваться лишние экземпляры Store, что крайне нежелательно.
Далее остается только вызывать метод bind в onCreate у метода StoreViewConnector как показано ниже.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StoreViewBinding.withRestore(factoryProvider = factory::get)
.bind(storeViewDelegate) // не забываем про этот метод, без него ничего не заработает
}
И весь фрагмент выглядит следующим образом:
class CounterFragment : Fragment(R.layout.fragment_counter), StoreView<Action, State> {
protected val broadcastChannel = BroadcastChannel<Action>(Channel.BUFFERED)
@Inject
lateinit var factory: Provider<CounterBindingFactory>
private var _binding: FragmentCounterBinding? = null
private val binding get() = _binding!!
override val actionFlow: Flow<Action> = broadcastChannel.asFlow()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentCounterBinding.bind(view)
binding.increaseButton.setOnClickListener { postAction(Action.Increment) }
binding.decreaseButton.setOnClickListener { postAction(Action.Increment) }
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun accept(value: State) {
binding.counterTextView.text = getString(R.string.fragment_counter_amount_format, value.count.toString())
}
private fun postAction(action: Action) = broadcastChannel.offer(action)
companion object {
fun newInstance() = CounterFragment()
}
}