Navgation
Navigation基本使用
- 创建navgation文件夹,编写xml
- 引用navgation的fragment需指定xml文件,已经通过
name
指定一个NavHostFragment或其派生类,是一个空的Fragment容器 - 指定fragment以及相应action(通过Desgin拖动视图级箭头设置)
NavigationUI
- 设置点击返回键Back时的回退路径destination
popUpToInclusive=true
时,返回栈帧删除fragment,连同制定fragmentpopUpToInclusive=false
时,返回栈帧删除fragment,留下指定fragment
- 若指定的destination是最初的fragment,且popUpToInclusive=true,则所有栈帧移除,返回键会推出该app
- NavigationUI 设置菜单栏,返回键,抽屉等
- 菜单栏是通过id指定navigation.xml中的fragment,注意对应
NavDirection
- 使用NavDirection实现fragment与activity之间的数据传输,代替Bundle。目的减少键值对使用出现的问题如key错误,类型不匹配等
- 需要安装gradle插件,因为编译期间需要创建Direction对象以携带数据
1
2
3
4// project gradle下添加
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
// module gradle下添加
apply plugin: 'androidx.navigation.safeargs' - 调用Direction的静态方法实现跳转的action,携带的数据以Direction内部属性的形式传递
- 接收数据一端,通过Args对象,fromBundle(getArguments())获取Bundle并转换为对应的Args对象,其中保存着传输的数据
LifeCycle
用于感知Activity与Fragment生命周期的组件。
ViewModel
UI controller is a UI-based class such as Activity or Fragment. A UI controller should only contain logic that handles UI and operating-system interactions such as displaying views and capturing user input. Don’t put decision-making logic, such as logic that determines the text to display, into the UI controller.
UI控制不应该包含决策逻辑,只负责界面的显示绘制,以及感知界面变化,如有按钮被点击时将这一信息传递到ViewModel
- ViewModel会在Activity被finish或Fragment被destroyed后调用onCleared()销毁
During configuration changes such as screen rotations, UI controllers such as fragments are re-created. However, ViewModel instances survive. If you create the ViewModel instance using the ViewModel class, a new object is created every time the fragment is re-created. Instead, create the ViewModel instance using a ViewModelProvider.
- 直接使用ViewModel创建实例,则UI控制器因为配置修改(经典旋转屏幕)而重新创建时,ViewModel会创建新的实例。所以使用ViewModelProvider创建ViewModel实例,ViewModel不会被重复创建,并且可以存储数据
- ViewModelProvider创建ViewModel且关联一个给定作用域,如activity或fragment。并且保留数据直到作用域被销毁
- 在绑定变量(databinding)的定义之后使用ViewModelProvider获取ViewModel,并且传递关联的上下文及具体的ViewModel.class
Because the app’s activities, fragments and views do not survive configuration changes, the ViewModel should not contain references to the app’s activities, fragments, or views.
- ViewModel不应该保存fragment,view等的引用。因为ViewModel在配置变化后存活,但frament和view不会存活(re-Create)
- 绑定变量以及Binding对象不应该存储到ViewModel中,因为他们包含了对view的引用。所有决定数据显示逻辑的代码应该在ViewModel中编写
- AndroidViewModel。类似ViewModel的基类,区别在于携带一个Application参数
最简单直白的使用,其实就是将控制逻辑和数据使用ViewModel包装起来,并且在View中使用ViewModelProvider加载,以实现View重建时,ViewModel不会重新加载而将保存的数据重复使用
ViewModelFactory
使用Factory的意义在于,单纯的使用ViewModelProvider创建ViewModel,无法向ViewModel传递参数,只能实例化没有参数的ViewModel
使用Factory,可以实现实例化ViewModel时传递多个参数
LiveData
- 使用MutableLiveData代替数据类型,通过泛型设置数据的真正类型,数据真正的值通过liveData.value访问
- LiveData的实现是基于观察者模式,因此需要在view中设置观察事件。在viewModel初始化以后,调用MutableLiveData.observe方法,设置回调事件。并且首先需要传入一个viewLifecycleOwner
Why use viewLifecycleOwner? - 在回调方法中将回调参数进行使用,则当viewModel中的liveData发生变化时,回调函数执行
LiveData封装
由于在View中可以直接访问到ViewModel的内容,直接修改LiveData,所以需要通过封装以及kotlin的幕后属性(Backing Property)实现LiveData的读写限制
尝试不使用ViewModel,只使用LiveData,理论上互不影响
kotlin-Backing property
Kotlin的幕后属性,用于实现属性对外表现可读,对内表现可写可读。1
2
3private var _name = ""
public val name : String
get() = _name
类内部使用_name
进行读写,对外由于没有设置set()写访问器,只能访问name
顺便说说kotlin的幕后字段field
。在kotlin中,使用属性都是通过访问器,所以在get()中使用该属性,实际上也是调用get(),即出现递归,最后堆栈溢出,setter同理。
所以在getter,setter中,使用field
表示该属性,且field也只能用于getter,setter。
DataBinding
1 | // Fragmen中加载layout |
DataBinding+LiveData
没有DataBinding的情况下,ViewModel与View直接没有直接的联系,其间通过UIController连接,如一个按钮被点击,UIController调用一个点击事件,通知ViewModel做出响应,然后更新
使用DataBinding,View与ViewModel直接通信,指的是在xml中直接与ViewModel通信,而不通过UIController,即直接在xml中编写逻辑。
- 首先需要在xml中引入ViewModel实例,在
以下建立标签,其中通过 引用viewModel对象,然后在UIController中,将viewModel传入
绑定的内容,主要分为两种,行为和数据 - 绑定行为,主要对象为按钮等。在
onClick
一项中通过@{() -> viewModel.onSkip() }的方式,直接调用ViewModel中的方法 - 绑定数据,主要对象为TextView等。在
text
一项中通过@{viewModel.data}的方式设置文本If the value of word is null, the LiveData object displays an empty string.
- 注意,需要把UIController的LifecycleOwner实例传入,否则LivaData的observe不生效。binding对象中自带lifecycleOwner属性,需要实例化
- 如果设置liveData时需要对文本进行修饰,可以使用
Current Score:%d 的形式
DataBinding+Transformations
实际上也是一种数据转换。即在ViewModel中另设一个变量,其值通过Transformtions.map(liveData)的方式定义,返回显示时需要的文本格式和内容
BindingAdapter
在Kotlin中,允许通过扩展函数的方式增加类的能力。
例如fun TextView.setFormatText(con: String)
,为TextView扩展了一个方法,在其中可以直接调用TextView的方法,有点类似于C++的友元函数
如果为该方法添加注解@BindingAdapter
,并且传入属性名称,可以实现在xml中通过该属性名调用该方法。该方法即成为该属性的适配器。
Room
LiteSQL上层封装,实现更加便捷的数据库操作
- 编写实体类,通过注解标志信息
- @Entity,默认使用类名创建数据表,可以通过参数name指定
- @PrimaryKey,标识主键,通过参数autoGenrate设置是否自增
- @ColumnInfo,标识表项
- 与其他数据库框架相似,Room也需要编写Dao模块接口,可将视为自定义访问数据库的接口。
- 需要使用@Dao标识,并且方法需要使用对应操作的注解标识,如@Insert,@Update
- 其他操作没有便捷的注解方式,如select或delete,需要在@Query注解內,传入对应的SQL语句
- 注意,当sql语句中需要引用接口方法传入的参数时,使用
where id = :key
的方式引用 - 使用@Delete可以删除多个特定数据项
- 通过使用LiveDate作为返回值类型,Room可以保持LiveData的持续更新,即只需要一次显式获取数据
- 使用单例模式实现数据库实例。通过注解
@Database
标识,传入参数为实体列表(数据表),版本号。使用build模式创建数据库实例,传入参数为app上下文,该数据库类,数据库名
Execution failed for task ‘:app:kaptDebugKotlin’.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
java.lang.reflect.InvocationTargetException (no error message)
如果出现以上报错,可以检查一下以下部分
- room模块需要添加的依赖包是否完整
implementation “androidx.room:room-ktx:$room_version”
implementation “androidx.room:room-runtime:$room_version”
kapt “androidx.room:room-compiler:$room_version”- Dao,Entity,Database中room使用是否规范
例如我的问题则是:Dao中@Update修饰的方法设置了返回值…
建议:不要在启动应用时即联网获取数据显示,而是直接从数据库读取数据进行显示,从而减少程序加载的时间。
而当获取到网络的数据时,应将数据存到数据库中,而不是直接显示数据。更新完数据库后再更新屏幕中的内容。
关于协程
关于从主线程获取任务完成的方式,有两种,回调和协程
回调可以实现在后台长时间执行任务而不阻塞主线程,任务结束后执行作为参数传入的回调任务。
然而回调函数也有明显的确定,即回调方法块会以异步的形式在某一个时间执行,因此代码不是顺序执行,大量的回调函数使得代码可读性下降。另外,回调函数不允许使用某些语言特性,如异常
在Kotlin中,可以直接使用协程实现线程协同。协程可以实现基于回调的代码转化为顺序执行,提升了可读性,甚至可以使用异常等语言特性
实际上,回调和协程都是完成了同样的事情,即等待一个耗时任务的结果,并继续执行。
使用suspend
标记函数,代表其可以被协程调用。当该函数被调用,协程将暂停执行,而不是像普通函数一样阻塞直到函数返回,也就是说,当协程暂停等待耗时任务的结果时,他不会阻塞其原来运行的线程。
suspend可以理解为挂起或暂停。与阻塞的区别在于,线程挂起时,在等到其结果返回前可以进行其他的工作;而阻塞线程时,不能进行其他工作。
返回LiveData的方法Room默认会在后台线程执行,因此不需要使用suspend标记。
关于RecyclerView
使用notifyDataSetChanged()
可以进行全列表的内容更新。但这实际上有些效率低下,因为每次更新的可能只是列表中的一个或几个Item,而不需要否定整个列表并对他进行重新绑定和绘制。
使用notifyItemChange()
可以解决以上问题,他可以更新列表中的某些个Item而不需要重绘整个列表。然而所以增删改查操作都通过它实现,可能涉及较多代码并且不好实现。
RecyclerView提供了更好的方法。
DiffUtil
通过实现DiffUtil.ItemCallback
接口,实现其中的比较方法逻辑,并将该子类实例化对象传入Adapter,实现自动进行代码比较与更新。
该接口中的两个比较方法,分别定义了Item对象的比较方式与Item内容的比较方式,并在内部通过比较算法,实现快速对比得到差异。
DataBinding
使用androidx.recyclerview.widget.ListAdapter
代替RecyclerView.Adapter
,配合databinding使用。
通过向ViewHolder传入一个DataBinding对象,代替原来的View,实现数据的绑定。并且在数据发生变化时(observer),通过adapter.submitList(data)
,进行数据更新。submitList()
方法会对比新旧两个数据列表的数据,比较的原则则是我们在DiffCallback中实现的两个方法。比较完后根据差异自动进行数据增删修改。
WorkManager
- Worker 接口中实现doWork()方法,指定后台工作内容
- WorkRequest 用于请求后台工作,可以配置工作优先级,调用时机,如电量瓶颈,连上wifi等
- WorkManager 安排并运行后台工作,分散系统负荷
doWork()
作为一个suspend方法,会在一个后台线程调用。Android系统最多会提供10min时间去完成一个任务,并返回一个Result对象。超过时间将会强制停止该工作。
- 后台任务分为定时任务和一次性任务。定时任务的周期必须大于15min。
- 使用WorkRequestBuilder创建WorkRequest。PeriodicWorkRequestBuilder用于创建定时任务,需要传入周期和单位
- 使用setConstraints()设置约束
- 执行任务,使用WorkManager调用enqueueUniquePeriodicWork(),将任务传入任务队列
- 定时任务会多次执行,除非其被取消。第一次设定即执行,或者满足约束即执行
- 定时任务的时间间隔并不精准,取决于系统的电池优化。
- 取消任务可以通过work.id,word.name,work.tag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 示例
private fun setupRecurringWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
.build()
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()
Timber.d("Periodic Work request for sync is scheduled")
WorkManager.getInstance().enqueueUniquePeriodicWork(
RefreshDataWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
repeatingRequest)
}总结
在ViewModel中,使用LiveData实现数据与界面协同,即数据变化时界面的变化
使用databinding,直接关联界面(xml)与数据