Android Jetpack

Google 在2018年推出了 Android Jetpack。Jetpack 是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助您提高工作效率。可全部使用,也可混合搭配!Jetpack 也包含了与平台 API 解除捆绑的 androidx.* 软件包库。

特点如下:

  • 加速开发:组件可以单独采用(不过这些组件是为协同工作而构建的),同时利用 Kotlin 语言功能帮助您提高工作效率。
  • 消除样板代码:Android Jetpack 可管理繁琐的 Activity(如后台任务、导航和生命周期管理),以便您可以专注于如何让自己的应用出类拔萃。
  • 构建高质量的强大应用:Android Jetpack 组件围绕现代化设计实践构建而成,具有向后兼容性,可以减少崩溃和内存泄漏。

JetPack的组成

WorkManager很强大,需要的地方可以替代以前的方案。LifeCycles也不错,扩展其他类具有关联生命周期的。还有Room数据库的框架,简单了很多。LiveData和ViewModel的结合基本上就是RxJava和RxAndroid的结合的功能了。

Navigation是一个可简化Android导航的库和插件

Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。

优点

  • 处理Fragment的切换(上文已说过)
  • 默认情况下正确处理Fragment的前进和后退
  • 为过渡和动画提供标准化的资源
  • 实现和处理深层连接
  • 可以绑定Toolbar、BottomNavigationView和ActionBar等
  • SafeArgs(Gradle插件) 数据传递时提供类型安全性
  • ViewModel支持

三要素

  1. Navigation Graph(New XML resource): 这是一个新的资源文件,用户在可视化界面可以看出他能够到达的Destination(用户能够到达的屏幕界面),以及流程关系。

  2. NavHostFragment(Layout XML view): 当前Fragment的容器

  3. NavController(Kotlin/Java object): 导航的控制者

可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。

重要属性

属性 解释
app:startDestination navigation标签: 默认的起始位置
app:destination action标签: 跳转完成到达的fragment的Id
app:popUpTo action标签: 将fragment从栈中弹出,直到某个Id的fragment
app:argType argument标签: 标签的类型
android:defaultValue argument标签: 默认值
app:navGraph fragment标签: 存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph
app:defaultNavHost=”true” fragment标签: 与系统的返回按钮相关联

导航示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
btnLogin.setOnClickListener {
// 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf")
findNavController().navigate(R.id.login, bundle,navOption)
}

如果不用Safe Args,action可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成

LiveData

LiveData有个内部类LifecycleBoundObserver,它实现了GenericLifecycleObserver,而GenericLifecycleObserver继承了LifecycleObserver接口。当组件(Fragment、Activity)生命周期变化时会通过onStateChanged()方法回调过来。

LiveData主要涉及到的时序有三个:
在Fragment/Activity中通过LiveData.observer()添加观察者(observer()方法中的第二个参数)。
根据Fragment/Activity生命周期发生变化时,移除观察者或者通知观察者更新数据。
当调用LiveData的setValue()、postValue()方法后,通知观察者更新数据(setValue必须发生在主线程,如果当前线程是子线程可以使用postValue)。

在LiveData.observe()方法中添加了组件(实现了LifecycleOwner接口的Fragment和Activity)生命周期观察者。而这个观察者就是LifecycleBoundObserver对象.

应用及知识点

  1. 使用ViewModel在同一个Activity中的Fragment之间共享数据:想要利用ViewModel实现Fragment之间数据共享,前提是Fragment中的FragmentActivity得相同。

  2. map是你将你的函数用于你传参的livedata的数据通过函数体中的逻辑改变,然后将结果传到下游。而switchmap,转换跟map差不多,只不过传到下游的是livedata类型

  3. MediatorLiveData 是 LiveData 的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。

  4. 使用LiveData共享数据:定义一个类然后继承LiveData,并使用单例模式即可。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 登录信息
    data class LoginInfo constructor(val account:String, val pwd:String, val email:String)

    /**
    * 自定义单例LiveData
    */
    class LoginLiveData:LiveData<LoginInfo>() {

    companion object {
    private lateinit var sInstance: LoginLiveData

    @MainThread
    fun get(): LoginLiveData {
    sInstance = if (::sInstance.isInitialized) sInstance else LoginLiveData()
    return sInstance
    }
    }
    }

