关于 Android App 架构,你可能会被问到的 20 个问题

6,250 阅读10分钟

LiveData 是否已经被弃用?

没有被弃用。在可以预见的未来也没有废弃的计划。

LiveData 可以使用简单的方式获取一个易于观察、状态安全的对象。虽然其缺少一些丰富的操作符,但是对于一些简单的 UI 业务场景已经足够。

FlowLiveData 相同的功能,其包含大量丰富的操作符,可以简单的完成复杂的业务逻辑处理。相应的会增加一定的使用门槛,如果不需要 Flow 的 全部功能,继续使用 LiveData 即可。

更多内容:Migrating from LiveData to Kotlin’s Flow

什么是业务逻辑?为什么将业务逻辑移动到 Data Layer ?

业务逻辑是给 App 带来价值的东西,是一系列决定 App 如何运行的规则集。比如,如果你有一个显示新闻文章的 App,你点击书签按钮,从书签文章到持久的交互,这就是业务逻辑。

将业务逻辑移动到 Data Layer 的主要原因是提供单一信源,App 的不同页面和不同逻辑使用到这部分数据将从同一个地方进行获取,方便统一管理;其次是分离职责,将不同的处理逻辑(控制UI的与操作数据)分散在不同的层级中,可以减少各自交互的复杂度。

如何在 ViewModel 中获取系统资源(字符串、颜色等)?

访问资源只应该放在 UI 层,如 View 或者是 Compose。 可以在 ViewModel 中提供资源 ID,但是不应该直接在 ViewModel 中直接访问资源。除了单一职责之外,就是可以响应手机配置的变化:

  • 切换语言的时候,会变成对应的语种字符串;
  • 切换亮暗模式的时候,对应的颜色会随之变化;

另外就是 Context 的上下文与 ViewModel 的生命周期不一致,可能会导致内存泄漏。

如何让 Service 与 Compose/ViewModel 进行交互?

Modern Android App Arch.001.png

他们不应该直接进行交互或者说是互相引用。

应该采用单一信源的方式定义一个 Repository,在 Service/ViewModel 中调用 Repository 中的方法来更改状态,UI(Views/Compose) 层通过数据流的方式监听数据变化。

可以确定的是,在 ViewModel 中应尽可能少的依赖 Android 系统组件,如 Service、Activity 等。

未来平稳过度到 KMM/KMP 的最佳实践有哪些?

通常最佳实践会随着时间的推移而建立起来的。对于未来如果 KMM/KMP ,并不需要做过多的事情,遵循架构指南即可。比如:将数据的操作放在 Data Layer,在此层尽量少的调用系统 API。但是目前并需要定义跨端的接口定义,因为 KMM/KMP 还是比较新的技术,在他被大规模使用之前还有不少变数。

ViewModel 被建议当做 State Holders 之后,其在 MVVM 中的职责?

ViewModel 在 MVVM 中就是一个状态持有者。在不同的语义场景下可能有不同的含义。如果是整个页面中的 View(不论是 Activity 还是 Compose 中的目的地),其对应的是 AAC 中的 ViewModel 类。如果是普通的自定义 View 或是 Compose 中的一个可组合函数,那么 View 的状态持有者定义成一个普通的类就可以。这里可以类比下 RecycleView 中的 ViewHolder 设计。

所以这是根据 UI 的范围所决定的,整个页面对应的就是 ViewModel,局部页面对应的就是普通的类。

ViewModel 可以当做是一种特殊的状态管理器,他可以持有普通的 State Holders。

架构指南中的层级对模块化开发有什么建议么?

官方正在研究模块化的架构指南,希望今年能够完成。

应该在 DataSource/Repostory/UseCase 中使用 Flow 么?

可以,但是应该在合适的地方使用 Flow。如果是提供一个一次性的数据(比如从云端接口返回的一个数据),使用 suspend 函数是比较合适的。如果是从 Room 或者是其他类似的数据源中提供可以变化的数据流时,应该使用 Flow。

在 Repository 中会合并多个数据源的数据,这些数据可能会会随时间的变化而变化,因此需要使用 Flow。

什么时候应该使用 UseCase ?

UseCase 主要是为了解决以下两个问题:

  • 封装复杂的业务逻辑,避免出现大型的 ViewModel 类;
  • 封装多 ViewModel 通用的业务逻辑,避免代码重复;

满足上述两个条件的任何一个都可以使用 UseCase。除此之外,使用 UseCase 之后可以 ViewModel 就不必依赖 Repository,而是在构造函数中直接使用 UseCase,这样在构造函数中就可以知道 ViewModel 中做了哪些事情。

在 Repository 中传递多个 Suspend 函数是反模式的么?

并不是,如果只是进行一次性的操作应该使用 suspend 函数。否则的话应该使用 Flow

ViewModel 中如何处理页面跳转?

这部分内容在 UI Layer 中有提到,我们把他称之为 UI Event 之类的东西。不管是 UI 事件还是构建 UI 的数据都应该放在 UiState 中,一旦 UI 层监听到对应的事件,进行响应的跳转即可。

具体取决于采用何种方式建模,比如你可以把他定义为一个 Boolean 值,在 ViewModel 中更新对应的 UiState,UI 层就可以根据对应的事件跳转到对应的页面中了。

以用户登录为例,用户登录成功之后需要进入到主页面。这其实是一种状态的变化,从未登录变成了已登录,所以只要改变 UiState UI 层就可以处理他。你可能会说这是不是太复杂了?

