使用Jetpack Compose遇到的一些问题及解决方案
状态管理
LiveData and State
如果你想通过数据变化自动刷新UI显示,LiveData和State都只能在它所包裹的对象发生变化时刷新UI。 所以当我们包裹的是一个对象,只是更改了对象中某个属性的值时,这并不会触发重组,刷新UI。
对于这种情况可以针对对象的某个属性使用MutableState<T>
包裹,例如:
data class People(var name: MutableState<String>, var sex:String) 复制代码
使用时还需注意,需要刷新的Widget还需显示的调用一次该属性,如:
val tom by viewModel.student Box(modifier = Modifier.fillMaxSize()) { Column() { Text(text = tom.name.value) Button(onClick = { viewModel.changeSex() }) { Text(text = "change value") } } } 复制代码
和
val tom by viewModel.student Box(modifier = Modifier.fillMaxSize()) { Column() { Text(text = tom.toString()) ... } } 复制代码
当改变name属性值时,只有前者会触发UI重组(Compose),这与compose的重组机制有关。
手动触发重组
currentRecomposeScope.invalidate()
数据类管理
对于后台返回的展示数据类,推荐自定义一个用于展示的数据类,这样做有几点好处:
方便UI状态控制,使用 MutableState包裹数据
对数据进行预处理,让后续业务流程可以直接使用该数据
方便维护,如果接口返回数据类型有变化,不会影响到业务模块
写法展示:
class GoodsInfoResp( //根据typeGuid已经分好类 val typeList: List<PadTypeRespDTO>? ) { fun toDisplayData() = this.typeList?.map { it.toDisplayData() } } data class PadTypeResp( val menuClassifyPictureType: Int? = 0, val name: String, val sort: Int, val typeGuid: String, val itemList: List<PadItemRespDTO> = mutableListOf(), ) { fun toDisplayData() = DisplayTypeData( menuClassifyPictureType = this.menuClassifyPictureType!!, name = mutableStateOf(this.name), ... padItemRespDTOList = this.itemList.map { it.toDisplayData(this.menuClassifyPictureType) } ) } //获取数据时,在repository中将数据转换成能直接使用的数据类 suspend fun requestGoodsList(): Flow<List<DisplayTypeData>?> { return source.getGoodsList() .map { it.toDisplayData() } .map {...} } 复制代码
事件传递
使用回调
在官方给的demo中,compose的事件都是通过回调层层下发的,像是这样:
@Composable fun RegisterScreen( modifier: Modifier, defaultPhone: String? = null, jump2Login:(String,String?) -> Unit, onHasRegistered:(String) -> Unit, onRegisterClick: ( registerMemberReq: RegisterMemberReq, onRegisterSuc: () -> Unit, onRegistered: () -> Unit ) -> Unit ) 复制代码
如果只有一层还好,如果要层层传递就很难受了,每个widget都要写。
使用viewmodel
如果直接将viewmodel传入widget,的确会省不少事,这样也会出现新的问题。
widget不解耦,需要传入特定的viewmodel
综上暂时没有完美的解决方案,只能权衡利弊使用这两种方式。
Dialog
compose中dialog是通过状态来控制显隐的,这样写也会遇到几个问题:
需要显示这个dialog的activity都要提前写好widget,并用状态去控制它
dialog不能单独处理业务,需要依附于viewmodel
这些问题导致它完全不能复用,所以对于需要复用的业务dialog,推荐还是使用DialogFragment。
ViewModel膨胀
如果使用了官方给的Compose Navigation会导致一个问题,页面其实还是使用的同一个Activity,只有一个ViewModel。
如果业务不够复杂还好,如果界面多、业务复杂会导致ViewModel越来越膨胀,针对这种情况最好还是使用原生的fragment,每个fragment再创建自己的ViewModel。
换肤
如果使用官方给的api换肤会有个颜色数量限制,只能使用这些命名:
class Colors( primary: Color, primaryVariant: Color, secondary: Color, secondaryVariant: Color, background: Color, surface: Color, error: Color, onPrimary: Color, onSecondary: Color, onBackground: Color, onSurface: Color, onError: Color, isLight: Boolean ) 复制代码
所以我模仿官方的写法自定义了个colorSet。代码如下:
class CustomColors( val primary: Color, val background: Color, val primaryVariant: Color, val secondary: Color, ... ) val darkColorSet = CustomColors( primary = green6DDACB, background = Color.Black, primaryVariant = Color.Yellow, secondary = Color.Blue ) val lightColorSet = CustomColors( primary = green6DDACB, background = Color.Cyan, primaryVariant = Color.Gray, secondary = Color.Blue ) @Composable fun ProvideColors( colorSet: CustomColors, content: @Composable () -> Unit ) { CompositionLocalProvider(LocalAppColors provides remember { colorSet }, content = content) } private val LocalAppColors = staticCompositionLocalOf { darkColorSet } object AppTheme { val colors: CustomColors @Composable get() = LocalAppColors.current } //最后在Theme外面包一层 //传入想要使用的主题 ProvideColors(colorSet = customSkin) { MaterialTheme( typography = Typography, shapes = Shapes, content = content ) } //使用时调用 AppTheme.colors.primary 复制代码
屏幕适配
屏幕适配方面我们采用了宽高分别计算比例,再来进行缩放,理论上适配任何屏幕。
首先获取屏幕宽高dp,根据设计稿宽高计算比例。
@Composable fun initScreenConfigInfo() { val config = LocalConfiguration.current val widthDp = config.screenWidthDp.toFloat() val heightDp = config.screenHeightDp.toFloat() scale = config.densityDpi/160f if (heightFactor == 0f) heightFactor = heightDp / designHeightDp if (widthFactor == 0f) widthFactor = widthDp / designWidthDp } @Stable inline val Int.wdp: Dp get() { val result = this.toFloat() * widthFactor return Dp(value = result) } @Stable inline val Int.hdp: Dp get() { val result = this.toFloat() * heightFactor return Dp(value = result) } @Stable inline val Int.spi:TextUnit get() { return this* heightFactor.sp } 复制代码
具体使用时需要根据宽高来选择wdp和hdp。
作者:baima
链接:https://juejin.cn/post/7021818856003862564