写在前面
现在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>
创建包
首先我们创建data和UI两个包,data表示对数据的存储以及操作,UI表示界面显示,然后将activity移入UI包中
创建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的简单逻辑架构之后,就为将来复杂项目打下了基础,待续…
转载请注明出处