对一些人来说这可能是一个顿悟时刻,不会把命令视为事件。ViewModel 中的事件整体上来看就是 App 的状态数据,这样 ViewModel 并不是告诉 UI 层应该做什么,而是 UI 层根据 App 的状态去做一些事情,这是思维方式上的转变。

如果根据用户配置定义导航视图?

和上一个问题基本类似,通过设置不同的 UiState 来控制导航到不同的视图。

WorkManager/Service 在架构中的什么位置?

WorkManager/Service 应该作为入口类来调用 Repository 中的 API。这样可以使我们的业务逻辑与 Android 系统的 API 中解放出来,因为 Android 系统 API 的行为逻辑一般是我们不可控的。当然还有另外的好处,比如更容易测试、以及后续有可能迁移到 KMM

当然,WorkManager 有其 API 自身的优势,其内部逻辑与节省电量 、WIFI 链接等 Android 平台的优势,可以根据设备自身的状态进行一次性或者周期性的任务。当然,如果是正常的耗时操作(如网络请求)使用简单的协程即可。

为什么有时在 Repository 中使用 IoDispatcher 访问数据库,有时不用?

Room 数据库目前已支持 suspend 函数,其默认会将任务放到后台线程中执行,当然也可以对其进行自定义的配置。当你调用 Room 的一个 suspend 函数的时候,你并不需要关心线程的问题,把这个问题交给 Room 处理即可。这样 Room 的所有操作都会放到 Room 控制的线程中执行,以尽可能的减少因访问同一个文件而导致的互相争夺资源的问题。

不管由于什么原因你无法使用/提供 suspend 函数,Room 不支持将其移动到另一个线程中。这种情况下可以使用 IO 线程或者 IoDispatcher 。通常情况下还是建议使用 suspend 函数或是 Flow。

另外,每个 DataSource 中都建议处理自己的线程问题,所以在 Repository 中并不需要关心 IO 线程的问题。

当我们处理内存敏感数据时,将复杂数据从一个屏幕传输到另一个屏幕的建议方法是什么?

最原始的处理方式可能是使用 Activity 当做两个 Fragment 的中介,在 Activity 中实现 Fragment 中定义接口,当 Fragment 被关联到 Activity 之后就可以调用 Fragment 中相关的逻辑了。当然这种方式已经一去不复返了。

现在可以使用 ViewModel 调用 UseCase 或者是 Repository 来更新数据,另外的 ViewModel 会使用 Flow 的方式接收到数据的变化,然后根据变化做出相应的处理即可。

在 Compose 中应该使用 MVVM 还是 MVI ?

两者没有本质的区别,都是采用单向数据流的方式。MVI 的特点是将 UI 事件封装成枚举方式,统一在一个函数中处理事件。

随着项目的扩展,我应该将 Domain Layer 或者 Data Layer移动到单独的模块吗?

这取决于你是否想进行模块化,但是两者的差异并不大,这里并没有一个明确的答案。 随着项目的扩展建议把 Data Layer 放到单独的 module 中,如果有 Domain Layer 的话,也是建议把他放在单独的模块中。

[!info] 注: 如果是在多仓库的模块架构以及同一数据层可能有不同 UI 实现的场景下建议采用上述方式,如果没有类似需求的话,个人建议通过 Feature (特性)划分模块,这样可以减少不必要的编译耗时,同样的功能也想多内聚。 当然,如果有对外提供 SDK 需求的,也应将 Data Layer 定义在单独的模块中。

将错误从其他层返回到表示层的最佳方法是什么?

指导文档 中的建议的方式是使用协程的异常处理机制将错误传递到展示层(UI Layer)。

对于使用协程或者是 Flow 的方式,建议使用协程的异常处理机制,当然 Folw 中也可以使用 catch 操作符;对于使用 suspend 函数的地方可以使用 try/catch 块。

另外一种 Data Layer 和上层交互的方式就是使用 Kotlin 自带的 Result<T> 类,此类中除了包含正常的数据 T 之外,还包含错误信息。除此之外提供很多好用的 API ,建议使用。

更多内容:Exceptions in coroutines

新的架构方式是否适用与非移动端的领域?如平板、Auto 等

在上述的领域上,仍然建议使用 UI LayerDomain LayerData Layer 这种架构方式。这种架构方式主要是用到了关注点分离以及数据驱动的方式,而这两种设计原则对非移动端的应用也是适用的。

多 Activity 还是单 Activity + 多 Fragment ?

Activity 有其自身的一些特定及职责,在以下场景中应该使用 Activity:

  • 当前页面需要支持 PIP(画中画)功能时;
  • 当 App 有多个入口可以启动时,如在其他 App 中拉起当前 App 中的一个二级页面,这个页面就需要使用 Activity;

简而言之,就是需要在 manifest 文件中 exported 需要被设置为 true 的逻辑都需要使用 Activity 来实现。

除此之外的一些逻辑,通常是 App 内部的一些页面导航,这个时候应该使用 Fragment 来实现。当然,现在我们还有另外一个新的选择,那就是使用 Compose 。

总结

这篇文章中的内容根据 Arch - MAD Skills 中的问答部分,结合自己的理解整理而得,会包含个人观点,更多详细内容可以观看原文。

问答中的大部分内容在之前的文章中都用提到,如有不清楚的地方可以查看之前的文章:

关于 Android App 架构的实践部分可以参考:

我从 Android 官方 App 中学到了什么?


更多内容会第一时间发布在微信公众号中,欢迎大家关注: