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开发规范设计我们的架构,有一些建议显得有些啰嗦,但是真正实施下来之后,我们项目整体的可扩展性就会变得很高了。

相关推荐
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿4 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
哔哥哔特商务网4 小时前
一文探究48V新型电气架构下的汽车连接器
架构·汽车
007php0074 小时前
GoZero 上传文件File到阿里云 OSS 报错及优化方案
服务器·开发语言·数据库·python·阿里云·架构·golang
1024小神5 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛5 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法6 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
码上有前6 小时前
解析后端框架学习:从单体应用到微服务架构的进阶之路
学习·微服务·架构
NotesChapter7 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快8 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android