[翻译]Android Architecture指南

原文地址:https://developer.android.com/topic/libraries/architecture/guide.html#addendum

现在按照google官方发布的android architecture来一步步的深入使用。

想象一下,我们构建一个用来显示用户信息的程序,用户信息是使用REST API从我们自己私人的服务器上获取的。

构建用户界面

程序中包含一个UserProfileFragment.java和他的布局文件user_profile_layout.xml.
为了用户界面的显示,我们的数据model需要有两个数据字段。

  • User ID 用户的标识,最好使用传递参数的方式将这些参数传递给fragment,如果Android操作系统销毁您的进程,这些信息将被保留,以便在您的应用下次重新启动时使用。
  • User object 保存用户数据的实体类

我们将会创建一个继承自ViewModelUserProfileViewModel来保存这些信息。

ViewModel为特定的UI组件(如Activity或Fragment)提供数据,并处理与数据处理业务部分的通信,例如调用其他组件来加载数据或转发用户修改操作。 ViewModel不需要知道UI,并且不受配置更改的影响,例如由于旋转而重新创建活动。

现在我们有3个文件。

  • user_profile.xml 用户信息的显示界面
  • UserProfileViewModel.java 为UI准备数据的类
  • UserProfileFragment.java 在ViewModel中显示数据并对用户交互作出反应的UI控制器。

现在我们开始用代码实现:

public class UserProfileViewModel extends ViewModel{
  private String userId,
  private User user;
  public void init(String userId){
    this.userId = userId;
  }
  public User getUser(){
    return user;
  }
}
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的用户字段被设置的时候,我们需要通知UI,这时候就需要引入LiveData。

LiveData是一个可观察的数据持有者,它允许应用程序中的组件观察LiveData对象的更改,而不会在它们之间创建明确的和严格的依赖关系路径。 LiveData还遵循应用程序组件(活动,片段,服务)的生命周期状态,并做正确的事情来防止对象泄漏,使您的应用程序不会消耗更多的内存。

注意:如果你已经使用了例如RxJava和Agrea的库的话,你可以继续使用他们用来代替LiveData,但是,当你使用它们或其他方法时,请确保正确处理生命周期,以便在相关的LifecycleOwner停止时停止数据流,并在销毁LifecycleOwner时销毁数据流。您还可以添加android.arch.lifecycle:reactivestreams组件以将LiveData与Rxjava2一起使用。

现在我们将UserProfileViewModel中的User字段替换成LiveData<User>以便于当数据改变的时候Fragment可以被通知到。LiveData最厉害的一点就是当不再需要的时候可以自动清理掉引用,从而不会引起生命周期相关问题。

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

现在来修改UserProfileFragment用来观察数据从而通知UI

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState){
  super.onActivityCreated(savedInstanceState);
  viewModel.getUser().observe(this,user -> {
    //更新UI
  });
 }

只要用户数据一更新,UI就会被刷新。
如果你熟悉使用可观察回调的其他库,您可能已经意识到我们不必重写片段的onStop()方法来停止观察数据。这对于LiveData来说是不必要的,因为它是生命周期感知的,这意味着它不会调用回调,除非片段处于活动状态(收到onStart(),但没有收到onStop())。当片段收到onDestroy()时,LiveData也会自动移除观察者。
我们也不需要为配置更改(例如,用户旋转屏幕)而做一些额外的工作。当配置更改的时候,ViewModel会自动保存数据,所以当新的fragment出现的时候,它将接收到同一个ViewModel的实例,回调将被立即调用当前数据。这就是ViewModel不能直接引用Views的原因。他们可以超越View的生命周期。请参阅ViewModel的生命周期

获取数据

现在我们已经将Fragment和ViewModel连接到一起了,但是ViewModel如何获取用户数据,下面我们使用Retrofit来获取我们用户数据。

public interface Webservice{
  @GET("/users/{user}")
  Call<User> getUser(@Path("user") String userId);
}

我们需要一个新的模块,存储库模块负责处理数据操作。他们提供了一个干净的API到应用程序的其余部分。他们知道从何处获取数据以及在更新数据时调用哪些API。您可以将它们视为不同数据源(持久模型,Web服务,缓存等)之间的中介。

UserRepository用来获取用户数据

public clas UserRepository{
  private Webservice webservice;
  public LiveData<User> getUser(int userId){
    final MutableLiveData<User> data = new MutableLiveData<>();
    webservice.getUser(userId).enqueue(new Callback<User>(){
      @Override
      public void onResponse(Call<User> call,Response<User> response){
        data.setValue(response.body());
      }
    });
    return data;
  }
}

即使存储库模块看起来不必要,它也有一个重要的作用。它从应用程序的其余部分提取数据源。现在我们的ViewModel不知道数据是由Webservice获取的,这意味着我们可以根据需要可以从其他地方提取数据。

管理组件之间的依赖关系

