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

前言

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,如下图所示:

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

界说数据模型

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 已经成为主流的开发技能。