Jetpack Compose has recently released its official version. I have been paying attention to this new UI framework since jetpack Compose came out, but I haven’t done a complete project based on it. I just want to understand the principle and features of this framework. Recently, I made a webrTC video calling project webrTC_Compose based on Jetpack Compose. During the development process of the project, I further deepened my understanding of Jetpack Compose, especially I thought about what kind of development architecture this new UI framework is suitable for.

Development Architecture selection

Although Jetpack Compose has been released in version 1.0.0, it may still be a long way from being used on a large scale, as a new framework will take time to complete the surrounding libraries and a large project to validate the choice of a suitable development architecture. Today I’m going to briefly talk about the combination of the current mainstream development architecture and Jetpack compose, as well as the advantages and disadvantages and problems that need to be solved.

The specific code mentioned in this article is in compose_Architecture, you can take it if you need it, and you can also help with the likes

The choice of the original Android development architecture

In the original View system of Android, the popular development architectures include MVC, MVP, MVVM, MVI, CLEAN, etc. Because Jetpack compose is a declarative UI framework, it is obviously not applicable to MVC MVP and so on that need to hold view reference. At the same time, since clean focuses on data and logic layering, MVVM and MVI can be used in the UI layer, so this article will not analyze. Therefore, we mainly analyze the combination of MVVM, MVI and Jetpack compose

MVVM

When it comes to MVVM development architecture, in fact, for the original Android View system MVVM is not a complete MVVM, because MVVM is originally designed for declarative UI, and the original Android View system is not a declarative UI, so there are always some nondescribable use. Jetpack Compose is a perfect solution to this problem. Because Jetpack compose and Jetpack ViewModel are perfectly compatible, there is not much difference between the implementation of MVVM in Jetpack compose and the original view system. The difference is that IT is more convenient to complete UI development in a declarative way.

Let’s look at a simple example where we implement a simple Add Count with MVVM

First of all, we should implement Content (Jetpack compose advocates one-way data flow, that is, the state is promoted to Screen. Content does not contain state, but only a simple UI interface, which is easy to test. For details, please refer to the official tutorial jetpack Compose state


@Composable
fun Content1(count: Int, click: () -> Unit, click1: () -> Unit) {
    Column(
        Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "The count is $count")
        Button(onClick = click) {
            Text(text = "goto screen2")
        }

        Button(onClick = click1) {
            Text(text = "add")
        }
    }
}
Copy the code

Then we analyze that this example only has a count state and an add operation, so this is the way to implement the viewModel, which is based on the jetpack viewModel and livedata components

class MvvmViewModel : ViewModel() {
    val countState = MutableLiveData(1)

    fun add(num: Int) {
        countState.postValue(countState.value as Int + num)
    }

    fun reduce(num: Int) {
        countState.postValue(countState.value as Int - num)
    }
}
Copy the code

Next we need to implement Screen, where Content and ViewModel are combined

@Composable
fun Screen1(
    navController: NavController
) {
    val viewModel: MvvmViewModel = viewModel()
    val count by viewModel.countState.observeAsState(0)

    Content1(count = count,
        { navController.navigate("screen2") }
    ) {
        viewModel.add(1)
    }
}

Copy the code

We can see that with the viewModel() method, we can easily create a viewModel in Jetpack compose and easily convert liveData into compose state. When the state changes, the interface will be reorganized and displayed automatically. It is much easier to use than the original Android View system.

MVI

MVI architecture most people may not be very familiar with, but it is not very difficult, it is MVVM two-way binding into one-way data flow, emphasizing one-way flow of data and data source uniqueness, state is immutable, view through state renders data, viewModel through action to change state

The Content code is the same as MVVM

The ViewModel code is as follows