注意:您可以使用 observeForever(Observer) 方法来注册未关联 LifecycleOwner 对象的观察者。在这种情况下,观察者会被视为始终处于活跃状态,因此它始终会收到关于修改的通知。您可以通过调用 removeObserver(Observer) 方法来移除这些观察者。

ViewModel

ViewModel类是被设计用来以可感知生命周期的方式存储和管理 UI 相关数据,ViewModel中数据会一直存活即使 activity configuration发生变化,比如横竖屏切换的时候。

由于 ViewModel 生命周期可能长与 activity 生命周期,所以为了避免内存泄漏 Google 禁止在 ViewModel 中持有 Context 或 activity 或 view 的引用。

viewmodel初始化:

ViewModelProviders.of(activity,factory).get(MyViewModel.class)

1、初始化了ViewModelProvider内部维护了 用于创建 VM 的 Factory,和用户存放 VM 的ViewModelStore
2、初始化了 用来生成 ViewModel 的 Factory(默认为DefaultFactory);
3、通过ViewModelStores的静态方法实例化了 HolderFragment,并实例化了ViewModelStore
4、然后是ViewModelProvider的 get 方法

Room

DataBinding

DataBinding 是google发布的一个数据绑定框架,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常

配置

app及对应module添加如下代码:

1
2
3
4
5
6
android {
...
dataBinding {
enabled = true
}
}

使用

1.activity中使用

1
2
3
4
5
6
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 给user初始化值
dataBinding.user = User("zhangsan", "12345")
}

2.在 Fragment 中的使用

1
2
3
4
5
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val blankFragmentBinding: BlankFragmentBinding =
DataBindingUtil.inflate(inflater, R.layout.blank_fragment, container, false)
return blankFragmentBinding.root
}

3.在RecyclerView中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder 	{
val itemMvvmBinding = DataBindingUtil.inflate<ViewDataBinding>(
LayoutInflater.from(parent.context), R.layout.item_mvvm, parent, false)
itemMvvmBinding.getRoot().setOnClickListener(this)
return RecyclerViewHolder(itemMvvmBinding)
}

override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val itemMvvmBinding = holder.getBinding()
val userBean = data.get(position)
itemMvvmBinding.setUser(userBean)
itemMvvmBinding.btnUpdate.setOnClickListener(OnBtnClickListener(1, userBean))
...
// 立刻执行绑定
itemMvvmBinding.executePendingBindings()
}

单向绑定

一个简单的ViewModel 类被更新后,并不会让 UI 自动更新。而数据绑定后,我们自然会希望数据变更后 UI 会即时刷新,Observable 就是为此而生的概念。实现数据变化自动驱动 UI 刷新的方式有三种:BaseObservable、ObservableField、ObservableCollection

BaseObservable

BaseObservable 提供了 notifyChange() 和 notifyPropertyChanged() 两个方法:

  • notifyChange() 它会刷新所有的值。
  • notifyPropertyChanged() 它只会根据对应的BR的flag更新,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图。

//由于kotlin的属性默认是public修饰,所以可以直接在属性上@Bindable, 如何设置了修饰符且不为public的话,则可使用@get Bindable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserInfo : BaseObservable() {
// 对name进行@Bindable标志,然后会生成BR.name
@Bindable
var name: String = ""
set(value) {
field = value
// 当name,发生改变时只会刷新与name相关控件的值,不会刷新其他的值
notifyPropertyChanged(BR.name)
}

@get: Bindable
var password: String = ""
set(value) {
field = value
// 当password 发生改变时,也会刷新其他属性相关的控件的值
notifyChange()
}
}

