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
useMVI
architecture
MVI
与 MVVM
It 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
Model
And:MVVM
In theModel
The difference is,MVI
theModel
Mainly refers toUI
State (State
). Page loading status, control location, etcUI
stateView
: with the otherMVX
In theView
Consistent, maybe oneActivity
Or anyUI
Carrying unit.MVI
In theView
By subscribing toModel
Changes to achieve interface refreshIntent
: thisIntent
notActivity
theIntent
Any operation of the user is wrapped asIntent
The coma toModel
Layer 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
- through
ViewState
Define all page states ViewEvent
Define one-time events such asToast
, page closing events, etc- through
ViewAction
Define all user actions
The main differences between the MVI architecture and the MVVM architecture are:
MVVM
There is no constraintView
Layer andViewModel
To be specificView
Layers can be called at willViewModel
, whileMVI
Under the architectureViewModel
The implementation of theView
Layer masking, can only be sentIntent
To drive events.MVVM
的ViewModle
More than one is defined separately inState
,MVI
useViewState
对State
Centralized management, only need to subscribe to oneViewState
To get all the state of the page, relativeMVVM
Much 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
singleActivity
architecture
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
- All of the
viewModel
All in oneActivity
theViewModelStoreOwner
So when a page is destroyed, the used pageviewModel
When should they be destroyed? - Sometimes a page needs to listen to its own page
onResume
.onPause
Life cycle, singleActivity
How 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
pageViewModel
When 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:
- Method 1 returns an and
ViewModelStoreOwner
(usuallyActivity
orFragment
) the bindingViewModel
If it does not exist, the value is created. If it does exist, the value is returned. It was obviously created this wayViewModel
The life cycle will be associated withActivity
Consistent, in singleActivity
The architecture will always exist and will not be released. - Mode 2 Through
Hilt
Implementation, can be inComposable
To deriveNavGraph Scope
orDestination Scope
的ViewModel
And automatically depend onHilt
Build.Destination Scope
的ViewModel
Will followBackStack
Eject automaticClear
, avoid leakage.
Overall, a hiltViewModel with Navigation is a better option
Compose
How 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:
OnActive (or onEnter)
: whenComposable
The first time you enter the component treeOnCommit (or onUpdate)
:UI
As therecomposition
When an update occursOnDispose (or onLeave)
: whenComposable
When 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
Compose
How to saveLazyColumn
A 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
LazyColumn
The main reason for this problem is the parameter it uses to record the scrolling positionLazyListState
I didn’t persist it when I go back toA
Page,LazyListState
The 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