哈Ha! 我向您提供
JetPack的“应用程序架构指南”的免费翻译。 我要求您将所有关于翻译的评论留在评论中,这些问题将得到解决。 同样,使用提出的体系结构的人的注释以及使用建议对每个人都是有用的。
本指南涵盖了用于构建健壮的应用程序的最佳实践和推荐的体系结构。 本页面假设您对Android Framework进行了基本介绍。 如果您不熟悉Android应用程序开发,请查看我们的
开发人员指南以开始使用,并详细了解本指南中提到的概念。 如果您对应用程序体系结构感兴趣,并且想从Kotlin编程的角度熟悉本指南中的资料,请查看Udacity课程
“使用Kotlin开发Android应用程序” 。
移动应用用户体验
在大多数情况下,桌面应用程序具有来自桌面或启动器的单个入口点,然后作为单个整体进程运行。 Android应用程序的结构要复杂得多。 一个典型的Android应用程序包含几个
应用程序组件 ,包括
Activity ,
Fragments ,
Services ,
ContentProviders和
BroadcastReceivers 。
您可以在应用程序
清单中声明所有或部分这些应用程序组件。 然后,Android使用此文件来决定如何将您的应用程序集成到设备的通用用户界面中。 鉴于编写良好的Android应用程序包含多个组件,并且用户经常在短时间内与多个应用程序进行交互,因此应用程序必须适应不同类型的工作流和用户驱动的任务。
例如,考虑当您在自己喜欢的社交媒体应用程序中共享照片时会发生什么:
- 该应用程序触发相机的意图。 Android启动相机应用程序来处理请求。 目前,用户已将应用程序留给社交网络使用,而他作为用户的经验是无可挑剔的。
- 相机应用程序可能会触发其他意图,例如启动文件选择器,这可能会启动另一个应用程序。
- 最后,用户返回到社交网络应用程序并共享照片。
在此过程中的任何时候,用户都可能会被电话或通知打扰。 与该中断相关联的操作之后,用户希望能够返回并继续此照片共享过程。 此应用程序切换行为在移动设备上很常见,因此您的应用程序必须正确处理这些要点(任务)。
请记住,移动设备的资源也受到限制,因此操作系统随时可能破坏某些应用程序进程,以便为新的应用程序腾出空间。
在这种环境下,应用程序的组件可以单独启动,而不能按顺序启动,操作系统或用户可以随时销毁它们。 由于这些事件不受您的控制,因此
您不应在应用程序组件中存储任何数据或状态,并且应用程序组件不应相互依赖。
一般建筑原则
如果不应该使用应用程序组件来存储数据和应用程序状态,那么应该如何开发应用程序?
责任分工
遵循的最重要的原则是
责任分担 。 一个常见的错误是当您在
Activity或
Fragment中编写所有代码时。 这些是用户界面类,应仅包含处理用户界面和操作系统之间交互的逻辑。 通过在这些类
(SRP)中尽可能地分担责任,您可以避免许多与应用程序生命周期相关的问题。
通过模型进行用户界面控制
另一个重要原则是必须
从模型 (最好是从永久模型)
控制用户界面 。 模型是负责处理应用程序数据的组件。 它们独立于
View对象和应用程序组件,因此,它们不受应用程序生命周期和相关问题的影响。
由于以下原因,永久模型是理想的:
- 如果Android操作系统销毁了您的应用程序以释放资源,您的用户将不会丢失数据。
- 当网络连接不稳定或不可用时,您的应用程序继续工作。
通过将应用程序的基础组织到具有明确定义的数据管理职责的模型类中,您的应用程序变得更易于测试和支持。
推荐的应用架构
本节演示了如何在
端到端使用场景中使用
体系结构组件来构建应用程序。
注意事项 不可能有一种方法来编写最适合每种情况的应用程序。 但是,对于大多数情况和工作流程,推荐的体系结构是一个很好的起点。 如果您已经有了编写符合一般架构原则的Android应用程序的好方法,则不要更改它。假设我们正在创建一个显示用户个人资料的用户界面。 我们使用私有API和REST API来检索配置文件数据。
复习
首先,请考虑完成的应用程序体系结构的模块的交互方案:

