It is said that this framework can solve Android MVI

Author: Yi Dong

preface

There is no perfect architecture, only the most appropriate architecture.

Android application architecture changes: MVC, MVP, MVVM, MVI.

There are numerous high-quality articles in the technology community on the concept, logic, implementation methods, advantages and disadvantages of these four architectures, which will not be repeated here.

Today, we will focus on how to quickly practice MVI (model view intent) architecture by using Mavericks, the Airbnb open source framework.

Mainly clarify the following issues:

  1. What is Mavericks?
  2. What is the core concept of Mavericks?
  3. How does Mavericks work?
  4. How effective is Mavericks practice?

Mavericks

Mavericks (formerly MvRx): Android on Autopilot

Mavericks is a powerful and easy to learn Android MVI framework open source by Aribnb. Mavericks builds up logic based on Android Jetpack and Kotlin Coroutines, and is unquestionable in terms of advanced technology and sustainability. As for the practicability of the framework, Mavericks, which has accepted the long-term test of large apps such as Airbnb and Tonal, will not disappoint developers.

Core concept

  • MavericksState: carries all data of the interface and is only responsible for carrying data.

  • Kotlin data class must be used

  • Immutable attributes must be used

  • Each attribute must have a default value

  • MavericksViewModel: update the interface State and expose individual states for local updates.

  • init { ... }

  • setState { copy(yourProp = newValue) }

  • withState()

  • Async < T > and execute(...) Processing asynchronous transactions

  • onEach() and onAsync() local updates

  • MavericksView: State driven and refreshed interface.

  • invalidate()

  • Get MavericksViewModel through agents such as activityViewModel(), fragmentViewModel(), parentFragmentViewModel(), existingViewModel(), and navGraphViewModel(navGraphId: Int)

A simple counting interface only needs the following lines of code, which is clear and concise.

/** State classes contain all of the data you need to render a screen. */
data class CounterState(val count: Int = 0) : MavericksState

/** ViewModels are where all of your business logic lives. It has a simple lifecycle and is easy to test. */
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
    fun incrementCount() = setState { copy(count = count + 1) }
}

/**
 * Fragments in Mavericks are simple and rarely do more than bind your state to views.
 * Mavericks works well with Fragments but you can use it with whatever view architecture you use.
 */
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {
    private val viewModel: CounterViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        counterText.setOnClickListener {
            viewModel.incrementCount()
        }
    }

    override fun invalidate() = withState(viewModel) { state ->
        counterText.text = "Count: ${state.count}"
    }
}

practice

Requirements: use WanAndroid API[1] to display the list of search hot words (support drop-down refresh)

Interface: https://www.wanandroid.com/hotkey/json

1. Dependence

dependencies {
  implementation 'com.airbnb.android:mavericks:2.5.1'
}

2. Initialization

Initialization is performed in the onCreate() function within the Application.

Mavericks.initialize(this)

3. MavericksState

Define the MainState and add two properties:

  • val hotKeys: List<HotKey> = emptyList()

    Search hot word data

  • val request: Async<Response<List<HotKey>>> = Uninitialized

    Network request status (loading, failed, successful, etc.)

data class MainState(
    val hotKeys: List<HotKey> = emptyList(),
    val request: Async<Response<List<HotKey>>> = Uninitialized
) : MavericksState

4. MavericksViewModel

Define MainViewModel, manage MainState, and realize the function of obtaining search hot words.

  • initState: default state. As mentioned earlier, each attribute of the MavericksState subclass needs a default value.
  • init {...}: initialization execution.
  • withState {}: get the current state at one time.
  • copy(): copy the object and adjust some attributes to update the status.
class MainViewModel(initState: MainState) : MavericksViewModel<MainState>(initState) {
    init {
        getHotKeys()
    }

    fun getHotKeys() = withState {
        if (it.request is Loading) return@withState
        suspend {
            Retrofitance.wanAndroidAPI.hotKey()
        }.execute(Dispatchers.IO, retainValue = MainState::request) { state ->
            copy(request = state, hotKeys = state()?.data ?: emptyList())
        }
    }
}

5. MavericksView

Create a mainframe and implement the MavericksView interface, which is used to display the search hot word list. Users can pull down and refresh the request for new data.

  • invalidate(): triggered automatically after status update.
  • withState(MavericksViewModel): obtain the MavericksState managed by MavericksViewModel at one time.
  • onAsync(): listens for asynchronous attribute changes.
  • onEach(): listen for common attribute changes.
class MainFragment : Fragment(R.layout.fragment_main), MavericksView {

