重学安卓之Jetpack

导读

jetpack当时还没来得及用,整个项目就被砍掉了,这两年一直没有接触,最近好多岗位都要求会jetpack,这两年逐渐用的人越来越多,重学Android赶紧简单了解一下,做了一点整理,都是皮毛,供自己查笔记用吧,如果有一些知识点需要增加备注,随时修改。

Navigation 用于fragment切换

优点

  • 可视化,可以as中可视化编辑
  • 通过destination和action完成页面的导航
  • 参数传递安全, safe args
  • 支持deeplink,支持创建PendingIntent
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 返回一个 PendingIntent
    */
    private PendingIntent getPendingIntent() {

    return Navigation.findNavController(requireActivity(), R.id.button)
    .createDeepLink()
    .setGraph(R.navigation.my_nav_graph)
    .setDestination(R.id.detailFragment)
    .createTaskStackBuilder()
    .getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE);

    }

主要组件

  1. Graph xml资源文件,包含所有页面和页面间的关系
  2. Controller 用于完成Graph中的页面切换
  3. NavHostFragment 特殊fragment容器
  4. 提供了NavigationUi控制各种bar的改变

操作步骤:

  1. 新建Android Resource File中的Graph文件,Resource type为Navigation,会在navigation目录下生成一个xml
  2. 将NavHostFragment添加到activity。``,作为其它fragment的容器,defaultNaveHost=true会自动处理返回键
    1
    2
    3
    4
    <fragment 
    android:name="android.navigation. fragment .NavHostFragment"
    app:defaultNaveHost="true"
    app:navGraph="@navigation/my_graph" />
  3. 创建新的destination,其实就是将fragment添加到navigation,navigation会有一个startDesination
  4. 建立action,在可视编辑里是将一个和另一个连起来,xml中为fragment含有一个action标签,指向另一个fragment。
  5. Navigation.findNavController(view).navigate(id)来完成导航

safe args传递参数

在fragment中添加参数

1
2
3
4
5
6
7
8
9
10
// 添加插件
apply plugin: 'androidx.navigation.safeargs'

<fragment>
<action/>
<argument
android:name="name"
app:argType="string"
android:defaultValue='"lefo"'/>

添加后,会生成对应的代码文件,就可以用Build创建对应的bundle了

NavigationUI 提供了一些静态方法来处理 顶部应用栏 / 抽屉式导航栏 / 底部导航栏中 的界面导航, 统一管理 Fragment 页面切换相关的UI改变
CollapsingToolbarlayout、Toolbar、ActionBar

1
2
3
4
5
6
// 1. 获取 NavController
navController = Navigation.findNavController(this, R.id.fragment)

// 2. 创建 AppBarConfiguration
appBarConfiguration = AppBarConfiguration.Builder(navController.graph).build();
NavigationUI.setupActionBarWithNavController(this,navController,appBarConfiguration);

LifeCycle

LifecycleOwner和LifecycleObserver

实现LifecyclerObserver接口,将要在具体生命周期执行的方法使用@OnLifecycleEvent注解

1
2
3
4
5
6
7
@OnLifecycleEvent (Lifecycle.Event.ONRESUME) 
private void onResumeEvent () {
Log.d (TAG, "onResumeEvent() ");
}
// ...
//在Activity中注册观察者
getLifecycle().addObserver(myObserver);
  • LifecycleService,使用方式等于Service
  • ProcessLifecycleOwner 整个应用的生命周期 使用方法ProcessLifecycleOwner.get().getLifecycle()
  • LifecycleRegistry类,管理mObserverMap维护state和event

原理

通过在Activity中绑定一个空的fragment来实现监听Activity生命周期。

ViewModel

使用ViewModelProvider创建一个ViewModel

