谈到 MVVM 架构,不得不祭出官方的架构图,架构图能帮助我们更好地理解,如下所示:
MVVM 和 MVP 的区别
MVP 中 V 层和 P 层互相持有对方的引用,在V 层调用 P 层逻辑后,P 层回调V 层的相应方法更新 UI。
而在 MVVM 中,上层只依赖直接下层,不能跨层持有引用,那 View 层调用 ViewModel 处理数据后,又如何更新自己呢?
答案就在 ViewModel 中的 LiveData,这是一种可观察的数据类型,在 View 层中观察者 Observer 对需要的数据进行订阅,当数据发生变化后,观察者 Observer 的回调方法 onChanged() 中会收到新的数据,从而可以更新 UI。
Jetpack 是 Google 为我们提供的架构组件,对于这些组件,我有以下理解和使用心得:
DataBinding
适用于数据繁杂的页面,可以减少大量 java 代码。xml 中可以做出类似于VUE 那样的数据双向绑定。同时可以直接调用控件id名称直接写UI逻辑代码不用写findviewById,也不用通过butterknife来注解实现findviewById的节省。
ViewModel
管理 Activity 或 Fragment 的数据
创建于Activity 或 Fragment 内,页面被销毁前,ViewModel 会一直存在
如果因配置变化导致页面销毁,ViewModel 不会销毁,它会被用于新的页面实例
一般在 ViewModel 中配合 LiveData 使用
一般用 ViewModelProviders 获取 ViewModelProvider,再用它的 get() 方法获取 ViewModel
在 get() 方法中会调用 Factory 的 create() 方法创建 ViewModel
Lifecycles
管理你的 Activity 和 Fragment 生命周期。
不用再通过写回调监听来感知Activity 和 Fragment 生命周期的变化来做相应的处理动作
LiveData
基于生命周期的消息订阅组件,不会发生内存泄漏,不用反注册。
下面根据我的开源项目 Jetpack_MVVM进一步讲解 MVVM 架构的运用,以下所有代码均来自于该项目
专门针对MVVM模式打造的BaseActivity、BaseFragment、BaseViewModel,在View层中不再需要定义ViewDataBinding和ViewModel,直接在BaseActivity、BaseFragment上限定泛型即可使用。
public abstract class BaseActivity extends RxAppCompatActivity implements LifecycleProvider {
protected V binding;
protected VM viewModel;
/**
* 注入绑定
*/
private void initViewDataBinding(Bundle savedInstanceState) {
//DataBindingUtil类需要在project的build中配置 dataBinding {enabled true }, 同步后会自动关联android.databinding包
binding = DataBindingUtil.setContentView(this, initContentView(savedInstanceState));
viewModelId = initVariableId();
viewModel = initViewModel();
if (viewModel == null) {
Class modelClass;
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1];
} else {
//如果没有指定泛型参数,则默认使用BaseViewModel
modelClass = BaseViewModel.class;
}
viewModel = (VM) createViewModel(modelClass);
}
...........................
}
/**
* 创建ViewModel
*
* @param
* @return
*/
protected T createViewModel(@NonNull Class modelClass) {
if (mActivityProvider == null) {
mActivityProvider = new ViewModelProvider(this);
}
return mActivityProvider.get(modelClass);
}
...............
}
retrofit+okhttp+rxJava负责网络请求;gson负责解析json数据;rxlifecycle负责管理view的生命周期;与网络请求共存亡;
RetrofitClient
NetWorkManager
这两个类分别封装了Retrofit的相关配置和具体的使用方法。
RetrofitClient 中对相关参数进行相关初始化配置,拦截配置,以及参数加密配置。
public class RetrofitClient {
//超时时间
private static final int DEFAULT_TIME_OUT = 10;//超时时间 70s
private static final int DEFAULT_READ_TIME_OUT = 10;
//缓存时间
private static final int CACHE_TIMEOUT = 10 * 1024 * 1024;
//服务端根路径
public static String baseUrl = "http://rap2.taobao.org:38080/app/mock/259908/";
private static Context mContext = App.getContext();
private static OkHttpClient okHttpClient;
private static Retrofit retrofit;
private Cache cache = null;
private File httpCacheDirectory;
private static class SingletonHolder {
private static RetrofitClient INSTANCE = new RetrofitClient();
}
public static RetrofitClient getInstance() {
return SingletonHolder.INSTANCE;
}
private RetrofitClient() {
this(baseUrl, null);
}
private RetrofitClient(String url, Map headers) {
if (TextUtils.isEmpty(url)) {
url = baseUrl;
}
if (httpCacheDirectory == null) {
httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
}
try {
if (cache == null) {
cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);
}
} catch (Exception e) {
LogUtils.e("Could not create http cache", e);
}
HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory();
okHttpClient = new OkHttpClient.Builder()
.cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))
.cache(cache)
.addInterceptor(new BaseInterceptor(headers))
.addInterceptor(new CacheInterceptor(mContext))
.addInterceptor(commonHeader())
.sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)
.addInterceptor(new LoggingInterceptor
.Builder()//构建者模式
.loggable(BuildConfig.DEBUG) //是否开启日志打印
.setLevel(Level.BASIC) //打印的等级
.log(Platform.INFO) // 打印类型
.request("Request") // request的Tag
.response("Response")// Response的Tag
.addHeader("log-header", "I am the log request header.") // 添加打印头, 注意 key 和 value 都不能是中文
.build()
)
.connectTimeout(DEFAULT_TIME_OUT, TimeUnit.SECONDS)
.readTimeout(DEFAULT_READ_TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(DEFAULT_READ_TIME_OUT, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(8, 60, TimeUnit.SECONDS))
// 这里你可以根据自己的机型设置同时连接的个数和时间,我这里8个,和每个保持时间为10s
.build();
retrofit = new Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(url)
.build();
}
/**
* create you ApiService
* Create an implementation of the API endpoints defined by the {@code service} interface.
*/
public T create(final Class service) {
if (service == null) {
throw new RuntimeException("Api service is null!");
}
return retrofit.create(service);
}
/**
* 利用拦截器配置请求头
*
* @return
*/
private Interceptor commonHeader() {
Interceptor commonInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder requestBuilder = request.newBuilder();
requestBuilder.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("version", BuildConfig.VERSION_NAME)
.addHeader("osType", DeviceUtils.getSDKVersionCode() + "")
.addHeader("DeviceType", DeviceUtils.getModel())
.addHeader("udid",DeviceUtils.getUniqueDeviceId())
.build();
Map signParams = new HashMap<>(); // 假设你的项目需要对参数进行签名
RequestBody originalRequestBody = request.body();
if(originalRequestBody!=null){
if (originalRequestBody instanceof FormBody) { // 传统表单
FormBody requestBody = (FormBody) request.body();
int fieldSize = requestBody == null ? 0 : requestBody.size();
for (int i = 0; i < fieldSize; i++) {
signParams.put(requestBody.name(i), requestBody.value(i));
}
UserBean loginBean = App.getInstance().getUserBean();
if(loginBean!=null){
String sign = getSign(signParams, loginBean.getToken());
requestBuilder.addHeader("sign", sign);
requestBuilder.addHeader("uid",loginBean.getUid());
requestBuilder.addHeader("username",loginBean.getUsername());
requestBuilder.addHeader("token",loginBean.getToken());
}
request = requestBuilder.build();
}
}
return chain.proceed(request);
}
};
return commonInterceptor;
}
//Sign加密
public static String getSign(Map params, String phone) {
List list = new ArrayList();
for (Map.Entry entry : params.entrySet()) {
if (entry.getValue() != null) {
Object object = entry.getValue();
// list.add(entry.getKey() + "=" + object + "&");
if (object instanceof HashMap) {
String valueStr = JSON.toJSONString(object);
Log.i("object", valueStr);
list.add(entry.getKey() + "=" + valueStr + "&");
} else if (object instanceof ArrayList) {
String valueStr = JSON.toJSONString(object);
Log.i("object", valueStr);
list.add(entry.getKey() + "=" + valueStr + "&");
} else {
list.add(entry.getKey() + "=" + object + "&");
}
}
}
int size = list.size();
String[] arrayToSort = list.toArray(new String[size]);
Arrays.sort(arrayToSort, String.CASE_INSENSITIVE_ORDER);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(arrayToSort[i]);
}
String result = sb.toString();
result += "key=" + phone;
result = EncryptUtils.encryptMD5ToString(result).toUpperCase();
return result;
}
}
NetWorkManager 针对网络框架进行了封装,便于快速使用网络请求。同时也做了相关生命周期的绑定工作,封装了手动解除网络请求的方法、showDialog 的相关封装以及跟UI 回调显示绑定工作。
public class NetWorkManager {
private WeakReference lifecycle;
private static HashMap disposableHashMap;
private UIChangeLiveData uc;
private static class NetWorkManagerHolder {
private static final NetWorkManager INSTANCE = new NetWorkManager();
}
public static final NetWorkManager getInstance() {
if(disposableHashMap==null){
disposableHashMap=new HashMap<>();
}
return NetWorkManagerHolder.INSTANCE;
}
public void execute(Observable observable,DisposableObserver subscriber) {
observable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.compose(RxUtils.handleGlobalError(App.getContext()))
.observeOn(AndroidSchedulers.mainThread())
//绑定生命周期,生命周期结束的时候解除网络订阅
.compose(RxUtils.bindToLifecycle(lifecycle.get()))
.subscribe(subscriber);
}
/**
* 手动添加 disposable 绑定tag
* @param disposable
* @param tag
*/
private void addSubscribe(Disposable disposable,String tag) {
CompositeDisposable mCompositeDisposable = getCompositeDisposable(tag);
mCompositeDisposable.add(disposable);
}
private CompositeDisposable getCompositeDisposable(String tag){
CompositeDisposable mCompositeDisposable = disposableHashMap.get(tag);
if (mCompositeDisposable == null) {
mCompositeDisposable = new CompositeDisposable();
disposableHashMap.put(tag,mCompositeDisposable);
}
return mCompositeDisposable;
}
/**
* 取消请求
*
* @param tag
*/
public void reMoveRequest(Object tag) {
if(disposableHashMap.get(tag)!=null){
disposableHashMap.get(tag).clear();
}
}
public final class UIChangeLiveData {
private SingleLiveEvent showDialogEvent;
private SingleLiveEvent dismissDialogEvent;
private SingleLiveEvent
我们以一个登录页面为例,首先当然是xml 文件:
布局使用的是谷歌最新的布局方式约束布局,有的人可能觉得约束布局拖动起来布局比较爽,但是实际使用中针对布局做一些小小的改动时确发现有一堆属性,感觉比较麻烦,其实不然,只要熟悉其中的用法和原理,我们可以像线性布局和相对布局一样进行手写xml 进行布局。这里推荐两篇帖子,大家进行参考,一个是视频,【阿里P7进阶学习】使用 ConstraintLayout 构建自适应界面。一个是文档,约束布局ConstraintLayout看这一篇就够了。
总的来说布局比较简单一个登录按钮,两个输入框。输入框分别做了数据双向绑定。布局嵌套 layout 标签后,编译器会自动生成 相关的databinding 类, 例如 activity_login.xml,会自动生成 ActivityLoginBinding 这个类。
下面我们来聊聊具体的ViewModel、DataBinding、LiveData 是如何具体的完成这一整套数据绑定和显示的过程的
LoginActivity, 由于我们封装了基类,所以只要 继承 BaseActivity 就可以直接获取DataBinding 和 ViewModel 的具体对象。 DataBinding 具体是通过DataBindingUtil 这个官方的工具类来获取的,ViewModel 是通过ViewModelProvider 来创建的。这个在基类封装里面已经做了介绍。
public class LoginActivity extends BaseActivity {
@Override
public int initContentView(Bundle savedInstanceState) {
return R.layout.activity_login;
}
@Override
public int initVariableId() {
return BR.vm;
}
@Override
protected void initData() {
binding.btLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.requestLogin(getClass().getSimpleName());
}
});
}
@Override
public void initViewObservable() {
String account = SPUtils.getInstance().getString("account");
String password = SPUtils.getInstance().getString("password");
viewModel.userName.set(account);
viewModel.userPassword.set(password);
viewModel.getUserBeanObservable().observe(this, new Observer>() {
@Override
public void onChanged(Result userBeanResult) {
if(userBeanResult.getReturnCode()==200){
ToastUtils.showLong("登录成功");
App.getInstance().saveUserBean(userBeanResult.data);
SPUtils.getInstance().put("account",viewModel.userName.get());
SPUtils.getInstance().put("password",viewModel.userPassword.get());
Intent intent = new Intent(LoginActivity.this,MainActivity.class);
startActivity(intent);
finish();
}else {
ToastUtils.showLong("登录失败");
}
}
});
}
}
DataBinding 首先我们使用DataBinding 需要在 app gradle文件中添加
buildFeatures {
dataBinding = true
}
这里要注意一下,之前的写法已经过时,
dataBinding {
enabled true
}
其次就是我们在xml 中使用 layout 标签来包裹 xml 布局,这样我们就可以直接在Activity 中直接使用控件Id来进行Ui 逻辑编写,这里可以回头去看看 LoginActivity 的代码。
@Override
protected void initData() {
binding.btLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.requestLogin(getClass().getSimpleName());
}
});
}
ViewModel和LiveData
ViewModel 中 定义一个LiveData对象 ,用来包装需要获取的登录实体类信息UserBean。 然后调取数据仓库请求登录的方法,将 LiveData 对象扔过去,当接口请求成功的时候对LiveData 进行赋值,然后LoginActivity 感知了LiveData 的数据变化,就能获取到了从网络请求下来的登录信息,然后进行登录相关操作。
LoginViewModel 代码:
public class LoginViewModel extends BaseViewModel {
//用户名的绑定
public ObservableField userName = new ObservableField<>("");
public ObservableField userPassword = new ObservableField<>("");
private MutableLiveData> userBeanObservable;
public LoginViewModel(@NonNull Application application) {
super(application);
}
public MutableLiveData> getUserBeanObservable(){
userBeanObservable = new MutableLiveData>();
return userBeanObservable;
}
public void requestLogin(){
Map param = new HashMap();
param.put("username",userName.get());
param.put("password",userPassword.get());
DataRepository.getInstance().login(param,userBeanObservable);
}
}
DataRepository 代码:
public class DataRepository implements ILocalRequest, IRemoteRequest {
public ApiService apiService = RetrofitClient.getInstance().create(ApiService.class);
private static final DataRepository S_REQUEST_MANAGER = new DataRepository();
private MutableLiveData responseCodeLiveData;
private DataRepository() {
}
public static DataRepository getInstance() {
return S_REQUEST_MANAGER;
}
............................
/**
* @param liveData
*/
public void login(Map param,MutableLiveData> liveData) {
NetWorkManager.getInstance().showDialog();
NetWorkManager.getInstance().execute(apiService.loginApi(param), new DisposableObserver>() {
@Override
public void onNext(Result t) {
NetWorkManager.getInstance().dismissDialog();
liveData.postValue(t);
}
@Override
public void onError(Throwable e) {
NetWorkManager.getInstance().dismissDialog();
}
@Override
public void onComplete() {
NetWorkManager.getInstance().dismissDialog();
}
});
}
...........................
}
具体流程如下: