使用LiveData和ViewModel为Android项目搭建MVVM架构(Kotlin语言版)(入门教程)

  • 写在前面
    现在MVVM架构大行其道,逐渐取代MVP架构成为Android开发的主流架构,并且google官方为了方便实现MVVM架构推出了Architecture系列的库,现已纳入到jetpack中,并且推出了KTX版本的,这样用Kotlin语言也可以更方便的使用Architecture了。但网上的教程大部分是JAVA语言版本的,而且很多就是按照官方文档翻译一下,并没有讲的很清楚。
    Kotlin语言和MVVM架构必然是将来Android开发的主流趋势,早一天学会,就能走在别人前面!
    下面我用一个完整的小项目实例带各位学习使用Kotlin语言如何开发MVVM
    既然是入门教程,我会用尽量少的代码和尽量详细的文字说明,如果后续想继续深入学习的,请继续关注我的教程
    话不多说,现在就开始吧!

  • 项目配置
    首先确保你的AndroidStudio版本不低于3.2
    创建一个新项目,创建时勾选include kotlin support(勾选了此选项,AndroidStudio会自动给你配置Kotlin开发环境),接着选择创建一个empty Activity
    项目创建完成之后需要做一定的配置
    1.为了将来使用ktx,我们需要先打开androidx,找到工程目录下面gradle.properties文件,添加以下两行

    android.useAndroidX=true
    android.enableJetifier=true
    

    2.打开app目录底下build.gradle,将compileSdkVersion改成28
    在dependencies下面,将

     	implementation 'com.android.support:appcompat-v7:28.0.0'
     	implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    

    这两行干掉,替换成androidx版本的

    	implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
    	implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    

    添加LiveData和ViewModel依赖库(这也是androidx版本的)

    	implementation "androidx.lifecycle:lifecycle-extensions:2.1.0-alpha01"
    

    最后sync一下,就算配置完成了

  • 布局文件:
    为了简单起见,我们设计一个最简单的功能,就是展示书籍名称和作者
    然后通过输入添加,来模拟数据变化,然后看看怎么通过MVVM来实现它
    布局不是本文的重点,我就直接贴代码出来了
    activity_main.xml

    
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/textView_books"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintHeight_percent="0.55"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:scrollbars="vertical"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"
            tools:text="《三体》 - 刘慈欣"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />
    
        <EditText
            android:id="@+id/editText_book_name"
            android:layout_width="0dp"
            app:layout_constraintWidth_percent="0.7"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:hint="BookName"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView_books"
            app:layout_constraintVertical_bias="0.0" />
    
        <EditText
            android:id="@+id/editText_author_name"
            android:layout_width="0dp"
            app:layout_constraintWidth_percent="0.7"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:hint="AuthorName"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/editText_book_name"
            app:layout_constraintVertical_bias="0.0" />
    
        <Button
            android:id="@+id/button_add_quote"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:backgroundTint="?colorAccent"
            android:text="Add Book"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="@+id/editText_author_name"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toEndOf="@+id/editText_book_name"
            app:layout_constraintTop_toTopOf="@+id/editText_book_name"
            app:layout_constraintVertical_bias="0.0"
            app:layout_constraintWidth_percent="0.25" />
    
    androidx.constraintlayout.widget.ConstraintLayout>
    

    然后,预览界面是这样
    使用LiveData和ViewModel为Android项目搭建MVVM架构(Kotlin语言版)(入门教程)_第1张图片

  • 创建包
    首先我们创建data和UI两个包,data表示对数据的存储以及操作,UI表示界面显示,然后将activity移入UI包中
    使用LiveData和ViewModel为Android项目搭建MVVM架构(Kotlin语言版)(入门教程)_第2张图片

  • 创建Book类
    在data目录下,创建一个Book类
    Book.kt

    data class Book(val bookName:String,
                val authorName:String) {
        override fun toString(): String {
            return "$bookName - $authorName"
        }
    }
    

    注意,该类为一个实体类,所以我们在class前面加上data,构造参数前面加上val 表示该参数有同名的成员变量可以直接使用,最后面是字符串模板就不介绍了

  • 创建数据访问类,也就是DAO
    在实际项目开发中,如果你有一个bean对象,你通常需要将它保存到数据库中(如果需要使用sqlite数据库,你可以使用相关组件ROOM),DAO类就是用于操作数据库的接口。但是本文主要介绍MVVM架构的体系,对于数据库操作以后的教程会展示,为了简便起见,我们是做一个列表来模拟一个假数据库

    class BookDao{
        //用list模拟一个假的数据库表
        private val bookList = mutableListOf<Book>()
    
        //LiveData是Architecture 组件的重要部分,它可以监听值的改变
        //MutableLiveData是LiveData的子类,它的值可以被改变,而LiveData的值不能被修改
        private val books = MutableLiveData<List<Book>>()
    
        init {
            books.value = bookList
        }
    
        fun addBook(book: Book){
            bookList.add(book)
            //当book发生改变之后,更新books的值,它会通知处于活跃状态的观察者
            books.value = bookList
        }
    
        //这里返回值是LiveData而不是MutableLiveData,因为我们不想其他的类能修改它的值
        fun getBooks():LiveData<List<Book>>{
            return books
        }
    }
    
  • 创建database类
    在实际生产开发中,一个数据库可能有多个DAO,因为有多张表,例如在聊天的应用中,需要关注用户和用户组,你就已经有两个DAO了
    所以需要建立一个Database类来管理所有的Dao
    同时拥有两个数据库实例没有意义,所以Database实例应该是单例模式
    尽管Kotlin可以通过object关键字来创建单例类,但是通常在实际生产中不适用,因为如果使用object关键字,你无法给构造函数传参,但实际生产开发中,你需要给ROOM传递Context参数,为了避免这个问题,需要使用类似于java方式来创建单例

    class DataBase private constructor(){
    
        var bookDao = BookDao()
            private set
            
        companion object {
            //@Volatile  - 表示写入此属性对其他线程立即可见
            @Volatile
            private var instance:DataBase? = null
            //如果instance是null,则在线程安全的环境中中创建一个对象,否则直接返回instance
            fun getInstance():DataBase{
                return instance?: synchronized(this){
                    instance?:DataBase().also { instance = it }
                }
            }
        }
    }
    
  • 创建Repository类
    Repository类是有关数据决策的类,是应该从服务器获取新数据还是使用本地数据? 需要保留5天的天气数据或仅需3天? 做出这样的决定是Repository类的工作
    同样,拥有多个Repository对象没有意义,因此它也是一个单例。 这次你需要传递Dao以便实现其功能。使用依赖注入的方式将Dao实例提供给Repository。
    将Dao作为构造参数传给Repository,也许你会觉得为什么不直接在Repository类内部直接初始化Dao呢?这样会破坏程序的可测试性,因为你在测试程序时可能需要传递一个模拟的Dao版本给Repository,模拟测试的原则是只在最少的地方将数据修改为模拟数据。
    这是依赖注入背后的核心理念 - 使事物完全模块化和独立。

    class BookRepository private constructor(private val bookDao: BookDao) {
    
        fun addBook(book:Book){
            bookDao.addBook(book)
        }
    
        fun getBooks():LiveData<List<Book>>{
            return bookDao.getBooks()
        }
    
        companion object {
            //@Volatile  - 表示写入此属性对其他线程立即可见
            @Volatile
            private var instance:BookRepository? = null
    
            //如果instance是null,则在线程安全的环境中中创建一个对象,否则直接返回instance
            fun getInstance(bookDao: BookDao):BookRepository{
                return instance?: synchronized(this){
                    instance?:BookRepository(bookDao).also { instance = it }
                }
            }
        }
    }
    
  • 创建ViewModel类
    前面已经完成了很多工作了,现在是时候将前面的内容链接到View部分了(也就是Activity或者Fragment),ViewModel就是处于链接的作用, Activities 和Fragments仅用于在屏幕上显示内容和接收用户操作,所有的逻辑,数据,数据操作都转移到ViewModel中,然后View仅仅调用ViewModel的函数。我们要实现一些功能还是需要通过Repository类来实现,同样,通过依赖注入的方式传递参数过来

    class BooksViewModel(private val bookRepository: BookRepository):ViewModel() {
    
        fun addBook(book: Book){
            bookRepository.addBook(book)
        }
        
        fun getBooks() = bookRepository.getBooks()
    }
    
  • 创建ViewModelFactory
    请注意,ViewModel实体必须通过ViewModelProvider来创建,因为上面的BookViewModel的构造方法中包含参数,所以我们还必须创建ViewModelFactory类提供给ViewModelProvider,以此创建ViewModel实体类

    class BooksViewModelFactory(private val repository: BookRepository):ViewModelProvider.NewInstanceFactory() {
    
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return BooksViewModel(repository) as T
        }
    }
    
  • 通过依赖注入连接ViewModelFactory和Repository
    来理一理前面的关系,数据层:Dao直接操作底层数据,DataBase管理Dao,Repository需要使用Dao操作数据,所以他先要获取DataBase对象
    ViewModel层:
    ViewModel来控制UI显示,但是ViewModel需要通过ViewModelFactory来产生实例,ViewModelFactory需要依赖Repository
    那么现在应该创建一个依赖注入类,来连接所有的关系了,同样,该类也应该是单例,因为无需参数,我们直接使用object来创建单例

    object InjectorUtils {
    
        //Activity会调用此方法
        fun provideBookViewModelFactory():BooksViewModelFactory{
    
            //所有的依赖就在这里,在这一个地方串联起来了
            val repository = BookRepository.getInstance(DataBase.getInstance().bookDao)
            return BooksViewModelFactory(repository)
        }
    }
    
  • 实现Activity
    最后来实现UI操作部分,这部分是最简单最容易理解的,就不多说明了,直接上代码

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            initUI()
        }
    
        private fun initUI() {
            //通过factory生成ViewModel,这是标准写法
            val factory = InjectorUtils.provideBookViewModelFactory()
            val viewModel = ViewModelProviders.of(this, factory).get(BooksViewModel::class.java)
    
            //通过LiveData监听数据改变实现对页面显示的改变
            viewModel.getBooks().observe(this, Observer {books->
                val stringBuilder = StringBuilder()
                books.forEach { book ->
                    stringBuilder.append("$book\n\n")
                }
                textView_books.text = stringBuilder.toString()
            })
            
            //通过此操作模拟数据变化
            button_add_quote.setOnClickListener {
                //点击时操作ViewModel而不是直接操作textView_books
                val book = Book(editText_book_name.text.toString(), editText_author_name.text.toString())
                viewModel.addBook(book)
                editText_book_name.setText("")
                editText_author_name.setText("")
            }
    
        }
    }
    
    

大功告成!!理解了小demo的简单逻辑架构之后,就为将来复杂项目打下了基础,待续…
转载请注明出处

你可能感兴趣的:(kotlin)