Android开发(28)——MVC、MVP、MVVM架构模式和组件化

本节内容

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设计模式的特点如下图所示:
MVC设计模式特点
  • 对于我们安卓开发来说,View就是xml文件
  • Controller相当于中间人,Model和View要通信的话必须得经过Controller。在安卓里一般是Activity或者Fragment来扮演控制器。控制器里面管理所有的逻辑和数据,所以它的任务很重,为了减轻控制器的负担,就出现了MVP模式。
3.MVP模式的设计特点如下图所示:
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设计模式架构如下图所示:
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)运行程序,得到和前面一样的结果。
运行结果

你可能感兴趣的:(Android开发(28)——MVC、MVP、MVVM架构模式和组件化)