上面的UserRepository类需要Webservice的一个实例来完成它的工作。他可以创建WebService的实力,这会使代码复杂化和复制(例如,每个需要Webservice实例的类将需要知道如何用它的依赖关系来构造它)。此外,UserRepository可能不是唯一需要Web服务的类。如果每个类创建一个新的WebService,这将会造成资源依赖严重。
可以使用依赖注入来解决这个问题,比如Dagger

连接ViewModel和repository

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

  @Inject
  public UserProfileViewModel(UserRepository userRepo){
    this.userRepo = userRepo;
  }
  public void init(String userId){
    if(this.user != null){
      return;
    }
    user = userRepo.getUser(userId);
  }

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

缓存数据

上面的存储库实现对抽象调用Web服务是有好处的,但是因为它只依赖于一个数据源,所以它不是很实用。 上面的UserRepository实现的问题是,在获取数据之后,它不保留在任何地方。如果用户离开UserProfileFragment并返回到它,应用程序将重新获取数据。这是不好的,原因有两个:浪费宝贵的网络带宽并强制用户等待新的查询完成。为了解决这个问题,我们将添加一个新的数据源到我们的UserRepository中,它将把用户对象缓存在内存中。

@Singleton
public class UserRepository{
  private Webservice webservice;
  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);
    webservice.getUser(userId).enqueue(new Callback<User>{
      @Override
      public void onResponse(Call<User> call, Response<User> response){
        data.setValue(response.body());
      }
    });
    return data;
  }
}

数据持久化

在我们当前的实现中,如果用户旋转屏幕或离开并返回到应用程序,则现有UI将立即可见,因为存储库从内存中高速缓存中检索数据。但是,如果用户离开应用程序,并被系统杀死该进程后数小时后回来,会发生什么?
此时我们就需要对数据进行持久化操作,从而避免重复从网络中获取数据而造成的资源浪费。

处理这个问题的正确方法是使用持久化模型。这时就需要使用Room来帮忙了。
Room是一个对象映射库,提供本地数据持久化的功能。在编译时,它会根据模式验证每个查询,以便断开的SQL查询导致编译时错误。还抽象出一些使用原始SQL表和查询的底层实现细节。它还允许观察对数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改。另外,它明确定义了解决常见问题的线程约束,例如访问主线程上的存储。

如果你的app已经使用了其他的持久化解决方案例如ORM,你就不需要替换现有的方案为Room。
使用Room,我们需要定义我们自己的本地模式,首先,需要在你的User类上添加@Entity注解标记为数据库中的一个表。

@Entity
class User{
  @PrimaryKey
  private int id;
  private String name;
  private String lastname;
}

接着,创建一个数据库类集成RoomDatabase

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

注意到MyDatabase是抽象的,Room会自动实现它。
现在我们需要插入数据到数据库中的方法,所以我们需要创建一个数据库访问实体类

@Dao
public interface UserDao{
  @Insert(onConflict = REPLACE)
  void save(User user);
  @Query("SELECT * FROM user WHERE id=:userId")
  LiveData<User> load(String userId);
}

然后在数据库类中引用DAO

public abstract class MyDatabase extends RoomDatabase{
  public abstract UserDao userDao();
}

注意load方法返回一个LiveData <User>。Room知道数据库何时被修改,当数据改变时它会自动通知所有活动的观察者。因为它使用的是LiveData,所以这将是有效的,因为只有至少有一个活动的观察者才会更新数据。
现在我们需要修改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 = weservice;
    this.userDao = userDao;
    this.executor = executor;
  }
  public LiveData<User> getUser(String userId){
    refreshUser(userId);
    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。这是抽象提供的灵活性。这对于测试也很好,因为在测试UserProfileViewModel的时候可以提供一个伪造的UserRepository。 现在我们的代码是完整的。如果用户以后回到相同的用户界面,他们会立即看到用户信息,因为我们队对数据进行了持久化的操作。同时,如果数据陈旧,我们的仓库将在后台更新数据。当然,根据您的使用情况,如果数据太旧,您可能不希望显示持久化数据。

下图展示的就是所有模块之间是如何进行交互的。
[图片上传失败...(image-ab6985-1511161885983)]

附录:暴露网络状态

//a generic class that describes a data with a status
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

从网络加载数据然后从磁盘显示数据是通用的方式,所以我们需要创建一个可以在多个地方重复使用的帮助类,NetworkBoundResource,一下是决策树:
[图片上传失败...(image-fa9dfd-1511161885983)]

下面是NetworkBoundResource的代码

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be
    // fetched from the network.
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    @MainThread
    protected void onFetchFailed() {
    }

    // returns a LiveData that represents the resource, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

请注意上面定义了两种数据类型(ResultType,RequestType),因为从Api返回的数据类型可能与本地使用的数据类型不匹配。

另外请注意,上面的代码使用ApiResponse进行网络请求。 ApiResponse是Retrofit2.Call类的一个简单包装,将其响应转换为LiveData。
以下是NetworkBoundResource类的rest的实现:

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

现在我们可以使用NetworkBoundResource来在repository中编写我们自己的磁盘和网络用户信息获取实现:

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

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

推荐阅读更多精彩内容