前言
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>
源码:
github.com/ThirdPrince…
总结
Jetpack MVVM + Kotlin + Coroutine 是一种高效、可维护、功能优异的 Android 开发技能。运用这些技能能够使咱们更轻松地编写 Android 运用,一起也能够进步开发功率和代码质量。在当时 Android 开发中,Jetpack MVVM + Kotlin + Coroutine 已经成为主流的开发技能。