安卓& Kotlin Books 匕首by Tutorials

2
遇见Busso App 由Massimo Carli撰写

在本书的第一章中,您就学会了什么 依赖性 意思是,不同类型的依赖关系是什么以及它们如何在代码中表示。您学习了以下内容:

  • 实现继承
  • 作品
  • 聚合
  • 界面继承

您看到了每种类型的依赖性的例子,并且您可以理解在各种情况下更好地工作。使用UML图表,您还学习为什么依赖性是您需要控制的依赖项,如果您希望将代码可维护。您看到为什么使用设计模式应用这些原则对于使代码进行可测试是很重要的。

到目前为止,如果您想成功使用像匕首或刀柄,则掌握许多您需要掌握许多概念的大量理论。)在Android上。现在,现在是时候超越理论并开始编码了。

在本章中,您将了解 Busso. App. ,您将在整本书中努力和改进。它是一个客户端 - 服务器应用程序,其中服务器使用 ktor. .

您将首先在本地安装服务器,或者只是在Heroku上使用预安装版本。然后您将配置,构建和运行Busso Android应用程序。

您从中启动的应用程序的版本是基本的,而不是为之骄傲的东西。您将花费章节的最后一部分了解原因和采取第一步改进它。

BUSSO应用程序

在本书中,您将实现Busso应用程序,允许您在您附近找到巴士站和有关到达时间的信息。该应用程序可在本书的材料部分中提供,并由服务器部件和客户端组成。它使用简单的Classic Client-Server架构,如图2.1所示:

图2.1  - 客户端服务器架构
图2.1 - 客户端服务器架构

图2.1中的UML图是一个 部署图 显示您许多有趣的信息:

  1. 大盒子代表计算机,设备或服务器等物理机器。你打电话给他们 节点 .
  2. 左边缘上有两个小矩形的框 成分 。 这 Busso. App. 组件生活在设备中 BUSSO服务器 生活在服务器机器上,可能在云中。
  3. BUSSO服务器 公开您代表的界面使用称为a 棒糖 。您可以使用标签提供有关通信中使用的特定协议的信息 - 在这种情况下,HTTP。
  4. Busso. App. 与HTTP接口进行交互 BUSSO服务器 提供。用棒棒糖拥有半圆形来表示这一点。

在进入这些组件的详细信息之前,请使用以下步骤运行应用程序。

安装和运行Busso Server

BUSSO服务器 用途 ktor. ,您可以使用Intellij打开。

笔记 :您现在不需要知道详细信息,但如果您有好奇,您可以在本书附录中阅读有关BUSSO服务器的更多信息。

笔记 :如果您不想运行BUSSO服务器 当地 ,运行现有安装 heroku. 。只需跳到下一节即可了解如何使用它。

打开 Busso. Server 项目,您将获得以下目录结构:

图2.2  -  Busso服务器文件结构
图2.2 - Busso服务器文件结构

现在,开放 application.kt. 和 find main function, which looks like this:

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

单击代码左侧的箭头,如图2.3所示:

图2.3  - 从代码运行Busso服务器
图2.3 - 从代码运行Busso服务器

或者,您可以使用相同的图标 配置 IntelliJ工具栏的一部分,如图2.4所示:

图2.4  - 从配置运行Busso服务器
图2.4 - 从配置运行Busso服务器

现在服务器启动,你会看到一些日志消息 跑步 Intellij的一部分,以这样的方式结束:

2020-07-30 01:12:01.177 [main] INFO  Application - No ktor.deployment.watch patterns specified, automatic reload is not active
2020-07-30 01:12:03.320 [main] INFO  Application - Responding at http://0.0.0.0:8080

如果您完成此操作,Busso服务器正在运行。恭喜!

现在,您的下一步是将服务器连接到Busso Android应用程序。

建设和运行Busso Android应用程序

在上一节中,您启动了BUSSO服务器。现在是时候建立并运行Busso Android应用程序了。为此,您需要:

  1. 定义服务器的地址
  2. 配置网络安全性
  3. 构建并运行应用程序

笔记 :如果在Heroku上使用Busso Server,则可以跳过此配置并按照“在Heroku上运行Busso服务器”部分中的说明进行操作。

定义服务器地址

采用 安卓Studio 打开 Busso. 项目在 起动机 本章材料的文件夹。您将看到图2.5中的文件结构。

图2.5  -  Busso Android文件结构
图2.5 - Busso Android文件结构

