由教程的Swifui - 新章节!

您在SWIFT中建立流体和宣言UI的最终指南。
现在在第三版中完全更新,具有新的内容和新的App设计。
在我们的商店上提供 - 今天!

首页 安卓& Kotlin Tutorials

在Android上使用MVP(模型视图演示者)入门

在此实际的教程中,我们将一个名为MVP的设计模式应用于MVP,用于模型 - 查看器,到Android应用程序。

4.6/5 14个评分

版本

  • Kotlin 1.3,Android 5.0,Android Studio 3

一个干净的码比始终是一种乐趣。一个良好的组织代码库易于维护,强大,表现良好,是可测试的,是自我记录的。为Android采摘架构可能很棘手,但在本教程中,您将通过使用的方式查看一种方法来实现干净的码 MVP., 短缺 模型看法主持人。您还将了解该模式如何适合Android生态系统。

在本教程中,您将构建一个名为的应用程序 当预测中有下雨时,这将显示一个伞形图标,以及外面晴朗,晴朗的阳光图标。这里重要的是,您将重构此应用程序的初始版本,以便它利用使用 模型看法主持人 图案。您还可以了解如何测试应用程序的重构组件。

入门

使用该教程下载和解压缩这些教程的材料 下载材料 此页面顶部或底部的按钮。在Android Studio 3.2.1或更高版本中打开Starter项目,然后构建并运行以查看您将与您一起使用的应用程序。

MVP.示例应用程序的屏幕截图

好吗? :]如果你点击 装载天气 button, the app will randomly change the weather between sunny and raining. Most of the logic is in a single class — MainActivity.kt. Familiarize yourself with the starter project code, which uses a structure as illustrated in the following diagram:

Android示例应用程序 - 模型视图控制器

天气数据来自 OpenWeatherMap API.。在生产应用程序中,您将使用网络库获取此数据,例如 改造.

To keep this tutorial simple, there is a class named WeatherRepositoryImpl, which inflates a 模型 object named Weather from a JSON payload using moshi.;一个json库for android和java,它可以轻松解析为Java对象。

应用程序的流程很简单。当。。。的时候 看法 完成加载,您获取天气数据。然后,您检查雨值是否是正面的,然后显示雨伞图像或太阳图像。但你不是在ui上的教程,你在这里关于建筑!首先,您将探讨被认为是许多系统的软件架构的传统方法。

MVC:传统方法

传统上写的Android应用程序由三个主要组件组成。

  • 楷模:模型包含显示的数据。通常,此数据从网络或本地数据库获取。然后数据放入其他组件可以使用的小型,简单的类别。
  • 意见:视图是向用户显示的内容。例如,它们还处理用户可能拥有的互动,例如屏幕 - 单击侦听器。视图应仅负责显示事物,不应包含任何业务逻辑。因此,与控制器相比,视图倾向于是轻量级分量,并且通常不包含太多代码。在Android中,观看责任通常会落到 活动碎片.
  • 控制器:控制器是一种将模型和视图连接在一起的方法。当视图中发生某些事情时,控制器更新模型。当模型更改时,控制器还将更新视图。通常,在活动和碎片中也发现了控制器职责。

此架构最为称为MVC。虽然这种缩写往往是糟糕的代名词,但它是在没有任何建筑的情况下良好的架构。但是,它确实有缺陷。

这种方法有什么问题?

在Android平台上实现MVC时,事情会变得棘手。要启动,大多数逻辑最终都在控制器中。具有包含所有逻辑的控制器活动是一个常见的Android问题。然后,控制器对屏幕上显示的内容所有责任。对于一个简单的屏幕,这可以是可管理的,但随着要添加的功能,此文件将继续增长。

此外,Android活动紧密耦合到UI和数据访问机制。因此,容易落入将控制器和视图逻辑放入活动或片段中的陷阱中。但是,这产生了紧密耦合的组件,这使得重构更加难以更改。

视图层应该只关注观点。它不必知道数据库或网络调用。而且,当它们紧密耦合时,难以测试组件。您应该测试控制器中存在的代码。正在测试的大部分代码最终需要运行Android SDK组件,例如活动,需要一个完整的运行设备或仿真器。基本单位测试不会有所帮助;您需要使用昂贵的仪表单元测试来测试应用程序的基本部分。

MVP.:一种替代方法

MVC呈现的问题的一个替代方案是将一些部分彼此分离。 MVP是一种架构模式,您可以用来处理MVC的一些缺点,并且是一个很好的替代架构。它提供了一种考虑您应用结构的简单方法。它提供 模块化, 可测试性 而且,一般来说,更多 干净的可维护 codeBase。

采摘缩写,MVP由以下组件组成:

  • 模型:该模型将继续包含简单类中的数据,因此此处无法在此处进行更改。
  • 看法:将使用活动或片段类继续执行视图,但我们将更改视图控件的范围。
  • 主持人: 最后部分是演示者,它根据数据模型的更改处理UI更新,也处理用户输入。演示者将包含大部分业务代码,并从MVC替换控制器。

在MVP中,而不是使用控制器活动类,它将两个更改处理到模型以及屏幕上显示的内容,控制器和视图部件分开,而Prinkeer和视图都变轻。

数据 (模型)和UI(看法),只通过中介互相互动( 主持人)。这 主持人 包含大部分业务逻辑,而该视图侧重于如何显示数据。现在,控制器责任在视图和演示者之间分开。演示者处理数据流,并从控制器上摘要业务逻辑。特定于android的代码留在视图层中,并且可以从Android SDK独立测试演示者。

那么这些组件之间的数据如何流动?看看这个图:

MVP.图

在代码中,您将看到定义Photoser和视图的接口。接口有助于解耦架构的部分。该界面在演示者之间形成了合同和视图。他们还将帮助您稍后定义和编写测试用例。

当数据有更改时,通知演示者数据已更改。然后,视图将通过演示者收到该数据,并使用来自演示者的数据更新自身。您也可以与视图中的事件相反。当用户与视图交互时,调用播放器上的方法。然后,演示者调用适当的方法以更新模型。

好的,足够的喋喋不休,时间来重构! :]

重构伞

您将要将伞形应用程序转换为MVP架构。这是您将添加到项目的组件的图表,以及每个组件如何相互交互。

Android示例应用程序 - 模型视图演示者

组织特色

管理应用程序部分的一种流行方式是通过 特征。一个功能由 模型, 这 意见, 这 主持人, 也 依赖注入 (di)代码创建和提供每个组件。这样,您可以将应用程序添加和删除作为模块的功能。

Your app has only one feature: the main screen. You are going to implement MVP for the main screen, and you will create some components prefixed with Main.

添加依赖注入

在本节中,您将创建手工依赖性注射器。

要启动,请创建一个名为的接口 依赖indenceIncillor. in the root com.raywenderlich.android.rwandroidtutorial/ package.

interface DependencyInjector {
​  fun weatherRepository() : WeatherRepository
}

接下来,创建一个类 decigendenceInjectorimpl. 在实现接口的同一包中。

class decigendenceInjectorimpl. : DependencyInjector {
  override fun weatherRepository() : WeatherRepository {
​    return WeatherRepositoryImpl()
  }
}

这里没有严格的原因,为什么我们将这个依赖性注射器类分成一个接口和一个实施类,但是,如果您希望在将来的不同实现中交换,它就被认为是良好的做法。

笔记:在一个生产应用程序中,您将在Android生态系统中的任何DI框架中进行选择(例如, 匕首2.),这将有助于与MVP架构应用程序一起使用。

要了解更多信息,请阅读教程 Android中的依赖注入与匕首2和Kotlin 或者 依赖注射用koin.

定义合同

我们还具有界面来定义演示者和视图。接口有助于解耦应用程序的部分。该界面在演示者之间形成了合同和视图。

First, create a new file named BasePresenter.kt in the same base package you’ve been working in, and add the following code:

interface BasePresenter {
​  fun onDestroy()
}

This is a generic interface that any presenter you add to your project should implement. It contains a single method named onDestroy() that basically acts as a facade for the Android lifecycle callback.

Also, create a new file named BaseView.kt, and add the following code:

interface BaseView<T> {
  fun setPresenter(presenter : T)
}

Similar to BasePresenter, this is the interface that all views in your app should implement. Since all views interact with a presenter, the view is given a generic type T for the presenter, and they must all contain a setPresenter() method.

接下来,创建命名的合同界面 主协会,它为主屏幕定义了视图和演示者的接口,并将其更新为如下所示:

interface MainContract {
  interface Presenter : BasePresenter {
​    fun onViewCreated()
​    fun onLoadWeatherTapped()
  }

  interface View : BaseView<Presenter> {
​    fun displayWeatherState(weatherState: WeatherState)
  }
}

Notice here that you’re creating interfaces for the specific activity, and that they inherit from the the base interfaces we previously defined. You can see that 主协会.Presenter is interested in being called back by the 主协会.View when the view is created through onViewCreated() 和 when the user taps on the “Load Weather” button through onLoadWeatherTapped(). Similarly, the view can be invoked to display weather information through displayWeatherState(), which is only called by the presenter.

定义演示者

You have your interfaces in place. Now it’s a matter of assigning these responsibilities to the proper class. First, create a new file named MainPresenter.kt, and set it up as follows:

// 1
class MainPresenter(view: MainContract.View,
                    dependencyInjector: DependencyInjector)
  : MainContract.Presenter {
  // 2
  private val weatherRepository: WeatherRepository
      = dependencyInjector.weatherRepository()

  // 3
  private var view: MainContract.View? = view
}

将此代码占用:

  1. Photoser构造函数占据了视图的实例,以及之前创建的依赖性注射器,它用于获取模型的实例。
  2. The presenter holds on to an instance of the WeatherRepository, which in this app is the model.
  3. The presenter also holds on to a reference to the view; however, note that it interacts with the interface only, as defined in 主协会.

Next, move two private methods from MainActivity into the presenter.

private fun loadWeather() {
  val weather = weatherRepository.loadWeather()
  val weatherState = weatherStateForWeather(weather)

  // Make sure to call the displayWeatherState on the view
  view?.displayWeatherState(weatherState)
}

private fun weatherStateForWeather(weather: Weather) : WeatherState {
​  if (weather.rain!!.amount!! > 0) {
​    return WeatherState.RAIN
​  }
​  return WeatherState.SUN
}

There’s nothing remarkable about these methods, however, be sure to forward the call to displayWeatherState() in loadWeather() to your view object:

view?.displayWeatherState(weatherState)  

最后,通过添加以下方法来实现其余的演示者合同:

override fun onDestroy() {
  this.view = null
}

override fun onViewCreated() {
​  loadWeather()
}

override fun onLoadWeatherTapped() {
​  loadWeather()
}

Here, you do some clean up in onDestroy() 和 invoke fetching the weather data in both onViewCreated()onLoadWeatherTapped().

注意的一个重要点是 演示者没有使用Android API的代码.

写下观点

Now, replace the content of MainActivity.kt with the following:

package com.raywenderlich.android.rwandroidtutorial

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Button
import android.widget.ImageView

// 1
class MainActivity : AppCompatActivity(), MainContract.View {
  internal lateinit var imageView: ImageView
  internal lateinit var button: Button

  // 2
  internal lateinit var presenter: MainContract.Presenter

  override fun onCreate(savedInstanceState: Bundle?) {
​    super.onCreate(savedInstanceState)
​    setContentView(R.layout.activity_main)

    imageView = findViewById(R.id.imageView)
    button = findViewById(R.id.button)
    
    // 3
    setPresenter(MainPresenter(this, decigendenceInjectorimpl.()))
    presenter.onViewCreated()
    
    // 4
    button.setOnClickListener { presenter.onLoadWeatherTapped() }
  }

  // 5
  override fun onDestroy() {
​    presenter.onDestroy()
​    super.onDestroy()
  }

  // 6
  override fun setPresenter(presenter: MainContract.Presenter) {
​    this.presenter = presenter
  }

  // 7
  override fun displayWeatherState(weatherState: WeatherState) {
​    val drawable = resources.getDrawable(weatherDrawableResId(weatherState),
​            applicationContext.getTheme())
​    this.imageView.setImageDrawable(drawable)
  }

  fun weatherDrawableResId(weatherState: WeatherState) : Int {
​    return when (weatherState) {
​      WeatherState.SUN -> R.drawable.ic_sun
​      WeatherState.RAIN -> R.drawable.ic_umbrella
​    }
  }
}

Let’s highlight the most important changes made to MainActivity:

  1. Implement the 主协会.View interface. This jives well with our expectations of views.
  2. Add a 主持人 property instead of the model weatherRepository. As was previously mentioned, the view needs the presenter to invoke user initiated callbacks.
  3. Store a reference to the presenter just after creating it. Notice that it also creates and passes an instance of decigendenceInjectorimpl. as part of the creation.
  4. 卸载按钮回调到演示者的处理。
  5. 当视图被销毁时通知赠送者。回想一下,演示者使用这个机会清理任何不再需要这种情况的国家。
  6. Implement the method required from the BaseView interface to set the presenter.
  7. Add override to the displayWeatherState() method, since it is now part of the view interface.

构建并运行应用程序只是为了确保它仍然有效。

MVP.示例应用程序的屏幕截图

Based on the refactoring you’ve done to MainActivity, it should be clear how the app data flows and how the plumbing is set up between the model, the view and the presenter.

恭喜!您可以使用不同的架构MVP-ified您的应用程序。您已设法将所有业务逻辑提取到现在可以轻松测试轻量级单元测试的地方。你会在下一节解决这个问题。

测试演示者

一旦使用MVP作为架构,测试您的应用会更容易。

开始,添加 Mockito. 在应用程序模块中的测试依赖项 build.gradle. file:

  
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

// Add mockito to your project
testImplementation 'org.mockito:mockito-core:2.22.0'

如果您不熟悉Mockito,只知道它是写作单元测试的嘲弄框架。如果您希望深入潜入框架,请查看我们的 教程 all about it.

有一个空的 单元测试 在测试包中命名的文件 mainpresentertest.kt.。将文件的内容更新为如下:

package com.raywenderlich.android.rwandroidtutorial

import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.junit.MockitoJUnitRunner

@RunWith(MockitoJUnitRunner::class)
class MainPresenterTest {

  @Mock
  private lateinit var mockMainActivity: MainContract.View

  private val dependencyInjector: DependencyInjector = StubDependencyInjector()

  private var presenter: MainPresenter? = null

  @Before
  fun setUp() {
​    MockitoAnnotations.initMocks(this)
​    presenter = MainPresenter(mockMainActivity, dependencyInjector)
  }

  @After
  fun tearDown() {
​    presenter?.onDestroy()
  }

  @Test
  fun testOnViewCreatedFlow() {
​    presenter?.onViewCreated()
​    verify(mockMainActivity).displayWeatherState(WeatherState.RAIN)
  }
}

class StubDependencyInjector : DependencyInjector {
  override fun weatherRepository(): WeatherRepository {
​    return StubWeatherRepository()
  }
}
class StubWeatherRepository : WeatherRepository {
  override fun loadWeather(): Weather {
​    var weather = Weather("xxx")
​    var rain = Rain()
​    rain.amount = 10
​    weather.rain = rain
​    return weather
  }
}

You’ve added a test class named MainPresenterTest to the file, and added a single test to the class named testOnViewCreatedFlow(). You added the mocking, stubbing, setup and tear down code you need to run the one test.

当测试说明时,你可以嘲笑 看法 interface (which in the app is implemented by MainActivity) and test your business logic with a simple unit test. The presenter is absent of Android framework components, and thus you can test it using lightweight unit tests rather than instrumented UI tests. The business logic in Umbrella, i.e. 如果雨量大于0,则应显示雨量图标,居住在演示者中。因此,为此逻辑添加测试非常重要,并且单元测试简单快速地编写和运行。

Go ahead and click the play triangle next to testOnViewCreatedFlow() 和 select 运行testonviewcreatedflow() 要查看当前运行和通过的单元测试。

注意,最终项目还包含一个名为的仪器测试文件 MainActityTest.kt., 在里面 和roidtest. package. The weatherIconIsLoadedInImageView() test verifies that the icon is correctly displayed in the view. With an instrumented UI test, you can also test your view components, but they must be run on an Android emulator or device, and so do not run as fast as unit tests.

MVP.的陷阱

不幸的是,它并非所有的阳光和MVP的彩虹。既然你已经看到了MVP架构在练习中的架构如何在Android平台上,你是一个更好的地方,了解其一些缺点。

查看生命周期

在任何给定的时间,活动和任何关联的片段(视图)可能被销毁。由于演示者对视图引用,您可以尝试更新已分离的活动。

In your sample app, you added a method named onDestroy() to BasePresenter. When the activity lifecycle method onDestroy() gets called on MainActivity, you call MainPresenter::onDestroy(), which sets the presenter reference to 看法 to null. Kotlin null safety features prevent you from calling a method on a null view.

提示代码重用

大多数逻辑将存在于您的应用程序的演示者中。在具有许多互动的屏幕上,演示者可以变得相当大。创建BasePresenters和实用程序函数是最简单的代码重用路径。演示者只能附加特定视图。内部的任何代码可能都不能重复使用,并且既不能说明。如果您有一个可能重用相同视图的应用程序,则难以在它们之间具有常见的演示者。

您可以编写BasePresenter,并在所有Photosers之间进行常见的函数,以便重用。其中大部分都是记录代码和生命周期代码,用实用程序函数最好处理的东西。

主持人

演示者也取决于其状态。一些方法可能需要检查演示者的当前状态以便运行。数据以两个方向流向模型和视图。演示者的行为类似于一个状态机,但实际上并不具有一个功能,这在试图弄清楚这些状态时可能导致尴尬。

然后去哪儿?

本教程概述了使用简单但有效的模式,使您的代码更容易使用。 MVP已使用超越移动编程,也用于桌面和Web应用程序。分离你担忧的想法是一个强大的想法。考虑您的需求,并探索其他一些方法可以帮助保持代码模块化和可测试。

您可以使用使用的最终项目 下载材料 此页面顶部或底部的按钮。

听到有其他模式,例如,你可能会感到惊讶 模型 - 视图 - 查看模型 (MVVM)和 模型视图意图 (mvi)让您探索。发现其他 与Kotlin的Android的常见设计模式 为了使您的Android代码清洁更易于了解Android应用程序的这些常见的设计模式。

深入进入MVP和MVVM,查看我们的 在Android上的MVP.在Android上的MVVM video courses.

另外,结账 Android建筑蓝图。用于讨论和展示Android应用程序的不同架构工具和模式的样本集合。

以下是一些附加链接,也可能有助于实现MVP:

MVP. by Florina Muntenescu - Android开发人员倡导@Google

简单的解释和辩论@ stackoverflow

Android架构@ CodePath

我希望你喜欢这个在模型 - 查看-pepler方面开始的教程!如果您有任何疑问或意见,请加入下面的论坛讨论。

平均评级

4.6/5

为此内容添加评级

14 ratings

更像这样的

贡献者

评论