Android官方架构指南2025融会贯通

发布于:2025-08-07 ⋅ 阅读:(25) ⋅ 点赞:(0)

本文学习官方应用架构指南 | App architecture | Android Developers

上一篇: android MVC/MVP/MVVM/MVI架构发展历程和编写范式

不是做翻译,而是把重点写出,并用自己的话讲出来。就像我上一篇文章android MVC/MVP/MVVM/MVI架构发展历程和编写范式-CSDN博客学习架构要关注的是思想重点,而非某种模式的教条模板代码。提到的:业务分层数据驱动单向数据流, 事件绑定监听, 生命周期感知关注内存泄露

整个官方文档包含概述2章,界面层4章,中间层1章,数据层3章。有的章节写出了提纲挈领的心得体会,有的章节写的不知所云,可能也是谷歌官网大量使用机翻,翻译的不够流畅的原因。本文主要以个人心得体会为主。

经过仔细学习,大概融会贯通,总结了一篇精华文章。相信观众一定能有所感悟。

一、概况

官方从来没有说过android架构到底属于哪种,MVVM或MVI架构。只提到了现代化andriod需要遵循的原则。这也是这些年kotlin协程,Flow大力发展后而形成的一种开发范式。比如协程(作用域,挂起函数,线程模型选择),单向数据流思想,UI状态/StateFlow,ViewModel等等。

UI层:View/Compose

​ ViewModel(UIState(StateFlow))

Data层:

​ Repository(Data Sources)

​ Repository(Data Sources)

​ Room, data Store, retrofit…

Domain层(可选):

​ UserCase(Repository, Reposity)

二、原则

首先我们思考下,一个稳健,易于维护的项目,有哪些原则?

  • 关注代码分离

    最常见的臃肿就是在Activity/Fragment中一把梭,编写所有代码。界面类,必须保持仅仅处理界面和交互的逻辑。

  • 数据驱动模型

    数据它们应该独立于界面组件,它们的生命周期与组件应该没有关联。

    数据应该是持久化的,应用杀死后不会丢失数据,没有网络等条件下能继续工作。

    增加可测试性和稳定性。

  • 单一数据源

    定义新数据类型时,它们应该被放在单一数据源里面,公开出去的是不可变类型;而为了修改,则公开函数或者接收其他类型的事件。这样做的好处:更改数据集中在一起;保护数据不被到处修改;可追溯数据的变更,更容易发现bug。它们可能是数据库,网络请求单例,在其他情况,可能是ViewModel或者界面。

  • 单向数据流

    单一数据源,往往就要求单项数据流一起使用。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向ViewModel再传递给单一数据源,在单一数据源中应用数据被修改并以不可变类型公开。

  • 数据/ViewModel远离Context

    不要在activity,service等应用的界面层作为数据源,他们的生命周期是短暂的。

    除了Activity/Fragment等UI界面与Context,Toast等UI层或者context有关。

    而数据类,ViewModel类都不要跟这些有任何关系。

    可以减少耦合,提升可测试性,也避免了生命周期的引用问题。

    相信有点经验的开发者都会避免这点。

  • 其他注意事项

    注意并发,耗时放到子线程,缓存数据来做离线显示提升用户体验,类的单一原则等。

三、分层架构

基于上述原则,一个应用可以分为几层:

  • 界面层:把数据显示到屏幕上(包含ViewModel)。 第四章会展开介绍。
  • 数据层:包含应用业务逻辑并公开数据。
  • 中间层(可选):简化和重复使用界面层与数据层之间的交互。(注意:不是ViewModel,而是多个ViewModel处理的时候,用来简化和重用的。)

同时,官方提出了一些现代化架构的重点

  • 响应式分层架构;

  • 单向数据流和单一数据源;

  • 包含状态容器的界面层,用于管理复杂的界面;状态容器:就是ViewModel包裹StateFlow。

  • 协程和数据流;

  • 依赖注入

看到这里,图穷匕见,所有现代化android架构

就是最新的kotlin,协程,ViewModel,StateFlow及所带来的单向数据流思想,Hilt等框架的应用…

接下来就是逐层展开。

四、界面层

常规思想:

分为两部分:

  • 使用View(传统的xml方式)或者Compose(目前主推)构建;
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。

你看,官方文档讲述的ViewModel其实属于UI层,与我们平时说的MVVM中的ViewModel有区别吧?让我们接着往下看。

4.1 StateHolders概念

android界面层:

什么是StateHolders状态容器呢?就是把UiState放在这个类里面去管理的,就是状态容器。
很明显,android上ViewModel就是最好的对象。

这张图很明显就是把StateHolders替换成了ViewModel。就是把架构从常规架构的思想,变成了android通用App架构。

4.2 界面生命周期

不要继承修改onResume等生命周期。
使用viewLifecycleOwner.lifecycle.addObserverrepeatOnLifecycleCompose CollectAsStateWiteLifecycle监听方式。