实现了 Observable 接口的类允许注册一个监听器OnPropertyChangedCallback,当可观察对象的属性更改时就会通知这个监听器。

1
2
3
4
5
6
//当中 propertyId 就用于标识特定的字段
user.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback(){
override fun onPropertyChanged(sender: Observable, propertyId: Int) {

}
})

ObservableField

继承于 Observable 类相对来说限制有点高,且也需要进行notify 操作,因此为了简单起见可以选择使用 ObservableField。 可以理解为官方对 BaseObservable 中字段的注解和刷新等操作的封装,官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通过 ObservableField 泛型来申明其他类型。

1
2
3
4
data class ObservableUser(
var name: ObservableField<String>,
var password: ObservableField<String>
)

ObservableCollection

dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新

双向数据绑定

双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据。绑定变量的方式比单向绑定多了一个等号,如:android:text=”@={user.name}”

事件绑定

严格意义上来说,事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已。

事件绑定包括方法引用和监听绑定:

  • 方法引用:参数类型和返回类型要一致,参考et_pwd EditText的android:onTextChanged引用。
  • 监听绑定:相比较于方法引用,监听绑定的要求就没那么高了,我们可以使用自行定义的函数,参考et_account EditText的android:onTextChanged引用。
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="utf-8"?>
<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>
<!--需要的viewModel,通过mBinding.vm=mViewMode注入-->
<variable
name="model"
type="com.joe.jetpackdemo.viewmodel.LoginModel"/>

<variable
name="activity"
type="androidx.fragment.app.FragmentActivity"/>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/txt_cancel"
android:onClick="@{()-> activity.onBackPressed()}"
/>

<TextView
android:id="@+id/txt_title"
app:layout_constraintTop_toTopOf="parent"
.../>

<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
android:onTextChanged="@{(text, start, before, count)->model.onNameChanged(text)}"
...
/>

<EditText
android:id="@+id/et_pwd"
android:text="@{model.p.get()}"
android:onTextChanged="@{model::onPwdChanged}"
...
/>

<Button
android:id="@+id/btn_login"
android:text="Sign in"
android:onClick="@{() -> model.login()}"
android:enabled="@{(model.p.get().isEmpty()||model.n.get().isEmpty()) ? false : true}"
.../>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

使用类方法

首先定义一个静态方法

1
2
3
4
5
6
object StringUtils {

fun toUpperCase( str:String):String {
return str.toUpperCase();
}
}

在 data 标签中导入该工具类

1
<import type="com.github.ixiaow.sample.StringUtils" />

然后就可以像对待一般的函数一样来调用了

1
2
3
4
5
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->userPresenter.onUserNameClick(userInfo)}"
android:text="@{StringUtils.toUpperCase(userInfo.name)}" />

表达式

  • 运算符 + - / * %
  • 字符串连接 +
  • 逻辑与或 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <= (Note that < needs to be escaped as <)
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • 方法调用
  • 域访问
  • 数组访问
  • 三元操作符

除了上述之外,Data Binding新增了空合并操作符??,例如android:text=”@{user.displayName ?? user.lastName}”,它等价于android:text=”@{user.displayName != null ? user.displayName : user.lastName}”。

@BindingMethod

如果XXXView类有成员变量borderColor,并且XXXView类有setBoderColor(int color)方法,那么在布局中我们就可以借助Data Binding直接使用app:borderColor这个属性。示例如下:

1
2
3
4
5
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

还用XXXView为例,它有成员变量borderColor,这次设置borderColor的方法是setBColor(总有程序员乱写方法名~),强行用app:borderColor显然是行不通的,可以这样用的前提是必须有setBoderColor(int color)方法,显然setBColor不匹配,但我们可以通过BindingMethods注解实现app:borderColor的使用,代码如下:

1
2
3
4
5
@BindingMethods(value = [
BindingMethod(
type = 包名.XXXView::class,
attribute = "app:borderColor",
method = "setBColor")])