configuration.kt. 包含以下代码:

// INSERT THE IP FOR YOUR SERVER HERE
const val BUSSO_SERVER_BASE_URL = "http://<YOUR SERVER IP>:8080/api/v1/"

BUSSO应用程序不知道尚未连接到哪里。您需要更改此项以包含您服务器的IP。但是你如何确定IP是什么?您需要在本地网络中查找服务器计算机的IP。

笔记 :你不能只是用 localhost. 或者 127.0.0.1 因为这将是Android设备的IP地址,而不是Busso服务器正在运行的设备。

如果您使用的是MAC,请打开终端并在使用以太网时运行以下命令:

# ipconfig getifaddr en0

或者,如果您是无线:

# ipconfig getifaddr en1

你会得到这样的IP:

# 192.168.1.124

请记住,您的特定IP将与上面所示的IP不同。

On Windows, run the ifconfig command to get the same information from a terminal prompt.

现在,在 configuration.kt., replace <YOUR SERVER IP> with your IP. With the previous value, your code would be:

// INSERT THE IP FOR YOUR SERVER HERE
const val BUSSO_SERVER_BASE_URL = "http://192.168.1.124:8080/api/v1/"

很棒,你已经完成了第一步!

配置网络安全性

如您所见,本地服务器使用HTTP协议,该协议需要在客户端上进行额外的配置。找到和开放 network_security_config.xml. 作为XML类型的资源,如图2.6所示:

图2.6  - 允许来自Android客户端的HTTP协议
图2.6 - 允许来自Android客户端的HTTP协议

您将获得以下XML内容:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true"><!-- YOUR SERVER IP --></domain>
  </domain-config>
</network-security-config>

Next, replace <!-- YOUR SERVER IP --> with the same IP you got earlier.

使用前一个示例中的IP值,您最终可以使用:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">192.168.1.124</domain>
  </domain-config>
</network-security-config>

现在,一切都已设置,您已准备好建立和运行。

建设和运行应用程序

现在,您可以通过选择图2.7所示的箭头来使用仿真器或真实设备运行应用程序:

图2.7  - 运行Busso Android应用程序
图2.7 - 运行Busso Android应用程序

当应用程序首次开始时,它将显示 飞溅屏幕 然后,对话框询问Location权限,如图2.8所示:

图2.8  - 询问许可
图2.8 - 询问许可

当然,如果要使用该应用程序,则必须选择 使用应用程序时允许 选项。这将为您带来图2.9所示的屏幕:

图2.9  - 巴士站附近
图2.9 - 巴士站附近

如果你有类似的结果,很棒!您已成功运行Busso Android应用程序。

你得到假数据 - 你不一定在伦敦:] - 但数据来自Busso Server。对于每个公共汽车站,您会看到类似于图2.10的东西:

图2.10  - 总线停止数据
图2.10 - 总线停止数据

你可以看到:

  • 一个 指标 公共汽车站,就像 M in the picture.
  • 你的 距离 从公共汽车站以米,就像 114 m.
  • 巴士站的名称。例如, 皮卡迪利马戏团Haymarket.
  • 目的地: rw办公室

现在,选择其中一个卡片,您将来到第二个屏幕:

图2.11  - 公共汽车的到达时间
图2.11 - 公共汽车的到达时间

低于有关标题中所选总线停止的信息,您可以看到具有其目的地的所有行的列表,以及到达时间列表。同样,数据是假的,但来自BUSSO服务器。

Busso. 应用程序现在正在运行,您可以通过设计原则开始旅程,具体地,依赖注入。

在heroku上运行busso服务器

如前所述,您可能不希望在自己的计算机上构建和运行BUSSO服务器。相反,您可以在以下地址使用Heroku上可用的运行应用程序:

//busso-server.herokuapp.com/

使用此服务器有两个主要优点:

  • 您不会超载您的机器运行 BUSSO服务器流程.
  • 该应用程序可以使用 https. 协议,而本地安装使用 http. 。使用 https. 协议,您不再需要图2.6中的配置。

您可以通过访问您喜欢的浏览器访问以前的URL,轻松验证服务器是否已启动并运行。如果使用Chrome,您将获得图2.12中所示的内容:

图2.12  - 访问公共Busso服务器
图2.12 - 访问公共Busso服务器

为Heroku Server配置BUSSO应用程序

要在Heroku上使用服务器安装,您需要输入以下代码 configuration.kt.:

