状态管理
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。