• 《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
            }
    
        }
    }
    
  • 相关阅读:
    C++位运算详解
    SQL语句获取时间的方法
    redis在windows下安装和ThinkPHP中使用
    数据同步存储过程代码
    C#重写OnKeyPress方法
    SQL Server 2008数据库生成数据库脚本(并带数据)
    C#中邮件的发送
    C#中DGV分页功能
    C#中保持文件夹A与B同步
    C# 获取文件大小,创建时间,文件信息,FileInfo类的属性表
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/16342049.html
Copyright © 2020-2023  润新知