概况

做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上了,欢迎下载学习​​。