《Android 编程权威指南》学习笔记 : 第19章 数据绑定与MVVM
第19章 数据绑定与MVVM
MVVM 架构
开始新项目之前,针对术语做如下说明:MVVM中的视图模型(view model)跟你在第4章和第9章使用的Jetpack库中的ViewModel类是两个不同的概念。
为避免混淆,二者在命名上做如下区分:
- 一个叫视图模型,
- 另一个叫ViewModel。
你应该还记得,Jetpack ViewModel是一个特殊的功能类,可以用来管理和保留fragment和activity(在它们的生命周期状态发生变化时)里的数据。而MVVM里的视图模型是架构方面的一种概念。
视图模型当然可以使用Jetpack ViewModel类来实现,但学完本章你就会知道,不使用ViewModel类也可以。
创建 BeatBox 应用
替换 MainActivity 默认布局:
app/build.grale
dependencies {
...
implementation 'androidx.recyclerview:recyclerview:1.2.1'
}
Sync Now
代码清单:res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
实现简单的数据绑定
启用数据绑定
首先,在应用的build.gradle文件里,通过应用kotlin-kapt插件,启用数据绑定
代码清单:app/build.gradle
plugins {
...
id 'kotlin-kapt'
}
//apply plugin: 'kotlin-kapt'
android {
...
dataBinding {
enabled = true
}
}
-
应用插件有两种方式:
- 第一种:
apply plugin: 'kotlin-kapt'
- 第二种:
plugins { ... id 'kotlin-kapt' }
-
应用kotlin-kapt插件后,数据绑定就可以执行Kotlin注解处理了、
-
dataBinding.enabled = true : 启用 DataBinding
-
记得:Sync Now
改造布局为:数据绑定布局
代码清单:/res/layout/activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</layout>
把整个布局放到 <layout>
标签中,该标签告诉DataBinding工具,这个布局应该由你来处理。
实例化绑定布局
完成以上两步后,DataBinding工具自动生成绑定类: com.example.beatbox.databinding.ActivityMainBinding
Tips:
如果找不到 ActivityMainBinding 类,尝试 rebuild项目(菜单【Build-> Make Module 'BeatBox.app'】)
然后使用帮助类:androidx.databinding.DataBindingUtil 实例化该绑定类。
代码清单:MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.recycleView.apply {
layoutManager = GridLayoutManager(context, 3)
}
}
}
- 不再使用
setContentView(R.layout.activity_main)
来实例化视图层级结构 - 使用
DataBindingUtil.setContentView
来实例化绑定类 - ActivityMainBinding类有两个引用:root和recyclerView,其中前者指整个布局,后者指RecyclerView,
布局只有一个视图,所以两个引用都指向了同一个视图:RecyclerView
导入 assets
把声音文件添加到项目里,以便应用调用。不过,这里不打算用资源系统,我们改用assets打包声音文件。可以把assets想象为经过精简的资源:它们也像资源那样打入APK包,
首先创建assets目录。右键单击app模块,选择New → Folder → Assets Folder菜单项,这会弹出如图所示的画面。
不勾选Change Folder Location选项,保持Target Source Set的main选项不变,然后点击Finish按钮完成
接着,右键单击assets目录,选择New → Directory菜单项,为声音资源创建sample_sounds子目录
assets目录中的所有文件都会随应用打包。为了方便组织文件,我们创建了sample_sounds子目录。与资源不同,assets一般不需要子目录。我们这么做是为了组织声音文件。
使用 assets
代码清单:Sound.kt
private const val WAV = ".wav"
class Sound(val assetPath: String) {
val name = assetPath.split("/").last().removeSuffix(WAV)
}
SoundViewModel
代码清单:SoundViewModel.kt
class SoundViewModel {
val title: MutableLiveData<String?> = MutableLiveData()
var sound: Sound? = null
set(sound) {
field = sound
title.postValue(sound?.name) // 通知布局,数据更新了
}
}
BeatBox
代码清单:BeatBox.kt
private const val TAG = "BeatBox"
private const val SOUNDS_FOLDER = "sample_sounds"
class BeatBox(private val assets: AssetManager) {
val sounds: List<Sound> // 音频文件
init {
sounds = loadSounds()
}
private fun loadSounds(): List<Sound> {
val soundNames: Array<String>
try {
soundNames = assets.list(SOUNDS_FOLDER)!!
} catch (e:Exception) {
Log.e(TAG, "Could not list assets", e)
return emptyList()
}
val sounds = mutableListOf<Sound>()
soundNames.forEach { fileName ->
val assetPath = "$SOUNDS_FOLDER/$fileName"
val sound = Sound(assetPath)
sounds.add(sound)
}
return sounds
}
}
- sounds = loadSounds():初始化要显示的总数据
- assets: AssetManager -> soundNames = assets.list(SOUNDS_FOLDER)!!:通过类AssetManager 获取【assert/sample_sounds】目录中的声音文件
ListItem的绑定布局
代码清单:res/layout/list_item_sound.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.beatbox.SoundViewModel" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="120dp"
android:text="@{viewModel.title}"
tools:text="Sound Name"/>
</layout>
- 最外层使用绑定布局
<layout>
包裹,让 DataBinding工具自动生成布局绑定类:ListItemSoundBinding - 标签
<variable>
中定义变量及其类型 - android:text="@{viewModel.title}" :进行数据的单向绑定
MainActivity
-
绑定的数据 BeatBox.sounds 通过SoundAdapter构造函数传入 :adapter = SoundAdapter(beatBox.sounds)
-
SoundAdapter.onCreateViewHolder(...) 中创建item的DataBinding,并将 DataBinding由SoundHolder构造函数传入
private inner class SoundAdapter(...) { ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoundHolder { // 创建Iten的 DataBinding val binding = DataBindingUtil.inflate<ListItemSoundBinding>( layoutInflater, R.layout.list_item_sound, parent, false ) // 告诉DataBinding框架观察 title属性时使用的LifecycleOwner binding.lifecycleOwner = this@MainActivity // binding 传入 Holder return SoundHolder(binding) } }
-
SoundAdapter.onBindViewHolder(...) 中获取item的数据:sounds[position],并 holder.bind(sound)
private inner class SoundHolder(...) { ... override fun onBindViewHolder(holder: SoundHolder, position: Int) { val sound = sounds[position] holder.bind(sound) } }
-
SoundHolder的 init {...}中创建 ViewModel, 并定义方法:bind(Sound),供 SoundAdapter在onBindViewHolder(...)方法中调用,传入对应当前item的数据
private inner class SoundHolder(private val binding: ListItemSoundBinding) : RecyclerView.ViewHolder(binding.root) { init { binding.viewModel = SoundViewModel() } fun bind(sound: Sound) { binding.apply { viewModel?.sound = sound executePendingBindings() // 可选,鉴于刷新视图极快,让item布局立即刷新 } } }
MainActivity完整代码
代码清单:MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var beatBox: BeatBox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
beatBox = BeatBox(assets)
// setContentView(R.layout.activity_main)
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.recycleView.apply {
layoutManager = GridLayoutManager(context, 3)
adapter = SoundAdapter(beatBox.sounds)
}
}
private inner class SoundHolder(private val binding: ListItemSoundBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.viewModel = SoundViewModel()
}
fun bind(sound: Sound) {
binding.apply {
viewModel?.sound = sound
executePendingBindings() // 可选,鉴于刷新视图极快,让item布局立即刷新
}
}
}
private inner class SoundAdapter(private val sounds: List<Sound>) :
RecyclerView.Adapter<SoundHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoundHolder {
val binding = DataBindingUtil.inflate<ListItemSoundBinding>(
layoutInflater,
R.layout.list_item_sound,
parent,
false
)
binding.lifecycleOwner = this@MainActivity // 告诉DataBinding框架观察 title属性时使用的LifecycleOwner
return SoundHolder(binding)
}
override fun onBindViewHolder(holder: SoundHolder, position: Int) {
val sound = sounds[position]
holder.bind(sound)
}
override fun getItemCount(): Int {
return sounds.size
}
}
}