一、mvvm是什么
引用度娘:MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑
m(Model):数据源,主要包括网络数据源和本地缓存数据源。
V(View):视图,主要是activity和Fragment,承担UI渲染和响应用户操作
VM(ViewModel):Model和View通信的桥梁,承担业务逻辑功能。
二、mvvm的优缺点
优点
1、在mvp模式中,View层和present层会互相依赖,耦合度很高,很容易出现内存泄漏,为了解决内存问题,需要注意回收内存,到处判空。mvvm中view单向依赖viewModel,降低了耦合度
2、livedata会随着页面的生命周期变化自动注销观察者,极大避免了页面结束导致的crash
3、极大的提高了扩展性和降低了维护难度
4、在规范的mvvm中,view层没有任何除view外的成员变量,更没有if,for,埋点等业务逻辑,代码非常简洁,可读性很高,很容易找到业务入口。
缺点
在规范的mvvm中,viewMode承担了太多业务,会导致viewModel,达到几千行甚至上万行。难以阅读,难以扩展,难以维护。
解决方案
1、多个viewModel
根据业务逻辑,拆分ViewModel为多个,但是会导致层次混乱,1对1变成1对多。
2、其他helper,util分担业务逻辑,减少viewmodel的负担。
推荐方案:mvvm_clean
参考:mvp_clean
实现:继续拆分viewModel层,分为viewModel和domain层
domain层:一个个独立的“任务”,主要使用命令模式把请求,返回结果封装了。这个任务可以到处使用,也实现责任链模式将复杂得业务简单化。井井有条。
步骤
1、在app中的build.gradle
添加ViewModel和LiveData依赖
implementation "android.arch.lifecycle:extensions:1.1.1"
annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
支持lambda表达式(lambda非常简单易用,可以简化代码,自行搜索)
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
2、命名模式实现
public abstract class UseCase {
public final static int CODE = -6;
private QmRequestValues;
private UseCaseCallback
mUseCaseCallback;
protected abstract void executeUseCase(Q value);
public QgetRequestValues() {
return this.mRequestValues;
}
public UseCaseCallback
getUseCaseCallback() {
return this.mUseCaseCallback;
}
void run() {
executeUseCase(this.mRequestValues);
}
public void setRequestValues(Q value) {
this.mRequestValues = value;
}
public void setUseCaseCallback(UseCaseCallback
useCaseCallback) {
this.mUseCaseCallback = useCaseCallback;
}
public interface RequestValues {
}
public interface ResponseValue {
}
public interface UseCaseCallback {
void onError(Integer code);
void onSuccess(R result);
}
}
关键就是这个类,本人改进了mvp_clean中不支持错误码的缺点,可以返回各种情况。
详细参考链接:
https://github.com/googlesamples/android-architecture/tree/todo-mvp-clean
2、view中:
BaseActivity:
public abstract class BaseVMActivity extends AppCompatActivity {
protected TmViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtil.i(getClass().getSimpleName(), "onCreate");
super.onCreate(savedInstanceState);
setContentView(getContentId());
initVm();
initView();
initData();
}
@Override
protected void onStart() {
super.onStart();
LogUtil.d(getClass().getSimpleName(), "onStart");
}
@Override
protected void onResume() {
super.onResume();
LogUtil.d(getClass().getSimpleName(), "onResume");
}
@Override
protected void onPause() {
super.onPause();
LogUtil.d(getClass().getSimpleName(), "onPause");
}
@Override
protected void onStop() {
super.onStop();
LogUtil.d(getClass().getSimpleName(), "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
LogUtil.i(getClass().getSimpleName(), "onDestroy");
}
protected abstract int getContentId();
//使用了泛型参数化
private void initVm() {
try {
ParameterizedType pt= (ParameterizedType) getClass().getGenericSuperclass();
// noinspection unchecked
Class clazz= (Class) pt.getActualTypeArguments()[0];
mViewModel = ViewModelProviders.of(this).get(clazz);
} catch (Exception e) {
e.printStackTrace();
}
Lifecycle lifecycle= getLifecycle();
lifecycle.addObserver(mViewModel);
}
protected abstract void initView();
protected abstract void initData();
}
LoginActivity
public class LoginActivity extends BaseVMActivity {
// UI references.
private AutoCompleteTextView mEmailView;
private EditText mPasswordView;
private View mProgressView;
private View mLoginFormView;
private Button mEmailSignInButton;
@Override
protected int getContentId() {
return R.layout.activity_login;
}
@Override
protected void initView() {
mEmailView = findViewById(R.id.email);
mLoginFormView = findViewById(R.id.login_form);
mProgressView = findViewById(R.id.login_progress);
mPasswordView = findViewById(R.id.password);
mEmailSignInButton = findViewById(R.id.email_sign_in_button);
}
@Override
protected void initData() {
populateAutoComplete();
mViewModel.getLoginPre().observe(this, aBoolean ->attemptLogin());
mViewModel.getPasswordError().observe(this, s ->onViewError(mPasswordView, s));
mViewModel.getEmailError().observe(this, s ->onViewError(mEmailView, s));
mViewModel.getShowProcess().observe(this, this::showProgress);
mViewModel.getOnLoginSuccess().observe(this, aBoolean ->{
Toast.makeText(LoginActivity.this, "Login success", Toast.LENGTH_LONG).show();
finish();
});
mViewModel.getRequestContacts().observe(this, this::requestContacts);
mViewModel.getPopulateAutoComplete().observe(this, aBoolean ->initLoader());
mViewModel.getEmaiAdapter().observe(this, stringArrayAdapter ->mEmailView.setAdapter(stringArrayAdapter));
mEmailSignInButton.setOnClickListener(view ->mViewModel.attemptLogin());
mPasswordView.setOnEditorActionListener((textView, id, keyEvent) ->mViewModel.onEditorAction(id));
}
private void populateAutoComplete() {
if (!mViewModel.mayRequestContacts()) {
return;
}
initLoader();
}
private void initLoader(){
//noinspection deprecation
getSupportLoaderManager().initLoader(0, null, mViewModel);
}
@TargetApi(Build.VERSION_CODES.M)
private void requestContacts(int requestCode) {
if (shouldShowRequestPermissionRationale(READ_CONTACTS)) {
Snackbar.make(mEmailView, R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE)
.setAction(android.R.string.ok, v ->requestPermissions(new String[]{READ_CONTACTS}, requestCode));
} else {
requestPermissions(new String[]{READ_CONTACTS}, requestCode);
}
}
/**
* Callback received when a permissions request has been completed.
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
mViewModel.onRequestPermissionsResult(requestCode,grantResults);
}
/**
* Attempts to sign in or register the account specified by the login form.
* If there are form errors (invalid email, missing fields, etc.), the
* errors are presented and no actual login attempt is made.
*/
private void attemptLogin() {
// Reset errors.
mEmailView.setError(null);
mPasswordView.setError(null);
// Store values at the time of the login attempt.
String email= mEmailView.getText().toString();
String password= mPasswordView.getText().toString();
mViewModel.toLogin(email, password);
}
private void onViewError(EditText editText, String message) {
editText.setError(message);
editText.requestFocus();
}
/**
* Shows the progress UI and hides the login form.
*/
private void showProgress(final boolean show) {
// On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
// for very easy animations. If available, use these APIs to fade-in
// the progress spinner.
int shortAnimTime= getResources().getInteger(android.R.integer.config_shortAnimTime);
mLoginFormView.setVisibility(show? View.GONE : View.VISIBLE);
mLoginFormView.animate().setDuration(shortAnimTime).alpha(
show? 0 : 1).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
}
});
mProgressView.setVisibility(show? View.VISIBLE : View.GONE);
mProgressView.animate().setDuration(shortAnimTime).alpha(
show? 1 : 0).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
}
});
}
}
都是页面交互相关的代码,几乎没有任何逻辑(没有if,for,埋点等,尽量每一行都页面交互相关的)
3、viewmodel
BaseVm
public abstract class BaseVm extends AndroidViewModel implements LifecycleObserver {
public BaseVm(@NonNull Application application) {
super(application);
}
}
2、viewModel
public class LoginViewModel extends BaseVm implements LoaderManager.LoaderCallbacks {
/**
* Id to identity READ_CONTACTS permission request.
*/
private static final int REQUEST_READ_CONTACTS = 0;
private MutableLiveData mLoginPre;
private MutableLiveData mPasswordError;
private MutableLiveData mEmailError;
private MutableLiveData mShowProcess;
private MutableLiveData mOnLoginSuccess;
private MutableLiveData mRequestContacts;
private MutableLiveData mPopulateAutoComplete;
private MutableLiveData> mEmaiAdapter;
public LoginViewModel(@NonNull Application application) {
super(application);
}
public boolean mayRequestContacts() {
boolean needRequest= Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
getApplication().checkSelfPermission(READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
if (needRequest) {
getRequestContacts().setValue(REQUEST_READ_CONTACTS);
}
return needRequest;
}
public void onRequestPermissionsResult(int requestCode, int[] grantResults) {
if (requestCode== REQUEST_READ_CONTACTS) {
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getPopulateAutoComplete().setValue(true);
}
}
}
public boolean onEditorAction(int id) {
if (id== EditorInfo.IME_ACTION_DONE || id== EditorInfo.IME_NULL) {
attemptLogin();
return true;
}
return false;
}
public void attemptLogin() {
getLoginPre().setValue(true);
}
public void toLogin(String email, String password) {
LoginTask.RequestValues values= new LoginTask.RequestValues(email, password);
UseCaseHandler.getInstance().execute(new LoginTask(), values, new UseCase.UseCaseCallback() {
@Override
public void onError(Integer code) {
switch (code) {
case LoginTask.ResponseValue.ERROR_INVALID_PASSWORD:
getPasswordError().setValue(getApplication().getString(R.string.error_invalid_password));
break;
case LoginTask.ResponseValue.ERROR_FIELD_REQUIRED:
getEmailError().setValue(getApplication().getString(R.string.error_field_required));
break;
case LoginTask.ResponseValue.ERROR_INVALID_EMAIL:
getEmailError().setValue(getApplication().getString(R.string.error_invalid_email));
break;
case LoginTask.ResponseValue.SHOW_PROCESS:
getShowProcess().setValue(true);
break;
case UseCase.CODE:
getShowProcess().setValue(false);
getPasswordError().setValue(getApplication().getString(R.string.error_incorrect_password));
break;
default:
getShowProcess().setValue(false);
getPasswordError().setValue(getApplication().getString(R.string.error_incorrect_password));
break;
}
}
@Override
public void onSuccess(LoginTask.ResponseValue result) {
getShowProcess().setValue(false);
getOnLoginSuccess().setValue(true);
}
});
}
@NonNull
@Override
public Loader onCreateLoader(int i, @Nullable Bundle bundle) {
return new CursorLoader(getApplication(), // Retrieve data rows for the device user's 'profile' contact.
Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI,
ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION,
// Select only email addresses.
ContactsContract.Contacts.Data.MIMETYPE +
" = ?", new String[]{ContactsContract.CommonDataKinds.Email
.CONTENT_ITEM_TYPE},
// Show primary email addresses first. Note that there won't be
// a primary email address if the user hasn't specified one.
ContactsContract.Contacts.Data.IS_PRIMARY + " DESC");
}
@Override
public void onLoadFinished(@NonNull Loader loader, Cursor cursor) {
List emails= new ArrayList<>();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
emails.add(cursor.getString(ProfileQuery.ADDRESS));
cursor.moveToNext();
}
//Create adapter to tell the AutoCompleteTextView what to show in its dropdown list.
ArrayAdapter adapter= new ArrayAdapter<>(getApplication(),
android.R.layout.simple_dropdown_item_1line, emails);
getEmaiAdapter().setValue(adapter);
}
@Override
public void onLoaderReset(@NonNull Loader loader) {
}
public MutableLiveData getLoginPre() {
if (mLoginPre == null) {
mLoginPre = new MutableLiveData<>();
}
return mLoginPre;
}
public MutableLiveData getPasswordError() {
if (mPasswordError == null) {
mPasswordError = new MutableLiveData<>();
}
return mPasswordError;
}
public MutableLiveData getEmailError() {
if (mEmailError == null) {
mEmailError = new MutableLiveData<>();
}
return mEmailError;
}
public MutableLiveData getShowProcess() {
if (mShowProcess == null) {
mShowProcess = new MutableLiveData<>();
}
return mShowProcess;
}
public MutableLiveData getOnLoginSuccess() {
if (mOnLoginSuccess == null) {
mOnLoginSuccess = new MutableLiveData<>();
}
return mOnLoginSuccess;
}
public MutableLiveData getRequestContacts() {
if (mRequestContacts == null) {
mRequestContacts = new MutableLiveData<>();
}
return mRequestContacts;
}
public MutableLiveData getPopulateAutoComplete() {
if (mPopulateAutoComplete == null) {
mPopulateAutoComplete = new MutableLiveData<>();
}
return mPopulateAutoComplete;
}
public MutableLiveData> getEmaiAdapter() {
if (mEmaiAdapter == null) {
mEmaiAdapter = new MutableLiveData<>();
}
return mEmaiAdapter;
}
}
登录任务的逻辑移动了domain中,viewmodel大大减负
3、domain
public class LoginTask extends UseCase {
/**
* A dummy authentication store containing known user names and passwords.
* TODO: remove after connecting to a real authentication system.
*/
private static final String[] DUMMY_CREDENTIALS = new String[]{
"[email protected]:hello", "[email protected]:world"
};
@Override
protected void executeUseCase(RequestValues value) {
boolean cancel= false;
// Check for a valid password, if the user entered one.
if (!TextUtils.isEmpty(value.getPassword()) && !isPasswordValid(value.getPassword())) {
getUseCaseCallback().onError(ResponseValue.ERROR_INVALID_PASSWORD);
cancel= true;
}
// Check for a valid email address.
if (TextUtils.isEmpty(value.getEmail())) {
getUseCaseCallback().onError(ResponseValue.ERROR_FIELD_REQUIRED);
cancel= true;
} else if (!isEmailValid(value.getEmail())) {
getUseCaseCallback().onError(ResponseValue.ERROR_INVALID_EMAIL);
cancel= true;
}
if (cancel) {
return;
}
getUseCaseCallback().onError(ResponseValue.SHOW_PROCESS);
try {
// Simulate network access.
Thread.sleep(2000);
} catch (InterruptedException e) {
getUseCaseCallback().onError(CODE);
return;
}
for (String credential: DUMMY_CREDENTIALS) {
String[] pieces= credential.split(":");
if (pieces[0].equals(value.getEmail())) {
// Account exists, return true if the password matches.
if( pieces[1].equals(value.getPassword())){
getUseCaseCallback().onSuccess(new ResponseValue(true));
}else {
getUseCaseCallback().onError(CODE);
}
return;
}
}
// TODO: register the new account here.
getUseCaseCallback().onError(CODE);
}
private boolean isEmailValid(String email) {
return email.contains("@");
}
private boolean isPasswordValid(String password) {
return password.length() > 4;
}
static class RequestValues implements UseCase.RequestValues {
private final String mEmail;
private final String mPassword;
public RequestValues(String email, String password) {
mEmail = email;
mPassword = password;
}
public String getEmail() {
return mEmail;
}
public String getPassword() {
return mPassword;
}
}
static class ResponseValue implements UseCase.ResponseValue {
public final static int ERROR_INVALID_PASSWORD = 999;
public final static int ERROR_FIELD_REQUIRED = 998;
public final static int ERROR_INVALID_EMAIL = 997;
public final static int SHOW_PROCESS = 996;
private boolean mIsTrue;
ResponseValue(boolean isTrue) {
mIsTrue = isTrue;
}
public boolean isTrue() {
return mIsTrue;
}
}
}
完整的一个登录任务,可以到处使用
ProfileQuery类:
public interface ProfileQuery {
String[] PROJECTION = {
ContactsContract.CommonDataKinds.Email.ADDRESS,
ContactsContract.CommonDataKinds.Email.IS_PRIMARY,
};
int ADDRESS = 0;
}
参考demo
[email protected]:gaobingqiu/MyProject.git