const val BUSSO_SERVER_BASE_URL = "//busso-server.herokuapp.com/api/v1/"

接下来,您需要将有效值放入 XML. 资源文件夹中 network_security_config.xml., 像这样:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">0.0.0.0</domain>
  </domain-config>
</network-security-config>

只要它是有效的IP地址,您使用的特定IP并不重要。

改善BUSSO应用程序

你喜欢BUSSO应用吗?好吧,它有效,但你不能说质量是最好的。但是有什么问题,你怎么能解决它们?

具体来说,Busso应用程序有:

  • 很多复制和粘贴的代码,导致重复您应该避免。
  • 没有生命周期或范围的概念。
  • 没有单位测试。

在以下部分中,您将更多地了解这些问题,并获得解决这些问题的想法。

减少重复

splashactity.kt. 包含以下代码:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  makeFullScreen()
  setContentView(R.layout.activity_splash)
  // 1
  locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // 2
  locationObservable = provideRxLocationObservable(locationManager, permissionChecker)
  // 3
  navigator = NavigatorImpl(this)
}

在这里,你:

  1. Get the reference to LocationManager using getSystemService().
  2. 在 voke provideRxLocationObservable() to get a reference to Observable<LocationEvent>, which you’ll subscribe to later. This will provide location events.
  3. 在 stantiate NavigatorImpl, passing the reference to Activity as the primary constructor parameter.

Busstopfragment.kt.,您会发现以下代码:

override fun onAttach(context: Context) {
  super.onAttach(context)
  // 1
  locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  // 2
  locationObservable = provideRxLocationObservable(locationManager, grantedPermissionChecker)
  navigator = NavigatorImpl(context as Activity)
}

这 code in onAttach() does basically the same thing as the previous example, because it:

  1. Gets the reference to LocationManager.
  2. 在 vokes provideRxLocationObservable() to get Observable<LocationEvent>.
  3. Creates another instance of NavigatorImpl, passing the reference to the same Activity as in the previous example.

更好地将在不同组件之间共享一些对象以减少代码复制。这是一个问题,即依赖注入有助于解决,因为您将在以下章节中看到。

考虑范围和生命周期

在 any Android app, all the other components of the same app should share some objects, while other objects should exist while a specific Activity 或者 Fragment exists. This is the fundamental concept of 范围 ,您将在以下章节中详细学习。范围是资源管理的重要组成部分。

添加应用程序范围

Look at useLocation() in Busstopfragment.kt.:

private fun useLocation(location: GeoLocation) {
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx) // HERE
            .findBusStopByLocation(location.latitude, location.longitude, 500)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusStop)
            .subscribe(busStopAdapter::submitList, ::handleBusStopError)
    )
  }
}

每一个 time you invoke provideBussoEndPoint(ctx), you create a different instance of the implementation of the Busso. Endpoint interface that 改造 provides.

笔记 : 改造 是一个由...创建的图书馆 正方形 这允许您以声明性和简便的方式实现网络层。

This also happens in getBusArrivals() in BusarriveFragment.kt..

private fun getBusArrivals() {
  val busStopId = arguments?.getString(BUS_STOP_ID) ?: ""
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx)
            .findArrivals(busStopId)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusArrivals)
            .subscribe(::handleBusArrival, ::handleBusArrivalError)
    )
  }
}

Only one instance of a Busso. Endpoint implementation should exist, and BusStopFragment, BusArrivalFragment 和 all the other places where you need to access the server should all share it.

Busso. Endpoint 应该有 与应用程序相同的生命周期.

添加活动范围

Other objects should have a different lifecycle, such as the Navigator implementation in navigatorimpl.kt. 位于 LIBS / UI /导航,如图2.13所示:

图2.13  -  Navigatorimpl类
图2.13 - Navigatorimpl类

class NavigatorImpl(private val activity: Activity) : Navigator {
  override fun navigateTo(destination: Destination, params: Bundle?) {
    // ...
  }
}

As you can see, NavigatorImpl depends on the Activity that it accepts as the parameter in its primary constructor.

This means that NavigatorImpl 应该有 the same lifecycle as the Activity you use for its creation. This currently isn’t happening, as you can see in onAttach() in Busstopfragment.kt.:

override fun onAttach(context: Context) {
  super.onAttach(context)
  locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
  locationObservable = provideRxLocationObservable(locationManager, grantedPermissionChecker)
  navigator = NavigatorImpl(context as Activity) // HERE
}

