第一行代码 第三版 第13章 Jetpack

第13章 Jetpack

Jetpack是开发组件工具集,主要目的是帮助我们编写出更加简洁的代码,简化开发过程。许多开发组件为MVVM架构量身打造。

13.2 ViewModel

  • 作用:帮助活动分担一部分工作,专门用来存放界面相关数据。
  • 横竖屏不会丢失数据,活动销毁后才会
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
  • 通常要为每个活动和碎片创建一个ViewModel

  • 计数器

  1. 创建ViewModel
class MainViewModel : ViewModel(){

    var counter = 0
}
  1. 布局按钮点击,展示数据
  2. 逻辑代码
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //不能直接创建实例,因为ViewModel的生命周期长于活动,如果活动创建的时候创建实例,那就无法保存数据
        //不推荐使用
        //ViewModelProviders.of().get(

13.2.2 向ViewModel中传递数据

  • 借助ViewModelProvider.Factory

让退出程序后也能保存数据

  1. 修改ViewModel
class MainViewModel(countReserved: Int): ViewModel(){

    var counter = countReserved
}
  1. 实现 ViewModelProvider.Factory 接口
class MainViewModelFactory(private val countReserved: Int) :ViewModelProvider.Factory {
    override fun  create(modelClass: Class): T {
        return MainViewModel(countReserved) as T
    }
}
  1. 逻辑
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)
        clearBtn.setOnClickListener {
            viewModel.counter = 0
            refreshCounter()
        }

        //不能直接创建实例,因为ViewModel的生命周期长于活动,如果活动创建的时候创建实例,那就无法保存数据
        viewModel = ViewModelProviders.of(this,MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
        plusOneBtn.setOnClickListener {
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        infoText.text  = viewModel.counter.toString()
    }

    override fun onPause() {
        super.onPause()
        sp.edit{
            putInt("count_reserved",viewModel.counter)
        }
    }
}

13.3 Lifecycles

  • 时刻感知活动的生命周期,以便在恰当时候进行相应的逻辑控制。如:界面中发情网络请求,得到响应的时候界面已经关闭,这时候就不应该对相应继续处理。

  • 如何在非活动的类中去感知活动的生命周期。1.活动中嵌入隐蔽的碎片2.手写监听器的方式感知

  • 手写监听器的方式感知,缺点:要在活动中编写大量代码,不够优雅

lateinit var observer: MyObserver

observer = MyObserver()

override fun onStart() {
        super.onStart()
        observer.activityStart()
    }

    override fun onStop() {
        super.onStop()
        observer.activityStop()
    }
  • Lifecycles:能够让任何一个类轻松感知活动的生命周期又不用再活动中编写大量代码
  1. 实现接口,编写方法和注解
class MyObserver : LifecycleObserver {

    private val TAG = "MyObserver"
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart() {
        Log.d(TAG, "activityStart: ")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun activityStop() {
        Log.d(TAG, "activityStop: ")
    }
}
  • ON_CREATE,ON_START,以此类推其他生命周期事件
  • ON_ANY 匹配任何生命周期
  1. 活动和碎片都是LifecycleOwner实例

在活动中调用

lifecycle.addObserver(MyObserver())
  • 主动获取生命周期状态,加入构造函数
class MyObserver(val lifecycle:Lifecycle) : LifecycleObserver {

    private val TAG = "MyObserver"
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart() {
        Log.d(TAG, "activityStart: ")
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun activityStop() {
        Log.d(TAG, "activityStop: ")
    }
}

有了对象后可以在任何地方调用

Lifecycle.currentState获知当前的生命周期状态

13.4 LiveData

  • 响应式编程组件,包含任何类型的数据,数据发生变化的时候通知给观察者。一般和ViewModel一起使用

  • 将计数器用 LiveData 包装,然后 活动中去观察它,就能主动把变化通知给活动

  1. 修改MainViewModel
class MainViewModel(countReserved: Int): ViewModel(){