@BindingAdapter

  • 用于标记修饰方法,方法必须为公共静态方法
  • 方法的第一个参数的类型必须为View类型,不然报错
  • 用来自定义view的任意属性

dataBinding 提供了 BindingAdapter 这个注解用于支持自定义属性,或者是修改原有属性。

示例如下:

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
@BindingAdapter({"url"})
public static void loadImage(ImageView view, String url) {
Log.d(TAG, "loadImage url : " + url);
}

//在 xml 文件中关联变量值,当中,bind 这个名称可以自定义
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<import type="com.github.ixiaow.databindingsample.model.Image" />
<variable
name="image"
type="Image" />
</data>

<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_background"
bind:url="@{image.url}" />
</android.support.constraint.ConstraintLayout>
</layout>

BindingAdapter 更为强大的一点是可以覆盖 Android 原先的控件属性。例如,可以设定每一个 Button 的文本都要加上后缀:“-Button”:

1
2
3
4
5
6
7
8
9
10
@BindingAdapter("android:text")
public static void setText(Button view, String text) {
view.setText(text + "-Button");
}

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->handler.onClick(image)}"
android:text='@{"改变图片Url"}'/>

@InverseBindingAdapter

  • 作用于方法,方法须为公共静态方法。
  • 方法的第一个参数必须为View类型,如TextView等
  • 用于双向绑定
  • 需要与@BindingAdapter配合使用

死循环绑定的解决方式:只处理新旧数据不一样的数据,参考源码中的例子:android.databinding.adapters.TextViewBindingAdapter。

需要注意的是,使用该语法必须要有反向绑定的方法,android原生view都是自带的,所以使用原生控件无须担心,但是自定义view的话需要我们通过InverseBindingAdapter注解类实现

@BindingConversion

dataBinding 还支持对数据进行转换,或者进行类型转换。作用于方法,被该注解标记的方法,被视为dataBinding的转换方法。方法必须为公共静态(public static)方法,且有且只能有1个参数。

与 BindingAdapter 类似,以下方法会将布局文件中所有以@{String}方式引用到的String类型变量加上后缀-conversionString

1
2
3
4
@BindingConversion
public static String conversionString(String text) {
return text + "-conversionString";
}

而 BindingConversion 的优先级要高些, 此外,BindingConversion 也可以用于转换属性值的类型:

1
2
3
4
5
6
7
8
9
10
11
@BindingConversion
public static Drawable convertStringToDrawable(String str) {
if (str.equals("红色")) {
return new ColorDrawable(Color.parseColor("#FF4081"));
}

if (str.equals("蓝色")) {
return new ColorDrawable(Color.parseColor("#3F51B5"));
}
return new ColorDrawable(Color.parseColor("#344567"));
}

其他

1.自定义生成的绑定类的类名

每个数据绑定布局文件都会生成一个绑定类,ViewDataBinding 的实例名是根据布局文件名来生成,采用驼峰命名法来命名,并省略布局文件名包含的下划线。控件的获取方式类似,但首字母小写。

通过如下方式自定义 ViewDataBinding 的实例名:

1
<data class="CustomBinding"></data>

2.alias 别名

如果存在 import 的类名相同的情况,可以使用 alias 指定别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="com.github.ixiaow.sample.model1.User" />
<import
alias="TempUser"
type="com.github.ixiaow.sample.model2.User" />
<variable
name="user"
type="User" />
<variable
name="tempUserInfo"
type="TempUser" />

<import type="java.util.List"/>
//<需要被替换成&lt;
<variable name="users" type="List&lt;User>"/>
</data>

3.默认值(默认值将只在预览视图中显示,且默认值不能包含引号)

1
android:text="@{userInfo.name,default=defaultValue}"

