Migrar de MVVM a MVI. Esta es la explicación de cómo nosotros… | de Kaaveh Mohamedi | marzo 2023

Habiendo comenzado recientemente a migrar a Jetpack Compose, decidimos migrar la arquitectura CLEAN+ MVVM a la arquitectura CLEAN+ MVI. Con ese fin, comenzamos un proyecto Playground en Github para probarlo en acción. Este artículo explica nuestro enfoque.Foto de Mostafa Meraji en UnsplashEn GityMarket, la arquitectura principal inicial estaba casi LIMPIA. Tenía capas de dominio, datos y presentación. Se utilizó la arquitectura MVVM recomendada por Google para implementar el nivel de presentación. La capa de modelo de la arquitectura MVVM se ha asignado a las capas de dominio y datos de la arquitectura CLEAN, y las capas de vista y modelo de vista de la arquitectura MVVM se han asignado a la capa de presentación de la arquitectura CLEAN.El diagrama de MVVM Durante este tiempo, mientras estaba en el proceso de refactorización de partes variantes del código base, algo me llamó la atención. He visto que algunos métodos en múltiples ViewModels son demasiado inteligentes y hacen más de una cosa. Este asunto viola la responsabilidad individual del principio SOLID y también hace que las pruebas sean tan difíciles. La primera solución al problema mencionado es desglosar todas las funciones inteligentes, pero estaba buscando una solución para evitar que este problema vuelva a ocurrir. Después de un tiempo, a principios de 2022, se actualizó e implementó la guía de arquitectura del desarrollador. Flujo de datos unidireccional (UDF) arquitectura. Ahora puede elegir cualquier arquitectura de capa de interfaz de usuario que se adapte a sus necesidades, p. B. MVVM, MVI, etc.Arquitectura de flujo de datos unidireccional recomendada por Google Tercero, necesitaba migrar la capa de IU desde XML Componer jet pack. A medida que profundizaba, me intrigó el concepto de Condición en el titular del estado. En este punto me enfrenté a la arquitectura MVI en mi investigación. Para obtener más detalles sobre MVI, consulte este artículo de Rim Gazzah. ¡El MVI brinda algunos beneficios que satisfacen nuestras necesidades y nos brindan cosas adicionales gratis!

  1. La interacción basada en intención entre UI y ViewModel tiene más restricciones para el cumplimiento Única responsabilidad.
  2. Todos los métodos de ViewModel se vuelven privados. Esto significa que The Encapsulation (uno de los principios de OOP) recibió más atención. A partir de ahora, se preocupará menos por la implementación real de ViewModel; Simplemente disparas la intención de la interfaz de usuario (¡Eso es tan bueno! 😍).
  3. El segundo beneficio de la interacción basada en la intención es la creciente abstracción (el otro principio de la programación orientada a objetos) de la comunicación entre la interfaz de usuario y ViewModel.
  4. Finalmente, descubrí que MVI se adapta mejor al uso de Jetpack Compose que MVVM. Sé que puede tener varios estados en el estilo antiguo de ViewModel de MVVM y pasar la llamada al método de ViewModel como una lambda a las funciones componibles, pero si existe MVI, ¿por qué obligarse a usar MVVM?!?😁

¡Yo después de familiarizarme con el MVI! 😍🥰El MVI afecta principalmente a la capa de la interfaz de usuario, por lo que en esta sección solo me centraré en esa capa. Para cada pantalla primero necesitamos un contrato; Una interfaz que se rompe Condición, caso(Intención en MVI) y Efecto(Intención especial que ViewModel desencadena en la interfaz de usuario, por ejemplo, una barra de refrigerios muestra que la interfaz de usuario debe manejarlo). {clase de datos Estado (val noticias: Lista = listOf(),val refresh: Boolean = false,val showFavoriteList: Boolean = false,)sealed class Event {data class OnFavoriteClick(val news: News) : Event()data class OnGetNewsList(val showFavoriteList: Boolean) : Event( ) clase de datos OnSetShowFavoriteList(val showFavoriteList: Boolean) : Event()object OnRefresh: Event()object OnBackPressed : Event()data class ShowToast(val message: String) : Event()}sealed class Effect {objeto OnBackPressed : Effect() Data clase ShowToast (mensaje de valor: Cadena): Efecto ()}} Nota: el modelo de vista unidireccional es una interfaz: interfaz Modelo de vista unidireccional {estado de valor: StateFlowefecto val: SharedFlowevento divertido (evento: EVENTO)} En ViewModel necesitamos tres cosas principales:

  1. Una variable para mantener el Condición
  2. Un arroyo para ellos Efecto
  3. Y anular la función de evento