    //泛型指定为Int ML表示的是可变的LiveData。
    //getValue 获取LD中的数据
    //setValue 设置数据,只能在主线程
    //postValue 非主线程设置数据
    val counter = MutableLiveData()

    init {
        counter.value = countReserved
    }

    fun plusOne() {
        val count = counter.value ?: 0
        counter.value = count + 1
    }

    fun clear() {
        counter.value = 0
    }

}
  1. 逻辑
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycle.addObserver(MyObserver(lifecycle))

        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)
        clearBtn.setOnClickListener {
            viewModel.clear()
        }

        //不能直接创建实例,因为ViewModel的生命周期长于活动,如果活动创建的时候创建实例,那就无法保存数据
        viewModel = ViewModelProviders.of(this,MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
        plusOneBtn.setOnClickListener {
            viewModel.plusOne()
        }
        //任何livedata对象都有observe方法
        //第一个参数是lifecycleowner,第二个参数是Observer接口,当数据发生变化的时候就会回调到这里
        viewModel.counter.observe(this, Observer { count ->
            infoText.text = count.toString()
        })
    }

    override fun onPause() {
        super.onPause()
        sp.edit{
            putInt("count_reserved",viewModel.counter.value ?: 0)
        }
    }
}
  • Lifecycle-livedata-ktx 就是为kotlin专门设计的库,对observe做了语法拓展。自己敲前面后面会显示版本
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
//优化写法
viewModel.counter.observe(this){ count ->
    infoText.text = count.toString()
}

问题:暴露数据counter给外部,破坏了livedata的封装性

解决:暴露不可变的LiveData给外部,这样非ViewModel中就只能观察Livedata的数据变化

class MainViewModel(countReserved: Int): ViewModel(){
    
    val counter:LiveData
        get() = _counter
    
    private val _counter = MutableLiveData()

    init {
        _counter.value = countReserved
    }

    fun plusOne() {
        val count = counter.value ?: 0
        _counter.value = count + 1
    }

    fun clear() {
        _counter.value = 0
    }

}

13.4.2 map和switchMap

看书的逻辑

  1. 文字一段一段的理解
  2. 有代码的时候不直接看代码,看后面的解释一句句的对应理解。一般前两步就能够把书中内容看懂
  3. 把代码在项目上复现,然后添加注释。
  4. 记录笔记
  • 作用:将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换
  1. Bean类
data class User(var firstName:String,var lastName:String,var age: Int) {
}
  1. 设置
val userLiveData = MutableLiveData()
  1. 如果只需要暴露姓名和用户给外界,不暴露年龄如何做,借助map
//当userLD数据发生变化后,map会监听到,然后转换函数后将数据通知给username的监听者
    //保证数据的封装性
    private val userLiveData = MutableLiveData()

    //map接收原始livedata,第一个参数是一个转换函数
    val username: LiveData = Transformations.map(userLiveData){ user ->
        "${user.firstName} ${user.lastName}"
    }
  • swipeMap( ) ViewMode中的某个LiveData对象是调用另一个方法获取的,可以用这个方法把livedata转换为一个可观察的livedata
  • 模拟
  1. 新建单例类
object Repository {

    fun getUser(userId: String) : LiveData {
        val liveData = MutableLiveData()
        liveData.value = User(userId, userId, 0)
        return liveData
    }
    
}
  1. 在ViewModel中调用另一个方法来获取LIveData
fun getUser(userId: String): LiveData {
    return Repository.getUser(userId)
}
  1. 修改方法,用来观察这个不可观察的变量
//当用户调用这个函数的时候会给userIdLiveData设值
fun getUser(userId: String) {
    userIdLiveData.value = userId
}
//userIdLiveData值发生变化后会通知,switchMap
private val userIdLiveData = MutableLiveData()

//switchMap函数会将不可观察的它转换为一个新的LiveData,我们观察这个新的就可以了
val user: LiveData = Transformations.switchMap(userIdLiveData){ userId ->
    Repository.getUser(userId)
}
  1. 调用
