前言
關於mvvm的出現已經很長一段時間了,但是博主一直沒有太過於關注,但是由於最近接觸的和新出的很多框架都是基於mvvm模式去開發的,於是花了點時間看了下。
關於學習mvvm前,可能需要首先了解databing,請自行百度了,介紹databing的博客一堆,由於道行不夠高深就不帶大家解讀源碼了。有了databing的加入你的項目再也不需要findViewById了,也不需要butterknife插件了,而且databing功能不僅如此,他還可以綁定事件、和數據轉換,在網上找來這張圖可以先了解下
那麼接下來開始我們的簡易封裝之旅
- BaseActivity封裝
- BaseFragment封裝
- BaseView統一接口方法封裝
- BaseViewModel封裝
- BaseException錯誤異常定義類
- BaseModelEntity接口返回數據統一處理類
- 封裝網絡請求框架,這裏後續會以Rxjava2+Retrofit 爲例
- 統一工具類封裝和api相關配置封裝
此篇文章我們先從BaseActivity、BaseFragment、BaseView、BaseViewModel講起
需要引入的庫有
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
//放着沒有及時回收造成RxJava內存泄漏
implementation 'com.trello.rxlifecycle2:rxlifecycle-components:2.2.2'
//Room的依賴引用
implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'
BaseActivity
一說到BaseActivity封裝那麼我們第一時間想到的就是不用重複findViewById和不用重複的setContentView,那麼我們可以定義一個抽象方法
/**
* 初始化佈局
* @return 佈局id
*/
protected abstract int getLayoutId();
當然我們還可以封裝統一的toolbar剩餘每個頁面都要引入toolbar,當然在引入toolbar之前肯定是先去掉原生的導航欄
<style name="AppBaseTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
<!--<item name="android:statusBarColor">@color/theme_backgroung_color</item>-->
<!--<item name="android:windowBackground">@color/window_background</item>-->
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/themeColor</item>
</style>
<style name="AppTheme" parent="AppBaseTheme"/>
然後我們在application節點下引入theme主題即可
這時我們可以嘗試引用一下databing的雙向綁定特性了,我們新建一個toolbar樣式類文件
/**
* Create by CherishTang on 2020/3/25 0025
* describe:toolbar配置
*/
public class ToolbarConfig extends BaseObservable {
private String title;
private @DrawableRes int backIconRes = R.mipmap.icon_fh_black;//toolbar返回按鈕資源樣式
private boolean defaultTheme = true;//toolbar的menu主題,默認主題爲黑色
private int textColor = R.color.black;//標題字體顏色
private int bgColor = R.color.white;//標題背景色
private boolean isShowBackButton = true;//是否顯示返回按鈕
public ToolbarConfig() {
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getBackIconRes() {
return backIconRes;
}
public void setBackIconRes(int backIconRes) {
this.backIconRes = backIconRes;
}
public boolean isDefaultTheme() {
return defaultTheme;
}
public void setDefaultTheme(boolean defaultTheme) {
this.defaultTheme = defaultTheme;
}
public int getTextColor() {
return textColor;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
}
public int getBgColor() {
return bgColor;
}
public void setBgColor(int bgColor) {
this.bgColor = bgColor;
}
public boolean isShowBackButton() {
return isShowBackButton;
}
public void setShowBackButton(boolean showBackButton) {
isShowBackButton = showBackButton;
}
}
然後新建一個base_activity.xml佈局文件,引入統一的toolbar
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="toolbarConfig"
type="com.jcloudsoft.demo.bean.base.ToolbarConfig" />
<import type="android.view.View" />
<import type="androidx.core.content.ContextCompat" />
<import type="androidx.annotation.LayoutRes" />
<import type="androidx.annotation.ColorRes" />
<variable
name="context"
type="android.content.Context" />
</data>
<LinearLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/main_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/y96"
android:background="@{ContextCompat.getColor(context,toolbarConfig.bgColor)}"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="@{ContextCompat.getDrawable(context,toolbarConfig.backIconRes)}"
android:theme="@style/ToolBarStyle_black">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLength="12"
android:maxLines="1"
android:text="@{toolbarConfig.title}"
android:textColor="@{ContextCompat.getColor(context,toolbarConfig.textColor)}"
android:textSize="@dimen/font_18"/>
</androidx.appcompat.widget.Toolbar>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>
關於上面的佈局文件大概會有幾個疑問:
1、context對象,在xml佈局中我們是無法回去context的對象,但是我們可以利用databing雙向綁定把this對象傳遞到xml佈局中,或者你可以直接自定義一個方法return相同的返回數據,效果一樣
bind.setVariable(BR.context, this);
2、樣式@style/ToolBarStyle_black的問題,如果不懂的可以看下自定義toolbar的博文,直接貼代碼,不做過多解釋了,如果你想把toolbar樣式更加炫酷,當然 你可以在此思路上增加更多的相關配置
<style name="ToolBarStyle_black" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="actionMenuTextColor">@android:color/black</item> <!-- menu 敲定顏色-->
<item name="android:textSize">@dimen/font_14</item> <!-- 搞掂字體大小-->
<item name="android:textStyle">normal</item>
<item name="colorControlNormal">@color/black</item><!--圖標顏色-->
<item name="overlapAnchor">false</item>
</style>
在activity中引入佈局,但是問題來了,如果我們部分頁面不需要toolbar怎麼辦?總不能setVisibility吧,這也太low了,我們可以先定義一個抽象方法,供子activity去控制toolbar的顯示與隱藏
/**
* 是否引用toolbar
* @return 默認顯示
*/
protected boolean hasToolBar() {
return true;
}
再定義一個標題文字方法
/**
* 設置toolbar的title
* @return 標題
*/
public abstract String setTitleBar();
然後我們如何去引用這些方法呢?當然是利用databing的雙向綁定特性啦,
//toolbarConfig是xml定義的name,setToolbarStyle()是我們剛纔新建的toolbar配置類
bind.setVariable(BR.toolbarConfig, setToolbarStyle())
當然我們需要暴露一個接口供子Activity去修改toolbar樣式
/**
* 設置toolbar默認樣式
*
* @return toolbar配置
*/
public ToolbarConfig setToolbarStyle() {
return new ToolBarSet().build();
}
自定義toolbar樣式,可以根據需求添加自己的方法
/**
* 自定義toolbar樣式類
*/
public class ToolBarSet {
private ToolbarConfig toolbarConfig;
public ToolBarSet() {
if (toolbarConfig == null) {
this.toolbarConfig = new ToolbarConfig();
}
toolbarConfig.setTitle(setTitleBar());
}
public ToolbarConfig getConfig(){
return toolbarConfig;
}
public ToolbarConfig build() {
return toolbarConfig;
}
public ToolBarSet setTitleTextColor(@ColorRes int colorRes) {
toolbarConfig.setTextColor(colorRes);
return this;
}
public ToolBarSet setBackIconRes(@DrawableRes int imgRes) {
toolbarConfig.setBackIconRes(imgRes);
return this;
}
public ToolBarSet setTitle(String title) {
toolbarConfig.setTitle(title);
return this;
}
public ToolBarSet setBgColor(@ColorRes int colorRes) {
toolbarConfig.setBgColor(colorRes);
return this;
}
public ToolBarSet setDefaultTheme(boolean defaultTheme) {
toolbarConfig.setDefaultTheme(defaultTheme);
return this;
}
public ToolBarSet setShowBackButton(boolean isShow) {
toolbarConfig.setShowBackButton(isShow);
return this;
}
}
那麼toolbar的自定義樣式綁定好了,可以我們引入和不引入toolbar的問題還沒有解決呢?在databing中綁定佈局文件寫法如下,
ViewDataBinding VDB = DataBindingUtil.setContentView(this, R.layout.base_activity)
那麼看這個,靈感是否有了呢,我們需要做到以下幾點
1、在UI頁面中我們綁定的佈局不能是base_activity,需要是我們getLayoutId()返回的佈局id
2、通過hasToolBar()方法去判斷toolbar的隱藏顯示
在databing中綁定佈局,與原來的有些區別,但是他們都是要走系統的setContentView方法的,那麼我們可以重寫系統的setContentView方法去實現不同的綁定關係
@Override
public void setContentView(int layoutResID) {
if (hasToolBar()) {//如果是引用toolbar佈局的話我們根佈局重寫一下,需要引入base_activity作爲根佈局文件,然後把各ui頁面的getLayoutId()定義的佈局資源添加到根佈局文件中去
super.setContentView(R.layout.base_activity);
FrameLayout container = findViewById(R.id.container);
mToolbar = findViewById(R.id.main_bar);
binding = DataBindingUtil.inflate(LayoutInflater.from(this), getLayoutId(), container, true);
mToolbar.setNavigationOnClickListener(v -> finish());
} else {//如果不需要toolbar的話,我們直接就以getLayoutId()的佈局資源id作爲根佈局
super.setContentView(layoutResID);
}
}
做好這個後我們就可以在onCreate中是去實現它啦
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (hasToolBar()) {
toolbarBind = DataBindingUtil.setContentView(this, R.layout.base_activity);//我們需要定義一個屬於toolbar的bind來控制toolbar的樣式
toolbarBind.setVariable(BR.context, this);//給view傳遞context對象
toolbarBind.setVariable(BR.toolbarConfig, setToolbarStyle());
} else {
binding = DataBindingUtil.setContentView(this, getLayoutId());
}
}
那麼到這裏我們的toolbar樣式就封裝好了,當然我們Activity封裝還沒有完,我們可以添加一些公用方法
/**
* 初始化佈局
*/
public abstract void initView();
/**
* 設置數據
*/
public abstract void initData(Bundle bundle);
但是說了半天我們還沒有引入ViewModel呢,別急!我們先介紹下fragment封裝,activity的源碼下面我會貼在文章中
BaseFragment
有了activity封裝的先例,我們fragment封裝就簡單多了,首先同樣的定義一些我們需要的公用方法
/**
* 該抽象方法就是 onCreateView中需要的layoutID
*
* @return
*/
protected abstract int getLayoutId();
/**
* 該抽象方法就是 初始化view
*
* @param view
* @param savedInstanceState
*/
protected abstract void initView(View view, Bundle savedInstanceState);
/**
* 執行數據的加載
*/
protected abstract void initData(Bundle bundle);
在fragment中綁定資源文件要與activity有些許區別
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container
, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
return binding.getRoot();
}
在fragment中綁定的View佈局我們後面可能還有用,所以我們定義一個View去接受一下它
protected View mContentView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container
, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
mContentView = binding.getRoot();
return mContentView;
}
然後去實現我們剛纔自定義的抽象方法
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initData(getArguments() == null ? new Bundle() : getArguments());
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container
, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
mContentView = binding.getRoot();
initView(mContentView, savedInstanceState);
return mContentView;
}
那麼fragment的簡易封裝要簡單很多啦。
BaseView
在封裝完了Activity和Fragment後發現少了很多公用方法,比如:Toast、加載框等等因爲這些方法是fragment和Activity共有的,我們可以定義一個接口,然後讓activity和fragment去實現他的方法體即可
public interface BaseView {
/**
* 顯示dialog
*/
void showLoading(String dialogMessage);
/**
* 更新dialog
*/
void refreshLoading(String dialogMessage);
/**
* 隱藏 dialog
*/
void hideLoading();
/**
* 顯示錯誤信息
*
* @param msg
*/
void showToast(String msg);
/**
* 錯誤碼
*/
void onErrorCode(BaseModelEntity model);
}
然後我們只需要在BaseActivity和BaseFragment中去實現他們的方法體即可
BaseViewModel
講了這麼多,要真正講到正主了ViewModel。
在說這個前,可能有人說要引入LifeCycle。我可以回答:不需要!!!!!在androidx中已經默認實現了LifeCycle的接口
如果不是特殊需求的話上面的
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
這個庫是可以不引入的,因爲google已經給你做好了
public class BaseViewModel extends AndroidViewModel {
public BaseViewModel(@NonNull Application application) {
super(application);
}
}
當然我們需要引入剛纔的接口baseView
public class BaseViewModel<V extends BaseView> extends AndroidViewModel {
protected V baseView;
public BaseViewModel(@NonNull Application application) {
super(application);
}
protected void setBaseView(V baseView) {
this.baseView = baseView;
}
public V getBaseView() {
return baseView;
}
}
這個時候我們還不知道在ViewModel中需要什麼,不要盲目的寫,用到自然就知道了,先不如我們viewModel就寫這麼多,我們現在需要做的就是在BaseActivity中引入BaseViewModel,當然我們不同的Activity會有不同的ViewModel,所以我們不如讓所有的ViewModel都繼承BaseViewModel,在BaseActivity中以泛型引入;
public abstract class BaseActivity<VM extends BaseViewModel, VDB extends ViewDataBinding> extends AppCompatActivity implements BaseView {
public Context context;
Toolbar mToolbar;
public VM mViewModel;
protected VDB binding;
private BaseActivityBinding toolbarBind;
private CustomProgressDialog customProgressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
context = this;
if (hasToolBar()) {
toolbarBind = DataBindingUtil.setContentView(this, R.layout.base_activity);
toolbarBind.setVariable(BR.context, this);//給view傳遞context對象
toolbarBind.setVariable(BR.toolbarConfig, setToolbarStyle());
} else {
binding = DataBindingUtil.setContentView(this, getLayoutId());
}
createViewModel();
initView();
initData((getIntent() == null || getIntent().getExtras() == null) ? new Bundle() : getIntent().getExtras());
}
private void closeLoadingDialog() {
if (customProgressDialog != null && customProgressDialog.isShowing()) {
customProgressDialog.dismiss();
}
}
private void showLoadingDialog(String dialogMessage) {
if (customProgressDialog == null || !customProgressDialog.isShowing()) {
customProgressDialog = new CustomProgressDialog(context);
customProgressDialog.isShowBg(StringUtil.isEmpty(dialogMessage));
customProgressDialog.setMessage(dialogMessage);
customProgressDialog.show();
}
}
@Override
public void showLoading(String dialogMessage) {
runOnUiThread(() -> showLoadingDialog(dialogMessage));
}
@Override
public void refreshLoading(String dialogMessage) {
runOnUiThread(() -> {
if (customProgressDialog != null && customProgressDialog.isShowing()) {
customProgressDialog.setMessage(dialogMessage);
}
});
}
@Override
public void hideLoading() {
runOnUiThread(this::closeLoadingDialog);
}
@Override
public void showToast(String msg) {
runOnUiThread(() -> {
if (StringUtil.isEmpty(msg)) return;
ToastUtils.showShort(this, msg);
});
}
@Override
public void onErrorCode(BaseModelEntity model) {
if (model != null && (UserAccountHelper.isLoginPast(model.getCode()) || UserAccountHelper.isNoPermission(model.getCode()))) {
LoginActivity.show(this, new Bundle());
}
}
}
我們現在對viewModel進行創建,如果你想給ViewModel添加構造方法可以通過他的工廠類去實現ViewModelProvider.Factory
/**
* 創建viewModel
*/
public void createViewModel() {
if (mViewModel == null) {
Class modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
//如果沒有指定泛型參數,則默認使用BaseViewModel
modelClass = BaseViewModel.class;
}
mViewModel = (VM) new ViewModelProvider(this).get(modelClass);
mViewModel.setBaseView(createBaseView());
}
}
在這裏我們setBaseView方法綁定activity與ViewModel
protected BaseView createBaseView() {
return this;
}
這裏的CustomProgressDialog.java可以自己去寫,一個全局加載彈框dialog
到這裏我們BaseActivity基本封裝好了,下面我們繼續在BaseFragment中去引用ViewModel;同樣的我們以泛型形式引入
public abstract class BaseFragment<VM extends BaseViewModel, VDB extends ViewDataBinding> extends Fragment{
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container
, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
mContentView = binding.getRoot();
createViewModel();
initView(mContentView, savedInstanceState);
return mContentView;
}
}
這裏創建ViewModel對象與activity中是一樣的
public void createViewModel() {
if (mViewModel == null) {
Class modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
//如果沒有指定泛型參數,則默認使用BaseViewModel
modelClass = BaseViewModel.class;
}
mViewModel = (VM) new ViewModelProvider(this).get(modelClass);
mViewModel.setBaseView(createBaseView());
}
}
protected BaseView createBaseView(){
return this;
}
然後我們在BaseFragment中去實現BaseView 接口類
public abstract class BaseFragment<VM extends BaseViewModel, VDB extends ViewDataBinding> extends Fragment implements BaseView {
protected Activity mActivity;
public CustomProgressDialog customProgressDialog;
protected VM mViewModel;
protected View mContentView;
protected VDB binding;
/**
* 獲得全局的,防止使用getActivity()爲空
*
* @param context
*/
@Override
public void onAttach(@NotNull Context context) {
super.onAttach(context);
this.mActivity = (Activity) context;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container
, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
mContentView = binding.getRoot();
createViewModel();
initView(mContentView, savedInstanceState);
return mContentView;
}
public void createViewModel() {
if (mViewModel == null) {
Class modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
//如果沒有指定泛型參數,則默認使用BaseViewModel
modelClass = BaseViewModel.class;
}
mViewModel = (VM) new ViewModelProvider(this).get(modelClass);
mViewModel.setBaseView(createBaseView());
}
}
protected BaseView createBaseView(){
return this;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initData(getArguments() == null ? new Bundle() : getArguments());
}
/**
* 該抽象方法就是 onCreateView中需要的layoutID
*
* @return
*/
protected abstract int getLayoutId();
/**
* 該抽象方法就是 初始化view
*
* @param view
* @param savedInstanceState
*/
protected abstract void initView(View view, Bundle savedInstanceState);
/**
* 執行數據的加載
*/
protected abstract void initData(Bundle bundle);
/**
* 關閉彈框
*/
private void closeLoadingDialog() {
if (customProgressDialog != null && customProgressDialog.isShowing()) {
customProgressDialog.dismiss();
}
}
/**
* 顯示加載彈框
*
* @param dialogMessage 彈框內容,如果內容爲空則不展示文字部分
*/
private void showLoadingDialog(String dialogMessage) {
if (customProgressDialog == null || !customProgressDialog.isShowing()) {
customProgressDialog = new CustomProgressDialog(getActivity());
customProgressDialog.isShowBg(StringUtil.isEmpty(dialogMessage));
customProgressDialog.setMessage(dialogMessage);
customProgressDialog.show();
}
}
@Override
public void showLoading(String dialogMessage) {
if (getActivity() != null && !getActivity().isFinishing()) {
getActivity().runOnUiThread(() -> showLoadingDialog(dialogMessage));
}
}
@Override
public void hideLoading() {
if (getActivity() != null && !getActivity().isFinishing()) {
getActivity().runOnUiThread(this::closeLoadingDialog);
}
}
@Override
public void refreshLoading(String dialogMessage) {
if (getActivity() != null && !getActivity().isFinishing()) {
getActivity().runOnUiThread(() -> {
if (customProgressDialog != null && customProgressDialog.isShowing()) {
customProgressDialog.setMessage(dialogMessage);
}
});
}
}
@Override
public void showToast(String msg) {
ToastUtils.showShort(getActivity(), msg);
}
@Override
public void onErrorCode(BaseModelEntity model) {
if (model != null && (UserAccountHelper.isLoginPast(model.getCode())||UserAccountHelper.isNoPermission(model.getCode()))) {
Bundle bundle = new Bundle();
LoginActivity.show(this, bundle);
}
}
}
這樣我們可以簡易的書寫代碼了
public class TestActivity extends BaseActivity<MainViewModel, BaseFragmentContainerBinding> {
@Override
protected int getLayoutId() {
return R.layout.base_fragment_container;
}
@Override
public String setTitleBar() {
return "測試";
}
@Override
public void initView() {
}
@Override
public void initData(Bundle bundle) {
}
}
fragment
public class TestFragment extends BaseFragment<TestViewModel, ActivityTestBinding> {
@Override
protected int getLayoutId() {
return R.layout.activity_test;
}
@Override
protected void initView(View view, Bundle savedInstanceState) {
}
@Override
protected void initData(Bundle bundle) {
binding.tvTest.setText("這是"+bundle.getInt(MainViewModel.ARG_PAGE)+"個頁面");
}
}
寫在最後,後續會上傳demo
這裏注意踩坑!!!如果在基類中引入其他泛型的話,需要把泛型寫在最後不能寫在ViewDataBinding、BaseViewModel前面,錯誤寫法
BaseActivity<T extends BeseBean,VM extends BaseViewModel, VDB extends ViewDataBinding>
正確寫法
BaseActivity<VM extends BaseViewModel, VDB extends ViewDataBinding,T extends BeseBean>
後續我們繼續封裝
- 統一異常錯誤處理BaseException;
- 網絡框架的封裝Rxjava2+Retrofit
- 接口數據統一數據
- application與相關api參數配置
- 工具類封裝
關於網絡請求和統一異常處理可以查看這篇博文關於mvvm簡易封裝(二)