Ahora implementemos ViewModel: @HiltViewModelClass NewsListViewModel @inject Constructor (private valneNewsusecase: GetNewSusecase, private Val getfavoritenewsusecase: GetfavoritenewsuSecase, NewsLaySLEWEWSECASEDSECASEDSECASEDSUSCASE: ToggleFavoritenEnewSewSeScase,). estado: StateFlow = mutableState.asStateFlow()private val effectFlow = MutableSharedFlow() anular el efecto de valor: SharedFlow = effectFlow.asSharedFlow()override fun event(evento: NewsListContract. Event) = cuando (evento) {ist NewsListContract.Event.OnSetShowFavoriteList -> onSetShowFavoriteList(showFavoriteList = event.showFavoriteList)ist NewsListContract.Event.OnGetNewsList -> getData(showFavoriteList = mutableState.value.showFavoriteList)ist NewsListContract.Event.OnFavoriteClick -> onFavoriteClick(news = event.news)NewsListContract.Event.OnRefresh -> getData(isRefreshing = true)NewsListContract.Event.OnBackPressed -> onBackPressed()ist NewsListContract.Event. ShowToast -> showToast(event.message)} diversión privada onSetShowFavoriteList( showFavoriteList: Boolean) {mutableState.update {it.copy(showFavoriteList = showFavoriteList)}} diversión privada getData(isRefreshing: Boolean = false, showFavoriteList: Boolean = false,) {if (isRefreshing)mutableState.update {NewsListContract.State(refreshing = true,)}viewModelScope.launch {if (showFavoriteList)getFavoriteNews()elsegetNewsList()}}private suspend fun getNewsList() = getNewsUseCase().catch { excepción – >mutableBaseState.update {BaseContract.BaseState.OnError(errorMessage = excepción .localizedMessage ?: «Ha ocurrido un error inesperado»)}}.onEach { resultado ->mutableState.update {NewsListContract.State(noticias = resultado)}} . launchIn(viewModelScope) diversión privada getFavoriteNews() = getFavoriteNewsUseCase().onEach { newList ->mutableState.update {it.copy(news = newList)}}.launchIn(viewModelScope)private fun onFavoriteClick(news: News) {viewModelScope. launch (Dispatchers.IO) {toggleFavoriteNewsUseCase(noticias)}}diversión privada onBackPressed () {viewModelScope.launch {effectFlow.emit(NewsListContract.Effect.OnBackPressed)}}private FunshowToast(mensaje: String) {viewModelScope.launch {effectFlow.emit ( NewsListContract.Effect.ShowToast(mensaje = mensaje))}}} Finalmente en la pantalla:

  1. Los datos en el Condición pasado a cualquier función componible requerida
  2. El Efecto el flujo comienza a ser recogido
  3. Y la función de evento se puede llamar en cualquier lugar que sea necesario y pasar el evento apropiado

