前语
上一篇文章咱们说了食选这个程序的开发原因和架构设计。咱们简单的讲了下依赖注入是怎样搭配到Room和数据源的。这节咱们就来讲一下怎样把各个模块组合起来,先构成一个携带导航的APP主页,当有了主页后咱们再试着从它的基础上进行开发。
事务需求
- 界面办理处理
- MVI完结
- 界面设计
事务完结
下面内容主张参考源代码阅览,张贴的代码不是许多
1250422131/FoodChoice: 食选,解决日子中每天吃饭,吃什么,做什么,怎样做的问题,此项目也是我对JetpackCompose的MVI架构学习的一次实践。 (github.com)
界面办理
还记得吗?咱们想试着用累个activity来展现根本一切的compose界面,这就意味着咱们需求自己去办理compose的收支栈,因为activity只需一个,咱们不能靠安卓自己来做收支栈了。
当然这儿我不太确认这样做是否合理,可是咱们采用了模块化,运用activity来对应各个界面就不太方便了。咱们在上一篇现已提及了这个问题,便是下面这个库,这是谷歌供给用来办理compose界面的一个库,用起来也比较好使。
运用 Compose 进行导航 | Jetpack Compose | Android Developers (google.cn)
让咱们看看它是简单运用,咱们在app模块的navigation下建立了FCNavHost.kt
,这儿便是路由导航办理,相似vue
里的路由办理。
@Composable
fun FCNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
startDestination: String = "app_home",
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
composable(homeRoute) {
HomeRoute(modifier = modifier, navController = navController)
}
composable(cookRoute) {
CookRoute(modifier = modifier, navController = navController)
}
composable(settingRoute) {
SettingRoute(modifier = modifier, navController = navController)
}
}
}
咱们现在能够看到,这个办法对NavHost
进行了封装,这儿面现已有3个界面了,里面的一个composable
就相当于一个界面,咱们能够看到都调用了一些顶层办法比方 HomeRoute,那便是主页的。
可是咱们发现composable
有一个参数,那便是这个界面的路由地址,同理NavHost
的startDestination,便是初始路由。
解说完后,假如从0开端,那么咱们得到的代码应该是这样的。
@Composable
fun FCNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
startDestination: String = "",
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
}
}
OK,咱们先把他放在一旁,因为待会才要运用它。
为MVI供给Base类
咱们前面的文章提到了一些MVI的概念,但仅此而已,下面咱们需求为每个界面都完结这样的才干,因而咱们就要做一个基类,让其他compose的ViewModel
都承继它。
让咱们回忆一下,UI
,绑定ViewModel
,而且利用其间的state
目标来驱动界面展现数据,当用户作出交互时UI
向ViewModel
发送一个目的,而ViewModel
收到后修改了其间的State
目标,造成UI的改写,又因为State
内的值产生改动,所以才引起了UI
的改变。
graph TD
User-->|操作界面| UI
UI -->|Intent| ViewModel
ViewModel-->|State| UI
怎样样?这样的设计就让咱们只关心数据和UI的改变了,其他的逻辑都是服务于它们。
将行为笼统到类
从上面咱们就能够知道,首要笼统下面的内容,另外要说的是,咱们模块化便是要细化一些逻辑,进行解耦,现在咱们将Base类应该放在哪个模块?
没错,便是common
模块,在这个模块里咱们需求放一些公共的功用,显然BaseViewModel
就符合这个要求。
目标
- ViewModel
- Intent
- State
行为
- 发送目的
- 处理目的
现在看看,让咱们先界说下Intent和State
interface UiState
interface UiIntent
是的你没有看错,它们是两个未完结的接口,因为咱们的ViewModel需求处理目的和状况,因而,咱们需求有一些东西来约束Intent和State,因而咱们在这儿就界说两个接口,虽然现在接口什么也没有做,或许后面你会进行扩展。
这儿则是咱们真实需求的ViewModel,还记得吗?咱们需求笼统出ViewModel的行为,也便是承受和处理目的。
interface IViewModelHandle<S : UiState, I : UiIntent> {
fun handleEvent(event: I, state: S)
}
想象一下,咱们处理这个目的应该在每个界面的ViewModel处理,因而,咱们界说为了接口,可是让ViewModel来完结这个接口,需求传入当前的目的和状况。
下面咱们就来看看最近中心的ViewModel,它就完结了IViewModelHandle,还承继了ViewModel,这便是咱们需求的。
abstract class ComposeBaseViewModel<S : UiState, I : UiIntent>(viewState: S) :
IViewModelHandle<S, I>,
ViewModel() {
private val intentChannel = Channel<I>(Channel.UNLIMITED)
var viewStates by mutableStateOf(viewState)
protected set
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch(Dispatchers.IO) {
intentChannel.consumeAsFlow().collect {
handleEvent(it, viewStates)
}
}
}
fun sendIntent(viewIntent: I) {
viewModelScope.launch(Dispatchers.IO) {
intentChannel.send(viewIntent)
}
}
}
不过仔细看,它是一个笼统类,因而在这儿咱们能够不完结刚刚接口的办法,而是直接调用,至于完结,当然是留给承继它的ViewModel去完结了。
咱们在这个类里界说了一个Channel
,而且不限制大小,这个Channel
里便是存放咱们的目的数据的,假设有个目的就会进入这个行列里等候处理。
这儿我再提一下Channel
,不知道咱们有没有用Flow
,咱们先从Flow
讲起,Flow
就如他的名字相同,像是水流相同。
咱们能够在Flow当中去添加一些东西,就像是这样,咱们在一个容器中放了许多的东西,当然这个容器能放多少东西是不一定的。 现在看,它们是闭塞在一个容器挡住的,放进去的东西出不来,因为出口被咱们封闭了。
现在咱们想要拿出其间的东西,就需求翻开出口,就像是这样,翻开后咱们就能够拿到里面一切的东西了,一个一个的从咱们眼前曩昔。
这便是Flow
flow需求在协程作用域里履行,emit是向流里添加一些数据,像上面,咱们添加了3个数据。
但现在数据并不会活动和履行,因为咱们还关着盖子,而collect是一种结尾操作符,相当于翻开盖子,里面的数据就会活动出来了,当然假如上面的流速快到下面的还没处理完,那么flow就会挂起一会,等下面处理。
当然Flow
还有许多许多的东西和操作符,以及背压问题等,需求咱们自己去看看。
说完Flow
咱们再说Channel
,它是一种出产者和顾客的办法,首要用于协程间通讯,其上游能够有多个出产者来出产数据,而下贱也能够有多个顾客来消费数据,相当于能够扇入和扇出。
可是呢Channel
是需求顾客主动去获取的管道里面的东西的,就像下面,咱们需求调用receive
办法才干拿到其间的一条数据,当然Channel
是相当聪明的,假设调用receive
时没有数据就会挂起,等又有数据send进来后就再次放行,此次类推,同理
val channel = Channel<Int>(Channel.UNLIMITED).apply {
send(1)
send(2)
send(3)
}
val mInt = channel.receive()
怎样样?Channel
看起来更适合咱们,因为Flow
有必要要在flow域里去添加数据,可是咱们发送目的在UI里,处理在ViewModel里,这就意味着采用Flow
会很麻烦。
即使如此仍然有问题,比方Channel
需求调用,ViewModel履行send,对管道内发送目的,那viewmode就需求履行receive(),但履行receive() 一次只能拿一个目的。
假如需求一向监听,那么就需求写为这样:
viewModelScope.launch(Dispatchers.IO) {
while (true){
val intent = intentChannel.receive()
}
}
饿汉式获取,对吧?
这样仍然不好,咱们想个办法结合Flow
与Channel
,flow是只需上游有东西,且翻开了收集就一向会向下贱,而Channel
能够在外部send
数据进去。果然,kotlin早就想到了这一点,能够将Channel
转换为Flow
。
private fun handleIntent() {
viewModelScope.launch(Dispatchers.IO) {
intentChannel.consumeAsFlow().collect {
handleEvent(it, viewStates)
}
}
}
现在consumeAsFlow()
就能够将管道转换为流了,再利用collect,当管道存在内容后,就会运送下来又collect接纳,其他时间挂起,这样的写法要更好。
至此,咱们现已完结了MVI中的中心功用,目的传递。让咱们回到ComposeBaseViewModel
,咱们刚刚说的便是其间的handleIntent
办法,它用来监听是否有目的传递,有的话就调用接口办法handleEvent
让ViewModel去处理。而其间的sendIntent
便是暴露给UI用的,UI经过调用这个办法来发送目的。
graph TD
User-->|操作界面| UI
UI -->|Intent| ViewModel
相当于这一部分,当然你也发现了,咱们或许不需求返回state,因为viewmodel一直持有state的目标,这个或许我后面会调整。
ViewModelBase类的运用
前面咱们现已写好了ViewModel,可是目的分发下去了,handleEvent
还没有人处理呢,抓住时机,这儿我以主页的ViewModel为比如,看看处理。
open class MainActivityIntent @Inject constructor() : UiIntent {
data class SelectNavItem(var index: Int) : MainActivityIntent()
data class SetShowBottomBar(val state: Boolean) : MainActivityIntent()
}
这个是MainActivity的目的,能够看到有两个内部类,都承继MainActivityIntent,但他们终究父类都是UiIntent。
class MainActivityViewModel : ComposeBaseViewModel<MainActivityState, MainActivityIntent>(
MainActivityState(),
) {
override fun handleEvent(event: MainActivityIntent, state: MainActivityState) {
when (event) {
is MainActivityIntent.SelectNavItem -> selectNavItem(event.index)
is MainActivityIntent.SetShowBottomBar -> {
viewStates = viewStates.copy(isShowBottomBar = event.state)
}
}
}
private fun selectNavItem(index: Int) {
viewStates = viewStates.copy(titleState = false)
viewModelScope.launch {
delay(250L)
viewStates = viewStates.copy(titleState = true)
}
viewStates = viewStates.copy(navItemIndex = index)
}
}
咱们承继了ComposeBaseViewModel
,因为ComposeBaseViewModel
是个笼统类而且它没有完结上级接口的handleEvent
办法,因而在这儿咱们需求覆写handleEvent
。
留意这儿的when,经过is来判别是哪个目的,不同的目的就做不同的事情。
比方viewStates = viewStates.copy(isShowBottomBar = event.state)
,它便是改动了State还记得吗?前面咱们说,UI会因为State的改动而更新,便是这个意思,留意event.state,因为用的is,when现已知道你用了哪个类了,因而能够直接拿到这个类里的特点,就像event.state
。
主页UI
主页实际上是比较简单的,咱们看看,便是需求顶部导航和底部导航,剩余的便是页面内容。
让咱们看看主页的代码,事实上这儿还不是主页的真实UI,咱们首先利用依赖注入,让MainActivity承载的内容能够进行注入,接下来咱们把ViewModel绑定,这样根本上就完结了。
可是留意这儿咱们调用了rememberNavController()
,事实上这个便是之前咱们运用的导航库,现在咱们需求初始化,让全局一致运用这一个导航办理。现在咱们把他们传递给了FoodApp
。
脚手架
这部分代码比较长,可能有一些我就不粘了,咱们翻开项目看
现在咱们来看看FoodApp
,事实上前面的界面其实很中规中矩,咱们能够用compose的脚手架就来完结,现在咱们看看FoodApp
中有个FullScreenScaffold
,事实上这是封装了谷歌的脚手架的,我在这儿加了一个沉浸式的代码,这个后面再说。
顶部导航
咱们首先看看顶部导航,事实上它便是个AppBar对吧?可是这儿我用了CenterAlignedTopAppBar
,意味着中心的标题是会居中的哦。
而这儿咱们还用了两个AnimatedVisibility
,它是Compose的一种动画组件,能够操控出现和消失的办法和作用。
想象一下,进入子页面后,导航栏或许底部导航栏就需求躲藏对不对,咱们为了让它滑润一些就加个动画来躲藏这个过,而第二个AnimatedVisibility
则是让导航界面切换时标题跳动一会儿。
而我,咱们发现这个标题用的便是State的值,比方viewStates.titleState
,这样子MVI就完结一大半了,现在咱们顶部导航就完结了。
底部导航
其实差不多对吧?其间AnimatedVisibility
便是操控是否展现底部导航的。
可是咱们现在看看NavigationBarItem
,咱们经过forEachIndexed来把一切的Item展现出来,再看看NavigationBarItem
的onClick
事情。
onClick = {
mainActivityViewModel.sendIntent(
MainActivityIntent.SelectNavItem(
index,
),
)
when (index) {
0 -> navController.navigateToHome()
1 -> navController.navigateToSetting()
}
},
咱们首先发送一个目的,目的是SelectNavItem
意味着现在是选中了一个Item了,这儿便是MVI的终究一个东西,目的发送,咱们调用了ViewModel的sendIntent
发出了目的。而其实完结便是上面ViewModel代码中的selectNavItem办法,经过延迟来让标题产生跳动改变。
界面内容
终究要说的便是界面内容了,咱们有了底部导航和顶部导航,还有主页加载的内容没说。
Spacer(modifier = Modifier.width(5.dp))
Row(modifier = Modifier.padding(it)) {
Spacer(modifier = Modifier.width(16.dp))
FCNavHost(navController = navController, modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(16.dp))
}
咱们在脚手架的内容部分能够看到写了这段代码,诶嘿发现了吗?这儿调用了咱们前面写的FCNavHost
办法,传递了导航办理的目标和样式特点。
没错,现在经过底部导航的切换代码,就能够操控NavHost
这个组件展现的内容了。
终究当页面只是因为底部导航切换时不需求躲藏两个导航,而进入子页面后就都躲藏起来,那么剩余的就只需FCNavHost
了,然后达到了咱们想要的作用。
终究咱们会得到一个相似它的界面
文末
假如咱们发现文章有内容错误欢迎指正,假如对ViewModel的封装有更好的主张也欢迎告诉我。
终究,咱们假如觉得不错,记得给项目star,项目后面会继续更新。
1250422131/FoodChoice: 食选,解决日子中每天吃饭,吃什么,做什么,怎样做的问题,此项目也是我对JetpackCompose的MVI架构学习的一次实践。 (github.com)