Android进阶宝典 -- Google对于开发者的一些架构建议

对于架构设计,Google一直想要规范开发者的开发习惯,但是在上层应用开发中又太过于灵活,所以一直没有形成统一的规范,即便如此,Google几乎在1-2年的时间范围内,都会推出一种新的架构设计模式,以此来优化此前的架构模式,从MVP,到MVVM,再到现在的MVI。那么在这篇文章中,我将会根据Google的开发者文档中给出的建议,通过实际的代码实现来深入说明架构的准则。

1 分层架构

其实分层架构核心的观念就是分离关注点,Google应用架构简介入口 在Android最早期的MVC架构中,往往在一个Activity或者Fragment中完成了所有业务逻辑的编写,这种设计现在看来就是一种错误,代码臃肿无法扩展,更不要说随着App大小不断增大,从而实现业务的可插拔(扩缩)。

因此从MVP架构出现以后,分离关注点开始逐渐被外界认可,数据层专注于数据生产,界面层专注于数据的展示,而Presenter层或者之后的ViewModel层,则是作为两者之间通信的桥梁。那么对于分层架构,Google对于开发者的建议是如何的呢?

1.1 【强烈建议】使用明确的数据层

数据层,主要用于提供数据,无论是从网络获取,还是从本地数据库获取,都是一个独立的模块,用于公开全部的应用数据,而且会处理绝大部分的业务逻辑。

在实际的项目开发中,建议将数据层存放在data软件包或者data provider模块中,如下

如果将数据层单独拆一个lib_data模块,那么这个模块就是一个公共模块,应用的其余模块均可引用(如果用到数据层的数据),在Google的建议中,即便是只有一个数据源,也要放在单独的软件包或者模块中。

在数据层中,你可以根据业务类型或者数据类型,创建不同的${type}Repository存储类,例如与登录相关的,可以叫做LoginRepository;与支付相关的,叫做PaymentRepository

记住一点,存储类是唯一可以直接跟数据源打交道的类,其他层不能直接访问数据源,也就意味着外层访问数据层的唯一入口就是存储类。 所以,存储类的主要作用就是:

(1)对外提供应用需要的数据;

(2)集中处理数据的变化;

(3)处理部分业务逻辑;

所以对于存储类来说,需要与数据源绑定在一起,每个数据源类只能负责处理一种数据来源,这个来源可以是网络、本地数据库、本地文件等。 我们拿LoginRepository举例来说:

登录数据源类LoginRemoteDataSource

kotlin 复制代码
/**
 * 与登录相关的远程数据源,一般指与网络相关的
 */
class LoginRemoteDataSource {

    suspend fun login(username: String, password: String): Boolean {
        //模拟根据用户名和密码登录
        return withContext(Dispatchers.IO) {
            //延迟2s
            delay(2_000)
            NetUtils.login(username, password)
        }
    }
}

LoginRepository存储类需要持有远程数据源的引用,如果有本地数据源,那么也需要持有本地数据源的引用。

kotlin 复制代码
/**
 * 登录数据存储类
 */
class LoginRepository(
    private val loginRemoteDataSource: LoginRemoteDataSource
) {
    /**
     * 登录
     * @param username 用户名
     * @param password 密码
     */
    suspend fun login(username: String, password: String): Boolean {
        return loginRemoteDataSource.login(username, password)
    }
}

这里讲一下命令规范,对于存储库类以其负责的数据命名。具体命名惯例如下:

数据类型 + Repository。 例如:NewsRepositoryMoviesRepositoryPaymentsRepository

数据源类以其负责的数据以及使用的来源命名。具体命名惯例如下:

数据类型 + 来源类型 + DataSource。数据来源可以使用Remote或者Local来代表,更易懂。

当我们完成存储库类和数据来源类的设计之后,那么其他层要访问数据层,那么就访问LoginRepository即可。

1.2 【强烈建议】使用明确的界面层

界面层一般指的就是用于渲染界面的Activity或者Fragment,在实际的项目开发中,建议将界面层相关的类放在ui软件包下。

