Jetpack MVVM实践:一个小demo带你深入了解架构设计

2,673 阅读6分钟

2.png

前言

Jetpack MVVM 是 Google 推出的一套 Android 开发框架,它来源于2017年Google推出的AAC框架,它提供了一系列组件和工具,可以帮助开发者更快速、更高效地开发 Android 应用。使用 Jetpack MVVM 架构可以使我们更好地组织代码、提高开发效率、提升应用性能,同时也可以让我们更加专注于业务逻辑的实现,而不是过多关注底层技术的实现细节。本文将以 Jetpack MVVM 实践为主题,通过一个小demo的开发过程,带领读者深入了解 Jetpack MVVM 架构的设计思想和实现方式,帮助读者更好地应用这些技术来开发高质量的 Android 应用。

MVVM 有哪些优势

MVVM 相对于传统的MVP或者MVC有以下优势:

  1. 数据绑定:MVVM 通过数据绑定将视图和 ViewModel 绑定在一起,当 ViewModel 中的数据发生变化时,视图会自动更新,减少了手动更新视图的代码量和出错的可能性。
  2. 低耦合:MVVM 中的 ViewModel 是视图和模型之间的桥梁,它们之间通过接口进行通信,使得视图和模型之间的耦合度更低,更易于维护和扩展。
  3. 可测试性:MVVM 中的 ViewModel 是一个纯逻辑的类,不依赖于视图和模型的具体实现,可以通过单元测试来验证其正确性。
  4. 代码重用:MVVM 中的 ViewModel 可以被多个视图所共享,使得相同的业务逻辑不需要重复编写,提高了代码的重用性和开发效率。

Jetpack架构组件

Jetpack提供了多个强大的组件,其中Lifecycle、LiveData和ViewModel是构建MVVM架构的关键组件。在本文中,我们将使用这些组件与Kotlin协程和Room协同工作,以实现更高效的MVVM架构。

  1. Lifecycle

Lifecycle是一个用于管理Android组件(如Activity和Fragment)生命周期的库。它提供了一种方便的方式来确保在组件的生命周期发生变化时,相关代码可以自动启动或停止。Lifecycle库通过将组件的生命周期状态与组件的相关操作(如启动和停止服务)进行关联,从而避免了内存泄漏和其他相关问题。

  1. LiveData

LiveData是一个具有生命周期感知能力的可观察数据存储类。它提供了一种方便的方式来实现数据驱动的UI,以及确保UI组件和数据存储之间的一致性。LiveData可以感知组件的生命周期状态,并在组件处于激活状态时通知观察者,从而避免了不必要的数据更新和内存泄漏。

  1. ViewModel

ViewModel是一个用于管理UI相关数据的类。它提供了一种方便的方式来避免数据丢失和内存泄漏,并确保在组件的生命周期发生变化时,数据可以自动保存和恢复。ViewModel通常与LiveData结合使用,以确保UI组件和数据存储之间的一致性。

4.Room

Room是Android官方提供的一个SQLite数据库ORM(对象关系映射)库,旨在简化Android应用程序中的数据库访问。Room提供了一种方便的方式来定义数据库表和访问这些表的方法,从而避免了手写SQL语句的繁琐和错误。

5.Kotlin协程

Kotlin 协程是Kotlin语言中的一种轻量级线程库,旨在简化异步编程和并发编程。Kotlin协程是一种非常实用和强大的异步编程和并发编程库,可以帮助开发者简化异步任务的处理和协调,并提高应用程序的性能和稳定性。在MVVM架构中,Kotlin协程通常与ViewModel和LiveData结合使用,以实现更高效、更健壮的数据存储和UI更新。

实现MVVM架构

本文的小Demo是获取GitHub 某个项目的start,展示UserList,如下图所示:

1.gif

定义数据模型

User.kt

@Entity(tableName = "user")
data class User(
    @PrimaryKey(autoGenerate = true) val primaryKey: Long,
    val login: String,
    @ColumnInfo(name = "user_id") val id: Long,
    val avatar_url: String
)

UI展示只用到了 login 和avatar_url,在这里我添加了主键primaryKey ,并且自增。

定义ViewModel

class UserViewModel : ViewModel() {

    private val TAG = "UserViewModel"


    val userObservableArrayList = ObservableArrayList<User>()

    private val _liveDataUser = MutableLiveData<List<User>>()

    private val liveDataUser: LiveData<List<User>>
        get() = _liveDataUser

    private val _liveDataLoading = MutableLiveData<Boolean>()

    val liveDataLoading: LiveData<Boolean>
        get() = _liveDataLoading

    private val _error = MutableLiveData<String>()

    val error: LiveData<String>
        get() = _error

    /**
     * 点击事件
     */
    val selectedItem = MutableLiveData<User>()


