Jepack常用组件使用

Aengus Sun 963 2020-07-07

Jetpack是Google官方推出的一套Android库,它帮助开发者更方便、更快速的开发出稳健性极佳的软件,简化开发流程与提高效率。Jetpack库常用的如下几个组件,它们都可以单独使用或者组合使用:

组建名称介绍
Android KTXKotlin扩展程序,包括扩展函数、扩展属性、协程等
AppCompat提供向后兼容性的Android组件
WorkManager管理应用退出或设备重启时仍应运行等可延迟异步任务
RoomSqlite数据库持久化抽象层
ViewModel以注重生命周期的方式存储和管理界面相关的数据
LiveData可观察到数据存储器类,在发生改变时自动更新UI

Android KTX

Android KTX分为多个模块,每个模块都含有一个或多个软件包。Android KTX包含一个核心模块,这个模块为通用框架提供Kotlin扩展程序。如果要在项目中使用核心模块,需要在build.gradle中声明以下依赖:

dependencies {
    implementation "androidx.core:core-ktx:1.3.0"
}

Android KTX中还含有下面几个模块,它们的依赖声明如下:

dependencies {
    // Collection KTX
    implementation "androidx.collection:collection-ktx:1.1.0"
    // Fragment KTX
    implementation "androidx.fragment:fragment-ktx:1.2.5"
    // Lifecycle KTX
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    // LiveData KTX
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    // Room KTX
    implementation "androidx.room:room-ktx:2.2.5"
    // SQLite KTX
    implementation "androidx.sqlite:sqlite-ktx:2.1.0"
    // ViewModel KTX
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    // WorkManager KTX
    implementation "androidx.work:work-runtime-ktx:2.3.4"
    // Navigation KTX
    implementation "androidx.navigation:navigation-runtime-ktx:2.3.0-rc01"
    implementation "androidx.navigation:navigation-fragment-ktx:2.3.0-rc01"
    implementation "androidx.navigation:navigation-ui-ktx:2.3.0-rc01"
    // Palette KTX
    implementation "androidx.palette:palette-ktx:1.0.0"
    // Reactive KTX
    implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:2.2.0"
}

ViewModel

当程序退出或者屏幕发生旋转导致软件方向发生变化时,ActivityFragment都会被销毁或者重新创建,此时存储在其中的任何临时性页面相关的数据都会丢失,虽然可以通过Bundle对此类数据进行存储,但是这种方法仅仅适合可以序列化再反序列化的少量数据,而不适合数量较大的数据。此外,Activity或者Fragment常常需要异步调用(如先需要联网下载资源后再显示在页面中),这些调用可能需要一些时间才能返回结果。ActivityFragment如果还负责加载数据,对用户操作进行响应或处理系统通信,会使得类越发膨胀,导致后续的维护及其困难,所以有必要将界面与数据控制分离。

使用ViewModel与LiveData等具有生命周期功能等组件时,可以直接在build.gradle中添加以下依赖:

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// 例子中还使用了协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

ViewModel类主要负责为界面准备数据,在界面发生重新创建时会自动保留ViewModel对象,以便它们存储的数据立即可供下一个ActivityFragment使用。ViewModel创建如下所示:

data class User(val name: String = "")

class UserViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }
    fun getUserList() = users
    private fun loadUsers() {
        // 加载用户
    }
}

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      
        val model: UserViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
        // 或者用 val model: UserViewModel by viewModels() 这是activity-ktx的Kotlin属性委派
        model.getUserList().observe(this, Observer { users ->
            // 更新界面
            textView.text = "${it[0].name} - ${it[1].name} - ${it[2].name}"
        })
    }
}

需要注意的是ViewModel中不能引用视图、Lifecycle或可能存储对Activity上下文的引用的任何类,这是因为ViewModel的生命周期比试图或者LifecycleOwners(在这个例子中是我们传给ViewModelProvides.of()函数的对象)更长,所以一旦含有视图等等引用,可能会导致内存泄露。ViewModel生命周期如下:

ViewModel生命周期

上面的UserViewModel中并没有构造参数,如果我们想要为ViewModel的构造传入参数的话,可以实现我们自己的Factory,并将其作为参数传入到ViewModelProviders.of()中就可以,对类进行以下改造:

