preface

At the end of July, Google officially released the stable version 1.0 of Jetpack Compose, which shows that Google thinks Compose is ready for production use. I believe that the widespread application of Compose is in the near future. Now is a good time to learn about Compose. After understanding the basic knowledge and principles of Compose, it would be a good way to continue learning about Compose through a complete project. This article is mainly based on Compose, MVI architecture, single Activity architecture, etc., to quickly implement a wanAndroid client, if it is helpful to you, you can click the Star: WanAndro-compose

rendering

Take a look at the renderings first

———————————————————— ————————————————————

Main implementation introduction

The specific implementation of each page can view the source code, here mainly introduces some of the main implementation and principle

useMVIarchitecture

MVIMVVMIt is similar in that it borrows from the ideas of the front-end framework, with more emphasis on one-way flow of data and unique data sources, as shown in the architecture diagram below



It is mainly divided into the following parts

  1. ModelAnd:MVVMIn theModelThe difference is,MVItheModelMainly refers toUIState (State). Page loading status, control location, etcUIstate
  2. View: with the otherMVXIn theViewConsistent, maybe oneActivityOr anyUICarrying unit.MVIIn theViewBy subscribing toModelChanges to achieve interface refresh
  3. Intent: thisIntentnotActivitytheIntentAny operation of the user is wrapped asIntentThe coma toModelLayer to make data requests

For example, the Model and Intent definitions for the login page are as follows

/ data class LoginViewState(val Account: String = "", val password: String = "", val isLogged: Boolean = false) /** * one-time event */
sealed class LoginViewEvent {
    object PopBack : LoginViewEvent()
    data class ErrorMessage(val message: String) : LoginViewEvent()
}

/ sealed class LoginViewAction {object Login: LoginViewAction() object ClearAccount: sealed class LoginViewAction() LoginViewAction() object ClearPassword : LoginViewAction() data class UpdateAccount(val account: String) : LoginViewAction() data class UpdatePassword(val password: String) : LoginViewAction() }Copy the code

As shown above

  1. throughViewStateDefine all page states
  2. ViewEventDefine one-time events such asToast, page closing events, etc
  3. throughViewActionDefine all user actions

The main differences between the MVI architecture and the MVVM architecture are:

  1. MVVMThere is no constraintViewLayer andViewModelTo be specificViewLayers can be called at willViewModel, whileMVIUnder the architectureViewModelThe implementation of theViewLayer masking, can only be sentIntentTo drive events.
  2. MVVMViewModleMore than one is defined separately inStateMVIuseViewStateStateCentralized management, only need to subscribe to oneViewStateTo get all the state of the page, relativeMVVMMuch less template code

Compose’s declarative UI comes from React. In theory, MVI, which also comes from Redux, should be the perfect companion for Compose. However, MVI is an improvement on MVVM. Compose also works well with MVVM, so you can choose the appropriate architecture for Compose

For Compose architecture, see: Jetpack Compose architecture. MVP, MVVM, MVI

singleActivityarchitecture

In the View era, there are many articles that recommend a single Activity+ multiple fragments architecture. Google also launched the Jetpack Navigation library to support the single Activity architecture for Compose. Because the Activity and Compose are routed through AndroidComposeView, the more activities, the more AndroidComposeView you need to create, which will affect performance. All transitions are done inside Compose, probably for this reason, so far Google’s sample projects are based on a single Activity+Navigation+ multiple Compose architecture

But using a single Activity architecture also requires some work

  1. All of theviewModelAll in oneActivitytheViewModelStoreOwnerSo when a page is destroyed, the used pageviewModelWhen should they be destroyed?
  2. Sometimes a page needs to listen to its own pageonResume.onPauseLife cycle, singleActivityHow do you listen for the lifecycle in the architecture?

Let’s take a look at how to solve these two problems in a single Activity architecture

pageViewModelWhen will it be destroyed?

In Compose, you can generally get a ViewModel in one of two ways

1 / / way
@Composable
fun LoginPage(
    loginViewModel: LoginViewModel = viewModel()
) {
	/ /...
}

2 / / way
@Composable
fun LoginPage(
    loginViewModel: LoginViewModel = hiltViewModel()
) {
	/ /...
}
Copy the code