    val selectedText = MutableLiveData<User>()


    private val adapter = UserAdapter(this)

    init {
        _liveDataLoading.value = true

        userObservableArrayList.addOnListChangedCallback(DynamicChangeCallBack<User>(adapter))
    }

    /**
     * 异常信息
     */
    private val exception = CoroutineExceptionHandler { _, throwable ->
        _error.value = throwable.message
        _liveDataLoading.value = false
        Log.e(TAG, throwable.message!!)
    }

    /**
     * 获取User
     */
    fun getUsers(isLoadDb: Boolean = true): LiveData<List<User>> {

        viewModelScope.launch(exception) {
            if (isLoadDb) {
                var users = getUsersFromDb()
                userObservableArrayList.addAll(users)
            }
            val response = RetrofitManager.gitHubService.getUsers()
            _liveDataUser.value = response
            userObservableArrayList.clear()
            userObservableArrayList.addAll(response)
            Log.e(TAG, "刷新页面")
            refreshList(response)
            insertDb(response)
            Log.e(TAG, "操作数据完成")
        }

        return liveDataUser
    }

    fun getUsersByDb(): LiveData<List<User>> {

        viewModelScope.launch(exception) {

            var users = getUsersFromDb()
            userObservableArrayList.addAll(users)
            refreshList(users)
        }

        return liveDataUser
    }

    /**
     * 异步查询数据库
     */
    private suspend fun getUsersFromDb(): List<User> {
        var users: MutableList<User>
        withContext(Dispatchers.IO) {
            val userDao = DbManager.db.userDao()
            users = userDao.getAll() as MutableList<User>
        }
        return users
    }

    /**
     * 更新数据库
     */
    private suspend fun insertDb(users: List<User>) {

        if (users.isNotEmpty()) {
            withContext(Dispatchers.IO) {
                val userDao = DbManager.db.userDao()
                userDao.deleteAll()
                userDao.insertAll(users)
                Log.e(TAG, "操作数据库")
            }
        }


    }


    /**
     * just Test
     */
    fun getUsersMore(): LiveData<List<User>> {

        viewModelScope.launch(exception) {
            val response = RetrofitManager.gitHubService.getUsers()
            _liveDataUser.value = response
            userObservableArrayList.addAll(response)
            refreshList(response)
        }

        return liveDataUser
    }

    private fun refreshList(users: List<User>) {
        _liveDataLoading.value = false

    }

    fun onItemClick(index: Int) {
        val user: User = getUserByIndex(index)
        selectedItem.value = user
    }


    fun onTextClick(index: Int) {
        val user: User = getUserByIndex(index)
        selectedText.value = user
    }

    /**
     * 点击Fb 更新主题颜色
     */
    fun onFbClick() {

        if (AppMode.currentMode == Mode.UIModeNight) {
            AppMode.update(Mode.UIModeDay)
        } else {
            AppMode.update(Mode.UIModeNight)
        }

    }

    fun getUserByIndex(index: Int): User {
        return userObservableArrayList[index]
    }

    fun getUserAdapter(): UserAdapter {
        return adapter
    }


}

上述代码UserViewModel,包含了获取用户数据、更新数据库、处理点击事件等业务逻辑。其中,使用了协程来异步获取数据(Coroutine+Retrofit)和操作数据库,使用了LiveData来更新UI界面。同时,也包含了处理异常情况和更新主题颜色的方法。

View一(Activity)

/**
 * UserUi
 * @author dhl
 */
class UserActivity : AppCompatActivity() , SwipeRefreshLayout.OnRefreshListener{

    private  val TAG = "UserActivity"
    private val userViewModel:UserViewModel by lazy {
        ViewModelProvider(this).get(UserViewModel::class.java)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        initData()
    }

    /**
     * 数据初始化
     */
    private  fun initData(){
        val binding = DataBindingUtil.setContentView<ActivityUserBinding>(this,R.layout.activity_user)
        binding.lifecycleOwner = this
        binding.userViewModel = userViewModel
        val decoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
        binding.rcy.addItemDecoration(decoration)
        binding.refresh.setOnRefreshListener(this)
        userViewModel.getUsers()
        binding.refresh.isRefreshing = true
        onClickEvent()
        observerError()

    }


    /**
     * 网络异常
     */
    private fun observerError(){
        userViewModel.error.observe(this, Observer {
            Toast.makeText(this,it,Toast.LENGTH_LONG).show()
        })
    }

    override fun onRefresh() {
        Log.e(TAG,"onRefresh")
        userViewModel.getUsers(false)
    }