这是您在以下章节中修复的其他内容。

范围的重要性

这个概念 范围 是基本的,你会在以下章节中读到很多关于它的内容。

图2.14  - 不同组件的不同范围
图2.14 - 不同组件的不同范围

这 diagram in Figure 2.14 gives you an idea about what the scope of the Busso App should be. Busso. Endpoint 应该有 the same scope as the app, while the Navigator 应该有 the same scope as the Activity.

图中没有明显的是生活在范围内的每个组件都应该可以访问生活在外部范围内的实例。

For instance, any component should be able to access the same Busso. Endpoint implementation, Fragments living in a specific Activity should share the same instance of the Navigator implementation, and so on.

如果这不清楚,请不要担心。您将在以下章节中了解这一概念。

添加单元测试

Bus 40应用程序的当前实现根本不包含任何单元测试。真丢脸!单元测试不仅适用于识别回归,它们也是编写更好代码的基本工具。

笔记 :正如您在本章的稍后会看到,所以位置的RX模块包含一些测试。但是,他们在一个不同的模块中。

正如现在,Busso应用程序几乎无法测试。看看 Busstopfragment.kt.。你如何测试这样的函数?

private fun useLocation(location: GeoLocation) {
  context?.let { ctx ->
    disposables.add(
        provideBussoEndPoint(ctx)
            .findBusStopByLocation(location.latitude, location.longitude, 500)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(::mapBusStop)
            .subscribe(busStopAdapter::submitList, ::handleBusStopError)
    )
  }
}

在下面的章节中,您将看到如何使用依赖注入和其他设计模式将使BUSSO应用程序易于测试。

RX模块用于位置

BUSSO应用程序使用一个模块,该模块提供使用位置数据的使用 rxjava. 图书馆。在继续进入依赖注入之前,看看这个库是有用的。它是位于图2.15目录结构中的模块。

图2.15  -  rxlocationObservable.kt文件
图2.15 - rxlocationObservable.kt文件

相同的模块还包含一些单元测试,实现 rxlocationobservablekttest.,它使用了 机器人模式。到目前为止,这实际上是唯一一个在项目中测试的模块。

RX. 模块包含一个实施 蜜蜂 在界定中定义 位置/ API. 模块在 libs. 文件夹,正如您在图2.16中所看到的:

图2.16  - 位置API的RX实现
图2.16 - 位置API的RX实现

API. 包含一些基本定义和抽象,即所有不同的实现都可以使用。在里面 API. module, you’ll find the GeoLocation data class, which describes a location in terms of latitude and longitude.

data class GeoLocation(
    val latitude: Double,
    val longitude: Double
)

每一个 API. implementation should provide some events of the LocationEvent type in diecyEvent.kt.。这是一个密封类,用于定义以下特定子类型:

  • LocationPermissionRequest
  • LocationPermissionGranted
  • LocationNotAvailable
  • LocationData
  • LocationStatus
  • LocationProviderEnabledChanged

这 events’ names are self-explanatory but it’s important to note that LocationPermissionRequest is an event that fires when the permission to access the user’s location is missing, and that you need to put some request permission procedure in place.

On the other hand, LocationPermissionGranted fires if you’ve already obtained the permission.

这 most important event is LocationData, which contains the information about the location in an object of type GeoLocation.

可以通过多种方式授予权限,因此您需要一个像这样定义的抽象:

interface GeoLocationPermissionChecker {
  val isPermissionGiven: Boolean
}

RX. 模块包含使用前一个API的实现 rxjava. 或者 rxkotlin. 。你可以看看它的逻辑 rxlocationobservable.kt..

笔记 :Rxjava是一个实现React规范的库。它在许多商业应用程序中使用。这本书不是rxjava,但你可以在里面了解它 用kotlin的反应性编程 book.

测试RxLocation模块

RX. 模块经过很好地测试。通过看看来看看 测试 文件夹 rxlocationobservablekttest.kt. 快速查看以下测试:

@Test
fun whenPermissionIsDeniedLocationPermissionRequestIsSentAndThenCompletes() {
  rxLocationTest(context) {
    Given {
      permissionIsDenied()
    }
    When {
      subscribeRx()
    }
    Then {
      permissionRequestIsFired()
      isComplete()
    }
  }
}

It verifies that you receive a LocationPermissionRequest when you subscribe to the RX. LocationObservable without having the necessary permissions. After that, Observable will complete.