Implementemos la interfaz de usuario: @Composablefun NewsListRoute(viewModel: NewsListViewModel = hiltViewModel(),showFavoriteList: Boolean = false,onNavigateToDetailScreen: (news: News) -> Unit,) { // Implementación de `use` en la sección de notas (state, efecto, evento) = use(viewModel = viewModel)val actividad = LocalContext.actual como? Actividad/*Obtener datos inicialesNo use el bloque init en ViewModel tan a menudo como sea posible¡Hace que las pruebas sean difíciles!*/LaunchedEffect(key1 = Unit) {event.invoke(NewsListContract.Event.OnSetShowFavoriteList(showFavoriteList = showFavoriteList,))event.invoke ( NewsListContract.Event.OnGetNewsList(showFavoriteList = showFavoriteList,))} // Implementación de `collectInLaunchedEffect` en la sección de notas effect.collectInLaunchedEffect { if (it) {NewsListContract.Effect.OnBackPressed -> {actividad?.onBackPressed()} NewsListContract es .Efecto. ShowToast -> {Toast.makeText(actividad, it.message, Toast.LENGTH_LONG).show()}}}NewsListScreen(newsListState = state,onNavigateToDetailScreen = onNavigateToDetailScreen,onFavoriteClick = { noticias ->event.invoke(NewsListContract. Event.OnFavoriteClick (noticias = noticias))},onRefresh = {event.invoke(NewsListContract.Event.OnRefresh)},onBackPressed = {event.invoke(NewsListContract.Event.OnBackPressed)},showToast = {mensaje ->evento.invoke (NewsListContract. Event.ShowToast(message))},)}@OptIn(ExperimentalMaterialApi::class)@Composableprivate fun NewsListScreen(newsListState: NewsListContract.State,onNavigateToDetailScreen: (noticias: Noticias) -> Unidad,onFavoriteClick: (noticias: Noticias ) -> Unidad,onRefresh: () -> Unidad,onBackPressed: () -> Unidad,showToast: (mensaje: String) -> Unidad,) {val refreshState = RememberPullRefreshState(refrescante = newsListState.refreshing,onRefresh = onRefresh,) Box(modifier = Modifier.fillMaxWidth().pullRefresh(refreshState)) {AnimatedVisibility(visible = !newsListState.refreshing,enter = fadeIn(),exit = fadeOut(),) {Row {Button(onClick = {onBackPressed()} ) {Text (texto = «onBackPressed»)}Spacer(modificador = Modifier.width(16.dp))Button(onClick = {showToast(mensaje = «¡Holaiiiiii!»)}) {Text(texto = «Mostrar Toast») }}LazyColumn (modifier = Modifier.fillMaxWidth()) {items(newsListState.news) {noticias ->NewsListItem(noticias = noticias,onItemClick = {onNavigateToDetailScreen(noticias)},onFavoriteClick = {onFavoriteClick(noticias)})}}} PullRefreshIndicator(newsListState .refreshing,refreshState,Modifier.align(Alignment.TopCenter))}}@SuppressLint(«UnusedMaterialScaffoldPaddingParameter»)@ThemePreviews@Composableprivate fun NewsListScreenPrev(@PreviewParameter(NewsListStateProvider::class)newsListState: NewsListContract.State) {ComposeNewsTheme Andamio {NewsListScreen (newsListState = newsListState,onNavigateToDetailScreen = {},onFavoriteClick = {},onRefresh = {},onBackPressed = {},showToast = {},)}}}Nota: La implementación de use:@Composableinline fun use (modelo de vista: modelo de vista unidireccional,): Efecto de despacho de estado {estado de val por viewModel.state.collectAsStateWithLifecycle()val dispatch: (EVENT) -> Unit = { event ->viewModel.event(event)}return StateDispatchEffect(state = state,effectFlow = viewModel.effect,dispatch = dispatch,) }clase de datos StateDispatchEffect(estado de val: ESTADO, despacho de val: (EVENTO) -> Unidad, flujo de efecto de val: SharedFlow,)Y la implementación de collectInLaunchedEffect :@Suppress(«ComposableNaming»)@Composablefun Flujo compartido.collectInLaunchedEffect(función: suspender (valor: T) -> Unidad) {val sharedFlow = thisLaunchedEffect(key1 = sharedFlow) {sharedFlow.collectLatest(función)}}

Deja una respuesta

Tu dirección de correo electrónico no será publicada.