Jetpack Compose 正式版发布也已半年了,对我来说,应用到项目中也很久了。 目前很多文章还集中于初探上,因此萌生了写作本文的想法,算是为Compose中文资料提供绵薄之力。本文的内容来自Android官方视频:Deep dive into Jetpack Compose layouts总览Jetpack Compose 中,单个可组合项被显示出来,总体上经历三个过程Composition(组合) -> Layout(布局) -> Drawing(绘制) ,其中Layout阶段又存在两个方面的内容:Measure(测量) 和 Place(摆放)今天我们主要着眼于 Layout 阶段,看看各个 Composable 是如何正确确定各自位置和大小的LayoutLayout阶段主要做三件事情:测量所有子微件的大小确定自己的大小正确摆放所有子元素的位置为简化说明,我们先给出一个简单例子。该例子中,所有元素只需要遍历一次。如下图的 SearchResult微件,它的构成如下:现在我们来看看Layout过程在这个例子中是什么情况Measure请求测量根布局,即RoWRow为了知道自己的大小,就得先知道自己的子微件有多大,于是请求Image和Column测量它们自己对于Image,由于它内部没有其他微件,所以它可以完成自身测量过程并返回相关位置指令接下来是Column,因为它内部有两个Text,于是请求子微件测量。而对于Text,它们也会正确返回自己的大小和位置指令这时 Column 大小和位置指令即可正确确定最后,Row内部所有测量完成,它可以正确获得自己的大小和位置指令测量阶段到此结束,接下来就是正确的摆放位置了Place完成测量后,微件就可以根据自身大小从上至下执行各子微件的位置指令,从而确定每个微件的正确位置现在我们把目光转向Composition阶段。大家平时写微件,内部都是由很多更基本的微件组合而来的,而事实上,这些基本的微件还有更底层的组成部分。如果我们展开刚刚的那个例子,它就成了这个样子在这里,所有的叶节点都是Layout这个微件我们来看看这个微件吧Layout Composable此微件的签名如下:@Composable inline fun Layout( content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy )我们先看看第三个参数,这是之前从未见过的东西;而它恰恰控制着如何确定微件大小以及它们的摆放策略那来写个例子吧。我们现在自定义一个简单的纵向布局,也就是低配版Column自定义布局 - 纵向布局写个框架fun VerticalLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables: List<Measurable>, constrains: Constraints -> } }Measurable代表可测量的,其定义如下:interface Measurable : IntrinsicMeasurable { /** * Measures the layout with [constraints], returning a [Placeable] layout that has its new * size. A [Measurable] can only be measured once inside a layout pass. */ fun measure(constraints: Constraints): Placeable }可以看到,这是个接口,唯一的方法measure返回Placeable,接下来根据这个Placeable摆放位置。而参数measurables其实也就是传入的子微件形成的列表而Constraints则描述了微件的大小策略,它的部分定义摘录如下:举个栗子,如果我们想让这个微件想多大就多大(类似match_parent),那我们可以这样写:如果它是固定大小(比如长宽50),那就是这样写接下来我们就先获取placeable吧val placeables = measurables.map { it.measure(constrains) }在这个简单的例子中,我们不对measure的过程进行过多干预,直接测完获得有大小的可放置项接下来确定我们的VerticalLayout的宽、高。对于咱们的布局,它的宽应该容纳的下最宽的孩子,高应该是所有孩子之和。于是得到以下代码:// 宽度:最宽的一项 val width = placeables.maxOf { it.width } // 高度:所有子微件高度之和 val height = placeables.sumOf { it.height }最后,我们调用layout方法返回最终的测量结果。前两个参数为自身的宽高,第三个lambda确定每个Placeable的位置layout(width, height){ var y = 0 placeables.forEach { it.placeRelative(0, y) y += it.height } }这里用到了Placeable.placeRelative方法,它能够正确处理从右到左布局的镜像转换一个简单的Column就写好了。试一下?fun randomColor() = Color(Random.nextInt(255),Random.nextInt(255),Random.nextInt(255)) @Composable fun CustomLayoutTest() { VerticalLayout() { (1..5).forEach { Box(modifier = Modifier.size(40.dp).background(randomColor())) } } }嗯,工作基本正常。接下来我们实现一个更复杂一点的:简易瀑布流自定义布局—简易瀑布流先把基本的框架撸出来,在这里只实现纵向的,横向同理@Composable fun WaterfallFlowLayout( modifier: Modifier = Modifier, content: @Composable ()->Unit, columns: Int = 2 // 横向几列 ) { Layout( modifier = modifier, content = content, ) { measurables: List<Measurable>, constrains: Constraints -> TODO() } }我们加入了参数columns用来指定有几列。由于瀑布流宽度是确定的,所以我们需要手动指定宽度val itemWidth = constrains.maxWidth / 2 val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth) val placeables = measurables.map { it.measure(itemConstraints) }在这里我们用新的 itemConstraints 对子微件的大小进行约束,固定了子微件的宽度接下来就是摆放了。瀑布流的摆放方式其实就是看看当前哪一列最矮,就把当前微件摆到哪一列,不断重复就行代码如下:@Composable fun WaterfallFlowLayout( modifier: Modifier = Modifier, columns: Int = 2, // 横向几列 content: @Composable ()->Unit ) { Layout( modifier = modifier, content = content, ) { measurables: List<Measurable>, constrains: Constraints -> val itemWidth = constrains.maxWidth / columns val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth) val placeables = measurables.map { it.measure(itemConstraints) } // 记录当前各列高度 val heights = IntArray(columns) layout(width = constrains.maxWidth, height = constrains.maxHeight){ placeables.forEach { placeable -> val minIndex = heights.minIndex() placeable.placeRelative(itemWidth * minIndex, heights[minIndex]) heights[minIndex] += placeable.height } } } }这里用到了一个自定义的拓展函数minIndex,作用是寻找数组中最小项的索引值,代码很简单,如下:fun IntArray.minIndex() : Int { var i = 0 var min = Int.MAX_VALUE this.forEachIndexed { index, e -> if (e<min){ min = e i = index } } return i }效果如下(设置列数为3):现在的布局只是简单情况,然而事实上,很多时候往往涉及到其他内容。Modifier 的奥秘也等待我们进一步探索。再叙。
本文具体包括:列表项数据顺序变更时的轮换动画、添加/删除列表项时的小动画、侧滑删除动画不废话,本文主要介绍的就是一个修饰符:Modifier.animateItemPlacement()。该修饰符首次出现于Jetpack Compose 1.1.0-beta03版本(此版本于2021年11月17日发布),目前尚处于试验性阶段。但它能实现的效果是非常有趣的。顺序变更 动画简单看一个例子: var list by remember { mutableStateOf(listOf("A", "B", "C", "D", "E")) } LazyColumn { item { Button(onClick = { list = list.shuffled() }) { Text("打乱顺序") } } items(items = list, key = { it }) { Text("列表项:$it", Modifier.animateItemPlacement()) } }运行效果如下:实现如此简单!侧滑删除动画在Jetpack Compose中,实现侧滑删除需要用到SwipeToDismiss微件简单例子如下:数据类: data class Student(val id:Int, val name:String)微件 var studentList by remember { mutableStateOf( (1..100).map { Student(it, "Student $it") } ) } LazyColumn( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(studentList, key = {item: Student -> item.id }){ item -> // 侧滑删除所需State val dismissState = rememberDismissState() // 按指定方向触发删除后的回调,在此处变更具体数据 if(dismissState.isDismissed(DismissDirection.StartToEnd)){ studentList = studentList.toMutableList().also { it.remove(item) } } SwipeToDismiss( state = dismissState, // animateItemPlacement() 此修饰符便添加了动画 modifier = Modifier.fillMaxWidth().animateItemPlacement(), // 下面这个参数为触发滑动删除的移动阈值 dismissThresholds = { direction -> FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f) }, // 允许滑动删除的方向 directions = setOf(DismissDirection.StartToEnd), // "背景 ",即原来显示的内容被划走一部分时显示什么 background = { /*保证观看体验,省略此处内容*/ } ) { // ”前景“ 显示的内容 /*省略一部分不重要修饰*/ Text(item.name, Modifier.padding(8.dp), fontSize = 28.sp) } } }效果如下:上述代码来自于 android - SwipeToDismiss inside LazyColumn with animation - Stack Overflow其他animateItemPlacement修饰符仅在LazyColumn和LazyRow中有效,并且必须指定key参数。除重排外,由更改alignment或者arrangement引起的位置改变也会被添加动画。此修饰符有一个可选参数:animationSpec: FiniteAnimationSpec<IntOffset>,你可以修改此参数以更改动画效果。
前言FunnyTranslation 是基于Jetpack Compose写成的翻译软件。它首先是个可以用的软件,其次也是个开源项目。开源地址:FunnySaltyFish/FunnyTranslation: 基于Jetpack Compose开发的翻译软件,支持多引擎、插件化~ | Jetpack Compose+MVVM+协程+Room (github.com)运行截图上面截图显示的所有UI都是基于Jetpack Compose搭建的挑点啥说一说项目开始时觉得没什么,但是真的写起来却发现不少坑。下面简单挑一个例子。导航栏内容与底部同步软件的导航栏是基于Row自己定义的,摘录部分如下: @ExperimentalAnimationApi @Composable fun CustomNavigation( screens : Array<TranslateScreen>, currentScreen: TranslateScreen = screens[0], onItemClick: (TranslateScreen) -> Unit ) { Row( modifier = Modifier .background(MaterialTheme.colors.background) .padding(start=8.dp,end=8.dp,bottom = 4.dp,top = 2.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceAround ) { screens.forEach{ screen-> CustomNavigationItem(item = screen, isSelected = currentScreen==screen) { onItemClick(screen) } } } }UI实现并不复杂,问题在于用它导航的时候首先是点击Icon跳转到对应页面,这一部分理论上并不复杂,用navController.navigate(screen.route)就能达到最简单的效果然后第一个问题出现了:每次进行navigate的时候,都会重新创建对应的Screen,导致之前页面输入的内容啥的都会被清空;而且每一次点开对应页面就会有一个新的,导致返回的时候需要疯狂点击回退,在不同页面间反复横跳……这肯定不行怎么办呢?参考官方文档,说是加上launchSingleTop=true即可,也就是改成下面这样 navController.navigate(screen.route){ launchSingleTop = true }然后……然后居然还是不行!我不得其解,终于在过了几天后找到了有效的写法: navController.navigate(screen.route){ //当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页 popUpTo(navController.graph.startDestinationId){ saveState = true } //从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例 launchSingleTop = true //切换状态的时候保存页面状态 restoreState = true }这应该是从一篇博客上看到的,具体哪篇现在也找不到了。在此表示感谢!上面的问题解决了,新的问题又来了:点击返回键时,导航栏也应该跟着变动,回到主页面。这一部分的需求又该怎么实现呢?最终在开源项目里面一顿乱翻,终于找到了一个解决方案:在NavController.OnDestinationChangedListener回调中使用hierarchy进行匹配,让页面内容与底部导航栏始终保持同步。相关代码如下: val selectedItem = remember { mutableStateOf<TranslateScreen>(TranslateScreen.MainScreen) } DisposableEffect(this) { val listener = NavController.OnDestinationChangedListener { _, destination, _ -> when { destination.hierarchy.any { it.route == TranslateScreen.MainScreen.route } -> { selectedItem.value = TranslateScreen.MainScreen } // ...省略类似的几个 destination.hierarchy.any { it.route == TranslateScreen.ThanksScreen.route } -> { selectedItem.value = TranslateScreen.ThanksScreen } } } addOnDestinationChangedListener(listener) onDispose { removeOnDestinationChangedListener(listener) } }顺便用BackHandler实现了退出时二次确认: BackHandler(enabled = true) { if (navController.previousBackStackEntry == null){ val curTime = System.currentTimeMillis() if(curTime - activityVM.lastBackTime > 2000){ scope.launch { scaffoldState.snackbarHostState.showSnackbar(FunnyApplication.resources.getString(R.string.snack_quit)) } activityVM.lastBackTime = curTime }else{ exitAppAction() } }else{ Log.d(TAG, "AppNavigation: back") //currentScreen = TranslateScreen.MainScreen } }这部分完整代码在这:FunnyTranslation/TransActivity.kt at compose · FunnySaltyFish/FunnyTranslation (github.com)这样类似的地方在应用开发时还有很多,我相信这里的各位能有同感,就不在这搞吐槽大会了。不过既然尝试了一门新技术,就得做好万事碰壁自己探索的准备。这样也才有点Coder的精神嘛~
前言:在去年的这个时候,谷歌官方推荐使用 Flow 替代LiveData,一年时间过去了,我相信还是有很多android开发的朋友和我一样有以下几个问题:Android开发人员需要从 LiveData 迁移到 Kotlin Flows 吗?LiveData 现在是否已弃用?官方文档:developer.android.google.cn/kotlin/flow推荐阅读:zhuanlan.zhihu.com/p/139582669推荐阅读:juejin.cn/post/697900…通过阅读本文你能了解到或学到什么:① Flow, Shared flow & State flow的使用(具体操作本文就不多说了,给大家推荐好文)② SharedFlow 和 StateFlow,它们也有自己的可变类型——MutableSharedFlow 和 MutableStateFlow,对比LiveData我到底用哪一个?③我们已经有了Flow,为啥还会有SharedFlow、StateFlow,Flow不够用吗?④我要迁移到Flow吗?有人问:LiveData是不是真的快要被废弃了。LiveData:你是故意找茬?我要迁移到Flow吗?我们先来回答这个大家最关心的问题结论:如果 LiveData 满足您的需求,那么就不急于替换它,如果是一个新项目,推荐在 UI 中用 LiveData,在Repo层 中用 Flow。下面请欣赏RxJava与LiveData&Flow的爱恨情仇:在2017年之前,大家都是使用RxJava去配合Retrofit实现网络请求,RxJava实现事件订阅。但是,谁用谁知道 (真的复杂,各种线程的切换,头脑爆炸,不过,现在用协程就可以啦) 。因为是真的复杂,对大部分开发者不是很友好,于是在2017年那样的环境下,谷歌推出了LiveData。但是LiveData的功能却完全可以使用RxJava来实现,那么谷歌为啥还要费那么大劲整这么个库出来呢?当然是因为LiveData比较简单啦~(而且RxJava不是谷歌自己的东西,谷歌:我可不想当大冤种)。于是在之后的一段时间中,对于简单场景大家开始使用LiveData了,对于复杂的场景大家还是在使用RxJava。因为LiveData驾驭不了复杂场景啊。(LiveData:我太难了)。不过好在,Flow出现了。Flow:LiveData老弟别怕,大哥给你撑腰来啦!(其实我来代替你来了,嘿嘿)至今,它们之间的爱恨情仇还在继续.....引用 扔物线(朱凯)大佬的话:协程的 Flow 和 RxJava 的功能范围非常相似——其实我觉得就是一样的——但是 Flow 是协程里必不可少的一部分,而协程是 Kotlin 里必不可少的一部分,而 Kotlin 是 Android 开发里必不可少的一部分——哦这个说的不对,重新说——而 Kotlin 又是 Android 现在主推的开发语言以及未来的趋势,这样的话,Flow 一出来,那就没 LiveData 什么事了。别说 LiveData 了,以后 RxJava 也没什么事了。LiveData会被废弃吗?LiveData会因为Flow而被废弃吗?虽然官方一直在推荐使用Flow代替LiveData,但是在GDG的社区中的答案和多位国内外的GDE口中的答案是:不会被废弃!原因有两点:在简单的场景下使用LiveData已经够了,而且LiveData比较简单,上手快,RxJava学习成本真的很高,Flow也相对没有那么简单。Flow 是协程的东西,如果你用的是Java来开发Android,那么你没有办法使用Flow。而且现在招聘平台至少有50%以上的Android岗位还在使用Java,所以LiveData不会被废弃!总结:如果不需要使用到 Kotlin 数据流的强大功能,就用 LiveData。Flow是比LiveData更好,但是在特定的场景下LiveData更合适!SharedFlow 和 StateFlow,对比LiveData我到底用哪一个?核心:LiveData 适用于 MVVM,但不适用于 MVI在MVI中,View通过触发事件与ViewModel通信,然后在ViewModel的内部处理完这些事件后,发出新的ViewState并更新UI。而且使用LiveData处理视图状态非常简单,可以同时用于MVVM和MVI,但是当我们想要像以前一样显示一个简单的Snackbar时问题就来了。如果我们使用LiveEvent类,那么整个单向状态流就会受到干扰,因为我们只是在ViewModel中触发了一个事件来与UI交互,但它应该是相反的。而使用StateFlow和SharedFlow则可以解决这个问题。StateFlow 和 LiveData 有相似之处,两者都是可观察的数据持有者类,它们的不同在于StateFlow需要将初始状态传递给构造函数,而 LiveData 不需要。当视图进入 STOPPED 状态时,LiveData.observe() 会自动取消注册消费者,而 StateFlow不会自动停止收集。如果想要实现相同的功能的话,需要在Lifecycle.repeatOnLifecycle中收集流。SharedFlow 和 StateFlow 之间的主要区别在于,StateFlow 通过构造函数获取一个默认值,并在有人开始收集时便立即发出,而 SharedFlow 不接受任何值,默认情况下什么也不发出。我们已经有了Flow,为啥还会有SharedFlow、StateFlow。Flow不够用吗?StateFlow 和 SharedFlow 是 Flow API,它们使流能够以最佳的方式发出状态更新,并向多个消费者发出消息。单单一个Flow当然是不够的啦Flow是无状态的,它没有 .value属性。它只是一个可以收集的数据流。Flow是声明性的(冷流)。它仅在收集时实现,并且对于每个新收集器都会创建一个新流。对于访问数据库和其他不必每次都重复的操作,这不是一个好的选择。Flow本来是不知道Android的生命周期,但是后来可以通过向LifecycleCoroutineScope添加扩展方法launchWhenStarted来解决,但大多数人不知道如何正确使用它。而且因为Flow有一个订阅计数属性,当Lifecycle.Event达到ON_STOP时该属性不会改变。这意味着Flow将在内存中仍然处于活动状态,就会可能导致内存泄漏!
本篇文章主要介绍CoroutineLiveData的使用,这是一个基于协程+MediatorLiveData实现的一种build构建livedata的类。普通LiveData的方式class MainViewModel: ViewModel() { //可变不对外暴漏 private val _data = MutableLiveData<String>() //对外暴漏 val data: LiveData<String> get() = _data fun login() { //经过一些列网络请求 _data.value = "shengxu" } } 然后再Activity中添加观察者等等,请注意这里有个小细节:对外暴漏不可变的Livedata使用的是get() = _data而不是val data: LiveData<String> = _data,这样做的好处是前者减少了一个属性的声明。如果我们想要实现先从本地数据库读取数据,再从远程网络获取数据源,只能这样实现:fun getData() { //从数据库读取数据 _data.value = "bendi" //经过一些列网络请求 _data.value = "shengxu" } 这样有一个不好的问题,可能由于程序的不当逻辑,发生多次getData()的操作,比如横竖屏切换,ViewModel不会销毁且保存着原来的数据,而程序错把getData()的请求放在了比如Activity的onStart()方法中,即使ViewModel有数据也会发生重复向服务器获取数据的情况。针对于这种情况,我们可以将getData()方法放到MainViewModel的init{}代码块中实现,而CoroutineLiveData提供了一种更好的方式。CoroutineLiveDatabuild模式构建val mLiveData = liveData { //发送单个内容 emit("10") //发送livedata emitSource(_data) } 实际上这个就是创建了一个CoroutineLiveData类型的LiveData,下面的emit()和emitSource()方法就是基于CoroutineLiveData实现。直接通过livedata{}扩展方法动态构建一个不可变的LiveData,调用emit()或者emitSource()方法就相当于调用之前LiveData的setValue()方法,当然后者实现上稍微复杂些。emit()这里随便拿官方的例子演示:val user = liveData<Model> { var backOffTime = 1_000 var succeeded = false while(!succeeded) { try { emit(api.fetch(id)) succeeded = true } catch(ioError : IOException) { delay(backOffTime) //每次轮询执行的时间间隔都会增加,最大60s backOffTime *= minOf(backOffTime * 2, 60_000) } } } 请注意livedata{}代码块中的内容默认是执行在主线程中的。在这里我们调用api.fetch(id)模拟轮询执行网络请求直到成功,每次执行的时间间隔都比上次大一些,相比较于在MainViewModel的init{}执行该逻辑,放到livedata的构造代码块中执行显得更加方便,既不会导致init{}代码块过于臃肿,也更加符合单一原则或者封装性(自己感觉...)。emitSource()这个方法需要传入一个LiveData类型的参数:override suspend fun emitSource(source: LiveData<T>): DisposableHandle = withContext(coroutineContext) { return@withContext target.emitSource(source) } 大家应该都用过MediatorLiveData,emitSource()底层的实现就是该类,所以该方法可以理解为监听传入的LiveData<T>数据源变化并将数据赋值到我们通过livedata{}扩展创建的LiveData中。请注意如果在调用emit()方法之前调用了emitSource(),会移除emitSource()添加的LiveData数据源监听,使之无效,具体的源码分析将放到下一篇文章中。总结本篇文章主要介绍了普通LiveData的构建方式和通过livedata{}构建:后者相比较前者更加的灵活,封装性更好,不过缺点是无法在livedata{}代码块之外进行赋值更新LiveData中的数据前者就可以在MainViewModel的各个地方更新LiveData的数据源,请大家根据具体场景进行使用。
前言:在我们日常开发中,经常要和数据打交道,所以存储数据是很重要的事。Android从最开始使用SQLite作为数据库存储数据,再到许多的开源的数据库,例如QRMLite,DBFlow,郭霖大佬开发的Litepal等等,都是为了方便SQLite的使用而出现的,因为SQLite的使用繁琐且容易出错。Google当然也意识到了SQLite的一些问题,于是在Jetpack组件中推出了Room,本质上Room也是在SQLite上提供了一层封装。因为它官方组件的身份,和良好的开发体验,现在逐渐成为了最主流的数据库ORM框架。Room官方文档:developer.android.google.cn/jetpack/and…SQL语法教程:www.runoob.com/sqlite/sqli…本文代码地址:github.com/taxze6/Jetp…为什么要使用Room?Room具有什么优势?Room在SQLite上提供了一个抽象层,以便在充分利用SQLite的强大功能的同时,能够享有更强健的数据库访问机制。Room的具体优势:有可以最大限度减少重复和容易出错的样板代码的注解简化了数据库迁移路径针对编译期SQL的语法检查API设计友好,更容易上手,理解与SQL语句的使用更加贴近,能够降低学习成本对RxJava、 LiveData 、 Kotlin协程等都支持Room具有三个主要模块Entity: Entity用来表示数据库中的一个表。需要使用@Entity(tableName = "XXX")注解,其中的参数为表名。Dao: 数据库访问对象,用于访问和管理数据(增删改查)。在使用时需要@DAO注解Database: 它作为数据库持有者,用@Database注解和Room Database扩展的类如何使用Room呢?①添加依赖最近更新时间(文章发布时的最新版本)稳定版Alpha 版2022 年 6 月 1 日2.4.22.5.0-alpha02plugins { ... id 'kotlin-kapt' } def room_version = "2.4.2" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" kapt 'androidx.room:room-compiler:$room_version' ②创建Entity实体类,用来表示数据库中的一张表(table)@Entity(tableName = "user") data class UserEntity( //主键定义需要用到@PrimaryKey(autoGenerate = true)注解,autoGenerate参数决定是否自增长 @PrimaryKey(autoGenerate = true) val id:Int = 0, //默认值为0 @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name:String?, @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age:Int? ) 其中,每个表的字段都要加上@ColumnInfo(name = "xxx", typeAffinity = ColumnInfo.xxx),name属性表示这张表中的字段名,typeAffinity表示改字段的数据类型。其他常用注解:@Ignore :Entity中的所有属性都会被持久化到数据库,除非使用@Ignore kotlin复制代码@Ignore val name: String?@ForeignKey:外键约束,不同于目前存在的大多数ORM库,Room不支持Entitiy对象间的直接引用。Google也做出了解释,具体原因请查看:developer.android.com/training/da…,不过Room允许通过外键来表示Entity之间的关系。ForeignKey我们文章后面再谈,先讲简单的使用。@Embedded :实体类中引用其他实体类,在某些情况下,对于一张表的数据,我们用多个POJO类来表示,所以在这种情况下,我们可以使用Embedded注解嵌套对象。③创建数据访问对象(Dao)处理增删改查@Dao interface UserDao { //添加用户 @Insert fun addUser(vararg userEntity: UserEntity) //删除用户 @Delete fun deleteUser(vararg userEntity: UserEntity) //更新用户 @Update fun updateUser(vararg userEntity: UserEntity) //查找用户 //返回user表中所有的数据 @Query("select * from user") fun queryUser(): List<UserEntity> } Dao负责提供访问DB的API,我们每一张表都需要一个Dao。在这里使用@Dao注解定义Dao类。@Insert, @Delete需要传一个entity()进去kotlin复制代码Class<?> entity() default Object.class;@Query则是需要传递SQL语句kotlin复制代码public @interface Query { //要运行的SQL语句 String value(); }☀注意:Room会在编译期基于Dao自动生成具体的实现类,UserDao_Impl(实现增删改查的方法)。 🔥Dao所有的方法调研都在当前线程进行,需要避免在UI线程中直接访问!④创建Room database@Database(entities = [UserEntity::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao } 通过Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()获取Database实例val db = Room.databaseBuilder( applicationContext, UserDatabase::class.java, "userDb" ).build() ☀注意:创建Database的成本较高,所以我们最好使用单例的Database,避免反复创建实例所带来的开销。单例模式创建Database:@Database(entities = [UserEntity::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun getUserDao(): UserDao companion object { @Volatile private var INSTANCE: UserDatabase? = null @JvmStatic fun getInstance(context: Context): UserDatabase { val tmpInstance = INSTANCE if (tmpInstance != null) { return tmpInstance } //锁 synchronized(this) { val instance = Room.databaseBuilder(context, UserDatabase::class.java, "userDb").build() INSTANCE = instance return instance } } } } ⑤在Activity中使用,进行一些可视化操作activity_main:<LinearLayout ... tools:context=".MainActivity" android:orientation="vertical"> <Button android:id="@+id/btn_add" ... android:text="增加一条数据"/> <Button android:id="@+id/btn_delete" ... android:text="删除一条数据"/> <Button android:id="@+id/btn_update" ... android:text="更新一条数据"/> <Button android:id="@+id/btn_query_all" ... android:text="查新所有数据"/> </LinearLayout> MainActivity:private const val TAG = "My_MainActivity" class MainActivity : AppCompatActivity() { private val userDao by lazy { UserDatabase.getInstance(this).getUserDao() } private lateinit var btnAdd: Button private lateinit var btnDelete: Button private lateinit var btnUpdate: Button private lateinit var btnQueryAll: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) init() //添加数据 btnAdd.setOnClickListener { //数据库的增删改查必须在子线程,当然也可以在协程中操作 Thread { val entity = UserEntity(name = "Taxze", age = 18) userDao.addUser(entity) }.start() } //查询数据 btnQueryAll.setOnClickListener { Thread { val userList = userDao.queryUser() userList.forEach { Log.d(TAG, "查询到的数据为:$it") } }.start() } //修改数据 btnUpdate.setOnClickListener { Thread { userDao.updateUser(UserEntity(2, "Taxzeeeeee", 18)) }.start() } //删除数据 btnDelete.setOnClickListener { Thread { userDao.deleteUser(UserEntity(2, null, null)) }.start() } } //初始化 private fun init() { btnAdd = findViewById(R.id.btn_add) btnDelete = findViewById(R.id.btn_delete) btnUpdate = findViewById(R.id.btn_update) btnQueryAll = findViewById(R.id.btn_query_all) } } 结果:到这里我们已经讲完了Room的最基本的使用,如果只是一些非常简单的业务,你看到这里已经可以去写代码了,但是还有一些进阶的操作需讲解一下,继续往下看吧!数据库的升级Room在2021 年 4 月 21 日发布的版本 2.4.0-alpha01中开始支持自动迁移,不过很多朋友反应还是有很多问题,建议手动迁移,当然如果你使用的是更低的版本只能手动迁移啦。具体信息请参考:developer.android.google.cn/training/da…具体如何升级数据库呢?下面我们一步一步来实现吧!①修改数据库版本在UserDatabase文件中修改version,将其变为2(原来是1)在此时,我们需要想一想,我们要对数据库做什么升级操作呢?我们这里为了演示就给数据库增加一张成绩表:@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2) 添加表:@Entity(tableName = "score") data class ScoreEntity( @PrimaryKey(autoGenerate = true) var id: Int = 0, @ColumnInfo(name = "userScore") var userScore: Int ) ②创建对应的Dao,ScoreDao@Dao interface ScoreDao { @Insert fun insertUserScore(vararg scoreEntity: ScoreEntity) @Query("select * from score") fun queryUserScoreData():List<ScoreEntity> } ③在Database中添加迁移@Database(entities = [UserEntity::class,ScoreEntity::class], version = 2) abstract class UserDatabase : RoomDatabase() { abstract fun getUserDao(): UserDao //添加一个Dao abstract fun getScoreDao():ScoreDao companion object { //变量名最好为xxx版本迁移到xxx版本 private val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ create table userScore( id integer primary key autoincrement not null, userScore integer not null) """.trimIndent() ) } } @Volatile private var INSTANCE: UserDatabase? = null @JvmStatic fun getInstance(context: Context): UserDatabase { ... synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, UserDatabase::class.java, "userDb" ) .addMigrations(MIGRATION_1_2) .build() INSTANCE = instance return instance } } } } ④使用更新后的数据在xml布局中添加两个Button:<Button android:id="@+id/btn_add_user_score" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="增加user的score数据"/> <Button android:id="@+id/btn_query_user_score" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="查询user的score数据"/> 在MainActivity中加入:private val userScoreDao by lazy { UserDatabase.getInstance(this).getScoreDao() } ... private lateinit var btnAddUserScore: Button private lateinit var btnQueryUserScore: Button ... btnAddUserScore = findViewById(R.id.btn_add_user_score) btnQueryUserScore = findViewById(R.id.btn_query_user_score) ... btnAddUserScore.setOnClickListener { Thread{ userScoreDao.insertUserScore(ScoreEntity(userScore = 100)) }.start() } btnQueryUserScore.setOnClickListener { Thread{ userScoreDao.queryUserScoreData().forEach{ Log.d(TAG,"userScore表的数据为:$it") } }.start() } 这样对数据库的一次手动迁移就完成啦!如果你想继续升级,就重复之前的步骤,然后将2→3private val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ .... 再一次新的操作 """.trimIndent() ) } } ... .addMigrations(MIGRATION_1_2,MIGRATION_2_3) 使用Room更多的骚操作!①想知道更多的Room数据库迁移的操作吗,那你可以看看这篇文章:www.modb.pro/db/139101②更优雅的修改数据在上面的修改数据操作中,我们是需要填入每个字段的值的,但是,大部分情况,我们是不会全部知道的,比如我们不知道User的age,那么我们的age字段就填个Null吗?val entity = UserEntity(name = "Taxze", age = null) 这显然是不合适的!当我们只想修改用户名的时,却又不知道age的值的时候,我们需要怎么修改呢?⑴创建UpdateNameBeanclass UpdateNameBean(var id:Int,var name:String) ⑵在Dao中加入新的方法@Update(entity = UserEntity::class) fun updataUser2(vararg updataNameBean:UpdateNameBean) ⑶然后在使用时只需要传入id,和name即可userDao.updateUser2(updataNameBean(2, "Taxzeeeeee")) 当然你也可以给用户类创建多个构造方法,并给这些构造方法添加@lgnore③详解@Insert 插入@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUsers(vararg userEntity: UserEntity) } 其中onConflict用于设置当事务中遇到冲突时的策略。有如下一些参数可以选择:OnConflictStrategy.REPLACE : 替换旧值,继续当前事务OnConflictStrategy.NONE : 忽略冲突,继续当前事务OnConflictStrategy.ABORT : 回滚④@Query 指定参数查询每次都查表的全部信息这也不是事啊,我们要用到where条件来指定参数查询。@Dao interface UserDao { @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<UserEntity> } 大家可以自己学习一下SQL语法~⑤多表查询很多业务情况下,我们是需要同时在多张表中进行查询的。@Dao interface UserDao { @Query( "SELECT * FROM user " + "INNER JOIN score ON score.id = user.id " + "WHERE user.name LIKE :userName" ) fun findUsersScoreId(userName: String): List<UserEntity> } ⑥@Embedded内嵌对象我们可以使用@Embedded注解,将一个Entity作为属性内嵌到另外一个Entity,然后我们就可以像访问Column一样去访问内嵌的Entity啦。data class Score( val id:Int?, val score:String?, ) @Entity(tableName = "user") data class UserEntity( @PrimaryKey(autoGenerate = true) val id:Int = 0, ..... @Embedded val score: Score? ) ⑦使用@Relation 注解和 foreignkeys注解来描述Entity之间更复杂的关系可以实现一对多,多对多的关系⑧预填充数据库可以查看官方文档:developer.android.google.cn/training/da…⑨类型转换器 TypeConverter....Room配合LiveData和ViewModel下面我们通过一个Room+LiveData+ViewModel的例子来完成这篇文章的学习吧话不多说,先上效果图:①创建UserEntity@Entity(tableName = "user") data class UserEntity( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT) val name: String?, @ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER) val age: Int?, ) ②创建对应的Dao@Dao interface UserDao { //添加用户 @Insert fun addUser(vararg userEntity: UserEntity) //查找用户 //返回user表中所有的数据,使用LiveData @Query("select * from user") fun getUserData(): LiveData<List<UserEntity>> } ③创建对应的Database代码在最开始的例子中已经给出了。④创建ViewModelclass UserViewModel(userDao: UserDao):ViewModel(){ var userLivedata = userDao.getUserData() } ⑤创建UserViewModelFactory我们在UserViewModel类中传递了UserDao参数,所以我们需要有这么个类实现ViewModelProvider.Factory接口,以便于将UserDao在实例化时传入。class UserViewModelFactory(private val userDao: UserDao) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return UserViewModel(userDao) as T } } ⑥编辑xmlactivity_main:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <EditText android:id="@+id/user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="请输入UserName" /> <EditText android:id="@+id/user_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="请输入UserAge" /> <Button android:id="@+id/btn_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="添加一条user数据" /> <ListView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </LinearLayout> 创建一个simple_list_item.xml,用于展示每一条用户数据<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/userText" android:layout_width="wrap_content" android:layout_height="wrap_content" /> ⑦在MainActivity中调用class MainActivity : AppCompatActivity() { private var userList: MutableList<UserEntity> = arrayListOf() private lateinit var arrayAdapter: ArrayAdapter<UserEntity> private val userDao by lazy { UserDatabase.getInstance(this).getUserDao() } lateinit var viewModel: UserViewModel private lateinit var listView: ListView private lateinit var editUserName: EditText private lateinit var editUserAge: EditText private lateinit var addButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) init() arrayAdapter = ArrayAdapter(this, R.layout.simple_list_item, userList) listView.adapter = arrayAdapter //实例化UserViewModel,并监听LiveData的变化。 viewModel = ViewModelProvider(this, UserViewModelFactory(userDao)).get(UserViewModel::class.java) viewModel.userLivedata.observe(this, Observer { userList.clear() userList.addAll(it) arrayAdapter.notifyDataSetChanged() }) addButton.setOnClickListener { addClick() } } //初始化控件 private fun init() { editUserName = findViewById(R.id.user_name) editUserAge = findViewById(R.id.user_age) addButton = findViewById(R.id.btn_add) listView = findViewById(R.id.recycler_view) } fun addClick() { if (editUserName.text.toString() == "" || editUserAge.text.toString() == "") { Toast.makeText(this, "姓名或年龄不能为空", Toast.LENGTH_SHORT).show() return } val user = UserEntity( name = editUserName.text.toString(), age = editUserAge.text.toString().toInt() ) thread { userDao.addUser(user) } } } 这样一个简单的Room配合LiveData和ViewModel实现页面自动更新的Demo就完成啦具体代码可以查看Git仓库
主要是统一下在AppCompatActivity获取ViewModel+ViewBinding的入口,先看下最终的封装效果:下面就让我们一步步的实现下这种效果:定义中间类BaseMvvmActivityabstract class BaseMvvmActivity() : AppCompatActivity() {} 这个类继承了AppCompatActivity,将作为之后界面继承的的基类,在这个类中统一获取ViewModel+ViewBinding的入口。封装ViewBinding的获取入口ViewBinding的具体创建函数是ViewBinding.inflate(),首先我们要明确一个点,函数和函数类型是可以相互转化的:private val vb: (LayoutInflater) -> ActivityMainBinding = ActivityMainBinding::inflate 我们将这个创建函数转换成一个函数类型并作为构造参数传入BaseMvvmActivity,其次还要在BaseMvvmActivity中传入<T: ViewBinding>,这样才能够获取ViewBinding的具体实现类型:abstract class BaseMvvmActivity<T: ViewBinding>( private val vb: (LayoutInflater) -> ActivityMainBinding ): AppCompatActivity() { protected lateinit var mBinding: VB } 然后重写onCreate()方法,在该方法中完成具体ViewBinding的创建以及当前界面根布局的设置:override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mBinding = vb(layoutInflater) setContentView(mBinding.root) } 这样我们就可以在界面内中这样使用:class ZygoteActivity : BaseMvvmActivity<ActivityMainBinding>( ActivityMainBinding::inflate ) { fun test2() { //直接从父类中获取ViewBinding的实现对象 mBinding.iconIv.visibility = View.VISIBLE } } 请注意,Fragment可不能进行如此封装,因为如果把ViewBinding的创建函数作为构造函数的一个参数传入,但Fragment创建销毁被重建后,该参数就直接丢失了,这就会造成后续在onCreateView()方法中调用该参数创建ViewBinding时直接空指针异常了。封装ViewModel的获取入口对于ViewModel的创建,我们只需要拿到具体要创建ViewModel的class对象即可,那我们就可以将这个class对象直接通过构造参数传入到BaseMvvmActivity中即可:abstract class BaseMvvmActivity<VB : ViewBinding, VM : ViewModel>( private val vb: (LayoutInflater) -> VB, private val vmClass: Class<VM> ) : AppCompatActivity() { } 在BaseMvvmActivity中通过懒加载的形式完成具体ViewModel的创建:protected val mViewModel: VM by lazy { ViewModelProvider(viewModelStore, defaultViewModelProviderFactory).get(vmClass) } 然后就可以这样使用:class ZygoteActivity : BaseMvvmActivity<ActivityMainBinding, MainViewModel>( ActivityMainBinding::inflate, MainViewModel::class.java ) { fun test2() { mBinding.iconIv.visibility = View.VISIBLE //使用ViewModel mViewModel.data1.observe(this) { } } } 请注意,如果要创建的具体ViewModel的构造方法带有参数,请重写AppCompatActivity的getDefaultViewModelProviderFactory方法,实现ViewModelProvider.Factory接口自定义一个ViewModel的创建工厂并返回即可。总结经过上面的封装,我们就可以实现文章一开始使用的那种效果,使用起来也是很简单,除了传入的构造参数写起来稍微有一丢丢麻烦,看起来不美观哈!!
前言🏀DataBinding只是一种工具,用来解决View和数据之间的绑定。Data Binding,顾名思义:数据绑定,它可以将布局页面中的组件和应用中的数据进行绑定,支持单向绑定和双向绑定,单向绑定就是如果数据有变化就会驱动页面进行变化,双向绑定就是除了单向绑定之外还支持页面的变化驱动数据的变化,如果页面中有一个输入框,那么我们就可以进行双向绑定,数据变化,它的显示内容就变了,我们手动输入内容也可以改变绑定它的数据。🌟官方文档:developer.android.google.cn/jetpack/and…🌟官方Demo地址:github.com/googlecodel…如何使用DataBinding呢?1.启用DataBinding引用官方文档: Databinding与 Android Gradle 插件捆绑在一起。您无需声明对此库的依赖项,但必须启用它。 ☀注意:即使模块不直接使用数据绑定,也必须为依赖于使用数据绑定的库的所有模块启用数据绑定。//在gradle的android下加入,然后点击sync android { ... //android studio 4.0以下 dataBinding{ } //android studio4.1以后 buildFeatures { dataBinding true } } 2.生成DataBinding布局在我们的布局文件中,选择根目录的View,按下Alt+回车键,点击Convert to data binding layout,就可以转换为DataBinding布局啦。然后我们的布局就会变成这样:<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> 我们可以发现,最外面变成了layout元素,里面有data元素。我们将在data元素中声明这个布局中使用到的变量,以及变量的类型。举个例子:<data> <import type="com.example...."/> <variable name="color" type="java.lang.String" /> </data> data: 在标签内进行变量声明和导入等variable: 进行变量声明import: 导入需要的类3.声明一个User实体类class User() { var name = "Taxze" var age = 18 fun testOnclick() { Log.d("onclick", "test") } } 4.在xml中使用然后在data中声明变量,以及类名<data> <!-- <variable--> <!-- name="user"--> <!-- type="com.taxze.jetpack.databinding.User" />--> <import type="com.taxze.jetpack.databinding.User" /> <variable name="user" type="User" /> </data> 然后在布局中使用@{}语法//伪代码,请勿直接CV <TextView ... android:text="@{user.name}" /> 5.在Activity或Fragment中使用DataBinding在Activity中通过DataBindingUtil设置布局文件,同时省略Activity的setContentView方法class MainActivity : AppCompatActivity() { private lateinit var mainBinding: ActivityMainBinding private lateinit var mainUser: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) mainUser = User() mainBinding.user = mainUser } } 在Fragment中使用:class BlankFragment : Fragment() { private lateinit var mainFragmentBinding:FragmentBlankBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { mainFragmentBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_blank,container,false) return mainFragmentBinding.root } } 系统会为每个布局文件都生成一个绑定类。一般默认情况下,类的名称是布局文件名称转化为Pascal大小写形式,然后在末尾添加Binding后缀,例如:名称为activity_main的布局文件,对应的类名就是ActivityMainBinding运行之后的效果:注意:只有当布局文件转换为layout样式之后,databinding才会根据布局文件的名字自动生成一个对应的binding类,你也可以在build/generated/data_binding_base_source_out目录下查看生成的类最最最基础的使用就是这样,接下来我们来讲讲如何更好的使用DataBinding如何在xml布局中更好的使用DataBinding1.使用集合中的元素加入我们传入了一个集合books,我们可以通过以下方式使用: 获取集合的值 android:text="@{books.get(0).name}" android:text="@{books.[0].name}" 添加默认值(⚡默认值无需加引号,且只在预览视图显示) android:text="@{books.pages,default=330}" 通过??或?:来实现 android:text="@{books.pages != null ? book.pages : book.defaultPages}" xml复制代码android:text="@{books.pages ?? book.defaultPages}"2.使用map中的数据map类型的结构也可以通过get和[]两种方式获取 //需要注意单双引号 android:text="@{books.get('name')}" android:text="@{books['name']}"3.转换数据类型因为DataBinding不会自动做类型转换,所有我们需要手动转换,例如在text标签内使用String.valueOf()转换为String类型,在rating标签内我们可以使用Float.valueOf()进行转换 android:text="@{String.valueOf(book.pages)}" android:rating="@{Float.valueOf(books.rating),default=2.0}"4.导入包名冲突处理如果我们导入的包名有冲突,我们可以通过alias为它设置一个别名 //伪代码,请勿直接CV <data> <import type="com.xxx.a.Book" alias="aBook"/> <import type="com.xxx.B.Book" alias="bBook"/> ... </data>5.隐式引用属性在一个view上引用其他view的属性//伪代码,请勿直接CV <import type="android.view.View"/> ... <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <CheckBox android:id="@+id/checkOne" .../> <ImageView android:visibility="@{checkOne.checked ? View.VISIBLE : View.GONE}" .../> </LinearLayout>**include标签和ViewStub**标签 include和merge标签的作用是实现布局文件的重用。就是说,为了高效复用及整合布局,使布局轻便化,我们可以使用include和merge标签将一个布局嵌入到另一个布局中,或者说将多个布局中的相同元素抽取出来,独立管理,再复用到各个布局中,便于统一的调整。 比如,一个应用中的多个页面都要用到统一样式的标题栏或底部导航栏,这时就可以将标题栏或底部导航栏的布局抽取出来,再以include标签形式嵌入到需要的布局中,而不是多次copy代码,这样在修改时只需修改一处即可。而我们同样可以通过DataBinding来进行数据绑定。 例如://layout_title.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="com.xxx.User" /> <variable name="userInfo" type="User" /> </data> <android.support.constraint.ConstraintLayout ... > <TextView ... android:text="@{userInfo.name}" /> </android.support.constraint.ConstraintLayout> </layout> 使用该布局,并传值<include layout="@layout/layout_title" bind:test="@{userInfo}"/> ViewStub也是类似的用法,这里就不说了。 DataBinding不支持merge标签6.绑定点击事件//伪代码,请勿直接CV onclick="@{()->user.testOnclick}" onclick="@{(v)->user.testOnclick(v)}" onclick="@{()->user.testOnclick(context)}" onclick="@{BindHelp::staticClick}" onclick="@{callback}" //例如: <Button android:layout_width="match_parent" android:layout_height="match_parent" android:onClick="@{()->user.testOnclick}" />💡文章到这里讲的都是DataBinding如何设置数据,以及通过DataBinding在xml中的一些基础使用。如果只是使用DataBinding这个功能,那就有点大材小用了。它还有一个很强大的功能我们还没有讲,那就是数据更新时自动刷新UI。实现数据变化时自动更新UI一个普通的实体类或者ViewModel被更新后,并不会让UI自动更新。而我们希望,当数据变更后UI要自动更新,那么要实现数据变化时自动更新UI,有三种方法可以使用,分别是BaseObservable,ObservableField,ObservableCollection💡单向数据绑定:BaseObservable BaseObservable提供了两个刷新UI的方法,分别是 notifyPropertyChanged() 和 notifyChange() 。 第一步:修改实体类 将我们的实体类继承与BaseObservable。需要响应变化的字段,就在对应变量的get函数上加 @Bindable 。然后set中notifyChange是kotlin的写法,免去了java的getter setter的方式。成员属性需要响应变化的,就在其set函数中,notify一下属性变化,那么set的时候,databinding就会感知到。 import androidx.databinding.BaseObservable import androidx.databinding.Bindable import androidx.databinding.library.baseAdapters.BR class User() : BaseObservable() { constructor(name: String, age: Int) : this() { this.name = name this.age = age } //这是单独在set上@bindable,name可以为声明private var name: String = "" set(value) { field = value notifyPropertyChanged(BR.name) } @Bindable get() = field //这是在整个变量上声明@bindable,所以必须是public的 @Bindable var age:Int = 18 set(value) { field = value notifyPropertyChanged(BR.age) } get() = field } 第二步:在Activity中使用 kotlin复制代码class MainActivity : AppCompatActivity() { private val TAG = "MainActivity" private lateinit var mainBinding: ActivityMainBinding private lateinit var mainUser: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) mainUser = User("Taxze", 18) mainUser.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { when { BR.user == propertyId -> { Log.d(TAG, "BR.user") } BR.age == propertyId -> { Log.d(TAG, "BR.age") } } } }) mainBinding.user = mainUser mainBinding.onClickPresenter = OnClickPresenter() } inner class OnClickPresenter { fun changeName() { mainUser.name = "Taxze2222222" } } } 需要注意的点 官方网站只是提示了开启DataBinding只需要在build.gradle中加入下面这行代码 buildFeatures { dataBinding true } 但是,如果你想更好的使用DataBinding这是不够的,你还需要添加这些配置:compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } 🔥重点:在使用DataBinding得时候,BR对象,发现调用不了,生成也会报错,运行,需要在咱们build.gradle中进行一下配置: apply plugin: 'kotlin-kapt' kapt { generateStubs = true } 然后重新运行一遍代码,你就会发现,BR文件自动生成啦!ObservableField 讲解了BaseObservable后,现在来将建最简单也是最常用的。只需要将实体类变化成这样即可: //注意observable的属性需要public权限,否则dataBinding则无法通过反射处理数据响应 class User() : BaseObservable() { var name: ObservableField<String> = ObservableField("Taxze") var age:ObservableInt = ObservableInt(18) }ObservableCollection dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap 实体类修改: //伪代码,请勿直接cv class User(){ var userMap = ObservableArrayMap<String,String>() } //使用时: mainUser.userMap["name"] = "Taxze" mainUser.userMap["age"] = "18" 使用ObservableCollection后,xml与上面的略有不同,主要是数据的获取,需要指定Key值 //伪代码,请勿直接cv ... <import type="android.databinding.ObservableMap" /> <variable name="userMap" type="ObservableMap<String, String>" /> //使用时: <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="Name" android:text="@{userMap[`userName`]}" />💡双向数据绑定:只需要在之前的单向绑定的基础上,将布局文件@{}变为@={},用于针对属性的数据改变的同时监听用户的更新DataBinding在RecyclerView中的使用在RecyclerView中使用DataBinding稍有变化,我们在ViewHolder中进行binding对象的产生,以及数据对象的绑定。我们通过一个非常简单的例子来讲解如何在RecyclerView中使用DataBinding。效果图:第一步:创建实体类 就是我们之前的,使用了BaseObservable的那个实体类,这里就不放代码了第二步:创建activity_main用于存放recyclerview <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activty_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" /> </RelativeLayout>第三步:创建text_item.xml用于展示recyclerview中的每一行数据 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="com.taxze.jetpack.databinding.User" /> <variable name="user" type="User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:background="#ffffff" android:orientation="horizontal" android:paddingStart="10dp"> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="@{`这个人的姓名是` + user.name}" /> <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:text="@{String.valueOf(user.age)}" /> </LinearLayout> </LinearLayout> </layout>第四步:创建Adapter 有了之前的基础之后,大家看下面这些代码应该很容易了,就不做过多讲解啦 import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView import com.taxze.jetpack.databinding.databinding.TextItemBinding class FirstAdapter(users: MutableList<User>, context: Context) : RecyclerView.Adapter<FirstAdapter.MyHolder>() { //在构造函数中声明binding变量,这样holder才能引用到,如果不加val/var,就引用不到,就需要在class的{}内写get函数 class MyHolder(val binding: TextItemBinding) : RecyclerView.ViewHolder(binding.root) private var users: MutableList<User> = arrayListOf() private var context: Context init { this.users = users this.context = context } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder { val inflater = LayoutInflater.from(context) val binding: TextItemBinding = DataBindingUtil.inflate(inflater, R.layout.text_item, parent, false) return MyHolder(binding) } override fun onBindViewHolder(holder: MyHolder, position: Int) { //java 写法可以setVariable holder.binding.user = users[position] holder.binding.executePendingBindings() } //kotlin中,return的方式,可以简写 override fun getItemCount() = users.size }第五步:在MainActivity中使用 import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initView() } //给RecyclerView设置数据 private fun initView() { val recyclerView = findViewById<View>(R.id.recyclerView) as RecyclerView recyclerView.layoutManager = LinearLayoutManager(this) val users: MutableList<User> = ArrayList() for (i in 0..100) { val user = User() user.name = "Taxze" user.age = i users.add(user) } val adapter = FirstAdapter(users, this) recyclerView.adapter = adapter } }这样就完成了在RecyclerView中使用DataBinding啦。高级用法第一个:用于appCompatImageView的自定义属性//伪代码,请勿直接cv /** * 用于appCompatImageView的自定义属性,bind:imgSrc,命名空间bind:可以省略,也就是写作 imgSrc亦可。可以用于加载url的图片 * 函数名也是随意,主要是value的声明,就是新加的属性名了,可以多个属性同用,并配置是否必须一起作用 * 函数名随意,方法签名才重要,匹配对象控件,以及属性参数。 * 这里还可以添加old 参数,获取修改新参数 之前对应的值。 * todo 加载网络图片,需要网络权限!!! */ @JvmStatic @BindingAdapter(value = ["bind:imgSrc"], requireAll = false) fun urlImageSrc(view: AppCompatImageView, /*old: String?, */url: String) { Glide.with(view) .load(url) .placeholder(R.drawable.img_banner) .centerInside() .into(view) } 第二个:配合swipeRefreshLayout的刷新状态的感知第一步:单向的,数据变化,刷新UI//伪代码,请勿直接cv @JvmStatic @BindingAdapter("sfl_refreshing", requireAll = false) fun setSwipeRefreshing(view: SwipeRefreshLayout, oldValue: Boolean, newValue: Boolean) { //判断是否是新的值,避免陷入死循环 if (oldValue != newValue) view.isRefreshing = newValue }第二步:ui的状态,反向绑定给数据变化 //伪代码,请勿直接cv @JvmStatic @BindingAdapter("sfl_refreshingAttrChanged", requireAll = false) fun setRefreshCallback(view: SwipeRefreshLayout, listener: InverseBindingListener?) { listener ?: return view.setOnRefreshListener { //由ui层的刷新状态变化,反向通知数据层的变化 listener.onChange() } }第三步: 反向绑定的实现 //伪代码,请勿直接cv /** * 反向绑定的实现,将UI的变化,回调给bindingListener,listener就会onChange,通知数据变化 * 注意这里的attribute和event,是跟上面两步配合一致才有效 */ @JvmStatic @InverseBindingAdapter(attribute = "sfl_refreshing", event = "sfl_refreshingAttrChanged") fun isSwipeRefreshing(view: SwipeRefreshLayout): Boolean { return view.isRefreshing }DataBinding配合ViewModel&LiveData一起使用我将通过一个简单的例子带大家学习他们如何一起使用,话不多说,先上效果图:第一步:创建UserModel//将其继承于AndroidViewModel(AndroidViewModel也是继承于ViewModel的,但是ViewModel本身没有办法获得 Context,AndroidViewModel提供Application用作Context,并专门提供 Application 单例) //UserName 使用MutableLiveData class UserModel(application: Application) : AndroidViewModel(application) { var UserName = MutableLiveData("") }第二步:创建activity_main和对应的MainActivity<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="loginModel" type="com.taxze.jetpack.databinding.model.UserModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/linearLayout" style="@style/InputBoxStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" app:layout_constraintBottom_toTopOf="@+id/guideline2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" tools:ignore="MissingConstraints"> <EditText android:id="@+id/editText" style="@style/EditTextStyle" android:layout_width="match_parent" android:layout_height="50dp" android:hint="请输入账号" android:text="@={loginModel.UserName}" tools:ignore="MissingConstraints" /> </LinearLayout> <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text='@{"您输入的账号名是:"+loginModel.UserName,default=123123123}' android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button" tools:ignore="MissingConstraints" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.4" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:background="@drawable/button_drawable" android:text="登录" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/linearLayout" tools:ignore="MissingConstraints" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>第三步:在MainActivity中绑定页面和绑定声明周期 class MainActivity : AppCompatActivity() { lateinit var viewDataBinding: ActivityMainBinding lateinit var model: UserModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //绑定页面 viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) //绑定生命周期 viewDataBinding.lifecycleOwner = this model = ViewModelProvider.AndroidViewModelFactory.getInstance(this.application) .create(UserModel::class.java) viewDataBinding.loginModel = model ... } }第四步:传值 viewDataBinding.button.setOnClickListener { val intent = Intent(MainActivity@ this, SecondActivity::class.java) intent.putExtra("user", "${model.UserName.value}") startActivity( intent ) }第五步:在另外一个activity中调用 class SecondActivity : AppCompatActivity() { lateinit var viewDataBinding: ActivitySecondBinding lateinit var userName: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //绑定页面 viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_second) //绑定生命周期 viewDataBinding.lifecycleOwner = this userName = intent.getStringExtra("user").toString() viewDataBinding.tvName.text = "登录的账号是:$userName" } }帮你踩坑🍖:TextView的text属性,需要注意data不能为Number类型反射属性、函数必须是publicobservableField数据的时候,在某些场合需要初始化,否则会运行报错!使用LiveData作为dataBinding的时候,需要在ui中设置binding.lifecycleOwner名为ALuoBo的读者补充:xml中可以使用中文字符,格式如下: android:text='@{state.isChinese? "确定" :"OK"}'尾述这篇文章已经很详细的讲了DataBinding的大部分用法,不过在看完文章后,你仍需多多实践,相信你很快就可以掌握DataBinding啦😺 有问题欢迎在评论区留言讨论~
前言什么是Navigation?官方文档的话有点不容易让人理解。所以,这里用我自己的话来总结一下,我们在处理Fragment是需要通过写Fragment的事务去操作Fragment的,而Navigation的出现是为了解决我们之前开发的一些痛点。Navigation主要用于实现Fragment代替Activity的页面导航功能,让Fragment能够轻松的实现跳转与传递参数,我们可以通过使用Navigation,让Fragment代替android项目中绝大多数的Activity。但需要注意的是在使用Navigation切换页面生命周期的变化情况,避免开发过程中踩坑。官方文档:developer.android.google.cn/jetpack/and…navigation项目地址:github.com/googlecodel…本文Demo地址:github.com/taxze6/Jetp…使用Navigation具有什么优势?处理Fragment事务默认情况下,能正确处理往返操作为动画和转换提供标准化资源实现和处理深层链接包括导航界面模式,例如抽屉式导航栏和底部导航,我们只需要完成少量的代码编写Safe Args - 可在目标之间导航和传递数据时提供类型安全的Gradle插件ViewModel支持 - 您可以将ViewModel的范围限定为导航图,以在图标的目标之间共享与界面相关的数据如何使用Navigation呢?Navigation目前仅AndroidStudio 3.2以上版本支持,如果您的版本不足3.2,请点此下载最新版AndroidStudio(2202年了应该没有人还在用3.2以下的版本吧!)在开始学习Navigation组件之前,我们需要先对Navigation主要组成部分有个简单的了解,Navigation由三部分组成:Navigation graph:一个包含所有导航相关信息的 XML 资源NavHostFragment:一种特殊的Fragment,用于承载导航内容的容器NavController:管理应用导航的对象,实现Fragment之间的跳转等操作下面我们正式开始学习Navigation啦第一步:添加依赖//project的Navigation依赖设置 dependencies { //文章发布时的最新稳定版本: def nav_version = "2.4.2" // 使用java作为开发语言添加下面两行: implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" // Kotlin: implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" } //Compose版本: implementation "androidx.navigation:navigation-compose:$nav_version" 第二步:创建导航图①右键点击res目录,然后依次选择New→ Android Resource Directory。此时系统会显示 New Resource Directory对话框。Directory name输入你的文件夹名(一般为navigation),Resource type选择navigation②右键navigation文件夹,然后new →Navigation Resource File在File name中输入名称(常用nav_graph_main或nav_graph)第三步:创建Fragment为了让跳转更加的丰富,我们这里创建三个Fragment✔我们可以自己手动创建:FirstFragment:<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".FirstFragment"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="hello world" /> </FrameLayout> class FirstFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_first, container, false) } } 另外创建两个和FirstFragement一样的:SecondFragment,ThirdFragment我们也可以通过 Navigation graph 创建我们在新建好的nav_graph_main.xml下,在右上角切换到Design模式,然后在Navigation Editor中,点击Create new destination,选择所需要的Fragment后,点击Finish,你就会发现Fragment已经出现在我们可以拖动的面板中了。第四步:将Fragment拖入面板并进行跳转配置只需要在Navigation Editor中双击想要的Fragment就会被加入到面板中啦。点击其中一个Fragment,你会发现,在他的右边会有一个小圆点,拖曳小圆点指向想要跳转的那个Fragment,我们这里设置FirstFragment → SecondFragment → ThirdFragment → FirstFragment我们将nav_graph_main.xml切换到Code下,我们来解读一下xml<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph_main" app:startDestination="@id/firstFragment"> <fragment android:id="@+id/firstFragment" android:name="com.taxze.jetpack.navigation.FirstFragment" android:label="fragment_first" tools:layout="@layout/fragment_first" > <action android:id="@+id/action_firstFragment_to_secondFragment2" app:destination="@id/secondFragment" /> </fragment> <fragment android:id="@+id/secondFragment" android:name="com.taxze.jetpack.navigation.SecondFragment" android:label="fragment_second" tools:layout="@layout/fragment_second" > <action android:id="@+id/action_secondFragment_to_thirdFragment2" app:destination="@id/thirdFragment" /> </fragment> <fragment android:id="@+id/thirdFragment" android:name="com.taxze.jetpack.navigation.ThirdFragment" android:label="fragment_third" tools:layout="@layout/fragment_third" > <action android:id="@+id/action_thirdFragment_to_firstFragment" app:destination="@id/firstFragment" /> </fragment> </navigation> navigation是根标签,通过startDestination配置默认启动的第一个页面,这里配置的是firstFragment,我们可以在代码中手动改mainFragment(启动时的第一个Fragment),也可以在可视化面板中点击Fragment,再点击Assign Start Destination,同样可以修改mainFragmentfragment标签就代表这是一个Fragmentaction标签定义了页面跳转的行为,就是上图中的每条线,destination定义跳转的目标页,还可以加入跳转时的动画注意:在fragment标签下的android:name属性,其中的包名的是否正确声明第五步:处理MainActivity①编辑MainActivity的布局文件,在布局文件中添加NavHostFragment。我们需要告诉Navigation和Activity,我们的Fragment展示在哪里,所以NavHostFragment其实就是导航界面的容器<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <fragment android:id="@+id/nav_host_fragment" android:layout_width="0dp" android:layout_height="0dp" android:name="androidx.navigation.fragment.NavHostFragment" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph_main" /> </androidx.constraintlayout.widget.ConstraintLayout> fragment标签下的android:name是用于指定NavHostFragmentapp:navGraph是用于指定导航视图的app:defaultNavHost=true是在每一次Fragment切换时,将点击记录在堆栈中保存起来,在需要退出时,按下返回键后,会从堆栈拿到上一次的Fragment进行显示。但是在某些情况下,这样的操作不是很友好,不过好在我们只需要将app:defaultNavHost=true改为app:defaultNavHost=false或者删除这行即可。在其为false的情况下,无论怎么切换Fragment,再点击返回键就都直接退出app。当然我们也可以对其堆栈进行监听,从而来实现,点击一次返回键回到主页,再点击一次返回键退出app。②修改MainActivity我们重写了onSupportNavigateUp,表示我们将Activity的back点击事件委托出去class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onSupportNavigateUp(): Boolean { return findNavController(R.id.nav_host_fragment).navigateUp() } } 第六步:处理Fragment的对应跳转事件①Fragment布局:给一个按钮用于跳转,一个TextView用于标识,三个Fragment布局相同<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".FirstFragment"> <Button android:id="@+id/firstButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="点击跳转到第二个fragment" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="@string/hello_first_fragment" /> </FrameLayout> //secondFragment中加入: <Button android:id="@+id/firstButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="点击跳转到第三个fragment" /> //thirdFragment中加入: <Button android:id="@+id/firstButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="返回第一个fragment" /> ②配置Fragment对应的跳转事件class FirstFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_first, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.firstButton).apply { setOnClickListener { it.findNavController().navigate(R.id.action_firstFragment_to_secondFragment2) } } } } //secondFragment中加入: override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.firstButton).apply { setOnClickListener { it.findNavController().navigate(R.id.action_secondFragment_to_thirdFragment2) } } } //thirdFragment中加入: override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.firstButton).apply { setOnClickListener { it.findNavController().navigate(R.id.action_thirdFragment_to_firstFragment) } } } 其中的R.id.action_firstFragment_to_secondFragment2这样的标识,是在nav_graph_main.xml的action标签下配置的。<action android:id="@+id/action_firstFragment_to_secondFragment2" app:destination="@id/secondFragment" /> ③最后的效果图:跳转动画&自定义动画我们会发现,刚刚的例子中,我们在跳转界面时,没有左滑右滑进入界面的动画,显得很生硬,所以我们要给跳转过程中添加上动画。①添加默认动画:在nav_graph_main.xml文件中的Design模式下,点击连接两个Fragment的线。然后你会发现在右侧会出现一个Animations的面板,然后我们点击enterAnim这些选项最右侧的椭圆点,然后就会弹出Pick a Resoure的面板,我们可以在这里选择需要的动画,这里我们就设置nav_default_enter_anim。同理exitAnim我们也设置一个动画,nav_default_exit_anim。然后运行代码你就发现一个渐变的跳转动画。然后配置动画后会发现action多了动画相关的属性<fragment android:id="@+id/firstFragment" android:name="com.taxze.jetpack.navigation.FirstFragment" android:label="fragment_first" tools:layout="@layout/fragment_first" > <action android:id="@+id/action_firstFragment_to_secondFragment2" app:destination="@id/secondFragment" app:enterAnim="@anim/nav_default_enter_anim" app:exitAnim="@anim/nav_default_exit_anim" /> </fragment> enterAnim: 跳转时的目标页面动画exitAnim: 跳转时的原页面动画popEnterAnim: 回退时的目标页面动画popExitAnim:回退时的原页面动画②自定义动画 在真正的业务需求中是会有很多不同的跳转动画的,官方提供的默认动画是不够的,所以我们要学会自定义动画。⑴创建Animation资源文件右键点击res目录,然后依次选择New→ Android Resource File。此时系统会显示 New Resource File对话框。File name输入你的文件夹名(这里设置为slide_from_left),Resource type选择Animation,然后我们就可以发下在res目录下多了一个anim目录,里面存放着我们自定义的动画。⑵编写我们的动画代码这里举几个常用的例子://左滑效果 <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="300" android:fromXDelta="-100%" android:toXDelta="0%"> </translate> </set> //右滑效果 <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="300" android:fromXDelta="0%" android:toXDelta="100%"> </translate> </set> //旋转效果 <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <scale android:duration="1000" android:fromXScale="0.0" android:fromYScale="0.0" android:pivotX="50%" android:pivotY="50%" android:toXScale="1.0" android:toYScale="1.0" /> <rotate android:duration="1000" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:toDegrees="360" /> </set> 常用的属性:属性介绍alpha透明度设置效果scale大小缩放效果rotate旋转效果translate位移效果常用设置:属性介绍android:duration动画时长fromXX开始状态toXX结束状态⑶使用自定义动画文件使用自定义动画也是和使用默认动画是一样的。旋转效果:如何传递数据?Navigation 支持您通过定义目的地参数将数据附加到导航操作。一般情况下,建议传递少量数据。例如传递userId,而不是具体用户的信息。如果需要传递大量的数据,还是推荐使用ViewModel。在nav_graph_main.xml文件中的Design模式下。点击选中其中的Fragment。在右侧的Attributes 面板中,点击Arguments选项右侧的加号。添加需要的参数。添加参数后,在箭头 Action 上点击,会在右边的 Argument Default Values中显示这个userId变量,在xml中也可以看到//伪代码,请勿直接cv <fragment ... > ... <argument android:name="userId" android:defaultValue="1" app:argType="integer" /> </fragment> 代码处理://默认将 箭头 Action 中设置的参数传递过去 it.findNavController().navigate(R.id.action_firstFragment_to_secondFragment2) 动态传递数据①我们可以使用Bundle在目的地之间传递参数//伪代码,请勿直接cv view.findViewById<Button>(R.id.firstButton).setOnClickListener { val bundle = Bundle() bundle.putString("userId", "1") val navController = it.findNavController() navController.navigate(R.id.action_firstFragment_to_secondFragment2, bundle) } ②然后我们就可以接受参数啦在对应的Fragment:val tv = view.findViewById<TextView>(R.id.textView) tv.text = arguments?.getString("userId") 在Activity使用setGraph切换不同的Navigation通常情况下,我们不止一个navigation的文件,我们需要根据业务情况去判断使用哪个,这里就可以用到我们的setGraph去切换不同的Navigation了。①把activity_main中fragment标签下的app:navGraph这个属性去除<fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:defaultNavHost="true"/> ②在代码中通过setGraph设置app:navGraphclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initNav(1) } private fun initNav(id: Int) { var controller = Navigation.findNavController(this@MainActivity, R.id.nav_host_fragment) if (id == 1) { //设置对应的app:navGraph controller.setGraph(R.navigation.first) } if (id == 2) { controller.setGraph(R.navigation.second) } this@MainActivity.finish() } } 到此,我们已经讲完了Navigation大部分的使用情况,下面来讲解几个重要的点,并通过一个案例来学习总结这篇文章吧。NavController 我们在前面已经讲了Navigation的使用,在处理Fragment的对应跳转事件时,我们用到了findNavController这个方法,其实呢,这个就是Navigation的三部分中的NavController中的一个api。那么什么是NavController呢?它是负责操作Navigation框架下的Fragment的跳转与退出、动画、监听当前Fragment信息,当然这些是基本操作。因为除了在Fragment中调用,在实际情况中它也可以在Activity中调用。如果能灵活的使用它,它可以帮你实现任何形式的页面跳转,也可以使用TabLayout配合Navigation在主页进行分页设计。如何获取NavController实例呢?在前面的基础使用中我们也用到了。//伪代码,请勿直接cv //activity: //Activity.findNavController(viewId: Int) findNavController(R.id.nav_host_fragment).navigateUp() //Fragment: //Fragment.findNavController() //View.findNavController() findNavController().navigate(R.id.action_thirdFragment_to_firstFragment) Navigation常用操作:①popBackStack弹出Fragment现在我们从oneFragment跳转到secondFragment在到thirdFragment,然后我们想从thirdFragment回到secondFragment那儿就可以使用popBackStackoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ..... btn.setOnClickListener{ //弹出Fragment controller.popBackStack() } } ②弹出到指定的Fragment也是使用popBackStack,这个方法可以实现清空中间导航栈堆的需求。//xxxFragment弹出到指定的Fragment。 //第二个参数的布尔值如果为true则表示我们参数一的Fragment一起弹出,意思就是如果是false就popBackStack到 //xxxFragment,如果是true,就在xxxFragment在popBackStack()一次 controller.popBackStack(xxxFragment,true) ③navigateUp() 向上导航findNavController(R.id.nav_host_fragment).navigateUp() navigateUp也是执行返回上一级Fragment的功能。和popBackStack功能一样。那么既然它存在肯定是有它特殊的地方的。navigateUp向上返回的功能其实也是调用popBackStack的。 但是,navigateUp的源码里多了一层判断,判断这个Navigation是否是最后一个Fragment。使用popBackStack()如果当前的返回栈是空的就会报错,因为栈是空的了,navigateUp()则不会,还是停留在当前界面。④添加导航监听val listener: NavController.OnDestinationChangedListener = object : OnDestinationChangedListener() { fun onDestinationChanged( controller: NavController, destination: NavDestination, @Nullable arguments: Bundle? ) { Log.d(TAG, "onDestinationChanged: id = ${destination.getId()}") } } //添加监听 controller.addOnDestinationChangedListener(listener) //移除监听 controller.removeOnDestinationChangedListener(listener) ⑤获取当前导航目的地使用getCurrentDestination可以获取当前导航的目的地//获取 val destination = controller.getCurrentDestination() Log.d(TAG, "onCreate: NavigatorName = ${destination.getNavigatorName()}") Log.d(TAG, "onCreate: id = ${destination.getId()}") Log.d(TAG, "onCreate: Parent = ${destination.getParent()}") ⑥判断当前页面显示的Fragment是不是目标Fragment//可直接cv fun <F : Fragment> isActiveFragment(fragmentClass: Class<F>): Boolean { val navHostFragment = this.supportFragmentManager.fragments.first() as NavHostFragment navHostFragment.childFragmentManager.fragments.forEach { if (fragmentClass.isAssignableFrom(it.javaClass)) { return true } } return false } 使用 Safe Args 确保类型安全 通过上面的NavController我们就可以实现fragment之间的跳转,但是Google建议使用 Safe Args Gradle 插件实现。这个插件可以生成简单的对象和构建器类,这些类就可以在目的地之间进行安全的导航和参数的传递啦。那么,该如何使用 Safe Args呢。①在项目最外面的build.gradle中加入//将其放在plugins{}之前 buildscript { repositories { google() } dependencies { def nav_version = "2.4.2" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } } ②在app、module下的build.gradle加入如需生成适用于 Java 模块或Java 和 Kotlin混合模块的Java语言代码apply plugin: "androidx.navigation.safeargs" 如需生成适用于 Kotlin独有的模块的Kotlin代码,请添加以下行:apply plugin: "androidx.navigation.safeargs.kotlin" 要使用Safe Args,必须在gradle.properties这个文件中加入 ini复制代码android.useAndroidX=true android.enableJetifier=true为了避免生成的类名和方法名过于复杂,需要对导航图进行调整,修改了action的id,因为自动生成的id太长了,会导致插件生成的类名和方法名也很长。//伪代码,请勿直接cv <fragment android:id="@+id/blankFragment" android:name="com.taxze.jetpack.navigation.BlankFragment" android:label="fragment_blank" tools:layout="@layout/fragment_blank" > <action android:id="@+id/toSecond" 修改此处id app:destination="@id/blankFragment2" /> </fragment> 然后就会为我们生成BlankFragment和BlankFragment2的类啦,然后就可以和上面一样使用,传递参数啦。var action = BlankFragment.actionJump("111") 也可以使用set方法对对应的参数进行赋值action.setParam("222") 通过Navigation模仿WeChat底部跳转通过Navigation实现开发中超级常用的底部跳转功能话不多说,先上效果图:①右键点击res目录,然后依次选择New→ Android Resource File。此时系统会显示 New Resource File对话框。File name输入你的文件夹名(这里设置为menu),Resource type选择Menu,然后我们就可以发下在res目录下多了一个menu目录,里面存放着我们底部跳转的item。②进入menu.xml的Design下填入四个Item,并分别设置id,title,icon③创建四个Fragment,并在Navigation中建立链接Fragment的布局十分简单,这里就放一个的代码。<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".HomeFragment"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="Home" /> </FrameLayout> class HomeFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_home, container, false) } } 在nav_graph_main.xml建立连接,注意这里每个Fragment的id要和menu.xml中的id一一对应④在activity_main中使用BottomNavigationView建立底部跳转这里在drawable下创建一个selector_menu_text_color.xml用于区分当前的Item<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#818181" android:state_checked="false"/> <item android:color="#45C01A" android:state_checked="true"/> </selector> <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <fragment android:id="@+id/nav_host_fragment" android:layout_width="0dp" android:layout_height="0dp" android:name="androidx.navigation.fragment.NavHostFragment" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph_main" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/nav_view" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:itemIconTint="@drawable/selector_menu_text_color" app:labelVisibilityMode="labeled" app:itemTextColor="@drawable/selector_menu_text_color" app:menu="@menu/menu" /> </androidx.constraintlayout.widget.ConstraintLayout> ⑤在MainActivty中设置切换逻辑class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //去除标题栏 supportRequestWindowFeature(Window.FEATURE_NO_TITLE) setContentView(R.layout.activity_main) val navController: NavController = Navigation.findNavController(this, R.id.nav_host_fragment) val navigationView = findViewById<BottomNavigationView>(R.id.nav_view) NavigationUI.setupWithNavController(navigationView, navController) } } 这样我们就可以实现效果图中的效果啦,具体代码可以查看Git仓库尾述这篇文章已经很详细的讲了Jetpack Navigation的大部分用法,不过在看完文章后,你仍需多多实践,相信你很快就可以掌握Navigation啦因为我本人能力也有限,文章有不对的地方欢迎指出,有问题欢迎在评论区留言讨论~
加载数据在Android开发中应该算是非常频繁的操作了,因此简单在Jetpack Compose中实现一个通用的加载微件效果如下:加载中(转圈圈)另外加载失败后显示失败并可以点击重试实现实现这个微件其实非常简单,无非就是根据不同的状态加载不同页面加载状态Bean首先把加载的状态抽象出来,写个数据类 sealed class LoadingState<out R> { object Loading : LoadingState<Nothing>() data class Failure(val error : Throwable) : LoadingState<Nothing>() data class Success<T>(val data : T) : LoadingState<T>() val isLoading get() = this is Loading val isSuccess get() = this is Success<*> }此处借鉴了朱江大佬写的玩安卓Compose版本,在此感谢微件然后就是加载了。考虑到加载一般耗时,所以用协程。写成微件大概就是下面这个样子 private const val TAG = "LoadingWidget" /** * 通用加载微件 * @author [FunnySaltyFish](https://blog.funnysaltyfish.fun/) * @param modifier Modifier 整个微件包围在Box中,此处修饰此Box * @param loader 加载函数,返回值为正常加载出的结果 * @param loading 加载中显示的页面,默认为转圈圈 * @param failure 加载失败显示的页面,默认为文本,点击可以重新加载(retry即为重新加载的函数) * @param success 加载成功后的页面,参数[T]即为返回的结果 */ @Composable fun <T> LoadingContent( modifier: Modifier = Modifier, loader : suspend ()->T, loading : @Composable ()->Unit = { DefaultLoading() }, failure : @Composable (error : Throwable, retry : ()->Unit)->Unit = { error, retry-> DefaultFailure(error, retry) }, success : @Composable (data : T?)->Unit ) { var key by remember { mutableStateOf(false) } val state : LoadingState<T> by produceState<LoadingState<T>>(initialValue = LoadingState.Loading, key){ value = try { Log.d(TAG, "LoadingContent: loading...") LoadingState.Success(loader()) }catch (e: Exception){ LoadingState.Failure(e) } } Box(modifier = modifier){ when(state){ is LoadingState.Loading -> loading() is LoadingState.Success<T> -> success((state as LoadingState.Success<T>).data) is LoadingState.Failure -> failure((state as LoadingState.Failure).error){ key = !key Log.d(TAG, "LoadingContent: newKey:$key") } } } }基于produceState加载并保存数据,然后根据不同的加载状态显示不同的页面。官方对此函数的翻译如下:/** * Return an observable [snapshot][androidx.compose.runtime.snapshots.Snapshot] [State] that * produces values over time from [key1]. * * [producer] is launched when [produceState] enters the composition and is cancelled when * [produceState] leaves the composition. If [key1] changes, a running [producer] will be * cancelled and re-launched for the new source. [producer] should use [ProduceStateScope.value] * to set new values on the returned [State]. * * The returned [State] conflates values; no change will be observable if * [ProduceStateScope.value] is used to set a value that is [equal][Any.equals] to its old value, * and observers may only see the latest value if several values are set in rapid succession. * * [produceState] may be used to observe either suspending or non-suspending sources of external * data, for example: * * @sample androidx.compose.runtime.samples.ProduceState * * @sample androidx.compose.runtime.samples.ProduceStateAwaitDispose */翻译过来就是:/** * 返回一个可观察的[snapshot][androidx.compose.runtime.snapshots.Snapshot] [State] 对象,它的值由[key1]随时间变化产生. * * [producer] 在 [produceState] 进入 composition 后会被启动,当[produceState] 离开 composition 时被取消. 如果 [key1] 改变, 当前正在运行的 [producer] 将被取消并根据新来源重启. * [producer] 应当使用 [ProduceStateScope.value] ,在返回的 [State] 中设置 value 的值. * * 返回的 [State] 与 value 一致; 如若新的值与旧value相等[Any.equals] ,则此变化不会被观察到 * 如果在短时间内多个新值被赋予,则观察着可能仅能观察到最新的值 * * [produceState] 可被用在 suspending / non-suspending 的外部数据来源中,如: * * @sample androidx.compose.runtime.samples.ProduceState * * @sample androidx.compose.runtime.samples.ProduceStateAwaitDispose */除开数据加载外,上面的代码也给出了几个默认页面。分别有默认的加载页面(转圈圈)和默认的错误页面(点击重试) @Composable fun DefaultLoading() { CircularProgressIndicator() } @Composable fun DefaultFailure(error: Throwable, retry : ()->Unit) { Text(text = stringResource(id = R.string.loading_error), modifier = Modifier.clickable(onClick = retry)) }此微件使用起来也很简单直接 LoadingContent( modifier = Modifier.align(CenterHorizontally) , loader = (vm.sponsorService::getAllSponsor) ) { list -> list?.let{ Column { SponsorList(it) Text("加载完成") } } }完整代码在这里:加载微件:FunnyTranslation/LoadingWidget.kt使用示例:FunnyTranslation/ThanksScreen.kt