Android Architecture Components(
简介
Android Architecture components 是一组 Android 库,它可以帮助我们以一种健壮的、可测试和可维护的方式构建 APP。
注意: 这个架构目前仍处于 alpha 开发状态。在正式版发布之前 API 可能会改变,你可能会遇到稳定性和性能问题。
下面将会介绍使用生命周期感知(lifecycle-aware)组件去构建APP:
-
ViewModel - 提供了创建和检索(类似于单例)绑定到特定生命周期对象的方法,
ViewModel
通常是存储View
数据的状态并与其他的组件进行通信,如数据存储组件和处理业务逻辑的组件。想了解更多,请查看 ViewModel 指南。
-
LifecycleOwner/LifecycleRegistryOwner -
LifecycleOwner
和LifecycleRegistryOwner
都是在LifecycleActivity
和LifecycleFragment
类中实现的接口。你可以将自己的组件也实现这些接口以观察生命周期所有者的状态变化。想了解更多,请查看 Lifecycles 指南。
-
LiveData - 允许您观察应用程序的多个组件的数据更改,而不会在它们之间创建明确的,刚性的依赖路径。
LiveData
关心应用程序组件复杂的生命周期,包括Activity
,Fragmnet
,Service
或者 APP 中定义的任何LifecycleOwner
。LiveData
会管理观察者的订阅状态,LifecycleOwner
对象stopped
时会暂停订阅,以及LifecycleOwner
对象Finished
时会取消订阅。想了解更多,请查看 LiveData 指南。
如果你已经安装了 Android Studio 2.3 或者更高版本并且熟悉 Activity 生命周期 ,那么你可以继续向下看了。
配置环境
-
下载源码,并导入 Android Studio
-
运行 【Step 1】,界面大概是下面这样的
- 旋转屏幕,计时器会从0开始重新计时。
这部分代码比较简单,首先是布局文件:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.android.lifecycles.step1.ChronoActivity1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:id="@+id/hello_textview"/>
<Chronometer
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/hello_textview"
android:layout_centerHorizontal="true"
android:id="@+id/chronometer"/>
</RelativeLayout>
然后在 Activity 的 onCreate
函数中开始计时即可,如下:
public class ChronoActivity1 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer);
chronometer.start();
}
}
Chronometer
是 Android 提供的一个计时器组件,该组件继承自 TextView
,它显示从某个起始时间开始过去了多少时间。
想要在屏幕旋转时计时器状态不被更改,您可以使用 ViewModel
,因为此类可以在配置更改(例如屏幕旋转)时不会被回收。
注: 在
mainfest
文件中可以配置Activity
在屏幕旋转时不重新创建Activity
,从而保持计时器的状态,这种情况不在本篇谈论范围内。
注:如源码中的 google maven 不能访问,可将项目中 gradle 文件改为如下
repositories { jcenter() maven { url "https://dl.google.com/dl/android/maven2/" } }
添加 ViewModel
在此步骤中,您可以使用 ViewModel
在屏幕旋转之间保持状态,并解决您在上一步中观察到的行为(旋转屏幕时计时器会重置)。在上一步中,运行了一个显示计时器的 Activity
。当配置更改(如屏幕旋转)会导致 Activity
被销毁,此定时器将重置。
您可以使用 ViewModel
在 Activity
或 Fragment
的整个生命周期中保留数据。如上一步所示,使用 Activity
来管理 APP 的数据是不明智的。 Activity
和 Fragment
是一种短命的对象,它们随着用户与应用程序交互而频繁创建和销毁。 ViewModel
还适用于管理与网络通信相关的任务,以及数据的操作和持久化。
使用ViewModel来保持计时器的状态
首先需要创建一个ViewModel,如下
public class ChronometerViewModel extends ViewModel {
@Nullable
private Long startDate;
@Nullable
public Long getStartDate() {
return startDate;
}
public void setStartDate(final long startDate) {
this.startDate = startDate;
}
}
打开 ChronoActivity2
并检查该 Activity
如何检索并使用 ViewModel
的,如下:
public class ChronoActivity2 extends LifecycleActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ViewModelProviders 会提供一个新的或者之前已经创建过的 ViewModel
ChronometerViewModel chronometerViewModel
= ViewModelProviders.of(this).get(ChronometerViewModel.class);
// 获取 chronometer
Chronometer chronometer = (Chronometer) findViewById(R.id.chronometer);
if (chronometerViewModel.getStartDate() == null) {
// 如果开始时间为 null , 那么这个 ViewModel 是刚被创建的
long startTime = SystemClock.elapsedRealtime();
chronometerViewModel.setStartDate(startTime);
chronometer.setBase(startTime);
} else {
// 否则这个 ViewModel 是从 ViewModelProviders 中被检索出来的,所以需要设置刚开始的开始时间
chronometer.setBase(chronometerViewModel.getStartDate());
}
chronometer.start();
}
}
其中
ChronometerViewModel chronometerViewModel
= ViewModelProviders.of(this).get(ChronometerViewModel.class);
this
是 LifecycleOwner
的实例。 只要 LifecycleOwner
的范围是活跃的(alive),框架就可以使 ViewModel
处于活跃状态。如果其所有者因为配置更改(例如屏幕旋转)而被毁坏(destroyed),ViewModel
则不会被销毁。生命周期所有者会重新连接到现有的 ViewModel
,如下图所示:
注意:
Activity
或Fragment
的范围从created
到finished
(或终止),您不能与被destroyed
混淆。请记住,当一个设备旋转时,Activity
被销毁,但是与它关联的任何ViewModel
的实例并不会被销毁。
试试看
运行应用程序并确认执行以下任一操作时定时器不会重置:
- 旋转屏幕。
- 导航到另一个应用程序,然后返回。
但是,如果你或系统退出应用程序,则定时器将重置。
注意: 系统会在生命周期所有者(例如
Activity
或Fragment
)的整个生命周期中将ViewModel
实例保存在内存中 。 系统不会将ViewModel
的实例持久化到长期存储。
使用 LiveData 包装数据
这一步使用自定义的 Timer
实现上一步的中的 chronometer
。将上面的逻辑添加到 LiveDataTimerViewModel
类中,将 Activity
的主要功能放在管理用户和UI之间的交互上。
Activity
会在定时器通知时更新UI。为了避免内存泄露, ViewModel
不应该持有 Activity 的实例。比如,配置更改(例如屏幕旋转)会使 Activity 被系统回收,如果这时 ViewModel
仍持有Activity 的实例那么 Activity 就不会被系统回收从而导致内存泄露 。系统会保留 ViewModel
的实例至到 Activity
或生命周期持有者不再存在。
注意: 在
ViewModel
中持有 Context 或者 View 的实例可能会导致内存泄露。避免引用 Context 或者 View 类的实例的字段。ViewModel
中的 onCleared() 方法可用于取消订阅或清除对具有较长生命周期的其他对象的引用,但不用于清除 Context 或者 View 对象的引用。
可以设置 Activity
或 Fragment
来观察数据源,当数据更改时将会接收通知,而不是直接从 ViewModel
修改视图。这就是观察者模式。
注意: 要想将数据可被观察,可将数据包装在LiveData类中。
如果你使用过 Data Binding Library 或其他响应式库(如RxJava),你应该很熟悉观察者模式。LiveData
是一个特殊的可观测类,它可以感知生命周期,并且只在声明周期活跃时通知观察者。
LifecycleOwner
ChronoActivity3
是 LifecycleActivity
的子类,他提供了声明周期的状态。如下:
public class LifecycleActivity extends FragmentActivity implements LifecycleRegistryOwner {...}
LifecycleRegistryOwner
接口的作用是用来将 ViewModel
和 LiveData
绑定到 Activity
或 Fragment
生命周期中。如果Fragment 的话可以继承 LifecycleFragment
类。
Update ChronoActivity
- 在
ChronoActivity3
中添加如下代码,在subscribe()
方法中创建订阅者:
public class ChronoActivity3 extends LifecycleActivity {
private LiveDataTimerViewModel mLiveDataTimerViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chrono_activity_3);
mLiveDataTimerViewModel = ViewModelProviders.of(this).get(LiveDataTimerViewModel.class);
subscribe();
}
private void subscribe() {
// 创建观察者
final Observer<Long> elapsedTimeObserver = new Observer<Long>() {
@Override
public void onChanged(@Nullable final Long aLong) {
String newText = ChronoActivity3.this.getResources().getString(
R.string.seconds, aLong);
((TextView) findViewById(R.id.timer_textview)).setText(newText);
}
};
mLiveDataTimerViewModel.getElapsedTime().observe(this, elapsedTimeObserver);
}
}
- 接下来在
LiveDataTimerViewModel
中设置计时时间,如下:
public class LiveDataTimerViewModel extends ViewModel {
private static final int ONE_SECOND = 1000;
private MutableLiveData<Long> mElapsedTime = new MutableLiveData<>();
private long mInitialTime;
public LiveDataTimerViewModel() {
mInitialTime = SystemClock.elapsedRealtime();
Timer timer = new Timer();
// Update the elapsed time every second.
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
final long newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000;
// setValue() 不允许在子线程中调用,所以需要post主线程执行
// 其实也可以直接使用 postValue() 方法,该方法会在内部post到主线程执行
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
//setValue时,观察者可监听到值的变化
mElapsedTime.setValue(newValue);
}
});
}
}, ONE_SECOND, ONE_SECOND);
}
public LiveData<Long> getElapsedTime() {
return mElapsedTime;
}
}
- 运行 APP 你会发现界面每秒都在更新,除非你导航到另外一个 APP。如果你的设备支持多窗口,或者旋转屏幕并不会影响APP显示。如下图:
注意:
LiveData
只有在 Activity 或者LifecycleOwner
活跃(active)时才发送更新。如果你导航到其他的APP,Activity 中的日志打印将会停止,至到你重新回到APP。当LiveData
对象的各自的生命周期所有者处于STARTED
或者RESUMED
,我们才称LiveData
是活跃的。
订阅生命周期事件
许多 Android 组件或库要求你
- 订阅或初始化组件或库
- 取消订阅或 stop 组件或库
没有做上面的步骤可能会导致内存泄露或者产生bugs。
生命周期所有者可以传递给生命周期感知组件生命周期的状态,以确保他们知道生命周期的当前状态。
你可以用下面的方法查询生命周期的状态:
lifecycleOwner.getLifecycle().getCurrentState()
上面的语句返回的是什么周期的状态,如 Lifecycle.State.RESUMED
, 或 Lifecycle.State.DESTROYED
。
实现 LifecycleObserver
接口的生命周期感知对象还可以观察生命周期所有者的状态的变化:
lifecycleOwner.getLifecycle().addObserver(this);
可以通过注解的方式,实现在各个生命周期事件执行相应的方法:
@OnLifecycleEvent(Lifecycle.EVENT.ON_RESUME)
void addLocationListener() { ... }
创建生命周期感知组件
在这里我们会创建一个对 Activity 生命周期所有者做出响应的组件,使用 Fragment 作为生命周期所有者时,可以采用类似的原则和步骤。
使用 Android framework 的 LocationManager
去获取经纬度并且展示给用户,这允许你
-
订阅更改并使用
LiveData
自动更新UI。 -
创建一个包装器,他可以根据 Activity 生命状态的来决定是注册还是注销对LocationManager的监听。
通常的做法是在 Activity 的 onStart()
或 onResume()
中注册监听器,在 onStop()
或 onPause()
方法中移除监听器,如下:
// 在 activity 中通常这样做
@Override
protected void onResume() {
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mListener);
}
@Override
protected void onPause() {
mLocationManager.removeUpdates(mListener);
}
看下我们是如何改造上面代码的,首先我们需要创建一个 BoundLocationManager
类,它需要实现 LifecycleOwner
接口并且需要传入 LifecycleRegistryOwner
的实例。 BoundLocationManager
类的实例绑定到 Activity 的生命周期上了,如下:
static class BoundLocationListener implements LifecycleObserver {
private final Context mContext;
private LocationManager mLocationManager;
private final LocationListener mListener;
public BoundLocationListener(LifecycleOwner lifecycleOwner,
LocationListener listener, Context context) {
mContext = context;
mListener = listener;
//想要观察 Activity 的声明周期,必须将其添加到观察者中。添加下面的代码
//才能是 BoundLocationListener 实例监听到生命周期
lifecycleOwner.getLifecycle().addObserver(this);
}
//可以使用 @OnLifecycleEvent 注解来监听 Activity 生命周期的变化
// 可以使用下面的注解来添加 addLocationListener() 方法
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
void addLocationListener() {
mLocationManager =
(LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mListener);
Log.d("BoundLocationMgr", "Listener added");
// Force an update with the last location, if available.
Location lastLocation = mLocationManager.getLastKnownLocation(
LocationManager.GPS_PROVIDER);
if (lastLocation != null) {
mListener.onLocationChanged(lastLocation);
}
}
// 可以使用下面的注解来移除 removeLocationListener() 方法
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void removeLocationListener() {
if (mLocationManager == null) {
return;
}
mLocationManager.removeUpdates(mListener);
mLocationManager = null;
Log.d("BoundLocationMgr", "Listener removed");
}
}
注意: 观察者可以观测到提供者的当前状态,所以不需要从构造函数调用
addLocationListener()
方法。他是在观察者添加到生命周期所有者的时候被调用的。
运行APP,在旋转屏幕的时候会有下面的打印
D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed
D/BoundLocationMgr: Listener added
D/BoundLocationMgr: Listener removed
使用 Android 模拟器改变设备的位置,UI 就会更新,如下图:
改变设备的位置在 Fragmnet 之间共享 ViewModel
在 Fragmnet 之间共享 ViewModel 之前,我们需要做一些前期准备
首先需要创建一个 Activity ,该 Activity 中包含两个 Fragment,在其布局文件中添加两个 Fragment 即可,如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.android.lifecycles.step5_solution.Activity_step5">
<fragment
android:id="@+id/fragment1"
android:name="com.example.android.lifecycles.step5_solution.Fragment_step5"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<fragment
android:id="@+id/fragment2"
android:name="com.example.android.lifecycles.step5_solution.Fragment_step5"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
其中 Fragment_step5
就是需要共享 ViewModel 的 Fragment,这个 Fragment 也很简单,只是包含了一个 SeekBar 控件,如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.lifecycles.step5_solution.Fragment_step5">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
然后就是创建一个包含 LiveData 的 ViewModel,如下:
public class SeekBarViewModel extends ViewModel {
public MutableLiveData<Integer> seekbarValue = new MutableLiveData<>();
}
运行这一步中的代码,你会发现这是两个彼此独立的SeekBar实例:
使用 ViewModel
在 Fragment 之间通信,以便当一个 SeekBar
更改时,另一个 SeekBar
将被更新:
使用 ViewMode 进行通信的代码如下:
public class Fragment_step5 extends Fragment {
private SeekBar mSeekBar;
private SeekBarViewModel mSeekBarViewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View root = inflater.inflate(R.layout.fragment_step5, container, false);
mSeekBar = (SeekBar) root.findViewById(R.id.seekBar);
mSeekBarViewModel = ViewModelProviders.of(getActivity()).get(SeekBarViewModel.class);
subscribeSeekBar();
return root;
}
private void subscribeSeekBar() {
// 当 SeekBar 变化的时候更新 ViewModel
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
Log.d("Step5", "Progress changed!");
mSeekBarViewModel.seekbarValue.setValue(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// 当 ViewModel 变化的时候更新 SeekBar
mSeekBarViewModel.seekbarValue.observe((LifecycleOwner) getActivity(),
new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer value) {
if (value != null) {
mSeekBar.setProgress(value);
}
}
});
}
}
注意: 你应该使用 Activity 作为生命周期的持有者,因为每个 Fragment 的生命周期都是独立的。
至此,你应该对 AAC 有一个初步的认识了。