class MVIViewModel : ViewModel() { val viewState = MutableLiveData(ViewState()) val userIntent = Channel<UiAction>(Channel.UNLIMITED) init { handleAction() } private fun add(num: Int) { viewState.value? .let { viewState.postValue(it.copy(count = it.count + 1)) } } private fun reduce(num: Int) { viewState.value? .let { viewState.postValue(it.copy(count = it.count - 1)) } } private fun handleAction() { viewModelScope.launch { userIntent.consumeAsFlow().collect { when (it) { is UiAction.AddAction -> add(it.num) is UiAction.ReduceAction -> reduce(it.num) } } } } data class ViewState(val count: Int = 1) sealed class UiAction { class AddAction(val num: Int) : UiAction() class ReduceAction(val num: Int) : UiAction() } }Copy the code

The Screen code looks like this

@Composable
fun Screen1(
    navController: NavController
) {
    val viewModel: MVIViewModel = viewModel(navController = navController)
    val viewState by viewModel.viewState.observeAsState(MVIViewModel.ViewState())
    val coroutine = rememberCoroutineScope()
    Content1(count = viewState.count,
        { navController.navigate("screen2") }
    ) {
        coroutine.launch {
            viewModel.userIntent.send(MVIViewModel.UiAction.AddAction(1))
        }
    }
}

Copy the code

We should be able to see the difference between MVVM and MVI by looking at the appeal code

Multiple page communication problems

For an application, it is usually not possible to have only one page, because MVVM and MVI’s viewModel is bound to the page, for multiple pages, to achieve cross-page communication may be more difficult, which is a big problem for MVVM and MVI. However, this problem is easy to solve. We can define a method to get the viewModel of other pages or the global viewModel. But how do we do that in compose, the first thing we need to know is how do we keep the ViewModel in compose

For example, compose and navigation are used for compose and navigation. For viewModel(), we analyze the source code and find that every time a new page is used, It’s going to create a new ViewModelStoreOwner, so if we don’t specify a ViewModelStoreOwner we’re not going to get the last page and the global ViewModel, Therefore, we can provide a method to create a viewModel. When creating a viewModel, we first obtain the existing viewModel in the routing stack and global. If we cannot obtain the viewModel, we can create a new one or throw an exception, so that we can obtain the viewModel in other pages. The code to implement the communication between pages is also very simple, as shown below

@Suppress("MissingJvmstatic") @Composable inline fun <reified VM : ViewModel> viewModelOfNav( navController: NavController, key: String? = null, factory: ViewModelProvider.Factory? = null ): VM { val javaClass = VM::class.java var viewModelStoreOwner: ViewModelStoreOwner? = null navController.backQueue.forEach { if (it.existViewModel(javaClass, key = key)) { viewModelStoreOwner = it return@forEach } } if (viewModelStoreOwner == null) { val context = LocalContext.current if (context is ViewModelStoreOwner && context.existViewModel(javaClass, key = key)) { viewModelStoreOwner = context } } return viewModel( javaClass, viewModelStoreOwner = viewModelStoreOwner ? : checkNotNull( LocalViewModelStoreOwner.current ) { "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" },  key = key, factory = factory ) } class NotExistException : Exception("not exist") class ExistFactory : ViewModelProvider.Factory { override fun <T : ViewModel? > create(modelClass: Class<T>): T { throw NotExistException() } } fun <VM : ViewModel> ViewModelStoreOwner.existViewModel( modelClass: Class<VM>, key: String? = null ): Boolean { var isExist = true val provider = ViewModelProvider(this, ExistFactory()) try { if (key ! = null) { provider.get(key, modelClass) } else { provider.get(modelClass) } } catch (e: NotExistException) { isExist = false } return isExist }Copy the code

In fact, we can see that our MVI architecture with multi-page communication is a little bit similar to the Bloc architecture of The Flutter. It is the same one-way data flow, but one is sink and one is stream, and here is action and state. Bloc can also achieve inter-page communication through Provideof, so after our analysis, many architectures are similar

conclusion

Today, we simply analyze the combination and adaptability of the original Android development architecture MVVM, MVI and Jetpack compose. We find that the two can be combined perfectly, even more convenient and efficient than the original Android View development. In fact, this is the benefit of declarative UI framework. I will continue to analyze the combination of other development architectures such as Redux and Bloc with Jetpack compose, as well as their implementation in Compose