基于livedata实现的mvvm_clean

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a990924291/article/details/82353679

一、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<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> {
    public final static int CODE = -6;
    private Q mRequestValues;
    private UseCaseCallback<P> mUseCaseCallback;

    protected abstract void executeUseCase(Q value);

    public Q getRequestValues() {
        return this.mRequestValues;
    }

    public UseCaseCallback<P> getUseCaseCallback() {
        return this.mUseCaseCallback;
    }

    void run() {
        executeUseCase(this.mRequestValues);
    }

    public void setRequestValues(Q value) {
        this.mRequestValues = value;
    }

    public void setUseCaseCallback(UseCaseCallback<P> useCaseCallback) {
        this.mUseCaseCallback = useCaseCallback;
    }

    public interface RequestValues {
    }

    public interface ResponseValue {
    }

    public interface UseCaseCallback<R> {
        void onError(Integer code);

        void onSuccess(R result);
    }
}

关键就是这个类,本人改进了mvp_clean中不支持错误码的缺点,可以返回各种情况。

详细参考链接:

https://github.com/googlesamples/android-architecture/tree/todo-mvp-clean

2、view中:

package com.gbq.myproject.base;

import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import com.gbq.myproject.util.LogUtil;

import java.lang.reflect.ParameterizedType;

public abstract class BaseVMActivity<T extends BaseVm> extends AppCompatActivity {

    protected T mViewModel;

    @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<T> clazz = (Class<T>) 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

package com.gbq.myproject.login;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.gbq.myproject.R;
import com.gbq.myproject.base.BaseVMActivity;

import static android.Manifest.permission.READ_CONTACTS;

/**
 * A login screen that offers login via email/password.
 */
public class LoginActivity extends BaseVMActivity<LoginViewModel> {

    // 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);
    }
}

viewModel

public class LoginViewModel extends BaseVm implements LoaderManager.LoaderCallbacks<Cursor> {

    /**
     * Id to identity READ_CONTACTS permission request.
     */
    private static final int REQUEST_READ_CONTACTS = 0;

    private MutableLiveData<Boolean> mLoginPre;

    private MutableLiveData<String> mPasswordError;
    private MutableLiveData<String> mEmailError;
    private MutableLiveData<Boolean> mShowProcess;
    private MutableLiveData<Boolean> mOnLoginSuccess;
    private MutableLiveData<Integer> mRequestContacts;
    private MutableLiveData<Boolean> mPopulateAutoComplete;
    private MutableLiveData<ArrayAdapter<String>> 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<LoginTask.ResponseValue>() {
            @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<Cursor> 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<Cursor> loader, Cursor cursor) {
        List<String> 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<String> adapter = new ArrayAdapter<>(getApplication(),
                        android.R.layout.simple_dropdown_item_1line, emails);
        getEmaiAdapter().setValue(adapter);
    }

    @Override
    public void onLoaderReset(@NonNull Loader<Cursor> loader) {

    }

    public MutableLiveData<Boolean> getLoginPre() {
        if (mLoginPre == null) {
            mLoginPre = new MutableLiveData<>();
        }
        return mLoginPre;
    }

    public MutableLiveData<String> getPasswordError() {
        if (mPasswordError == null) {
            mPasswordError = new MutableLiveData<>();
        }
        return mPasswordError;
    }

    public MutableLiveData<String> getEmailError() {
        if (mEmailError == null) {
            mEmailError = new MutableLiveData<>();
        }
        return mEmailError;
    }

    public MutableLiveData<Boolean> getShowProcess() {
        if (mShowProcess == null) {
            mShowProcess = new MutableLiveData<>();
        }
        return mShowProcess;
    }

    public MutableLiveData<Boolean> getOnLoginSuccess() {
        if (mOnLoginSuccess == null) {
            mOnLoginSuccess = new MutableLiveData<>();
        }
        return mOnLoginSuccess;
    }

    public MutableLiveData<Integer> getRequestContacts() {
        if (mRequestContacts == null) {
            mRequestContacts = new MutableLiveData<>();
        }
        return mRequestContacts;
    }

    public MutableLiveData<Boolean> getPopulateAutoComplete() {
        if (mPopulateAutoComplete == null) {
            mPopulateAutoComplete = new MutableLiveData<>();
        }
        return mPopulateAutoComplete;
    }

    public MutableLiveData<ArrayAdapter<String>> getEmaiAdapter() {
        if (mEmaiAdapter == null) {
            mEmaiAdapter = new MutableLiveData<>();
        }
        return mEmaiAdapter;
    }
}

登录任务的逻辑移动了domain中,viewmodel大大减负

3、domain

public class LoginTask extends UseCase<LoginTask.RequestValues, LoginTask.ResponseValue> {
    /**
     * 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

猜你喜欢

转载自blog.csdn.net/a990924291/article/details/82353679