1
2
3
public constructor(
owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

基本原理如下

  1. ViewModelProvider接收参数ViewModelStoreOwner,参数的对象实现方法getViewModelStore
  2. ViewModelStore提供一个HashMap<String,ViewModel>

LiveData

LiveData的作用是ViewModel在数据发生变化时通知页面

  • 实际上就是一个观察者模式的数据容器,当数据改变时,通知UI刷新;
  • 能感知activity fragment组件生命周期,
  • 通常和viewModel一起使用,LiveData作为ViewModel的一个成员变量。
  • liveData.observe()注册一个对数据的观察
  • observeForever使用后一定要用removeObserver方法停止观察

DataBinding

基础用法

  • DataBinding结合ViewModel是常用的做法
  • 向include的二级页面传递数据
  • BaseObservable和ObservableField支持多个控件互相绑定数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!-- activity_main.xml -->
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
    <variable
    name="user"
    type="com.example.User" />
    </data>

    <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name}" />

    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click"
    android:onClick="@{() -> user.onButtonClick()}" />

    </RelativeLayout>
    </layout>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // 使用 DataBindingUtil 设置布局文件和数据绑定
    val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    // 创建 User 对象
    val user = User("John Doe")

    // 将 User 对象绑定到布局中的 user 变量
    binding.user = user

    // 设置点击事件
    binding.button.setOnClickListener {
    user.onButtonClick()
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <include 
    layout="@layout/layout2"
    app:user="@{user}"/>

    <!--在2级页面这么写-->
    <data>
    <variable
    name="user"
    type="com.example.User" />

自定义BindAdapter

还可以增加一个参数作为旧值,但此时必须先写旧值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
object CustomBindingAdapters {

@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(imageView: ImageView, url: String?) {
// 使用 Glide 加载图片
Glide.with(imageView.context)
.load(url)
.into(imageView)
}

@BindingAdapter("visibility")
@JvmStatic
fun setVisibility(view: View, isVisible: Boolean) {
// 设置 View 的可见性
view.visibility = if (isVisible) View.VISIBLE else View.GONE
}
}

1
2
3
4
5
6
7
8
9
10
11
12
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{viewModel.imageUrl}" />

<View
android:id="@+id/customView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:visibility="@{viewModel.isVisible}" />

ViewBinding

减少findviewbyid,简单举例一下

1
2
3
// 使用 View Binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

Room

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity(tableName = "user_table")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String
)

// Dao
@Dao
interface UserDao {
@Query("SELECT * FROM user_table")
suspend fun getAllUsers(): List<User>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
}

// @Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

数据库,使用起来和之前的注解框架差不多,可以配合LiveData和ViewModel实现数据的刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class UserRepository(private val userDao: UserDao) {
suspend fun getUsersFromNetwork(): List<User> {
// 从网络获取数据的逻辑
val networkData = // ...

// 将数据存储到数据库
userDao.insertUser(networkData)

return networkData
}

suspend fun getAllUsers(): List<User> {
// 从数据库获取数据的逻辑
return userDao.getAllUsers()
}
}

// ViewModel中使用Repository
class MyViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> get() = _users

fun fetchData() {
viewModelScope.launch {
// 从网络获取数据并更新数据库
userRepository.getUsersFromNetwork()

// 从数据库获取数据并更新 LiveData
_users.value = userRepository.getAllUsers()
}
}
}

Paging

Paging还是看官方文档更合适,网上搜的信息Paging2和Paging3内容混乱,这里我也不整理了,特别是使用RemoteMediator去同时接入 db + network 的方案。
paging库3.0变化较大,放个官方链接
官方DEMO

  1. PagingDataAdapter,首先RecyclerView的Adapter需要继承这个类,此外,您也可以使用随附的 AsyncPagingDataDiffer 组件构建自己的自定义适配器
  2. Pager 基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中公开的 PagingData 实例。
  3. PagingSource,数据载入,有一个load方法需要实现
  4. PagingData 用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果。
  5. RemoteMediator 处理分层数据源。

Paging 2 中的 PageKeyedDataSource、PositionalDataSource 和 ItemKeyedDataSource 都合并到了 Paging 3 中的 PagingSource API 中。所有旧版 API 类中的加载方法都合并到了 PagingSource 中的单个 load() 方法中。这样可以减少代码重复,因为在旧版 API 类的实现中,各种加载方法之间的很多逻辑通常是相同的。
在 Paging 3 中,所有加载方法参数被一个 LoadParams 密封类替代,该类中包含了每个加载类型所对应的子类。如果需要区分 load() 方法中的加载类型,请检查传入了 LoadParams 的哪个子类:LoadParams.Refresh、LoadParams.Prepend 还是 LoadParams.Append。

  1. BoundaryCallback弃用了,使用RemoteMediator。在分页数据耗尽时,Paging 库会触发 RemoteMediator 以从网络源加载新数据。RemoteMediator 会将新数据存储在本地数据库中,因此无需在 ViewModel 中使用内存缓存。最后,PagingSource 会使自身失效,而 Pager 会创建一个新实例以从数据库中加载新数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
private val query: String,
private val database: RoomDb,
private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
val userDao = database.userDao()

override suspend fun load(
loadType: LoadType,
state: PagingState<Int, User>
): MediatorResult {
// ...
}
}

WorkManager