class UserViewModel(private val usersCache: List<User>) : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }
    fun getUserList() = users
    private fun loadUsers() {
        // 模拟加载用户
        GlobalScope.launch(Dispatchers.Main) {
            withContext(Dispatchers.IO) {
                delay(1000L)
            }
            users.value = listOf(User("a"), User("b"), User("c"))
        }
    }
}
// 实现自己的ViewModelFactory
class UserViewModelFactory(private val users: List<User>) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UserViewModel(users) as T
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val cache = listOf(User("x"), User("y"), User("z"))
        val model: UserViewModel = ViewModelProviders.of(this, UserViewModelFactory(cache)).get(UserViewModel::class.java)
        model.getUserList().observe(this, Observer {
            textView.text = "${it[0].name} - ${it[1].name} - ${it[2].name}"
        })
    }
}

这个例子的效果是界面首先显示x - y - z,经过一秒后变为a - b - c。

ViewModelProviders.of()在新版本中已被废弃,可以直接使用ViewModelProvider()获取。

如果ViewModel需要Application上下文,那么可以继承AndroidViewModel,它接收Application作为参数。

LiveData

LiveData是一种可观察的数据存储类,它和普通的可观察类的不同之处在于它拥有生命周期感知能力,这种能力可以确保LiveData仅更新处于活跃生命周期状态(即处于STARTEDRESUMED状态)的应用组件观察者。

使用LiveData的步骤如下:

  1. 创建LiveData实例来存储某种数据,通常放在ViewModel中;
  2. 创建可定义onChanged()方法的Observer对象,该方法会在LiveData对象存储的数据发生变化时被调用,通过是在Activity中或者Fragment中创建Observer对象;
  3. 使用LiveData.observe()方法将第二步创建的Observer对象附加到LiveData对象上,并传入一个LifecycleOwner(通常是ActivityFragment);

当更新存储在LiveData中的数据时,它会自动触发所有已经注册的观察者(只要这个观察者处于活跃状态)进行更新。可以看一下上面ViewModel中的例子。

大部分情况下,在组件的onCreate()方法中开始观察LiveData对象是比较正确的方法,这样做不仅可以确保系统不会从ActivityFragementonResume()方法进行冗余调用,还可以确保ActivityFragement变为活跃状态后具有可以立刻要显示的数据。

LiveData规范的用法如下:

class UserViewModel(userCache: List<User>) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    
    val users: LiveData<List<User>>
        get() = _users
    // 或者
    fun getUserList(): LiveData<List<User>> = _users
}

它与上面的例子不同之处在于ViewModel仅仅对外暴露了不可变的LiveData,而不是MutableLiveData,这样可以保证View层无法直接对LiveData进行修改,而只能通过ViewModel中的方法进行数据更新。

使用Transformations.map()函数可以将LiveData中存储的数据应用函数传到另外一个LiveData对象中,该函数返回LiveData中存储的数据类型:

val userLiveData: LiveData<List<User>> = UserLiveData()
val userNameLiveData: LiveData<List<String>> = Transformations.map(users) { userList ->
    val userNames = mutableListOf<String>()
    userList.forEach {
        userNames.add(it.name)
    }
    userNames
}

使用Transformations.switchMap()函数可以将LiveData对象解封并应用函数,该函数参数必须返回LiveData类型:

val userNameLiveData: LiveData<List<String>> = Transformations.switchMap(users) { userList ->
    val result = MutableLiveData<List<String>>()
    val userNames = mutableListOf<String>()
    userList.forEach {
        userNames.add(it.name)
    }
    result.value = userNames
    result
}

MediatorLiveDataLiveData的子类,允许合并多个LiveData源。只要任何原始的LiveData源对象发生更改,就会触发 MediatorLiveData 对象的观察者更新。上面的map()函数与switchMap()返回的实际上都是MediatorLiveData

从接口声明上来看我们似乎可以改写map使其功能和switchMap类似,这种思路在同步代码上是可行的,但是异步代码下用map代替switchMap会报错。下面是一个完整的例子,使用Retrofit2库进行网络请求并将结果显示在页面上:

依赖如下:

implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'

ViewModel类文件:

data class Update(val version: String = "1.0.0",
                  @SerializedName("download_url") val downloadUrl: String = "",
                  @SerializedName("update_content") val updateContent: String = "",
                  @SerializedName("have_update") val haveUpdate: String = "0")

class UpdateViewModel(cache: Update) : ViewModel(){
    private var currVersion: MutableLiveData<Update> = MutableLiveData(cache)
    var click = false

    val latestVersion: LiveData<Update> = Transformations.switchMap(currVersion) {
         if (click) {
             Repository.checkUpdate(it.version)
         } else {
             MutableLiveData(cache)
         }
    }

    fun getLatestVersion(version: Update) {
        click = true
        currVersion.value = version
    }
}

class UpdateViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return UpdateViewModel(Update()) as T
    }

}

object Repository {
    fun checkUpdate(version: String): LiveData<Update> {
        val liveData = MutableLiveData<Update>()
        GlobalScope.launch(Dispatchers.Main) {
            val deferred = async(Dispatchers.IO) {
                val retrofit = Retrofit.Builder()
                    .baseUrl("https://allpass.aengus.top/api/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                val appService = retrofit.create(AppService::class.java)
                appService.getAll(version).await()
            }
            liveData.value = deferred.await()
        }
        return liveData
}

页面文件:

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

        val model: UpdateViewModel = ViewModelProvider(this, UpdateViewModelFactory()).get(UpdateViewModel::class.java)
        model.latestVersion.observe(this, Observer {
            textView.text = "${it.version}"
        })

        get_button.setOnClickListener {
            model.getLatestVersion(Update("1.0.0"))
        }
    }
}

软件运行时会在页面显示“1.0.0”,点击按钮后变为“1.2.2”。

Room

Room是在SQLite的基础上的抽象层,Room使用起来非常像Mybatis,通常的使用方式包含创建数据类(与数据库表一一对应),创建DAO编写SQL语句,创建数据库管理DAO,创建Repository控制数据库。用法如下:

一、创建数据类。下面注解中的tableNamename都可以省略,若省略则默认和类名与属性名相同;

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "username") val username: String = "",
    @ColumnInfo(name = "password") val password: String = ""
)

二、创建DAO。

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>> // Room内有对LiveData的支持
    
    @Insert
    fun insert(user: User)
    
    @Insert
    fun insertAll(vararg users: User)
    
    @Update
    fun update(user: User)
    
    @Delete
    fun delete(user: User)
}

三、创建数据库类。version指定数据库版本,将来升级时基于此。

@Database(entities = [User::class], verson = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null
        
        fun getDatabase(context: Context, scope: CoroutineScope): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    name = "users_demo"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

四、创建Repository。

class UserRepository(private val userDao: UserDao) {
    val userList = userDao.getAllUsers()
    
    fun insert(user: User) = userDao.insert(user)
    
    fun update(user: User) = userDao.update(user)
    
    fun delete(user: User) = userDao.delete(user)
}

五、使用。下面的AndroidViewModel在前面说过。

class UserViewModel(application) : AndroidViewModel(application) {
    private val repository: UserRepository
    val usersLiveData: LiveData<User>
    
    init {
        val userDao = UserDatabase.getDatabase(application, viewModelScope).userDao()
        repository = UserRepository(userDao)
        userLiveData = repository.userList
    }
    
    fun insert(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.insert(user)
    }
    
    fun update(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.update(user)
    }
    
    fun delete(user: User) = viewModelScope.launch(Dispatchers.IO) {
        repository.delete(user)
    }
}

Room数据库升级的步骤是:首先需要更改数据库版本,然后定义一个Migration对象,并在databaseBuilder()后运行:

@Database(entities = [User::class], verson = 1)
abstract class UserDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    val MIGRATION_1_2: Migration = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE users ADD COLUMN age INT")
        }
    }
    
    companion object {
        @Volatile
        private var INSTANCE: UserDatabase? = null
        
        fun getDatabase(context: Context, scope: CoroutineScope): UserDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    UserDatabase::class.java,
                    name = "users_demo"
                )
                .addMigrations(MIGRATION_1_2)	// 在此使用Migration对象
                .build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

利用同样的方法可以定义从2-3,3-4,或者1-4的数据库版本迁移。

