在Android项目中,采用了MVP的架构,MVP架构主要是为了解决以往MVC架构下,在Activity中处理业务逻辑导致的耦合性强的问题。MVP架构的介绍。在上一篇文章(Live-client-1-UI界面的设计)中,已经完成大部分的UI界面的设计,再编写对应的Activity和fragment就大致完成了MVP中的View视图层,接下来要做的是Model层和Presenter层的设计和实现。
在项目中选用了Room,这个Google官方的数据库框架,也是Jetpack架构组件库中的一员,通常和Databinding、LiveData、ViewModel框架使用,实现MVVM架构。但是在本项目中并没有这么做, 而是将Room替代以往复杂的Sqlite操作,并利用Room数据库框架提供可以直接返回RxJava的Flowable可观察对象的特性,来实现数据库的访问,并返回Flowable可观察对象提供给Rxjava进行处理。
既然提及到了Rxjava,那么Rxjava又是什么呢?Rxjava是一个基于事件、通过使用观察者序列来编写异步代码的库。Rxjava将链式编程风格和异步结合在一起,并基于观察者模式。在Android中使用Rxjava,能快速实现线程切换、异步回调、消息处理等日常编码中出现的难题,同时使得代码逻辑更加清晰明了。
Retrofit的官方描述
A type-safe HTTP client for Android and Java
一种适用于Android和Java的类型安全的HTTP客户端。通过官方的介绍,可以得知Retrofit也是支持链式调用,同时也支持将请求的结果转为Flowable可观察对象,因此通过Retrofit网络框架请求得来的网络数据也可以通过Rxjava来进行处理。
于是,我们就可以通过Rxjava来处理本地数据库和网络请求得到的数据,供Presenter层进行处理。
Model层
Model层要做的东西其实还挺多的:
- 首先要定义实体类;
- 根据实体类创建Room数据库框架所需要的Dao、Database,完成数据库的定义和创建;
- 创建数据接口,实现本地、网络数据接口,最后创建数据仓库Repository类来组合本地数据和网络数据。
但是在这个项目中,我将网络请求部分从Repository中分离,直接提供接口给Presenter,这样做会加重Presenter层的负担,但是在逻辑上更加清晰,明确需要从网络中请求时就直接调用retrofit的接口,而不是向以往一样从repository中调用网络数据接口发起retrofit的请求。尽管逻辑清晰了, 但是会使得Model和Presenter的耦合性增加,因此不太推荐这种写法,在后期可能会对该部分进行重构。
Model目录如下:
实体类
定义一个User实体类,包含用户名、账号、密码三个属性,同时作为Room框架中的实体,要添加@Entity注解。
**
* 用户表
* @author Ljh 2019/7/10 15:49
*/
@Entity
public class User {
private String name;
@NonNull
@PrimaryKey
private String account;
@NonNull
private String password;
public User() {
}
@Ignore
public User(String name, @NonNull String phone, @NonNull String password) {
this.name = name;
this.account = phone;
this.password = password;
}
// ... get/set方法省略
}
Dao
UserDao编写数据访问的相关方法及返回值,要添加@Dao注解将类标记为数据访问对象。代码示例
/**
* @author Ljh 2019/7/10 16:26
*/
@Dao
public interface UserDao {
/**
* 按照主键phone查询User,通过rxjava的single
*
* @param account 用户账号:手机/email
* @return 返回单个user,如果找不到onError(EmptyResultSetException.class)
*/
@Query("SELECT * FROM user where account =:account")
Single<User> getUserByPhone(String account);
/**
* 添加用户
*
* @param user 用户
* @return 返回
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
Completable insertUser(User user);
/**
* 更新用户
*
* @param user 用户信息
* @return 返回成功与否
*/
@Update(onConflict = OnConflictStrategy.REPLACE)
Completable updateUser(User user);
/**
* 删除用户
*
* @param account 用户账号 手机/email
* @return 返回成功与否
*/
@Query("DELETE FROM user where account = :account")
Completable deleteUser(String account);
}
在最初接触Room框架时,Room仅仅支持对@Query查询出来的数据提供Flowable可观察对象,在Version 2.1.0-alpha01时,就添加对@Insert、@Update、@Delete等操作结果的观察,也就是说在Version 2.1.0-alpha01后的版本的Room配合Rxjava使用才不会那么别扭,可以观察所有的数据库操作,然后放入Rxjava中操作。
数据库创建
创建数据库时要使用单例模式,避免创建多个数据库实例,导致内存泄漏。需要使用@Database()注解配置数据实体类、数据库版本等信息。
/**
* @author Ljh 2019/7/10 17:42
*/
@Database(entities = {User.class, LiveInfo.class}, version = 1, exportSchema = false)
public abstract class LiveDataBase extends RoomDatabase {
public static final String TAG = LiveDataBase.class.getSimpleName();
public static LiveDataBase INSTANCE;
public abstract UserDao userDao();
public abstract LiveInfoDao liveInfoDao();
private static final Object sLock = new Object();
public static LiveDataBase getInstance() {
synchronized (sLock) {
if (INSTANCE == null) {
Log.d(TAG, "Create Database");
INSTANCE = Room.databaseBuilder(MyApplication.getMyApplicationContext(),
LiveDataBase.class, "Live.db")
.build();
}
return INSTANCE;
}
}
}
完成以上步骤之后,就完成了实体类、数据表、数据库和数据库CRUD的接口。接下来要看数据接口怎么设计。
数据接口与数据仓库Repository
编写数据接口的目的是为了在数据库CRUD接口之上再次进行封装、处理,然后提供给Presenter层使用,其本质还是CRUD等操作。UserDataSource如下:
public interface UserDataSource {
Single<User> getUserByPhone(String account);
Completable insertUser(User user);
Completable updateUser(User user);
Completable deleteUser(String account);
}
本地数据源UserLocalDataSource:
public class UserLocalDataSource implements UserDataSource {
private static UserLocalDataSource INSTANCE;
private UserDao mUserDao;
public UserLocalDataSource(UserDao userDao) {
this.mUserDao = userDao;
}
public static UserLocalDataSource getInstance(UserDao userDao) {
if (INSTANCE == null) {
INSTANCE = new UserLocalDataSource(userDao);
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
@Override
public Single<User> getUserByPhone(String account) {
return mUserDao.getUserByPhone(account)
.subscribeOn(Schedulers.io());
}
@Override
public Completable insertUser(User user) {
return mUserDao.insertUser(user)
.subscribeOn(Schedulers.io());
}
@Override
public Completable updateUser(User user) {
return mUserDao.updateUser(user)
.subscribeOn(Schedulers.io());
}
@Override
public Completable deleteUser(String account) {
return mUserDao.deleteUser(account)
.subscribeOn(Schedulers.io());
}
}
UserRepository数据仓库:
/**
* @author Ljh 2019/7/15 10:04
*/
public class UserRepository implements UserDataSource {
private static UserRepository INSTANCE;
private final UserLocalDataSource mUserLocalDataSource;
private UserRepository() {
this.mUserLocalDataSource = UserLocalDataSource.getInstance(LiveDataBase.getInstance().userDao());
}
public static UserRepository getInstance() {
if (INSTANCE == null) {
INSTANCE = new UserRepository();
}
return INSTANCE;
}
@Override
public Single<User> getUserByPhone(String account) {
return mUserLocalDataSource.getUserByPhone(account);
}
@Override
public Completable insertUser(User user) {
return mUserLocalDataSource.insertUser(user);
}
@Override
public Completable updateUser(User user) {
return mUserLocalDataSource.updateUser(user);
}
@Override
public Completable deleteUser(String account) {
return mUserLocalDataSource.deleteUser(account);
}
}
可以看到,所有的操作只是对数据库Dao接口的调用,再封装一个数据仓库的单例模式。如果这个简单的应用没有后续的扩展和改善的话,那么像数据仓库中不处理网络请求的写法,其实有很多部分都是冗余的,没有必要的。因为UserRepository中只是简单的调用了UserLocalDataSource,而UserLocalDataSource又只是简单的调用了数据库Dao的接口。如果添加一个UserRemoteDataSource专门用来处理网络请求及其响应数据,然后在UserRepository中进行本地、网络的逻辑处理,那么这样的Model层设计还算有点用处。
View层与Presenter层
View层和Presenter层的关联比较紧密,这里就一同讲述。每一个View和Presenter都有一些相同的方法,我们将该部分方法抽出,分离成接口,让每个View和Presenter来实现。
BaseView
在BaseView这个接口中,需要将每个Presenter和当前view进行绑定,也需要在每个View中显示Toast,于是BaseView的设计如下:
public interface BaseView<T> {
void setPresenter(T presenter);
void showToast(String message);
}
通过一个模板类的设计,可以将view和不同的Presenter进行绑定。
BasePresenter
在BasePresenter中则较为简单,只是提供了一个start()方法,给每个Presenter进行一些初始化的操作。
public interface BasePresenter {
void start();
}
Login功能模块:
LoginContract
下面以登录这个功能来讲解MVP架构中,View和Presenter的实现。
Contract的意思是契约,通过LoginContract类,来详细定义View和Presenter中所需要的方法:
- 在Presenter方面:首先在登录注册功能中,肯定要有一个登录方法、一个注册方法来完成这两项功能,其次,在注册的时候,需要获取验证码进行注册,于是又要定义一个发送验证码的方法。
- 在View方面:由于要在一个页面中转换登录和注册框,因此需要一个显示登录框的方法、一个显示注册框的方法;由于背景使用了Bing的每日一图,于是又定义一个显示每日一图的方法;在请求验证码时,通常要避免用户不断请求验证码,导致验证码的时效发生变化和避免增加服务器的负担,需要设置一个倒计时方法。
这时,可能会有这样的疑问:为什么还要设置一个契约接口来定义上述的方法呢?其实目的是为了将View和Presenter的所有方法集中在一起,便于查看。同时,通过setPresenter()方法的实现,可以将view和presenter联系在一起,相互调用对方的方法,给对方提供调用接口,在逻辑上更加清晰。
代码示例:
public interface LoginContract {
interface View extends BaseView<Presenter> {
//显示Bing每日一图作为全局背景
void showBingPicBg(String address);
//显示注册框
void showRegister();
//显示登录框
void showLogin();
//对页面的返回进行设置
void setResult(String result, String msg);
//取消短信倒计时
void cancelTimer();
}
interface Presenter extends BasePresenter {
//使用账号密码登录
void login(String phone, String password, String accountType);
//通过user和验证码注册
void register(User user, String code, String accountType);
//发送验证码
void sendMessage(String phone, String accountType);
}
}
LoginFragment
创建一个实现了LoginContract.View接口的Fragment,并通过单例模式获取该Fragment,避免重复创建View带来的花销。
在Fragment的onCreateView方法中,通过LayoutInflater来查找和加载Fragment对应的ui布局,并给ui布局中的控件进行初始化。
public class LoginFragment extends Fragment implements LoginContract.View, View.OnClickListener {
public static final String TAG = LoginFragment.class.getSimpleName();
private LoginContract.Presenter mPresenter;
//... 省略UI控件
//验证码倒计时
private CountDownTimer timer = new CountDownTimer(60 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
codeBt.setEnabled(false);
codeBt.setText("(" + millisUntilFinished / 1000 + ")");
}
@Override
public void onFinish() {
codeBt.setEnabled(true);
codeBt.setText("重新获取验证码");
}
};
public static LoginFragment newInstance() {
return new LoginFragment();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_login, container, false);
// ... 省略控件的初始化
mPresenter.start();
return root;
}
@Override
public void showBingPicBg(final String address) {
try {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Glide.with(getContext()).load(address).into(bgImage);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 显示注册框
* <p>
* 显示验证码,显示undo返回键,修改文字
*/
@Override
public void showRegister() {}
@Override
public void setResult(String result, String msg) {
if (result.equals("LOGIN_SUCCESS")) {
Log.d(TAG, "setResult: LOGIN_SUCCESS");
getActivity().setResult(1);
getActivity().finish();
} else {
Log.d(TAG, "setResult: LOGIN_ERROR");
}
}
/**
* 显示登录框
* <p>
* 隐藏验证码、隐藏undo返回、显示登录按钮
*/
@Override
public void showLogin() {}
@Override
public void cancelTimer() {
timer.cancel();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
codeBt.setEnabled(true);
codeBt.setText("获取验证码");
}
});
}
@Override
public void setPresenter(LoginContract.Presenter presenter) {
mPresenter = presenter;
}
@Override
public void showToast(final String message) {}
@Override
public void onClick(View v) {
String account = "";
String password = "";
String code = "";
switch (v.getId()) {
case R.id.bt_login_login:
account = accountEt.getText().toString();
password = passwordEt.getText().toString();
//... 省略输入验证
mPresenter.login(account, password, ACCOUNT_TYPE);
break;
case R.id.bt_login_register:
//... 省略输入验证
mPresenter.register(user, code, ACCOUNT_TYPE);
}
break;
case R.id.bt_login_undo:
showLogin();
break;
case R.id.bt_login_code:
//发送短信验证码
account = accountEt.getText().toString();
//验证码倒计时开始
timer.start();
mPresenter.sendMessage(account, ACCOUNT_TYPE);
break;
}
}
}
LoginPresenter
LoginPresenter要继承LoginContract中的Presenter接口,实现接口中的功能,并在构造函数中创建一个View对象(LoginContact中的View),完成view与presenter的绑定。
public class LoginPresenter implements LoginContract.Presenter {
public static final String TAG = LoginPresenter.class.getSimpleName();
private final LoginContract.View mView;
private UserRepository mUserRepository;
public LoginPresenter(LoginContract.View view) {
mUserRepository = UserRepository.getInstance();
mView = view;
mView.setPresenter(this);
}
@Override
public void login(final String account, String password, String accountType) {
LiveService liveService = LiveService.Factory.create();
Disposable subscribe = liveService.login(account, MD5Util.encrypt(password), accountType)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(new Consumer<Result<String>>() {
@Override
public void accept(Result<String> stringResult) throws Exception {
if (stringResult.getState().equals("200")) { //登录成功
mView.setResult("LOGIN_SUCCESS", stringResult.getMsg());
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(MyApplication.getMyApplicationContext()).edit();
editor.putString(Constant.SP_ACCOUNT, account);
editor.putBoolean(Constant.IS_LOGIN, true);
editor.commit();
} else if (stringResult.getState().equals("100")) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(MyApplication.getMyApplicationContext()).edit();
editor.putBoolean(Constant.IS_LOGIN, false);
editor.commit();
mView.setResult("LOGIN_ERROR", stringResult.getMsg());
mView.showToast(stringResult.getMsg());
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
mView.showToast("网络异常,请稍后重试");
}
});
}
@Override
public void register(final User user, String code, String accountType) {
user.setPassword(MD5Util.encrypt(user.getPassword()));
LiveService liveService = LiveService.Factory.create();
liveService.register(user.getAccount(), user.getPassword(), user.getName(), code, accountType)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(new Consumer<Result<String>>() {
@Override
public void accept(Result<String> stringResult) throws Exception {
if (stringResult == null) {
mView.showToast("网络异常,请稍后重试");
return;
}
String state = stringResult.getState();
String msg = stringResult.getMsg();
if (state.equals("200")) {
//注册成功,将该用户保存到数据库中,并将phone保存到sp中,跳转到登录界面
//将sp中的phone加载到登录界面中的phone中
mUserRepository.insertUser(user);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(MyApplication.getMyApplicationContext()).edit();
editor.putString(Constant.SP_ACCOUNT, user.getAccount());
editor.commit();
//子线程中能显示吗?
mView.showToast(msg);
mView.showLogin();
} else if (state.equals("100")) {
//注册失败,提醒用户注册失败原因
mView.showToast(msg);
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "accept: " + throwable.getMessage());
mView.showToast("网络异常,请稍后重试");
}
});
}
@Override
public void sendMessage(String account, String accountType) {
if (TextUtils.isEmpty(account)) {
mView.showToast("手机号不能为空");
return;
}
LiveService liveService = LiveService.Factory.create();
liveService.sendMessage(account, accountType)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Result<String>>() {
@Override
public void accept(Result<String> stringResult) throws Exception {
if (stringResult.getState().equals("200")) {
//获取验证码成功
mView.showToast("验证码已发送到您的手机");
} else {
mView.showToast(stringResult.getMsg());
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "accept: " + throwable.getMessage());
mView.showToast("网络错误,稍后重试");
mView.cancelTimer();
}
});
}
@Override
public void start() {
loadBingPic();
}
/**
* 加载每日一图。
* <p>
* 获取新的每日一图,成功,则加载到ImageView中,作为loginActivity的背景
* 获取不成功,则加载原有的每日一图。
*/
private void loadBingPic() {
BingPicService bingPicService = BingPicService.Factory.create();
bingPicService.getBingPic()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(new Consumer<BingResult>() {
@Override
public void accept(BingResult bingResult) throws Exception {
if (bingResult == null) { //获取不到新的必应每日一图,则用旧图取代
showOldBingPicBg();
return;
}
String urlTemp = bingResult.getImages().get(0).getUrl();
if (TextUtils.isEmpty(urlTemp)) {//获取不到新的必应每日一图,则用旧图取代
showOldBingPicBg();
return;
} else {
String newBingPicURL = "http://s.cn.bing.net" + urlTemp;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(MyApplication.getMyApplicationContext());
String oldBingPicURL = preferences.getString("BING_PIC", null);
if (oldBingPicURL == null || !oldBingPicURL.equals(newBingPicURL)) { //地址不相等
SharedPreferences.Editor editor = preferences.edit();
editor.putString("BING_PIC", newBingPicURL);
editor.commit();
}
mView.showBingPicBg(newBingPicURL);
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "onFailure: ");
showOldBingPicBg();
}
});
}
}
在上面的代码中,可以看到login方法中调用了LoginService.Fractory.create()方法创建了一个LiveService,而这个LiveService又是什么呢?这个LiveService就是前面所提到的Retrofit的网络调用接口。调用LiveService的login方法,发起登录的网络请求,同时通过链式调用将网络请求在子线程中进行,请求完成后将登录结果放置到SharePreference中,并调用View中对应的方法,进行页面的控制。
LiveService
Retrofit网络请求的实现,就是通过定义一个接口来完成。这个接口中有不同注解标注的方法,定义了Http请求中的GET、POST、PUT、DELETE等请求方式。而在这个接口中,最为重要的创建Retrofit对象实例,通过在接口中创建工厂类的方式,来创建Retrofit。
public interface LiveService {
/**
* 用户登录 - 表单
*
* @param account 用户账号:手机/Email
* @param password 密码
* @return 返回登录情况
*/
@FormUrlEncoded
@POST("login")
Flowable<Result<String>> login(@Field("account") String account,
@Field("password") String password,
@Field("type") String accountType);
/**
* 注册 - 表单
*
* @param account 用户账号:手机/Email
* @param password 密码
* @param name 用户名称(默认与手机号相同)
* @param code 验证码
* @return 返回注册情况
*/
@FormUrlEncoded
@POST("register")
Flowable<Result<String>> register(@Field("account") String account,
@Field("password") String password,
@Field("name") String name,
@Field("code") String code,
@Field("type") String accountType);
/**
* 注销
*
* @return 返回注销情况
*/
@GET("logout")
Flowable<Result<String>> logout();
/**
* 发送验证码 - 表单
*
* @param account 用户账号:手机/Email
* @return 返回发送验证码情况
*/
@FormUrlEncoded
@POST("message")
Flowable<Result<String>> sendMessage(@Field("account") String account,
@Field("type") String accountType);
/**
* 获取最新版本信息
*
* @return
*/
@GET("version")
Flowable<Result<String>> getVersion();
/**
* 下载差分包
* 使用streaming方式,retrofit不会一次性将ResponseBody读取如内存,否则用以造成OOM
*
* @param url 下载的差分包的地址
* @return 返回值使用ResponseBody之后会对ResponseBody进行读取
*/
@GET
@Streaming
Flowable<ResponseBody> downloadApk(@Url String url);
class Factory {
static boolean isDebug = false;
static OkHttpClient client = new OkHttpClient
.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.addInterceptor(new AddCookiesInterceptor()) //这部分
.addInterceptor(new ReceivedCookiesInterceptor()) //这部分
.build();
public static LiveService create() {
String url = isDebug ? "本地地址" : "服务器地址";
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(url)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
return retrofit.create(LiveService.class);
}
}
}
编写了LiveService后,Presenter中就完成了登录中数据的本地和网络请求。
MVP模式中的M、V、P都已经实现了,那么问题又来了,如何将Presenter和View相互匹配呢?上述的过程只是将V、P实现了,那他们之间的桥梁要怎么搭建呢?答案就是通过Activity来实现。
LoginActivity
在LoginActivity中创建Fragment和Presenter,然后在Fragment中绑定Presenter,这样就将V、P之间的桥梁搭建了起来。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= 21) { //android 5.0以上系统,状态栏也显示图片
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
setContentView(R.layout.activity_login);
LoginFragment loginFragment = (LoginFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (loginFragment == null) {
loginFragment = LoginFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), loginFragment, R.id.contentFrame);
}
mPresenter = new LoginPresenter(loginFragment);
}
总结
MVP架构下,有许多种变换形式,比如使用原始的数据库操作、使用Litepal数据库、接口回调的方式实现异步请求、Arouter实现页面跳转等,可以根据自身的需求来选择架构的实现形式。