Android Architecture Component

什么是 architecture component?解决了什么问题?

它是一组库,能帮助你设计出一个具有健壮的,易测的,可维护的应用程序。

由哪些库组件的

  1. LifeCycle-Aware Components
  2. LiveData
  3. ViewModel
  4. Room

这些库都有什么作用

LifeCycle-Aware Components

  1. 什么是LifeCycle-Aware Components

    一个 LifeCycle-Aware Components 是由 Lifecycle,LifecycleOwner组成的。

    (1) 什么是 Lifecycle?

    Lifecycle 是一个用来保存组件生命周期的状态信息的类,它允许其他对象观察这些状态信息。主要使用了Event 和 State 这两个枚举类来跟踪绑定的组件的生命周期状态信息。

    • Event

      生命周期事件。这些事件与 activities 和 fragments 的事件形成映射关系。

    • State

      组件的生命周期状态,由 Lifecycle 对象进行跟踪。

lifecycle state

​ (2) 什么是 LifecycleOwner ?干啥用的呢?

​ LifecycleOwner 是只有一个方法的接口,表示这个类有一个 Lifecycle。只有一个 getLifecycle() 方法。

  1. 适用场景

    如果一个库提供的类需要使用 Android 生命周期,建议使用 lifecycle-aware component。

    a. 从粗糙的显示界面到精细的显示界面的切换。

    b. 停止和开始视频缓冲。

    c. 开始和停止网络连接。

    d. 暂停和恢复动画效果。

  2. 示例

没有 LifeCycle 时,我们是这么实现定位功能的:

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

使用 LifeCycle 来实现定位功能:

