Code Monkey home page Code Monkey logo

wanandroid-compose's Introduction

前言

今年七月底,Google 正式发布了 Jetpack Compose1.0 稳定版本,这说明Google认为Compose已经可以用于生产环境了。相信Compose的广泛应用就在不远的将来,现在应该是学习Compose的一个比较好的时机
在了解了Compose的基本知识与原理之后,通过一个完整的项目继续学习Compose应该是一个比较好的方式。本文主要基于Compose,MVI架构,单Activity架构等,快速实现一个wanAndroid客户端,如果对您有所帮助可以点个Star: wanAndroid-compose

效果图

首先看下效果图

请添加图片描述 在这里插入图片描述
请添加图片描述 在这里插入图片描述
------------------------------------------------------------ ------------------------------------------------------------
请添加图片描述 请添加图片描述

主要实现介绍

各个页面的具体实现可以查看源码,这里主要介绍一些主要的实现与原理

使用MVI架构

MVIMVVM 很相似,其借鉴了前端框架的**,更加强调数据的单向流动和唯一数据源,架构图如下所示

其主要分为以下几部分

  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态
  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新
  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求

例如登录页面的ModelIntent定义如下

/**
* 页面所有状态
/
data class LoginViewState(
    val account: String = "",
    val password: String = "",
    val isLogged: Boolean = false
)

/**
 * 一次性事件
 */
sealed class LoginViewEvent {
    object PopBack : LoginViewEvent()
    data class ErrorMessage(val message: String) : LoginViewEvent()
}

