美文网首页
基于livedata实现的mvvm_clean

基于livedata实现的mvvm_clean

作者: 天天听听 | 来源:发表于2018-09-01 16:39 被阅读0次

    一、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[]{

                "foo@example.com:hello", "bar@example.com: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

    git@github.com:gaobingqiu/MyProject.git

    相关文章

      网友评论

          本文标题:基于livedata实现的mvvm_clean

          本文链接:https://www.haomeiwen.com/subject/tqduwftx.html