As shown above:

  1. Method 1 returns an andViewModelStoreOwner(usuallyActivityorFragment) the bindingViewModelIf it does not exist, the value is created. If it does exist, the value is returned. It was obviously created this wayViewModelThe life cycle will be associated withActivityConsistent, in singleActivityThe architecture will always exist and will not be released.
  2. Mode 2 ThroughHiltImplementation, can be inComposableTo deriveNavGraph ScopeorDestination ScopeViewModelAnd automatically depend onHiltBuild.Destination ScopeViewModelWill followBackStackEject automaticClear, avoid leakage.

Overall, a hiltViewModel with Navigation is a better option

ComposeHow do I capture the life cycle?

In order to capture the life cycle of Compose, we need to understand the side effects of Compose. The side effects can be summarized in one sentence: the execution of a function can have additional effects on the caller, such as modifying global variables or modifying parameters, in addition to returning the value of the function.

Side effects must be implemented at the right time, we first need to define the duration of the Composable:

  1. OnActive (or onEnter): whenComposableThe first time you enter the component tree
  2. OnCommit (or onUpdate):UIAs therecompositionWhen an update occurs
  3. OnDispose (or onLeave): whenComposableWhen removed from the component tree

Now that we know about Compose’s lifecycle, we can see that if we listen for the Activity’s lifecycle in onActive and cancel it in onDispose, we can get the lifecycle in Compose. DisposableEffect can help to meet this requirement, DisposableEffect can be executed when the Key it listens to changes, or when onDispose occurs. We can also add parameters to make it execute only when onActive and onDispose occur: For example DisposableEffect(true) or DisposableEffect(Unit)

You can listen for the page life cycle in Compose in the following way

@Composable
fun LoginPage(
    loginViewModel: LoginViewModel = hiltViewModel()
) {
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(key1 = Unit) {
        val observer = object : LifecycleObserver {
            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
            fun onResume(a) {
                viewModel.dispatch(Action.Resume)
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            fun onPause(a) {
                viewModel.dispatch(Action.Pause)
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }

    }
}
Copy the code

Of course, sometimes you don’t need to do this. For example, when you enter or return to the ProfilePage page, you need to refresh the login status and validate the page UI based on the login status

@Composable
fun ProfilePage(
    navCtrl: NavHostController,
    scaffoldState: ScaffoldState,
    viewModel: ProfileViewModel = hiltViewModel()
) {
    / /...

    DisposableEffect(Unit) {
        Log.i("debug"."onStart")
        viewModel.dispatch(ProfileViewAction.OnStart)
        onDispose {
        }
    }
}    
Copy the code

As shown above, we can refresh the page login status every time we enter or return to the page

ComposeHow to saveLazyColumnA list of state

I’m sure those of you who have used LazyColumn have encountered the following problem

Load the paging data using Paging3 and display it on page A’s LazyColumn, slide down the LazyColumn and navigate. navigate to page B, then navigatUp to page A where the LazyColumn is back at the top of the list

LazyColumnThe main reason for this problem is the parameter it uses to record the scrolling positionLazyListStateI didn’t persist it when I go back toAPage,LazyListStateThe data is set back to the default value of 0, and it goes back to the top, as shown below

Since the reason is that LazyListState is not saved, we can save LazyListSate in the ViewModel, as shown below

@HiltViewModel
class SquareViewModel @Inject constructor(
    private var service: HttpService,
) : ViewModel() {
    private val pager by lazy { simplePager { service.getSquareData(it) }.cachedIn(viewModelScope) }
    val listState: LazyListState = LazyListState()
}

@Composable
fun SquarePage(
    navCtrl: NavHostController,
    scaffoldState: ScaffoldState,
    viewModel: SquareViewModel = hiltViewModel()
) {
    val squareData = viewStates.pagingData.collectAsLazyPagingItems()
    // val listState = viewStates.listState
    // Special processing for Paging: viewstates.listState
    val listState = if (squareData.itemCount > 0) viewStates.listState else LazyListState()

    RefreshList(squareData, listState = listState) {
        itemsIndexed(squareData) { _, item ->
           / /...}}}Copy the code

It should be noted that for general pages, viewModel.listState can be used directly. However, WHEN I use Paing, I find that itemCount of Paging will temporarily change to 0 when returning to the page, resulting in listState also changing to 0. So some special treatment needs to be done for the LazyColumn scroll loss problem. For more detailed discussion, please refer to: Scroll position of LazyColumn built with collectAsLazyPagingItems is lost when using Navigation

conclusion

The project address

Github.com/shenzhen201… Open source is not easy, if the project helps you, welcome to like,Star, favorites ~

The resources

Github.com/manqianzhua… Github.com/linxiangche… Write a complete Compose version of the weather from zero to one