MVVM
前言
时间: 23/10/12
AndroidStudio
版本: Giraffe 2022.3.1 JDK:17 开发语言: Kotlin
Gradle版本: 8.0 Gradle plugin Version: 8.1.1
概述
前面已经将大部分 jetpack 组件介绍完成,虽然还有一些组件缺少了部分细节,但是已经不影响我们日常的开发了。这一节我们就讲一下由 Google 推荐的 Android 应用开发框架 ------ MVVM 。
虽然目前 Google 又推出了一个 MVI 开发框架,但是这并不影响 MVVM 的地位,目前使用最多的仍然是 MVVM 。当然了,没有最好的框架,只有最适合的框架。
什么是 MVVM
MVVM 是 Model - View - ViewModel 的简称。
MVVM架构的本质是数据驱动,它的最大的特点是单向依赖。MVVM架构通过观察者模式让ViewModel与View解耦,实现了View依赖ViewModel,ViewModel依赖Model的单向依赖。
-
模型层(Model) ,负责与数据库和网络层通信,获取并存储数据。与MVP的区别在于Model层不再通过回调通知业务逻辑层数据改变,而是通过观察者模式实现。
-
视图(View) 负责将Model层的数据做可视化的处理,同时与ViewModel层交互。
-
视图模型(ViewModel) 主要负责业务逻辑的处理,同时与 Model 层 和 View层交互。与MVP的Presenter相比,ViewModel不再依赖View,使得解耦更加彻底。
MVVM 相对于 MVC、MVP 框架有很多有点,其解耦更彻底,数据单向流动。用户操作时是 View -> ViewModel -> Model,数据显示是则相反。
MVVM 是 Google 推荐使用的框架,所以 Google 也提供了相应的一系列组件来实现这个框架。上面的图片是 Google 给出的通过 jetpack 组件来实现 MVVM 框架的结构图。
-
Activity/Fragment 即 "View" 层,完成视图 UI 的操作,它拥有 ViewModel,用以实现数据交流。
-
ViewModel 中存在 LiveData 组件,组成 MVVM 中的 "ViewModel" 层,它主要负责各种数据交互,它拥有 Repository。
-
Repository 是 Google 定义的与数据交互的一个概念,它分有两种方式去获取数据,即为本地数据,网络数据。也就是说 Repository 以下的部分可以统称为 "Model" 层。
它们的拥有形式都是单向的,例如 Activity/Fragment 拥有 ViewModel,而 ViewModel 不能操作 UI 甚至不能存在对 UI 操作的可能,每个模块负责相关的业务,这样就能实现最大化的解耦。
原则上来讲,框架是一种理念,它并存在硬性要求。换句话说,只要你写的代码它符合 MVVM 框架的理念,它就可以归于 MVVM 框架。只不过用 jetpack 来实现 MVVM 相对会更加便利。
实现 MVVM 框架
在这节,我会先简单使用 jetpack 组件来构建一个 mvvm 框架的简易 demo。当然我使用的数据来源是网络,即我自己搭建了一个服务器,所以关于数据获取的方式请根据条件自行决定。从本地数据库即 SQLite 获取是一样的。
涉及到的 jetpack 组件有 LiveData、DataBinding、ViewModel 以及 Kotlin 协程。网络请求框架使用的是 Retrofit
实现 User Login 功能
一般来说,我们写功能是从数据开始往视图推进,即先完成数据的获取,再完成视图的构建和相关操作。
-
添加 Retrofit 相关依赖
groovyimplementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0")
-
构建 User 实体类
kotlinclass User : BaseObservable() { var id = 0 @get: Bindable var username: String? = null set(value) { field = value notifyPropertyChanged(BR.username) } @get: Bindable var password: String? = null set(value) { field = value notifyPropertyChanged(BR.password) } var userStatus = 0 override fun toString(): String { return "User(id=$id, username=$username, password=$password, userStatus=$userStatus)" } }
其中 username 和 password 实现了数据变化监听,这样能简化登录时按钮监听中的获取数据的步骤。
-
新建两个类,一个是用于接收 json 数据的 class ApiResult,一个是网络请求接口类 interface NetWorkApi。
kotlin//ApiResult<T> class ApiResult<T>( var status: Int = -1,//网络请求状态,一般成功是 200,还有大家熟悉的404等 var msg: String? = null,//网络请求状态说明 var data: T? = null//请求返回的数据 ) //NetWorkApi interface NetWorkApi { @POST("user/login") suspend fun login(@Body body: RequestBody): ApiResult<User?> }
其中 ApiResult 需要根据实际返回的 json 文件来构建,一般我们写后端构建服务器时,都是返回这样一个模板,传回的数据自定义泛型就行。
-
新建一个 Retrofit 单例工具类 RetrofitService,用以获取接口类的 Retrofit 实现。
kotlinobject RetrofitService { private val retrofit = Retrofit.Builder() .baseUrl(NetWorkConst.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() fun getApi(): NetWorkApi { return retrofit.create() } }
NetWorkConst.BASE_URL 是全局静态常量,这里就不具体展示了哈。实在需要的话就私信我吧。
通常情况下,我们在编辑代码时,是不能出现 硬编码( hard code ) 的,因为 hardcode 不方便管理与维护。
hardcode指的是代码中直接使用 字符串,数值等。那么我们就可以新建一个类,来存放这些数值。当然还有一种情况,就是调用 string.xml 中的字符,这个情况我后面也有。
一般来说,我们在编码时需要使用的,包括参数,标识等都存放在 Constants 类中,而如果我们需要显示,例如弹出的 Toast,Dialog 等有字符串,这些字符串是需要保存在 string.xml 中的。简单来说就是,用户看得见的在 string.xml 中,用户看不见的在 Constants 类中。
-
根据 Google 组件框架图,新建一个 UserRepository 类,里头存放的是网络请求的具体实现。
kotlinclass UserRepository { private val api = RetrofitService.getApi() suspend fun login(user: MutableLiveData<User?>, requestBody: RequestBody) { execute({ api.login(requestBody) }, user) } suspend fun <T> execute( block: suspend () -> ApiResult<T>,//挂起函数作为参数 response: MutableLiveData<T>, ) { try { val result = block.invoke() if (result.status == SUCCESS_STATUS) { response.postValue(result.data) } else { result.msg?.let { Toast.error(appContext, it) } } } catch (e: IOException) { e.printStackTrace() Toast.error(appContext, appContext.getString(R.string.connect_server_failed)) } } }
注意到 login 方法中传入了两个参数,第一个是用来接收数据的,第二个是封装好的RequestBody用以网络请求的。
使用 MutableLiveData 来接收的好处是,我们可以直接在 View 层,即Activity/Fragment 中通过 LiveData的observer 来获取网络请求的结果。具体看下面的 ViewModel。
Toast 是我自己根据开源项目 Toasty 重构的一个库,appContext 是applicationContext,我在 Application 类中获取的全局可用的 Context,这样可以防止内存泄漏。
Toast 依赖
groovyimplementation("com.github.Beacon0423.myLib:toast:v1.1.2") //需要在 settings.gradle.kts 中添加 dependencyResolutionManagement { repositories { google() mavenCentral() maven(url = "https://jitpack.io")//添加这行就行 } }
appContext实现
kotlinclass App : Application() { companion object { lateinit var appContext: Context } override fun onCreate() { super.onCreate() appContext = applicationContext } }
这里编写的 execute 方法是一个通用的网络请求函数,关于 User 的网络请求都应该可以使用这个方法,理论是我们可以编写一个 BaseRepository 类来实现这个方法,我们可以继承这个类直接调用这个方法,会更加简洁。
-
新建一个 UserModelView 类,在 ViewModel 中使用协程来调用 suspend fun。
kotlinclass UserViewModel : ViewModel() { private var _user = MutableLiveData<User?>() val userLiveData: LiveData<User?> = _user private val repository = UserRepository() fun login(user: User?) { if (user == null || user.username.isNullOrEmpty() || user.password.isNullOrEmpty()) { Toast.warning(appContext, appContext.getString(R.string.username_or_password_empty)) return } val requestBody = RequestBody.create(MediaType.parse("application/json;"), Gson().toJson(user)) viewModelScope.launch { repository.login(_user, requestBody) } } }
login 方法只需要传入用户输入的 user,而其中只有 username 和 password 是必要的,其余是否为空都无关。
-
新建 BaseActivity,LoginActivity
kotlin//BaseActivity abstract class BaseActivity<VDB : ViewDataBinding>: AppCompatActivity() { protected lateinit var binding: VDB override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = setDataBinding() setContentView(binding.root) } protected abstract fun setDataBinding() : VDB } //LoginActivity class LoginActivity : BaseActivity<ActivityLoginBinding>() { private val TAG = "LoginActivity" private lateinit var userViewModel: UserViewModel private lateinit var mUser: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) userViewModel = ViewModelProvider(this)[UserViewModel::class.java] mUser = User() binding.user = mUser userViewModel.userLiveData.observe(this) { if (it == null) return@observe if (mUser.username == it.username) { startActivity(Intent(this@LoginActivity, MainActivity::class.java)) finish() } } binding.btnLogin.setOnClickListener { Log.d(TAG, mUser.toString()) userViewModel.login(mUser) } } override fun setDataBinding(): ActivityLoginBinding { return ActivityLoginBinding.inflate(layoutInflater) } }
BaseActivity 我就不作说明了,具体可以看之前的同系列 ViewBinding 组件文章。
在 LoginAcvtivity 中,我们使用了 Databinding 的数据双向绑定功能,如此就不需要在 btnLogin 的点击监听获取用户输入的 username 和 password 了,直接调用 viewmodel 的 login 方法即可。而对于获取结果后的操作,则放在了 observer 中,因为在 Repository 中post了 新的值,这里检测到后就能执行相应逻辑( 这里就实现了界面跳转 )
-
新建 activity_login.xml
xml<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.may.part_10.entity.User" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.google.android.material.textfield.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="160dp" android:layout_marginEnd="40dp"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/text_your_username" android:text="@={user.username}" /> </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout android:id="@+id/textInputLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="20dp" android:layout_marginEnd="40dp"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:digits="@string/password_digits" android:hint="@string/text_your_password" android:inputType="textPassword" android:maxLength="16" android:text="@={user.password}" /> </com.google.android.material.textfield.TextInputLayout> <Button android:id="@+id/btn_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginTop="80dp" android:layout_marginEnd="40dp" android:text="@string/login" /> <TextView android:id="@+id/tv_register" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" android:layout_marginTop="6dp" android:layout_marginEnd="40dp" android:text="@string/noaccount_register" android:textColor="#294FC5" /> </LinearLayout> </layout>
最后别忘了在 AndroidManifest.xml 中注册 LoginActivity 为首启动activity,申明Application的name为App。以及添加网络请求权限。
xml<uses-permission android:name="android.permission.INTERNET" /> <application android:name=".App" ....> <activity android:name=".activities.LoginActivity" android:launchMode="singleInstancePerTask" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".activities.MainActivity" />
到这里,登录功能就基本实现了,看看效果
总结
本节通过结合前面 jetpack 所学到的一些知识,编写了一个使用 jetpack 组件构建的 MVVM 框架的 demo。
后续我还会根据这个 demo 进行深入,包括 用户注册、用户密码加密、其它 Model( Book ) 的一些功能等。总的来说,关于 MVVM 框架 demo 的编写不止一篇文章,后续还会出一篇文章关于我的服务器的实现,后端代码使用的是 springboot 框架。以及如何零基础,低费用甚至零费用就能实现的简易后端。
至于网络框架 Retrofit 的使用,这边没有过多描述。可以自行查阅相关文章,例如我之前写的( 没错,我又来了 )
有道智云翻译API + retrofit实现在线翻译Android app
看到这里了,不妨点个赞,加个收藏吧。也欢迎评论指出不足
参考文章: