ViewModel让开发更放心

概况

做android开发,有时我们会采用MVP模式,把业务逻辑从Activity中分解出来。但是Presenter的生命周期不容易管理。对于一个复杂的Activity和Fragment来说,可能绑定了多个Presenter、Manager或者View,代码写起来就会很复杂。尤其是当这些被其他人复用的时候,很难让别人也注意到这一点,很容易发生内存溢出问题。

LifeCycle

Google推出的LifeCycle就可以解决辅助类的生命周期问题。
在API 26.1.0之后,android.support.v4.app中的FragmentActivity和Fragment都集成了LifeCycle的相关功能。
LiveData 和 ViewModel 生命周期组件是 Android 官方架构组件中的核心组件, 它可以使各种实例作为观察者与 Activity 和 Fragment 等具有生命周期特性的组件绑定在一起。我们将需要绑定生命周期的实例注册给该组件, 该组件就会在指定的某个生命周期方法执行时通知这个实例。它们用到了观察者模式。

LiveData
LiveData是一个可观察的数据持有者类。与常见的观察者不同,LiveData是有生命周期感知的。这种感知确保LiveData只更新处于生命周期状态内的应用程序组件(这一点太重要了)。

ViewModel
ViewModel 有两个功能, 第一个功能可以使 ViewModel 以及 ViewModel 中的数据在屏幕旋转或配置更改引起的 Activity 重建时存活下来, 重建后数据可继续使用; 第二个功能可以帮助开发者轻易实现 Fragment 与 Fragment 之间, Activity 与 Fragment 之间的通讯以及共享数据。

使用方法

  • 通过创建MyViewModel extends ViewModel
  • 在MyViewModel内部新建MutableLiveData<T> 创建可监听数据
  • 在Activity 或 Fragment中获取获取MyViewModel里的MutableLiveData并添加监听 viewModel.liveData.observer()

实例

我们以一个登录页LogActivity来举例说明吧。一般我们把业务交给ViewModel来处理。View则根据数据的变化来做调整,这部分是写在Fragment或Activity类里。
(1)分析登录页LoginActivity的需求

  • 登录数据有效性验证,如用户名、密码的有效性验证,可以用一个类来记录这些登录表单的状态:
class LoginFormState {
   @Nullable
   private Integer usernameError;// 用户名错误
   @Nullable
   private Integer passwordError;// 密码错误
   // 我们用这个字段来控制登录按钮的状态
   private boolean isDataValid;

   LoginFormState(@Nullable Integer usernameError, @Nullable Integer passwordError) {
       this.usernameError = usernameError;
       this.passwordError = passwordError;
       this.isDataValid = false;
   }

   LoginFormState(boolean isDataValid) {
       this.usernameError = null;
       this.passwordError = null;
       this.isDataValid = isDataValid;
   }

   @Nullable
   Integer getUsernameError() {
       return usernameError;
   }

   @Nullable
   Integer getPasswordError() {
       return passwordError;
   }

   boolean isDataValid() {
       return isDataValid;
   }
}
  • 当数据有效性通过后,就开始登录,登录或成功或失败,我们用一个类来记录这些登录的结果:
class LoginResult {
    @Nullable
    private LoggedInUserView success; // 成功则返回数据
    @Nullable
    private Integer error; // 错误则返回一个错误码

    LoginResult(@Nullable Integer error) {
        this.error = error;
    }

    LoginResult(@Nullable LoggedInUserView success) {
        this.success = success;
    }

    @Nullable
    LoggedInUserView getSuccess() {
        return success;
    }

    @Nullable
    Integer getError() {
        return error;
    }
}

成功返回的数据的类:

class LoggedInUserView {
    private String displayName;
    //... other data fields that may be accessible to the UI

    LoggedInUserView(String displayName) {
        this.displayName = displayName;
    }

    String getDisplayName() {
        return displayName;
    }
}

(2)以上这些需求,都将在LoginViewModel里帮我们完成这些数据校验和登录等数据逻辑。
由于在LoginActivity里要通过以下这种方式来获得LoginViewModel:

loginViewModel = ViewModelProviders.of(this, new LoginViewModelFactory())
                .get(LoginViewModel.class);
  • 因此我们要先建一个工厂类LoginViewModelFactory来创建我们的LoginViewModel:
public class LoginViewModelFactory implements ViewModelProvider.Factory {

   @NonNull
   @Override
   @SuppressWarnings("unchecked")
   public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
       if (modelClass.isAssignableFrom(LoginViewModel.class)) {
           return (T) new LoginViewModel(LoginRepository.getInstance(new LoginDataSource()));
       } else {
           throw new IllegalArgumentException("Unknown ViewModel class");
       }
   }
}

