基于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[]{

            "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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容