重学安卓之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);
}
主要组件
- Graph xml资源文件,包含所有页面和页面间的关系
- Controller 用于完成Graph中的页面切换
- NavHostFragment 特殊fragment容器
- 提供了NavigationUi控制各种bar的改变
操作步骤:
- 新建Android Resource File中的Graph文件,Resource type为Navigation,会在navigation目录下生成一个xml
- 将NavHostFragment添加到activity。``,作为其它fragment的容器,defaultNaveHost=true会自动处理返回键
1
2
3
4<fragment
android:name="android.navigation. fragment .NavHostFragment"
app:defaultNaveHost="true"
app:navGraph="@navigation/my_graph" /> - 创建新的destination,其实就是将fragment添加到navigation,navigation会有一个startDesination
- 建立action,在可视编辑里是将一个和另一个连起来,xml中为fragment含有一个action标签,指向另一个fragment。
- Navigation.findNavController(view).navigate(id)来完成导航
safe args传递参数
在fragment中添加参数
1 | // 添加插件 |
添加后,会生成对应的代码文件,就可以用Build创建对应的bundle了
NavigationUi
NavigationUI 提供了一些静态方法来处理 顶部应用栏 / 抽屉式导航栏 / 底部导航栏中 的界面导航, 统一管理 Fragment 页面切换相关的UI改变
CollapsingToolbarlayout、Toolbar、ActionBar
1 | // 1. 获取 NavController |
LifeCycle
LifecycleOwner和LifecycleObserver
实现LifecyclerObserver接口,将要在具体生命周期执行的方法使用@OnLifecycleEvent注解
1 | (Lifecycle.Event.ONRESUME) |
- LifecycleService,使用方式等于Service
- ProcessLifecycleOwner 整个应用的生命周期 使用方法ProcessLifecycleOwner.get().getLifecycle()
- LifecycleRegistry类,管理mObserverMap维护state和event
原理
通过在Activity中绑定一个空的fragment来实现监听Activity生命周期。
ViewModel
使用ViewModelProvider创建一个ViewModel
1 | public constructor( |
基本原理如下
- ViewModelProvider接收参数ViewModelStoreOwner,参数的对象实现方法getViewModelStore
- 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
20class 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 | object CustomBindingAdapters { |
1 | <ImageView |
ViewBinding
减少findviewbyid,简单举例一下
1 | // 使用 View Binding |
Room
1 |
|
数据库,使用起来和之前的注解框架差不多,可以配合LiveData和ViewModel实现数据的刷新。
1 | class UserRepository(private val userDao: UserDao) { |
Paging
Paging还是看官方文档更合适,网上搜的信息Paging2和Paging3内容混乱,这里我也不整理了,特别是使用RemoteMediator去同时接入 db + network 的方案。
paging库3.0变化较大,放个官方链接
官方DEMO
- PagingDataAdapter,首先RecyclerView的Adapter需要继承这个类,此外,您也可以使用随附的 AsyncPagingDataDiffer 组件构建自己的自定义适配器
- Pager 基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中公开的 PagingData 实例。
- PagingSource,数据载入,有一个load方法需要实现
- PagingData 用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果。
- 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。
BoundaryCallback弃用了,使用RemoteMediator。在分页数据耗尽时,Paging 库会触发 RemoteMediator 以从网络源加载新数据。RemoteMediator 会将新数据存储在本地数据库中,因此无需在 ViewModel 中使用内存缓存。最后,PagingSource 会使自身失效,而 Pager 会创建一个新实例以从数据库中加载新数据。
1 |
|
WorkManager
最低兼容api 14,在api 23以上使用JobScheduler,在api 23以下使用AlarmManager
- 创建Worker
1
2
3
4
5
6
7
8
9
10
11class MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
// 后台任务逻辑
Timber.d("Performing long running task in MyWorker")
// 返回任务执行结果
return Result.success()
}
} - 设置约束条件
1
2
3
4
5
6
7import androidx.work.Constraints
import androidx.work.NetworkType
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.build() - 创建请求
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() - 加入队列WorkManager还支持链式任务,观察任务状态,传递参数。
1
2
3
4
5// 将一次性工作请求加入队列
WorkManager.getInstance(context).enqueue(oneTimeRequest)
// 将周期性工作请求加入队列
WorkManager.getInstance(context).enqueue(periodicRequest)
- 链式: 在加入队列时使用beginWith(oneTimeRequest).then(periodicRequest),还可以combine…then…
- 观察任务状态: WorkManager.getWorkInfosXXX,可以通过tag,id,worker对象查看任务状态,还能获取对应的LiveData,通过LiveData就能在任务状态变化时收到通知
- 传参: Data类
Hilt
1 | // 在应用模块的 build.gradle 文件中 |
- 在 Application 类中启用 Hilt: 在你的 Application 类中使用 @HiltAndroidApp 注解启用 Hilt。
1
2@HiltAndroidApp
class MyApplication : Application() - 配置依赖注入: 使用 @Inject 注解标记需要注入的依赖,同时使用 @HiltViewModel 注解标记需要注入的 ViewModel。
1
2
3
4
5
6
7
8class MyRepository @Inject constructor() {
// ...
}
@HiltViewModel
class MyViewModel @Inject constructor(private val repository: MyRepository) : ViewModel() {
// ...
} - 在 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
// ...
} - 在 Module 中配置依赖: 如果需要自定义依赖注入的配置,可以创建一个 Dagger Module,并在其上使用 @InstallIn 注解指定作用范围。
1 | @Module |
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 | val userPreferencesFlow: Flow<UserPreferences> = dataStore.data |
Proto DataStore
- 区别于之前的Preferences DataStore的读取方式.map{ preferences-> { } }
- 写入方式使用updateData(){ preferences-> {…}}
生成pb对象和类创建UserPreferencesSerializer1
2
3
4
5
6
7
8
9syntax = "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;
}创建DataStore1
2
3
4
5
6
7
8
9
10
11
12object 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)
}读取数据1
2
3
4
5
6
7
8private 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
12private 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
5suspend fun updateShowCompleted(completed: Boolean) {
dataStore.updateData { preferences ->
preferences.toBuilder().setShowCompleted(completed).build()
}
}