Lately, after beginning to migrate to Jetpack Compose, we determined emigrate CLEAN + MVVM structure to CLEAN + MVI structure. For this objective, now we have provide you with a playground mission on Github to check it in motion. This text explains the method we took.
In GityMarket, the primary unique structure is sort of CLEAN. It has area, knowledge, and presentation layers. To implement the presentation layer, the MVVM structure proposed by Google was used. The Mannequin layer of the MVVM structure is mapped to the area + knowledge layer of the CLEAN structure and the View and ViewModel courses of the MVVM structure are mapped to the presentation layer of the structure CLEAN.
Throughout this time, as I used to be refactoring the variant sections of the codebase, I observed one thing. I discover some strategies in some ViewModels too intelligent and do a number of work. This problem violates sole duty of the SOLID precept and likewise makes testing troublesome!
The primary answer to the issue talked about is to interrupt all of the good capabilities, however I’m on the lookout for an answer to stop this downside from occurring once more.
After some time, in early 2022, the Builders structure information was up to date they usually launched Unidirectional Knowledge Stream (UDF) structure business. Now you may select any structure for the UI layer that fits your wants, equivalent to MVVM, MVI, and so forth.
The third factor I bumped into was migrating the UI layer from XML to Jetpack Compose. As I delved deeper, I used to be intrigued by the idea of Standing within the Host State. At the moment, throughout my analysis, I used to be confronted with MVI structure. For extra particulars about MVI, you may learn this paragraph by Rim Gazzah.
MVI gives a number of advantages that meet our necessities and provides us rather a lot without cost!
- Intent-based interplay between UI and ViewModel has extra restrictions to adjust to sole duty.
- All strategies of ViewModel turn out to be personal. Because of this Encapsulation (one of many ideas of OOP) has been noticed extra. Any longer, you’re much less involved with the precise implementation of the ViewModel; You simply want to fireside the intent from the UI (So stunning! 😍).
- The second good thing about Intent-based interplay is rising Abstraction (one other precept of OOP) within the communication between UI and ViewModel.
- In the long run, I discover MVI extra appropriate to make use of Jetpack Compose than MVVM. I do know that you may have a number of old-style state of MVVM’s ViewModel and go ViewModel’s methodology calls as lambda to Composable capabilities, however in the case of MVI, why pressure your self to make use of MVVM?! ?😁
MVI primarily impacts the UI layer, so on this part I solely deal with this layer. For everybody Display screen
Initially, we want a Contract
; A wrapper interface Standing, Occasion(Intent in MVI), And Operate(The particular intent that the ViewModel fires within the person interface, for instance displaying a Snackbar
the person interface has to deal with it).
interface NewsListContract :
UnidirectionalViewModelNewsListContract.Impact> {
knowledge class State(
val information: Record = listOf(),
val refreshing: Boolean = false,
val showFavoriteList: Boolean = false,
)
sealed class Occasion {
knowledge class OnFavoriteClick(val information: Information) : Occasion()
knowledge class OnGetNewsList(val showFavoriteList: Boolean) : Occasion()
knowledge class OnSetShowFavoriteList(val showFavoriteList: Boolean) : Occasion()
object OnRefresh: Occasion()
object OnBackPressed : Occasion()
knowledge class ShowToast(val message: String) : Occasion()
}
sealed class Impact {
object OnBackPressed : Impact()
knowledge class ShowToast(val message: String) : Impact()
}
}
Be aware: The UnidirectionalViewModel
is an Interface:
interface UnidirectionalViewModel {
val state: StateFlow
val impact: SharedFlow
enjoyable occasion(occasion: EVENT)
}
Within the ViewModel, we want three principal issues:
- A variable to maintain Standing
- A move for Operate
- And overwrite
occasion
perform
Now, let’s implement the ViewModel:
@HiltViewModel
class NewsListViewModel @Inject constructor(
personal val getNewsUseCase: GetNewsUseCase,
personal val getFavoriteNewsUseCase: GetFavoriteNewsUseCase,
personal val toggleFavoriteNewsUseCase: ToggleFavoriteNewsUseCase,
) : NewsListContract {
personal val mutableState = MutableStateFlow(NewsListContract.State())
override val state: StateFlow =
mutableState.asStateFlow()
personal val effectFlow = MutableSharedFlow()
override val impact: SharedFlow =
effectFlow.asSharedFlow()
override enjoyable occasion(occasion: NewsListContract.Occasion) = when (occasion) {
is NewsListContract.Occasion.OnSetShowFavoriteList ->
onSetShowFavoriteList(showFavoriteList = occasion.showFavoriteList)
is NewsListContract.Occasion.OnGetNewsList ->
getData(showFavoriteList = mutableState.worth.showFavoriteList)
is NewsListContract.Occasion.OnFavoriteClick ->
onFavoriteClick(information = occasion.information)
NewsListContract.Occasion.OnRefresh -> getData(isRefreshing = true)
NewsListContract.Occasion.OnBackPressed -> onBackPressed()
is NewsListContract.Occasion.ShowToast -> showToast(occasion.message)
}
personal enjoyable onSetShowFavoriteList(showFavoriteList: Boolean) {
mutableState.replace {
it.copy(showFavoriteList = showFavoriteList)
}
}
personal enjoyable getData(
isRefreshing: Boolean = false,
showFavoriteList: Boolean = false,
) {
if (isRefreshing)
mutableState.replace {
NewsListContract.State(
refreshing = true,
)
}
viewModelScope.launch {
if (showFavoriteList)
getFavoriteNews()
else
getNewsList()
}
}
personal droop enjoyable getNewsList() = getNewsUseCase()
.catch { exception ->
mutableBaseState.replace {
BaseContract.BaseState.OnError(
errorMessage = exception.localizedMessage
?: "An surprising error occurred."
)
}
}
.onEach { outcome ->
mutableState.replace {
NewsListContract.State(information = outcome)
}
}
.launchIn(viewModelScope)
personal enjoyable getFavoriteNews() = getFavoriteNewsUseCase()
.onEach { newList ->
mutableState.replace {
it.copy(information = newList)
}
}.launchIn(viewModelScope)
personal enjoyable onFavoriteClick(information: Information) {
viewModelScope.launch(Dispatchers.IO) {
toggleFavoriteNewsUseCase(information)
}
}
personal enjoyable onBackPressed() {
viewModelScope.launch {
effectFlow.emit(NewsListContract.Impact.OnBackPressed)
}
}
personal enjoyable showToast(message: String) {
viewModelScope.launch {
effectFlow.emit(
NewsListContract.Impact.ShowToast(message = message)
)
}
}
}
Lastly in Display screen
:
- Knowledge in Standing go to all needed Composable capabilities
- The Operate move begins to be collected
- And
occasion
the perform known as wherever wanted and through the corresponding occasion
let’s implement the UI:
@Composable
enjoyable NewsListRoute(
viewModel: NewsListViewModel = hiltViewModel(),
showFavoriteList: Boolean = false,
onNavigateToDetailScreen: (information: Information) -> Unit,
) {
// Implementation of `use` within the notice part
val (state, impact, occasion) = use(viewModel = viewModel)
val exercise = LocalContext.present as? Exercise
/*
Get preliminary knowledge
Do not use the init block in ViewModel as a lot as doable
This makes testing tougher!
*/
LaunchedEffect(key1 = Unit) {
occasion.invoke(
NewsListContract.Occasion.OnSetShowFavoriteList(
showFavoriteList = showFavoriteList,
)
)
occasion.invoke(
NewsListContract.Occasion.OnGetNewsList(
showFavoriteList = showFavoriteList,
)
)
}
// Implementation of `collectInLaunchedEffect` within the notice part
impact.collectInLaunchedEffect {
when (it) {
NewsListContract.Impact.OnBackPressed -> {
exercise?.onBackPressed()
}
is NewsListContract.Impact.ShowToast -> {
Toast.makeText(exercise, it.message, Toast.LENGTH_LONG).present()
}
}
}
NewsListScreen(
newsListState = state,
onNavigateToDetailScreen = onNavigateToDetailScreen,
onFavoriteClick = { information ->
occasion.invoke(NewsListContract.Occasion.OnFavoriteClick(information = information))
},
onRefresh = {
occasion.invoke(NewsListContract.Occasion.OnRefresh)
},
onBackPressed = {
occasion.invoke(NewsListContract.Occasion.OnBackPressed)
},
showToast = { message ->
occasion.invoke(NewsListContract.Occasion.ShowToast(message))
},
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
personal enjoyable NewsListScreen(
newsListState: NewsListContract.State,
onNavigateToDetailScreen: (information: Information) -> Unit,
onFavoriteClick: (information: Information) -> Unit,
onRefresh: () -> Unit,
onBackPressed: () -> Unit,
showToast: (message: String) -> Unit,
) {
val refreshState = rememberPullRefreshState(
refreshing = newsListState.refreshing,
onRefresh = onRefresh,
)
Field(
modifier = Modifier
.fillMaxWidth()
.pullRefresh(refreshState)
) {
AnimatedVisibility(
seen = !newsListState.refreshing,
enter = fadeIn(),
exit = fadeOut(),
) {
Row {
Button(onClick = {
onBackPressed()
}) {
Textual content(textual content = "onBackPressed")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = {
showToast(message = "Hiiiiiii!")
}) {
Textual content(textual content = "Present Toast")
}
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
gadgets(newsListState.information) { information ->
NewsListItem(
information = information,
onItemClick = {
onNavigateToDetailScreen(information)
},
onFavoriteClick = {
onFavoriteClick(information)
}
)
}
}
}
PullRefreshIndicator(
newsListState.refreshing,
refreshState,
Modifier.align(Alignment.TopCenter)
)
}
}
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@ThemePreviews
@Composable
personal enjoyable NewsListScreenPrev(
@PreviewParameter(NewsListStateProvider::class)
newsListState: NewsListContract.State
) {
ComposeNewsTheme {
Scaffold {
NewsListScreen(
newsListState = newsListState,
onNavigateToDetailScreen = {},
onFavoriteClick = {},
onRefresh = {},
onBackPressed = {},
showToast = {},
)
}
}
}
Be aware: The implementation use
:
@Composable
inline enjoyable use(
viewModel: UnidirectionalViewModel,
): StateDispatchEffect {
val state by viewModel.state.collectAsStateWithLifecycle()
val dispatch: (EVENT) -> Unit = { occasion ->
viewModel.occasion(occasion)
}
return StateDispatchEffect(
state = state,
effectFlow = viewModel.impact,
dispatch = dispatch,
)
}
knowledge class StateDispatchEffect(
val state: STATE,
val dispatch: (EVENT) -> Unit,
val effectFlow: SharedFlow,
)
And the implementation collectInLaunchedEffect
:
@Suppress("ComposableNaming")
@Composable
enjoyable SharedFlow.collectInLaunchedEffect(perform: droop (worth: T) -> Unit) {
val sharedFlow = this
LaunchedEffect(key1 = sharedFlow) {
sharedFlow.collectLatest(perform)
}
}
John Wick: Chapter 4 (FREE) FULLMOVIE The Super Mario Bros Movie avatar 2 Where To Watch Creed 3 Free At Home Knock at the Cabin (2023) FullMovie Where To Watch Ant-Man 3 and the Wasp: Quantumania Cocaine Bear 2023 (FullMovie) Scream 6 Full Movie
Supply hyperlink