公众号「稀有猿诉」 原文链接 轻松解决Jetpack Compose中的一些痛点问题
暑去秋来,金桂飘香,不知不觉中我们已经练完了『降Compose十八掌』,相信通过这一系列文章能够对Jetpack Compose有足够的理解,并能在实际项目中进行运用。今天将继续Compose之旅,总结一下Compose使用过程中经常会遇到的一些痛点问题,并学会如何优雅的解决这些问题。
定义slot时要注明布局作用域
先来看一个比较常规的问题,Compose开发过程中,非常鼓励开发者把可以复用的部分抽象成为一个函数,然后接收一个尾部lambda作为参数进行差异化的定制。这种范式叫做slot模式,slot模式的好处在于能够大大加强代码复用,开发者在构建UI的时候,像搭积木那样把一个一个的slot叠在一起。Compose自己的API中都大量的采用了这种模式。
为了让slot更加的通用,我们需要明确传入的lambda与slot之间的约定,这就要求我们对lamdba的类型进行严格的限制。
首先要添加注解@Composable,这个是显而易见的,因为slot是为了绘制一些自定义UI元素而准备的,所以肯定是要加上@Composable,否则在lambda中无法写UI,因为非Composable不能调用Compose的方法。
另外,不是那么明显的就是这个lamdba的类型,要指定其Receiver,以限定它所在的布局。比如说slot是用在一个Column里面的,那么要给lamdba指定ColumnScope作为receiver,这样在实现lambada的时候就知道是作为Column的一部分,并且可以使用Column布局的特有相关参数,如左右居中和垂直排列:
@Composable
MyLayout(modifier: Modifier,content: @Composable ColumnScope.()->Unit
) {Column() {// 共用的一部分content()}
}// 调用的地方
MyLayout { // this = ColumnScope// 隐式this指针指向一个ColumnScope对象,就像在一个Column中一样// 定制的部分
}
UI元素很多都会涉及到居中,对齐的调整,以及内部元素的排列,而对齐和排列又会明确的受到所在父布局的影响,比如说Box与Column的对齐和排列方式就是不同的。所以在使用slot时一定要明确 标注它所在的布局,以让调用者能够明确地知道lambda所在的布局作用域。
扩展阅读:
- Slotting in with Compose UI
- Practical Compose Slot API example
- Designing Slot APIs in Jetpack Compose
- 初探 Jetpack Compose — Slot API
如何在ViewModel中使用平台相关的资源
我们在降Compose十八掌之『神龙摆尾』| Architecture中讨论过,ViewModel作为Domain层,目的是把逻辑尽可能的从UI层中抽出来,让UI尽可能的只做UI渲染。ViewModel也要做到平台独立,这样才方便移植和测试。ViewModel中吐出来的数据要是加工过的可以直接方便地在UI层展示的数据,如字符串或者图片。
但有一个问题,资源文件如何管理都是平台强相关的。对于要展示给用户的文案,也不可能直接把字符串传给UI,因为UI语言都要能够本地化以适应不同的国家和地区,当然了如果说不需要考虑多语言的问题,比如我的应用只给某一个语言使用,那当然也可以直接把处理好的字符串当作UiState传给UI层。
最为理想的解决方案就是ViewModel层定义一些状态码,对应着不同的提示语言,由UI负责一一对应的,把状态码再转成字符串。对于其他的资源也可以采用类似方式处理。这是从ViewModel输出到UI层的情况。
还会反过来,对于需要从UI层输入到ViewModel的资源,也是要去除平台的相关性,比如转成ViewModel中定义的状态码,或者转成原始数据类型String,或者转成平台无关的输入输出流等等。
字符串资源
对于Android平台来说,可以用一个简单的方式来解决字符串资源问题,因为资源的引用是一个整数,所以可以直接把资源的ID当作字段传给UI,Compose拿到后直接用函数stringResource取出来就可以了:
data class UiState(val loading: Boolean = false,@StringRes val errorMsg: Int = 0
)class HomeViewModel {val timeout = UiState(false, R.string.error_message_timeout)_state.update(timeout)
}// 在Compose中
HomeScreen() {Text(stringResource(uiState.errorMsg))
}
虽然说这并不太通用,因为换成其他平台时可能不是用资源ID来获取资源,但转成状态码的方式也会很容易,所以问题不大。
如果是输入的话,在Compose中直接读取资源变成String传给ViewModel就好了。
扩展阅读:
- How should I get Resources(R.string) in viewModel in Android (MVVM and databinding)
- Using String Resources in a ViewModel
图片资源
图片资源一般来说都是UI自己指定,但有些时候可能会有逻辑,比如一些需要经过运算才能得到的复杂的状态,其代表的Icon,由ViewModel来直接指定要好一些。图片资源也可以直接使用资源ID,然后在Compose中使用painterResource来获取:
data class UiState(@DrawableRes val icon: Int = 0
)// in ViewModel
val state = UiState(R.drawable.ic_windy)// in Compose
Icon(painterResource(uiState.icon))
如果是输入的话,可以在Compose中把图片资源转成输入流传给ViewModel去处理。
其他资源
其他资源如dimen或者color,也可以如法炮制。
输入的话,对于普通的资源像字符串资源,dimen或者color等读出来转成基础数据类型String,Int或者Array传给ViewModel就好。而像比较麻烦的资源,如Assets中的资源,就转成输入流传给ViewModel处理。
如何在常规函数中调用Composables
在Compose的开发过程中最为令人不爽的地方在于Compose 的API,只能在被注解@Composable标注的函数中调用,其他地方是无法调用的。一般来说,这个问题也不大,因为Compose的入口是肯定是一个composable啊,一坨坨的composables的调用最终会生成UI树。
但有些地方却跑出了Composable之外,比如像很多UI元素的事件响应,比如Button,它的事件响应onClick接收的就是一个普通的lambda:
@Composable
fun MainContent(modifier: Modifier = Modifier,serviceOn: Boolean,context: Context
) {Column() {Button(onClick = { AccessibilityHelper.gotoChronosSettings(context) },) {Text(stringResource(if (serviceOn) R.string.turn_off_service else R.string.enable_service))}}
}
在Button的onClick里面可以执行一些普通函数调用,比如调用ViewModel等,但是不可以调用Compose的API,因为它是非Composable的,已经跑到了Composable之外。有些场景,这会带来比较大的不方便。
响应点击按扭的方式可能有很多,有些是执行一些普通函数调用,但有些时候也会修改UI,大部分时候也会创建新UI,比如说会弹出对话框。对于修改UI,可以直接通过修改状态的值,状态的值发生改变会触发重组,进而UI状态就会改变(通过读取状态的值显示 不同的UI)。
对话框Dialog也是一个Composable,它只能被Composable调用,无法在Button的onClick中直接调用Dialog。解决的办法依旧是借助状态,用一个Boolean型值的状态,当其为true时显示Dialog,在Button的onClick中更改此状态为true,状态变了触发重组,在重组时值为true就会显示Dialog了:
@Composable
fun InputSettingsEntry(label: String,description: String,value: String,onChange: (String)->Unit
) {var showing by remember { mutableStateOf(false) }Button(onClick = { showing = true }) {Text(value)}if (showing) {InputDialog(title = label,message = description,value = value,onDismiss = { showing = false },onConfirm = {onChange(it)showing = false})}
}
另外,其实Dialog本身的一些事件响应也都是非Composable的,都是通过状态来控制Dialog的显示与否。
扩展阅读:
- Not able to show AlertDialog from onClick in Jetpack Compose
- Alert Dialog with Jetpack compose: A Step-by-Step Guide
总结
Jetpack Compose博大精深,看似简单就是一坨函数,但在实际项目使用中会遇到各种细节问题。遇到问题也不用慌,用我们的『降Compose十八掌』都能解决,没事就多读一读,理解了Compose的思想与原理,做到心中无剑,很多问题都能迎刃而解。
References
- 10 Android Jetpack Compose Best Practices
- 6 Jetpack Compose Best Practices for Optimizing Your App Performance
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!