getUserBtn.setOnClickListener {
    val userId = (0..10000).random().toString()
    viewModel.getUser(userId)
}
viewModel.user.observe(this, Observer { user ->
    infoText.text = user.firstName
})

问题 ViewModel 获取数据的方法可能是没有参数的,需要搭建一个空的LiveData

class MyViewModel : ViewModel() {

    private val refreshLiveData = MutableLiveData()

    val refreshResult = Transformations.switchMap(refreshLiveData){
        Repository.refresh()
    }

    fun refresh() {
        refreshLiveData.value = refreshLiveData.value
    }

}

13.5 Room

  • ORM 也叫对象关系框架:面向对象的语言和面向关系的数据库之间建立了一种映射关系。

13.5.1 使用 Room 进行增删查改

  • Entity 实体类:对应数据库中的表
  • Dao 数据访问对象,定义对数据库进行的各项操作
  • Database 定义数据库的关键信息,提供访问实例
  1. 依赖

https://developer.android.google.cn/jetpack/androidx/releases/room?hl=zh_cn ROOM官网地址

//kapt只能在kotlin项目中使用,如果是java要使用,annotationProcessor
id 'kotlin-kapt'

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
  1. Entity 实体类
//注解Entity:声明为实体类
@Entity
data class User(var firstName:String,var lastName:String,var age: Int) {

    //添加字段id,设置为主键,并且自动生成
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}
  1. Dao 数据访问对象。接口
//Room识别成Dao
@Dao
interface UserDao {
    
    //插入完把生成的主键返回
    //对插入的时候已经有相应id进行处理
    //解决插入冲突 它是取代就数据同时继续事务
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUser(user: User):Long
    
    @Update
    fun updateUser(newUser: User)
    
    @Query("select * from User")
    fun loadAllUsers(): List
    
    @Query("select * from User where age > :age")
    fun loadUserOlderThan(age: Int): List
    
    @Delete
    fun deleteUser(user: User)
    
    @Query("delete from User where lastName = :lastName")
    fun deleteUserByLastName(lastName: String):Int
    
}
  • 查询需要写SQL语句,可以将传入的参数添加到SQL语句里
  • 增删改传入非实体类,都只能用Query注解,写SQL语句,见最后一条
  1. 增删查改按钮
  2. 运用
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User("Tom","Brady",40)
val user2 = User("Tom","Hanks",63)
addDataBtn.setOnClickListener {
    thread {
        user1.id = userDao.insertUser(user1)
        user2.id = userDao.insertUser(user2)
    }
}
updateDataBtn.setOnClickListener {
    thread {
        user1.age = 42
        userDao.updateUser(user1)
    }
}
deleteDataBtn.setOnClickListener {
    thread {
        userDao.deleteUserByLastName("Hanks")
    }
}
queryDataBtn.setOnClickListener {
    thread {
        for (user in userDao.loadAllUsers()) {
            Log.d(TAG, user.toString())
        }
    }
}

注:Room不允许在主线程中使用,添加一个方法就允许在主线程中使用,建议只在测试环境中用

AppDatabase::class.java,"app_database")
    .allowMainThreadQueries()
    .build().apply {
        instance = this
    }

13.5.2 Room 的数据库升级

  • 不正规写法
AppDatabase::class.java,"app_database")
    .allowMainThreadQueries()
    .fallbackToDestructiveMigration()//能够在数据库升级的时候删除旧表,创建新表
    .build().apply {
        instance = this
    }
  • 正规写法
  1. 要创建的新表实体类
@Entity
data class Book(var name:String,var pages:Int) {

    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}
  1. 数据库操作
@Dao
interface BookDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertBook(book:Book):Long

    @Query("select * from Book")
    fun loadAllBooks():List

}
  1. 数据库升级逻辑
