以一个用户登录实例,学习MVP 架构。
其中包括登录界面和内容主页界面, 这两个都使用了MVP 实现,思路是一样的。最后给出了UML 类图及总结。
最好是自己对着例子写一遍,加深印象和理解。
目录
(1) 用户登录界面, 可以输入账号 和密码, 点击登录后进行验证, 并显示进度条。
(2) 验证结果分为:账号或密码为空,则提示; 其它情况视为验证通过
2.1.3 Presenter 持有登录界面接口(View)的引用,并实现Model 定义的接口
2.1.4 Activity 实现LoginView 接口,创建并持有Presenter对象
2.2.3 Presenter 持有主页内容定义的接口(View)的引用,并实现Model 定义的接口
2.2.4 Activity 实现SimpleContentView 接口,创建并持有Presenter对象
1. 简单概述
(1) 用户登录界面, 可以输入账号 和密码, 点击登录后进行验证, 并显示进度条。
(2) 验证结果分为:账号或密码为空,则提示; 其它情况视为验证通过
(3) 验证通过后, 模拟加载页面并显示内容
(4) 点击内容提示 (Toast 提示,省略截图)
2. 实现代码
2.1 登录界面
2.1.1 抽象登录界面接口(View)
public interface LoginView {
void showProgress();
void hideProgress();
void setUserNameError();
void setPasswordError();
void navigateToHome();
}
提供显示、隐藏进度条, 设置用户名、密码错误提示,验证通过后进入内容主页操作
2.1.2 模拟服务器验证 (Model)
public class LoginIterator {
public interface OnLoginFinishedListener{
void onUsernameError();
void onPasswordError();
void onSuccess();
}
public void login(final String username, final String password, final OnLoginFinishedListener listener){
new Handler().postDelayed(()-> {
if(TextUtils.isEmpty(username)) {
listener.onUsernameError();
return;
}
if(TextUtils.isEmpty(password)) {
listener.onPasswordError();
return;
}
listener.onSuccess();
}, 2000);
}
}
模拟对用户名 和 密码进行验证操作, 并且通过接口返回验证结果
2.1.3 Presenter 持有登录界面接口(View)的引用,并实现Model 定义的接口
//模拟向服务器验证用户名和密码,并把验证结果转发给View
public class LoginPresenter implements LoginIterator.OnLoginFinishedListener {
private LoginView mLoginView;
private final LoginIterator mLoginInterator;
public LoginPresenter(LoginView loginView, LoginIterator loginIterator) {
mLoginView = loginView;
mLoginInterator = loginIterator;
}
public void verify(String username, String password) {
if (mLoginView != null) {
mLoginView.showProgress();
}
mLoginInterator.login(username, password, this);
}
public void onDestroy(){
mLoginView = null;
}
@Override
public void onUsernameError() {
if (mLoginView != null) {
mLoginView.setUserNameError();
mLoginView.hideProgress();
}
}
@Override
public void onPasswordError() {
if (mLoginView != null) {
mLoginView.setPasswordError();
mLoginView.hideProgress();
}
}
@Override
public void onSuccess() {
if (mLoginView != null) {
mLoginView.navigateToHome();
}
}
}
实际上传入的对象 loginView 是LoginView 接口的具体实现类。
2.1.4 Activity 实现LoginView 接口,创建并持有Presenter对象
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import androidx.appcompat.app.AppCompatActivity;
import com.example.mvplogindemo.model.LoginIterator;
import com.example.mvplogindemo.presenter.LoginPresenter;
import com.example.mvplogindemo.ui.LoginView;
import com.example.mvplogindemo.ui.SimpleContentActivity;
public class LoginActivity extends AppCompatActivity implements LoginView {
private EditText mUserNameEt;
private EditText mPasswordEt;
private Button mLoginBtn;
private ProgressBar mLoginProgress;
private LoginPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
mUserNameEt = findViewById(R.id.username);
mPasswordEt = findViewById(R.id.password);
mLoginBtn = findViewById(R.id.login_button);
mLoginProgress = findViewById(R.id.login_progress);
mLoginBtn.setOnClickListener(v -> verify());
mPresenter = new LoginPresenter(this, new LoginIterator());
}
@Override
protected void onDestroy() {
mPresenter.onDestroy();
super.onDestroy();
}
@Override
public void showProgress() {
mLoginProgress.setVisibility(View.VISIBLE);
}
@Override
public void hideProgress() {
mLoginProgress.setVisibility(View.GONE);
}
@Override
public void setUserNameError() {
mUserNameEt.setError(getResources().getString(R.string.username_error));
}
@Override
public void setPasswordError() {
mPasswordEt.setError(getResources().getString(R.string.password_error));
}
@Override
public void navigateToHome() {
startActivity(new Intent(this, SimpleContentActivity.class));
finish();
}
private void verify() {
mPresenter.verify(mUserNameEt.getText().toString(), mPasswordEt.getText().toString());
}
}
可以看到,
(1) Activity 中仅对View 进行加载 和更新操作, 逻辑操作会转发Presenter, 并且没有持有Model 的引用, 这也是MVP 的核心思想 (View Model 解耦)。
(2) SimpleContentActivity 是模拟登陆成功后, 主页显示的内容,在下面会介绍。
(3) activity_login.xml 是一个线性布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="vertical">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_username"
android:gravity="center_vertical"
android:hint="@string/user_name"
android:inputType="text"/>
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_password"
android:gravity="center_vertical"
android:hint="@string/password"
android:inputType="text"/>
<Button
android:id="@+id/login_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login"
android:layout_marginTop="8dp"/>
<ProgressBar
android:id="@+id/login_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone"/>
</LinearLayout>
其中, android:drawableStart="@drawable/ic_username" 和 android:drawableStart="@drawable/ic_password" 表示输入框的前面的图标, 这里是用 vector标签实现的,Android studio 可以很方便的添加。
ic_username.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M3,5v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2L5,3c-1.11,0 -2,0.9 -2,2zM15,9c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3zM6,17c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1L6,18v-1z"/>
</vector>
ic_password.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
至此,关于登录界面的实现,已经是使用了MVP 去实现的。
可以看出,Presenter 是核心类, 使得View 和Model 分离, 并且Presenter 到 View / Model 到Presenter 是使用接口调用的, 使得代码更加利于管理和维护(即类间的联系关注接口 和它的实现即可)
登录后的内容也可以使用MVP 实现。
2.2 内容主页
同样是按上面的思路, 先对View 的操作进行抽象,再模拟Model 提供数据,接着Presenter 持有View 的接口实现类和Model对象, 最后Activity 实现View接口并创建Presenter.
这里显示的内容数据使用到RecyclerView, 对应会有RecyclerViewAdapter.
2.2.1 抽象内容主页操作接口(View)
import java.util.List;
public interface SimpleContentView {
void showProgress();
void hideProgress();
void setItems(List<String> items);
void showMessage(String message);
}
其中包含显示、隐藏进度条, 设置RecyclerView的数据(setItems), 以及显示点击消息(showMessage)
2.2.2 模拟向服务器获取主页数据 (Model)
import android.os.Handler;
import java.util.Arrays;
import java.util.List;
public class FindItemsIterator {
public interface OnFinishedListener {
void onFinished(List<String> items);
}
public void findItems(final OnFinishedListener listener) {
new Handler().postDelayed(() -> listener.onFinished(createContentList()), 2000);
}
private List<String> createContentList() {
return Arrays.asList(
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",
"Item 6",
"Item 7",
"Item 8",
"Item 9"
);
}
}
其中
(1) findItems 为提供获取数据的接口,供Presenter 调用。 此处使用Handler 延迟两秒返回数据。
(2) OnFinishedListener 接口用于获取数据后,回调通知调用者(即Presenter需要实现这个接口)
(3) createContentList 模拟了返回的数据
2.2.3 Presenter 持有主页内容定义的接口(View)的引用,并实现Model 定义的接口
import com.example.mvplogindemo.model.FindItemsIterator;
import com.example.mvplogindemo.ui.SimpleContentView;
import java.util.List;
public class SimpleContentPresenter {
private SimpleContentView mSimpleContentView;
private final FindItemsIterator mFindItemsIterator;
public SimpleContentPresenter(SimpleContentView simpleContentView, FindItemsIterator findItemsIterator){
mSimpleContentView = simpleContentView;
mFindItemsIterator = findItemsIterator;
}
public void onResume(){
if (mSimpleContentView != null) {
mSimpleContentView.showProgress();
}
mFindItemsIterator.findItems(this::onFinished);
}
public void onFinished(List<String> items){
if (mSimpleContentView != null) {
mSimpleContentView.hideProgress();
mSimpleContentView.setItems(items);
}
}
public void onDestroy() {
mSimpleContentView = null;
}
}
其中,
(1) onResume/ onDestroy 方法供Activity 生命周期的调用, onResume 时显示进度条, 并且向Model 发起获取数据请求
(2) onFinished 为Model 的接口回调函数, 传回来数据,Presenter 再把它转发给Activity(View)
(3) 注意解除view的引用: mSimpleContentView = null;
2.2.4 Activity 实现SimpleContentView 接口,创建并持有Presenter对象
package com.example.mvplogindemo.ui;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.example.mvplogindemo.R;
import com.example.mvplogindemo.model.FindItemsIterator;
import com.example.mvplogindemo.presenter.SimpleContentPresenter;
import java.util.List;
public class SimpleContentActivity extends AppCompatActivity implements SimpleContentView{
private RecyclerView mRecyclerView;
private ProgressBar mProgressBar;
private ContentRecyclerViewAdapter mContentRecyclerViewAdapter;
private SimpleContentPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_content);
mRecyclerView = findViewById(R.id.content_recyclerview);
mProgressBar = findViewById(R.id.content_progress);
initRecyclerView();
mPresenter = new SimpleContentPresenter(this, new FindItemsIterator());
}
@Override
protected void onDestroy() {
mPresenter.onDestroy();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
mPresenter.onResume();
}
private void initRecyclerView() {
mContentRecyclerViewAdapter = new ContentRecyclerViewAdapter();
//1. layoutManager
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
//2. adapter
mRecyclerView.setAdapter(mContentRecyclerViewAdapter);
//3. listener
mContentRecyclerViewAdapter.setOnItemClickListener(new ContentRecyclerViewAdapter.onItemClickListener() {
@Override
public void onItemClick(String title) {
showMessage(title);
}
});
}
@Override
public void showProgress() {
mProgressBar.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.INVISIBLE);
}
@Override
public void hideProgress() {
mProgressBar.setVisibility(View.INVISIBLE);
mRecyclerView.setVisibility(View.VISIBLE);
}
@Override
public void setItems(List<String> items) {
mContentRecyclerViewAdapter.setItems(items);
}
@Override
public void showMessage(String message) {
Toast.makeText(this, message + " clicked", Toast.LENGTH_SHORT).show();
}
}
其中,
(1) Activity 中仍是不会持有Model 引用, 仅是通过Presenter 交互
(2) 布局文件 activity_simple_content.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.SimpleContentActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/content_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar
android:id="@+id/content_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout>
相当简单, 仅包含一个RecyclerView 和一个 ProgressBar
需要注意的是,使用RecyclerView时,需要在配置文件(app 目录的build.gradle) 中添加依赖包, 否则编译器会报错
apply plugin: 'com.android.application'
dependencies {
//省略其它的
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
(3) RecyclerView 对应的RecyclerViewAdapter 适配器:
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.mvplogindemo.R;
import java.util.ArrayList;
import java.util.List;
public class ContentRecyclerViewAdapter extends RecyclerView.Adapter<ContentRecyclerViewAdapter.InnerViewHolder> {
private List<String> mItems;
private onItemClickListener mOnItemClickListener;
public ContentRecyclerViewAdapter(){
mItems = new ArrayList<>();
}
public void clearItems(){
mItems.clear();
}
public void setItems(List<String> items){
clearItems();
mItems.addAll(items);
notifyDataSetChanged();
}
@NonNull
@Override
public ContentRecyclerViewAdapter.InnerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = View.inflate(parent.getContext(), R.layout.content_item_view, null);
InnerViewHolder holder = new InnerViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(@NonNull ContentRecyclerViewAdapter.InnerViewHolder holder, int position) {
final String title = mItems.get(position);
holder.setData(title);
holder.titleTv.setOnClickListener(v -> {
if (mOnItemClickListener!=null) {
mOnItemClickListener.onItemClick(title);
}
});
}
@Override
public int getItemCount() {
return mItems == null ? 0 : mItems.size();
}
class InnerViewHolder extends RecyclerView.ViewHolder{
public TextView titleTv;
public InnerViewHolder(@NonNull View itemView) {
super(itemView);
titleTv = itemView.findViewById(R.id.title);
}
public void setData(String title) {
titleTv.setText(title);
}
}
public interface onItemClickListener {
void onItemClick(String title);
}
public void setOnItemClickListener(onItemClickListener listener) {
mOnItemClickListener = listener;
}
}
这里无非就是 Adapter 的使用,存储数据、创建ViewHolder、绑定数据、返回条码数量、提供点击回调接口 等等, 可以参考之前的RecyclerView 的使用介绍。
需要注意的是setItems方法, Activity 在收到Presenter的转发后(也是setItems), 会调用这个方法,进行数据的更新, 并且 要调用父类 RecyclerView.Adapter 的 notifyDataSetChanged去更新数据和刷新界面。
item view 的布局很简单(content_item_view.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="item "
android:gravity="center"
android:textSize="20sp"/>
</LinearLayout>
至此, 内容主页的代码也完成,和登录界面的思想是类似的, 为了对View 和 Model 解耦。
此外, Presenter 也可抽象为一个公用的接口,抽象一些公用的方法,如Activity生命周期相关的函数,并且可以持有一个View的泛型接口或泛型抽象类, 增强通用性。