此指南适⽤于那些曾经或现在进⾏Android应⽤的基础开发,并希望了解和学习编写Android程序的最佳实践和架构。通过学习来构建强⼤的⽣产级别的应⽤。
注意:此指南默认你对Android开发有⽐较深的理解,熟知Android Framework。如果你还只是个Android开发新⼿,那么建议先学习下Android的基础知识。
Android程序员⾯临的问题
传统的桌⾯应⽤程序开发在⼤多数情况下,启动器快捷⽅式都有⼀个⼊⼝点,并作为⼀个单⼀的过程运⾏,但Android应⽤程序的结构更为复杂。典型的Android应⽤程序由多个应⽤程序组件构成,包括Activity,Fragment,Service,ContentProvider和Broadcast Receiver。⼤多数这些应⽤程序组件在Android操作系统使⽤的AndroidManifest中声明,以决定如何将应⽤程序集成到设备上来为⽤户提供完整的体验。尽管如前所述,桌⾯应⽤程序传统上是作为⼀个单⼀的进程运⾏的,但正确编写的Android应⽤程序则需要更灵活,因为⽤户通过设备上的不同应⽤程序编织⽅式,不断切换流程和任务。
举个例⼦,当⽤户在社交App上打算分享⼀张照⽚,那么Android系统就会为此启动相机来完成此次请求。此时⽤户离开了社交App,但是这个⽤户体验是⽆缝连接的。相机可能⼜会触发并启动⽂件管理器来选择照⽚。最终回到社交App并分享照⽚。此外,在此过程中的任何时候,⽤户可能会被打电话中断,并在完成电话后再回来分享照⽚。
在Android中,这种应⽤间跳转⾏为很常见,因此你的应⽤必须正确处理这些流程。请记住,移动设备是资源有限的,所以在任何时候,操作系统可能需要杀死⼀些应⽤来为新的应⽤腾出空间。
你的应⽤程序的所有组件都可以被单独启动或⽆序启动,并且在任何时候由⽤户或系统销毁。因为应⽤程序组件是短暂的,它们的⽣命周期(创建和销毁时)不受你的控制,因此你不应该将任何应⽤程序数据或状态存储在应⽤程序组件中,并且应⽤程序组件不应相互依赖。
常见的架构原理
如果你⽆法使⽤应⽤程序组件来存储应⽤程序数据和状态,应如何构建应⽤程序?
在你的App开发中你应该将重⼼放在分层上,如果将所有的代码都写在Activity或者Fragment中,那问题就⼤了。任何不是处理UI或跟操作系统交互的操作不应该放在这两个类中。尽量保持它们代码的精简,这样你可以避免很多与⽣命周期相关的问题。记住你并不能掌控Activity和Fragment,他们只是在你的App和Android系统间起了桥梁的作⽤。任何时候,Android系统可能会根据⽤户操作或其他因素(如低内存)来回收它们。最好尽量减少对他们的依赖,以提供坚实的⽤户体验。
还有⼀点⽐较重要的就是持久模型驱动UI。使⽤持久模型主要是因为当你的UI被回收或者在没有⽹络的情况下还能正常给⽤户展⽰数据。模型是⽤来处理应⽤数据的组件,它们独⽴于应⽤中的视图和四⼤组件。因此模型的⽣命周期必然和UI是分离的。保持UI代码的整洁,会让你能更容易的管理和调整UI。让你的应⽤基于模型开发可以很好的管理你应⽤的数据并是你的应⽤更具测试性和持续性。
应⽤架构推荐
回到这篇⽂章的主题,来说说Android官⽅架构组件(⼀下简称架构)。⼀下会介绍如何在你的应⽤中实践这⼀架构模式。
注意:不可能存在某⼀种架构⽅式可以完美适合任何场景。话虽如此,这种架构应该是⼤多数⽤例的良好起点。如果你已经有了很好的Android应⽤程序架构⽅式,请继续保持。
假设我们需要⼀个现实⽤户资料的UI,该⽤户的资料⽂件将使⽤REST API从服务端获取。
构建⽤户界⾯
我们的这个⽤户界⾯由⼀个UserProfileFragment.java⽂件和它的布局⽂件user_profile_layout.xml。为了驱动UI,数据模型需要持有下⾯两个数据:
User ID:⽤户的标识符。最好使⽤Fragment的参数将此信息传递到Fragment中。如果Android操作系统回收了Fragment,则会保留此信息,以便下次重新启动应⽤时,该ID可⽤。User Object:传统的Java对象,代表⽤户的数据。
为此,我们新建⼀个继承⾃ViewModel的名为UserProfileViewModel的模型来持有这个数据。
ViewModel提供特定UI组件的数据,例如Activity和Fragment,并处理与数据处理业务部分的通信,例如调⽤其他组件来加载数据或转发⽤户修改。ViewModel不了解View,并且不受UI的重建(如重由于旋转⽽导致的Activity的重建)的影响。现在我们有⼀下三个⽂件:
user_profile.xml: 视图的布局⽂件。
UserProfileViewModel.java: 持有UI数据的模型。
UserProfileFragment.java: ⽤于显⽰数据模型中的数据并和⽤户进⾏交互。
⼀下是具体代码(为了简化,布局⽂件省略)。
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); }}
注意:上⾯的UserProfileFragment继承⾃LifeCycleFragment⽽不是Fragment。当Lifecycle的Api稳定后,Fragment会默认实现LifeCycleOwner。
现在,我们有三个⽂件,我们如何连接它们?毕竟,当ViewModel的⽤户字段被设置时,我们需要⼀种通知UI的⽅法。这⾥就要提到LiveData了。
LiveData是⼀个可观察的数据持有者。它允许应⽤程序中的组件观察LiveData对象持有的数据,⽽不会在它们之间创建显式和刚性的依赖路径。LiveData还尊重你的应⽤程序组件(Activity,Fragment,Service)的⽣命周期状态,并做正确的事情以防⽌内存泄漏,从⽽你的应⽤程序不会消耗更多的内存。
如果你已经使⽤了想Rxjava活着Agrea这类第三⽅库,那么你可以使⽤它们代替LiveData,不过你需要处理好它们与组件⽣命周期之间的关系。
现在我们使⽤LiveData public class UserProfileViewModel extends ViewModel { ... private LiveData public LiveData 然后将UserProfileFragment修改如下,观察数据并更新UI: @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); viewModel.getUser().observe(this, user -> { // update UI });} ⼀旦⽤户数据更新,onChanged回调将被调⽤然后UI会被刷新。 如果你熟悉⼀些使⽤观察者模式第三⽅库,你会觉得奇怪,为什么没有在Fragment的onStop()⽅法中将观察者移除。对于LiveData来说这是没有必要的,因为它是⽣命周期感知的,这意味着如果UI处于不活动状态,它就不会调⽤观察者的回调来更新数据。并且在onDestroy后会⾃动移除。 我们也不需要处理任何视图重建(如屏幕旋转)。ViewModel会⾃动恢复重建前的数据。当新的视图被创建出来后,它会接收到与之前相同的ViewModel实例,并且观察者的回调会被⽴刻调⽤,更新最新的数据。这也是ViewModel为什么不能直接引⽤视图对象,因为它的⽣命周期长于视图对象。 获取数据 现在我们将视图和模型连接起来,但是模型该怎么获取数据呢?在这个例⼦中,我们假设使⽤REST API从后台获取。我们将使⽤Retrofit来向后台请求数据。 我们的retrofit类Webservice如下: 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 如果只是简单的实现,ViewModel可以直接操作Webservice来获取⽤户数据。虽然这样可以正常⼯作,但你的应⽤⽆法保证它的后续迭代。因为这样做将太多的责任让ViewModel来承担,这样就违反类之前讲到的分层原则。⼜因为ViewModel的⽣命周期是绑定在Activity和 Fragment上的,所以当UI被销毁后如果丢失所有数据将是很差的⽤户体验。所以我们的ViewModel将和⼀个新的模块进⾏交互,这个模块叫Repository。 Repository模块负责处理数据。它为应⽤程序的其余部分提供了⼀个⼲净的API。他知道在数据更新时从哪⾥获取数据和调⽤哪些API调⽤。你可以将它们视为不同数据源(持久性模型,Web服务,缓存等)之间的中介者。UserRepository类如下: public class UserRepository { private Webservice webservice; // ... public LiveData // This is not an optimal implementation, we'll fix it below final MutableLiveData public void onResponse(Call return data; }} 虽然repository模块看上去没有必要,但他起着重要的作⽤。它为App的其他部分抽象出了数据源。现在我们的ViewModel并不知道数据是通过WebService来获取的,这意味着我们可以随意替换掉获取数据的实现。管理组件间的依赖关系 上⾯这种写法可以看出来UserRepository需要初始化Webservice实例,这虽然说起来简单,但要实现的话还需要知道Webservice的具体构造⽅法该如何写。这将加⼤代码的复杂度,另外UserRepository可能并不是唯⼀使⽤Webservice的对象,所以这种在内部构建Webservice实例显然是不推荐的,下⾯有两种模式来解决这个问题: 依赖注⼊:依赖注⼊允许类定义它们的依赖关系⽽不构造它们。在运⾏时,另⼀个类负责提供这些依赖关系。我们建议在Android应⽤程序中使⽤Google的Dagger 2库实现依赖注⼊。Dagger 2通过遍历依赖关系树⾃动构建对象,并在依赖关系上提供编译时保证。服务定位器:服务定位器提供了⼀个注册表,其中类可以获取它们的依赖关系⽽不是构造它们。与依赖注⼊(DI)相⽐,实现起来相对容易,因此如果您不熟悉DI,请改⽤Service Locator。 这些模式允许你扩展代码,因为它们提供明确的模式来管理依赖关系,⽽不会重复代码或增加复杂性。两者都允许交换实现进⾏测试;这是使⽤它们的主要好处之⼀。在这个例⼦中,我们将使⽤Dagger 2来管理依赖关系。 连接ViewModel和Repository 现在,我们的UserProfileViewModel可以改写成这样: public class UserProfileViewModel extends ViewModel { private LiveData @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 缓存数据 上⾯的Repository虽然⽹络请求做了封装,但是它依赖后台数据源,所以存在不⾜。 上⾯的UserRepository实现的问题是,在获取数据之后,它不会保留在任何地⽅。如果⽤户离开UserProfileFragment并重新进来,则应⽤程序将重新获取数据。这是不好的,有两个原因:它浪费了宝贵的⽹络带宽和迫使⽤户等待新的查询完成。为了解决这个问题,我们将向我们的UserRepository添加⼀个新的数据源,它将把User对象缓存在内存中。如下: @Singleton // informs Dagger that this class should be constructed oncepublic class UserRepository { private Webservice webservice; // simple in memory cache, details omitted for brevity private UserCache userCache; public LiveData LiveData final MutableLiveData // this is still suboptimal but better than before. // a complete implementation must also handle the error cases. webservice.getUser(userId).enqueue(new Callback public void onResponse(Call return data; }} 持久化数据 在当前的实现中,如果⽤户旋转屏幕或离开并返回到应⽤程序,现有UI将⽴即可见,因为Repository会从内存中检索数据。但是,如果⽤户离开应⽤程序,并在Android操作系统杀死进程后⼏⼩时后⼜会怎么样? 在⽬前的实现中,我们将需要从⽹络中再次获取数据。这不仅是⼀个糟糕的⽤户体验,也是浪费,因为它将使⽤移动数据来重新获取相同的数据。你以通过缓存Web请求来简单地解决这个问题,但它会产⽣新的问题。如果请求⼀个朋友列表⽽不是单个⽤户,会发⽣什么情况?那么你的应⽤程序可能会显⽰不⼀致的数据,这是最令⼈困惑的⽤户体验。例如,相同的⽤户的数据可能会不同,因为朋友列表请求和⽤户请求可以在不同的时间执⾏。你的应⽤需要合并他们,以避免显⽰不⼀致的数据。正确的处理⽅法是使⽤持久模型。这时候Room就派上⽤场了。 Room是⼀个对象映射库,它提供本地数据持久性和最少的样板代码。在编译时,它根据模式验证每个查询,从⽽错误的SQL查询会导致编译时错误,⽽不是运⾏时失败。Room抽象了使⽤原始SQL表和查询的⼀些基本实现细节。它还允许观察数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改。要使⽤Room我们⾸先需要使⽤@Entity来定义实体: @Entityclass User { @PrimaryKey private int id; private String name; private String lastName; // getters and setters for fields} 接着创建数据库类: @Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase {} 值得注意的是MyDatabase是⼀个抽象了,Room会在编译期间提供它的⼀个实现类。接下来需要定义DAO: @Dao public interface UserDao { @Insert(onConflict = REPLACE) void save(User user); @Query(\"SELECT * FROM user WHERE id = :userId\") LiveData 接着在MyDatabase中添加获取上⾯这个DAO的⽅法: @Database(entities = {User.class}, version = 1) public abstract class MyDatabase extends RoomDatabase { public abstract UserDao userDao();} 这⾥的load⽅法返回的是LiveData,所以当相关数据库中的数据有任何变化时,Room都会通知LiveData上的处于活动状态的观察者。现在我们可以修改UserRepository了: @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 // 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的直接数据来源从Webservice改为本地数据库,但我们却不需要修改UserProfileViewModel或者UserProfileFragment。这就是抽象层带来的好处。这也给测试带来了⽅便,因为你可以提供⼀个虚假的UserRepository来测试你的UserProfileViewModel。 现在,如果⽤户重新回到这个界⾯,他们会⽴刻看到数据,因为我们已经将数据做了持久化的保存。当然如果有⽤例需要,我们也可不展⽰太⽼旧的持久化数据。 在⼀些⽤例中,⽐如下拉刷新,如果正处于⽹络请求中,那UI需要告诉⽤户正处于⽹络请求中。⼀个好的实践⽅式就是将UI与数据分离,因为UI可能因为各种原因被更新。从UI的⾓度来说,请求中的数据和本地数据类似,只是它还没有被持久化到数据库中。以下有两种解决⽅法: 将getUser的返回值中加⼊⽹络状态。 在Repository中提供⼀个可以返回刷新状态的⽅法。如果你只是想在⽤户通过下拉刷新来告诉⽤户⽬前的⽹络状态的话,那这个⽅法是⽐较适合的。数据唯⼀来源 在以上实例中,数据唯⼀来源是数据库,这样做的好处是⽤户可以基于稳定的数据库数据来更新页⾯,⽽不需要处理⼤量的⽹络请求状态。数据库有数据则使⽤,没有数据则等待其更新。 测试 我们之前提到分层可以个应⽤提供良好的测试能⼒,接下来就看看我们怎么测试不同的模块。 ⽤户界⾯与交互:这是唯⼀⼀个需要使⽤到Android UI Instrumentation test的测试模块。测试UI的最好⽅法就是使⽤Espresso框架。你可以创建Fragment然后提供⼀个虚假的ViewModel。因为Fragment只跟ViewModel交互,所以虚拟⼀个ViewModel就⾜够了。ViewModel:ViewModel可以⽤JUnit test进⾏测试。因为其不涉及界⾯与交互。⽽且你只需要虚拟UserRepository即可。 UserRepository:测试UserRepository同样使⽤JUnit test。你可以虚拟出Webservice和DAO。你可以通过使⽤正确的⽹络请求来请求数据,让后将数据通过DAO写⼊数据库。如果数据库中有相关数据则⽆需进⾏⽹络请求。 UserDao:对于DAO的测试,推荐使⽤instrumentation进⾏测试。因为此处⽆需UI,并且可以使⽤in-memory数据库来保证测试的封闭性,不会影响到磁盘上的数据库。 Webservice:保持测试的封闭性是相当重要的,因此即使是你的Webservice测试也应避免对后端进⾏⽹络呼叫。有很多第三⽅库提供这⽅⾯的⽀持。例如,MockWebServer是⼀个很棒的库,可以帮助你为你的测试创建⼀个假的本地服务器。 架构图 指导原则 编程是⼀个创意领域,构建Android应⽤程序也不例外。有多种⽅法来解决问题,⽆论是在多个Activity或Fragment之间传递数据,还是检索远程数据并将其在本地保持离线模式,或者是任何其他常见的场景。 虽然以下建议不是强制性的,但经验告诉我们,遵循这些建议将使你的代码库从长远来看更加强⼤,可测试和可维护。 在AndroidManifest中定义的Activity,Service,Broadcast Receiver等,它们不是数据源。相反,他们只是⽤于协调和展⽰数据。由于每个应⽤程序组件的寿命相当短,运⾏状态取决于⽤户与其设备的交互以及运⾏时的整体当前运⾏状况,所以不要将这些组件作为数据源。 你需要在应⽤程序的各个模块之间创建明确界定的责任范围。例如,不要在不同的类或包之间传递⽤于加载⽹络数据的代码。同样,不要将数据缓存和数据绑定这两个责任完全不同的放在同⼀个类中。 每个模块之间要竟可能少的相互暴露。不要抱有侥幸⼼理去公开⼀个关于模块的内部实现细节的接⼝。你可能会在短期内获得到便捷,但是随着代码库的发展,你将多付多次技术性债务。 当你定义模块之间的交互时,请考虑如何使每个模块隔离。例如,拥有⽤于从⽹络中提取数据的定义良好的API将使得更容易测试在本地数据库中持久存在该数据的模块。相反,如果将这两个模块的逻辑组合在⼀起,或者将整个代码库中的⽹络代码放在⼀起,那么测试就更难(如果不是不可能)。 你的应⽤程序的核⼼是什么让它独⽴出来。不要花时间重复轮⼦或⼀次⼜⼀次地编写相同的样板代码。相反,将精⼒集中在使你的应⽤程序独⼀⽆⼆的同时,让Android架构组件和其他推荐的库来处理重复的样板代码。 保持尽可能多的相关联的新鲜数据,以便你的应⽤程序在设备处于脱机模式时可⽤。虽然你可以享受恒定和⾼速连接,但你的⽤户可能不会。 你的Repository应指定⼀个数据源作为真实的单⼀来源。每当你的应⽤程序需要访问这些数据时,它应该始终源于真实的单⼀来源。 扩展: 公开⽹络状态 在上⾯的⼩结我们故意省略了⽹络错误和加载状态来保证例⼦的简洁性。在这⼀⼩结我们演⽰⼀种使⽤Resource类来封装数据及其状态。以此来公开⽹络状态。下⾯是简单的Resource实现: //a generic class that describes a data with a statuspublic class Resource @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 public static public static 以为从⽹络上抓取视频的同时在UI上显⽰数据库的旧数据是很常见的⽤例,所以我们要创建⼀个可以在多个地⽅重复使⽤的帮助类NetworkBoundResource。以下是NetworkBoundResource的决策树: NetworkBoundResource从观察数据库开始,当第⼀次从数据库加载完实体后,NetworkBoundResource会检查这个结果是否满⾜⽤来展⽰的需求,如不满⾜则需要从⽹上重新获取。当然以上两种情况可能同时发⽣,你希望先将数据显⽰在UI上的同时去⽹络上请求新数据。如果⽹络请求成果,则将结果保存到数据库,然后重新从数据库加载数据,如果⽹络请求失败,则直接传递错误信息。 注意:在上⾯的过程中可以看到当将新数据保存到数据库后,我们重新从数据库加载数据。虽然⼤部分情况我们不必如此,因为数据库会为我们传递此次更新。但另⼀⽅⾯,依赖数据库内部的更新机制并不是我们想要的如果更新的数据与旧数据⼀致,则数据⾕不会做出更新提⽰。我们也不希望直接从⽹络请求中获取数据直接⽤于UI,因为这样违背了单⼀数据源的原则。下⾯是NetworkBoundResource类的公共api: // ResultType: Type for the Resource data// RequestType: Type for the API response public abstract class NetworkBoundResource 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 // Called to create the API call. @NonNull @MainThread protected abstract LiveData protected void onFetchFailed() { } // returns a LiveData that represents the resource public final LiveData 注意到上⾯定义了两种泛型,ResultType和RequestType,因为从⽹络请求返回的数据类型可能会和数据库返回的不⼀致。另外注意到上⾯代码中的ApiResponse这个类,他是将Retroft2.Call转换成LiveData的⼀个简单封装。下⾯是NetworkBoundResource余下部分的实现: public abstract class NetworkBoundResource private final MediatorLiveData NetworkBoundResource() { //1初始化NetworkBoundResource result.setValue(Resource.loading(null)); //2从数据库加载本地数据 LiveData result.addSource(dbSource, data -> { //3加载完成后判断是否需要从⽹上更新数据 result.removeSource(dbSource); if (shouldFetch(data)) { //4从⽹上更新数据 fetchFromNetwork(dbSource); } else { //直接⽤本地数据更新 result.addSource(dbSource, newData -> result.setValue(Resource.success(newData))); } }); } private void fetchFromNetwork(final LiveData LiveData newData -> result.setValue(Resource.loading(newData))); result.addSource(apiResponse, response -> { result.removeSource(apiResponse); result.removeSource(dbSource); //noinspection ConstantConditions if (response.isSuccessful()) { //6请求数据成功,保存数据 saveResultAndReInit(response); } else { //请求失败使⽤,传递失败信息 onFetchFailed(); result.addSource(dbSource, newData -> result.setValue( Resource.error(response.errorMessage, newData))); } }); } @MainThread private void saveResultAndReInit(ApiResponse @Override protected Void doInBackground(Void... voids) { //7保存请求到的数据 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. //8再次加载数据库,使⽤数据库中的最新数据 result.addSource(loadFromDb(), newData -> result.setValue(Resource.success(newData))); } }.execute(); }} 接着我们就可以在UserRepository中使⽤NetworkBoundResource了。 class UserRepository { Webservice webservice; UserDao userDao; public LiveData 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 @NonNull @Override protected LiveData }.getAsLiveData(); }} 因篇幅问题不能全部显示,请点此查看更多更全内容