美文网首页JetPackJetpack
用Jetpack+Compose写个简单的界面

用Jetpack+Compose写个简单的界面

作者: cwzqf | 来源:发表于2022-04-03 15:00 被阅读0次

    一、前言

           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那种界面搭积木的玩法。
           接着方法内就是写界面,这里用到了BoxColunm等,实际上对应了Android xml界面的FrameLayoutLinearLayout(vertical),写过Flutter的更不会陌生,简单写了几种组件,感觉相比原始写法高效率了很多,因为属性方法很丰富,以前可能需要写一堆drawable(我最受不了这个),现在可能通过指定modifierstyle一句实现,这才是新时代该有的东西。
           关于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

    相关文章

      网友评论

        本文标题:用Jetpack+Compose写个简单的界面

        本文链接:https://www.haomeiwen.com/subject/afqhdrtx.html