随着Android应用开发技术的不断成熟,应用功能越来越丰富,迭代速度要求的越来越高,应用的开发架构也在不断演进、优化,从MVC、MVP到MVVM,到如今的MVI。谷歌官方也在不断推广、优化适合Android平台的开发架构,并推出了一系列的组件如Jetpack来支撑其架构的演进。
但不管架构如何演进,其本质目的就是尽量解耦各模块、各业务之间的依赖,消除样板代码,让开发人员专注于业务开发,快速、高效、高质量完成应用的开发。
所以在了解各架构之前,我们还是要先回顾一下面向对象六大原则
一个类中应该是一组相关性很高的函数、数据的封装
软件中的对象(类、模块、函数等)应该对于扩展是开发的,但是对于修改是封闭的。
所有引用基类的地方必须能透明地使用其子类地对象。
指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。
在java语音中,抽象就是指接口或抽象类,细节就是实现类,高层模块就是调用端,低层模块就是具体实现类
依赖倒置:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
客户端不应该依赖它不需要的接口。
类间的依赖关系应该建立在最小的接口上。
一个对象应该对其他对象有最少的了解。
视图层(View):对应于XML布局文件和java代码动态添加、删除view的部分
控制层(Controller):主要负责业务逻辑,在android中由Activity、Fragment、Service等承担,同时因为XML视图功能太弱,所以Activity/Fragment等既要负责视图的显示,还要加入控制逻辑,业务处理等,承担了太多的功能。
模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源
存在问题:
以下都是以登录功能为例
class MVCDemoActivity : AppCompatActivity(), CoroutineScope by MainScope() {
private lateinit var usernameEdit: EditText
private lateinit var passwordEdit: EditText
private lateinit var usernameText: TextView
private lateinit var passwordText: TextView
private lateinit var loginBtn: Button
private var username: String = ""
private var password: String = ""
private var loadingDialog: LoadingDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mvc)
initViews()
}
private fun initViews() {
usernameText = findViewById(R.id.tv_username)
passwordText = findViewById(R.id.tv_password)
usernameEdit = findViewById(R.id.edit_username)
usernameEdit.afterTextChanged {
username = it
}
passwordEdit = findViewById(R.id.edit_password)
passwordEdit.afterTextChanged {
password = it
}
loginBtn = findViewById(R.id.btn_login)
loginBtn.setOnClickListener {
doLogin()
}
}
@SuppressLint("SetTextI18n")
private fun doLogin() {
showLoading(true)
launch(Dispatchers.IO) {
when (val loginResult = HttpUtil.login(username, password)) {
is LoginResult.Success -> {
runOnUiThread {
usernameEdit.visibility = View.GONE
passwordEdit.visibility = View.GONE
loginBtn.visibility = View.GONE
usernameText.text =
getString(R.string.username) + loginResult.userInfo.userName
passwordText.text =
getString(R.string.password) + loginResult.userInfo.password
showLoading(false)
showToast("登录成功")
}
}
is LoginResult.Fail -> {
runOnUiThread {
showLoading(false)
showToast(loginResult.message)
}
}
}
}
}
}
<--activity_mvc.xml-->
从上面的代码可以看出,所有的业务逻辑以及UI更新都放在了Activity中,如果功能简单还比较容易维护,但随着功能越来越复杂,Activity的代码量会成倍的膨胀,并且业务逻辑会交织在一起,越来混乱。
为了解决上面的问题,MVP就推广开了。
View层:对应于Activity与XML,只负责显示UI,只与Presenter层交互,与Model层没有耦合
Presenter层: 主要负责处理业务逻辑,通过接口调用Model层获取数据,并回调View层
Model层:主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源
存在问题:
登录示例代码
class MVPDemoActivity : AppCompatActivity(), LoginContact.LoginView {
private lateinit var loginPresenter: LoginContact.LoginPresenter
private lateinit var usernameEdit: EditText
private lateinit var passwordEdit: EditText
private lateinit var usernameText: TextView
private lateinit var passwordText: TextView
private lateinit var loginBtn: Button
private var username: String = ""
private var password: String = ""
private var loadingDialog: LoadingDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mvp)
loginPresenter = LoginPresenterImp(this)
initViews()
}
private fun initViews() {
usernameText = findViewById(R.id.tv_username)
passwordText = findViewById(R.id.tv_password)
usernameEdit = findViewById(R.id.edit_username)
usernameEdit.afterTextChanged {
username = it
}
passwordEdit = findViewById(R.id.edit_password)
passwordEdit.afterTextChanged {
password = it
}
loginBtn = findViewById(R.id.btn_login)
loginBtn.setOnClickListener {
loginPresenter.login(username, password)
}
}
override fun showSuccess(userInfo: UserInfo) {
runOnUiThread {
usernameEdit.visibility = View.GONE
passwordEdit.visibility = View.GONE
loginBtn.visibility = View.GONE
usernameText.text = getString(R.string.username) + userInfo.userName
passwordText.text = getString(R.string.password) + userInfo.password
showToast("登录成功")
}
}
override fun showFail(message: String) {
runOnUiThread {
showToast(message)
}
}
override fun showLoading(show: Boolean) {
if (show) {
loadingDialog = LoadingDialog(this)
loadingDialog?.show()
} else {
loadingDialog?.cancel()
loadingDialog = null
}
}
}
interface LoginContact {
interface LoginModel {
suspend fun login(username: String, password: String)
}
interface LoginView {
fun showSuccess(userInfo: UserInfo)
fun showFail(message: String)
fun showLoading(show: Boolean)
}
interface LoginPresenter {
fun login(username: String, password: String)
fun onLoginSuccess(userInfo: UserInfo)
fun onLoginFail(message: String)
}
}
class LoginPresenterImp(private val loginView: LoginContact.LoginView) :
LoginContact.LoginPresenter,
CoroutineScope by MainScope() {
private val loginModel: LoginContact.LoginModel
init {
loginModel = LoginModelImpl(this)
}
override fun login(username: String, password: String) {
loginView.showLoading(true)
launch(Dispatchers.IO) {
loginModel.login(username, password)
}
}
override fun onLoginSuccess(userInfo: UserInfo) {
loginView.showLoading(false)
loginView.showSuccess(userInfo)
}
override fun onLoginFail(message: String) {
loginView.showLoading(false)
loginView.showFail(message)
}
}
class LoginModelImpl(private val loginPresenter: LoginContact.LoginPresenter) : LoginContact.LoginModel {
override suspend fun login(username: String, password: String) {
when (val loginResult = HttpUtil.login(username, password)) {
is LoginResult.Success -> {
loginPresenter.onLoginSuccess(loginResult.userInfo)
}
is LoginResult.Fail -> {
loginPresenter.onLoginFail(loginResult.message)
}
}
}
}
从上面的代码可以看出,Activity不再与Model模块有交互,只和Presenter进行接口调用,所有的业务逻辑、UI刷新逻辑都由Presenser去实现,但这样就导致了Presenter持有了Activity的引用,存在耦合关系;同时可以看到LoginContact中定义了大量的接口类,随着业务的增加,接口类会几何倍数的增加,维护起来会越发困难。
为了解决上面的问题,Google开始推广MVVM架构,并为其定制实现了Jetpack组件,为开发人员快速实现MVVM架构提供强大的支持。
View: 对应于Activity和XML,负责View的绘制以及与用户交互。
Model:主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源
ViewModel: 负责完成View与Model间的交互,负责业务逻辑。
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向数据绑定(Data-Binding):View的变动,自动反映在 ViewModel,反之亦然
MVVM的两种实现方案:
Jetpack是Google为了解决Android架构问题而引入的,Google官方说的说法:“Jetpack是一套库、工具和指南,可以帮助开发者更轻松地编写应用程序。Jetpack中的组件可以帮助开发者遵循最佳做法、摆脱编写样板代码的工作并简化复杂的任务,以便他们能将精力集中放在业务所需的代码上。”
Jetpack学习https://blog.csdn.net/gxlgxjhll/category_10836219.html
Google官方推荐的MVVM架构
登录示例,采用AAC方案(Google官方推荐)
class MVVMDemoActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
private var loadingDialog: LoadingDialog? = null
private lateinit var binding: ActivityMvvmBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMvvmBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.vm = viewModel
initObserver()
}
@SuppressLint("SetTextI18n")
private fun initObserver() {
binding.editUsername.afterTextChanged {
viewModel.userName = it
}
binding.editPassword.afterTextChanged {
viewModel.password = it
}
viewModel.loginMessage.observe(this) { message ->
showToast(message)
}
viewModel.userInfo.observe(this) { userInfo ->
binding.editUsername.visibility = View.GONE
binding.editPassword.visibility = View.GONE
binding.btnLogin.visibility = View.GONE
binding.tvUsername.text = getString(R.string.username) + userInfo.userName
binding.tvPassword.text = getString(R.string.password) + userInfo.password
}
viewModel.isLoading.observe(this) { show ->
showLoading(show)
}
}
}
<--activity_mvvm.xml-->
class LoginViewModel : ViewModel() {
private val repository = LoginRepository()
var userName: String = ""
var password: String = ""
private val _loginMessage = MutableLiveData()
val loginMessage: LiveData = _loginMessage
private val _userInfo = MutableLiveData()
val userInfo: LiveData = _userInfo
private val _isLoading = MutableLiveData()
val isLoading: LiveData = _isLoading
fun login() {
viewModelScope.launch(Dispatchers.IO) {
_isLoading.postValue(true)
when (val loginResult = repository.login(userName, password)) {
is LoginResult.Success -> {
_isLoading.postValue(false)
_userInfo.postValue(loginResult.userInfo)
_loginMessage.postValue("登录成功")
}
is LoginResult.Fail -> {
_isLoading.postValue(false)
_loginMessage.postValue(loginResult.message)
}
}
}
}
}
class LoginRepository {
suspend fun login(username: String, password: String): LoginResult {
return HttpUtil.login(username, password)
}
}
从上面的代码可以看到,Activity只进行UI更新,Repository负责数据获取,而ViewModel负责业务逻辑,通过LiveData将数据发送到Activity中,各组件已最小的持有其他引用。
但这种架构还是存在一定的问题,一是为了保证LiveData数据的更新只在ViewModel中,则必须定义一个私有和一个公开的属性,随着数据的增多,属性会成倍的增加;二是Activity中是通过LiveData的observe方法来更新UI的,而在ViewModel中LiveData数据的更新并没有约束,各种方法里都可能进行更新,使用混乱,难以管理。
而为了解决上面的问题,Google官方推出了最新的架构,我们姑且称之为MVI吧(我觉得这种说法并不准确)。
要了解MVI架构,首先要先了解两个架构原则。
Goolge官方介绍:
在应用中定义新数据类型时,您应为其分配单一数据源 (SSOT)。SSOT 是该数据的所有者,而且只有此 SSOT 可以修改或转变该数据。为了实现这一点,SSOT 会以不可变类型公开数据;而且为了修改数据,SSOT 会公开函数或接收其他类型可以调用的事件。
此模式具有多种优势:
- 将对特定类型数据的所有更改集中到一处。
- 保护数据,防止其他类型篡改此数据。
- 更易于跟踪对数据的更改。因此,更容易发现 bug。
在离线优先应用中,应用数据的单一数据源通常是数据库。在其他某些情况下,单一数据源可以是 ViewModel 甚至是界面。
单一数据源原则常常与单向数据流 (UDF) 模式一起使用。在 UDF 中,状态仅朝一个方向流动。修改数据的事件朝相反方向流动。
在 Android 中,状态或数据通常从分区层次结构中较高的分区类型流向较低的分区类型。事件通常在分区层次结构中较低的分区类型触发,直到其到达 SSOT 的相应数据类型。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向 SSOT,在 SSOT 中应用数据被修改并以不可变类型公开。
此模式可以更好地保证数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。
然后我们再看一下Google官方推荐的架构
基于上一部分提到的常见架构原则,每个应用应至少有两个层:
- 界面层 - 在屏幕上显示应用数据。
- 数据层 - 包含应用的业务逻辑并公开应用数据。
您可以额外添加一个名为“网域层”的架构层,以简化和重复使用界面层与数据层之间的交互。
界面层
界面层(或呈现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。
界面层由以下两部分组成:
- 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
- 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。
数据层
应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。
数据层由多个代码库组成,其中每个代码库可包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个
MoviesRepository
类,或者为与付款相关的数据创建一个PaymentsRepository
类。
代码库类负责以下任务:
- 向应用的其余部分公开数据。
- 集中处理数据变化。
- 解决多个数据源之间的冲突。
- 对应用其余部分的数据源进行抽象化处理。
- 包含业务逻辑。
每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。数据源类是应用与数据操作系统之间的桥梁。
根据官方推荐的架构,转化成我们实际开发架构,可以用下图来表示:
数据是从界面发出的事件(意图),即 MVI 中 I(Intent),ViewModel根据业务对这个Intent进行处理(数据请求、业务逻辑),将处理的数据封装成State,即UI状态,发送给View进行UI更新。
而这个State通常是UI所有元素显示状态的集合,通过其对UI进行统一的管理。
登录示例代码
class MVIDemoActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
private lateinit var binding: ActivityMviBinding
private var loadingDialog: LoadingDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMviBinding.inflate(layoutInflater)
binding.vm = viewModel
setContentView(binding.root)
initObserver()
}
@SuppressLint("SetTextI18n")
private fun initObserver() {
binding.editUsername.afterTextChanged {
viewModel.userName = it
}
binding.editPassword.afterTextChanged {
viewModel.password = it
}
lifecycleScope.launch {
this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.loginStateFlow.collect { loginState ->
loginState.userInfo?.let { userInfo ->
binding.tvUsername.text = getString(R.string.username) + userInfo.userName
binding.tvPassword.text = getString(R.string.password) + userInfo.password
}
if (loginState.isLogin) {
binding.editUsername.visibility = View.GONE
binding.editPassword.visibility = View.GONE
binding.btnLogin.visibility = View.GONE
} else {
binding.editUsername.visibility = View.VISIBLE
binding.editPassword.visibility = View.VISIBLE
binding.btnLogin.visibility = View.VISIBLE
}
if (loginState.isLoading) {
loadingDialog = LoadingDialog(this@MVIDemoActivity)
loadingDialog?.show()
} else {
loadingDialog?.cancel()
loadingDialog = null
}
}
}
}
lifecycleScope.launch {
this@MVIDemoActivity.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.loginEventFlow.collect { event ->
when (event) {
is LoginViewModel.LoginEvent.ToastEvent -> {
showToast(event.message, event.isShort)
}
}
}
}
}
}
}
class LoginViewModel : ViewModel() {
private val repository = LoginRepository()
var userName: String = ""
var password: String = ""
private val _loginStateFlow = MutableStateFlow(LoginState())
private val _loginEventFlow = MutableSharedFlow()
val loginStateFlow: StateFlow = _loginStateFlow
val loginEventFlow: SharedFlow = _loginEventFlow
fun login() {
viewModelScope.launch(Dispatchers.IO) {
_loginStateFlow.update { loginState ->
loginState.copy(isLoading = true)
}
when (val loginResult = repository.login(userName, password)) {
is LoginResult.Success -> {
_loginStateFlow.update { loginState ->
loginState.copy(
userInfo = loginResult.userInfo,
isLoading = false,
isLogin = true
)
}
_loginEventFlow.emit(LoginEvent.ToastEvent("登录成功"))
}
is LoginResult.Fail -> {
_loginStateFlow.update { loginState ->
loginState.copy(isLoading = false, isLogin = false)
}
_loginEventFlow.emit(LoginEvent.ToastEvent(loginResult.message))
}
}
}
}
data class LoginState(
val userInfo: UserInfo? = null,
val isLoading: Boolean = false,
val isLogin: Boolean = false
)
sealed class LoginEvent {
class ToastEvent(val message: String, val isShort: Boolean = true) : LoginEvent()
}
}
从上面的代码可以看出,State对UI的状态进行了约束,所有出口都由其提供,并且其只和UI显示的元素相关,和业务逻辑解耦。
AndroidMVC、MVP、MVVM、MVI架构示例https://download.csdn.net/download/gxlgxjhll/86937904