基于livedata实现的mvvm_clean

一、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层:一个个独立的“任务”,主要使用命令模式把请求,返回结果封装了。这个任务可以到处使用,也实现责任链模式将复杂得业务简单化。井井有条。

步骤


mvvm_clean流程图


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

你可能感兴趣的:(基于livedata实现的mvvm_clean)