前言
在前面几篇我们已经熟悉了MVI的业务,做了网络、数据库封装、做了导航探索甚至还做了一个KSP库,这些内容可以让我完成一些简单的需求开发了,但是我最近做前端的界面,突然想起了响应式,这就让我想起Android的响应式了,我们之前得靠SlidingPaneLayout
或者Fragment
来做这件事,这很繁琐不是吗?这就让我探索起了在Compose
上的响应式。
谷歌的想法
我们可以看到Android自己对大屏设备的适配想法。
支持不同的屏幕尺寸 | Android 开发者 | Android Developers (google.cn)
我们暂时不谈及响应式后界面内容的变化,那么从图片我们可以看出,随着尺寸的变化,导航栏的位置,大小都发生了改变。
我在前端 做适配的时候也会做类似的事情,比如在桌面端 变为移动端 时,将抽屉导航换到顶部导航 ,将网格布局的Item项数减少 ,将横向的Flex布局改为垂直排布的,是的在这个适配过程中我不希望内容减少,我希望的是它可以去合适的地方展示。

Android自己也有类似的说法,不过它提及了我们可能需要在不同分辨率 下使用 不同的Model
,这适合于下面这样的界面。
比如你的数据类型可能变多,比如在大屏幕页展示更多种类的内容,或者下面这种,可以直接展示文章内容的,你可能需要准备更多。

这就留给各位开发者继续探索了。
问题分析与解决
我们现在要在Jtepack Compose
完成响应式,即不同界面展示不同的样式,我们将前端的办法进行迁移,我们在前端要通过媒体查询 来设置断点,以此完成响应式,那么我们在安卓是否也是可以呢?答案当然是可以的!
那么我就去想怎么样获取到窗口大小了 我在刚刚的官网当中就发现了这个内容,官方指出,Jetpack Compose中有窗口尺寸相关API androidx.compose.material3.windowsizeclass | Android Developers (google.cn)

好,calculateWindowSizeClass
这个API相当简单,传入上下文即可获得窗口大小,这个方法返回了widthSizeClass
和heightSizeClass
对应了前面官网提出的两个响应式方式(在我第一个粘贴的网址可见)。
但是有问题,这个东西在Compose
用就要Context ,但是我不可能处处写LocalContext.current
这很麻烦,我们需要想想办法。
解决方案
最终,我们落到的点就是,需要用calculateWindowSizeClass
这个方法,但是我们需要为他进行封装。
kotlin
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun getWidthSizeClass(context: Context = LocalContext.current): WindowWidthSizeClass {
return calculateWindowSizeClass(context as ComponentActivity).widthSizeClass
}
我们封装下这个方法,让它只能在Compose函数内使用,我们对他注解@Composable
,这个注解本身是给一个Compose函数注解的,在这个注解的方法内可以写Compose的UI,那么就可以调用LocalContext.current
,这样我们就可以给这个函数一个默认值,让它不需要传Context进去。
啊哈,现在我们使用时只需要调用getWidthSizeClass()
即可获取到窗口宽度!
响应式实践
下面内容推荐拉下源码边看边读,文章中不会粘贴完整代码。
底部导航转侧边导航
底部导航栏

首先让我们试试看,将底部导航变化到侧边导航上,这个问题在Compose里可以说相当好办了,我们所有的界面反馈都是被状态驱动的。

这是在Scaffold 组件上的底部导航,其实是否转换到侧边导航,底部导航要做的就是隐藏 或者显示 ,我们在上一篇文章导航实践
里也说了这里动画的原因,就是切换界面时好看一些,让界面动起来,现在我们也可以把条件加在这里,当界面是Compact
时并且需要底部导航那么就展示。
这样我们就实现底部导航只在手机设备 或者说小屏幕设备展示。
侧边导航栏

侧边导航栏比较特殊,它不在Scaffold 的插槽中,而是在其Content中,在图上我圈出的部分,就是侧边导航栏。

此外,这里的条件是 viewStates.isShowBottomBar
并且getWidthSizeClass() > WindowWidthSizeClass.Compact
,什么意思呢,就是底部导航展示,你也展示,因为需要底部导航也就意味着需要侧边导航 ,但是他们是二选一的,这就是后一个条件,窗口大小必须大于小窗口大小。
这样,比手机设备大的窗口就显示侧边导航,而手机设备就展示底部导航。
网格布局转换
上面食选的响应式还有个有意思的地方就在于不同设备下展示的Item数不同。

这就要提到我们的LazyVerticalGrid
了,这个组件可以定义columns,也就是一行展示多少个,我们这里用when来完成,小屏幕一个,中形屏幕两个,大屏幕三个。

怎么样?在Jetpack Compose
上做响应式是如此的简单,另外最近KMM
也在走向完善和成熟,大家可以将这个想法迁移到跨平台上去做,我相信使用体验也是很不错的。
重构小课堂
从上一篇文章大家会发现文末会有一个小课堂了,后面每篇应该都会有,主要讲一讲之前设计的问题和新的想法,订阅和长期阅读这个专栏的读者可以看看,假如只是对本文感兴趣那下面的内容可能对你帮助不是很大。
NavHost的小坑
我发现切换页面时,隐藏底部和顶部导航的动画会卡,进一步说就是首页切换下一个界面时会卡顿,这让我百思不得其解,我最开始认为是第二页数据加载多导致的,后来我减轻了一些数据发现的确有效果,但是不明显。
对于这个问题我就仔细看了下这个切换动画,我发现除了我自己写的AnimatedVisibility
外,好像还有别的东西,切换时会有一个淡入的效果,但是我没写过这样的动画啊,这时我就将视线转到了NavHost
,我们看看这个方法里有啥。

啊?真有自带的动画!!!

但这并不是我想要的,于是我们赶快给NavHost去除默认动画,干掉之后我发现界面切换卡顿不明显了,减少数据会更流畅。
因此我认为NavHost的动画,与我自己的动画重叠后发生了这个问题,给大家分享出来算是个坑。
网络请求改良
这是CookFoodInfoRepository
,在data模块 ,是我们定义的一个数据源,还记得我们早期是怎么同步数据的吗?
我们用runCatching
来捕获异常,这样虽然也很不错,但是我们下面拿值却要用runCatching
的结果调用getOrNull
,再通过空判断决定是否更新。

这样显然有一些麻烦,我们还有一个另一个手段,那就是flow,我们可以直接在流中捕获异常,而且我们不需要判空了,因此现在把请求结果作为流来处理,还记得吗Retrofit
本身支持协程,配合Flow也很不错。
但是请求是个耗时操作,需要挂起,我们需要flowOn来转换下不同的线程池,这显然是一个重复工作,我们把他封装起来。
kotlin
@OptIn(ExperimentalTypeInference::class)
fun <T> makeRequestInFlow(@BuilderInference requestBlock: suspend FlowCollector<T>.() -> Unit): Flow<T> {
// 切换流到IO线程,特别的,由于协程上下文传递,ViewModel处理意图本身就是在IO里,可以不可以这么做,观察后是否
return flow(block = requestBlock).flowOn(Dispatchers.IO)
}
我在data模块新建了Request.kt的文件,我们对Flow进行一次简单的封装。

最后我们的同步函数内就变为了这样,用emit发送请求结果给下游消费,其实感觉也不差,flow能做的也很强。
当然这种做法不一定对,我个人现在喜欢用Flow来处理,后面有其他改动再分享给大家。
文末
非常感谢各位看到这里,如果对本文章的内容感兴趣,可以Star项目看看。
文章内容如果有问题或者你有更好的解决方案,欢迎在评论区告诉我。