那么界面层的组件,例如Activity、Fragment、ViewModel想要访问数据层的数据时,就会使用到之前的LoginRepository类,通常对于数据访问存储逻辑,是放在ViewModel中实现,也为了保存界面层的数据。

2 界面层

前面讲到了,在界面层中需要获取数据,就要与数据层通信,而为了避免数据层和界面层之间的强耦合,会采用ViewModel作为两者的中间件来处理,那么对于界面层,Google有哪些规范呢?

2.1 【强烈建议】遵循单向数据流原则

什么是单向数据流呢?就是通过ViewModel来公开界面层的状态,并通过方法调用来接收界面的操作,而且界面层无法更改ViewModel中界面层的状态,以此形成一个单向的数据流,这也是MVI架构的一个核心概念。

kotlin 复制代码
/**
 * 与登录相关的状态
 */
sealed class LoginUiState {
    object IdleUiState : LoginUiState()
    class LoginSuccessUiState(val username: String) : LoginUiState()
    class LoginErrorUiState(val errorCode: Int, val errorMsg: String) : LoginUiState()
}

LoginUiState是与登录相关的状态,通过ViewModel向界面层公开,界面层在获取这些状态之后,做出相应的操作即可。

kotlin 复制代码
class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    /**
     * 登录逻辑
     */
    fun login(username: String, password: String) {
        viewModelScope.launch {
            loginRepository.login(username, password)
        }
    }
}

LoginViewModel是登录页面持有的ViewModel,其中定义的login方法就是界面层调用,而ViewModel层接收到界面层的操作之后,开始执行登录的操作。

2.2 【强烈建议】使用生命周期感知型界面状态收集方式

具备生命周期感知的收集方式,主要就是LiveData和Stateflow,而在2.1中,Google建议我们使用ViewModel公开界面层状态,LiveData就不适合这个场景,因此剩下的就是Stateflow,本身Stateflow是不具备生命周期感知的,但是在上篇文章中,我们在介绍Kotlin中的热流时,讲过使用repeatOnLifecycleAPI就能够使得flow具备生命周期感知能力。

Kotlin 复制代码
class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    private val _loginState: MutableStateFlow<LoginUiState> =
        MutableStateFlow(LoginUiState.IdleUiState)

    /**对外暴露登录的状态*/
    val loginState: StateFlow<LoginUiState> = _loginState


    /**
     * 登录逻辑
     */
    fun login(username: String, password: String) {
        viewModelScope.launch {
            val isLoginSuccess = loginRepository.login(username, password)
            if (isLoginSuccess) {
                _loginState.value = LoginUiState.LoginSuccessUiState(username)
            } else {
                _loginState.value = LoginUiState.LoginErrorUiState(-1, "登录失败")
            }
        }
    }
}

我们在LoginViewModel中向界面层暴露了登录的状态loginState,那么在用户点击登录时,便可以拿到这些状态。