除了addMigrations(vararg migrations: Migration)方法,还有fallbackToDestructiveMigrationFrom(varargs startVersions: Int)fallbackToDestructiveMigration(),这两个函数允许Room当要求的数据库版本与实际的数据库版本不同时破坏性的重新创建数据库表,不同之处在于前者可以从指定版本号的数据库表进行迁移。

WorkManager

WorkManager可以用来管理即使在应用退出后或者设备重启时仍运行的任务,支持一次性或周期性任务,并可以添加网络可用性或充电状态等约束,遵循省电模式等功能。WorkManager不适合应用结束后进行安全终止(如应用数据保存)的后台工作,也不适合需要立刻执行的工作

要使用WorkManager,需要在build.gradle中添加如下依赖:

implementation "androidx.work:work-runtime:2.3.4"	// Java
implementation "androidx.work:work-runtime-ktx:2.3.4"  // Kotlin + coroutines

使用WorkManager,常常有如下几个步骤:

一、创建任务Worker,重写doWork()函数返回Result,失败使用Result.failure(),需要重试使用Result.retry()

class MyWorker(appContext: Context, workerParams: WorkerParams) : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 做一些工作
        return Result.success() // 任务成功
    }
}

二、配置运行任务的方式和时间。

val myWorkRequest = OneTimeWorkRquestBuilder<MyWorker>() // 一次性的
val anotherWorkRequest = PeriodicWorkRequest<MyWorker>() // 周期性的

PeriodicWorkRequest()会不断运行直到任务被取消,更精细的操作,需要使用PeriodicWorkRequestBuilder()可以定义的最短重复时间间隔为15分钟。示例如下:

val constraints = Constraints.Builder()
		.setRequiresCharging(true)	// 约束为充电状态
		.build()
// 一小时执行一次,但是必须在接通电源时运行
val saveRequest = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
		.setConstraints(constraints).build()

Contraints.Builder()还有以下方法:

// 当一个本地content(Uri)更新时任务是否需要运行
addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean)
// 任务运行是否要求设备处于特定网络模式
setRequiredNetworkType(networkType: NetworkType)
// 任务运行是否要求设备不处于低电量模式
setRequiredBatteryNotLow(requiresBatteryNotLow: Boolean)
// 任务运行是否要求设备处于空闲时
setRequiredDeviceIdle(requiresDeviceIdle: Boolean)
// 任务运行是否要求设备具有较多的存储空间
setRequiredStorageNotLow(requiresStorageNotLow: Boolean)
// 任务预定好时,content: Uri第一次发生改变后的延时
setTriggerContentMaxDelay(duration: Duration)
setTriggerContentMaxDelay(duration: Long, timeUnit: TimeUnit)
// 任务预定好时,content: Uri发生改变后的延时
setTriggerContentUpdateDelay(duration: Duration)
setTriggerContentUpdateDelay(duration: Long, timeUnit: TimeUnit)

一次性任务和周期性任务都可以使用setInitialDelay(duraiton: Long, timeUnit: TimeUnit)设置初始延迟(注意调用后还需要调用build()才能返回WorkRequest)。

可以用setBackoffCriteria(backoffPolicy: BackoffPolicy, backoffDelay: Long, timeUnit: TimeUnit)设置退避延迟政策,也就是两个任务冲突时如需要重试,那么在重试工作前需要等待的最短时间,退避政策有BackoffPolicy.EXPONENTIALBackoffPolicy.LINEAR,前者代表指数增长等待时间,后者代表线性增长等待时间;backoffDelay是等待时间延迟,对于一次性任务和周期性任务分别有它们的MIN_BACK_MILLIS=5*60*1000MAX_BACKOFF_MILLIS=5*60*60*1000

任务也可以有输入输出,输入输出都是以键值对的形式存储在Data对象中,示例如下:

val inputData = workDataof("key" to "value")
// 输入数据
val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
            .setInputData(inputData)
            .build()
// 获取输出数据
class MyWorker(appContext: Context, workerParams: WorkerParams) : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 做一些工作
        return Result.success(outputData) // 任务成功,获取输出数据
    
}

可以使用addTag(tag: String)WorkRequest添加标签,并使用WorkManager.cancelAllWorkByTag(tag: String)取消特定标签的任务。

三、将任务提交给系统。

WorkManager.getInstance(myContext).enqueue(myWorkRequest)