从上面我们可以看到在创建LoginViewModel时传了一个LoginRepository单例进去。LoginRepository在实例化时传了一个LoginDataSource实例进去。
根据前面的分析我们可知LoginViewModel会提供登录及表单数据的有效性验证,但是如果需要共享登录的方法,甚至退出登录的方法、登录的状态、登录后用户的信息我们该怎么办呢?很简单只要将这些部分放在一个单例类里就好了。在我们这里就是LoginRepository这个类来充当共享的单例类。它的代码如下:

public class LoginRepository {

    private static volatile LoginRepository instance;

    private LoginDataSource dataSource;

    // If user credentials will be cached in local storage, it is recommended it be encrypted
    // @see https://developer.android.com/training/articles/keystore
    private LoggedInUser user = null;

    // private constructor : singleton access
    private LoginRepository(LoginDataSource dataSource) {
        this.dataSource = dataSource;
    }

    public static LoginRepository getInstance(LoginDataSource dataSource) {
        if (instance == null) {
            instance = new LoginRepository(dataSource);
        }
        return instance;
    }

    // 用户的登录状态,用户已登录则 返回true,否则返回false
    public boolean isLoggedIn() {
        return user != null;
    }

    // 退出时,清空LoginRepository单例类里所有的数据
    public void logout() {
        user = null;
        dataSource.logout();
    }

    // 登录成功后,我们就将用户的信息保存在LoginRepository单例类里
    private void setLoggedInUser(LoggedInUser user) {
        this.user = user;
        // If user credentials will be cached in local storage, it is recommended it be encrypted
        // @see https://developer.android.com/training/articles/keystore
    }

    // 共享的登录方法
    public Result<LoggedInUser> login(String username, String password) {
        // handle login
        Result<LoggedInUser> result = dataSource.login(username, password);
        if (result instanceof Result.Success) {// 如果登录成功,则将用户信息记录在单例类里,以备共享
            setLoggedInUser(((Result.Success<LoggedInUser>) result).getData());
        }
        return result;
    }
}

用户数据实体类:

public class LoggedInUser {

    private String userId;
    private String displayName;

    public LoggedInUser(String userId, String displayName) {
        this.userId = userId;
        this.displayName = displayName;
    }

    public String getUserId() {
        return userId;
    }

    public String getDisplayName() {
        return displayName;
    }
}

登录结果的实体类:

public class Result<T> {
    // hide the private constructor to limit subclass types (Success, Error)
    private Result() {
    }

    @Override
    public String toString() {
        if (this instanceof Result.Success) {
            Result.Success success = (Result.Success) this;
            return "Success[data=" + success.getData().toString() + "]";
        } else if (this instanceof Result.Error) {
            Result.Error error = (Result.Error) this;
            return "Error[exception=" + error.getError().toString() + "]";
        }
        return "";
    }

    // Success sub-class
    public final static class Success<T> extends Result {
        private T data;

        public Success(T data) {
            this.data = data;
        }

        public T getData() {
            return this.data;
        }
    }

    // Error sub-class
    public final static class Error extends Result {
        private Exception error;

        public Error(Exception error) {
            this.error = error;
        }

        public Exception getError() {
            return this.error;
        }
    }
}

负责处理用户登录和验证,并获取用户数据的数据源:

public class LoginDataSource {

    public Result<LoggedInUser> login(String username, String password) {

        try {
            // TODO: handle loggedInUser authentication
            LoggedInUser fakeUser =
                    new LoggedInUser(
                            java.util.UUID.randomUUID().toString(),
                            "Jane Doe");
            return new Result.Success<>(fakeUser);
        } catch (Exception e) {
            return new Result.Error(new IOException("Error logging in", e));
        }
    }

    public void logout() {
        // TODO: revoke authentication
    }
}

LoginViewModel登场:

public class LoginViewModel extends ViewModel {

    // 持有一个可观察的数据LoginFormState的LiveData类
    private MutableLiveData<LoginFormState> loginFormState = new MutableLiveData<>();
    // 持有一个可观察的数据LoginResult的LiveData类
    private MutableLiveData<LoginResult> loginResult = new MutableLiveData<>();
    // 缓存类,所有需要共享的方法数据都在里面
    private LoginRepository loginRepository;

    LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    // 返回一个可观察的数据持有者类LiveData,它持有LoginFormState数据
    LiveData<LoginFormState> getLoginFormState() {
        return loginFormState;
    }

    // 返回一个可观察的数据持有者类LiveData,它持有LoginResult数据
    LiveData<LoginResult> getLoginResult() {
        return loginResult;
    }

    // 处理用户登录的
    public void login(String username, String password) {
        // 处理用户登录
        Result<LoggedInUser> result = loginRepository.login(username, password);

        if (result instanceof Result.Success) {// 成功登录
            LoggedInUser data = ((Result.Success<LoggedInUser>) result).getData();
            loginResult.setValue(new LoginResult(new LoggedInUserView(data.getDisplayName())));// 设置新数据,并通知观察者
        } else {
            loginResult.setValue(new LoginResult(R.string.login_failed));// 设置新数据,并通知观察者
        }
    }

    // 处理表单数据变化的
    public void loginDataChanged(String username, String password) {
        if (!isUserNameValid(username)) {// 验证用户名的有效性
            loginFormState.setValue(new LoginFormState(R.string.invalid_username, null));
        } else if (!isPasswordValid(password)) {// 验证密码的有效性
            loginFormState.setValue(new LoginFormState(null, R.string.invalid_password));
        } else {// 用户名和密码都有效
            loginFormState.setValue(new LoginFormState(true));
        }
    }

    // A placeholder username validation check
    private boolean isUserNameValid(String username) {
        if (username == null) {
            return false;
        }
        if (username.contains("@")) {
            return Patterns.EMAIL_ADDRESS.matcher(username).matches();
        } else {
            return !username.trim().isEmpty();
        }
    }

    // A placeholder password validation check
    private boolean isPasswordValid(String password) {
        return password != null && password.trim().length() > 5;
    }
}

前面都是在讲LoginViewModel,最后我们讲一讲在LoginActivity里如何使用它:

package com.tisson.kmc.ui.login;

import android.app.Activity;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.tisson.kmc.MainActivity;
import com.tisson.kmc.R;
import com.tisson.kmc.ui.login.LoginViewModel;
import com.tisson.kmc.ui.login.LoginViewModelFactory;

public class LoginActivity extends AppCompatActivity {

    private LoginViewModel loginViewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // 获取ViewModel
        loginViewModel = ViewModelProviders.of(this, new LoginViewModelFactory())
                .get(LoginViewModel.class);


        final EditText usernameEditText = findViewById(R.id.username);
        final EditText passwordEditText = findViewById(R.id.password);
        final Button loginButton = findViewById(R.id.login);
        final ProgressBar loadingProgressBar = findViewById(R.id.loading);

        // 观察表单数据的变化
        loginViewModel.getLoginFormState().observe(this, new Observer<LoginFormState>() {
            @Override
            public void onChanged(@Nullable LoginFormState loginFormState) {
                if (loginFormState == null) {
                    return;
                }
                loginButton.setEnabled(loginFormState.isDataValid());// 设置按钮状态
                if (loginFormState.getUsernameError() != null) {// 用户名错误
                    usernameEditText.setError(getString(loginFormState.getUsernameError()));
                }
                if (loginFormState.getPasswordError() != null) {// 密码错误
                    passwordEditText.setError(getString(loginFormState.getPasswordError()));
                }
            }
        });

        // 观察登录结果的变化
        loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {
            @Override
            public void onChanged(@Nullable LoginResult loginResult) {
                if (loginResult == null) {
                    return;
                }
                loadingProgressBar.setVisibility(View.GONE);// 关闭loading动画
                if (loginResult.getError() != null) {// 登录失败
                    showLoginFailed(loginResult.getError());// 显示登录失败消息
                }
                if (loginResult.getSuccess() != null) {// 登录成功
                    updateUiWithUser(loginResult.getSuccess());// 显示登录成功消息
                }
                setResult(Activity.RESULT_OK);// 对使用startActivityForResult有效

                //Complete and destroy login activity once successful
                finish();
            }
        });

        // EditText输入框的变化
        TextWatcher afterTextChangedListener = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // ignore
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // ignore
            }

            @Override
            public void afterTextChanged(Editable s) {
                // 设置表单数据的变化,通知观察者
                loginViewModel.loginDataChanged(usernameEditText.getText().toString(),
                        passwordEditText.getText().toString());
            }
        };
        
        usernameEditText.addTextChangedListener(afterTextChangedListener);// 注册文本变化监听器
        passwordEditText.addTextChangedListener(afterTextChangedListener);// 注册文本变化监听器
        passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {

            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {// 软键盘Done事件
                if (actionId == EditorInfo.IME_ACTION_DONE) {
                    loginViewModel.login(usernameEditText.getText().toString(),
                            passwordEditText.getText().toString());
                }
                return false;
            }
        });

        // 登录按钮事件
        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadingProgressBar.setVisibility(View.VISIBLE);
                loginViewModel.login(usernameEditText.getText().toString(),
                        passwordEditText.getText().toString());
            }
        });
    }

    private void updateUiWithUser(LoggedInUserView model) {
        String welcome = getString(R.string.welcome) + model.getDisplayName();
        // TODO : initiate successful logged in experience
        Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show();
        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
        startActivity(intent);
    }

    private void showLoginFailed(@StringRes Integer errorString) {
        Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show();
    }
}

Demo已在Github上了,欢迎下载学习

猜你喜欢

转载自blog.csdn.net/weixin_40763897/article/details/92772085