kotlin 复制代码
val loginViewModel = LoginViewModel(LoginRepository(LoginRemoteDataSource()))
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        loginViewModel.loginState.collect { state ->
            when (state) {
                is LoginUiState.IdleUiState -> {

                }

                is LoginUiState.LoginSuccessUiState -> {
                    Toast.makeText(
                        this@MainActivity,
                        "${state.username}登录成功",
                        Toast.LENGTH_SHORT
                    ).show()
                }

                is LoginUiState.LoginErrorUiState -> {
                    Toast.makeText(
                        this@MainActivity,
                        "登录失败",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }
}
findViewById<Button>(R.id.btn_login).setOnClickListener {
    loginViewModel.login("layz4android", "123456")
}

所以这里还会再提一句,为什么LiveData虽然也具备感知生命周期的能力,但是不适用的原因,在状态频繁变化的场景,LiveData会丢失一些状态,但是LiveData一定能够把最新的状态返回界面层。

2.3 【强烈建议】请勿将来自 ViewModel 的事件发送到界面

其实这个建议,我并没有看懂,如果按照单向数据流的原则来看,ViewModel是处理事件的那一方,而界面层只是感知状态的变化,而不需要接收ViewModel的事件。

这里需要注意一点:ViewModel事件应始终会引发界面状态更新。

3 ViewModel

对于ViewModel层的界限,我认为它属于界面层的一部分,但又不完全与界面层强绑定在一起,通过前面的介绍,ViewModel就是用来提供界面状态以及对数据层的访问。

3.1 【强烈建议】ViewModel不应该与Android生命周期有关

ViewModel是有自己的生命周期的,其存在的时间是首次请求ViewModel创建,到宿主完全消失,我们拿Activity举例,当Activity调用onCreate之后,ViewModel就会被创建。

当屏幕发生旋转时,虽然Activity执行onDestroy但是会立刻重建,此时ViewModel还是存在内存当中,所以为什么会采用ViewModel存储页面状态,这就是原因之一,等到Activity调用finish完全消失之后,ViewModel才会销毁。

那么即便如此,在使用ViewModel时,不能将ActivityFragmentContextResources等作为依赖传递,如果一定要使用这些参数,那么就不能放到ViewModel中,需要考虑放在其他层中。

3.2 【强烈建议】使用协程和数据流

其实如果我们在使用MVI架构之后,数据流将会变成一个非常常用的工具,而在ViewModel中是会提供viewModelScope协程作用域,用于开启协程。

所以ViewModel在访问数据层时,因为大多数情况下都是进行网络请求,属于耗时操作,因此数据层中大部分的函数都是挂起函数,所以可以在ViewModel中开启协程,通过异步回调的方式获取服务端的结果,并解析数据分发数据流。

3.3 【强烈建议】在屏幕级别使用ViewModel

什么是屏幕级别,我理解就是我们常见的ActivityFragmentGoogle官方禁止在一些可重复使用的界面使用ViewModel,如果想要处理可重复使用的界面,需要使用普通的状态容器类(JetPack Compose)。

3.4 【建议】ViewModel公开界面状态

这是Google推出MVI架构之后,出现的一种概念叫UiState,在以往的架构设计中,界面层会拿到服务端请求的数据,根据拿到的数据判断要展示哪个页面。

像这样业务逻辑,如果少还好,但如果这个界面需要依赖多个接口的数据,那么势必会使整个界面层变得臃肿起来,我个人理解界面层应该只负责展示数据,而不是需要根据数据判断自己该展示什么页面。 所以界面层观察ViewModel层公开的界面状态,根据UiState选择展示某个页面,例如loading、error、success等页面。

但是对于一些开发者来说,这种迁移可能需要一段时间,因此Google仅仅是建议开发者使用这种思想,而不是强烈建议,看个人所好了。

像在2.2小节中,我们请求服务端拿到的是一个Boolean类型的数据状态,因此我们可以自己创建一个数据流MutableStateFlow,并对外提供不可变的StateFlow;但是如果从服务端拿到的是一个数据流,而且是一个冷流,官方建议使用stateIn将其转换为一个StateFlow。但是我理解在ViewModel层收集数据,将状态往界面层抛反而更合适一些

kotlin 复制代码
fun login2(username: String, password: String) {
    viewModelScope.launch {
        loginRepository.login2(username, password).collect{ userInfo->
            if (userInfo == null){
                _loginState.value = LoginUiState.LoginErrorUiState(-1,"登录失败")
            }else{
                _loginState.value = LoginUiState.LoginSuccessUiState(userInfo.username)
            }
        }
    }
}

通过这种方式反而能够统一规范。

这篇文档其实主要介绍了Google对于架构规范给出的一些建议,我们在实际的项目开发中,希望能够按照现代Android开发规范设计我们的架构,有一些建议显得有些啰嗦,但是真正实施下来之后,我们项目整体的可扩展性就会变得很高了。

相关推荐
工业甲酰苯胺7 小时前
分布式系统架构:服务容错
数据库·架构
拭心8 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
Java程序之猿9 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
带电的小王10 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡11 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道11 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
小蜗牛慢慢爬行12 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
阿甘知识库12 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道12 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频