天天看點

Android中的MVP架構模式

作者:雨風1930

初級的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的使用者的輸入執行動作。
Android中的MVP架構模式

MVP架構

MVP架構要點

  1. View和Presenter以及Presenter和Model之間通過接口(也稱為contract)通信。
  2. 一個Presenter管理一個View,即:presenter和view是一對一的關系。
  3. Model和View之間無關聯。

MVP結構示例

為了展示MVP架構模式的實作,接下來我們完成隻有一個activity的android應用程式。應用程式将在View(activity)上顯示一些字元,這些字元從Model中随機選擇。Presenter的作用是保持業務邏輯和activity的分離。下面将介紹如何一步一步地實作。注意:我們将使用Java語言來實作該項目。

以下步驟在Android Studio 4.0版本上調試通過。

Step 1:建立一個新的項目

  1. 軟體菜單:File -> New => New Project。
  2. 選擇Empty activity
  3. 選擇程式設計語言Java
  4. 選擇最小的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層将變得臃腫。

譯者注:

  1. View、Model、Presenter通過依賴(實作)抽象接口,實作了他們之間的互相無依賴。
  2. Activity實作了View接口,并且有一個(has a)Presenter實作
  3. Presenter實作了Presenter接口和Model接口中的回調部分,有一個View的實作,有一個Model的實作,這2個實作都是在View建立的時候傳入的。
  4. Model實作了Model接口,注意,在他的一些方法中以接口的方式可以回調Presenter傳入的實作。

By 楊玉鋒@2023/7/29

英文原文:https://www.geeksforgeeks.org/mvp-model-view-presenter-architecture-pattern-in-android-with-example/

繼續閱讀