用Jetpack+Compose写个简单的界面
一、前言
compose的出现,预示着Android原生端也迈向了声明式界面的开发模式,Android传统的开发方式是界面在XML里编写,然后在Activity里findViewById拿到视图节点进行更新数据,而compose颠覆了这种做法,且不止于此。
我写了个简单的入门demo,请求一个接口,然后用compose组件展示数据,想看看jetpack搭配上compose会擦出什么样的火花。
二、实践
1、接口请求
接口来源:每日诗词
接口使用:1.获取token保存本地(永久有效) 2.后续请求header带上token
以下是viewModel
的代码:
class MainViewModel(private val dataStore: DataStore<Preferences>) : ViewModel() {
private object PreferencesKeys {
val USER_TOKEN = stringPreferencesKey("user_token")
}
var token = MutableLiveData<String>()
/**
* 检查本地token是否存在
*/
suspend fun findLocalToken() {
dataStore.data
.map { preferences ->
val localToken = preferences[PreferencesKeys.USER_TOKEN] ?: ""
if (localToken.isEmpty()) {
obtainToken()
} else {
token.postValue(localToken)
}
}.collect {
}
}
/**
* 获取token并缓存本地
* token永久有效
*/
private suspend fun obtainToken() = PoetryRepository.token()
.collect {
saveToken(it.data)
}
/**
* 请求诗词数据
*/
val poetry: LiveData<PoetryResponse> = token.switchMap {
PoetryRepository.poetry(it).asLiveData()
}
private suspend fun saveToken(value: String) {
dataStore.edit { mutablePreferences ->
token.postValue(value)
mutablePreferences[PreferencesKeys.USER_TOKEN] = value
}
}
}
代码介绍
- 数据Token保存使用到了Jetpack的DataStore,为什么抛弃了sp,而使用DataStore,以下摘自谷歌工程师Florina描述:
1.SharedPreferences毕竟是磁盘IO操作,其同步API有可能阻塞主线程
2 SharedPreferences将解析错误直接作为异常抛出
而DataStore则配合上协程(Flow),数据的处理也更加的灵活,且数据操作在Dispatchers.IO上进行,规避了Sp的线程安全问题。
用法不再详细描述,Write和Read的方法都在这个viewModel里
- 拿到接口数据后,暴露LiveData给UI层,这里使用到了LiveData的switchMap方法,这个方法实际上使用场景比较有限,但对于监听某个变量非常好用,我们这里监听了 token ,一旦token的value发生改变,switchMap的方法自动执行,UI层监听poetry拿到数据更新界面,怎么更新界面,以下进入compose环节。
2、compose界面展示
直接晒干了沉默的代码:
@Composable
private fun MainContent(
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel()
) {
Box(modifier = modifier.fillMaxSize()) {
val poetry by viewModel.poetry.observeAsState()
WavesLoadingIndicator(modifier = Modifier.fillMaxSize(), color = Purple500, progress = .4f)
Text(
color = Purple500,
modifier = Modifier.fillMaxSize(),
text = poetry?.data?.matchTags?.first() ?: "唐", fontSize = 188.sp,
fontFamily = FontFamily(Font(R.font.yegenyou, weight = FontWeight.Bold))
)
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = poetry?.data?.content?.renderPoetry() ?: "nothing",
lineHeight = 46.sp,
fontSize = 36.sp,
fontFamily = FontFamily(Font(R.font.yegenyou, weight = FontWeight.Bold)),
fontWeight = FontWeight.Bold
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 12.dp),
horizontalArrangement = Arrangement.End
) {
Text(
modifier = Modifier
.background(Color.Red, RectangleShape)
.padding(6.dp),
text = "${poetry?.data?.origin?.author}",
fontSize = 16.sp,
color = Color.White,
fontFamily = FontFamily(Font(R.font.wanghanz, weight = FontWeight.Bold)),
textAlign = TextAlign.End
)
Text(
text = "《${poetry?.data?.origin?.title}》",
fontSize = 16.sp,
textAlign = TextAlign.End
)
}
}
val showDialogState: Boolean by viewModel.showDialog.collectAsState()
Image(
painterResource(id = R.drawable.ic_brush), contentDescription = null,
modifier = Modifier
.width(60.dp)
.height(60.dp)
.align(Alignment.TopEnd)
.statusBarsPadding()
.padding(horizontal = 12.dp)
.clickable(onClick = {
viewModel.openDialog()
})
)
PersonalPoetryDialog(show = showDialogState, onDismiss = { },
onConfirm = {
viewModel.onDialogDismiss()
})
}
}
compose的使用也不再详细描述,这次是因为我现在只懂得以下这些皮毛,有一说一。
首先@Composable注解表面该方法是界面组合者,简单讲就是界面的一部分,有点RN那种界面搭积木的玩法。
接着方法内就是写界面,这里用到了Box
、Colunm
等,实际上对应了Android xml界面的FrameLayout
和LinearLayout
(vertical),写过Flutter的更不会陌生,简单写了几种组件,感觉相比原始写法高效率了很多,因为属性方法很丰富,以前可能需要写一堆drawable(我最受不了这个),现在可能通过指定modifier
或style
一句实现,这才是新时代该有的东西。
关于compose的布局不多说,因为目前我的demo才写了一点,涉及面比较窄,这里只记录下一个遇到的问题,以及解决方法。
我需要一个弹窗,ok,我写了一个PersonalPoetryDialog.kt
@Composable
fun PersonalPoetryDialog(
show: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
var text by remember {
mutableStateOf("")
}
if (show) {
AlertDialog(onDismissRequest = onDismiss,
title = {
Text(text = "开始你的创作")
},
text = {
OutlinedTextField(value = text, label = { Text("题名") }, onValueChange = {
text = it
})
},
buttons = {
...
}
)
}
}
然后这个dialog是在一个click事件里调用
.clickable(onClick = {
PersonalPoetryDialog()
})
这时候报错了:@Composable invocations can only happen from the context of a @Composable function compose
compose方法只能在compose方法里使用(简单翻译),我们无法在click事件里调用compose注解的方法,这里的解决方法实际有点vue的v-if那味,如PersonalPoetryDialog
方法参数的show
,我们控制这个show
达到控制dialog的显示隐藏
val showDialogState: Boolean by viewModel.showDialog.collectAsState()
PersonalPoetryDialog(show = showDialogState, onDismiss = { },
onConfirm = {
viewModel.onDialogDismiss()
})
只有当这个showDialogState
设为true
,才会显示dialog,反之隐藏
viewmodel里的代码
private val _showDialog = MutableStateFlow(false)
val showDialog: StateFlow<Boolean> = _showDialog.asStateFlow()
fun openDialog(){
_showDialog.value = true
}
fun onDialogConfirm(){
onDialogDismiss()
}
fun onDialogDismiss() {
_showDialog.value = false
}
click里不再强行塞一个compose方法,而是调用openDialog()
改变_showDialog
的值,_showDialog
值一旦发生改变,外部监听者通过collectAsState()
就会监听到,进而作出响应,这跟前半段我们监听poetry,最后也是observeAsState()
来监听它,达到了数据驱动的效果,如果像以前的MVP那确实够麻烦的,都要手动来操作。
三、总结
以上就是我这两天利用空闲尝试下mvvm,jetpack,compose这些Android界新生品的记录,只能是简单入门,像remember
这些概念还没熟练使用,因为这块现在用的还是比较少,所以也没太多的机会去实践,今后有时间会继续完善这个demo,深入jetpack+compose这套东西,用起来不得不说很香。顺便po上我的demo github地址 Poetry 。