最低兼容api 14,在api 23以上使用JobScheduler,在api 23以下使用AlarmManager

  1. 创建Worker
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    override fun doWork(): Result {
    // 后台任务逻辑
    Timber.d("Performing long running task in MyWorker")

    // 返回任务执行结果
    return Result.success()
    }
    }

  2. 设置约束条件
    1
    2
    3
    4
    5
    6
    7
    import androidx.work.Constraints
    import androidx.work.NetworkType

    val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresCharging(true)
    .build()
  3. 创建请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 一次性工作请求
    val oneTimeRequest = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .setConstraints(constraints)
    .build()

    // 周期性工作请求
    val periodicRequest = PeriodicWorkRequest.Builder(MyWorker::class.java, 24, TimeUnit.HOURS)
    .setConstraints(constraints)
    .build()
  4. 加入队列
    1
    2
    3
    4
    5
    // 将一次性工作请求加入队列
    WorkManager.getInstance(context).enqueue(oneTimeRequest)

    // 将周期性工作请求加入队列
    WorkManager.getInstance(context).enqueue(periodicRequest)
    WorkManager还支持链式任务,观察任务状态,传递参数。
  • 链式: 在加入队列时使用beginWith(oneTimeRequest).then(periodicRequest),还可以combine…then…
  • 观察任务状态: WorkManager.getWorkInfosXXX,可以通过tag,id,worker对象查看任务状态,还能获取对应的LiveData,通过LiveData就能在任务状态变化时收到通知
  • 传参: Data类

Hilt

1
2
3
4
5
6
7
// 在应用模块的 build.gradle 文件中
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-android-compiler:2.38.1"

// 如果使用了ViewModel,还需要添加以下依赖
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0-alpha03"
  1. 在 Application 类中启用 Hilt: 在你的 Application 类中使用 @HiltAndroidApp 注解启用 Hilt。
    1
    2
    @HiltAndroidApp
    class MyApplication : Application()
  2. 配置依赖注入: 使用 @Inject 注解标记需要注入的依赖,同时使用 @HiltViewModel 注解标记需要注入的 ViewModel。
    1
    2
    3
    4
    5
    6
    7
    8
    class MyRepository @Inject constructor() {
    // ...
    }

    @HiltViewModel
    class MyViewModel @Inject constructor(private val repository: MyRepository) : ViewModel() {
    // ...
    }
  3. 在 Activity 或 Fragment 中使用 Hilt 注入: 在需要注入依赖的 Activity 或 Fragment 中,使用 @AndroidEntryPoint 注解启用 Hilt,并使用 @Inject 注解标记需要注入的字段。
    1
    2
    3
    4
    5
    6
    7
    8
    @AndroidEntryPoint
    class MyActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel

    // ...
    }
  4. 在 Module 中配置依赖: 如果需要自定义依赖注入的配置,可以创建一个 Dagger Module,并在其上使用 @InstallIn 注解指定作用范围。
1
2
3
4
5
6
7
8
9
@Module
@InstallIn(ApplicationComponent::class)
object MyModule {

@Provides
fun provideMyRepository(): MyRepository {
return MyRepository()
}
}

DataStore

  • 用于存储较小的数据集,分为Preferences DataStore和Proto DataStore,可以代替SP,可以轻松迁移数据。
  • 官方介绍

Preferences DataStore

  • 读取用flow
  • 写入用edit
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 创建dataStore context.createDataStore
    // 使用委托创建dataStore
    private const val USER_PREFERENCES_NAME = "user_preferences"

    private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
    // Since we're migrating from SharedPreferences, add a migration based on the
    // SharedPreferences name
    listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
    )

当 DataStore 从文件读取数据时,如果读取数据期间出现错误,系统会抛出 IOExceptions。我们可以通过以下方式处理这些事务:在 map() 之前使用 catch() Flow 运算符,并且在抛出的异常是 IOException 时发出 emptyPreferences()。如果出现其他类型的异常,最好重新抛出该异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
UserPreferences(showCompleted)
}

suspend fun updateShowCompleted(showCompleted: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
}
}

// 可以使用扩展函数asLiveData转换flow为LiveData
userPreferencesFlow.asLiveData().observe(viewLifecycleOwner,{ })

Proto DataStore

  1. 区别于之前的Preferences DataStore的读取方式.map{ preferences-> { } }
  2. 写入方式使用updateData(){ preferences-> {…}}
    生成pb对象和类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    syntax = "proto3";

    option java_package = "com.codelab.android.datastore";
    option java_multiple_files = true;

    message UserPreferences {
    // filter for showing / hiding completed tasks
    bool show_completed = 1;
    }
    创建UserPreferencesSerializer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences {
    try {
    return UserPreferences.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
    throw CorruptionException("Cannot read proto.", exception)
    }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
    }
    创建DataStore
    1
    2
    3
    4
    5
    6
    7
    8
    private const val USER_PREFERENCES_NAME = "user_preferences"
    private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
    private const val SORT_ORDER_KEY = "sort_order"

    private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
    )
    读取数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private val TAG: String = "UserPreferencesRepo"

    val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
    // dataStore.data throws an IOException when an error is encountered when reading data
    if (exception is IOException) {
    Log.e(TAG, "Error reading sort order preferences.", exception)
    emit(UserPreferences.getDefaultInstance())
    } else {
    throw exception
    }
    }
    写入数据
    1
    2
    3
    4
    5
    suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
    preferences.toBuilder().setShowCompleted(completed).build()
    }
    }