JetpackCompose的响应式布局实践 | 实验分享

前言

在前面几篇我们已经熟悉了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相当简单,传入上下文即可获得窗口大小,这个方法返回了widthSizeClassheightSizeClass对应了前面官网提出的两个响应式方式(在我第一个粘贴的网址可见)。

但是有问题,这个东西在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()即可获取到窗口宽度!

响应式实践

下面内容推荐拉下源码边看边读,文章中不会粘贴完整代码。

FoodChoice: 食选,解决生活中每天吃饭,吃什么,做什么,怎么做的问题.

底部导航转侧边导航

底部导航栏

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

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

这样我们就实现底部导航只在手机设备 或者说小屏幕设备展示。

侧边导航栏

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

此外,这里的条件是 viewStates.isShowBottomBar并且getWidthSizeClass() > WindowWidthSizeClass.Compact,什么意思呢,就是底部导航展示,你也展示,因为需要底部导航也就意味着需要侧边导航 ,但是他们是二选一的,这就是后一个条件,窗口大小必须大于小窗口大小。

这样,比手机设备大的窗口就显示侧边导航,而手机设备就展示底部导航。

网格布局转换

上面食选的响应式还有个有意思的地方就在于不同设备下展示的Item数不同。

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

怎么样?在Jetpack Compose上做响应式是如此的简单,另外最近KMM也在走向完善和成熟,大家可以将这个想法迁移到跨平台上去做,我相信使用体验也是很不错的。

重构小课堂

从上一篇文章大家会发现文末会有一个小课堂了,后面每篇应该都会有,主要讲一讲之前设计的问题和新的想法,订阅和长期阅读这个专栏的读者可以看看,假如只是对本文感兴趣那下面的内容可能对你帮助不是很大。

我发现切换页面时,隐藏底部和顶部导航的动画会卡,进一步说就是首页切换下一个界面时会卡顿,这让我百思不得其解,我最开始认为是第二页数据加载多导致的,后来我减轻了一些数据发现的确有效果,但是不明显。

对于这个问题我就仔细看了下这个切换动画,我发现除了我自己写的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项目看看。

FoodChoice: 食选,解决生活中每天吃饭,吃什么,做什么,怎么做的问题。

文章内容如果有问题或者你有更好的解决方案,欢迎在评论区告诉我。

相关推荐
陈旭金-小金子9 小时前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
移动开发者1号12 小时前
Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
android·kotlin
移动开发者1号12 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·kotlin
alexhilton16 小时前
使用用例(Use Case)以让Android代码更简洁
android·kotlin·android jetpack
zimoyin18 小时前
Java/Kotlin selenium 无头浏览器 [Headless Chrome] 实现长截图 三种方式
java·selenium·kotlin
androidwork20 小时前
Android 中 OkHttp 的自定义 Interceptor 实现统一请求头添加
android·java·okhttp·kotlin
雨白2 天前
高阶函数的应用:简化 SharedPreferences 与 ContentValues 操作
kotlin
移动开发者1号2 天前
Retrofit动态URL与Path参数处理
android·kotlin
移动开发者1号2 天前
Android 中 OkHttp 的自定义 Interceptor 实现统一请求头添加
android·kotlin
小金子同志2 天前
发现 Kotlin MultiPlatform 的一点小变化
kotlin