4.3 定义UiState

因为用户的交互(比如前面的收藏按钮),或者网络请求,其他事件等都会修改数据层的数据。如果到处可以修改数据,或者修改界面,代码变得耦合,难以测试,没有边界。

我们的目的是希望界面的唯一职责是使用和显示UiState

我们需要需要把界面抽象成UiState, 包括2个原则:

  • State是把整个UI所有可能放在一个State中,使用一个State类去描述界面的当前状态。

    比如登录状态变量Boolean,loading中变量Boolean,数据列表List,消息内容List,全部都放在一个State类里面。要做的是变更StateFlow<State>的value,从而刷新UI。

    这与LiveData做显示的时候,有一些区别,往往我们会定义一堆LiveData公开出去让界面注册。这里官方就强调最好是一个类,包含一个页面所有的元素的状态。

    但也并不是一定要这样做,如果某些界面类型不相关,是可以考虑分开的。

  • 不可变性

    定义状态变量都使用val/final不可变属性。

    比如:某个收藏按钮,如果你的UiState里面定义了可变的收藏状态var isFavourite;那么, 当你直接变更UI,并修改此变量。界面层就会跟数据层都会对数据对象进行竞争更改。

    破坏了单一数据源原则。正确做法是UI层通知数据层去发生变化,从而影响State的变化。

4.4 单向数据流原则

单向数据流是什么?状态向下流动,事件向上传递

  • ViewModel 会存储并公开界面要使用的状态。界面状态是经过 ViewModel 转换的应用数据。
  • 界面会向 ViewModel 发送用户事件通知。
  • ViewModel 会处理用户操作并更新状态。
  • 更新后的状态将反馈给界面以进行呈现。
  • 系统会对导致状态更改的所有事件重复上述操作。

为什么要用单向数据流呢?

  • 代码分离:状态变化的来源的代码位置,转变的实现代码位置,最终使用的代码位置,这些是独立分离的

    通过观察状态变化来显示信息,并通过将这些变化传递给 ViewModel 来传递用户事件。

  • 数据一致性:单一数据源,唯一可信;

  • 可测试性:状态来源是独立于界面的,便可测;

  • 可维护性:状态的更改定义十分明确,即状态更改是用户事件及其数据拉取来源共同作用的结果。

4.5 推荐使用ViewModel

有如下原则:

  • ViewModel不要引用context,resource,toast等;比如列表显示,弹窗消息,控件显示,一定在Activity/Fragment等界面代码上;不能在ViewModel中;

  • 使用协程,viewModelScope和挂起函数;

  • ViewModel 应该通过名为 uiState 的单个属性向界面公开数据。如果界面显示多块不相关的数据,可以考虑多个UiState;

  • 使用StateFlow来包裹UiState;

  • 如果数据作为来自层次结构中的其他层的数据流传入,您应该使用 stateIn 运算符和 WhileSubscribed(5000) (示例)来创建 uiState。(留个坑,学习Flow的combine,和转换,但与架构无关。)

  • 您可以选择将 UiState 作为能够包含数据、错误和加载信号的数据类。如果不同状态是互斥的,该类也可以是密封的类。

基本流程如下:

ViewModel中持有StateFlow(数据对象,就是用来包裹uiState的),它们可以供View层去注册监听,刷新UI。
定义了所有的显示UI的State;
定义了所有的View层往ViewModel调用的Events;

View->ViewModel:通过函数传递Events
ViewModel->Model: 解析具体某个Action,执行对应的Model数据层请求,并转变得到State。
ViewModel->View: State更新以后,触发View层的UI更新。

Demo代码:可参考https://blog.csdn.net/jzlhll123/article/details/149835752 的MVI章节的代码。如果没有写过Kotlin的ViewModel+StateFlow建议看一下加深印象。

4.6 界面事件的常规做法

这一小节告诉我们,界面层的操作,比如点击事件,输入内容,刷新按钮等应该如何调用ViewModel。

Demo:

//通过ViewModel函数传递事件。对应架构图中的events
binding.refreshButton.setOnClickListener {
    viewModel.refreshNews()
}

//recyclerView的点击事件
data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    val publicationDate: String,
    val onBookmark: () -> Unit //点击事件定义在ViewHolder绑定的数据类型Bean中
)

class LatestNewsViewModel(
    private val formatDateUseCase: FormatDateUseCase,
    private val repository: NewsRepository
)
    val newsListUiItems = repository.latestNews.map { news ->
        NewsItemUiState(
            title = news.title,
            body = news.body,
            bookmarked = news.bookmarked,
            publicationDate = formatDateUseCase(news.publicationDate),
            onBookmark = { //点击事件的操作
                repository.addBookmark(news.id)
            }
        )
    }
}

其实这一小结很简单,就是想告诉我们界面通过ViewModel的函数来传递events;