    private val mainViewModel: MainViewModel by fragmentViewModel()
    private val binding: FragmentMainBinding by viewBinding()

    private val adapter by lazy {
        HotKeyAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mainViewModel.onAsync(MainState::request,
            deliveryMode = uniqueOnly(),
            onFail = {
                viewLifecycleOwner.lifecycleScope.launchWhenStarted {
                    Snackbar.make(
                        binding.root,
                        "HotKey request failed.",
                        Snackbar.LENGTH_INDEFINITE
                    )
                        .apply {
                            setAction("DISMISS") {
                                this.dismiss()
                            }
                            show()
                        }
                }
            },
            onSuccess = {
                viewLifecycleOwner.lifecycleScope.launchWhenStarted {
                    Snackbar.make(
                        binding.root,
                        "HotKey request successfully.",
                        Snackbar.LENGTH_INDEFINITE
                    ).apply {
                        setAction("DISMISS") {
                            this.dismiss()
                        }
                        show()
                    }
                }
            }
        )
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.list.adapter = adapter
        binding.list.addItemDecoration(
            DividerItemDecoration(
                context,
                DividerItemDecoration.VERTICAL
            )
        )

        binding.refresh.setOnRefreshListener {
            mainViewModel.getHotKeys()
        }
    }

    override fun invalidate() {
        withState(mainViewModel) {
            binding.refresh.isRefreshing = !it.request.complete
            adapter.submitList(if (Random.nextBoolean()) it.hotKeys.reversed() else it.hotKeys)
        }
    }
}

6. Effect

Source code

Talk is cheap, Show me the code.

https://github.com/onlyloveyd/AndroidSamples

Focus

1. Async

The asynchronous processing seal class has four subclasses: Uninitialized, Loading, Success and Fail, which represent the four states of asynchronous processing respectively.

sealed class Async<out T>(private val value: T?) {

    open operator fun invoke(): T? = value

    object Uninitialized : Async<Nothing>(value = null)

    data class Loading<out T>(private val value: T? = null) : Async<T>(value = value)

    data class Success<out T>(private val value: T) : Async<T>(value = value) {
        override operator fun invoke(): T = value
    }

    data class Fail<out T>(val error: Throwable, private val value: T? = null) : Async<T>(value = value)
}

2. onAsync

Asynchronous attribute state change monitoring

data class MyState(val name: Async<String>) : MavericksState
...
onAsync(MyState::name) { name ->
    // Called when name is Success and any time it changes.
}

// Or if you want to handle failures
onAsync(
    MyState::name,
    onFail = { e -> .... },
    onSuccess = { name -> ... }
)

3. retainValue

Data displayed during loading or after loading failure.

In the example, we remove the retainValue in the getHotKeys() function, and the interface will flash obviously when updating data.

fun getHotKeys() = withState {
    if (it.request is Loading) return@withState
    suspend {
        Retrofitance.wanAndroidAPI.hotKey()
    }.execute(Dispatchers.IO) { state ->
        copy(request = state, hotKeys = state()?.data ?: emptyList())
    }
}

4. Monitoring mode: DeliveryMode

  • RedeliverOnStart: as the name suggests
  • UniqueOnly: for example, the SnackBar only needs to be played once and should not be displayed again during page reconstruction. It is suitable to use the listening mode of UniqueOnly.

5. Status monitoring to prevent crash

In order to prevent the program from crashing due to the destruction of the interface during callback, the launchWhenStarted defense strategy is adopted.

mainViewModel.onAsync(MainState::request,
    deliveryMode = uniqueOnly(),
    onFail = {
        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            Snackbar.make(
                binding.root,
                "HotKey request failed.",
                Snackbar.LENGTH_INDEFINITE
            )
                .apply {
                    setAction("DISMISS") {
                        this.dismiss()
                    }
                    show()
                }
        }
    },
    onSuccess = {
        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            Snackbar.make(
                binding.root,
                "HotKey request successfully.",
                Snackbar.LENGTH_INDEFINITE
            ).apply {
                setAction("DISMISS") {
                    this.dismiss()
                }
                show()
            }
        }
    }
)

summary

Simple and easy to use, is the basic standard for selecting wheels.

After starting Mavericks, I feel that the code level is clear, the integration is convenient, simple and easy to use, and meets the standard of good wheels.

It's good news for me who don't love my tossing frame. The next step is to rewrite the WAN Android client written before with Mavericks.

Keywords: Java Android Design Pattern kotlin

Added by Chris.P on Fri, 07 Jan 2022 09:00:06 +0200