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,数据显现是则相反。

Jetpack系列(九) -- MVVM 框架demo实现(1)

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 功用

一般来说,咱们写功用是从数据开端往视图推进,即先完结数据的获取,再完结视图的构建和相关操作。

  1. 增加 Retrofit 相关依靠

        implementation("com.squareup.retrofit2:retrofit:2.9.0")
        implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    
  2. 构建 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 完结了数据改变监听,这样能简化登录时按钮监听中的获取数据的步骤。

  3. 新建两个类,一个是用于接纳 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 文件来构建,一般咱们写后端构建服务器时,都是返回这样一个模板,传回的数据自界说泛型就行。

  4. 新建一个 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 类中。

  5. 依据 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 类来完结这个办法,咱们能够承继这个类直接调用这个办法,会愈加简洁。

  6. 新建一个 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 是必要的,其他是否为空都无关。

  7. 新建 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了 新的值,这儿检测到后就能执行相应逻辑( 这儿就完结了界面跳转 )

  8. 新建 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开发攻略-运用架构攻略