0%

Android-Jetpack初识

Navgation

  • 创建navgation文件夹,编写xml
  • 引用navgation的fragment需指定xml文件,已经通过name指定一个NavHostFragment或其派生类,是一个空的Fragment容器
  • 指定fragment以及相应action(通过Desgin拖动视图级箭头设置)
  • 设置点击返回键Back时的回退路径destination
    • popUpToInclusive=true时,返回栈帧删除fragment,连同制定fragment
    • popUpToInclusive=false时,返回栈帧删除fragment,留下指定fragment
  • 若指定的destination是最初的fragment,且popUpToInclusive=true,则所有栈帧移除,返回键会推出该app
  • NavigationUI 设置菜单栏,返回键,抽屉等
    • 菜单栏是通过id指定navigation.xml中的fragment,注意对应
  • 使用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
3
private 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
2
3
4
5
// Fragmen中加载layout
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater, R.layout.fragment_title, container, false)
return binding.root
}

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)
如果出现以上报错,可以检查一下以下部分

  1. room模块需要添加的依赖包是否完整
    implementation “androidx.room:room-ktx:$room_version”
    implementation “androidx.room:room-runtime:$room_version”
    kapt “androidx.room:room-compiler:$room_version”
  2. 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对象。超过时间将会强制停止该工作。

Developer

  • 后台任务分为定时任务和一次性任务。定时任务的周期必须大于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)与数据

组件在实际项目中的应用


未完 待续 (╯‵□′)╯︵┻━┻