//版本号和数据库包含的实体类,如果实体类不止一个用逗号分隔
//必须继承,指定抽象类,提供抽象方法,获取之前的Dao实例
@Database(version = 2, entities = [User::class,Book::class]) 1
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao():UserDao
    abstract fun bookDao(): BookDao 2

    companion object{
        
        //数据库版本从1升级到2时候执行匿名类中的升级逻辑
        val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) { 3
                database.execSQL("create table Book (id integer primary" +
                        " key autoincrement not null,name text not null," +
                        "pages integer not null)")
            }
        }
        
        //单例模式
        private var instance: AppDatabase? = null

        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            //第一个参数只能使用这个,不能用context
            //第二个参数是AppDatabase的class类型
            return Room.databaseBuilder(context.applicationContext,
            AppDatabase::class.java,"app_database")
                .allowMainThreadQueries()
                .addMigrations(MIGRATION_1_2) 4
                .build().apply {
                    instance = this
                }
        }

    }
}

如何给某张表加一个列

  1. 实体类更新
@Entity
data class Book(var name:String,var pages:Int,var author: String) {

    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}
  1. 数据库升级
package com.android.jetpacktest

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

//版本号和数据库包含的实体类,如果实体类不止一个用逗号分隔
//必须继承,指定抽象类,提供抽象方法,获取之前的Dao实例
@Database(version = 3, entities = [User::class,Book::class]) 1
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao():UserDao
    abstract fun bookDao(): BookDao

    companion object{
        val MIGRATION_2_3 = object : Migration(2, 3) { 2
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("alter table Book add column author text not null default 'unknown'")
            }

        }

        //数据库版本从1升级到2时候执行匿名类中的升级逻辑
        val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("create table Book (id integer primary" +
                        " key autoincrement not null,name text not null," +
                        "pages integer not null)")
            }
        }

        //单例模式
        private var instance: AppDatabase? = null

        @Synchronized
        fun getDatabase(context: Context): AppDatabase {
            instance?.let {
                return it
            }
            //第一个参数只能使用这个,不能用context
            //第二个参数是AppDatabase的class类型
            return Room.databaseBuilder(context.applicationContext,
            AppDatabase::class.java,"app_database")
                .allowMainThreadQueries()
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3) 3
                .build().apply {
                    instance = this
                }
        }

    }
}

13.6 WorkManager

作用:处理一些定时执行的任务,可以根据操作系统的版本自动选择底层使用的是AlarmManager还是JobScheduler。支持周期性任务,链式任务处理。和服务没联系,在应用退出,手机重启依然会执行。适用于执行定期和服务器交互的任务:周期性的同步数据。不一定准时,因为为了减少耗电,会把几个任务放在一起执行。

implementation "androidx.work:work-runtime:2.7.1"
  1. 定义一个后台任务,并实现具体的任务逻辑
class SimpleWorker(context: Context, params:WorkerParameters):Worker(context,params) {
    override fun doWork(): Result {
    		//后台任务逻辑,不会运行在主线程,可以编写耗时逻辑
        Log.d("SimpleWorker","do work in SimpleWorker")
        return Result.success()
        //Result.failure()表示任务失败
        //Result.retry()表示失败并且结合后面学的内容可以重新执行任务
    }
}
  1. 配置该后台任务的运行条件和约束信息,并构建后台任务请求

添加按钮,如果按钮超出LL的范围可以用ScrollView滚动查看屏幕外的内容

doWorkBtn.setOnClickListener {

    //构建单次运行的后台任务请求
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    //PeriodWorkRequest.Builder周期性运行后台任务,不过传入的周期间隔不能少于15分钟   
    WorkManager.getInstance(this).enqueue(request)
}
  • 由于没有指定任何约束条件,所以立即执行

13.6.2 WorkManager 处理复杂的任务

doWorkBtn.setOnClickListener {

    //构建单次运行的后台任务请求
    //PeriodWorkRequest.Builder周期性运行后台任务,不过传入的周期间隔不能少于15分钟
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
        .setInitialDelay(5,TimeUnit.MINUTES)//让任务在指定的延迟时间后执行。时间单位可以自由的指定:ms,s,m,h,d
        .addTag("simple")//给后台任务请求添加标签,好处:同一组标签可以同时一下取消
        //如果后台任务doWork返回了Result.retry()
        //可以结合下面方法来重新执行任务
        .setBackoffCriteria(BackoffPolicy.LINEAR,10,TimeUnit.SECONDS)
        //第一个参数指定任务失败,下次重新执行以什么样的形式延迟。这个是线性
        //EXPONENTIAL这个是指数
            //第二个和第三个指定多久后执行,不能少于10分钟
        .build()
    
    WorkManager.getInstance(this).enqueue(request)
}