class MyLocationListener implements LifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       lifecycle.addObserver(this);
       ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void start() {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void stop() {
        // disconnect if connected
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

LiveData

  1. 什么是 LiveData

    LiveData 是一种持有可被观察数据的类。和其他可被观察的类不同的是,LiveData 是有生命周期感知能力的,这意味着它可以在 activities, fragments 或者 services 生命周期是活跃状态时更新这些组件。那么什么是活跃状态呢?lifecycle 中提到的 STARTED 和 RESUMED就是活跃状态,只有在这两个状态下LiveData 是会通知数据变化的。

    使用 LiveData,必须配合实现了 LifecycleOwner 的对象。当对应的生命周期对象 DESTORY 时,才能移除观察者。对于 activities 和 fragments 非常重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。

  2. LiveData 优点

    • 界面与数据保持一致性。
    • 避免内存泄漏。
    • 不会再产生由于Activity处于stop状态而引起的崩溃。如果观察者的生命周期是不活跃的,例如 activity 处于后台,那么将不会收到任何 LiveData 事件。
    • 不再需要手动的管理生命周期。
    • 总是实时刷新数据。当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据
    • 适当的configuration changes。如果 activity 或者 fragment 因为 configuration change 重新创建,例如设备旋转,立刻能收到最新的数据。
    • 资源共享。使用单例模式来扩展 LiveData 包装系统服务,数据可以在你的应用程序间共享。
  3. LiveData 示例

    LiveData 其实是一个包装类,适用于任何数据类型,通常与 ViewModel 共同使用。如下示例演示了如何创建一个 LiveData 对象。

    public class UserProfileViewModel extends ViewModel {
        ...
        // private User user;
        private LiveData<User> user;
        public LiveData<User> getUser() {
            return user;
        }
    }
    

ViewModel

  1. 什么是 ViewModel

    ViewModel设计的目的就是存放和处理和UI相关的数据,并且这些数据不受配置变化(Configuration Changes,例如:旋转屏幕,组件被系统回收)的影响。

  2. ViewModel 的作用

(1)避免由于(Activity/Fragment)被系统随时销毁或重新创建引起的数据丢失。对于简单的数据可以使用 onSaveInstanceState()保存,在onCreate()中恢复。对于少量的用户数据,比如UI状态是没有问题的。但是对于大量的数据,比如用户列表,这样做就会不合适。

(2)避免 UI 组件自己管理的维护成本以及容易产生的内存泄漏。由于 UI 组件频繁的异步请求,需要很多时间等待结果回调,UI 组件需要人为的管理这些回调。不仅浪费资源,还容易因为界面已经销毁而产生内存泄漏。

(3)避免职责过度集中。UI 组件需要对用户的操作作出响应,并且处理和操作系统的通信,同时还需要从数据库或者网络载入对应的数据。一个类所需要负责的事情太多了,不仅使代码臃肿,也造成了测试难度加大。

  1. ViewModel 示例

如下示例,定义一个 ViewModel:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}

访问 ViewModel

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

当 Activity 旋转屏幕等重新创建时,oncreate 将收到与第一次创建 Activity 时相同的 MyViewModel 实例;当 Activity 销毁时,系统会调用 ViewModel 对象的 onCleared() 方法来清除数据。

注意:ViewModel 绝对不可以引用 view、Lifycycle 或者任何持有 context 的类。

  1. ViewModel 的生命周期

    ViewModel只有在Activity finish或者Fragment detach之后才会销毁。

viewmodel lifecycle

Room

  1. 什么是 Room

    Room 在 SQLite 上提供了一个方便访问的抽象层。最常使用的场景是缓存一些相关的数据,当设备无法连接上网络时,用户仍然可以浏览内容;当设备连上网络时,立刻更新用户数据。

  2. Room 的组成

    三个重要的组件:

    • Database:包含数据库访问入口

    • Entity:表示数据库中的表

    • DAO:包含访问数据库的方法

    room architecture

如何使用这些库

如何添加这些组件到工程

  1. 在根目录的 build.gradle 中,添加 google()

    allprojects {
        repositories {
            jcenter()
            google()
        }
    }
    
  2. 在 app 或者 要使用到的 module 的 build.gradle 中,添加类库依赖。

    dependencies {
        // ViewModel and LiveData
        implementation "android.arch.lifecycle:extensions:1.1.0"
        // alternatively, just ViewModel
        implementation "android.arch.lifecycle:viewmodel:1.1.0"
        // alternatively, just LiveData
        implementation "android.arch.lifecycle:livedata:1.1.0"
    
        annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
    
        // Room (use 1.1.0-alpha1 for latest alpha)
        implementation "android.arch.persistence.room:runtime:1.0.0"
        annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
    
        // Paging
        implementation "android.arch.paging:runtime:1.0.0-alpha5"
    
        // Test helpers for LiveData
        testImplementation "android.arch.core:core-testing:1.1.0"
    
        // Test helpers for Room
        testImplementation "android.arch.persistence.room:testing:1.0.0"
    }
    

    如果使用 Java 8,建议使用下面的类库取代 android.arch.lifecycle:compiler

    dependencies {
        // Java8 support for Lifecycles
        implementation "android.arch.lifecycle:common-java8:1.1.0"
    }
    

    单独使用 LifeCycles,不使用 LiveData 或 ViewModel,则建议使用下面的类库

    dependencies {
        // Lifecycles only (no ViewModel or LiveData)
        implementation "android.arch.lifecycle:runtime:1.1.0"
        annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
    }
    

如何应用

举个简单的例子:

以编写一个个人信息页面为例,由UserProfileFragment.java和对应的布局文件user_profile_layout.xml组成。

界面搭建

为了展示界面,我们需要两个数据:

  • The User ID:用户的 ID,用于标识唯一的用户。
  • The User object:个人信息的 POJO

由于个人信息不应该随着界面的销毁而重新初始化,并且通常有根据用户 ID 请求网络获取完整用户信息的需求,因此,使用 ViewModel 来存储用户数据。

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}

因此,我们现在有三个文件:

  • user_profile.xml: 个人信息布局文件
  • UserProfileViewModel.java: 为界面提供数据的类
  • UserProfileFragment.java: UI 界面展示类,负责数据展示和界面交互
public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

接下来,当 ViewModel 的 user 字段被设置,我们需要一个手段来通知 UI,这就用到了 LiveData。

LiveData的使用分为两个步骤:

  1. 用 LiveData<User> 来取代 User 字段,这样当数据更新时,fragment 可以收到通知。
