《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
    }
}

  • 应用插件有两种方式:

    1. 第一种:
    apply plugin: 'kotlin-kapt'
    
    1. 第二种:
     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
        }

    }
}
posted @ 2022-06-04 16:19  easy5  阅读(164)  评论(0编辑  收藏  举报