前言
目前Android開發中,常用的幾種項目架構模式分別是MVC、MVP和MVVM。當然根據項目的體量和業務的不同,可能還會對這幾種模式進行融合,產生其他變種模式,這個我們暫且不談。我們本篇的主角是最近越來越受歡迎的MVVM。
隨着Google在近兩年推出了Jetpack系列的工具庫後,MVVM的開發模式越來受開發者歡迎,畢竟是Google的親兒子。對於開發中的MVVM模式,我們主要是依託DataBinding、ViewModel和LiveData這三者來實現。
一、對於ViewModel的使用建議
ViewModel是Jetpack全家桶中的一員,也是構建MVVM模式的重要組成部分。因爲ViewModel擁有遠比Activity和Fragment還要長的生命週期,所以ViewModel中最好不要持有Activity或者Fragment的引用,否則很容易引起內存泄漏。一般建議在ViewModel只做數據處理,保存Activity/Fragment中的頁面數據,在Activity被銷燬重建時也能拿到之前的頁面數據。此外,Google建議一個Activity/Fragment最好只擁有一個ViewModel,一個ViewModel中可以擁有多個Model實例(即數據邏輯處理的類,比如網絡請求數據)。
eg: 當屏幕旋轉時,Activity可能會先被銷燬,在重新創建新的實例;而因爲ViewModel的生命週期長於Activity,新的Activity中的ViewModel持有的是之前被銷燬的Activity的引用(具體原理可看ViewModelProvider源碼中對ViewModel的存取處理),這樣就會導致內存泄漏。
二、給ViewModel添加頁面的生命週期函數
在開發中,ViewModel中的方法不可避免的會被在指定的Activity/Fragment的生命週期函數中調用,我們可以在Activity/Fragment的生命週期函數中主動調用ViewModel中的方法,但是我們還有一種更完美的方法,即讓ViewModel可以擁有和Activity/Fragment同步的一樣的生命週期函數,ViewModel在自己的生命週期函數中主動調用自己的方法即可,與Activity/Fragment更加解耦,也更加方便單元測試。
讓ViewModel擁有和Activity一樣的生命週期函數,我們常見的做法如下,在Activity基類中的生命週期中調用ViewModel基類中對應的生命週期函數,達到ViewModel的生命週期函數和Activity的生命週期函數同步。
class BaseActivity:AppCompatActivity(){
override fun onResume() {
super.onResume()
viewModel.onResume()
}
}
class BaseViewModel:ViewModel(){
fun onResume(){
}
}
但是,Google的Jetpack全家桶還給了我們另一種更爲簡便的實現,直接實現LifecycleObserver接口
interface ViewModelLifecycle:LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate()
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause()
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop()
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy()
}
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle {
private lateinit var lifcycleOwner: LifecycleOwner
override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
this.lifcycleOwner = owner
}
override fun onCreate() {
}
override fun onStart() {
}
override fun onResume() {
}
override fun onPause() {
}
override fun onStop() {
}
override fun onDestroy() {
}
}
至此,ViewModel就擁有了自己的Lifecycle,而其實我們的Activity/Frament也是默認實現了LifecycleOwner接口的,我們只需要在Activity/Fragment中如下操作即可使ViewModel的生命週期函數與Activity/Fragment生命週期函數同步:
// Activty/Fragment中的內部方法
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
// 將實現了LifecycleObserver接口的ViewModol實例作爲觀察者,添加到Activity/Fragment的生命週期觀察者隊列中
// 可以在實例化ViewModel後,即刻調用這個方法
getLifecycle().addObserver(viewModel)
// 將ViewModel從Activity/Fragment的生命週期觀察者隊列中移除
// 一般可以在onDestory()方法中調用這個方法
getLifecycle().removeObserver(viewModel)
三、爲ViewModel添加一些常用事件
我們在開發中經常會遇到這樣的一些場景:
- 網絡請求數據時,發生錯誤,彈出toast提示
- 網絡請求數據後,數據源爲空,顯示無數據視圖
- 網絡請求數據時,彈出Loading視圖,在網絡請求結束後關閉
…
對於上述提到的類似操作,我們可以事先在ViewModel中埋下用於通信使用的對應的LiveData,利用LiveData的觀察者機制和在頁面處於非活躍狀態下不會通知UI更新的特性,便捷的通知Activity/Fragment作出對應的UI處理,並且完美的避過內存泄漏。
- 常用的UI操作接口
interface ViewBehavior {
/**
* 是否顯示Loading視圖
*/
fun showLoadingUI(isShow: Boolean)
/**
* 是否顯示空白視圖
*/
fun showEmptyUI(isShow: Boolean)
/**
* 彈出Toast提示
*/
fun showToast(map: Map<String, *>)
/**
* 不帶參數的頁面跳轉
*/
fun navigateTo(page: Any)
/**
* 返回鍵點擊
*/
fun backPress(arg: Any?);
/**
* 關閉頁面
*/
fun finishPage(arg: Any?)
}
- ViewModel中添加事件LiveData
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
// loading視圖顯示Event
var _loadingEvent = MutableLiveData<Boolean>()
private set
// 無數據視圖顯示Event
var _emptyPageEvent = MutableLiveData<Boolean>()
private set
// toast提示Event
var _toastEvent = MutableLiveData<Map<String, *>>()
private set
// 不帶參數的頁面跳轉Event
var _pageNavigationEvent = MutableLiveData<Any>()
private set
// 點擊系統返回鍵Event
var _backPressEvent = MutableLiveData<Any?>()
private set
// 關閉頁面Event
var _finishPageEvent = MutableLiveData<Any?>()
private set
lateinit var application: Application
private lateinit var lifcycleOwner: LifecycleOwner
override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
this.lifcycleOwner = owner
}
override fun onCreate() {
}
override fun onStart() {
}
override fun onResume() {
}
override fun onPause() {
}
override fun onStop() {
}
override fun onDestroy() {
}
override fun showLoadingUI(isShow: Boolean) {
_loadingEvent.postValue(isShow)
}
override fun showEmptyUI(isShow: Boolean) {
_emptyPageEvent.postValue(isShow)
}
override fun showToast(map: Map<String, *>) {
_toastEvent.postValue(map)
}
override fun navigateTo(page: Any) {
_pageNavigationEvent.postValue(page)
}
override fun backPress(arg: Any?) {
_backPressEvent.postValue(arg)
}
override fun finishPage(arg: Any?) {
_finishPageEvent.postValue(arg)
}
protected fun showToast(str: String) {
showToast(str, null)
}
protected fun showToast(str: String, duration: Int?) {
val map = HashMap<String, Any>().apply {
put(
FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
)
put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
if (duration != null) {
put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
}
}
showToast(map)
}
protected fun showToast(@StringRes resId: Int) {
showToast(resId, null)
}
protected fun showToast(@StringRes resId: Int, duration: Int?) {
val map = HashMap<String, Any>().apply {
put(
FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
)
put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
if (duration != null) {
put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
}
}
showToast(map)
}
protected fun backPress() {
backPress(null)
}
protected fun finishPage() {
finishPage(null)
}
}
- Activity中對ViewModel中的事件進行處理
abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
ViewBehavior {
protected lateinit var viewModel: VM
protected fun injectViewModel() {
val vm = createViewModel()
viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
.get(vm::class.java)
viewModel.application = application
lifecycle.addObserver(viewModel)
}
override fun init(savedInstanceState: Bundle?) {
injectViewModel()
initialize(savedInstanceState)
initInternalObserver()
}
fun getActivityViewModel(): VM {
return viewModel
}
override fun onDestroy() {
super.onDestroy()
binding.unbind()
lifecycle.removeObserver(viewModel)
}
protected fun initInternalObserver() {
viewModel._loadingEvent.observeNonNull(this, {
showLoadingUI(it)
})
viewModel._emptyPageEvent.observeNonNull(this, {
showEmptyUI(it)
})
viewModel._toastEvent.observeNonNull(this, {
showToast(it)
})
viewModel._pageNavigationEvent.observeNonNull(this, {
navigateTo(it)
})
viewModel._backPressEvent.observeNullable(this, {
backPress(it)
})
viewModel._finishPageEvent.observeNullable(this, {
finishPage(it)
})
}
protected abstract fun createViewModel(): VM
/**
* 初始化操作
*/
protected abstract fun initialize(savedInstanceState: Bundle?)
}
四、爲ViewModel添加Application
在ViewModel中,我們可能會使用到Context,但是上述我們瞭解到,在ViewModel中並適合持有Activity/Fragment的引用,所以Google爲我們提供了一個AndroidViewModel,源碼很簡單,如下:
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
//noinspection unchecked
return (T) mApplication;
}
}
上面的AndroidViewModel僅僅是在ViewMdoel中添加了一個Application的引用,我們也完全可以自己實現這一步,讓我們的封裝更靈活
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
......
@SuppressLint("StaticFieldLeak")
lateinit var application: Application
......
}
abstract class BaseBVMActivity<B : ViewDataBinding, VM : BaseViewModel> : BaseBindingActivity<B>(),
ViewBehavior {
protected lateinit var viewModel: VM
protected fun injectViewModel() {
val vm = createViewModel()
viewModel = ViewModelProvider(this, BaseViewModel.createViewModelFactory(vm))
.get(vm::class.java)
viewModel.application = application
lifecycle.addObserver(viewModel)
}
}
五、使用工廠模式創建ViewModel
ViewModel的創建並不止一種形式,我們常用的創建方式,如下
val viewModel = ViewModelProvider(this).get(vm::class.java)
使用以上的方式創建ViewModel基本可以滿足我們一般的需求,但是假如我們需要在實例化ViewModel的時候傳入參數,那麼我們就必須使用工廠方式來創建ViewModel
val vm = LoginViewModel(loginRepository)
viewModel = ViewModelProvider(this,BaseViewModel.createViewModelFactory(vm))
.get(vm:class.java)
六、ViewModel封裝的完整代碼
/**
* @author: Albert Li
* @contact: [email protected]
* @time: 2020/6/7 10:27 PM
* @description: ViewModel感知生命週期接口
* @since: 1.0.0
*/
interface ViewModelLifecycle:LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
fun onAny(owner: LifecycleOwner, event: Lifecycle.Event)
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate()
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause()
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop()
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy()
}
/**
* @author: Albert Li
* @contact: [email protected]
* @time: 2020/6/9 8:01 PM
* @description: 頁面的常用操作
* @since: 1.0.0
*/
interface ViewBehavior {
/**
* 是否顯示Loading視圖
*/
fun showLoadingUI(isShow: Boolean)
/**
* 是否顯示空白視圖
*/
fun showEmptyUI(isShow: Boolean)
/**
* 彈出Toast提示
*/
fun showToast(map: Map<String, *>)
/**
* 不帶參數的頁面跳轉
*/
fun navigateTo(page: Any)
/**
* 返回鍵點擊
*/
fun backPress(arg: Any?);
/**
* 關閉頁面
*/
fun finishPage(arg: Any?)
}
/**
* @author: Albert Li
* @contact: [email protected]
* @time: 2020/6/7 10:30 PM
* @description: ViewModel的基類
* @since: 1.0.0
*/
abstract class BaseViewModel : ViewModel(), ViewModelLifecycle, ViewBehavior {
// loading視圖顯示Event
var _loadingEvent = MutableLiveData<Boolean>()
private set
// 無數據視圖顯示Event
var _emptyPageEvent = MutableLiveData<Boolean>()
private set
// toast提示Event
var _toastEvent = MutableLiveData<Map<String, *>>()
private set
// 不帶參數的頁面跳轉Event
var _pageNavigationEvent = MutableLiveData<Any>()
private set
// 點擊系統返回鍵Event
var _backPressEvent = MutableLiveData<Any?>()
private set
// 關閉頁面Event
var _finishPageEvent = MutableLiveData<Any?>()
private set
@SuppressLint("StaticFieldLeak")
lateinit var application: Application
private lateinit var lifcycleOwner: LifecycleOwner
override fun onAny(owner: LifecycleOwner, event: Lifecycle.Event) {
this.lifcycleOwner = owner
}
override fun onCreate() {
}
override fun onStart() {
}
override fun onResume() {
}
override fun onPause() {
}
override fun onStop() {
}
override fun onDestroy() {
}
override fun showLoadingUI(isShow: Boolean) {
_loadingEvent.postValue(isShow)
}
override fun showEmptyUI(isShow: Boolean) {
_emptyPageEvent.postValue(isShow)
}
override fun showToast(map: Map<String, *>) {
_toastEvent.postValue(map)
}
override fun navigateTo(page: Any) {
_pageNavigationEvent.postValue(page)
}
override fun backPress(arg: Any?) {
_backPressEvent.postValue(arg)
}
override fun finishPage(arg: Any?) {
_finishPageEvent.postValue(arg)
}
protected fun showToast(str: String) {
showToast(str, null)
}
protected fun showToast(str: String, duration: Int?) {
val map = HashMap<String, Any>().apply {
put(
FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_STR
)
put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, str)
if (duration != null) {
put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
}
}
showToast(map)
}
protected fun showToast(@StringRes resId: Int) {
showToast(resId, null)
}
protected fun showToast(@StringRes resId: Int, duration: Int?) {
val map = HashMap<String, Any>().apply {
put(
FlyBaseConstants.FLY_TOAST_KEY_CONTENT_TYPE,
FlyBaseConstants.FLY_TOAST_CONTENT_TYPE_RESID
)
put(FlyBaseConstants.FLY_TOAST_KEY_CONTENT, resId)
if (duration != null) {
put(FlyBaseConstants.FLY_TOAST_KEY_DURATION, duration)
}
}
showToast(map)
}
protected fun backPress() {
backPress(null)
}
protected fun finishPage() {
finishPage(null)
}
companion object {
@JvmStatic
fun <T : BaseViewModel> createViewModelFactory(viewModel: T): ViewModelProvider.Factory {
return ViewModelFactory(viewModel)
}
}
}
/**
* 創建ViewModel的工廠,以此方法創建的ViewModel,可在構造函數中傳參
*/
class ViewModelFactory(val viewModel: BaseViewModel) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return viewModel as T
}
}
Github傳送門
https://github.com/albert-lii/Fly-Android
今後文章將同步更新在作者微信公衆號中,歡迎關注