Android Architecture Components(AAC) 初探

简介

Android Architecture components 是一组 Android 库,它可以帮助我们以一种健壮的、可测试和可维护的方式构建 APP。

注意: 这个架构目前仍处于 alpha 开发状态。在正式版发布之前 API 可能会改变,你可能会遇到稳定性和性能问题。

下面将会介绍使用生命周期感知(lifecycle-aware)组件去构建APP:

  • ViewModel - 提供了创建和检索(类似于单例)绑定到特定生命周期对象的方法, ViewModel 通常是存储 View 数据的状态并与其他的组件进行通信,如数据存储组件和处理业务逻辑的组件。想了解更多,请查看 ViewModel 指南
  • LifecycleOwner/LifecycleRegistryOwner - LifecycleOwnerLifecycleRegistryOwner 都是在 LifecycleActivityLifecycleFragment 类中实现的接口。你可以将自己的组件也实现这些接口以观察生命周期所有者的状态变化。想了解更多,请查看 Lifecycles 指南
  • LiveData - 允许您观察应用程序的多个组件的数据更改,而不会在它们之间创建明确的,刚性的依赖路径。LiveData 关心应用程序组件复杂的生命周期,包括 Activity , Fragmnet , Service 或者 APP 中定义的任何 LifecycleOwnerLiveData 会管理观察者的订阅状态,LifecycleOwner 对象 stopped 时会暂停订阅,以及 LifecycleOwner 对象 Finished 时会取消订阅。想了解更多,请查看 LiveData 指南

如果你已经安装了 Android Studio 2.3 或者更高版本并且熟悉 Activity 生命周期 ,那么你可以继续向下看了。

配置环境

  • 下载源码,并导入 Android Studio

  • 运行 【Step 1】,界面大概是下面这样的

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 被销毁,此定时器将重置。

您可以使用 ViewModelActivityFragment 的整个生命周期中保留数据。如上一步所示,使用 Activity 来管理 APP 的数据是不明智的。 ActivityFragment 是一种短命的对象,它们随着用户与应用程序交互而频繁创建和销毁。 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);

thisLifecycleOwner 的实例。 只要 LifecycleOwner 的范围是活跃的(alive),框架就可以使 ViewModel 处于活跃状态。如果其所有者因为配置更改(例如屏幕旋转)而被毁坏(destroyed),ViewModel 则不会被销毁。生命周期所有者会重新连接到现有的 ViewModel ,如下图所示:

Viewmodel Lifecycle

注意ActivityFragment 的范围从 createdfinished(或终止),您不能与被 destroyed 混淆。请记住,当一个设备旋转时, Activity 被销毁,但是与它关联的任何 ViewModel 的实例并不会被销毁。

试试看

运行应用程序并确认执行以下任一操作时定时器不会重置:

  1. 旋转屏幕。
  2. 导航到另一个应用程序,然后返回。
定时器不会重置

但是,如果你或系统退出应用程序,则定时器将重置。

注意: 系统会在生命周期所有者(例如 ActivityFragment )的整个生命周期中将 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 对象的引用。

可以设置 ActivityFragment 来观察数据源,当数据更改时将会接收通知,而不是直接从 ViewModel 修改视图。这就是观察者模式。

注意: 要想将数据可被观察,可将数据包装在LiveData类中。

如果你使用过 Data Binding Library 或其他响应式库(如RxJava),你应该很熟悉观察者模式。LiveData 是一个特殊的可观测类,它可以感知生命周期,并且只在声明周期活跃时通知观察者。

LifecycleOwner

ChronoActivity3LifecycleActivity 的子类,他提供了声明周期的状态。如下:

public class LifecycleActivity extends FragmentActivity implements LifecycleRegistryOwner {...}

LifecycleRegistryOwner 接口的作用是用来将 ViewModelLiveData 绑定到 ActivityFragment 生命周期中。如果Fragment 的话可以继承 LifecycleFragment 类。

Update ChronoActivity

  1. 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);
    }
}
  1. 接下来在 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;
    }
}
  1. 运行 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 有一个初步的认识了。

参考

Android Architecture Components

Android lifecycle-aware components codelab

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容