/**
* 页面Intent,即用户的操作
/
sealed class LoginViewAction {
    object Login : LoginViewAction()
    object ClearAccount : LoginViewAction()
    object ClearPassword : LoginViewAction()
    data class UpdateAccount(val account: String) : LoginViewAction()
    data class UpdatePassword(val password: String) : LoginViewAction()
}

如上所示

  1. 通过ViewState定义页面所有状态
  2. ViewEvent定义一次性事件如Toast,页面关闭事件等
  3. 通过ViewAction定义所有用户操作

MVI架构与MVVM架构的主要区别在于:

  1. MVVM并没有约束View层与ViewModel的交互方式,具体来说就是View层可以随意调用ViewModel中的方法,而MVI架构下ViewModel的实现对View层屏蔽,只能通过发送Intent来驱动事件。
  2. MVVMViewModle 中分散定义了多个 StateMVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

Compose 的声明式UI**来自 React,理论上同样来自 Redux **的 MVI 应该是 Compose 的最佳伴侣
但是MVI也只是在MVVM的基础上做了一定的改良,MVVM 也可以很好地配合 Compose 使用,各位可根据自己的需要选择合适的架构

关于Compose的架构选择可参考:Jetpack Compose 架构如何选? MVP, MVVM, MVI

Activity架构

早在View时代,就有不少推荐单Activity+多Fragment架构的文章,Google也推出了Jetpack Navigation库来支持这种单Activity架构
对于Compose来说,因为ActivityCompose是通过AndroidComposeView来中转的,Activity越多,就需要创建出越多的AndroidComposeView,对性能有一定影响
而使用单Activity架构,所有变换页面跳转都在Compose内部完成,可能也是出于这个原因,目前Google的示例项目都是基于单Activity+Navigation+多Compose架构的

但是使用单Activity架构也需要解决一些问题

  1. 所有的viewModel都在一个ActivityViewModelStoreOwner中,那么当一个页面销毁了,此页面用过的viewModel应该什么时候销毁呢?
  2. 有时候页面需要监听自己这个页面的onResumeonPause等生命周期,单Activity架构下如何监听生命周期呢?

我们下面就一起来看下如何解决单Activity架构下的这两个问题

页面ViewModel何时销毁?

Compose中一般可以通过以下两种方式获取ViewModel

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

//方式2   
@Composable
fun LoginPage(
    loginViewModel: LoginViewModel = hiltViewModel()
) {
	//...
}

如上所示:

  1. 方式1将返回一个与ViewModelStoreOwner(一般是ActivityFragment)绑定的ViewModel,如果不存在则创建,已存在则直接返回。很明显通过这种方式创建的ViewModel的生命周期将与Activity一致,在单Activity架构中将一直存在,不会释放。
  2. 方式2通过Hilt实现,可以在Composable中获取NavGraph ScopeDestination ScopeViewModel,并自动依赖 Hilt 构建。Destination ScopeViewModel 会跟随 BackStack 的弹出自动 Clear ,避免泄露。

总得来说,通过hiltViewModelNavigation配合,是一个更好的选择

Compose如何获取生命周期?

为了在Compose中获取生命周期,我们需要先了解下副作用
用一句话概括副作用:一个函数的执行过程中,除了返回函数值之外,对调用方还会带来其他附加影响,例如修改全局变量或修改参数等。

副作用必须在合适的时机执行,我们首先需要明确一下Composable的生命周期:

  1. onActive(or onEnter):当Composable首次进入组件树时
  2. onCommit(or onUpdate)UI随着recomposition发生更新时
  3. onDispose(or onLeave):当Composable从组件树移除时

了解了Compose的生命周期后,我们可以发现,如果我们在onActive时监听Activity的生命周期,在onDispose时取消监听,不就可以实现在Compose中获取生命周期了吗?
DisposableEffect可以帮助我们实现这个需求,DisposableEffect在其监听的Key发生变化,或onDispose时会执行
我们还可以通过添加参数,让其仅在onActiveonDispose时执行:例如DisposableEffect(true)DisposableEffect(Unit)

通过以下方式,就可以实现在Compose中监听页面生命周期

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

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

    }
}

当然有时也不需要这么复杂,比如我们需要在进入或返回ProfilePage页面时刷新登录状态,并根据登录状态确认页面UI,就可以通过以下方式实现

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

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

如上所示,每当进入页面或返回该页面时,我们就可以刷新页面登录状态了

Compose如何保存LazyColumn列表状态

相信使用过LazyColumn的同学都碰到过下面的问题

使用Paging3加载分页数据,并显示到页面ALazyColumn上,向下滑动LazyColumn,然后navigation.navigate跳转到页面B,接着再navigatUp回到页面A,页面ALazyColumn又回到了列表顶部

但是我们可以看到,LazyListState其实是通过rememberLazyListState做了持久化保存的,如下图所示

既然做了持久化保存,那为什么返回时的位置还有问题呢?其实纯粹使用 Paging + LazyColumn,当页面切换时,会记录当前页面位置,但如果通过item加上HeaderFooter就不行了
这是因为rememberLazyListState会在列表中至少有一项时restore滚动位置,同时Paging是通过Flow获取数据的,当返回到页面重组时并不能马上获取到Paging数据,第一帧时PagingitemCount为0
但同时因为LazyColumn中已经有了一个Header,这时便会还原保存的位置,但因为这时Paging中的数据还为空,不能滚动到正确的位置,于是便又滚动到顶部了
而当LazyColumn中没有Header时,列表中至少有一项时便是Paging数据成功填充的时候,这个时候还原的位置就是对的,所以没有问题

既然原因在于LazyListState没有在正确的时机被还原,那我们将LazyListSate保存在ViewModel中,并且在Paging中有数据时再还原listState,如下所示:

@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()
    // 当`Paging`有数据时,返回`ViewModel`中的`listState`
    val listState = if (squareData.itemCount > 0) viewStates.listState else LazyListState()

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

总得来说,对于一般的页面,rememberLazyListState已经足够,但是对于有HeaderFooterPaging页面,需要一些特殊处理
关于LazyColumn滚动丢失的问题,更详细的讨论可参考:Scroll position of LazyColumn built with collectAsLazyPagingItems is lost when using Navigation

总结

项目地址

https://github.com/shenzhen2017/wanandroid-compose
开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~

参考资料

https://github.com/manqianzhuang/HamApp
https://github.com/linxiangcheer/PlayAndroid
从零到一写一个完整的 Compose 版本的天气

wanandroid-compose's People

Contributors

ricardojiang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

wanandroid-compose's Issues

关于将 state 全部封装到一个类中有一个疑问

大佬在文中提到

MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

并且示例代码也是写的:

/**
* 页面所有状态
**/
data class LoginViewState(
   val account: String = "",
   val password: String = "",
   val isLogged: Boolean = false
)

