Android Jetpack
Jetpack是一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码(业务代码)上。
Android Jetpack组件包含一系列Android库,这些库是为协同工作而构建的,它们都在Android应用中提供向后兼容性。google在不但完善这套库,而且将它放在官网首页单独的一栏,足以证明google对它的重视和支持。它包含四个部分内容:
AndroidX不是一个Android版本,它是一套API集合,命名空间为androidx
,其中的所有软件包都使用一致的命名空间,以字符串androidx
开头。
AndroidX包含了Android Jetpack组件库的所有内容,也就是说Android Jetpack组件相关代码是放在AndroidX包中的。
不仅如此,AndroidX还包含Android支持库的内容,在之前我们使用Android支持库相关API(android.support.v4.app.Fragment
、android.support.v7.widget.RecyclerView
等)都需要依赖很多支持库:
support库可以看作是三方库,只是它是由google官方发布的,这部分内容是在发布android sdk时没有考虑到(未包含),之后又想在低版本android设备上使用的内容(基于低版本sdk实现),所以google在提供了一个新的api时会以support的形式提供,我们就可以像使用三方库一样依赖使用
implementation 'com.android.support:appcompat-v7:27.1.0'
implementation 'com.android.support:recyclerview-v7:27.1.0'
implementation 'com.android.support:design:27.1.0'
这样会导致很多问题,比如所有com.android.support
库必须使用完全相同的版本规范,混合版本可能导致运行时崩溃,这为项目管理带来很多挑战。
现在这些支持库都被集合在AndroidX中了,今后可能会发布新的支持库也都将在AndroidX库中进行,原来的support支持库不再维护(最后版本Support Library 28),所以不应该继续使用。
目前很多官方组件库都迁移到了AndroidX,一些三方开源库也在积极响应,所以Android Jetpack是今后Android开发的一个趋势,如果公司现有项目还没有迁移到AndroidX,应该尽快迁移,而新开发的项目更应该基于AndroidX。
有关androidx
命名空间中的所有软件包和类,请参阅 AndroidX参考文档
如果要在新项目中使用命名空间为 androidx 的库,按照如下步骤:
compileSdkVersion
设置为Android9.0(API级别28)或更高版本gradle.properties
文件中将以下两个Android Gradle插件标记设置为true### Android 插件会使用对应的 AndroidX 库,而非支持库。默认为 false。
android.useAndroidX=true
### Android 插件会通过重写其二进制文件来自动迁移现有的第三方库,以使用 AndroidX 依赖项。默认为 false。
android.enableJetifier=true
#### root build.gradle
allprojects {
repositories {
google() //添加 google() 代码库
jcenter()
}
}
#### app build.gradle
implementation 'androidx.appcompat:appcompat:1.0.2'
Jetpack库和Support库的内容都被集合在AndroidX中了,但是每个库还是单独维护的,我们可以选择性使用其中某些库
谷歌开发者微信公众号前不久2020/02/26发表了一篇中文版文章是时候迁移至 AndroidX了!,介绍了为什么要迁移至AndroidX以及怎样迁移,讲解非常详细,大家也可以看一下。
在讲解MVVM模式之前,我们要搞清楚这种模式为什么会出现?它有什么优点能解决什么问题?这两个问题其实想表达的是一个意思,任何事物的出现必定是为了解决一些问题,那解决了这些问题就是这个事物的优点。
MVVM模式是两年才被提出推广,它不是凭空出现的,开发模式的优化是一个进化的过程(MVC—>MVP—>MVVM)。早期开发最火的模式是MVC,后来由于种种弊端,出现了MVP,然后才是MVVM,这三种模式就是后者对前者的优化,所以为了搞清楚MVVM,不妨从头开始说起,这样才能知道MVVM到底有什么好处。
模式只是一种思想,在不同的语言、平台都可以使用,并非局限于Android。每个人对每种开发模式的理解不一样,模式中的字母对应什么内容也没有强行规定,只要自己能够理解就行。下面我们简单的分析一下这些模式
在Android初期,google在设计时就是基于MVC模式(三层模型Model-View-Controller)的,将同一种类型的代码放在一块实现代码分层,这样方便管理。所以我们在开发项目时都是写布局layout,创建bean,在Activity中写逻辑获取数据,刷新页面这一套固定流程。
public class User {
private String userName;
private String userPwd;
public User(String userName, String userPwd) {
this.userName = userName;
this.userPwd = userPwd;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPwd() {
return userPwd;
}
public void setUserPwd(String userPwd) {
this.userPwd = userPwd;
}
}
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.openxu.core.view.TitleLayout
android:id="@id/titleLayout"
style="@style/TitleDefStyle"
app:textcenter="登录"
app:iconBack="@null"/>
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="@dimen/activity_sides_margin"
android:hint="请输入用户名"/>
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="@dimen/activity_sides_margin"
android:hint="请输入密码"/>
<Button
android:id="@+id/btn_login"
style="@style/btn_red"
android:layout_marginTop="20dp"
android:text="登录"/>
LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_centerInParent="true"/>
RelativeLayout>
public class LoginActivity extends BaseActivity {
private Button btn_login;
private EditText et_name, et_pwd;
private ProgressBar progressBar;
@Override
protected int getContentView() {
return R.layout.activity_login;
}
@Override
protected void initView() {
btn_login = findViewById(R.id.btn_login);
et_name = findViewById(R.id.et_name);
et_pwd = findViewById(R.id.et_pwd);
progressBar = findViewById(R.id.progressBar);
btn_login.setOnClickListener(v -> {
User user = new User(et_name.getText().toString().trim(), et_pwd.getText().toString().trim());
//延迟2s模拟请求登录接口
progressBar.setVisibility(View.VISIBLE);
new Handler().postDelayed(() -> {
progressBar.setVisibility(View.GONE);
if (TextUtils.isEmpty(user.getUserName()) || TextUtils.isEmpty(user.getUserPwd())){
XToast.error("登录失败,请检查用户名密码").show();
return;
}
XToast.success("登录成功").show();
}, 2000);
});
}
}
数据模型JavaBean对应模型层(Model),xml布局对应视图层(View),Activity对应控制层(Controller),这种设计的初衷是好的,也在一定程度上实现了分层,但是随着Android的发展,应用的复杂性,导致问题逐渐暴露出来。
视图层对应的xml布局是一个静态页面,绑定数据等操作需要在Activity中绑定。模型层的数据请求、数据库处理和一些I/O操作都被放到了Activity中,模型层仅仅是JavaBean。而充当控制层的Activity中不仅仅要承担业务逻辑,还需要担任绑定数据等视图操作。这样会导致Activity中代码量非常大,代码臃肿繁杂,可读性差且不利于维护。
鉴于MVC模式的弊端,在Android领域就有人尝试使用MVP模式(Model-View-Presenter)来解决这些问题,各大开源平台上也有很多MVP的开源框架。下面看一下使用MVP实现登录功能的项目结构(代码量较多,寄不贴了):
相对于MVC,MVP模式在Android中应用的最大改进就是,将业务逻辑的控制和数据请求的内容从Activity中剥离出来,让Activity变为View层了,解决了Activity内容臃肿的问题,而且层次更加分明。业务逻辑在Presenter层处理,数据请求等跟数据相关的都划分为Model层。三层之间通过接口Interface
实现通信,Presenter作为中间层,View不与Model直接接触,业务逻辑和数据层是可以复用的,实现了解耦。
MVP最大的问题就是需要定义大量的接口,代码量变多了,虽然有很多开源框架实现了一键生成模板,但是还是有很多人接受不了,我本人就是其中一个,所以当初公司项目重构时搭建框架没有选择MVP模式
Android架构组件是Android Jetpack的一部分,它是一组库,可帮助您设计稳健、可测试且易维护的应用。这个架构组件可以说是google在Android平台推广MVVM模式而编写的一套类库,使用这套类库中的API很优雅的实现MVVM模式。
下面我们一个一个的学习这些支持类库,相关类库版本信息和依赖方法参考:AndroidX版本说明
如果要使用ViewModel
,不能再继承普通android.app.Activity
,而是继承androidx.appcompat.app.AppCompatActivity
,Fragment需要继承androidx.fragment.app.Fragment
。
androidx库中的AppCompatActivity
和Fragment
实现了LifecycleOwner
和ViewModelStoreOwner
等必要接口,而ViewModelStoreOwner
是创建ViewModel
时必须要实现的接口。androidx.appcompat:appcompat
就是之前的com.android.support:appcompat-v7
,这个库的作用是对低版本做兼容,而且里面有很多资源文件我们在开发中需要用到,比如style.xml
中的主题。
//api 'com.android.support:appcompat-v7:28.0.0'
//添加依赖
api 'androidx.appcompat:appcompat:1.1.0'
诸如 Activity和Fragment之类的界面控制器主要用于显示界面数据、对用户操作做出响应或处理操作系统通信(如权限请求)。如果要求界面控制器也负责从数据库或网络加载数据,那么会使类越发膨胀。为界面控制器分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,而不是将工作委托给其他类。以这种方式为界面控制器分配过多的责任也会大大增加测试的难度。从界面控制器逻辑中分离出视图数据所有权的做法更易行且更高效。
上面这段话摘自官网,意思就是ViewModel
的作用就是替Activity
分担加载数据的工作,让Activity
只负责显示界面、响应用户交互等工作。
//添加依赖
def lifecycle_version = "2.2.0"
def arch_version = "2.1.0"
// ViewModel
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData(示例中需要用到)
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
/**
* Author: openXu
* Time: 2020/5/13 16:56
* class: LoginViewModel
* Description:
*
* 需要继承ViewModel类,ViewModel类源码注释有示例怎样使用。
*
* 1. ViewModel负责为Activity/Fragment准备和管理数据,它还可以处理一部分业务逻辑。
* 2. ViewModel总是与Activity/Fragment关联创建,并且只要Activity/Fragment还活着就会保留,直到被销毁。
* 换句话说,这意味着如果ViewModel的所有者因配置更改(例如旋转)而被销毁,则ViewModel不会被销毁。所有者的新实例将重新连接到现有的ViewModel。
* 3. ViewModel通常通过LiveData或者DataBinding实现与Activity/Fragment的数据通信
* 4. ViewModel的唯一职责是管理数据,他不应该持有Activity/Fragment的引用
*
*/
public class LoginViewModel extends ViewModel {
//对请求接口的结果数据使用LiveData封装,LiveData可以通知Activity/Fragment数据变化
public final MutableLiveData loginResult = new MutableLiveData<>();
//显示进度对话框
public final MutableLiveData showProgress = new MutableLiveData<>();
public void login(User user){
//显示进度对话框
showProgress.setValue(true);
//延迟2s模拟请求登录接口
new Handler().postDelayed(() -> {
//隐藏进度对话框
showProgress.setValue(false);
if (TextUtils.isEmpty(user.getUserName()) || TextUtils.isEmpty(user.getUserPwd())){
//登录失败,设置结果数据为false
loginResult.setValue(false);
return;
}
//登录成功,设置结果数据为true
loginResult.setValue(true);
}, 2000);
}
/**
* ★Activity调用onDestroy()之前会调用此方法
* 表示ViewModel不再使用并将销毁,可以在此进行清除数据的操作,避免内存泄漏
*/
@Override
protected void onCleared() {
super.onCleared();
XLog.w("===========Activity销毁了,ViewModel清除数据");
}
}
androidx.appcompat.app.AppCompatActivity
,Fragment需要继承androidx.fragment.app.Fragment
public abstract class BaseActivity extends AppCompatActivity {
...
}
onCreate()
方法使用new ViewModelProvider(this).get(XXXViewModel.class)
获取ViewModel实例对象,并调用其方法请求数据public class LoginActivity extends BaseActivity {
private Button btn_login;
private EditText et_name, et_pwd;
private ProgressBar progressBar;
LoginViewModel viewModel;
@Override
protected int getContentView() {
return R.layout.activity_login;
}
@Override
protected void initView() {
btn_login = findViewById(R.id.btn_login);
et_name = findViewById(R.id.et_name);
et_pwd = findViewById(R.id.et_pwd);
progressBar = findViewById(R.id.progressBar);
/**
* ★使用new ViewModelProvider(ViewModelStoreOwner owner).get(XXXViewModel.class)获取对应的ViewModel对象
* ViewModelProvider构造方法接受ViewModelStoreOwner实例,androidx.appcompat.app.AppCompatActivity和androidx.fragment.app.Fragment都实现了ViewModelStoreOwner
*/
viewModel = new ViewModelProvider(this).get(LoginViewModel.class);
//★在Activity中监听viewModel的LiveData数据变化,实现ViewModel和Activity的通信
viewModel.showProgress.observe(this, aBoolean -> {
progressBar.setVisibility(aBoolean?View.VISIBLE:View.GONE);
});
viewModel.loginResult.observe(this, aBoolean -> {
if(aBoolean)
XToast.success("登录成功").show();
else
XToast.error("登录失败,请检查用户名密码").show();
});
btn_login.setOnClickListener(v -> {
//★调用viewModel的login()方法实现登录接口请求
User user = new User(et_name.getText().toString().trim(), et_pwd.getText().toString().trim());
viewModel.login(user);
});
}
}
下图左边是官网上对ViewModel生命周期的描述,右边是我对该声明周期做的测试打印的log和解释
LiveData是一种可观察的数据存储器类(可用于任何类型数据包括复杂数据的封装),就是在我们数据外面包裹一层变为被观察者。与常规的被观察类不同,LiveData具有生命周期感知能力,它遵循应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保LiveData仅更新处于活跃生命周期状态的应用组件观察者,并能够自动清理以防止对象泄漏和过多的内存消耗。。
ViewModel
类中创建LiveData
实例以存储某种类型的数据/**
* LiveData是一个抽象类,而且它没有公开可用的方法来更新存储的数据,应该使用其子类MutableLiveData
* MutableLiveData类公开了setValue(T)和postValue(T)方法用于更新存储的数据
* setValue(T)应该在主线程中调用,postValue(T)应该在子线程中调用
*/
public final MutableLiveData loginResult = new MutableLiveData<>();
public final MutableLiveData showProgress = new MutableLiveData<>();
Activity
或Fragment
的onCreate()
方法中创建Observer
观察者对象观察LiveData数据变化/**
* observeForever(Observer)观察LiveData数据变化,这种方式没有关联LifecycleOwner对象,
* 观察者会被视为始终处于活跃状态,因此它始终会收到关于修改的通知。
* 可以通过调用removeObserver(Observer) 方法来移除这些观察者
*/
viewModel.loginResult.observeForever(new Observer() {
@Override
public void onChanged(Boolean aBoolean) {
//当数据变化时回调
}
});
/**
* observe(LifecycleOwner owner, Observer super T> observer)
* 为LiveData添加观察者并绑定组件的生命周期,只有当组件活跃(started)时才会收到数据更新
*/
viewModel.loginResult.observe(this, new Observer() {
@Override
public void onChanged(Boolean aBoolean) {
//当数据变化时回调
}
});
参见工程com.openxu.mvvm.LoginActivity
演示过程:LoginActivity
是一个模拟登录的页面,当点击登录按钮会调用LoginViewModel
的登录方法,同时会打开两个新的遮罩页面,第一个遮罩页面是半透明的,第二个遮罩页面是不透明的。等待登录完成(2s),一层层返回,可以看到只有当Activity
是可见状态(onStart()
执行之后)才能接受LiveDate
的数据更新
虽然MVVM模式中将Activity和xml布局都作为View层,但是Activity和xml布局毕竟是两个独立的文件,这两个文件怎么实现交互?
最开始,我们为Activity设置布局id,然后通过findViewById()
实例化控件,调用控件实例的方法(如setText())实现视图交互,如果页面很复杂,会造成Activity
中大量的findViewById()
的代码。
private Button btn_login;
private EditText et_name, et_pwd;
private ProgressBar progressBar;
btn_login = (Button)findViewById(R.id.btn_login);
et_name = (EditText)findViewById(R.id.et_name);
et_pwd = (EditText)findViewById(R.id.et_pwd);
progressBar = (ProgressBar)findViewById(R.id.progressBar);
后来,ButterKnife出现了,这个开源库专注于Android系统View层的注入,可以省去findViewById()
的调用,通过注解在编译阶段生成新的类帮我们完成View的初始化操作。
//初始化控件实例
@BindView(R.id.et_name)
EditText username;
@BindView(R.id.et_pwd)
EditText password;
//将控件数据绑定到对象上
@BindString(R.string.login_error)
String loginErrorMessage;
//绑定事件
@OnClick(R.id.R.id.btn_login)
void submit() {
// TODO call server...
}
但是我们还是需要在Activity中注册View的引用,而且ButterKnife
的介绍中有一段话:
Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped.
注意:现在不建议使用此工具。请切换到ViewBinding。很明显,现有版本将继续起作用,但是仅考虑与AGP集成的关键错误修复。功能开发和常规错误修复已停止。
通过视图绑定功能,可以更轻松地编写可与视图交互的代码。在module中启用视图绑定之后,系统会为该module中的每个XML布局文件生成一个绑定类,绑定类的实例包含对在相应布局中具有ID的所有View的直接引用。避免了视图ID不存在导致的空指针异常,和视图类型转换异常。
设置
ViewBinding可按模块启用,在需要启用的module的build.gradle
脚本中添加如下配置:
android {
...
//注意:视图绑定在 Android Studio 3.6 Canary 11 及更高版本中可用
viewBinding {
enabled = true
}
}
开启绑定后,gradle配置阶段会自动为该module下所有xml布局生产绑定类,如果希望某些布局文件不要生成绑定类,请在布局中设置tools:viewBindingIgnore="true"
:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
LinearLayout>
自动生成的Binding类
Android Gradle插件会将xml布局文件的名称转换为驼峰式大小写,并在末尾添加Binding
一词作为自动生成的绑定类的类名,比如activity_login.xml
会生成ActivityLoginBinding.java
。这个类持有所有布局中设置了id的控件的引用,以及跟布局的引用。
使用
private ActivityLoginBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
XLog.v("-----Activity.onCreate()");
//使用静态inflate()方法获取生成的绑定类的实例
binding = ActivityLoginBinding.inflate(R.layout.activity_login);
//getRoot()方法获取布局文件的根视图对象
setContentView(binding.getRoot);
//通过binding对象中持有的view引用直接对view进行设置,而不需要findViewById()
binding.name.setText("name");
binding.button.setOnClickListener(v->{});
}
注意
ViewBinding
只有在Android Studio 3.6 Canary 11 及更高版本中才能使用。比如现在我的Android Studio版本才到3.2.1,肯定就没法用ViewBinding了,这么好的库不用岂不是浪费?其实这个库的功能非常简单,只是帮我们省去了findViewById()
的烦恼,下面有一个更加强大的库,可以完全代替ViewBinding
,还没有Studio版本限制。所以我这里就不升级了,直接上更加强大的库,如果大家Studio版本符合要求可以按照上面的步骤试一试。
DataBinding
数据绑定库与Android Gradle插件捆绑在一起。您无需声明对此库的依赖项,但必须启用它。也就是说DataBinding相关的库我们不需要单独依赖,只需要在module中开启即可
★注意:即使某个module不需要使用DataBinding,但是它依赖的别的module使用了,也需要在该module中配置数据绑定
android {
...
dataBinding {
enabled = true
}
}
标签包裹://activity_login.xml
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.openxu.bean.User"/>
data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<TextView
android:id="@+id/tv_result"
style="@style/text_style_def"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.userName}"/>
RelativeLayout>
layout>
//Activity中通过DataBindingUtil.setContentView()设置布局获取绑定类实例对象(代替原来的setContentView(getContentView(id));)
ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
//也可以使用下面的方式绑定视图
ActivityLoginBinding binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//如果要在Fragment、ListView或RecyclerView适配器中使用数据绑定项,可以使用绑定类或DataBindingUtil类的inflate()方法
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
//如果布局是使用其他机制扩充的,可单独绑定
View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);
//通过DataBindingUtil设置布局获取绑定类实例对象
ActivityLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_login);
//xml布局中带id的控件对象
String userName = binding.etName.getText().toString().trim();
String userPwd = binding.etPwd.getText().toString().trim();
//设置变量
binding.setUser(new User(userName,userPwd));
默认情况下,绑定类是根据布局文件的名称生成的,以大写字母开头,移除下划线(_),将后一个字母大写,最后添加后缀Binding。该类位于模块包下的databinding包中。例如,布局文件activity_login.xml
会生成ActivityLoginBinding.java
。如果模块包是com.openxu.mvvm
,则绑定类放在com.openxu.mvvm.databinding
包中。通过调整data
元素的class
特性,绑定类可重命名或放置在不同的包中。示例:
//当前模块包+databinding com.openxu.mvvm.databinding.ActivityLogin1Binding
<data class="ActivityLogin1Binding">
<variable name="user" type="com.openxu.bean.User"/>
data>
//当前模块包(相对路径) com.openxu.mvvm.ActivityLogin1Binding
<data class=".ActivityLogin1Binding">
<variable name="user" type="com.openxu.bean.User"/>
data>
//完整软件包名称(绝对路径) com.openxu.databinding.ActivityLogin1Binding
<data class="com.openxu.databinding.ActivityLogin1Binding">
<variable name="user" type="com.openxu.bean.User"/>
data>
在根标签
中的标签中使用
标记数据变量,该变量就是可以在此布局中使用的属性。name
属性是变量名称,type
属性是变量类型全名,在布局中使用@{}
表达式绑定数据到控件属性中。上面的示例中,android:text="@{user.userName}"
会访问User
类的userName
属性、getUserName()
或者userName()
中的一个(如果存在)。如果属性是不可访问的(private修饰无get方法),构建时会报错,不能生成Binding类:
通过Binding
类的setUser(user)方法,可以为变量赋值,而xml布局中使用@{}
访问的数据就会自动刷新。需要注意的是只有设置新的user对象才会触发数据绑定,如果仅仅是user对象的某个属性变化或者设置的user对象与之前相同,则不会触发数据绑定。
绑定表达式详解
//@{}表达式中可以直接引用对象的属性(属性、getter方法、lastName())
android:text="@{user.lastName}"
算术运算符 + - / * %
字符串连接运算符 +
逻辑运算符 && ||
二元运算符 & | ^
一元运算符 + - ! ~
移位运算符 >> >>> <<
比较运算符 == > < >= <=(请注意,< 需要转义为 <)
instanceof
分组运算符 ()
字面量运算符 - 字符、字符串、数字、null
类型转换
方法调用
字段访问
数组访问 []
三元运算符 ?:
//相当于 user.displayName!=null ? user.displayName : user.lastName
android:text="@{user.displayName ?? user.lastName}"
//生成的数据绑定代码会自动检查有没有null值并避免出现空指针异常。如下,在表达式 @{user.name} 中,如果user为Null,则为user.name分配默认值 null。如果您引用user.age,其中age的类型为int,则数据绑定使用默认值 0。
android:text="@{user.name}"
注意:要使 XML 不含语法错误,您必须转义 < 字符。例如:不要写成 List 形式,而是必须写成 List
。
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
data>
//使用[]运算符访问常见集合,例如数组、列表、稀疏列表和映射
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
//还可以使用 object.key 表示法在映射中引用值。例如上面 @{map[key]} 可替换为 @{map.key}
android:text="@{map[key]}"
//可将属性值用单引号括住,这样就可以在表达式中使用双引号表示字符串了
android:text='@{map["firstName"]}'
//也可以使用反单引号 ` (英文状态下键盘Esc下面的按键)表示字符串字面量
android:text="@{map[`firstName`]}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
//方法引用,这种方式方法的参数必须是(View view),不能是其他的,相当于设置onClick事件
public class LoginViewModel extends ViewModel {
public void login(View v){ ... }
}
/* 绑定表达式@{}可将视图的点击监听器分配给 LoginViewModel.login() 方法 */
//监听器绑定,相当于在布局中就设置了onClick监听,当点击时执行匿名的监听的onClick方法,而这个方法调用presenter.onSaveClick(task)
public class Presenter {
public void onSaveClick(Task task){}
public void onCompletedChanged(Task task, boolean completed){}
//★如果监听的事件返回类型不是void,则表达式也必须返回相同类型的值
public boolean onLongClick(View view, Task task) { }
}
//android:onClick="@{(view) -> presenter.onSaveClick(task)}" 可以忽略方法的所有参数,也可以命名所有参数
//监听长按事件,返回只为boolean
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
//如果您需要将表达式与谓词(例如,三元运算符)结合使用,则可以使用 void 作为符号
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data >
<variable name="user" type="com.openxu.bean.User"/>
<import type="java.util.Map"/>
<variable name="map" type="Map<String, String>"/>
<import type="android.view.View"/>
<import type="com.openxu.view.View"
alias="MyView"/>
<import type="com.openxu.bean.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="user1" type="com.openxu.bean.User"/>
<variable name="userList" type="List<User>"/>
<import type="com.openxu.core.utils.XTimeUtils"/>
data>
...
<TextView android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
<TextView android:text="@{XTimeUtils.date2Str(date, `yyyy-MM-hh`)}"/>
下面的布局由name.xml和contact.xml组成,子布局中使用了user变量,可以在父布局中使用bind传递变量给子布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
LinearLayout>
layout>
当xml布局中使用了@{}
绑定表达式后,表达式中绑定的值发生变化,生成的Binding
类就会调用绑定视图(绑定的控件)的对应属性的setter
方法将表达式的值设置给视图。
绑定适配器就是负责将xml布局绑定表达式@{}
的值设置给View
的对应属性。比如android:text="@{user.name}"
,其实就是适配器调用TextView.setText()
将user.name
的值设置进去。DataBinding框架有一系列自带的预定义适配器,可在androidx.databinding:databinding-adapters-3.2.1
库的androidx.databinding.adapters
包下查看。
只有定义了适配器的属性才能在布局中使用@{}绑定表达式;同理如果想适配器方法被调用,布局中绑定值必须使用@{}表达式。DataBinding框架允许我们自己定义适配器,自定义的适配器会被自动检测并覆盖自带预定义适配器,不用做特殊设置
绑定适配器有三种数据适配方式,每一种对适配方法都有所要求,如果不符合会导致编译不通过:
如果被绑定的属性在相应控件类中有对应
setter
方法,则会自动选择该方法进行绑定值的设置。如果没有setter
方法则需要使用下面两种方式指定适配方法,否则编译会报错(找不到setter
方法)
★注意:一定是标准的setter方法,比如text属性的setter方法为
public void setText(String){}
,返回类型为void
如果一些属性具有名称不符的
setter
方法,比如下面自定义控件TitleLayout中textcenter属性,没有对应的setTextcenter(String)
方法,但是有一个setMyTextcenter(Stirng)
,绑定框架不能自动匹配上setter
。可以通过@BindingMethods
、@BindingMethod
注解将属性和方法配对关联起来。
/**
* BindingMethods、BindingMethod注解可以标记在任何类上,甚至是一个空类
* 通过这两个注解可以将属性和set方法关联,type属性为控件类型,attribute为特性的属性,method为需要匹配的方法
* ★注意 :
* 数据绑定库在匹配时会忽略自定义命名空间,所以attribute = "app:textcenter"可省略app:
* method对应的方法必须是public void setXXXX(...)形式,返回类型为void,方法参数为@{}表达式值的类型
*/
@BindingMethods({
@BindingMethod(type = TitleLayout.class, attribute = "textcenter", method = "setMyTextcenter"),
})
public class XTitleLayoutBindingAdapter {
}
上面两种情况我们只需要了解一下就行,平时开发中基本上是用不到的,因为框架已经帮我们把上面两种类型的适配器都封装好了。但是如果我们需要自定义绑定数据逻辑,可以使用
@BindingAdapter
注解。比如下面的示例
public class XTextViewBindingAdapter {
/**
* ★★★应用: 1. 使用BindingAdapter注解标记方法为适配器方法,方法名和类名随便写
* 参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。
* @param view 固定的,为被绑定的视图对象
* @param url 绑定表达式中接受的类型,比如android:src="@{user.imageUrl}"中user.imageUrl为String类型
*
* 注意:方法必须是static的,返回值类型不做限制,参数必须由 视图 + @{值}值类型 组成
*/
@BindingAdapter("android:src")
public static String setImage(ImageView view, String url) {
XLog.v("适配器拦截setImage:"+" "+url);
//拦截android:src属性的数据绑定,使用Glide统一绑定图片
Glide.with(view.getContext())
.load(url)
.apply(new RequestOptions().placeholder(0).centerCrop())
.into(view);
return "";
}
/**
* 2. 标记多个属性特性的适配器方法
* value 接受字符串数组
* requireAll表示是否必须同时满足
* true : 布局中同时设置value中的属性才会调用适配器
* false : 设置了任意属性时调用适配器,未设置的属性实参为默认值(String 为null, int为0...)
* @param view 固定的,为被绑定的视图对象
* @param color 绑定表达式中接受的类型
* @param size 绑定表达式中接受的类型
*/
@BindingAdapter(value={"android:textColor", "android:textSize"}, requireAll = false)
public static void setTextCS(TextView view, int color, int size) {
XLog.v("适配器拦截:"+" "+color + " "+size);
view.setTextColor(color);
view.setTextSize(size);
}
}
上面我们讲的数据绑定都是单向数据绑定@{}
,android:text="@{user.userName}"
当user属性发生变化时,会自动修改控件属性值。
@={}
可以看作是@{}
的逆向,@={}
表达式当属性的数据更改时会绑定到某一个属性上。在项目中最常用的就是用来将EditText
的text
属性值自动绑定到变量中,这样省略editText.getText().toString()
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data >
<variable name="user" type="com.openxu.bean.User"/>
data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@={user.userName}"
android:hint="请输入用户名"/>
...
RelativeLayout>
layout>
//为xml布局中user属性绑定对象
User user = new User(null, null);
binding.setUser(user);
//当点击按钮时,打印user.getUserName()的值,由于布局中使用了@={}表达式,EditText的text属性值变化会自动绑定到user的userName属性上
binding.btnLogin.setOnClickListener(v -> {
//下面两种打印结果相同
XLog.v("自动绑定了数据:"+user.getUserName());
XLog.v("自动绑定了数据:"+binding.getUser().getUserName());
viewmodel.login(user);
...
});
视图绑定和数据绑定库均会生成可用于直接引用视图的绑定类,不过,这两者之间存在明显差异:
代码创建的数据绑定布局。ViewBinding
主要目的是取代findViewById()的调用,它不支持布局变量或布局表达式,因此它不能在XML中将布局与数据绑定。DataBinding
库即可lifecycle
库是生命周期感知组件,用于响应组件(Activity/Fragment等)的生命周期。在之前的模式中,通常在组件生命周期方法中实现依赖组件的操作,这会导致代码条理性很差而且会扩散错误。通过使用生命周期感知型组件,您可以将依赖组件的代码从生命周期方法移入组件本身中。
注意:要将androidx.lifecycle
导入 Android 项目,请参阅 Lifecycle 版本说明中关于声明依赖项的说明。
androidx.appcompat:appcompat:1.1.0
// 单独导入lifecycle依赖Lifecycles only (without ViewModel or LiveData)
api "androidx.lifecycle:lifecycle-runtime:2.2.0"
上面的依赖是单独引入lifecycle
库,其实这个库我们可以不用引入就可以使用了,因为在上面依赖的appcompat
、lifecycle-viewmodel
、lifecycle-livedata
这些库也依赖了lifecycle
库,从而依赖传递,所以即使这里不特定依赖也能使用。但是如果这里单独依赖后可能会出现依赖冲突,原因是传递依赖的版本和我们单独依赖的版本不同导致:
AndroidX是一系列类库的集合,如果需要使用其中某个类库,需要单独添加该类库的依赖。开发过程中通常会遇到一些类库没有依赖,却能使用,可能是因为你依赖的其他类库间接将它依赖进来了,但是为了管理方便我们最好还是显示添加需要的依赖,gradle会自动选择版本高的。如果出现依赖冲突,可以通过查看依赖树找到冲突的类库,然后使用下面的脚本指定版本号:
gradlew :app:dependencies --configuration releaseCompileClasspath
查看release构建变体的依赖
//指定库版本
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
def requested = details.requested
if (requested.group == 'androidx.lifecycle') {
if (requested.name.startsWith("lifecycle-runtime")) {
details.useVersion '2.2.0'
}
}
}
}
}
lifecycle
库中最重要的两个类是LifecycleOwner
和LifecycleObserver
,分别代表声明周期提供者和观察者。在AndroidX中androidx.fragment.app.Fragment
和androidx.core.app.ComponentActivity
都实现了LifecycleOwner
,表示他们是生命周期提供者,具有被观察的能力。其实原理很简单,我们也可以为某个组件自定义LifecycleOwner
:
import android.app.Activity;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
public class XLifecycleOwnerActivity extends Activity implements LifecycleOwner {
//用于管理组件生命周期状态信息,他可以被多个LifecycleObserver对象观察
private LifecycleRegistry lifecycleRegistry;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//创建注册者,构造方法中接受生命周期提供者
lifecycleRegistry = new LifecycleRegistry(this);
//设置当前生命周期状态为CREATED
lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
}
@Override
public void onStart() {
super.onStart();
//设置当前生命周期状态为STARTED
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
//实现LifecycleOwner接口的方法,返回lifecycleRegistry对象,这样我们自定义的Activity就能被LifecycleObserver观察了
return lifecycleRegistry;
}
}
LifecycleObserver
将类标记为生命周期观察者,它没有任何方法,而是依赖于OnLifecycleEvent
注释标记某个方法在组件的哪个生命周期状态下回调。
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
/**
* 生命周期观察者对象
*/
public class MyObserver extends LifecycleObserver {
//ON_ANY可以匹配所有生命周期方法
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
void onAny(LifecycleOwner owner, Lifecycle.Event event){}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
void onCreate(){}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void onStart(){}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
void onResume(){}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void onPause(){}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void onStop(){}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void onDestroy(){}
}
我们可以通过调用Lifecycle.addObserver()
传递观察者的实例来添加观察者
//LifecycleOwner的实现类(AppCompatActivity或者Fragment)中添加观察者
getLifecycle().addObserver(viewModel);
AndroidX库中有很多类库API都具有生命周期感知能力,比如上面讲到的LiveData
和ViewModel
,本质上就是通过LifecycleObserver
观察组件的声明周期方法。在平时开发过程中,我们并不需要做额外的动作就可以方便的处理组件的声明周期。
上面我们主要学习了AndroidX架构组件中的一些类库,这些类库可以让我们轻松优雅的实现MVVM模式,MVVM中最关键的就是VM组件ViewModel
,其他的都是一些辅助类库。使用架构组件实现MVVM模式步骤如下:
gradle.properties
中配置android.useAndroidX=true
启用AndroidX
作为根标签,使用@{}
表达式绑定数据Activity
或者Fragment
中使用databinding
对象引用控件和设置layout变量ViewModel
中实现数据请求,通过LiveData
实现与Activity
的数据传递,使用databinding
对象更新layout变量实现页面刷新