WorkManager.getInstance(this).cancelAllWorkByTag("simple")//通过标签来取消后台任务
WorkManager.getInstance(this).cancelWorkById(request.id)//通过id来关闭任务,坏处:只能关闭单个
WorkManager.getInstance(this).cancelAllWork()//关闭所有后台任务
//doWork返回值的利用
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id).observe(this){workInfo ->
    if ((workInfo.state == WorkInfo.State.SUCCEEDED)) {
        Log.d(TAG, "do work success")
    }else if(workInfo.state == WorkInfo.State.FAILED){
        Log.d(TAG, "do work failed")
    }
}
  • 链式任务
//链式任务
//假设有三个独立的后台任务
val sync = ...
val compress = ...
val upload = ...
/**
 *前一个后台任务完成后,后一个任务才能执行。前面的如果被取消,后面的也就不会执行。
 *WorkManager在国产手机上不稳定,不要用它写核心功能
 */
WorkManager.getInstance(this)
    .beginWith(sync)//开启链式任务
    .then(compress)//需要接上的链式任务
    .then(upload)
    .enqueue(request)

13.7 Kotlin:使用DSL构建专有的语法结构(Domain Specific Language)

  • DSL:领域特定语言,别写出一些看似脱离原始语法结构的代码。

  • 利用高阶函数实现DSL。实现类似依赖库中的内容

class Dependency{

    val libraries = ArrayList()

    fun implementation(lib: String) {
        libraries.add(lib)
    }

}

fun dependencies(block: Dependency.() -> Unit): List {
    val dependency = Dependency()
    dependency.block()
    return dependency.libraries
}

fun main() {
    //调用
    dependencies {
        implementation("com.squareup.retrofit2:retrofit2:2.6.1")
        implementation("com.squareup.retrofit2:converter-gson2.6.1")
    }
    //获取所有添加到依赖库中的数据
    val libraries = dependencies {
        implementation("com.squareup.retrofit2:retrofit2:2.6.1")
        implementation("com.squareup.retrofit2:converter-gson2.6.1")
    }
    for (lib in libraries) {
        println(lib)
    }
}
  • Kotlin 中动态生成表格所对应的 HTML代码
package com.android.jetpacktest

import java.lang.StringBuilder

class Td{
    var content = ""

    fun html() = "\n\t\t$content"
}

class Tr{
    private val children = ArrayList()

    fun td(block: Td.() -> String) {
        val td = Td()
        td.content = td.block()
        children.add(td)
    }

    fun html():String{
        val builder = StringBuilder()
        builder.append("\n\t")
        for (childTag in children) {
            builder.append(childTag.html())
        }
        return builder.toString()
    }
}

class Table{
    private val children = ArrayList()

    fun tr(block: Tr.() -> Unit) {
        val tr = Tr()
        tr.block()
        children.add(tr)
    }

    fun html(): String {
        val builder = StringBuilder()
        builder.append("")
        for (childTag in children) {
            builder.append(childTag.html())
        }
        builder.append("\n
") return builder.toString() } } fun table(block: Table.() -> Unit): String { val table = Table() table.block() return table.html() } fun main() { val html = table { tr{ td { "Apple" } td { "Grape" } td { "Orange" } } tr { td { "Pear" } td { "Banana" } td { "Watermelon" } } } println(html) } fun main() { val html = table { repeat(2) { tr { val fruits = listOf("Apple","Grape","Orange") for (fruit in fruits) { td { fruit } } } } } println(html) }

你可能感兴趣的:(安卓,android,kotlin,开发语言)