public class UserProfileViewModel extends ViewModel {
    ...
    // private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}
  1. 在 fragment 中观察数据并更新 UI
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

通过以上两个步骤的 LiveData 使用,每当数据更新时,onChanged 回调会被执行,UI可以在这里进行刷新。

tips:

(1)不需要在 fragment 的 onStop 方法中注销这个观察者,这就是 LiveData 的好处啦,除非 fragment 处于活跃状态,否则它不会执行回调函数;同时,当 fragment onDestroy 时,LiveData 会自动移除观察者。

(2)不需要处理 configuration changes(例如手机屏幕旋转)。ViewModel 会在屏幕旋转时,自动存储数据。当 fragment 重新创建时,会收到同一个 ViewModel 实例。

获取数据

前面已经把界面搭建好了,下面关心如何获取数据。

  1. 从网络获取数据

这里以 Retrofit 类库作为例子。

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

可以直接在 ViewModel 中通过 Webservice 来获取数据,但是这样会使应用难以维护。ViewModel 承担了太多职责,代码太快臃肿。另外,ViewModel 与 Activity 或 Fragment 的生命周期相绑定,当生命周期结束以后,ViewModel 会释放掉相关的数据。所以把获取数据的功能,指派给 Repository 模块。

如下代码,UserRepository 使用 WebService 来获取数据。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

然后修改 UserProfileViewModel 来使用 UserRepository:

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

现在,UserProfileViewModel 并不知道数据是从 Webservice 获取来的,也就是说,我们可以根据需要替换我们获取数据的实现方式。

  1. 从缓存获取数据

使用网络获取数据的问题是,如果用户离开了 fragment 然后再回来,应用需要重新通过网络获取数据,这样做有两个坏处:浪费宝贵的网络带宽和迫使用户等待新的查询完成。因此,增加从缓存获取数据的方法来避免这些问题。

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}
  1. 从数据库中获取数据

如果用户杀死这个 App,那么再回来,就无法从 cache中获取数据了,需要再次从网络拉取数据。隔了一段时间用户重新打开相同的页面仍然需要从网络获取相同的数据。Room 闪亮登场啦。

下面详细介绍如何使用 Room。

  1. 定义 User 类,并使用 @Entity 注解,标记这个类为数据库中的一张表
@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}
  1. 创建一个 database 类继承 RoomDatabase。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

注意:MyDatabase 是一个抽象类,Room 会自动提供它的实现类。

  1. 创建一个 DAO ,将 user 数据插入到数据库中。
@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

然后,在 MyDatabase 中引用 UserDao

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

注意:UserDaoload()方法返回的是LiveData<User>。这就是为什么Room能够在数据库内容发生改变的时候通知对数据库内容感兴趣的类。

然后修改 UserRepository 包含 Room 数据源

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}

从上面的代码可以看出,即使是改变了UserRepository中的数据来源,也无需对UserProfileViewModel或者UserProfileFragment作任何更改。这就是抽象出了数据层的灵活性。

现在基本上就解决之前提到的一些问题。如果已经查看过某个用户的数据,那么再次打开这个用户的页面时,数据可以瞬间被展示。如果数据过期的话,还可以立刻去更新数据。当然,如果你规定了数据过期就不允许展示,也可以展示从网络获取的数据。

最终的架构

architecture

总结

  1. LifeCycle-Aware Components 是能够感知生命周期的组件。如果一个类库需要关心 Android 的生命周期,比如开始和停止视频的缓冲,用户通常需要自己管理 Android 的各个生命周期。使用 LifeCycle,这些繁琐的工作就可以不再操心,可以有效的避免内存泄漏,同时可以做到代码功能的解耦。
  2. LiveData 是能够感知生命周期的数据,不需要开发者自己关心组件的生命周期,只需要通过观察者,就可以在生命周期的激活状态下更新 UI 数据。通常与 ViewModel 共同使用。
  3. ViewModel 用来存储与管理 UI 数据。
    (1)由于他的生命周期从组件 oncreate 创建时就存在,直到组件被彻底销毁才会消失。期间如果旋转屏幕,组件重新创建,会得到同一个 ViewModel 实例。所以 ViewModel 可以用于处理 UI 组件的相关数据。
    (2)由于 ViewModel 在一个 Activity 中只有一个实例,并且贯穿整个 Activity的生命周期。所以 ViewModel 可用于 fragment 之间共享数据。减轻 Activity 的工作,同时使 fragment 之间解耦。
  4. Room 提供了一个 SQLite 之上的抽象层,使得在充分利用 SQLite 功能的前提下简单顺畅的访问数据库。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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