1、常见的架构原则
- 分离关注点
- 模型驱动界面(最好是持久化模型)
2、推荐应用架构(MVVM模式)
此架构采用MVVM模式,即Model、View、ViewModel
2.1 View层
Activity/Fragment
2.2 ViewModel层
包含ViewModel、LiveData、SavedStateHandle
LiveData 是一种可观察的数据存储器,还遵循应用组件(如 Activity、Fragment 和 Service)的生命周期状态,并包括清理逻辑以防止对象泄漏和过多的内存消耗。
SavedStateHandle :允许 ViewModel 访问相关 Fragment 或 Activity 的已保存状态和参数。
LiveData将ViewModel连接到View
2.3 Model层
包含Repository(称存储区)、数据源(数据库、远端网络数据、缓存数据)
存储区模块会处理数据操作,对接ViewModel,处理数据源.。
解决依赖引用可用以下设计模式解决:
- 依赖注入:推荐用Dagger 2
- 服务定位器:服务定位器模式提供一个注册表,类可以从中获取其依赖项而不构造它们
实现服务注册表比使用 DI 更容易,因此,如果不熟悉 DI,请改用服务定位器模式。
2.3.1 Repository(存储区)
2.3.2 数据源(远端网络数据)
2.3.3 数据源(缓存数据)
2.3.4 数据源(数据库数据)
持久性模型,Room,Room是一个对象映射库,可利用最少样板代码实现本地数据持久化。
Room 可以抽象化处理原始 SQL 表格和查询的一些底层实现细节。它还允许您观察对数据库数据(包括集合和连接查询)的更改,并使用 LiveData 对象公开这类更改。它甚至明确定义了解决一些常见线程问题(如访问主线程上的存储空间)的执行约束。
Room 使用步骤(下列已Kotlin语言展示)
1)给数据模型添加@Entity注释,并向该类的id字段添加@PrimaryKey
@Entity
data class User(
@PrimaryKey private val id: String,
private val name: String,
private val lastName: String
)
2)创建数据访问对象(DAO)
提供数据获取、插入数据库的方法
@Dao
interface UserDao {
@Insert(onConflict = REPLACE)
fun save(user: User)
@Query("SELECT * FROM user WHERE id = :userId")
fun load(userId: String): LiveData<User>
}
3) 创建数据库类
@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
- UserDatabase是抽象类,Room将自动提供它的实现。
- 数据库类引用 DAO对象
4)使用DAO
// Informs Dagger that this class should be constructed only once.
@Singleton
class UserRepository @Inject constructor(
private val webservice: Webservice,
// Simple in-memory cache. Details omitted for brevity.
private val executor: Executor,
private val userDao: UserDao
) {
fun getUser(userId: String): LiveData<User> {
refreshUser(userId)
// Returns a LiveData object directly from the database.
return userDao.load(userId)
}
private fun refreshUser(userId: String) {
// Runs in a background thread.
executor.execute {
// Check if user data was fetched recently.
val userExists = userDao.hasUser(FRESH_TIMEOUT)
if (!userExists) {
// Refreshes the data.
val response = webservice.getUser(userId).execute()
// Check for errors here.
// Updates the database. The LiveData object automatically
// refreshes, so we don't need to do anything else here.
userDao.save(response.body()!!)
}
}
}
companion object {
val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1)
}
}
2.3.5 单一可信来源
不同的 REST API 端点返回相同的数据是一种很常见的现象。例如,如果我们的后端有其他端点返回好友列表,则同一个用户对象可能来自两个不同的 API 端点,甚至可能使用不同的粒度级别。如果
UserRepository
按原样从Webservice
请求返回响应,而不检查一致性,则界面可能会显示混乱的信息,因为来自存储区的数据的版本和格式将取决于最近调用的端点。因此,我们的
UserRepository
实现会将网络服务响应保存在数据库中。这样一来,对数据库的更改将触发对活跃 LiveData 对象的回调。使用此模型时,数据库会充当单一可信来源,应用的其他部分则使用UserRepository
对其进行访问。无论您是否使用磁盘缓存,我们都建议您的存储区将某个数据源指定为应用其余部分的单一可信来源。
2.3.6 显示正在执行的操作
在某些用例(如下拉刷新)中,界面务必要向用户显示当前正在执行某项网络操作。将界面操作与实际数据分离开来是一种很好的做法,因为数据可能会因各种原因而更新。例如,如果我们获取了好友列表,可能会程序化地再次获取相同的用户,从而触发 LiveData<User>
更新。从界面的角度来看,传输中的请求只是另一个数据点,类似于 User
对象本身中的其他任何数据。
我们可以使用以下某个策略,在界面中显示一致的数据更新状态(无论更新数据的请求来自何处):
- 更改
getUser()
以返回一个LiveData
类型的对象。此对象将包含网络操作的状态。
有关示例,请参阅 Android 架构组件 GitHub 项目中的NetworkBoundResource
实现。 - 在
UserRepository
类中再提供一个可以返回User
刷新状态的公共函数。如果您只想在数据获取过程源自于显式用户操作(如下拉刷新)时在界面中显示网络状态,使用此策略效果会更好。
2.3.7 可测试每个模块
在分离关注点部分中,我们已经提到,遵循此原则的一个主要好处是可测试性。
如何测试MVVM架构中每个模块:
- 界面和交互:使用 Android 界面插桩测试。创建此测试的最佳方法是使用 Espresso 库。您可以创建 Fragment 并为其提供模拟
UserProfileViewModel
。由于 Fragment 只与UserProfileViewModel
通信,因此模拟这一个类就足以完整测试应用的界面。 - ViewModel:您可以使用 JUnit 测试来测试
UserProfileViewModel
类。您只需模拟一个类,即UserRepository
。 - UserRepository:您也可以使用 JUnit 测试来测试
UserRepository
。您需要模拟Webservice
和UserDao
。在这些测试中,请验证以下行为:- 存储区是否进行了正确的网络服务调用。
- 存储区是否将结果保存到数据库中。
- 在数据已缓存且保持最新状态时,存储区是否不会发出不必要的请求。
- 由于
Webservice
和UserDao
都是接口,因此您可以模拟它们或者为更复杂的测试用例创建虚假实现。 -
UserDao:使用插桩测试来测试 DAO 类。由于这些插桩测试不需要任何界面组件,因此它们会快速运行。对于每个测试,都请创建内存中数据库以确保测试没有任何副作用(例如更改磁盘上的数据库文件)。
-
Webservice:在这些测试中,请避免对后端进行网络调用。所有测试(尤其是基于网络的测试)都务必独立于外界。有几个库(包括 MockWebServer)可帮助您为这些测试创建虚假的本地服务器。
-
测试工件:架构组件提供了一个可控制其后台线程的 maven 工件。
androidx.arch.core:core-testing
工件包含以下 JUnit 规则:InstantTaskExecutorRule
:使用此规则可立即在调用线程上执行任何后台操作。CountingTaskExecutorRule
:使用此规则可等待架构组件的后台操作。您还可以将此规则作为空闲资源与 Espresso 关联。