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 相关依靠
implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0")
-
构建 User 实体类
class 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。
//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 完结。
object 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 类,里头存放的是网络恳求的详细完结。
class 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 依靠
implementation("com.github.Beacon0423.myLib:toast:v1.1.2") //需要在 settings.gradle.kts 中增加 dependencyResolutionManagement { repositories { google() mavenCentral() maven(url = "https://jitpack.io")//增加这行就行 } }
appContext完结
class App : Application() { companion object { lateinit var appContext: Context } override fun onCreate() { super.onCreate() appContext = applicationContext } }
这儿编写的 execute 办法是一个通用的网络恳求函数,关于 User 的网络恳求都应该能够运用这个办法,理论是咱们能够编写一个 BaseRepository 类来完结这个办法,咱们能够承继这个类直接调用这个办法,会愈加简洁。
-
新建一个 UserModelView 类,在 ViewModel 中运用协程来调用 suspend fun。
class 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
//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 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。以及增加网络恳求权限。
<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
看到这儿了,无妨点个赞,加个保藏吧。也欢迎谈论指出缺乏
Demo地址(GitHub)
参考文章:
Android 架构思维与 MVVM 结构封装
Android开发攻略-运用架构攻略