更多文章可以訪問我的博客Aengus | Blog
Jetpack是Google官方推出的一套Android庫,它幫助開發(fā)者更方便、更快速的開發(fā)出穩(wěn)健性極佳的軟件例隆,簡化開發(fā)流程與提高效率愈诚。Jetpack庫常用的如下幾個組件,它們都可以單獨使用或者組合使用:
組建名稱 | 介紹 |
---|---|
Android KTX | Kotlin擴展程序床未,包括擴展函數(shù)、擴展屬性振坚、協(xié)程等 |
AppCompat | 提供向后兼容性的Android組件 |
WorkManager | 管理應用退出或設備重啟時仍應運行等可延遲異步任務 |
Room | Sqlite數(shù)據(jù)庫持久化抽象層 |
ViewModel | 以注重生命周期的方式存儲和管理界面相關的數(shù)據(jù) |
LiveData | 可觀察到數(shù)據(jù)存儲器類薇搁,在發(fā)生改變時自動更新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
當程序退出或者屏幕發(fā)生旋轉導致軟件方向發(fā)生變化時,Activity
或Fragment
都會被銷毀或者重新創(chuàng)建逮壁,此時存儲在其中的任何臨時性頁面相關的數(shù)據(jù)都會丟失孵坚,雖然可以通過Bundle
對此類數(shù)據(jù)進行存儲,但是這種方法僅僅適合可以序列化再反序列化的少量數(shù)據(jù)窥淆,而不適合數(shù)量較大的數(shù)據(jù)卖宠。此外,Activity
或者Fragment
常常需要異步調用(如先需要聯(lián)網(wǎng)下載資源后再顯示在頁面中)祖乳,這些調用可能需要一些時間才能返回結果逗堵。Activity
或Fragment
如果還負責加載數(shù)據(jù),對用戶操作進行響應或處理系統(tǒng)通信眷昆,會使得類越發(fā)膨脹蜒秤,導致后續(xù)的維護及其困難汁咏,所以有必要將界面與數(shù)據(jù)控制分離。
使用ViewModel與LiveData等具有生命周期功能等組件時作媚,可以直接在build.gradle
中添加以下依賴:
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// 例子中還使用了協(xié)程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
ViewModel類主要負責為界面準備數(shù)據(jù)攘滩,在界面發(fā)生重新創(chuàng)建時會自動保留ViewModel對象,以便它們存儲的數(shù)據(jù)立即可供下一個Activity
或Fragment
使用纸泡。ViewModel創(chuàng)建如下所示:
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()
函數(shù)的對象)更長女揭,所以一旦含有視圖等等引用蚤假,可能會導致內存泄露。ViewModel生命周期如下:
上面的UserViewModel
中并沒有構造參數(shù)吧兔,如果我們想要為ViewModel的構造傳入?yún)?shù)的話磷仰,可以實現(xiàn)我們自己的Factory
,并將其作為參數(shù)傳入到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"))
}
}
}
// 實現(xiàn)自己的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扭弧,經過一秒后變?yōu)閍 - b - c幻碱。
ViewModelProviders.of()
在新版本中已被廢棄唆迁,可以直接使用ViewModelProvider()
獲取葵硕。
如果ViewModel需要Application
上下文,那么可以繼承AndroidViewModel
吴藻,它接收Application
作為參數(shù)瞒爬。
LiveData
LiveData是一種可觀察的數(shù)據(jù)存儲類,它和普通的可觀察類的不同之處在于它擁有生命周期感知能力调缨,這種能力可以確保LiveData僅更新處于活躍生命周期狀態(tài)(即處于STARTED
或RESUMED
狀態(tài))的應用組件觀察者疮鲫。
使用LiveData的步驟如下:
- 創(chuàng)建
LiveData
實例來存儲某種數(shù)據(jù)吆你,通常放在ViewModel
中弦叶; - 創(chuàng)建可定義
onChanged()
方法的Observer
對象,該方法會在LiveData
對象存儲的數(shù)據(jù)發(fā)生變化時被調用妇多,通過是在Activity
中或者Fragment
中創(chuàng)建Observer
對象伤哺; - 使用
LiveData.observe()
方法將第二步創(chuàng)建的Observer
對象附加到LiveData
對象上,并傳入一個LifecycleOwner
(通常是Activity
或Fragment
)者祖;
當更新存儲在LiveData
中的數(shù)據(jù)時立莉,它會自動觸發(fā)所有已經注冊的觀察者(只要這個觀察者處于活躍狀態(tài))進行更新∑呶剩可以看一下上面ViewModel中的例子蜓耻。
大部分情況下,在組件的onCreate()
方法中開始觀察LiveData
對象是比較正確的方法械巡,這樣做不僅可以確保系統(tǒng)不會從Activity
或Fragement
的onResume()
方法進行冗余調用刹淌,還可以確保Activity
或Fragement
變?yōu)榛钴S狀態(tài)后具有可以立刻要顯示的數(shù)據(jù)饶氏。
LiveData規(guī)范的用法如下:
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
中的方法進行數(shù)據(jù)更新。
使用Transformations.map()
函數(shù)可以將LiveData
中存儲的數(shù)據(jù)應用函數(shù)傳到另外一個LiveData
對象中蔼卡,該函數(shù)返回LiveData
中存儲的數(shù)據(jù)類型:
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()
函數(shù)可以將LiveData
對象解封并應用函數(shù)喊崖,該函數(shù)參數(shù)必須返回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
}
MediatorLiveData
是LiveData
的子類,允許合并多個LiveData源雇逞。只要任何原始的LiveData源對象發(fā)生更改荤懂,就會觸發(fā) MediatorLiveData
對象的觀察者更新。上面的map()
函數(shù)與switchMap()
返回的實際上都是MediatorLiveData
塘砸。
從接口聲明上來看我們似乎可以改寫map
使其功能和switchMap
類似势誊,這種思路在同步代碼上是可行的,但是異步代碼下用map
代替switchMap
會報錯谣蠢。下面是一個完整的例子粟耻,使用Retrofit2庫進行網(wǎng)絡請求并將結果顯示在頁面上:
依賴如下:
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”,點擊按鈕后變?yōu)椤?.2.2”眉踱。
Room
Room是在SQLite的基礎上的抽象層挤忙,Room使用起來非常像Mybatis,通常的使用方式包含創(chuàng)建數(shù)據(jù)類(與數(shù)據(jù)庫表一一對應)谈喳,創(chuàng)建DAO編寫SQL語句册烈,創(chuàng)建數(shù)據(jù)庫管理DAO,創(chuàng)建Repository控制數(shù)據(jù)庫婿禽。用法如下:
一赏僧、創(chuàng)建數(shù)據(jù)類。下面注解中的tableName
和name
都可以省略扭倾,若省略則默認和類名與屬性名相同淀零;
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "username") val username: String = "",
@ColumnInfo(name = "password") val password: String = ""
)
二、創(chuàng)建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)
}
三驾中、創(chuàng)建數(shù)據(jù)庫類。version
指定數(shù)據(jù)庫版本模聋,將來升級時基于此肩民。
@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
}
}
}
}
四、創(chuàng)建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數(shù)據(jù)庫升級的步驟是:首先需要更改數(shù)據(jù)庫版本工窍,然后定義一個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的數(shù)據(jù)庫版本遷移究珊。
除了addMigrations(vararg migrations: Migration)
方法,還有fallbackToDestructiveMigrationFrom(varargs startVersions: Int)
與fallbackToDestructiveMigration()
纵苛,這兩個函數(shù)允許Room當要求的數(shù)據(jù)庫版本與實際的數(shù)據(jù)庫版本不同時破壞性的重新創(chuàng)建數(shù)據(jù)庫表剿涮,不同之處在于前者可以從指定版本號的數(shù)據(jù)庫表進行遷移。
WorkManager
WorkManager可以用來管理即使在應用退出后或者設備重啟時仍運行的任務攻人,支持一次性或周期性任務取试,并可以添加網(wǎng)絡可用性或充電狀態(tài)等約束,遵循省電模式等功能怀吻。WorkManager不適合應用結束后進行安全終止(如應用數(shù)據(jù)保存)的后臺工作瞬浓,也不適合需要立刻執(zhí)行的工作。
要使用WorkManager蓬坡,需要在build.gradle
中添加如下依賴:
implementation "androidx.work:work-runtime:2.3.4" // Java
implementation "androidx.work:work-runtime-ktx:2.3.4" // Kotlin + coroutines
使用WorkManager猿棉,常常有如下幾個步驟:
一、創(chuàng)建任務Worker
屑咳,重寫doWork()
函數(shù)返回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) // 約束為充電狀態(tài)
.build()
// 一小時執(zhí)行一次化焕,但是必須在接通電源時運行
val saveRequest = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
.setConstraints(constraints).build()
Contraints.Builder()
還有以下方法:
// 當一個本地content(Uri)更新時任務是否需要運行
addContentUriTrigger(uri: Uri, triggerForDescendants: Boolean)
// 任務運行是否要求設備處于特定網(wǎng)絡模式
setRequiredNetworkType(networkType: NetworkType)
// 任務運行是否要求設備不處于低電量模式
setRequiredBatteryNotLow(requiresBatteryNotLow: Boolean)
// 任務運行是否要求設備處于空閑時
setRequiredDeviceIdle(requiresDeviceIdle: Boolean)
// 任務運行是否要求設備具有較多的存儲空間
setRequiredStorageNotLow(requiresStorageNotLow: Boolean)
// 任務預定好時,content: Uri第一次發(fā)生改變后的延時
setTriggerContentMaxDelay(duration: Duration)
setTriggerContentMaxDelay(duration: Long, timeUnit: TimeUnit)
// 任務預定好時计寇,content: Uri發(fā)生改變后的延時
setTriggerContentUpdateDelay(duration: Duration)
setTriggerContentUpdateDelay(duration: Long, timeUnit: TimeUnit)
一次性任務和周期性任務都可以使用setInitialDelay(duraiton: Long, timeUnit: TimeUnit)
設置初始延遲(注意調用后還需要調用build()
才能返回WorkRequest
)锣杂。
可以用setBackoffCriteria(backoffPolicy: BackoffPolicy, backoffDelay: Long, timeUnit: TimeUnit)
設置退避延遲政策,也就是兩個任務沖突時如需要重試番宁,那么在重試工作前需要等待的最短時間,退避政策有BackoffPolicy.EXPONENTIAL
與BackoffPolicy.LINEAR
赖阻,前者代表指數(shù)增長等待時間蝶押,后者代表線性增長等待時間;backoffDelay
是等待時間延遲火欧,對于一次性任務和周期性任務分別有它們的MIN_BACK_MILLIS=5*60*1000
和MAX_BACKOFF_MILLIS=5*60*60*1000
棋电。
任務也可以有輸入輸出茎截,輸入輸出都是以鍵值對的形式存儲在Data
對象中,示例如下:
val inputData = workDataof("key" to "value")
// 輸入數(shù)據(jù)
val myWorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setInputData(inputData)
.build()
// 獲取輸出數(shù)據(jù)
class MyWorker(appContext: Context, workerParams: WorkerParams) : Worker(appContext, workerParams) {
override fun doWork(): Result {
// 做一些工作
return Result.success(outputData) // 任務成功赶盔,獲取輸出數(shù)據(jù)
}
}
可以使用addTag(tag: String)
為WorkRequest
添加標簽企锌,并使用WorkManager.cancelAllWorkByTag(tag: String)
取消特定標簽的任務。
三于未、將任務提交給系統(tǒng)撕攒。
WorkManager.getInstance(myContext).enqueue(myWorkRequest)