Android—Jetpack教程(二)

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

前言

在上一篇中,对Jetpack有了初步的认知,也详细讲解了LifeCycle的实际使用。在本篇中,将会对Jetpack对应的ViewModel进行详解!

1、ViewModel

ViewModel诞生之前出现的问题:

  • 瞬态数据丢失
  • 异步调用的内存泄露
  • 类膨胀提高维护难度和测试难度

ViewModel的作用

1.png

如图所示

  • 它是介于View(视图)和Model(数据模型)之间的桥梁
  • 使视图和数据能够分离,也能保持通信

ViewModel的生命周期特性

4.png

如图所示

我们看到:

  • ViewModel的生命周期长与Activity的生命周期,因此在使用ViewModel时,不要向ViewModel传入Activity的Context,因为这样会导致内存泄露!
  • 如果硬要使用Context,那就使用AndroidViewModel中的Application

概念说了,接下来实战:

1.1 实战一(未使用ViewModel)

布局文件就一个TextView,所以这里就不贴布局代码了!

class Step1Activity : AppCompatActivity() {
    private var textView: TextView? = null
    private var count = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById(R.id.textView)
    }
    fun plusNumber(view: View) {
        count++
        textView!!.text = "$count"
    }
}
复制代码

这里我们看到,当我们点击按钮时,全局变量count 自加一,随后给textView赋值!

来看看运行效果(屏幕旋转前)

2.png

保持这个值,旋转屏幕,看看效果:

3.png

当屏幕旋转时,我们发现用这种方式,自加的数据丢失了!

当然这个肯定有解决方案,这里也并不是解决这个方案出现的问题的。只是为了和ViewModel作个对比!

那现在看看使用ViewModel将会是怎样的?

1.2 实战二(使用ViewModel)

MyViewModel.kt


class MyViewModel(application: Application) : AndroidViewModel(application) {
    var number = 0 //
}
复制代码

这里代码很简单,就继承了AndroidViewModel,里面定义了number 变量。

来看看使用

class Step2Activity : AppCompatActivity() {

    private var textView: TextView? = null
    private var viewModel: MyViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById(R.id.textView)
        
        //实例化viewModel 的方式
        viewModel =
            ViewModelProvider(this, AndroidViewModelFactory(application))[MyViewModel::class.java]
        
        textView!!.text = String.valueOf(viewModel!!.number)
    }

    fun plusNumber(view: View) {
        textView!!.text = String.valueOf(++viewModel!!.number)
    }
}
复制代码

这里我们看到,实例化对应的viewModel 后,通过访问viewModel 里的属性实现的逻辑。

这时我们运行发现,不管怎么切换横竖屏,对应的屏幕上的数字都不会被清空,也就是说,并没有出现之前的数据丢失的情况! 在这就不放运行效果图了哈。

接下来下一个组件!

2、LiveData

LiveData和ViewModel的关系

5.png

如图所示

它们对应关系是:在ViewModel中数据发生变化时通知页面

既然如此,那就开始实战校验一番!

2.1 实战一(简单应用)

上面说了,LiveDta需要有ViewModel才会发挥其作用,因此:

MyViewModel.kt

class MyViewModel : ViewModel() {

	//定义对应的LiveData集合
    private var linkNumber: MutableLiveData<Int>? = null

	//得到对应的LiveData集合
    fun getLinkNumber(): MutableLiveData<Int>? {
        //保证linkNumber不为null
        if (linkNumber == null) {
        	//初始化
            linkNumber = MutableLiveData()
            linkNumber!!.value = 0
        }
        return linkNumber
    }

	//给外部提供修改集合内部属性的方法
    fun addLinkedNumber(n: Int) {
        linkNumber!!.value = linkNumber!!.value!! + n
    }

}
复制代码

我们可以看到,在这对应的ViewModel里,定义了MutableLiveData<Int>对应的LiveData集合,然后就是一顿初始化,修改等方法。

来看看怎么使用的?

