Android Architecture ComponentsAndroid开发经验谈Android开发

翻译官方文档Guide to App Architecture

2017-09-16  本文已影响112人  lanceJin

官方文档链接:https://developer.android.google.cn/topic/libraries/architecture/guide.html

1.前言


由于用户在使用某一功能时会涉及不同的应用程序,需要不断地切换流程和任务。举个例子,在使用社交软件分享照片时,会需要打开摄像头,也会需要从图库中选择文件,而将数据返回社交软件的过程中,有可能会有电话打来。这一下子启动了许多应用程序,但是移动设备资源有限,操作系统随时会杀死一些应用程序来腾出空间给新的应用。所以应用程序组件的存在不由开发者控制,不应该存储数据或状态,更不应该彼此依赖。

之前通过生命周期进行相关操作,但这引入另一套逻辑,增加代码的复杂度。

2.常见架构原则


2.1.View层分离

使Activity和Fragment中的代码尽可能的少,只处理与界面或与操作系统的交互。因为这些类是操作系统和应用程序的中间件,不由开发者控制,所以应该最小化依赖。

2.2.Model层驱动

通过持久化模型驱动界面,具有两点好处:

持久化模型独立于组件,不受系统的控制。基于它的应用程序可以保证界面代码简单,同时业务逻辑具有可测试性。

3.推荐的应用框架


没有一种架构适用于所有场景。意味着,这是一个好的起点,但若已经有更适合的了,是不需要改变的。以网络获取并显示用户配置信息为例子,展示架构组件的使用方式。

3.1.建立用户界面

界面包含一个UserProfileFragment.java的组件类和user_profile_layout.xml的布局文件。为了驱动界面,基于ViewModel类创建UserProfileViewModel.java的数据模型来保存信息。数据元素主要包括:

一个ViewModel给一个特定的界面组件(Activity或Fragment)提供数据,并处理业务逻辑中数据相关的操作,如调用其它组件来加载数据或转发用户的修改。ViewModel与View隔离,不受屏幕旋转导致重建等Configuration Changes的影响。而Activity或Fragment则是UI Controller,与用户行为和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;
    }
}
public class UserProfileFragment extends LifecycleFragment {
    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);
    }
}

目前使用LifecycleFragment代替Fragment,等到lifecycles版本稳定后,支持库中的Fragment将实现LifecycleOwner接口。

ViewModel与Presenter最大的不同是,Presenter内部封装着一系列的行为,而ViewModel持有的是数据(状态),那么数据如何传递呢?这里就得提LiveData了。

LiveData是一个可被观察的数据持有者。它让应用中的组件观察自己的变化,却不需要显式的和刚性的依赖。LiveData同时会监听应用组件(Activity,Fragment,Services)的生命周期状态,并且做正确的事情来防止对象的内存泄露。

如果已经使用RxJava或Agera库,你可以继续使用来替代LiveData。不过当你使用它们时,确保正确处理生命周期,例如:LifecycleOwner调用了onStop()方法,需暂停相关的数据流;LifecycleOwner调用了onDestory()方法,需销毁相关的数据流。你也可以添加android.arch.lifecycle:reactivestreams工件来让LiveData和其它的响应流库(RxJava2)一起使用。

现在,用LiveData<User>来替代User,这样当数据改变时,Fragment将会收到通知。更好的是,LiveData是支持生命周期的,当自己不再被使用时,自动清理引用。

public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}
// UserProfileFragment.java
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ...
    viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
    ...
    viewModel.getUser().observe(this, user -> {
        // update UI
    });
 }

与其它使用观察回调的库不同。当Fragment不处于活跃状态时,是不会回调的,所以不需要手动在Fragment的onStop()方法中停止观察数据;当Fragment销毁了,LiveData将会移除观察者,释放资源。

不需要做额外的事,ViewModel在配置改变时,将自动恢复相同的ViewModel实例及对当前数据的回调。

3.2.获取数据

将ViewModel和Fragment连接后,还需要ViewModel获取到用户数据。这里,假设使用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中直接获取数据并复制给User对象,但随着项目的变大,将越来越难以维护,而且给ViewModel太多的职责,与单一职责的原则相违背。此外,ViewModel的活动范围与Activity或Fragment的生命周期相关,即生命周期结束后将丢失所有数据。为此,引入了Repository这个概念。

Repository是专门用来处理数据操作的。知道从什么地方获取数据和调用什么接口更新数据,是不同数据源之间的中转,例如持久化模型、Web服务、缓存等。

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;
    }
}