笔记 :机器人模式是一种有用的测试模式,允许您编写更可读的测试。您可以了解所有关于机器人模式和其他测试程序 一个 droid测试驱动驱动的教程 book.

挑战:一些单位测试为热身

建设和运行Busso应用程序后,是时候挑战了。 如您所知,Busso应用程序没有单位测试。你能写一些与之相关的代码吗? busstopmapper.kt.BusarrivalMapper.kt. 文件,如图2.17所示?

图2.17  - 映射器类
图2.17 - 映射器类

这 se files contain simple functions for mapping the Model you get from the network, with the ViewModel you use to display information in the UI.

挑战解决方案:一些单位测试为热身

busstopmapper.kt. contains mapBusStop(), which you use to convert a BusStop model into a BusStopViewModel. What’s the difference?

BusStop 包含有关来自服务器的总线站的纯数据。它看起来像这样:

data class BusStop(
    val id: String,
    val name: String,
    val location: GeoLocation,
    val direction: String?,
    val indicator: String?,
    val distance: Float?
)

BusStopViewModel 包含实际显示在应用中的信息,例如关于语言环境的信息或某些I18N(Internationalization)字符串。在这种情况下,它是:

data class BusStopViewModel(
    val stopId: String,
    val stopName: String,
    val stopDirection: String,
    val stopIndicator: String,
    val stopDistance: String
)

For instance, BusModel’s distance property is mapped onto the stopDistance property of BusStopViewModel 。 这 former is an optional Float 和 the latter is a String. Why do you need to test these?

Tests allow you to write better code. In this case, mapBusStop() is pure, so you have to verify that for a given input, the output is what you expect.

打开 busstopmapper.kt. 和 select the name of mapBusStop(). Now, open the quick actions menu with 控制 - 输入 到图2.18中所示:

图2.18  - 创建新的单元测试
图2.18 - 创建新的单元测试

选择 测试… 菜单项和图2.19中的对话框将出现:

图2.19  - 创建测试信息
图2.19 - 创建测试信息

现在,按下 好的 将出现按钮和新对话框,询问您要在哪里进行测试。在这种情况下,您正在创建一个单元测试,因此选择 测试 文件夹并选择 好的 button again:

图2.20  - 选择测试文件夹
图2.20 - 选择测试文件夹

现在,Android Studio将为您创建一个新文件,如下所示:

class BusStopMapperKtTest {
  @Test
  fun mapBusStop() {
  }

  @Test
  fun testMapBusStop() {
  }
}

在编写单位测试时,您需要问自己的第一个问题是: 我在测试什么?

在 this case, the answer is that, given a BusStop, you need to get the expected BusStopViewModel. This must be true in the happy case and in all the edge cases.

Now, replace the existing mapBusStop() with the following code:

@Test
fun mapBusStop_givenCompleteBusStop_returnsCompleteBusStopViewModel() {
  // 1
  val inputBusStop = BusStop(
      "id",
      "stopName",
      GeoLocation(1.0, 2.0),
      "direction",
      " 指标 ",
      123F
  )
  // 2
  val expectedViewModel = BusStopViewModel(
      "id",
      "stopName",
      "direction",
      " 指标 ",
      "123 m"
  )
  // 3
  assertEquals(expectedViewModel, mapBusStop(inputBusStop))
}

在此测试中,您:

  1. Create a BusStop object to use as input for the function.
  2. Define an instance of BusStopViewModel like the one you expect as result.
  3. 使用JUnit来验证结果是您的预期。

现在,您可以在图2.21中运行选择箭头的测试:

图2.21  - 运行单元测试
图2.21 - 运行单元测试

如果一切都很好,你将获得一个选中标记,如下所示:

图2.22  - 成功测试
图2.22 - 成功测试

恭喜和谢谢!你改进了Busso应用程序 - 但仍有很多待的事情。

作为一项练习,添加缺失的测试并检查它们是否与本章的最终项目中找到的测试类似。

关键点

  • Busso. 应用程序是一个 客户端服务器 app.
  • BUSSO服务器已通过 ktor. 。您可以在本地运行或使用现有的Heroku安装。
  • Busso. 应用程序的工作原理,但您可以通过删除代码复制和添加单元测试来改进它。
  • 这个概念 范围 或者 生命周期 是基本的,你将在整本书中更多地了解它。

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里 .

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC