Android MVVM 开发新姿势 一
1. 前言
网上有不少关于Android架构的讨论,如MVC, MVP,MVVM,最近还有MVI,emmm…不得不感慨时代变化太快。MVVM出来也有一段很长的时间了,接触时间不长,写一篇文章记录一下。同时也作为一个思路,抛砖引玉。
2. MVVM概略
首先,要了解一个架构思想那么首先要看一下相关文章,其中又以谷歌为佳,下面就是MVVM(Model-View-ViewModel)结构的架构图。
相关链接:https://developer.android.google.cn/jetpack
简单介绍一下各个模块:
View层 绿色框中的Activity/Fragment,继承至LifecycleActivity\LifecycleFragment,是UI控件的宿主。
核心职责是:
更新UI控件显示,包括状态及数据,由ViewModel驱动
监听UI事件及其生命周期,驱动ViewModel
View层不直接处理任何业务逻辑及数据加工。尽量做到瘦身,代码逻辑简约,减轻UI线程负担。
ViewModel层 蓝色框的ViewModel。只做业务逻辑操作,不持有任何UI控件的引用。那数据的更新如何通知到View层,这就要仰仗LiveData,具体使用后面会提及。
Model层 橘黄色框的Repository及其下都是Model层。Model层就是数据层。数据来源有:
本地存储数据,如数据库,文件,SharedPreferences(本质也是文件)
内存的缓存或临时数据
通过各种网络协议获取的远程数据
Repository是数据仓库,整合各路来源的数据,再统一暴露给ViewModel层使用。官方新框架在这一层只提供给了一个新的SQLite数据库封装类库Room,这是可选的。换言之,Model层,官方只提供指导思想,并未有具体实现方案。也就是可以自由发挥,各种嗨,Emm
各个层次的依赖如图:
3. 框架搭建
首先解决一下依赖最底层model的问题。
一般而言,model一般用于从数据源获取数据。当然也可以从本地获取数据
数据源一般就是后台服务器。
我们抽象一下,抽出来就是一个抽象类。
public abstract class BaseModel {
public void getData(int code, BaseSubscriber subscriber , Object... parameter){
//TODO 调用网络请求前的操作
requestInNet(code, subscriber ,parameter);
}
/**
* 网络请求
* @param code 请求对应的code用于区分不同的请求
* @param subscriber 回调
* @param parameter 可变参数列表 存放请求所需参数
*/
protected abstract void requestInNet(int code,BaseSubscriber subscriber ,Object... parameter);
}
接下来需要思考一个问题,下层不存在上层依赖,那么viewmodel和model之间该怎么通讯呢?思索一番,决定用rxjava的回调。Emmm,那么肯定要根据需求改造一番。代码如下:
public abstract class BaseSubscriber<T> extends ResourceSubscriber<T> {
protected BaseViewModel vm;
public BaseSubscriber(BaseViewModel vm) {
this(vm, null);
}
public BaseSubscriber(BaseViewModel vm, @Nullable String requestKey) {
this.vm = vm;
this.vm.subscribe(this, requestKey);
}
@Override
public void onStart() {
super.onStart();
//TODO 此处可以判断网络,无网络调用onStartOffline
onStartRequest();
}
@Override
public void onNext(T t) {
onSuccess(t);
}
/**
* 请求失败
* 可重写复用
*/
@Override
public void onError(Throwable t) {
vm.onError(t);
}
/**
* 完成时回调
*/
@Override
public abstract void onComplete();
/**
* 当开始请求时没有网络
* 可重写复用
*/
public void onStartOffline(){
}
/**
* 当开始请求时时
* 可重写复用
*/
public void onStartRequest(){
}
/**
* 当成功返回时
* @param t 返回的数据
*/
public abstract void onSuccess(T t);
}
回调改造完成,那么我们就写一个viewmodel。
public abstract class BaseViewModel<M extends BaseModel> extends AndroidViewModel {
/* RX相关注册 */
/**
* 一次性容器,可以容纳多个其他一次性用品和提供I/O添加和移除复杂性。
*/
private CompositeDisposable mCompositeDisposable;
private ArrayMap<String, Disposable> mDisposableMap;
protected M model;
protected final MutableLiveData<Intent> startIntent = new MutableLiveData<>();
protected final MutableLiveData<ArrayList<Object>> messageToContext = new MutableLiveData<>();
public BaseViewModel(Application application){
super(application);
model = setModel();
}
/**
* 设置model
* @return model
*/
protected abstract M setModel();
protected void getData(int code , BaseSubscriber subscriber , Object... param){
if (model == null){
throw new NullPointerException("进行获取数据的操作前,检查Model是否赋值");
}
model.getData(code,subscriber,param);
}
protected void sendMessageToContext(ArrayList<Object> params){
messageToContext.setValue(params);
}
/**
* 注册RX可处理的线程
* @param disposable 可处理的线程
* @param requestKey 请求键
*/
public void subscribe(Disposable disposable, @Nullable String requestKey) {
if (null == mCompositeDisposable) {
mCompositeDisposable = new CompositeDisposable();
}
mCompositeDisposable.add(disposable);
if (null == mDisposableMap){
mDisposableMap = new ArrayMap<>();
}
if (!TextUtils.isEmpty(requestKey)){
mDisposableMap.put(requestKey, disposable);
}
}
/**
* 注销所有线程
*/
public void unsubscribe() {
if (null != mCompositeDisposable) {
mCompositeDisposable.clear();
}
}
/**
* 移除线程
* @param requestKey 请求键
*/
public void removeDisposable(@NotNull String requestKey) {
if (null != mCompositeDisposable && null != mDisposableMap
&& mDisposableMap.containsKey(requestKey)) {
mCompositeDisposable.remove(mDisposableMap.get(requestKey));
mDisposableMap.remove(requestKey);
}
}
/**
* 请求时客户端错误处理
*/
@CallSuper
public void onError(Throwable throwable) {
throwable.printStackTrace();
// 当在主线程时显示异常Toast
if (Looper.getMainLooper() == Looper.myLooper()) {
//TODO 后面添加
}
}
}
最后搞定activity
public abstract class BaseMvvmActivity <VM extends BaseViewModel,VDB extends
ViewDataBinding> extends AppCompatActivity {
protected VDB binding;
protected VM viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = setViewModel();
doBeforeBinding();
onBinding();
initUI();
setDefaultObservers();
}
protected void setDefaultObservers() {
}
/**
* 设置布局文件id
* @return layout id
*/
protected abstract int getLayoutId();
protected void doBeforeBinding(){}
protected void onBinding(){
binding = DataBindingUtil.setContentView(this, getLayoutId());
binding.setLifecycleOwner(this);
binding.setVariable(BR.viewModel,viewModel);
}
/**
* 设置ViewModel
* @return viewModel
*/
protected abstract VM setViewModel();
/**
* 初始化ui的方法
*/
protected abstract void initUI();
public void changeIntoLightStatusBar(boolean isLight){
if (isLight){
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}else{
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
}
}
4. 实现简单功能
网络加载一张图片&获取一段数据显示
首先图片加载库glide,
配置glide balabala…此处省略1000字
然后新建一个类,ViewUtils 名字随意呀
public class ViewUtils {
@BindingAdapter({"imageUrl"})
public static void loadImageOnTheInternet(final ImageView imageView, final String url) {
if (null != url && url.startsWith("http")) {
imageView.post(() ->
Glide.with(imageView.getContext()).load(url).into(imageView));
}
}
}
图片加载搞定了,下面是网络数据加载
同样省略1000字
public class NetworkClient {
private static Retrofit retrofit;
private static final int TIME_OUT = 60*5;
/**
* 私有化构造方法
*/
private NetworkClient(){
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(message -> LogUtil.i("HttpLog",message));
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient()
.newBuilder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(Constant.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
private static class SingletonHolder{
private static final NetworkClient INSTANCE = new NetworkClient();
}
public static NetworkClient getNetworkClient (){
return SingletonHolder.INSTANCE;
}
/**
* 获取api
* @param service
* @param <T>
* @return
*/
public <T> T createApi(Class<T> service){
return retrofit.create(service);
}
}
public interface UserApi {
/**
* 测试,获取公众号列表
* @return
*/
@GET("wxarticle/chapters/json")
Flowable<ResponseBean<ArrayList<PublicNumBean>>> wxarticle();
}
关键代码如上。
下面前期准备工作都搞定了,我们开始写布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<import type="java.lang.String"/>
<variable
name="viewModel"
type="com.tao.androidx_mvvm.viewmodel.ViewModelOfMain"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<Button
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.text}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="@{(view)->viewModel.onClick(view)}"/>
<ImageView
app:layout_constraintTop_toBottomOf="@id/text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:imageUrl="@{viewModel.text}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
一个button一个imageView搞定
ViewModel
public class ViewModelOfMain extends BaseViewModel<ModelOfMain> {
public final MutableLiveData<String> text = new MutableLiveData<>();
public ViewModelOfMain(Application application) {
super(application);
text.setValue("Hello");
}
public void getData(){
getData(TEXT, new BaseSubscriber(this) {
@Override
public void onComplete() {
}
@Override
public void onSuccess(Object o) {
LogUtil.i("ViewModelOfMain",o.toString());
text.setValue(o.toString());
}
@Override
public void onError(Throwable t) {
super.onError(t);
LogUtil.i("ViewModelOfMain",t.getMessage());
}
});
}
@Override
protected ModelOfMain setModel() {
return new ModelOfMain();
}
public void onClick(View v){
if (v.getId() == R.id.text){
getData();
}
}
}
Model:
public class ModelOfMain extends BaseModel {
public static final int TEXT = 1001;
@Override
protected void requestInNet(int code, BaseSubscriber subscriber, Object... parameter) {
switch (code) {
case TEXT:
LogUtil.i("ModelOfMain", "requestInNet");
NetworkClient.getNetworkClient().createApi(UserApi.class)
.wxarticle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
break;
default:
LogUtil.i("ModelOfMain", "default");
break;
}
}
}
Ativity:
class MainActivity : BaseMvvmActivity<ViewModelOfMain,ActivityMainBinding>() {
override fun setViewModel(): ViewModelOfMain {
return ViewModelOfMain(application)
}
override fun initUI() {
viewModel.text.value = "http://img5.imgtn.bdimg.com/it/u=3300305952,1328708913&fm=26&gp=0.jpg"
}
override fun getLayoutId(): Int {
return R.layout.activity_main
}
}
简单效果就如上面一样。
关于recyclerview,fragment,dialog的使用就下次再写。
github:https://github.com/huangtaoOO/AndroidXMVVM
接口:https://www.wanandroid.com/