初级的Android开发工程师,编写代码时会创建MainActivity类,应用程序的所有逻辑都放到了这个类中。这种开发方式导致UI和业务耦合在一起,间接带来的问题是维护或扩展变得困难。为提升应用程序的可维护性、可读性、可扩展性等,通常采用分层思想,通过使用架构模式可以将UI与业务逻辑进行分离。MVP(Mode-View-Presenter)是最流行的架构模式之一。
MVP是传统MVC架构模式的扩展。使用MVC架构模式,开发者常会遇到以下困难:
- 大部分的核心业务逻辑放在Controller中,在应用程序的整个生命周期内,这个文件会变得越来越大,越来越难维护。
- 由于UI和业务逻辑的紧密耦合,Controller层和View层都将属于同一个activity或fragment。这将导致在更改应用程序功能时出现问题。
- 由于大多数测试的部分依赖Android SDK组件,因此针对不同层执行单元测试时变得困难了。
MVP模式克服了MVC模式的这些挑战,并且提供了一种简单的方法来构造项目代码。MVP模式之所以被广泛接受,因为它提供了模块化、可测试以及更干净和更易于维护的代码基准。它由以下三部分组成:
- Model:用于存储数据。它负责处理领域逻辑以及与数据库或网络层的通信。
- View:UI层,提供数据可视化界面,并跟踪用户的操作,以便通知presenter。
- Presenter:从Model层获取数据,并且应用UI逻辑来决定显示什么。它管理View的状态,并且根据来自于View的用户的输入执行动作。
MVP架构
MVP架构要点
- View和Presenter以及Presenter和Model之间通过接口(也称为contract)通信。
- 一个Presenter管理一个View,即:presenter和view是一对一的关系。
- Model和View之间无关联。
MVP结构示例
为了展示MVP架构模式的实现,接下来我们完成只有一个activity的android应用程序。应用程序将在View(activity)上显示一些字符,这些字符从Model中随机选择。Presenter的作用是保持业务逻辑和activity的分离。下面将介绍如何一步一步地实现。注意:我们将使用Java语言来实现该项目。
以下步骤在Android Studio 4.0版本上调试通过。
Step 1:创建一个新的项目
- 软件菜单:File -> New => New Project。
- 选择Empty activity
- 选择编程语言Java
- 选择最小的SDK依赖
Step 2:修改string.xml
这个文件中配置好项目中需要的所有的字符串,如下:
<resources>
<string name="app_name">GfG | MVP Architecture</string>
<string name="buttonText">Display Next Course</string>
<string name="heading">MVP Architecture Pattern</string>
<string name="subHeading">GeeksforGeeks Computer Science Online Courses</string>
<string name="description">Course Description</string>
</resources>
Step 3: 编辑activity_main.xml文件
打开activity_main.xml文件,加上一个Button,一个TextView(用于显示字符串)和一个Progress bar(用于给出动态效果),下面是代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:background="#168BC34A"
tools:context=".MainActivity">
<!-- TextView to display heading of the activity -->
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/roboto"
android:text="@string/heading"
android:textAlignment="center"
android:textColor="@android:color/holo_green_dark"
android:textSize="30sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.060000002" />
<!-- TextView to display sub heading of the activity -->
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/roboto"
android:text="@string/subHeading"
android:textAlignment="center"
android:textColor="@android:color/holo_green_dark"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.356" />
<!-- TextView to display the random string -->
<TextView
android:id="@+id/textView"
android:layout_width="411dp"
android:layout_height="wrap_content"
android:fontFamily="@font/roboto"
android:gravity="center"
android:padding="8dp"
android:text="@string/description"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2"
app:layout_constraintVertical_bias="0.508" />
<!-- Button to display next random string -->
<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@android:dimen/notification_large_icon_height"
android:background="#4CAF50"
android:text="@string/buttonText"
android:textAllCaps="true"
android:textColor="@android:color/background_light"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.79" />
<!-- Progress Bar to be displayed before displaying next string -->
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
app:layout_constraintVertical_bias="1.0"
app:srcCompat="@drawable/banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
Step 4:定义Model、View和Presenter用到的接口(Contract Interface)
在View-Presenter和Presenter-Model之间建立通信,需要一些接口。下面的接口类中包含了在View、Model和Presenter中用到的所有的抽象方法
public interface Contract {
interface View {
// method to display progress bar
// when next random course details
// is being fetched
void showProgress();
// method to hide progress bar
// when next random course details
// is being fetched
void hideProgress();
// method to set random
// text on the TextView
void setString(String string);
}
interface Model {
// nested interface to be
interface OnFinishedListener {
// function to be called
// once the Handler of Model class
// completes its execution
void onFinished(String string);
}
void getNextCourse(Contract.Model.OnFinishedListener onFinishedListener);
}
interface Presenter {
// method to be called when
// the button is clicked
void onButtonClick();
// method to destroy
// lifecycle of MainActivity
void onDestroy();
}
}
Step 5:创建Model类
创建名字为Model的类,以分离所有的字符串数据和获取他们的方法,Model类完全不知道View类的存在。
import android.os.Handler;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class Model implements Contract.Model {
// array list of strings from which
// random strings will be selected
// to display in the activity
private List<String> arrayList = Arrays.asList(
"DSA Self Paced: Master the basics of Data Structures and Algorithms to solve complex problems efficiently. ",
"Placement 100: This course will guide you for placement with theory,lecture videos, weekly assignments " +
"contests and doubt assistance.",
"Amazon SDE Test Series: Test your skill & give the final touch to your preparation before applying for " +
"product based against like Amazon, Microsoft, etc.",
"Complete Interview Preparation: Cover all the important concepts and topics required for the interviews. " +
"Get placement ready before the interviews begin",
"Low Level Design for SDE 1 Interview: Learn Object-oriented Analysis and Design to prepare for " +
"SDE 1 Interviews in top companies"
);
@Override
// this method will invoke when
// user clicks on the button
// and it will take a delay of
// 1200 milliseconds to display next course detail
public void getNextCourse(final OnFinishedListener listener) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
listener.onFinished(getRandomString());
}
}, 1200);
}
// method to select random
// string from the list of strings
private String getRandomString() {
Random random = new Random();
int index = random.nextInt(arrayList.size());
return arrayList.get(index);
}
}
Step 6:创建Presenter类
Presenter类中的方法包含了所有的业务逻辑,比如显示什么以及如何显示,它触发View类完成对UI的更改。
public class Presenter implements Contract.Presenter, Contract.Model.OnFinishedListener {
// creating object of View Interface
private Contract.View mainView;
// creating object of Model Interface
private Contract.Model model;
// instantiating the objects of View and Model Interface
public Presenter(Contract.View mainView, Contract.Model model) {
this.mainView = mainView;
this.model = model;
}
@Override
// operations to be performed
// on button click
public void onButtonClick() {
if (mainView != null) {
mainView.showProgress();
}
model.getNextCourse(this);
}
@Override
public void onDestroy() {
mainView = null;
}
@Override
// method to return the string
// which will be displayed in the
// Course Detail TextView
public void onFinished(String string) {
if (mainView != null) {
mainView.setString(string);
mainView.hideProgress();
}
}
}
Step 7:在MainActivity中定义View的功能
View类负责根据Presenter的触发来更新UI。View将使用Model提供的数据,并将变化反映到activity上。
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import static android.view.View.GONE;
public class MainActivity extends AppCompatActivity implements Contract.View {
// creating object of TextView class
private TextView textView;
// creating object of Button class
private Button button;
// creating object of ProgressBar class
private ProgressBar progressBar;
// creating object of Presenter interface in Contract
Contract.Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// assigning ID of the TextView
textView = findViewById(R.id.textView);
// assigning ID of the Button
button = findViewById(R.id.button);
// assigning ID of the ProgressBar
progressBar = findViewById(R.id.progressBar);
// instantiating object of Presenter Interface
presenter = new Presenter(this, new Model());
// operations to be performed when
// user clicks the button
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.onButtonClick();
}
});
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.onDestroy();
}
@Override
// method to display the Course Detail TextView
public void showProgress() {
progressBar.setVisibility(View.VISIBLE);
textView.setVisibility(View.INVISIBLE);
}
@Override
// method to hide the Course Detail TextView
public void hideProgress() {
progressBar.setVisibility(GONE);
textView.setVisibility(View.VISIBLE);
}
@Override
// method to set random string
// in the Course Detail TextView
public void setString(String string) {
textView.setText(string);
}
}
MVP架构的优势
与Android组件没有概念关系
model、view和presenter的分离,使得程序易于维护和测试
MVP架构的缺点:
如果开发者不遵守单一职责原则,Presenter层将变得臃肿。
译者注:
- View、Model、Presenter通过依赖(实现)抽象接口,实现了他们之间的相互无依赖。
- Activity实现了View接口,并且有一个(has a)Presenter实现
- Presenter实现了Presenter接口和Model接口中的回调部分,有一个View的实现,有一个Model的实现,这2个实现都是在View创建的时候传入的。
- Model实现了Model接口,注意,在他的一些方法中以接口的方式可以回调Presenter传入的实现。
By 杨玉锋@2023/7/29
英文原文:https://www.geeksforgeeks.org/mvp-model-view-presenter-architecture-pattern-in-android-with-example/