4.DataBinding的坑
官网上的demo很简单,简单到UserInfo中的所有字段都是string,它并没有告诉我们当字段是int时会有什么问题。假设我们没有在layout中对age写String.valueOf方法的话, userAge就是一个int对象,它会在这里被直接setText

1
2
3
4
5
// batch finished
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, userAge); //<--设置UI的操作
}

对setText传一个int值,会被当做Resource索引,然后导致崩溃。如果你是刚接触DataBinding的新手,估计会看到下面这种崩溃原因

Resource #0x0

原因就是缺少了String.valueOf调用了。

原理

DataBinding使用了apt技术,我们build项目时DataBinding会生成多个文件。

DataBinding通过布局中的tag将控件查找出来,然后根据生成的配置文件将V与M进行对应的同步操作,设置一个全局的布局变化监听来实时更新,M通过他的set方法进行同步。

数据绑定框架的目标就是免除开发者繁琐的操作UI,它帮我们做这些事情就好了。 所以它通过注解在编译期生成了ActivityMainBinding类,就是下面这里:

1
2
3
4
5
6
7
8
9
public class MainActivity extends Activity {
....
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mUser = new UserInfo();
binding.setUser(mUser);
}

ActivityMainBinding可以理解为观察者,它的父类是ViewDataBinding, 它有个抽象方法

1
2
3
4
/**
* @hide
*/
protected abstract void executeBindings();

所有的layout都会生成一个Binding类,这个类继承ViewDataBinding,然后实现了execute*方法。 理解DataBinding框架的关键代码就在这里,其他可以选择性忽略,我们看代码的时候是这样的,先抽脉络,细枝末节的处理可以在理解了框架之后再慢慢体会。 下面是这个抽象方法的具体实现逻辑,这些代码都是DataBinding帮我们生成的。

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
34
35
36
37
38
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userName = null;
java.lang.String stringValueOfUserAge = null;
int userAge = 0;
com.phoenix.databindingdemo.UserInfo user = mUser; //<--我们传入的对象

if ((dirtyFlags & 0xfL) != 0) {
if ((dirtyFlags & 0xbL) != 0) {
if (user != null) {
// read user.name
userName = user.getName();//<--UserInfo类中注解标识的get方法
}
}
if ((dirtyFlags & 0xdL) != 0) {
if (user != null) {
// read user.age
userAge = user.getAge();//<--UserInfo类中注解标识的get方法
}
// read String.valueOf(user.age)
stringValueOfUserAge = java.lang.String.valueOf(userAge);//<--layout中的String.valueOf
}
}
// batch finished
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvAge, stringValueOfUserAge); //<--设置UI的操作
}
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.tvName, userName); //<--设置UI的操作
}
}

我们在activity中把 mUser对象传入了binding类,在每次对它进行set操作的时候都会触发notify, 之后DataBinding框架会回调execute方法, 框架通过注解拿到get方法,然后拿到和UI所对应的数据,之后结合layout中对应的标注去更新UI。 整个观察者模式的逻辑基本就是这样。

WorkManager

service一直被用来做后台运行的操作,包括一些保活,上传数据之类的,这个后台运行的弊端很多,比如耗电,比如设计用户隐私之类的,谷歌对这些后台行为进行了一些处理,从Android Oreo(API 26) 开始,如果一个应用的目标版本为Android 8.0,当它在某些不被允许创建后台服务的场景下,调用了Service的startService()方法,该方法会抛出IllegalStateException。如果想继续使用service,必须调用Context.startForegroundService()。所以,在不久的将来,service的使用范围会越来越小,取而代之的,是谷歌推出的新的技术:WorkManager。

WorkManager 在工作的触发器 满足时, 运行可推迟的后台工作。WorkManager会根据设备API的情况,自动选用JobScheduler, 或是AlarmManager来实现后台任务,WorkManager里面的任务在应用退出之后还可以继续执行,这个技术适用于在应用退出之后任务还需要继续执行的需求。

WorkManager库的架构图如下所示:

WorkManager可以做很多事情: 取消任务, 组合任务, 构建任务链, 将一个任务的参数合并到另一个任务。大部分的后台任务处理,WorkManager都可以胜任:

相关应用架构

注意:任何应用编写方式都不可能是每种情况的最佳选择。话虽如此,但推荐的这个架构是个不错的起点,适合大多数情况和工作流。如果您已经有编写 Android 应用的好方法(遵循常见的架构原则),则无需更改。

常见的架构原则

如果您不应使用应用组件存储应用数据和状态,那么您应该如何设计应用呢?

分离关注点

要遵循的最重要的原则是分离关注点。一种常见的错误是在一个 Activity 或 Fragment 中编写所有代码。这些基于界面的类应仅包含处理界面和操作系统交互的逻辑。您应尽可能使这些类保持精简,这样可以避免许多与生命周期相关的问题。

请注意,您并非拥有 Activity 和 Fragment 的实现;它们只是表示 Android 操作系统与应用之间关系的粘合类。操作系统可能会根据用户互动或因内存不足等系统条件随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用维护体验,您最好尽量减少对它们的依赖。

通过模型驱动界面

另一个重要原则是您应该通过模型驱动界面(最好是持久性模型)。模型是负责处理应用数据的组件。它们独立于应用中的 View 对象和应用组件,因此不受应用的生命周期以及相关的关注点的影响。

持久性是理想之选,原因如下:

  • 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
  • 当网络连接不稳定或不可用时,应用会继续工作。

应用所基于的模型类应明确定义数据管理职责,这样将使应用更可测试且更一致。

最佳做法

编程是一个创造性的领域,构建 Android 应用也不例外。无论是在多个 Activity 或 Fragment 之间传递数据,检索远程数据并将其保留在本地以在离线模式下使用,还是复杂应用遇到的任何其他常见情况,解决问题的方法都会有很多种。

虽然以下建议不是强制性的,但根据我们的经验,从长远来看,遵循这些建议会使您的代码库更强大、可测试性更高且更易维护:

避免将应用的入口点(如 Activity、Service 和广播接收器)指定为数据源。

相反,您应只将其与其他组件协调,以检索与该入口点相关的数据子集。每个应用组件存在的时间都很短暂,具体取决于用户与其设备的交互情况以及系统当前的整体运行状况。

在应用的各个模块之间设定明确定义的职责界限。

例如,请勿在代码库中将从网络加载数据的代码散布到多个类或软件包中。同样,也不要将不相关的职责(如数据缓存和数据绑定)定义到同一个类中。

尽量少公开每个模块中的代码。

请勿试图创建“就是那一个”快捷方式来呈现一个模块的内部实现细节。短期内,您可能会省点时间,但随着代码库的不断发展,您可能会反复陷入技术上的麻烦。

考虑如何使每个模块可独立测试。

例如,如果使用明确定义的 API 从网络获取数据,将会更容易测试在本地数据库中保留该数据的模块。如果您将这两个模块的逻辑混放在一处,或将网络代码分散在整个代码库中,那么即便能够进行测试,难度也会大很多。

专注于应用的独特核心,以使其从其他应用中脱颖而出。

不要一次又一次地编写相同的样板代码,这是在做无用功。相反,您应将时间和精力集中放在能让应用与众不同的方面上,并让 Android 架构组件以及建议的其他库处理重复的样板。

保留尽可能多的相关数据和最新数据。

这样,即使用户的设备处于离线模式,他们也可以使用您应用的功能。请注意,并非所有用户都能享受到稳定的高速连接。

将一个数据源指定为单一可信来源。

每当应用需要访问这部分数据时,这部分数据都应一律源于此单一可信来源。

参考资料

https://developer.android.google.cn/jetpack?hl=zh_cn
即学即用Android Jetpack
深入了解架构组件之ViewModel
应用架构指南
从Service到WorkManager
android-navigation demo
Android DataBinding 使用
DataBinding常用注解