这样将数据源从应用其它部分独立出来,ViewModel不知道与什么数据源交互,便于替换。但是UserRepository需要Webservice实例,若通过构造方法提供,每个使用Webservice的类都得知道它的构造方法,使依赖变得复杂,同时都创建对象将占用大量资源。这里提供两种方法参考:

这些模式可以方便地扩展代码,因为提供了清晰的方式管理依赖,而不需要增加其它代码。更重要的是它们都很容易测试

3.3.ViewModel和Repository
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;
    }
}
3.4.缓存数据

若Repository只实现一个数据源,则会显得不太实用。需增加数据的持有,当用户再次进入界面时不用重新加载,不然会浪费宝贵的网络带宽和迫使用户等待新的请求。为此,给UserRepository添加新的数据源,在内存中缓存User对象。

@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;
    }
}
3.5.持久化数据

内存缓存只能针对屏幕切换等当前应用进程存在的情况,但若进程被系统杀死,则仍需重新网络请求。为了不使用移动网络重新获取相同的数据,可以通过缓存Web请求来解决。可当场景是通过两个不同类型的请求来获取相同类型的数据时,会出现显示不一致的问题,导致需手动合并它们。正确的做法是数据持久化,在数据库中完成合并操作。

Room是一个对象映射库,通过最少的样板代码实现本地数据持久化。在编译时会验证每条查询的样式,将中断SQL查询的错误反映在编译期而不是运行时。Room封装了一些与原始SQL表和查询相关的底层实现细节,也允许观察数据库中数据(包括集合和连接查询)的变化,并通过LiveData对象来反映。此外,它还显式地定义了线程约束来规避常见问题,比如主线程上访问存储。

如果熟悉其它持久化解决方案像SQLite ORM或不同的数据库Realm,就不需要更换,除非Room的功能与用例很契合。

// 创建表相关的样式
@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}
// 创建数据访问对象
@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}
// 创建数据库抽象类,编译自动实现
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

load方法能直接返回LiveData<User>,是很便利的。当Room发现数据库数据被改变了,且至少有一个活跃的相关的观察者,将自动通知它们更新相关操作。但是alpha1版本中,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,这有利于测试,例如,提供伪造的UserRepository来测试UserProfileViewModel。

有些情况下,例如下拉刷新,通过界面向用户展示当前进行中的网络操作是很重要的。好的做法是将界面操作与实际数据分离,因为数据容易被各种原因更新(例如,获取一个朋友列表,可能会再次获取到相同的user并触发LiveData更新)。从界面的角度来看,动态请求实际上只是另一个数据点,类似其它任何数据片段(如,User对象)。这里有两种常用解决方法:

需注意单一数据源原则。不同的后端接口返回相同的数据(粒度不同)是一种常见情况,但在请求的间隙,服务端的数据可能发生改变。若Repository直接返回网络请求,则导致界面显示冲突。这就是为什么在上面UserRepository实现时,网络请求的数据仅仅存到数据库中,触发LiveData的刷新。推荐,DataBase是应用唯一的数据源,而Repository是应用其它部分的唯一数据源。

3.6.测试

很容易将代码分成几个模块进行测试。

3.7.最终架构
AAC.png

4.指导原则


5.暴露网络状态


对于上面提到的网络状态问题,可以使用Resource类封装数据和状态。

//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类来复用。

Load.png

首先观察数据库的资源响应数据的更新。当从数据库中获取时,先判断得到的结果是否符合使用要求,不然就从网络获取。若想从网络更新时显示存储数据,它们可以同时进行。若网络加载成功,将结果存到数据库中,再次触发加载流程;若加载失败,直接调用失败操作。

新的数据存到磁盘上后,需从数据库重新取数据,若数据库可以发送改变消息,通常不用这么做。但是,依赖数据库发送改变消息,也会有些问题,因为数据更新后可能并没有变化,数据库将不会发送改变信息。同样,也不希望直接使用网络请求到的结果,因为这不符合单一数据源原则,哪怕也会触发数据库存储更新。最后,不希望没新数据更新的情况下发送SUCCESS标志,这会给客户端错误的信息。

// 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
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

上面的类定义两种类型参数(ResultType,RequestType),是考虑到网络获取的数据类型与本地使用的数据类型不匹配。而使用ApiResponse作为网络请求,是因为它对Retrofit2.Call进行简单的封装,将返回类型转成LiveData。

对于上面提出的要求,可以通过以下具体实现:

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();
    }
}

现在,用NetworkBoundResource替换之前UserRepository的获取数据的操作。由于Webservice和UserDao是由项目决定类型,无法提取到NetworkBoundResource中,所以相关操作放在UserRepository中实现。

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();
    }
}
上一篇下一篇

猜你喜欢

热点阅读