并且通过如下代码订阅了这个状态:

    var viewStates by mutableStateOf(LoginViewState())
        private set

我的疑问是,使用 mutableStateOf 订阅了整个 viewState 类,那么如果 viewState 中任意一个属性参数发生变化,都会导致使用到了这个 viewState 的 composable 发生重组?还是说,只会重组使用了改变了的属性的 composable ?

例如上述 viewState 我改变了 account 属性,它会重组所有用到 viewState 的 composable 还是只重组使用了 viewState.account 的 composable ?

如果是重组所有的 composable ,是否会影响到性能?以及这样的话在某些逻辑处理上会造成“死循环”。

能否将不同的状态分开成不同的 viewState ?但是这样的话就不符合您说的

MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

频繁点击底部导航出现崩溃

FATAL EXCEPTION: main
Process: com.zj.wanandroid, PID: 9135
java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back
at androidx.navigation.NavBackStackEntry.getViewModelStore(NavBackStackEntry.kt:180)
at androidx.lifecycle.ViewModelProvider.(ViewModelProvider.java:114)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:80)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72)
at com.zj.wanandroid.ui.page.main.category.stucture.StructurePageKt.StructurePage(StructurePage.kt:117)
at com.zj.wanandroid.ui.page.main.category.CategoryPageKt$CategoryPage$1$1$2.invoke(CategoryPage.kt:65)
at com.zj.wanandroid.ui.page.main.category.CategoryPageKt$CategoryPage$1$1$2.invoke(CategoryPage.kt:63)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:135)
at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$2.invoke(ComposableLambda.jvm.kt:141)
at androidx.compose.runtime.internal.ComposableLambdaImpl$invoke$2.invoke(ComposableLambda.jvm.kt:141)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2156)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2399)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2580)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:2566)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(SnapshotState.kt:540)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2566)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2542)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:614)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:764)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:103)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:447)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:416)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:34)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1140)
at android.view.Choreographer.doCallbacks(Choreographer.java:946)
at android.view.Choreographer.doFrame(Choreographer.java:870)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1127)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:210)
at android.os.Looper.loop(Looper.java:299)
at android.app.ActivityThread.main(ActivityThread.java:8105)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:556)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1045)

关于MVI UiState的疑问

按照mvi说法,所有的state都是集中的同一个data class中

我的疑问是既然状态不能集中,那么怎样实现状态集中而又不触发大范围的重组 ,vue和react是没有这种问题的 ,flutter有provider也可以局部更新 ,而compose难道只能铺平吗,希望大佬你有更好的方法启发一下

在传统的安卓开发中,可监测某个字段的变化局部更新。

但是在compose中由于重组是根据state的set来感知的,这样会导致state的data class任意一个字段变化都会导致所有用到state的地方重组,有性能浪费,并且界面会出现不必要的刷新,例如闪一下。

根据官方的在开发者大会上的说法:
1.状态需要铺平,不能集中到一个类里,这是一个不触发全部重组的前提。
2.状态的读取和使用和读取应该在同一个位置,即最小化返回。

1容易理解,2我用伪代码演示一下。

// 第一种 会触发全部重组
@Composable
fun Detail() {
    var count by remember { mutableStateOf(1) }

    Text("one")

    Two(count = count) //1.直接传入值,此地有读取动作

    Button(onClick = { count++ }) {
        Text("点击累加")
    }
    Text("three")
}

@Composable
fun Two(count: Int) {
    Text("$count") //2此处使用值
}
// 第二种 只在Two作用域内重组
@Composable
fun Detail() {
    var count by remember { mutableStateOf(1) }

    Text("one")

    Two { count } //1.传入一个lambda,用lazy形式获取值

    Button(onClick = { count++ }) {
        Text("点击累加")
    }
    Text("three")
}

@Composable
fun Two(getCount: () -> Int) {
    //这样读取和使用在同一个地方,而且是延迟读取,可以把重组范围缩小到Two中
    Text("${getCount()}") //2此处使用值
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.