对于recyclerView有2种办法:

第一种通过bean类传递lamda函数,如上demo中编写内容;

第二种办法,RecyclerView的item点击事件转达给Activity/Fragment,然后通过ViewModel函数调用。

综合言之,就是通过ViewModel公开函数去调用。而不能把ViewModel传来传去。

官方文档还介绍了:

函数里面,通过scope控制生命周期,通过协程,发起异步。然后就是讲Flow的用法。比如如何combine Flow变成StateFlow。 shareIn等等。后续我们学习一下Flow的具体用法。

五、数据层

数据层包含了业务逻辑,如何创建,存储,修改数据,给其他代码提供公开的数据。

2个分类:

本地存储:数据库,DataStore,SharedPreference,Firebase API, MMKV;

远程数据:网络请求okhttp/retrofit,蓝牙,GPS等等。

组织代码

  • 为每一类不同类型定义一个仓库(Repository):
  • 数据层可以包含多个仓库(repository),每一个仓库可以有0~N个数据源(DataSource);
  • 解决多个数据源(DataSource)的整合冲突;
  • 公开数据和集中处理数据变化;
  • 业务逻辑;
  • 一个DataSource仅处理一种数据(文件,网络,或数据库):

比如你有个应用数据字典,放在asset里面,app出厂有一份,网络更新/Cache各有一份。优先使用新的。怎么做呢?

AppDictRepository {

​ AssetAppDictDataSource()

​ NetworkCacheAppDictDataSource()

​ NetworkAppDictDataSource()

}

官方并告诉大家,应该如何组织多层代码:
请添加图片描述
应该使用room,okhttp,retrofit,协程等框架;
应该使用DataStore,room,sqlite,sharedPref等缓存技术;
使用WorkManger实现定期更新;
对于线程,生命周期的重点把控。

六、中间层 (可选)

负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。

重点:

  • 避免代码重复。
  • 改善使用类的可读性。
  • 改善应用的可测试性。
  • 让您能够划分好职责,从而避免出现大型类。

其实官方文档讲的不太好,云里雾里。总结一句话:

如何改造ViewModel里面的使用的那些可能到处使用的数据提供类。
请添加图片描述

有大量代码开发经验的工程师相信都会自行分离代码。
类的单一原则。 就是告诉我们一个类干一件事情。通过组合模式,降低代码的复杂度,便于重用。

  • 组合模式
ViewModelA {
	 AReposity
	 Breposity
	 CReposity
	 FormatHelperA
	 HelperB
}

ViewModelB {
	 AReposity
	 Breposity
	 HelperB
}

//转变为
UserCase {
	AReposity
	BRepsity
	HelperB
}
		
ViewModelA {
	UserCase
}
...

其中提到2个点,我觉得是有用的:

  • 生命周期的管控

    不应该由这些数据类或者中间层类来管理。我们可以通过suspend函数往外公开,那么它就可以被生命周期管理了,在ViewModel里面自行scope.launch来执行。

  • 线程处理

    参考如下,入参defaultDispatcher + withContext(defaultDispatcher)

    我认为觉得也没什么必要,直接在调用的地方,scope.launch(Dispatcher.IO/Default)里面传递就好了,更为直观,控制逻辑也交给ViewModel。

class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository,
  private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default //线程逻辑
) {
    suspend operator fun invoke(): List<ArticleWithAuthor> =
        withContext(defaultDispatcher) {
            val news = newsRepository.fetchLatestNews()
            val result: MutableList<ArticleWithAuthor> = mutableListOf()
            // This is not parallelized, the use case is linearly slow.
            for (article in news) {
                // The repository exposes suspend functions
                val author = authorsRepository.getAuthor(article.authorId)
                result.add(ArticleWithAuthor(article, author))
            }
            result
        }
}
七、管理类之间的依赖关系

类与类之间的关系,可以通过两种设计模式来解决:

  • 依赖注入(DI)
  • 服务定位器(service locator)

当然啦,就引出了Hilt的使用。主要解决构造函数注入。

如果类型包含多项需要共享的可变数据,可以通过工厂模式创建共同类和使用单例管理这些工厂代码的创建。具体参考https://developer.android.com/training/dependency-injection/manual?hl=zh-cn#dependencies-container

八、测试
最后

官方原话:

提供的建议和最佳实践可应用于各种应用。遵循这些建议和最佳实践可以提升应用的可扩展性、质量和稳健性,并可使应用更易于测试。不过,您应该将这些提示视为指南,并视需要进行调整来满足您的要求。

这是在告诉我们,不要教条盲从,非要使用一个复杂开发框架,把代码写的十分复杂,才是好的吗?

应该找到合适的开发方式就是最好的。

下一篇我将结合自己开发实例给出一套基于MVI, Redux思想,和官方StateFlow/SharedFlow的简易框架。


网站公告

今日签到

点亮在社区的每一天
去签到