    /**
     * 点击事件
     */
    private fun onClickEvent(){
        userViewModel.selectedItem.observe(this, Observer {
            Toast.makeText(this,it.login+"被点击了",Toast.LENGTH_LONG).show()
            userViewModel.getUsersMore()
        })

        userViewModel.selectedText.observe(this, Observer {
            Toast.makeText(this,it.login+"被点击了 Text",Toast.LENGTH_LONG).show()
        })

    }
}

UserActivity 实现了一个用户列表的界面,包括以下功能:

  1. 使用DataBinding绑定布局和ViewModel
  2. 使用ViewModel获取用户列表数据,并在界面上展示
  3. 使用RecyclerView展示用户列表,并添加分割线
  4. 添加下拉刷新功能,当下拉时重新获取用户列表数据
  5. 添加点击事件,当用户点击列表项时,展示该用户的详细信息,并获取更多的用户数据
  6. 当网络异常时,提示用户网络异常信息。

View二(Adapter)

/**
 * Adapter
 * @author dhl
 * 数据交给ViewModel 处理
 */
class UserAdapter(private val userViewModel: UserViewModel) :
    RecyclerView.Adapter<UserAdapter.ViewHolder>() {


    class ViewHolder(private val binding: UserItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(viewModel: UserViewModel, position: Int?) {
            binding.position = position
            binding.viewModel = viewModel

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding =
            DataBindingUtil.inflate<UserItemBinding>(
                layoutInflater,
                R.layout.user_item,
                parent,
                false
            )
        binding.lifecycleOwner = parent.context as LifecycleOwner

        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(userViewModel!!, position)
    }

    override fun getItemCount(): Int {
        return userViewModel.userObservableArrayList.size
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }


}

UserAdapter实现了一个RecyclerView的适配器,用于展示用户列表。具体实现包括以下功能:

  1. 在ViewHolder中使用DataBinding绑定布局和ViewModel
  2. 在onCreateViewHolder中使用DataBinding.inflate()方法创建ViewBinding对象
  3. 在onBindViewHolder中绑定ViewModel数据和列表项位置
  4. 实现getItemCount和getItemId方法,返回列表项数量和ID。

Databinding

ActivityUserBinding:

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="userViewModel"
            type="com.dhl.example.user.vm.UserViewModel" />

        <variable
            name="appmode"
            type="com.dhl.uimode.AppMode" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.dhl.example.user.ui.UserActivity">

            <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
                android:id="@+id/refresh"
                setRefresh="@{userViewModel.liveDataLoading.booleanValue()}"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:swipeRefreshLayoutProgressSpinnerBackgroundColor="@color/colorPrimary">

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/rcy"
                    setAdapter="@{userViewModel.userAdapter}"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:background="?attr/selectableItemBackground" />
            </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
        </androidx.appcompat.widget.LinearLayoutCompat>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_marginEnd="@dimen/fab_margin"
            android:layout_marginBottom="16dp"
            android:onClick="@{()->userViewModel.onFbClick()}"
            app:fbbackground="@{appmode.INSTANCE.content}"
            app:srcCompat="@android:drawable/ic_dialog_email" />
    </FrameLayout>

</layout>

UserItemBinding:

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="position"
            type="java.lang.Integer" />

        <variable
            name="viewModel"
            type="com.dhl.example.user.vm.UserViewModel" />

        <import type="com.dhl.uimode.AppMode" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:background="@{AppMode.INSTANCE.background}">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/my_ripple"
            android:clickable="true"
            android:focusable="true"
            android:onClick="@{()-> viewModel.onItemClick(position)}"
            android:orientation="horizontal"
            android:paddingStart="32dp"
            android:paddingTop="16dp"
            android:paddingEnd="32dp"
            android:paddingBottom="16dp">

            <ImageView
                imageUrl="@{viewModel.getUserByIndex(position).avatar_url}"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:background="@drawable/list_item_image_border"
                android:scaleType="centerCrop"
                app:imageNight="@{AppMode.INSTANCE.nightMode}" />

            <TextView
                android:id="@+id/txtName"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="@drawable/my_ripple"
                android:gravity="center_vertical"
                android:onClick="@{()-> viewModel.onTextClick(position)}"
                android:orientation="vertical"
                android:paddingStart="32dp"
                android:paddingEnd="32dp"
                android:text="@{viewModel.getUserByIndex(position).login}"
                android:textColor="@{AppMode.INSTANCE.content}"
                android:textSize="16dp"
                tools:text="Test" />
        </LinearLayout>
    </FrameLayout>
</layout>

源码:

github.com/ThirdPrince…

总结

Jetpack MVVM + Kotlin + Coroutine 是一种高效、可维护、性能优秀的 Android 开发技术。使用这些技术可以使我们更轻松地编写 Android 应用,同时也可以提高开发效率和代码质量。在当前 Android 开发中,Jetpack MVVM + Kotlin + Coroutine 已经成为主流的开发技术。