本節內容
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<Book>
fun getBooks():List<Book>
fun addBook(book: Book)
}
5.具體的結構如下圖所示:
結構圖
四、實現數據解耦
1.模仿從數據庫里面取數據,新建一個名為db的包,在里面新建一個BookDaoImpl類,并繼承自BookDao接口。
class BookDaoImpl : BookDao {
//模擬數據庫中存儲的數據
override var bookList: MutableList<Book> = mutableListOf()
override fun getBooks(): List<Book> {
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<Book> = mutableListOf()
override fun getBooks(): List<Book> {
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<Book>{
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<Book>{
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<Book>)
}
第二個接口名為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<Book>) {
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。
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 <T : ViewModel?> create(modelClass: Class<T>): 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<String> = 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<List<Book>> = 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)運行程序,得到和前面一樣的結果。
運行結果