前言
Jetpack MVVM 是 Google 推出的一套 Android 开发框架,它来源于2017年Google推出的AAC框架,它提供了一系列组件和工具,可以帮助开发者更快速、更高效地开发 Android 应用。使用 Jetpack MVVM 架构可以使我们更好地组织代码、提高开发效率、提升应用性能,同时也可以让我们更加专注于业务逻辑的实现,而不是过多关注底层技术的实现细节。本文将以 Jetpack MVVM 实践为主题,通过一个小demo的开发过程,带领读者深入了解 Jetpack MVVM 架构的设计思想和实现方式,帮助读者更好地应用这些技术来开发高质量的 Android 应用。
MVVM 有哪些优势
MVVM 相对于传统的MVP或者MVC有以下优势:
- 数据绑定:MVVM 通过数据绑定将视图和 ViewModel 绑定在一起,当 ViewModel 中的数据发生变化时,视图会自动更新,减少了手动更新视图的代码量和出错的可能性。
- 低耦合:MVVM 中的 ViewModel 是视图和模型之间的桥梁,它们之间通过接口进行通信,使得视图和模型之间的耦合度更低,更易于维护和扩展。
- 可测试性:MVVM 中的 ViewModel 是一个纯逻辑的类,不依赖于视图和模型的具体实现,可以通过单元测试来验证其正确性。
- 代码重用:MVVM 中的 ViewModel 可以被多个视图所共享,使得相同的业务逻辑不需要重复编写,提高了代码的重用性和开发效率。
Jetpack架构组件
Jetpack提供了多个强大的组件,其中Lifecycle、LiveData和ViewModel是构建MVVM架构的关键组件。在本文中,我们将使用这些组件与Kotlin协程和Room协同工作,以实现更高效的MVVM架构。
- Lifecycle
Lifecycle是一个用于管理Android组件(如Activity和Fragment)生命周期的库。它提供了一种方便的方式来确保在组件的生命周期发生变化时,相关代码可以自动启动或停止。Lifecycle库通过将组件的生命周期状态与组件的相关操作(如启动和停止服务)进行关联,从而避免了内存泄漏和其他相关问题。
- LiveData
LiveData是一个具有生命周期感知能力的可观察数据存储类。它提供了一种方便的方式来实现数据驱动的UI,以及确保UI组件和数据存储之间的一致性。LiveData可以感知组件的生命周期状态,并在组件处于激活状态时通知观察者,从而避免了不必要的数据更新和内存泄漏。
- 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,如下图所示:
定义数据模型
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 实现了一个用户列表的界面,包括以下功能:
- 使用DataBinding绑定布局和ViewModel
- 使用ViewModel获取用户列表数据,并在界面上展示
- 使用RecyclerView展示用户列表,并添加分割线
- 添加下拉刷新功能,当下拉时重新获取用户列表数据
- 添加点击事件,当用户点击列表项时,展示该用户的详细信息,并获取更多的用户数据
- 当网络异常时,提示用户网络异常信息。
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的适配器,用于展示用户列表。具体实现包括以下功能:
- 在ViewHolder中使用DataBinding绑定布局和ViewModel
- 在onCreateViewHolder中使用DataBinding.inflate()方法创建ViewBinding对象
- 在onBindViewHolder中绑定ViewModel数据和列表项位置
- 实现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>
源码:
总结
Jetpack MVVM + Kotlin + Coroutine 是一种高效、可维护、性能优秀的 Android 开发技术。使用这些技术可以使我们更轻松地编写 Android 应用,同时也可以提高开发效率和代码质量。在当前 Android 开发中,Jetpack MVVM + Kotlin + Coroutine 已经成为主流的开发技术。