本节内容
1.搭建界面
2.正常方式实现操作
3.分析数据模型Model
4.实现数据解耦
5.抽离Repository创建过程
6.MVP设计模式实现
7.ViewModel感知生命周期
8.自定义ViewModelProvider的factory
9.异步数据回调
10.liveData的使用
一、搭建界面
1.为了更好的理解MVC、MVP、MVVM架构模式,我们通过一个小demo来逐步慢慢学习这三种结构模式,首先我们需要搭建一个界面。
2.搭建的界面如下图所示,有两个输入框,输入完成之后点击右侧的按钮,我们刚刚输入的内容就会显示在最上方的灰色框中。
3.每次输入新的内容之后再点击按钮,内容会按行依次排列在上方的灰色框中。
4.现在我们来搭建一下界面。上面的灰色框就是一个TextView。下面两个输入框是两个EditText,右侧是一个Button。
-
TextView横向拉伸为0dp,高度写死为200dp,颜色为灰色。
-
EditText添加hint默认提示,分别为Name和Author。顺便给它们都加上id。
二、正常方式实现操作
1.在MainActivity里面创建一个名为initializeUI()的函数,并在onCreate方法里面调用该函数。实现按钮的点击事件,先判断输入框是不是为空,如果是的话就给出相应的提示。如果都不为空的话,就把相应的内容拼接到文本框中去。
private fun initializeUI(){
mButton.setOnClickListener {
if(mNameEditText.text.toString().isEmpty()){
Toast.makeText(this,"书名不能为空",Toast.LENGTH_LONG).show()
return@setOnClickListener
}
if(mAuthorEditText.text.toString().isEmpty()){
Toast.makeText(this,"作者不能为空",Toast.LENGTH_LONG).show()
return@setOnClickListener
}
mTextView.setText("${mNameEditText.text}-${mAuthorEditText.text}")
}
}
-
按照上述方法操作,只能添加一个文本。当我们再次输入新的内容时,它就会覆盖原来的文本,并不会排列在原来的文本下方。所以我们需要把文本拼接起来。
2.在显示文本框内容的时候,把前面的内容也加上就行了。这样就可以按行显示我们刚刚输入的内容。
val content = "${mTextView.text}\n ${mNameEditText.text}----${mAuthorEditText.text}"
mTextView.setText(content)
三、分析数据模型Model
1.刚刚我们的操作虽然实现了我们想要的功能,但这是没意义的。因为我们并没有把书和作者这些数据保存起来。
2.输入完这些数据以后,它们有两个去处
-
可能会保存到本地的数据库
-
也有可能通过网络上传到远程服务器。
3.为了方便数据的保存,我们新建一个包,然后在这个包里面新建一个数据类。里面只有书名和作者两个数据。我们把Book封装成一个Model,里面有书名和作者名。
data class Book (val name:String,val author:String){
}
4.对于外部的Activity来说,它想要获取数据,可以通过一个仓库来实现。这个仓库有所有的books,同时它还提供addBook()和getBooks()方法。仓库的数据来自本地数据库或者远程服务器。从哪个地方取可以自己配置。
-
不管从哪个地方取,它们都要有addBook()和getBooks()方法。既然这样的话,我们就可以提供一个接口,让它们都实现这个接口好了。
interface BookDao {
var bookList:MutableList
fun getBooks():List
fun addBook(book: Book)
}
5.具体的结构如下图所示:
四、实现数据解耦
1.模仿从数据库里面取数据,新建一个名为db的包,在里面新建一个BookDaoImpl类,并继承自BookDao接口。
class BookDaoImpl : BookDao {
//模拟数据库中存储的数据
override var bookList: MutableList = mutableListOf()
override fun getBooks(): List {
return bookList
}
override fun addBook(book: Book) {
bookList.add(book)
}
}
2.需要一个类来操作数据库对象,所以新建一个DataBase类来管理数据库的操作。因为管理数据库操作的只能有一个对象,所以必须用单例设计。
class DataBase private constructor(){
val bookDao = BookDaoImpl()
companion object{
private var instance:DataBase? = null
fun getInstance() = instance?: synchronized(this){
instance?: DataBase().also {
instance = it
}
}
}
}
3.模仿从网络里面获取数据。新建一个名为network的包,先新建一个BookDaoNetworkImpl类,继承自BookDao
class BookDaoNetworkImpl: BookDao {
override var bookList: MutableList = mutableListOf()
override fun getBooks(): List {
return bookList
}
override fun addBook(book: Book) {
bookList.add(book)
}
}
4.封装一个类供外部使用,并使用单例设计模式
class NetWork private constructor(){
val bookDao =BookDaoNetworkImpl()
companion object{
@Volatile private var instance:NetWork? = null
fun getInstance() = instance?: synchronized(this){
instance?: NetWork().also {
instance = it
}
}
}
}
5.在data包里面新建一个BookRepository类,作为仓库。仓库使用的是单例设计模式,必须私有化构造函数,根据参数的不同,来确定是从数据库还是从网络获取数据。
class BookRepository private constructor(){
//只要修改DataBase就可以选择从哪里获取数据 dao: BookDao
private val bookDao = DataBase.getInstance().bookDao
companion object{
@Volatile private var instance: BookRepository? = null
fun getInstance() = instance ?: synchronized(this){
instance ?: BookRepository().also {
instance = it
}
}
}
fun getBooks():List{
return bookDao.getBooks()
}
fun addBook(book: Book){
bookDao.addBook(book)
}
}
6.然后在MainActivity里面把这个书本添加到数据库里面去。
val book = Book(mNameEditText.text.toString(),mAuthorEditText.text.toString())
BookRepository.getInstance().addBook(book)
五、抽离Repository创建过程
1.想要使用哪种方式传递数据并不是数据库说了算,而是需要我们告诉它上传到哪儿。所以我们不能把在 BookRepository类里面把传递方式写死,我们最好是通过构造方法添加一个变量,方便 BookRepository和我们联系。
class BookRepository private constructor(private val bookDao: BookDao){
//只要修改DataBase就可以选择从哪里获取数据 dao: BookDao
companion object{
@Volatile private var instance: BookRepository? = null
fun getInstance(dao:BookDao) = instance ?: synchronized(this){
instance ?: BookRepository(dao).also {
instance = it
}
}
}
fun getBooks():List{
return bookDao.getBooks()
}
fun addBook(book: Book){
bookDao.addBook(book)
}
}
2.然后在MainActivity里面,也就是外部告诉它我们要上传到哪,然后再添加进去。比如说我们这里选择的就是NetWork
val book = Book(mNameEditText.text.toString(),mAuthorEditText.text.toString())
val dao = NetWork.getInstance().bookDao
BookRepository.getInstance(dao).addBook(book)
3.真正的repository是在MainActivity里面创建的,如果想要操作其他数据,那么就要重新写repository,又要在MainActivity里面创建。对于程序员来说,必须要知道repository的源代码才能进行修改,这样就很不方便。为了解决这个问题,我们提供一个单独的类即可。
4.新建一个名为Utils的包,然后在里面新建一个名为ProvideRepositoryFactory的类。这是个object类,所有的方法都是静态方法,直接用类名访问它即可。
object ProvideRepositoryFactory {
object ProvideRepositoryFactory {
fun getRepository(): BookRepository {
val dao = DataBase.getInstance().bookDao
return BookRepository.getInstance(dao)
}
}
}
5.在MainActivity里面把它添加进去即可。这个方法方便就方便在要修改的时候直接在ProvideRepositoryFactory里面修改好了
val book = Book(mNameEditText.text.toString(),mAuthorEditText.text.toString())
val repository = ProvideRepositoryFactory.ProvideRepositoryFactory.getRepository()
repository.addBook(book)
六、MVP设计模式实现
1.我们前面进行的这一系列操作就是用MVC方式来实现的。
2.MVC设计模式的特点如下图所示:
-
对于我们安卓开发来说,View就是xml文件
-
Controller相当于中间人,Model和View要通信的话必须得经过Controller。在安卓里一般是Activity或者Fragment来扮演控制器。控制器里面管理所有的逻辑和数据,所以它的任务很重,为了减轻控制器的负担,就出现了MVP模式。
3.MVP模式的设计特点如下图所示:
-
与MVC不同的是:MVP设计模式在C和M之间加了一个Presenter。
-
这下Activity/Fragment和View统称为View,没有控制器一说。
-
这里把逻辑和数据抽离出来,用了一个Presenter来管理,View只负责显示,具体的操作都放到Presenter里面来了。
-
Presenter想要和View进行通信的话,那么Presenter里面要有一个mView的对象,View里面也要有presenter的对象,这样两者之间才能进行交互。
-
因为它们是两个相互独立的模块,所以它们都不懂对方的东西(或者说方法)。要解决这个问题就需要统一接口,它们可以分别提供一个接口给对方使用。
-
MVP设计模式好就好在把Presenter和View独立出来了,但是要让它们进行交互的话就很麻烦。
4.用MVP设计模式实现上面的demo。
-
(1)先新建一个model,然后把前面创建的那几个包都加进去,xml的代码也复制进去
-
(2)新建一个名为UI的包,在里面添加两个接口。
-
第一个接口名为IBookView,里面有两个方法inputIsValid(),判断传过来的数据是否合法,showBooks(),刷新一下显示的内容。
interface IBookView {
fun inputIsValid(valid: Boolean)
fun showBooks(books:List)
}
-
第二个接口名为IBookPresenter,里面包括View操作数据的时候可以进行的操作,比如检查数据是否合法,以及添加数据。
interface IBookPresenter {
fun checkInput(content1:String,content2:String)
fun addBook(book: Book)
}
-
(3)在MainActivity里面继承一个IBookView,然后实现那两个方法
class MainActivity : AppCompatActivity() ,IBookView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mButton.setOnClickListener {
//检查是否合法
//添加数据
}
}
override fun inputIsValid(valid: Boolean) {
Toast.makeText(this, "输入不能为空", Toast.LENGTH_LONG).show()
}
override fun showBooks(books: List) {
val stringBuilder = StringBuilder()
books.forEach { book ->
stringBuilder.append("${book.name}----${book.author}\n")
}
mTextView.text = stringBuilder.toString()
}
}
-
在实现按钮的点击事件的时候,就要进行相关的操作。因为我们使用的是MVP设计模式,所以不能在MainActivity里面直接操作。我们需要通过Presenter来操作,所以在UI包里面我们新建一个名为BookPresenterImpl的类,并实现IBookPresenter接口。
class BookPresenterImpl : IBookPresenter{
override fun checkInput(content1: String,content2:String) {
}
override fun addBook(book: Book) {
}
}
-
(4)然后在MainActivity里面创建BookPresenterImpl的对象。
private val presenter = BookPresenterImpl()
-
检查数据是否合法的时候直接通过该对象调用相关的方法即可。
presenter.checkInput(mNameEditText.text.toString(),mAuthorEditText.text.toString())
-
那么inputIsValid方法里面,只有输入非法才需要弹出提示,否则就把这本书添加进去
override fun inputIsValid(valid: Boolean) {
if (!valid) {
Toast.makeText(this, "输入不能为空", Toast.LENGTH_LONG).show()
}else{
val book = Book(mNameEditText.text.toString(),mAuthorEditText.text.toString())
presenter.addBook(book)
mNameEditText.setText("")
mAuthorEditText.setText("")
}
}
-
(5)在BookPresenterImpl里面也要创建View的一个对象
var mView :IBookView? = null
-
然后在MainActivity里面给它赋值,这样presenter也得到了View 的对象
presenter.mView = this
-
(6)有了View的对象,就可以实现BookPresenterImpl类里面的方法了。
class BookPresenterImpl : IBookPresenter{
var mView :IBookView? = null
private val repository = ProvideRepositoryFactory.ProvideRepositoryFactory.getRepository()
override fun checkInput(content1: String,content2:String) {
if (content1.isEmpty()||content2.isEmpty()){
mView?.inputIsValid(false)
}else{
mView?.inputIsValid(true)
}
}
override fun addBook(book: Book) {
repository.addBook(book)
mView?.showBooks(repository.getBooks())
}
}
最后运行结果如下图所示:
七、ViewModel感知生命周期
1.MVP缺点:首先是比较复杂。其次是,每增加一个界面,就必须增加两个接口。
2.当界面旋转的时候,显示的内容就不见了。但是输入完之后,前面的数据和新的数据又会都显示出来。而且界面销毁之后,再输入新的内容之后,之前输入过的内容又会重新显示。数据并不能感知生命周期。
3.如果想要让数据感知生命周期,那么这个类就要继承自LifecycleObserver。那么这就涉及到我们要讲的这个MVVM设计模式。
4.在介绍MVVM设计模式之前,先了解一下ViewModel。
-
ViewModel
类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel
类让数据可在发生屏幕旋转等配置更改后继续留存
5.我们先新建一个项目,然后在第四个gradle的dependencies 里面添加以下代码,添加ViewModel。详情见https://developer.android.google.cn/jetpack/androidx/releases/lifecycle#declaring_dependencies
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
6.先任意布局一下xml页面,我就添加了一个TextView和一个Button
7.在MainActivity实现一下按钮的点击事件
class MainActivity : AppCompatActivity() {
private var content = "hello"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mButton.setOnClickListener {
mTextView.text = content
}
}
}
-
这样运行的话,点击之后文字会变为hello,但是旋转屏幕之后,又会变为原来的默认文字。这样文字就没有持久化。
8.想要让屏幕旋转之后,数据也不改变的话,那么就需要用到 onSaveInstanceState方法。如果获取到的数据不为空,那么就把它赋值给content,然后再显示出来,否则显示出来的就是默认值。这样不管屏幕再怎么旋转,TextView的值都不会改变。
class MainActivity : AppCompatActivity() {
private var content = "喜羊羊"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if(savedInstanceState!=null){
content = savedInstanceState.getString("str").toString()
mTextView.text = content
}else{
mTextView.text = content
}
mButton.setOnClickListener {
content = "懒羊羊"
mTextView.text = content
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("str",content)
}
}
9.前面用的是一般方法,如果要用ViewModel的话,可以先创建一个类来管理数据。随便添加一点数据。
class MyViewModel:ViewModel() {
var content = "喜羊羊"
}
10.然后在MainActivity里面添加一个MyViewModel的对象,通过ViewModelProvider来获取它的对象。然后在按钮的点击事件里面直接修改viewModel即可。
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
mTextView.text = viewModel.content
mButton.setOnClickListener {
viewModel.content = "灰太狼"
mTextView.text = viewModel.content
}
八、自定义ViewModelProvider的factory
1.前面那种方法,用 ViewModelProvider创建MyViewModel的对象,使用它的前提是默认myViewModel类中只有默认的构造函数。如果mvViewModel中存在有参数的构造函数,那么就不能用这种方法构建MyViewModel的对象,一般使用ViewModelProvider.Factory。
2.新建一个类,继承于 ViewModelProvider.NewInstanceFactory()。
class ViewModelProviderFactory : ViewModelProvider.NewInstanceFactory(){
override fun create(modelClass: Class): T {
return MyViewModel("懒羊羊") as T
}
}
3.然后在MainActivity里面创建具体对象,最后就可以得到我们想要的结果。
val factory = ViewModelProviderFactory()
val viewModel= ViewModelProvider(this,factory).get(MyViewModel::class.java)
九、异步数据回调
1.如果我们想要从网络下载数据,然后点击按钮之后显示的是我们下载的数据。想要实现这个功能,我们就新建一个类,模拟一下下载数据。
class TestNewWork {
fun loadData(){
Thread(Runnable {
Thread.sleep(1000)
val result = "下载的数据"
}).start()
}
}
2.然后在MainActivity里面创建这个类的对象,再调用这个方法
val net = TestNewWork()
net.loadData()
-
结果就是没有显示下载的数据,因为我们还没有把这个数据上传到TextView中。我们要把下载的数据传递给外部,那么就需要使用Handler。
3.在TestNewWork里面创建一个Handler对象,然后重写它的一个方法。在loadData函数里面就新建一个Message对象,并把下载的数据赋给它。然后在handleMessage方法里面通过高阶函数把结果回调过去。
class TestNewWork {
var callBack:((String)->Unit)?=null
//Handler
private val handler = object :Handler(){
override fun handleMessage(msg:Message){
super.handleMessage(msg)
if(msg.what==1){
val str = msg.obj as String
callBack?.let {
it(str)
}
}
}
}
fun loadData(){
Thread(Runnable {
Thread.sleep(1000)
val result = "下载的数据"
//把数据传给外部
val msg = Message()
msg.what = 1
msg.obj = result
handler.sendMessage(msg)
}).start()
}
}
4.在MainActivity里面就把回调过来的数据传递给TextView显示出来,最后就得到了我们想要的结果。
val net = TestNewWork()
net.callBack = {
mTextView.text = it
}
net.loadData()
十、liveData的使用
1.LiveData
是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
2.使用liveData具有以下优势:
-
确保界面符合数据状态
-
不会发生内存泄漏
-
不会因 Activity 停止而导致崩溃
-
不再需要手动处理生命周期
-
数据始终保持最新状态
-
适当的配置更改
-
共享资源
3.要使用liveData,先在gradle的dependencies导入以下代码,然后同步一下。
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
4.前面使用callBack进行回调的缺点就是,不能回调大量的数据。下面我们用liveData来实现一下上述功能
5.在TestNewWork里面,不再用callBack进行回调,我们使用liveData。先创建一个可变的liveData对象,然后初始化为"Empty",在handleMessage里面,直接把获取到的内容传给content.value即可。loadData的代码和前面一样没有变。
val content:MutableLiveData = MutableLiveData()
init {
content.value = "Empty"
}
//Handler
private val handler = object : Handler() {
override fun handleMessage(msg:Message){
super.handleMessage(msg)
if(msg.what==1){
content.value = msg.obj as String
}
}
}
6.在MainActivity里面创建ViewModel对象,然后调用observe方法,在按钮的点击事件里面直接调用loadData方法即可。最后也得到了我们想要的结果。
val viewModel = ViewModelProvider(this).get(TestNewWork::class.java)
viewModel.content.observe(this,{value->
mTextView.text = value
})
mButton.setOnClickListener{
viewModel.loadData()
}
十一、MVVM和组件化开发
1.MVVM设计模式架构如下图所示:
2.MVVM:Model View ViewModel
-
Model:负责数据和数据的逻辑
-
View:和用户交互的视图。包括View /Activity /Fragment。主要处理(1)用户交互事件(2)数据刷新
-
ViewModel:管理视图逻辑和模型数据(希望每一个界面的数据都跟它自己的生命周期相关联,所有的数据都能够使用liveData来监听它。所以把数据放到ViewModel就行了,用它来管理)
-
View和Model的交互:(1)通过View来改变Model,那么就需要提供相应的方法。
-
(2)Model里面有了数据之后,把它更新到View里面来,liveData已经做好了,我们不用管。
-
Repository:管理数据的入口。
3.用MVVM设计模式来实现一下我们前面添加name和Author到TextView的功能。
(1)新建一个工程,在里面再添加一个module。
-
如何设置模块是可运行的程序还是一个依赖库,以下就是不可运行的库。
id 'com.android.library'
id 'com.android.application'
(2)自己创建库,并让外部来依赖这个库。我们有两个module,app和data
(3)我们把data设置为不可运行的库,然后在app的module里面添加以下代码,代表app依赖于data库。
implementation project(path: ':data')
(4)把我们前面写那些包都拷贝到新的工程里面。
(5)在app项目里面,把前面的xml文件拷贝过来,这样就不用重新布局了。然后在MainActivity里面给按钮添加点击事件,在这里面我们要把书本添加进来,所以我们需要一个类来管理这些书本。(注意:前面添加的包都在data里面,并非app工程)
class BookViewModel :ViewModel(){
val books :MutableLiveData> = MutableLiveData()
private val repository = ProvideRepositoryFactory.ProvideRepositoryFactory.getRepository()
init {
books.value = repository.getBooks()
}
fun addBook(book: Book){
repository.addBook(book)
books.value = repository.getBooks()
}
}
(6)在MainActivity里面,使用一下ViewModel
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(BookViewModel::class.java)
viewModel.books.observe(this, Observer {books->
val stringBuilder = StringBuilder()
books.forEach {
stringBuilder.append("${it.name}----${it.author}")
}
mTextView.text =stringBuilder
mNameEditText.setText("")
mAuthorEditText.setText("")
})
mButton.setOnClickListener {
val book = Book(mNameEditText.text.toString(),mAuthorEditText.text.toString())
viewModel.addBook(book)
}
}
}
(7)运行程序,得到和前面一样的结果。