class MainActivity : AppCompatActivity() {
    private var viewModel: MyViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView: TextView = findViewById(R.id.textView)
        
        viewModel =
            ViewModelProvider(this, AndroidViewModelFactory(application))[MyViewModel::class.java]
        
        
        viewModel!!.getLinkNumber()!!.observe(this, Observer {
            textView.text = String.valueOf(it)
        })
    }

    fun reduce(view: View) {
        viewModel!!.addLinkedNumber(-1)
    }

    fun add(view: View) {
        viewModel!!.addLinkedNumber(1)
    }

}
复制代码

这个页面的布局很简单,就两个按钮和一个文本,因此就没有贴对应的布局代码。

这里我们看到:

  • 初始化ViewModel和上面如出一辙,当点击对应按钮时,将通过addLinkedNumber对ViewModel对应LiveData属性做了修改操作!
  • 随后activity通过viewModel!!.getLinkNumber()!!.observe(this, Observer {xx},来接受ViewModel发过来的修改通知,并及时更新至textView上

来看看运行效果:

在这里插入图片描述 哈哈哈,有点简陋哈!不过只要效果达到了就行!

接下来进入下一个实战!

2.2 实战二(Fragment通信)

2.2.1 对应的布局文件

fragment.xml (两个相同的布局)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">


    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="0dp"
        android:min="0"
        android:max="100"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
复制代码

activity布局

6.png

如图所示

首页布局,就两个Fragment,上下各一个,平均分配,每个Fragment布局相同,都含有相同的SeekBar

想要实现的效果:不管拖动哪个Fragment里面的SeekBar,另一个SeekBar跟着移动

2.2.2 对应实现逻辑

先看MyViewModel.kt

class MyViewModel : ViewModel() {

    private var progress: MutableLiveData<Int>? = null

    fun getProgress(): MutableLiveData<Int>? {
        if (progress == null) {
            progress = MutableLiveData()
            progress!!.value = 0
        }
        return progress
    }
}
复制代码

狠简单,直接看怎么使用!

FirstFragment.kt

class FirstFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
        
        var rootView = inflater.inflate(R.layout.fragment_first, container, false)
        var seekBar = rootView.findViewById<SeekBar>(R.id.seekBar)
        
        //实例化对应ViewModel
        val viewModel = ViewModelProvider(
            requireActivity(), AndroidViewModelFactory(
                requireActivity().application
            )
        )[MyViewModel::class.java]

        //接受ViewModel对应的通知
        viewModel.getProgress()!!.observe(requireActivity(), Observer {
            seekBar.progress = it
        })

        seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
            	//这里修改对应ViewModel里面LiveData的属性值
                viewModel.getProgress()!!.setValue(progress)
            }

            override fun onStartTrackingTouch(seekBar: SeekBar) {}
            override fun onStopTrackingTouch(seekBar: SeekBar) {}
        })

        return rootView
    }

}
复制代码

我们看到,这里使用方式如出一辙,当seekBar属性值发生改变时,将修改LiveData的值,随后通知对应Fragment达到刷新UI的作用!

第二个Fragment实现逻辑和这个一模一样,直接复制粘贴重命名一份。

来看看运行效果

在这里插入图片描述 完美,当然使用了ViewModel,任凭旋转屏幕哈,数据都不会丢失!

2.3 总结

从上面的实战中可以看出LiveData的优势

  • 确保界面符合数据状态
  • 不会发生内存泄露
  • 不会因Activity停止而导致崩溃
  • 不再需要手动处理生命周期
  • 数据始终保持最新状态
  • 适当的配置更改
  • 共享资源

结束语

好了,本篇到这里差不多结束了,相信读者对ViewModel+LiveData有所了解了!在下一篇中将会详解DataBinding以及将前面所讲解的集合成一个:LifeCycle+ViewModel+LiveData+DataBinding实战!

Demo下载:点我下载(包含下一篇部分源码)

猜你喜欢

转载自juejin.im/post/7034055627722719269