Skip to content

Как правильно соединить Store с View

Nikita Zaltsman edited this page Sep 20, 2020 · 1 revision

Самая первая задача которую необходимо решить при использовании библиотеки это каким образом правильно соединить Store со View. Для это нужно:

  1. Создать BindingRulesFactory
  2. Унаследовать у Activity/Fragment интерфейс StoreView
  3. Вызывать метод 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)
        }
    }
}

Шаг 1. Создать BindingRulesFactory

Begin

Далее нужно создать 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 когда он больше не будет нужен.

Advance

Также можно соединять 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 и более.

Шаг 2. Унаследовать у Activity/Fragment интерфейс StoreView

Тут все достаточно тривиально, нужно просто переопределить интерфейс 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 и т.д.

Шаг 3. Вызывать метод bind у StoreViewBinding в методе onCreate Activity/Fragment

Последнее что нужно сделать это создать нашу фабрику 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()
    }
}