请注意,每个组件仅取决于其下一级的组件。 例如,“活动”和“片段”仅取决于视图模型。 存储库是唯一依赖于许多其他类的类。 在此示例中,存储取决于持久性数据模型和远程内部数据源。
这种设计模式可创建一致且令人愉悦的用户体验。 无论用户是在关闭应用程序后几分钟还是几天后返回到应用程序,他都会立即看到用户的信息,即该应用程序已保存在本地。 如果此数据已过期,则应用程序存储模块将在后台开始更新数据。
创建一个用户界面
用户界面由
UserProfileFragment
片段和相应的
user_profile_layout.xml
布局
user_profile_layout.xml
。
要管理用户界面,我们的数据模型必须包含以下数据元素:
- 用户ID:用户ID。 最好的解决方案是使用片段的参数将此信息传递给片段。 如果Android操作系统破坏了我们的进程,则会保存此信息,因此在下次启动应用程序时可以使用该标识符。
- 用户对象:包含用户信息的数据类。
我们使用基于ViewModel体系结构的组件的
UserProfileViewModel
来存储此信息。
ViewModel对象为特定的用户界面组件(例如片段或活动)提供数据,并包含用于与模型进行交互的业务数据处理逻辑。 例如, ViewModel可以调用其他组件来加载数据,并且可以转发用户对数据更改的请求。 ViewModel不了解用户界面的组件,因此它不受配置更改的影响,例如在旋转设备时重新创建Activity。现在,我们确定了以下文件:
user_profile.xml
:定义的用户界面布局。UserProfileFragment
:描述了一个用户界面控制器,负责向用户显示信息。UserProfileViewModel
:一个类,负责准备数据以在UserProfileFragment
显示数据并响应用户交互。
以下代码段显示了这些文件的初始内容。 (为简单起见,省略了布局文件。)
class UserProfileViewModel : ViewModel() { val userId : String = TODO() val user : User = TODO() } class UserProfileFragment : Fragment() { private val viewModel: UserProfileViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.main_fragment, container, false) } }
现在我们有了这些代码模块,我们如何连接它们? 在UserProfileViewModel类中设置了用户字段之后,我们需要一种通知用户界面的方法。
注意事项 SavedStateHandle允许ViewModel访问关联的片段或操作的保存状态和参数。
现在,我们需要在收到用户对象时通知Fragment。 这是LiveData体系结构的组件出现的地方。
LiveData是可观察的数据持有者。 应用程序中的其他组件可以使用此持有人来跟踪对对象的更改,而无需在它们之间创建明确的硬路径。 LiveData组件还考虑了应用程序组件(例如“活动”,“片段”和“服务”)的生命周期状态,并包括清除逻辑以防止对象泄漏和过多的内存消耗。
注意事项 如果您已经在使用RxJava或Agera之类的库,则可以继续使用它们代替LiveData。 但是,在使用库和类似方法时,请确保正确处理应用程序的生命周期。 特别是,请确保在关联的LifecycleOwner停止时暂停数据流,并在关联的LifecycleOwner被破坏时销毁这些数据流。 您还可以添加工件android.arch.lifecycle:喷射流,以将LiveData与另一个喷射流库(如RxJava2)一起使用。为了在我们的应用程序中包含LiveData组件,我们将
UserProfileViewModel
的字段类型更改为LiveData。 现在通知
UserProfileFragment
有关数据更新。 另外,由于此
LiveData字段支持生命周期,因此当不再需要链接时,它将自动清除链接。
class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = TODO() }
现在,我们修改
UserProfileFragment
以观察
ViewModel
的数据并根据更改来更新用户界面:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.user.observe(viewLifecycleOwner) {
每次更新用户配置文件数据时,都会调用
onChanged()回调并更新用户界面。
如果您熟悉使用可观察的回调的其他库,那么您可能已经意识到我们没有重新定义该片段的
onStop()方法来停止观察数据。 对于LiveData而言,此步骤是可选的,因为它支持生命周期,这意味着如果片段处于非活动状态,它将不调用
onChanged()
回调; 也就是说,他收到了对
onStart()的调用,但尚未收到
onStop()
)。 当在片段上调用
onDestroy()方法时,LiveData还会自动删除观察者。
我们尚未添加任何逻辑来处理配置更改,例如由用户旋转设备的屏幕。 更改配置后,
UserProfileViewModel
自动恢复,因此,创建新片段后,它将立即接收相同的
ViewModel
实例,并使用当前数据立即调用回调。 鉴于
ViewModel
对象被设计为可以在它们更新的相应
View
对象中生存,因此您不应在ViewModel实现中包括对
View
对象的直接引用。 有关
ViewModel
生命周期的更多信息
ViewModel
与用户界面组件的生命周期相对应,请参阅
ViewModel生命周期。资料检索
现在,我们已经使用LiveData将
UserProfileViewModel
连接到
UserProfileFragment
,如何获取用户配置文件数据?
在此示例中,我们假设后端提供了REST API。 尽管您可以使用具有相同目的的其他库,但我们使用Retrofit库访问后端。
这是链接到后端的
Webservice
的定义:
interface Webservice { @GET("/users/{user}") fun getUser(@Path("user") userId: String): Call<User> }
实现
ViewModel
第一个想法可能涉及
Webservice
调用
Webservice
来检索数据并将该数据分配给我们的
LiveData
对象。 此设计有效,但使用它会使我们的应用程序随着增长而难以维护。 这给
UserProfileViewModel
类带来了太多责任,这违反了
利益分离的原则。 另外,ViewModel的范围与
Activity或
Fragment生命周期相关联,这意味着当关联的用户界面对象的生命周期结束时,来自
Webservice
数据将丢失。 此行为会产生不良的用户体验。
相反,我们的
ViewModel
检索数据的过程委托给新的存储模块。
存储库模块处理数据操作。 他们提供了一个干净的API,以便应用程序的其余部分可以轻松获取此数据。 他们知道从何处获取数据以及在更新数据时要调用什么API。 您可以将存储库视为不同数据源(例如,持久性模型,Web服务和缓存)之间的中介。如下面的代码片段所示,我们的
UserRepository
类使用
WebService
的实例来检索用户数据:
class UserRepository { private val webservice: Webservice = TODO()
尽管存储模块似乎不是必需的,但它有一个重要的目的:它从应用程序的其余部分抽象出数据源。 现在,我们的
UserProfileViewModel
不知道如何检索数据,因此我们可以为演示模型提供从几种不同的数据提取实现中获取的数据。
注意事项 为了简单起见,我们错过了网络错误的情况。 有关暴露错误和下载状态的替代实现,请参阅附录:网络状态披露。
管理组件之间的依赖关系上面的
UserRepository
类需要一个
Webservice
实例来检索用户数据。 他可以只创建一个实例,但是为此,他还需要了解
Webservice
类的依赖关系。 另外,
UserRepository
可能不是唯一需要Web服务的类。 这种情况需要我们复制代码,因为每个需要链接到
Webservice
需要知道如何创建它及其依赖项。 如果每个类都创建一个新的
WebService
,则我们的应用程序可能会
WebService
大量资源。
要解决此问题,可以使用以下设计模式:
- 依赖注入(DI) 。 依赖注入使类无需创建即可定义其依赖。 在运行时,另一个类负责提供这些依赖关系。 我们建议使用Dagger 2库在Android应用程序中实现依赖项注入。 Dagger 2自动创建对象,绕过依赖关系树,并为依赖关系提供编译时保证。
- (服务位置)服务定位器:服务定位器模板提供了一个注册表,其中类可以获取其依赖关系,而无需构建它们。
实施服务注册表比使用DI更容易,因此,如果您不熟悉DI,请改用template:服务位置。
这些模板使您能够扩展代码,因为它们提供了用于管理依赖项的清晰模板,而无需复制或复杂化代码。 此外,这些模板使您可以在数据采样的测试和生产实现之间快速切换。
我们的示例应用程序使用
Dagger 2来管理
Webservice
对象的依赖关系。
连接ViewModel和存储
现在,我们修改
UserProfileViewModel
以使用
UserRepository
对象:
class UserProfileViewModel @Inject constructor( savedStateHandle: SavedStateHandle, userRepository: UserRepository ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = userRepository.getUser(userId) }
快取
UserRepository
实现抽象化了
Webservice
对象的调用,但是由于它仅依赖于一个数据源,因此它不是非常灵活。
实现
UserRepository
的主要问题是从后端接收数据后,该数据不会存储在任何地方。 因此,如果用户离开
UserProfileFragment
然后返回它,我们的应用程序必须检索数据,即使它们没有更改。
由于以下原因,此设计不是最佳的:
- 它花费了宝贵的交通资源。
- 这使用户等待新请求的完成。
为了解决这些缺点,我们向
UserRepository
添加了一个新的数据源,该数据源将
User
对象缓存在内存中:
持续数据
使用我们当前的实现,如果用户旋转设备或离开设备并立即返回到应用程序,则现有的用户界面将立即可见,因为商店从内存中的缓存中检索数据。
但是,如果用户离开应用程序并在Android OS完成该过程数小时后返回,该怎么办? 在这种情况下,依靠我们当前的实现,我们需要再次从网络获取数据。 此升级过程不仅会带来不良的用户体验,而且还会带来一些麻烦。 这也很浪费,因为它消耗了宝贵的移动数据。
您可以通过缓存Web请求来解决此问题,但这会带来一个关键的新问题:如果在不同类型的请求中显示相同的用户数据(例如,在收到朋友列表时)会发生什么情况? 该应用程序将显示有冲突的数据,这充其量是令人困惑的。 例如,如果用户在不同时间发送了朋友列表请求和单用户请求,则我们的应用程序可能会显示同一用户的两种不同版本的数据。 我们的应用程序必须弄清楚如何合并这些冲突的数据。
处理这种情况的正确方法是使用常量模型。
房间永久数据库(DB)可以为我们提供帮助。
Room是一个对象映射库,它以最少的标准代码提供本地数据存储。 在编译时,它会检查每个查询是否符合您的数据架构,因此,不起作用的SQL查询会导致编译时错误,而不会导致运行时崩溃。 会议室从原始SQL表和查询的一些基本实现细节中提取了摘要。 它还允许您观察数据库数据的变化,包括集合和连接请求,并使用LiveData对象公开这些变化。 它甚至明确定义了解决常见线程问题的执行约束,例如访问主线程中的存储。
注意事项 如果您的应用程序已经使用了另一个解决方案,例如SQLite对象关系映射(ORM),则无需将现有解决方案替换为Room。 但是,如果您正在编写新的应用程序或重组现有的应用程序,我们建议使用Room保存您的应用程序数据。 因此,您可以利用库的抽象和查询验证。要使用Room,我们需要定义我们的本地布局。 首先,我们在
User
数据模型类中添加
@Entity
注释,并在类
id
字段中添加
@PrimaryKey
注释。 这些注释将
User
标记为数据库中的一个表,将
id
标记为该表的主键:
@Entity data class User( @PrimaryKey private val id: String, private val name: String, private val lastName: String )
然后,通过为应用程序实现
RoomDatabase
来创建数据库类:
@Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase()
请注意,
UserDatabase
是抽象的。 Room库自动提供了此实现。 有关详细信息,请参见“
房间”的文档。
现在,我们需要一种将用户数据插入数据库的方法。 为此,我们创建
一个数据访问对象(DAO) 。
@Dao interface UserDao { @Insert(onConflict = REPLACE) fun save(user: User) @Query("SELECT * FROM user WHERE id = :userId") fun load(userId: String): LiveData<User> }
请注意,
load
方法将返回LiveData类型的对象。 Room知道何时更改数据库,并自动将所有数据更改通知所有活动的观察者。 由于Room使用
LiveData ,因此此操作有效。 只有在至少有一个活动观察者的情况下,它才会更新数据。
注意:会议室会根据表格修改检查是否无效,这意味着会议室可以发送误报通知。定义了
UserDao
类之后,我们从数据库类中引用DAO:
@Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
现在,我们可以更改
UserRepository
以包括Room数据源:
请注意,即使我们更改了
UserRepository
的数据源,也不需要更改
UserProfileViewModel
或
UserProfileFragment
。 此小更新展示了我们的应用程序体系结构提供的灵活性。 这对于测试也非常
UserRepository
,因为我们可以提供伪造的
UserRepository
并同时测试生产的
UserProfileViewModel
。
如果用户几天后返回,则使用此体系结构的应用程序可能会显示过时的信息,直到存储库收到更新的信息为止。 根据您的用例,您可能不会显示过时的信息。 相反,您可以显示
占位符数据,该
占位符数据显示伪值并指示您的应用程序当前正在下载和加载最新信息。
唯一的事实来源,通常,不同的REST API端点返回相同的数据。例如,如果后端有另一个返回朋友列表的终结点,则同一用户对象可能来自两个不同的API终结点,甚至可能使用不同的详细程度。如果我们按原样UserRepository
从请求中返回响应Webservice
,而没有检查一致性,则我们的用户界面可能会显示令人困惑的信息,因为来自存储的数据的版本和格式将取决于最后调用的端点。因此,我们的实现UserRepository
将Web服务响应存储在数据库中。对数据库的更改然后触发活动LiveData对象的回调。使用此模型,数据库是唯一的事实来源,而应用程序的其他部分则通过我们的数据库进行访问UserRepository
。无论您是否使用磁盘缓存,我们都建议您的存储库将数据源标识为应用程序其余部分的唯一事实源。显示操作进度
在某些使用情况下,例如“按需刷新”,重要的是用户界面应向用户显示当前正在进行网络操作。建议将用户界面操作与实际数据分开,因为由于各种原因可以更新数据。例如,如果我们获得朋友列表,则可以通过编程方式再次选择同一用户,这将导致LiveData的更新。从用户界面的角度来看,正在进行请求的事实只是另一个数据点,类似于对象本身中的任何其他数据User
。无论数据更新请求来自何处,我们都可以使用以下策略之一在用户界面中显示约定的数据更新状态:在“利益分离”一节中,我们提到了遵循此原则的主要优势之一是可测试性。以下列表显示了如何从扩展示例中测试每个代码模块:- 用户界面和交互:使用Android UI测试工具包。创建此测试的最佳方法是使用Espresso库。您可以创建一个片段并为其提供布局
UserProfileViewModel
。由于片段结合只UserProfileViewModel
,mokirovanie (模拟)只有这个类是足以满足您的应用程序的用户界面的全面测试。 - ViewModel:
UserProfileViewModel
JUnit . , UserRepository
. - UserRepository:
UserRepository
JUnit. Webservice
UserDao
. :
Webservice
, UserDao
, .- UserDao: DAO . - , . , , , …
: Room , DAO, JSQL SupportSQLiteOpenHelper . , SQLite SQLite . - -: . , -, . , MockWebServer , .
- : maven .
androidx.arch.core
: JUnit:
InstantTaskExecutorRule:
.CountingTaskExecutorRule:
. Espresso .
编程是一个有创意的领域,创建Android应用程序也不例外。解决问题的方法有很多,例如在多个动作或片段之间传输数据,检索已删除的数据并离线将其存储在本地,或者非平凡应用程序遇到的许多其他常见情况。尽管不需要以下建议,但我们的经验表明,从长远来看,它们的实现可使您的代码库更可靠,可测试并受支持:避免将应用程序的入口点(例如动作,服务和广播接收器)指定为数据源。相反,他们只需要与其他组件进行协调即可获得与此入口点相关的数据的子集。应用程序的每个组件的寿命都很短,这取决于用户与其设备的交互以及系统的当前状态。在应用程序的各个模块之间创建明确的职责分工。例如,请勿分发将数据从网络下载到代码库中多个类或程序包的代码。同样,不要在同一类中定义多个不相关的职责,例如数据缓存和数据绑定。尽可能少暴露每个模块。抵制创建“仅一个”标签的诱惑,以从一个模块中揭示内部实现的细节。您可能会在短期内获得一些时间,但是随着代码库的发展,您将招致很多技术性债务。考虑一下如何使每个模块都可独立测试。例如,拥有用于从网络检索数据的定义明确的API,可以轻松测试将这些数据存储在本地数据库中的模块。相反,如果您将这两个模块的逻辑混合在一处或在整个代码库中分配网络代码,则测试将变得更加困难-在某些情况下甚至不是不可能。专注于应用程序的独特核心,以使其与其他应用程序脱颖而出。不要一遍又一遍地写相同的图案来重新发明轮子。相反,将时间和精力集中在使您的应用程序与众不同的方面,并让Android体系结构组件和其他推荐的库应对重复的模式。保持尽可能多的相关性和最新数据。因此,即使设备处于脱机状态,用户也可以享受应用程序的功能。请记住,并非所有用户都使用恒定的高速连接。将单个数据源指定为唯一的真实源。每当您的应用程序需要访问这些数据时,它都应始终来自这一单一事实来源。附录:披露网络状态
在推荐的应用程序体系结构的以上部分中,我们跳过了网络错误和启动状态,以简化代码段。本节说明如何使用Resource类显示网络状态,该类封装了数据及其状态。以下代码段提供了示例实现Resource:
由于在显示此数据的副本时从网络下载数据是很常见的做法,因此创建可在多个位置重用的帮助程序类很有用。在此示例中,我们创建一个名称为的类NetworkBoundResource
。下图显示了NetworkBoundResource
:的决策树:
它从观察数据库中的资源开始。第一次从数据库下载记录时,它将NetworkBoundResource
检查结果是否足够好以致可以发送,或者是否需要从网络中检索记录。请注意,这两种情况可能同时发生,因为您可能想在从网络更新数据时显示缓存的数据。如果网络调用成功,它将响应存储在数据库中并重新初始化流。如果网络请求NetworkBoundResource
失败,它将直接发送失败。. . , .
请记住,依赖数据库发送更改涉及使用相关的副作用,这不是很好,因为如果数据库未发送更改(因为数据未更改),则可能发生这些副作用的未定义行为。另外,请勿发送从网络接收到的结果,因为这违反了单一事实来源的原则。最后,数据库可能包含在保存操作期间更改数据值的触发器。同样,不要在没有新数据的情况下发送“ SUCCESS”,因为这样客户端将收到错误版本的数据。下面的代码片段显示了该类NetworkBoundResource
为其子类提供的开放API :
请注意以下有关类定义的重要细节:- 它定义了两个类型参数,
ResultType
并且RequestType
由于从API返回的数据类型可能与本地使用的数据类型不对应。 - 它使用一个类
ApiResponse
来处理网络请求。ApiResponse
是Retrofit2.Call
将响应转换为实例的类的简单包装LiveData
。
该类的完整实现NetworkBoundResource
出现在GitHub android-Architecture-components项目中。你已经创建完成后NetworkBoundResource
,我们可以用它来记录我们连接到磁盘和网络实现User
在课堂UserRepository
: