AndroidStudio3-和-Kotlin-学习手册-四-

210 阅读42分钟

AndroidStudio3 和 Kotlin 学习手册(四)

原文:Learn Android Studio 3 with Kotlin

协议:CC BY-NC-SA 4.0

十三、主题和菜单

我们将介绍的内容:

  • 主题和颜色

  • 菜单

谷歌 Play 商店有将近 350 万个应用。有很多应用可供选择,这对用户来说是好事,但对开发者来说,竞争非常激烈。

如果你要发布一个应用,你需要润色它——即使只是修饰一下——这样才不会显得寒酸。即使你有一个杀手级应用,你也应该考虑它对用户来说是什么样子(和感觉)。记住,不管你的代码有多棒,用户看到的不是代码,而是用户界面。

谷歌发布了一套用户界面指南。他们称之为材质设计,你可以在 http://material.io 了解更多信息。材质设计是一个很大的主题,它本身就可以填满整本书,我们不打算涵盖所有内容,但在这一章中,我们将看看主题以及如何将 AppBar 添加到您的应用中。

样式和主题

Android 平台有类似“风格”和“主题”的概念。样式是属性的集合,您可以在其中控制视图的外观、背景和前景色、字体大小等等。另一方面,主题是应用于整个应用的风格,而不仅仅是单个视图。当您将样式应用为主题时,应用中的每个视图都会遵循该主题。Android Manifest 的应用节点中的应用应用了一个主题,如以下代码片段所示:

android:theme="@style/AppTheme"

在本例中,“AppTheme”是样式的名称。样式在 app ➤ res ➤ styles.xml 中被写成 XML 文件——文件名通常是 style.xml,但它可以改变,这不是硬性要求。清单 13-1 显示了当前的 styles.xml 这是我们在项目创建向导之后得到的结果。

<resources>
 <!-- Base application theme. -->
  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>
</resources>

Listing 13-1app/res/values/styles.xml

styles (styles.xml)的根节点是“resources”,您可以在这个节点下定义任意多的样式。样式节点具有属性“名称”和“父节点”name 属性是您选择的东西,比如变量、类或函数的名称。父属性是您需要从一组现有主题中选择的东西。AS3 会用提示帮你解决,如图 13-1 所示。

img/463887_1_En_13_Fig1_HTML.jpg

图 13-1

编辑 styles.xml 时的代码提示

一旦定义了样式节点,就可以开始为应用定制颜色了。颜色被定义为“style”元素中的“item”条目。

谷歌的材质设计通过使用贯穿整个应用的原色和强调色,赋予你的品牌身份以生命。这些颜色定义如下:

  • colorPrimary: 应用栏的颜色

    colorPrimaryDark: 状态栏和上下文应用栏的颜色;这是彩色原色的黑暗版本

  • colorAccent: 复选框、单选按钮和编辑文本框等视图的颜色

  • 窗口背景:屏幕背景的颜色

  • textColorPrimary: 应用栏中 UI 文本的颜色

  • 状态栏颜色:状态栏的颜色

  • **导航条颜色:**导航条的颜色

您不必在 styles.xml 中定义所有这些,但是如果您愿意,您可以这样做。您可能已经注意到,颜色项的值本身并没有在 styles.xml 文件中定义,而是被重定向到另一个资源文件。在 styles.xml 中,当您看到这样的条目时

<item name="colorPrimary">@color/colorPrimary</item>

这意味着“colorPrimary”的实际值可以在 colors.xml 文件中找到,该文件位于 app ➤研究➤值文件夹中。清单 13-2 显示了 colors.xml 的当前内容

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
</resources>

Listing 13-2app/res/values/colors.xml

自定义主题

您可以通过两种方式编辑颜色。您可以直接编辑 colors.xml 文件,也可以使用 AS3 的主题编辑器修改颜色。要使用主题编辑器,在主编辑器中打开 styles.xml 文件,然后点击右上角的“打开编辑器”链接,如图 13-2 所示。

img/463887_1_En_13_Fig2_HTML.jpg

图 13-2

启动“打开编辑器”

主题编辑器允许您更改应用的颜色值。它还向您展示了应用在给定的配色方案下的外观。图 13-3 显示了主题编辑器的各个部分。

img/463887_1_En_13_Fig3_HTML.png

图 13-3

主题编辑器

要改变颜色,单击材质颜色旁边的样本(如图 13-3 )。这将启动拾色器(如图 13-4 )。

img/463887_1_En_13_Fig4_HTML.jpg

图 13-4

颜色选择器

Google 在 http://bit.ly/materialdesigndox 发布了关于材质设计的文档;在修改配色方案之前,最好先阅读一下。你可以使用的另一个网络资源是 materialPalette.com;它面向 Android 材质设计。图 13-5 显示了他们网站的截图。

img/463887_1_En_13_Fig5_HTML.jpg

图 13-5

截图自 https://www.materialpalette.com

基本的想法是选择两种颜色,网站为你建立一个调色板。现在你可以简单地复制原色、深原色、强调色、浅原色和其他颜色的十六进制值。

菜单

菜单在 UI 设计中非常重要。它们允许用户使用应用的功能。传统上,菜单系统是分层次地组织在介绍组中的,这意味着在用户到达他的目标动作之前,他需要遍历菜单的层次结构。Android 的菜单系统,在某个时间点上,已经完全像那样表现了——分组和分级。但那是过去的事了。Android 的菜单方式在其生命周期中发生了巨大的变化。

Android Honeycomb 之前的菜单依赖于硬件按钮,如图 13-6 所示。

img/463887_1_En_13_Fig6_HTML.png

图 13-6

旧 Android 硬件上的菜单

那时候,我们总是可以相信“home”和“option”按钮会出现在任何 Android 手机上。我们基于这些假设开发我们的应用,因为它们在当时是合理的。

时代变了,安卓硬件也变了。屏幕分辨率大幅提高,硬件按键消失。幸运的是,Android 的菜单方式也发生了变化,并跟上了硬件功能的发展。

Honeycomb 问世时,Android 中加入了一种新的菜单系统。最低目标 SDK 是 API 11 的应用现在可以使用“ActionBar”

如图 13-7 所示,ActionBar 是屏幕顶部的一个专用区域,在整个应用中持续存在。仔细想想很像 AS3 的主菜单栏。

img/463887_1_En_13_Fig7_HTML.jpg

图 13-7

带有动作栏的应用

您可以使用操作栏来显示应用最重要的功能,并以可预测的方式访问它们(例如,在顶部放置一个永久的搜索小部件,等等)。).它通过消除菜单中的混乱来创建一个更整洁的外观,并且在菜单中的所有项目不能适合屏幕的情况下,动作栏会显示一个溢出图标。溢出图标是一个垂直省略号,由三个点垂直排列而成,通常位于工具栏的最右侧。它还显示应用的名称,因此它强化了应用的品牌身份。

如今,ActionBar 已经有点过时了,已经被工具栏盖过了。工具栏更加通用,因为它不是永久地夹在屏幕的顶部,你可以把它放在任何你想放的地方,而且它有更多的功能。然而,对于简单的菜单系统,ActionBar 仍然是一个可行的解决方案;事实上,没有什么可以阻止你在应用中同时使用动作栏和工具栏。使用你拥有的最好的工具。

在 Android API level 10 或更低版本中,当用户按下硬件菜单按钮时,菜单选项会出现在屏幕底部。在 Android API 11 及更高版本中,选项菜单中的项目在应用栏中可用。默认情况下,系统会将所有项目放置在动作溢出中,用户可以通过应用栏右侧的动作溢出图标来显示。

要将菜单添加到应用,您需要执行以下操作:

  1. 创建一个菜单资源文件。我们将在 app/res 文件夹中创建一个菜单文件夹。然后,我们将在其中创建一个菜单资源文件。

  2. 在主程序中展开菜单资源。我们将覆盖 MainActivity 的onCreateOptionsMenu并调用菜单对象的膨胀函数。

  3. 添加事件处理程序到菜单项。我们将覆盖 MainActivity 的 onOptionsItemSelected 函数,并根据单击了哪个菜单项来路由用户操作。

  4. 或者,将矢量图像添加到菜单中。

让我们创建一个演示应用,这样我们就可以探索菜单。项目详情见表 13-1 。

表 13-1

演示应用的项目详情

|

项目明细

|

| | --- | --- | | 应用名称 | CH13AppBar(消歧义) | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 | | 向后兼容性 | 是的。应用兼容性 |

我们不会在这个应用中添加任何额外的视图元素,因为它们不会被需要,但我们会将 and android:id 添加到我们的布局容器中。注意清单 13-3 中的第六行:ID 属性在默认情况下是不存在的,你需要把它放进去。每个视图元素的 id 现在对我们来说更重要了,因为 Kotlin Android 扩展依赖于它。如果没有视图 id,扩展将无法合成视图 id。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/root_layout"
  tools:context=".MainActivity">
</android.support.constraint.ConstraintLayout>

Listing 13-3excerpt from activity_main.xml

我们还需要编辑模块级的 build.gradle 文件。为了使用 Snackbar 小部件,我们需要在 gradle 文件中包含“com.android.support:design”依赖项。图 13-8 显示了 gradle 文件在项目窗口中的位置。

img/463887_1_En_13_Fig8_HTML.jpg

图 13-8

模块级 build.gradle

您需要添加“com.android.support:design”行,如清单 13-4 所示。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation 'com.android.support:design:27.1.0'
}

Listing 13-4excerpt from build.gradle

AS3 会感觉到在构建文件中有什么改变,它会要求你“同步”gradle 文件。该提示将在主编辑器的上部显示为黄色条。单击“同步”以便继续。

现在我们准备创建菜单文件,但在此之前,让我们创建一个菜单文件夹。在项目窗口中右键点击 appres 文件夹,如图 13-9 所示。选择新建安卓资源目录

img/463887_1_En_13_Fig9_HTML.jpg

图 13-9

创建新的 Android 资源目录

给新创建的文件夹命名,如图 13-10 所示。

img/463887_1_En_13_Fig10_HTML.jpg

图 13-10

新菜单文件夹

现在我们有了一个菜单文件夹,右击它并创建一个新的菜单资源文件,如图 13-11 所示。

img/463887_1_En_13_Fig11_HTML.jpg

图 13-11

新菜单资源文件

让我们给新创建的菜单文件命名,如图 13-12 所示。

img/463887_1_En_13_Fig12_HTML.jpg

图 13-12

主菜单资源文件

让我们在菜单文件中添加一些项目。在主编辑器中打开文件app/RES/menu/main _ menu . XML,添加如清单 13-5 所示的菜单项。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:id="@+id/menuFile"
    android:title="@string/menuFile"
    />
  <item android:id="@+id/menuEdit"
    android:title="@string/menuEdit"
    />
  <item android:id="@+id/menuHelp"
    android:title="@string/menuHelp"
    />
  <item android:id="@+id/menuExit"
    android:title="@string/menuExit"
    />
</menu>

Listing 13-5app/res/menu/main_menu.xml

清单 13-5 中的每个项目元素代表一个菜单项。每个元素由两个属性组成:一个 android:id 和一个 android:title 。标题是您将在菜单上看到的内容,id 是对菜单项的编程引用。当我们想从程序中引用一个菜单项时,我们将使用这个 id。

android:id 是用 @+id 符号写的,这样在它还不存在的情况下会被创建。 android:title 是用 @string 符号编写的,所以标题的值在 app/res/values/strings.xml 文件中被解析。我们可以像这样对菜单标题进行硬编码:

<item android:id="@+id/menuFile"
  android:title="File" />

但这是一种糟糕的做法。Android 编程的惯例是在 strings.xml 资源文件中存储和检索字符串。将您的字符串存储在/app/res/values/strings.xml 中还可以让您更容易发布非英语版本的应用。想象一下,如果你创建了一个法语或意大利语版本的应用。您必须手动替换所有这些硬编码的字符串。但是如果您将字符串存储在 xml 文件中,那么您只需要在一个文件中替换它,这使得本地化和国际化变得更加容易。

一旦输入完菜单文件,您会注意到 AS3 抱怨新创建的菜单项。android:title 条目无法解析或者无法在 strings.xml 中找到。当然 AS3 找不到它——我们还没有创建它。

我们既可以手动将新条目添加到 strings.xml,也可以使用 AS3 的快速修复来解决错误。让我们使用快速修复。当 main_menu.xml 仍然在编辑器上时,点击@string/menuExit,如图 13-13 所示,然后按 OPTION + ENTERALT + ENTER

img/463887_1_En_13_Fig13_HTML.jpg

图 13-13

将菜单标题添加到 strings.xml

键入该项目的资源值,并对每个 android:title 属性重复这些步骤。资源值将存储在 app ➤ res ➤值➤ strings.xml 中——strings . XML 的内容如清单 13-6 所示。

<resources>
  <string name="app_name">CH13AppBar</string>
  <string name="menuExit">Exit</string>
  <string name="menuHelp">Help</string>
  <string name="menuEdit">Edit</string>
  <string name="menuFile">File</string>
</resources>

Listing 13-6app/res/values/strings.xml

下一步是将菜单与主活动关联起来。为此,我们需要通过覆盖 MainActivity 中的 onCreateOptionsMenu 来扩展菜单文件。

打开主活动。Kt,并开始添加一个顶级函数。一旦你开始输入 onCreateOptionsMenu 的前几个字符,AS3 会通过给出代码提示来帮助你。使用如图 13-14 所示的自动完成功能来完成功能的框架。

img/463887_1_En_13_Fig14_HTML.jpg

图 13-14

自动完成 onCreateOptionsMenu

复制清单 13-7 中的代码来完成 onCreateOptionsMenu。

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
  menuInflater.inflate(R.menu.main_menu, menu)
  return super.onCreateOptionsMenu(menu)
}

Listing 13-7onCreateOptionsMenu

inflate() 函数使用我们之前创建的菜单 XML 文件(第一个参数)创建菜单项,并将其附加到菜单对象(inflate 函数的第二个参数)。Android 运行时在调用 onCreateOptionsMenu 回调函数时会把菜单传给我们。

图 13-15 显示运行时的菜单;左边的图片显示了溢出图标——它是三个白点,像垂直省略号一样排列。通过点击或触摸溢出图标显示菜单项。右图显示了我们的应用,显示了所有菜单项。

img/463887_1_En_13_Fig15_HTML.jpg

图 13-15

查帕尔菜单

现在,菜单项出现了,但是它们还没有做任何事情。为了处理每个菜单项的事件,我们将在 MainActivity 中覆盖 onOptionsItemSelected() 函数。

清单 13-8 显示了被覆盖的 onOptionsItemSelected 的代码。每次用户单击菜单项时,Android 运行时都会调用该方法。运行库将 MenuItem 对象传递给表示所单击的菜单项的函数。

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
  return true
}

Listing 13-8onOptionsItemSelected

我们可以使用 MenuItem 来路由我们的程序逻辑,方法是将它的 itemId 属性与我们在 main_menu.xml 中定义的四个菜单项进行比较。

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
  if(item?.itemId == R.id.menuFile) {
    showMessage(“File Menu “) // user defined function
    return true
  }

}

Listing 13-9comparing itemId with R.id.menuFile

注意我们如何使用安全调用操作符(?。)测试期间。我们需要使用安全调用,因为 MenuItem 在 onOptionsItemSelected 中被声明为可空,而且该函数应该返回一个布尔值。在我们的例子中,我们返回了 true,,这告诉 Android 运行时我们已经消费了这个事件,并且不需要其他侦听器进一步处理这个事件。我们可以继续使用 if-else 构造来路由程序逻辑,但是在这种情况下, when 构造可能更合适。清单 13-10 展示了如何在处理程序逻辑时使用**。你可能还记得第三章的和中提到 Kotlin 没有一个 switch 语句——这个 when 结构相当于 Java 的 switch。**

override fun onOptionsItemSelected(item: MenuItem?): Boolean {

  when(item?.itemId) {
    R.id.menuFile -> {
      showMessage("File menu")
      return true
    }
    R.id.menuEdit -> {
      showMessage("Edit menu")
      return true
    }
    R.id.menuHelp -> {
      showMessage("Help menu")
      return true
    }
    R.id.menuExit -> {
      showMessage("Exit")
      return true
    }
  }

Listing 13-10using when to route program logic

清单 13-11 、 13-12 和 13-13 分别显示了 MainActivity、activity_main 和 build.gradle 的完整代码。如果您正在编写代码,您可以使用它作为参考。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.thelogbox.ch13appbar"
        minSdkVersion 23
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support:design:27.1.0'
}

Listing 13-13app/build.gradle

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/root_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Listing 13-12complete code for activity_main.xml

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.view.Menu
import android.view.MenuItem

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.main_menu, menu)

    return super.onCreateOptionsMenu(menu)
  }

  override fun onOptionsItemSelected

(item: MenuItem?): Boolean {

    when(item?.itemId) {
      R.id.menuFile -> {
        showMessage("File menu")
        return true
      }
      R.id.menuEdit -> {
        showMessage("Edit menu")
        return true
      }
      R.id.menuHelp -> {
        showMessage("Help menu")
        return true
      }
      R.id.menuExit -> {
        showMessage("Exit")
        return true
      }
    }

    return super.onOptionsItemSelected(item)
  }

  private fun showMessage(msg:String) {
    Snackbar.make(root_layout, msg, Snackbar.LENGTH_LONG).show()

  }
}

Listing 13-11complete code for MainActivity.Kt

章节总结

  • 使用样式和主题可以立刻给你的应用增加活力。升级你的应用是最简单的事情。

  • 动作栏中的菜单可以显示应用最重要的功能。

在下一章中,我们将:

  • 看片段。您可以使用它们来使您的应用适应不同的外形和设备方向(纵向与横向)。

  • 我们还将看看如何让片段相互交流。

十四、片段

我们将介绍的内容:

  • 片段简介

  • 横向和纵向

  • 片段间通信

在 Android 的早期,当它只在手机上运行,没有任何高分辨率的屏幕时,activities 作为一种编写 UI 和与用户交互的方式就足够了。随后出现了平板电脑和高分辨率屏幕,创建可以在手机和平板电脑上良好运行的应用变得越来越困难。开发者面临着艰难的选择。要么选择功能最差的硬件作为目标,并使其成为最小公分母,要么通过根据设备的功能移除和添加 UI 元素来使应用适应一系列外形,这被证明是非常难以手动完成的。API 11(蜂巢)出来的时候,安卓用片段解决了这个问题。

片段简介

片段是一个非常高级的概念,初学者可能会战战兢兢地接近它,但是它背后的基本概念非常简单。如果我们把一个活动看作是 UI 的组成单元,那么就把一个片段看作是一个迷你活动——它是一个更小的组成单元。您通常会在运行时显示(和隐藏)片段,以响应用户的操作(例如,倾斜设备或从纵向切换到横向,从而腾出更多的屏幕空间)。你甚至可以使用片段作为适应设备外形的策略;当应用在较小的屏幕上运行时,您将只显示一些片段。

像活动一样,片段由两部分组成:一个 Java 程序和一个布局文件。想法几乎是一样的——在 XML 文件中定义 UI 元素,然后在程序文件中膨胀 XML 文件,这样 XML 中的所有视图对象都将成为一个对象。之后,我们可以使用 R.class 引用 XML 中的每个视图对象。一旦我们理解了这个概念,就可以将片段想象成一个普通的视图对象,我们可以将它拖放到主布局文件上——当然,片段不是普通的视图,但它们是视图。

为了创建一个片段,我们通常做以下事情:

  1. 创建一个 XML 资源文件,放在 /app/res/layout 文件夹中,就像我们放 activity_main.xml 的地方一样。

  2. 给新的资源文件起一个描述性的名字——比如说,fragment_booktitles.

  3. 创建片段类。我们过去在创建片段时会在两个类之间选择——要么从原生的 android.app.Fragment 继承,要么从Android . support . v4 . app . fragment继承。如果您的目标 SDK 是 API 11 或更高版本,您可以使用前者,而对于任何低于 Android 3 (Honeycomb)的应用,您可以使用后者。你仍然可以使用 android.app.Fragment,但作为一个提醒,你需要知道 Android P(又名 Android 9)已经弃用了原生片段。如果您仍然想使用片段,请使用支持库,这样您就可以在所有 API 级别上获得一致的行为。

  4. 接下来,将片段类与 XML 资源布局联系起来。您可以通过在 Fragment 类的 onCreate 方法中膨胀 XML 资源文件来做到这一点。

  5. 添加新创建的片段。

让我们在 Android Studio 中完成它们。首先,创建一个包含空活动的项目,就像我们已经创建的所有其他项目一样。

现在,创建一个 XML 资源文件,放入 /app/res/layout ,如图 14-1 所示。

使用上下文菜单,右键单击项目工具窗口中的 /app/res/layout 文件夹(图 14-1 )。选择新建布局 资源文件。这个布局资源文件将包含我们片段的所有视图元素。你会看到一个“新资源文件”的对话窗口;输入资源文件的名称—出于练习的目的,我将其命名为“book_titles”

img/463887_1_En_14_Fig1_HTML.jpg

图 14-1

新布局资源文件

您可以放置任何您需要的视图元素。这个片段资源文件与我们之前处理过的任何活动资源文件没有什么不同。您可以将任何内容放入活动资源文件,也可以放入片段资源文件。

接下来,让我们创建片段类。再次使用上下文菜单创建类,如图 14-2 所示。

img/463887_1_En_14_Fig2_HTML.jpg

图 14-2

创建新的 Kotlin 类

如果您在创建新的 Kotlin 类时右键单击了Javanet . working dev . fragments test,那么新创建的类将与您的其余代码属于同一个包。如果您在创建新的 Kotlin 类时右键单击 java 文件夹,该类将在默认包中;当这种情况发生时,您需要自己将 package 语句添加到类中。

您将被要求创建哪种 Kotlin 文件。从下拉菜单中选择类别,如图 14-3 所示。

img/463887_1_En_14_Fig3_HTML.jpg

图 14-3

给 Kotlin 类命名

片段类可以通过膨胀资源文件并从 onCreateView 回调中返回来与 UI 资源文件相关联。清单 14-1 包含 MainActivity 的注释和解释片段;它展示了如何将片段类与 UI 资源文件连接起来。具体来说,项目符号❸是负责将片段类与 UI 资源文件相关联的代码。

| -好的 | 我们使用支持库中的 Fragment 类,因为 Android 9 不赞成使用 **android.app.Fragment** 。即使我们通常以 API 23 为目标,从现在开始最好总是使用受支持的库。 | | ❷ | **onCreateView** 回调类似于活动的 onCreate。但是注意不要在这里引用任何视图元素——它们现在还不可用。如果您试图在这里引用一个 UI 元素(例如,一个按钮或文本字段),它将返回 null。 | | -你好 | 在这个例子中,UI 资源文件的名称是 **book_titles** 。因此,假设您有一个名为**/app/RES/layout/book _ titles . XML**的文件。膨胀 XML 资源文件并返回它,以便 MainActivity 可以在它的末端组成 UI。当你在 **onCreateView** 中时,你不能引用任何 UI 元素的原因是因为你还没有膨胀 XML 资源,所以此时你的 UI 元素都不存在。Android 运行时将**充气器**和**容器**对象传递给 **onCreateView** 方法。我们需要这些对象来扩充 UI 资源。 | | (a) | 当所有 UI 元素就绪时,运行时调用 onViewCreated 方法**。这是您可以开始使用和引用 UI 元素的地方。** |
import android.support.v4.app.Fragment    ❶
...

class BookTitle : Fragment() {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {            ❷

    val v = inflater.inflate(R.layout.book_titles, container, false) ❸
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ❹
  }
}

Listing 14-1BookTitle Fragment

注意

“膨胀”UI 资源文件意味着接受 UI 定义(XML 格式),创建实际的视图和视图组对象,并呈现它们。在展开过程之后,您将能够以编程方式引用视图对象。

最后一步是将片段添加到活动中。您可以通过两种方式向活动添加片段:在运行时或在设计时。现在,我们将在设计时添加片段。

打开 MainActivity 的 UI 资源文件(如果它尚未打开)。在项目工具窗口中,双击**/app/RES/layout/activity _ main . XML**。在“设计”模式下打开它。在调色板中,到常用,寻找 <片段>,,如图 14-4 。

img/463887_1_En_14_Fig4_HTML.jpg

图 14-4

将片段元素拖到 activity_main 中

<片段> 元素拖放到活动中的任意位置,就像拖放任何视图元素一样。将弹出一个片段对话框;您需要选择想要添加到 activity_main 布局中的片段类。在我们的例子中,只有一个片段类—选择 BookTitle 片段。

就这样,我们现在可以运行我们无趣和无趣的片段样本了。如果你运行它,它看起来像模拟器中的图 14-5 。

img/463887_1_En_14_Fig5_HTML.jpg

图 14-5

片段测试,运行

尽管它很无趣,但它应该让你在片段的基础上打好基础。现在,我们准备好了一些更有趣的东西。在下一节中,我们将创建一个包含两个片段的演示项目。

书名和描述,片段演示

我们想做的是:

  1. 在我们的主活动中使用两个片段。

  2. 其中一个片段包含一个书单;我们将让用户通过单击一个单选按钮来选择一本书。

  3. 另一个片段包含对当前选中的书的描述。

  4. 这些片段将根据用户手持设备的方式(纵向或横向)自行重新排列。

在运行时,当设备垂直放置时,应用看起来如图 14-6 所示。

img/463887_1_En_14_Fig6_HTML.png

图 14-6

图书标题应用,垂直方向(纵向)

当用户在横向模式下手持设备时,它看起来如图 14-7 所示。

img/463887_1_En_14_Fig7_HTML.png

图 14-7

水平方向,横向

我们已经知道了如何创建片段,以及如何将它们添加到活动中,但是为了完成这个演示项目,我们将需要散列更多的细节。

  1. 我们如何使用单选按钮作为选择器,这样当一个按钮被选中时,其他的按钮就会被取消选中?我们将使用一个单选按钮组并收集该组下的所有单选按钮。

  2. 我们将在哪里存储每本书的文本定义?我们将使用一个 XML 文件,然后将其加载到一个数组中。数组的每个元素将包含一本书的定义。

  3. 我们如何同步两个片段之间的信息呢?我们将探索片段间的交流。我们不会让片段直接相互通信(虽然我们可以,但这不是好的做法)。我们将通过活动来管理同步。

  4. 我们如何处理设备方向的变化?我们将在 /app/res 中专门为风景布局创建另一个布局文件夹。它将被命名为/app/RES/layout-land**;当设备处于横向时,这是我们放置布局文件的地方。**

那我们开始工作吧。我为此演示创建了一个新项目;详情见表 14-1 。

表 14-1

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | ch14 片段书籍 | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

让我们创建 XML 资源文件,它将保存书籍描述的文本。为此,您可以:

img/463887_1_En_14_Fig8_HTML.jpg

图 14-8

创建新的 XML 值文件

  1. 使用上下文菜单,在项目工具窗口中右键单击/ app/res/values ,然后

  2. 点击新建XML值 XML 文件,如图 14-8 所示。

  3. 将其命名为“图书描述”——不要键入*。xml* 扩展名;Android Studio 会处理好的。

在编辑器中打开 bookdescriptions.xml,将清单 14-2 的内容复制到其中。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string-array name="bookdescriptions">
    <item>
       How to use Android Studio 3, but also teaches you how basic
Android programming. And hey, in case you're also a beginner in Java, that's covered too.
    </item>
    <item>
      This book is also about how to use Android Studio. Like the first one,
      it also teaches you the basics of the IDE and Android programming; but
      this time around, you'll use Kotlin. The newest kid in the JVM block
    </item>
    <item>
      Minimum Android Programming is the book that got me started. I wrote
      in an age when even the Eclipse ADT doesn't exist yet. So, this means
      you'll use the Android SDK in all the glory of the CLI tools
    </item>
  </string-array>
</resources>

Listing 14-2/app/res/values/bookdescriptions.xml

现在我们可以研究片段了。让我们首先创建 book_titles 片段。创建一个新的布局资源文件,并将其命名为“book_titles”

清单 14-3 显示了**/app/RES/layout/book _ titles . XML**的内容

| -好的 | 获取单选按钮组视图。 | | ❷ | 添加第一个单选按钮作为 RadioGroup 的子节点。 | | -你好 | 对第二个单选按钮执行相同的操作。 | | (a) | 对第三个无线电按钮进行同样的操作 |
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
  tools:layout_editor_absoluteY="81dp">

  <RadioGroup                                 ❶
    android:id="@+id/radioGroup"
    android:layout_width="354dp"
    android:layout_height="wrap_content"
    tools:layout_editor_absoluteX="16dp"
    tools:layout_editor_absoluteY="75dp">

    <RadioButton                              ❷
      android:id="@+id/rlas3"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Learn Android Studio 3"
      android:textSize="18sp" />

    <RadioButton                              ❸
      android:id="@+id/rlas3kotlin"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Learn Android Studio 3 with Kotlin"
      android:textSize="18sp" />

    <RadioButton                              ❹
      android:id="@+id/rminandroid"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:text="Minimum Android Programming"
      android:textSize="18sp" />
  </RadioGroup>
</android.support.constraint.ConstraintLayout>

Listing 14-3/app/res/layout/book_titles.xml

接下来,让我们为 book_titles UI 创建片段类。使用上下文菜单,右键点击**/app/Java/net . working dev . ch 14 fragment books**,然后选择新建Kotlin 文件/类。创建一个类,并将其命名为 BookTitle。在本课程中,我们需要做以下工作:

  1. 它是一个片段,所以它需要继承片段类。

  2. 覆盖 onCreateView 回调,膨胀 UI 资源文件,并返回。

  3. 处理单选按钮的单击事件。有几种方法可以做到这一点。一种方法是为 radioGroup 设置一个监听器,另一种方法是为每个单选按钮设置一个点击监听器;我们选择后者。

清单 14-4 中显示了带注释(和解释)的 BookTitle 类。

| -好的 | 我们从支持库中扩展了片段类。我们还实现了**视图。OnClickListener** 接口。我们将使用该类作为三个单选按钮的 **onClick** 监听器对象。 | | ❷ | 运行时调用 **onCreateView** 方法来组成片段的 UI。此时,该片段的 UI 元素都是不可访问的。您不能在这里进行任何 UI 更改或初始化。 | | -你好 | 这将向运行时返回一个视图对象。我们正在膨胀 UI 资源文件。**膨胀**方法有三个参数:1. **UI 资源文件**。片段的 XML 布局,我们将使用 R.layout.book_titles。2.**这是片段的潜在父级,或者根**。为此,我们将只使用**容器**。3.**附着根**。这是一个布尔值。这个值将决定膨胀视图是否应该附加到根参数?如果为 false,root 仅用于为 XML 中的根视图创建 LayoutParams 的正确子类。 | | (a) | 我们说单选按钮的监听器对象是 BookTitle 类的实例, *this* 类。 | | (一) | **onClick** 回调来自**视图。OnClickListener** 接口。当单击其中一个单选按钮时,运行时将调用此方法并传递被单击的实际视图对象。这是我们规划程序逻辑的地方。我们将知道哪个单选按钮实际上被点击了。 | | ❻ | **when** 构造非常适合路由程序逻辑。我们在这里测试 **View.id** 的运行时值; *R.id.rlas3* 、 *R.id.rlas3kotlin* 和 *R.id.rminandroid* 是 book_title.xml 中单选按钮声明的*id*。 | | ❼ | 我们给 **rlas3** 赋值为零,因为 rlas3 的描述在图书描述数组的*0*th元素中找到(我们还没有创建这个数组)。同样,rlas3kotlin 的定义是图书描述数组的第 1 个 st 元素,而 rminandroid 的定义是第 2 个 nd 元素。 |
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class BookTitle : Fragment(), View.OnClickListener {  ❶

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {    ❷
    val v = inflater.inflate(R.layout.book_titles, container, false) ❸
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    rlas3.setOnClickListener(this)          ❹
    rlas3kotlin.setOnClickListener(this)
    rminandroid.setOnClickListener(this)
  }

  override fun onClick(v: View?) {  ❺
    var index:Int = 0
    when(v?.id) {
      R.id.rlas3 -> {               ❻
        index = 0                   ❼
      }
      R.id.rlas3kotlin -> {
        index = 1
      }
      R.id.rminandroid -> {
        index = 2
      }
    }
  }
}

Listing 14-4BookTitle Fragment Class

既然已经完成了 book_titles 片段的两个组件,我们就可以处理 book_description 片段了。你已经知道如何创建一个片段,所以我将跳过指令,直接跳到代码。

创建一个新的 UI 资源,命名为 book_description ,确保它在 /app/res/layout 文件夹中。作为片段类的,将其命名为图书描述

清单 14-5 和 14-6 分别显示了 book_description.xml 和 BookDescription 类的代码。

book_description 片段很简单。它只有一个 TextView 元素。注意,我们没有对这个片段使用 ConstraintLayout 我们可以使用,但是使用 LinearLayout 要简单得多。我们希望文本视图的宽度占据整个屏幕的宽度。如果您尝试按照练习进行,您可以简单地复制清单 14-5 并覆盖 book_description.xml 的内容。

| -好的 | 该语句读取文件**/app/RES/values/book descriptions . XML**,并从中创建一个数组。 | | ❷ | 我们创建了一个小函数,它将负责修改描述文本视图中的文本。它接受一个 **Int** 值,我们将用它作为描述的选择器。数组的每个元素包含一本书的描述。 | | -你好 | **arrbookdesc[bookindex]** 从数组中获取一个描述,然后将 TextView 的**文本**属性设置给它。 |
class BookDescription : Fragment() {

  lateinit var arrbookdesc: Array<String>
  var bookindex = 0

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val v = inflater.inflate(R.layout.book_description, container, false)
    arrbookdesc = resources.getStringArray(R.array.bookdescriptions) ❶

    return v
  }

  fun changeDescription(index:Int) : Unit { ❷
    bookindex = index
    txtdescription?.setText(arrbookdesc[bookindex]) ❸
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    changeDescription(bookindex)
  }
}

Listing 14-6
BookDescription class

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=http://schemas.android.com/apk/res/android
  xmlns:tools=http://schemas.android.com/tools
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <TextView
    android:id="@+id/txtdescription"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="TextView"
    android:textSize="24sp" />
</LinearLayout>

Listing 14-5/app/res/layout/book_description.xml

现在这两个片段已经构建好了,我们可以把注意力放在主活动上了。它需要做三件事:

  1. 把两个片段抱在一起

  2. **充当每个片段的信使。**当用户在 book_titles 片段中选择一本书时,我们需要在 bookdescriptions 数组中查找该书的描述,并相应地修改 book_description 片段中的文本描述;和

  3. 根据设备的方向调整两个片段的排列。如果设备垂直定向,两个片段将从上到下堆叠排列。当设备水平放置时,堆叠将从左到右进行。

让我们先努力实现第三个目标。现在,我们只有一个布局文件夹, /app/res/layout 文件夹是 Android 寻找布局资源的默认位置。这就是为什么我们总是把我们的 activity_main.xml 放在这个文件夹中的原因。有一个约定,如果我们创建一个名为 **/app/res/layout-land 的文件夹,**当设备处于风景模式时,Android 会在这个文件夹中查找布局文件。我们将利用这一惯例来实现我们的目标。

还有,我们需要解决从上到下和从左到右的堆叠顺序。最简单的方法是将 activity_main 的布局从 ConstraintLayout 改为 LinearLayout。想法是为/app/res/layout 和/app/res/layout-land 提供相同的 activity_main xml 文件,但是我们将更改 LinearLayout 方向,以便在默认布局文件夹中,方向是垂直的(默认),而在 layout-land 文件夹中,方向是水平的。我们还会做一些改动,但是我们会在一段时间内完成。

要将 activity_main 的布局转换为 LinearLayout,请执行以下操作:

  1. 在设计视图中打开 activity_main.xml。

  2. In the “Component Tree” tool window, right-click on “ConstraintLayout, as shown in Figure 14-9.

    img/463887_1_En_14_Fig9_HTML.jpg

    图 14-9

    将 activity_main 转换为 LinearLayout

  3. 选择转换视图

  4. A dialog box will appear; choose LinearLayout, as shown in Figure 14-10.

    img/463887_1_En_14_Fig10_HTML.jpg

    图 14-10

    转换为线性布局

清单 14-7 显示了修改后的 activity_main 的代码(转换为 LinearLayout 之后)。

| -好的 | layout _ width:“match _ parent”表示布局将跨越整个屏幕宽度。 | | ❷ | \这意味着布局将跨越屏幕的整个高度。 | | -你好 | 方向:“垂直”意味着我们在布局中放置的任何视图都将从上到下排列。 |
<LinearLayout 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:orientation="vertical"       ❸
  tools:context=".MainActivity">

</LinearLayout>

Listing 14-7Code of activity_main.xml

接下来,将这两个片段添加到 activity_main 中。在设计模式下打开 activity_main,进入调色板常用,然后找到 **<片段>,**如图 14-11 所示。首先添加 BookTitle 片段。重复该过程并添加图书描述。

img/463887_1_En_14_Fig11_HTML.jpg

图 14-11

将片段元素拖到 activity_main 中

清单 14-8 显示了添加了两个片段的 activity_main.xml。

| -好的 | 我们希望顶部片段横跨整个宽度。 | | ❷ | 高度设置为 0px 即可。我们将让运行时为我们决定高度。反正我们用的是布局权重。 | | -你好 | 假设权重为“1”你在这里用什么数并不重要,只要另一个片段有相同的重量。 | | (a) | 我们还希望底部的片段横跨整个宽度。 | | (一) | 我们让运行时决定高度;把这个也设置成 0px。 | | ❻ | 我们希望顶部和底部的片段有相等的高度。因此,我们也将这里的权重设置为“1”。 |
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
  tools:context=".MainActivity">

  <fragment
    android:id="@+id/fragmentbooktitle"
    android:name="net.workingdev.ch14fragmentsbooks.BookTitle"
    android:layout_width="match_parent"   ❶
    android:layout_height="0px"           ❷
    android:layout_weight="1" />          ❸

  <fragment
    android:id="@+id/fragmentbookdescription"
    android:name="net.workingdev.ch14fragmentsbooks.BookDescription"
    android:layout_width="match_parent"   ❹
    android:layout_height="0px"           ❺
    android:layout_weight="1" />          ❻

</LinearLayout>

Listing 14-8activity_main With book_titles and book_description Fragments

这就是默认的纵向方向。现在,让我们在横向方向上工作。为了在设备水平放置时控制应用的外观和行为,我们需要做四件事。它们概述如下:

  1. 创建文件夹 /app/res/layout-land

  2. layout-land 中创建另一个 UI 资源文件;我们也将命名为 activity_main。

  3. 将/app/res/layout/activity_main 的内容复制到/app/RES/layout-land/activity _ main。

  4. 在/app/RES/layout-land/activity _ main 中进行必要的方向更改。

首先,您需要切换项目工具窗口的视图。现在我们正在使用“Android 视图”,我们需要转到“项目视图”转到项目工具窗口的上部区域,点击向下箭头(如图 14-12 所示),然后选择“项目”

img/463887_1_En_14_Fig12_HTML.jpg

图 14-12

从 Android 视图更改为项目视图

创建文件夹布局-在/app/res 文件夹中着陆。右键点击/app/res 文件夹,然后选择新建安卓资源目录。将新目录命名为“layout-land”,如图 14-13 所示。

img/463887_1_En_14_Fig13_HTML.jpg

图 14-13

新资源目录

右键单击新创建的 layout-land 文件夹,然后选择新建布局资源文件

将文件命名为“activity_main”,并为“根元素”选择 LinearLayout,如图 14-14 所示。

img/463887_1_En_14_Fig14_HTML.jpg

图 14-14

新布局资源文件

将/app/RES/layout/activity_main . XML 的内容复制到 layout-land 中这个新创建的 activity _ main 中,并进行适当的修改,如清单 14-9 所示。

| -好的 | 我们处于横向模式,所以这需要是“水平的”。使用此设置,片段将从左到右排列,而不是从上到下。 | | ❷ | 在纵向模式下,layout_width 设置为“match_parent”,layout_height 设置为“0px”。我们将在横向模式下反转这些设置。所以把 layout_width 设置为“0px”。 | | -你好 | 将 layout_height 设置为“match_parent”。 | | (a) | 和往常一样,我们希望这两个片段具有相等的权重,所以在这里使用“1”。确保另一个片段中的 layout_weight 也是“1” |
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="horizontal"   ❶

  tools:context=".MainActivity">

  <fragment
    android:id="@+id/fragmentbooktitle"
    android:name="net.workingdev.ch14fragmentsbooks.BookTitle"
    android:layout_width="0px"             ❷
    android:layout_height="match_parent"   ❸
    android:layout_weight="1" />           ❹

  <fragment
    android:id="@+id/fragmentbookdescription"
    android:name="net.workingdev.ch14fragmentsbooks.BookDescription"
    android:layout_width="0px"
    android:layout_height="match_parent"
    android:layout_weight="1" />

</LinearLayout>

Listing 14-9/app/res/layout-land/activity_main.xml

这个项目的最后一部分是同步这两个片段。图 14-15 提醒我们我们的小项目应该做什么。

img/463887_1_En_14_Fig15_HTML.jpg

图 14-15

同步片段

当用户单击 book_titles 片段中的一个单选按钮时,book_description 片段应该会改变并显示当前所选书籍的描述。前面我们在 BookDescription 类中写了 changeDescription 函数;我们可以简单地从 BookTitle 类中调用这个函数,但这不是好的做法。为什么?因为如果我们这样做了,BookTitle 类将会知道很多关于 BookDescription 类的信息——这使得前者依赖于后者。开发人员称之为“紧耦合”,大多数时候应该避免这种情况。

如果我们不直接从 BookTitle 调用 changeDescription,我们要怎么做呢?图 14-16 显示了我们的表演。

img/463887_1_En_14_Fig16_HTML.png

图 14-16

片段之间的通信

这个想法是通过主要活动来引导行动。在序列图中,BookTitle 调用活动中的 onBookChanged 函数,然后活动调用 BookDescription 中的 changeDescription 函数。敏锐的读者可能会注意到,我们只是简单地将依赖性从图书描述转移到主活动,这将使图书标题依赖于(并紧密耦合于)主活动。如果我们把主要活动和书名特别联系起来,你可能是对的。我们不会。我们将使用一个接口来代替;这种方法给了我们某种程度的间接性。它不再是紧密耦合的——至少,没有那么紧密。这是我们要做的。

  1. 创建一个协调器接口——让我们把它命名为协调器,为什么不呢?

  2. 在 MainActivity 中实现协调器接口。

  3. 使用 BookTitle 中的协调员类型。当我们需要调用 BookTitle 中的 coordinator 方法时,我们将针对 Coordinator 类型进行调用,而不是针对 MainActivity。

要创建一个接口,在项目工具窗口中右击你的项目包(如图 14-17 ),然后点击新建Kotlin 文件/类

img/463887_1_En_14_Fig17_HTML.jpg

图 14-17

创建新的 Kotlin 文件/类

如图 14-18 所示,将其命名为“协调器”,然后将“种类”改为“接口”

img/463887_1_En_14_Fig18_HTML.jpg

图 14-18

新界面

清单 14-10 显示了协调器接口的代码。

| -好的 | 声明一个接口。 | | ❷ | 声明一个抽象方法。它接受一个 Int 参数。此参数代表 bookdescriptions 数组中的元素编号。无论我们在这里收到什么值,我们都将使用它来调用 BookDescription 片段中的 **changeDescription** 方法。顺便说一下,我们不必显式地将这个方法声明为*公共*和*抽象*——这是接口中所有方法的默认设置。 |
interface Coordinator {         ❶
  fun onBookChanged(index:Int)  ❷
}

Listing 14-10Coordinator.Kt

接下来,让我们在 MainActivity 中实现这个接口。清单 14-11 显示了带注释的代码。

| -好的 | 让我们实现协调器接口。 | | ❷ | 覆盖 **onBookChanged** 方法。这在协调器接口中被声明为抽象的;我们必须在 MainActivity 中覆盖它,这样我们才能提供具体的行为。 | | -你好 | 我们来获取一个对 BookDescription 片段的引用; **fragmentbookdescription** 是片段的 **id** 。这个调用返回一个**片段**类;*还没有*图书描述类。如果你以前使用 Java 处理过片段,你可能记得我们需要使用 **findFragmentById** 来做这种事情。我们不必再这样做了。Kotlin Android 扩展让我们可以通过 id 直接引用片段——它已经在 MainActivity 中合成了。 | | (a) | 我们将**片段**(仍然是一个片段类)转换为**图书描述**。Kotlin 中的 **is** 操作器足够聪明,可以为我们自动执行转换。我们不必再执行显式强制转换。这是 Java 和 Kotlin 的又一个区别;在前一种情况下,你必须显式强制转换。在 Kotlin 中, **is** 操作符不仅相当于的**实例,它还为我们执行智能强制转换。** | | (一) | 现在,我们可以调用 BookDescription 类的 **changeDescription** 方法。 |
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity(), Coordinator { ❶

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

  override fun onBookChanged(index:Int) {   ❷
    val frag = fragmentbookdescription      ❸
    if (frag is BookDescription) {          ❹
      frag.changeDescription(index)         ❺
    }
  }
}

Listing 14-11MainActivity, Annotated

剩下要做的是在 BookTitle 类中进行更改。当单击单选按钮时,我们将执行以下操作:

  1. 找出哪个按钮被点击了。

  2. 根据单击时 radiobutton 的值,我们将为一个索引变量赋值;0—“学习 Android Studio 3”;1——跟 Kotlin 学习《Android Studio 3》;以及2–“最少安卓编程。整数 0、1 和 2 对应于 bookdescriptions.xml 的三个数组元素。

  3. 使用协调器类型获取对 MainActivity 的引用;然后

  4. 调用 onBookChanged 方法。

清单 14-12 展示了所有这些在代码中的样子。

| -好的 | 让我们找出哪个按钮被点击了。 | | ❷ | 如果是“学习 Android Studio 3”的按钮,我们会将**索引**的值设置为 0,并相应地为 *rlas3kotlin* 和*terminandroid 设置**索引**的值。***when**构造实质上是将 radiobutton 的运行时值转换成一个 Int,我们可以用它作为数组的索引。 | | -你好 | 让我们获取一个对当前正在运行的活动的引用,它是 **MainActivity** 。注意 **getActivity()** 不返回 MainActivity 的具体实例;它只是返回 main activity(fragmentation activity)的超类型。 | | (a) | 让我们将**活动**转换为**协调者**类型。 | | (一) | 最后,调用 **onBookChanged** 方法。 |
class BookTitle : Fragment(), View.OnClickListener {

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val v = inflater.inflate(R.layout.book_titles, container, false)
    return v
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    rlas3.setOnClickListener(this)
    rlas3kotlin.setOnClickListener(this)
    rminandroid.setOnClickListener(this)
  }

  override fun onClick(v: View?) {
    var index:Int = 0
    when(v?.id) {                 ❶
      R.id.rlas3 -> {             ❷
        index = 0
      }
      R.id.rlas3kotlin -> {
        index = 1
      }
      R.id.rminandroid -> {
        index = 2
      }
    }

    val activity = getActivity()    ❸
    if (activity is Coordinator) {  ❹
      activity.onBookChanged(index) ❺
    }
  }
}

Listing 14-12BookTitle, Annotated

我们已经把所有的点联系起来了。现在我们可以:

  • 在一个片段中使用单选按钮显示书籍;

  • 在另一个片段中显示当前所选书籍的描述;和

  • 根据设备方向的变化调整片段的布局。

尝试在模拟器中运行应用。点按几个按钮,然后尝试将方向从纵向更改为横向。尝试在从纵向模式到横向模式之间切换单选按钮。如果您想从横向切换到纵向,请使用模拟器上的旋转按钮(如图 14-19 所示),反之亦然。

img/463887_1_En_14_Fig19_HTML.png

图 14-19

设备旋转按钮,模拟器

您可能已经注意到,当您更改设备方向时,这两个片段不同步。 book_description 片段总是回到“学习 Android Studio 3”的描述(bookdescription 数组上的第一个元素)。

只要您不改变设备的方向,这两个片段就会保持同步。当你改变方向时,片段会发生一些变化。

随着设备方向的改变,MainActivity 及其片段会发生一些变化。记住一个活动有一个生命周期?片段也有生命周期——它们与活动的生命周期相似,但也有显著的不同。我们不会进入片段生命周期的细节,也不会讨论活动生命周期如何影响片段的生命周期。我只想指出,当你改变装置的方向时,这个活动,连同片段,将会被拆除并重新构建。活动可能会进入以下状态并在其中转换(回调):

  1. **activity . onSaveInstanceState .**将调用片段的 onSaveInstanceState。

  2. Activity.onPause 。将调用片段 onPause。

  3. 活动停止。Fragment 的 onStop 也将被调用。

  4. Activity.onCreate 。片段的 oncreate➤oncreate 视图➤ onViewCreated 将被调用。

  5. Activity.onStart 。将调用片段 onStart。

  6. activity . onrestoreinstancestate

  7. Activity.onResume 。将调用片段的 onRestoreInstance。

这里需要注意的是,当你改变方向时,片段会失去它们当前的状态。我们需要找到一种方法来保存数组索引的值(在 BookDescription 类中),在它被拆除并重新构建之前。幸运的是,我们知道运行时会调用活动的 onSaveInstanceState ,推而广之,也会调用片段的onSaveInstanceState;这个方法让我们将值保存在一个包中,所以当设备旋转时,我们将使用它来保存数组索引的值。清单 14-13 显示了 BookDescription 类的完整和带注释的代码。

| -好的 | 我们需要检查“bookindex”键是否不为空。我们第一次启动应用时,它将为空,因为应用还没有调用 **onSaveInstanceState** 。如果是 null,我们就默认**book index = 0**;我们使用数组中的第一个描述。 | | ❷ | 如果它不为空,我们已经在“bookindex”键中保存了一个值;因此,获取“bookindex”的值,并将 **bookindex** 变量的值设置为该值。 | | -你好 | 就在活动和片段被拆分和重建之前,运行时调用 **onSaveInstanceState** 。这个方法让我们可以访问一个 Bundle 对象;这是我们在 **onCreateView** 回调期间得到的同一个 Bundle 对象。使用键“bookindex”将 **bookindex** 的当前值保存到包中。 |
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.book_description.*

class BookDescription : Fragment() {

  lateinit var arrbookdesc: Array<String>
  var bookindex = 0

  override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val v = inflater.inflate(R.layout.book_description, container, false)
    arrbookdesc = resources.getStringArray(R.array.bookdescriptions)

    bookindex = if(savedInstanceState?.getInt("bookindex") == null) 0else { savedInstanceState.getInt("bookindex")}                      ❷

    return v
  }

  override fun onSaveInstanceState(outState: Bundle) {                  ❸
    outState.putInt("bookindex", bookindex)
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    changeDescription(bookindex)
  }

  fun changeDescription(index:Int) : Unit {
    bookindex = index
    println("BOOK INDEX = $bookindex")
    txtdescription?.setText(arrbookdesc[bookindex])
    println(arrbookdesc[bookindex])
  }
}

Listing 14-13Complete Code for BookDescription, Annotated

片段演示,动态

既然我们知道了如何在设计时处理片段,让我们看看如何动态地处理片段。为了动态地添加片段,我们通常必须做以下事情:

  1. 为片段创建布局资源和相应的 Kotlin 类;就像我们在之前的项目中所做的一样。

  2. 在 MainActivity 中,我们创建了 fragment 类的一个实例。

  3. 创建 FragmentManager 和 FragmentTransaction 对象的实例。

  4. 为活动布局文件中的片段创建占位符。占位符是我们稍后放置片段的地方。

  5. 使用 FragmentTransaction 对象,将片段添加到活动中。

这个项目和上一个几乎一样。唯一的区别是我们添加片段的方式。我认为最好为此创建一个新项目,这样可以保持之前的项目不变,以备将来参考。

用以下细节创建一个新项目(表 14-2 )。

表 14-2

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | CH14FragmentsBooksDynamic | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 |

在大多数情况下,您只需复制并粘贴上一个项目中的文件。我建议你不要复制整个项目文件夹。创建一个新项目,并在以前的项目中重新创建您的步骤;使用与前一个项目完全相同的文件名创建相同的类、接口、xml 资源和 UI 资源。然后,将前一个项目中的文件内容复制到新项目的相应文件中。

完成后,表 14-3 显示了当前项目中哪个文件保持不变,哪个文件将发生变化。

表 14-3

新项目中的变更摘要

|

文件

|

描述

| | --- | --- | | MainActivity.Kt 公司 | 变化=是。我们需要添加 FragmentManager 和 FragmentTransaction 代码。 | | activity_main.xml | 变化=是。我们将移除元素并用一个占位符替换它。 | | book_description.xml | 更改=否。保持不变。你可以复制粘贴然后不管它。 | | 图书描述。滨鹬 | 更改=否。复制、粘贴,然后不去管它。 | | book_titles.xml | 更改=否。原样复制。 | | 书名.Kt | 更改=否。原样复制。 | | bookdescriptions.xml | 更改=否。原样复制。 | | 协调员。滨鹬 | 更改=否。原样复制。 |

正如您所看到的,这些更改都包含在主活动文件中。清单 14-14 显示了完整的代码,并注释了 activity_main.xml 中的变化。

| -好的 | 我们添加了一个 LinearLayout 容器;和 | | ❷ | 我们将第一个容器命名为 **fragtop** 。这是 BookTitles 片段的占位符。 | | -你好 | 我们添加了另一个 LinearLayout 容器;和 | | (a) | 将这个命名为 **fragbottom** 。这是 BookDescription 片段的占位符。 |
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
  tools:context=".MainActivity">

  <LinearLayout                             ❶
    android:id="@+id/fragtop"               ❷
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:orientation="horizontal">

  </LinearLayout>

  <LinearLayout                             ❸
    android:id="@+id/fragbottom"            ❹
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:orientation="horizontal">

  </LinearLayout>
</LinearLayout>

Listing 14-14/app/res/layout/activity_main.xml

您会注意到 activity_main 不再包含 <片段> 元素。相反,我们放置了两个 LinearLayout 容器作为片段的占位符。当我们调用将片段添加到我们的活动中时,我们将把它们放在这些占位符中。这就是 UI 资源布局的变化程度。大部分的变化实际上将发生在主活动上。

在之前的项目中,我们静态地将片段添加到活动中,就片段而言,我们做得不多;但是现在我们将动态添加片段,我们将需要添加必要的代码来在运行时添加片段。

要动态地处理片段,您需要两个对象:FragmentManager 和 FragmentTransaction。您可以使用 FragmentManager 做很多事情,比如通过 Id 和标签查找片段;但是出于我们的目的,我们将只使用它来获取 FragmentTransaction 对象。

FragmentTransaction 负责在运行时添加、附加、分离和删除片段。出于我们的目的,我们将只使用它来添加片段。

清单 14-15 中显示了 MainActivity 的完整代码。

| -好的 | 创建一个 **BookTitle** 片段的实例。 | | ❷ | 创建一个 **BookDescription** 片段的实例。 | | -你好 | 让我们获取一个 FragmentTransaction 对象。 **supportFragmentManager** 是 Android Studio 和 Kotlin 的一项便利功能。实际的调用是**getSupportFragmentManager()**,但是它已经为我们合成了,所以我们不必使用实际的方法。接下来, **beginTransaction()** 调用是一个工厂方法,它给了我们一个 FragmentTransaction 对象。 | | (a) | 让我们使用 FragmentTransaction 来添加一个片段。 **add** 方法有两个参数:1.视图对象的 id。这是我们在 activity_main.xml (fragtop)中添加的 LinearLayout 占位符的 id。2.片段的实例(fragBookTitle) | | (一) | 同样的,我们再加上图书描述片段。 | | ❻ | 我们必须调用 FragmentTransaction 的 **commit()** 方法来完成 FragmentTransaction 中的所有更改。如果不调用这个方法,什么都不会发生——不会添加片段。 | | ❼ | 您还记得这个方法,当用户单击 **BookTitle** 片段中的一个单选按钮时,该片段将调用 MainActivity 中的 **onBookChanged()** 方法。 | | ❽ | 在之前的项目中,我们必须找到 book_description 片段的 id,然后在调用 **changeDescription** 之前将其转换为 BookDescription 对象。我们不必再这样做了,因为我们可以直接引用 **BookDescription** 片段的实例。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity(), Coordinator {

  lateinit var fragBookDescription: BookDescription
  lateinit var fragBookTitle: BookTitle

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

    fragBookTitle = BookTitle()                 ❶
    fragBookDescription = BookDescription()     ❷
    val fragTransaction = supportFragmentManager.beginTransaction() ❸
    fragTransaction.add(R.id.fragtop, fragBookTitle)  ❹
    fragTransaction.add(R.id.fragbottom, fragBookDescription) ❺
    fragTransaction.commit() ❻

  }

  override fun onBookChanged(index:Int) {         ❼
    fragBookDescription.changeDescription(index)  ❽
  }
}

Listing 14-15MainActivity, Annotated

练习和本章到此结束。我们仅仅触及了片段的表面——它们比这里呈现的要多得多;但是希望,当你进一步探索它们的时候,这会给你一个好的基础。

章节总结

  • 像活动一样,片段可以包含视图元素。他们也是一个组成单位,但更小。

  • 您可以使用片段来响应不同的设备方向、外形或大小。

  • 片段和活动一样,也有生命周期回调。

  • 活动的生命周期对片段有影响。

  • 当您更改设备的方向时,活动(和片段)会被拆除并重新构建。他们经历了一系列的生命周期回调。

  • Android P 弃用 android.app.Fragments 。所以,如果你想使用片段,使用支持库中的类。

在下一章中,我们将了解 Android 所谓的“jank”以及如何在代码中避免它。

十五、在后台运行

我们将介绍的内容:

  • 用户界面线程

  • 线程和可运行程序

  • 处理程序和消息

  • 异步任务

  • 安科的 doasync

没有人愿意使用缓慢的应用。用户希望他们的应用简洁明快。每个开发人员都希望这样——没有人开始构建他们的应用时会说,“这个应用太快了,也许我应该让它慢一点”;没人会这么做。那么,为什么会有像糖蜜一样移动的应用呢?你可能见过我说的这些应用中的一些——你知道你试图滚动一个回收器视图或列表,然后它开始,停止,并发出劈啪声。呆滞。

我们可以列出一些应用运行缓慢的原因,但我敢打赌 10 大原因之一是主线程上运行的太多了。它可能被一个 I/O 例程或一个复杂的计算所拖累,或者两者兼而有之,这很糟糕。

这是否意味着你不应该在你的应用中进行任何 I/O 调用或任何复杂的计算?一点也不。但是你应该知道把 I/O 调用或者复杂的计算放在哪里;而且不在主线程上。

在这一章中,我们将看看如何让运行缓慢的代码远离主线程,从而让应用能够快速而敏捷地做出响应。

基本概念

应用启动时会创建一个流程。它被分配了一些资源,比如内存和其他一些它需要的东西,这样它才能完成它的工作。它至少有一个线程。

不严格地说,线程是一系列指令。它是真正执行你的代码的东西。在应用运行期间,线程将利用进程的资源。它可以读取或写入数据到内存、磁盘,有时甚至是网络 I/O。当线程与所有这些交互时,它实际上只是在等待。它不能在等待时利用 CPU 周期。我们不能让这些 CPU 周期白白浪费掉。可以吗?我们可以做的是创建其他线程,这样当一个或多个线程在等待时,其他线程可以利用 CPU。多线程应用就是这种情况。

当运行时创建应用的实例时,该进程被赋予一个线程。它被称为主线程。一些开发者称之为 UI 线程。运行时只给了我们一个线程,仅此而已。但好消息是我们可以创造更多。UI 线程被允许产生其他线程。

用户界面线程

在我们深入到生成或创建子线程的细节之前,让我们先讨论一下 UI 线程。它负责启动主活动和扩展布局 xml,以便其中的所有视图元素都变成实际的 Java 对象(例如,按钮、文本视图等)。).简而言之,它是负责 UI 的人。

当你发出类似 setText 或者 setHint 的调用时,会在主线程上完成;如果你认为这些调用会立即执行,那就错了。无论你在应用中写什么,一般都会遵循以下步骤:

  1. 这些语句将被放在一个 MessageQueue 中,并一直放在那里,直到

  2. 一个处理程序把它捡起来执行;最后

  3. 它在主线程上执行。

你可能会说,“知道这些很好,但那又怎么样呢?”。嗯,你应该关心这个,因为主线程不仅仅用于绘制 UI 元素。它也用于应用中发生的所有其他事情。记住,活动还有其他方法,如 onCreateonStoponResumeonCreateOptionsMenuonOptionsItemSelected ,以及其他类似的方法;每当代码在这些块上运行时,Android 运行时无法处理队列中的任何消息。处于阻塞状态;阻塞状态是一个并发术语,开发人员用它来表示应用正在等待某件事情完成,然后才能继续处理它的业务。不要在意行话——只要记住屏蔽可能对用户体验不利。

怎么会这样?答案是“因为我们只有一个线程来做所有这些事情。”这个问题的解决方案是创建一个后台线程或子线程,并在其中执行我们的非 UI 任务——但并不总是这样。如果您认为这个调用在处理资源方面足够便宜,比方说 1 ms 到 15 ms,那么就在主线程上进行吧。如果需要 16 毫秒以上的时间,你应该在后台线程上完成。

16 毫秒阈值是 Android 4.1 (Jellybean)发布时“黄油计划”中的一个指导原则。它旨在提高 Android 应用的性能。当运行时感觉到你在主线程上做了太多的事情,它会开始丢帧。当你不打昂贵的电话时,应用以平滑的 60 FPS(每秒帧数)运行。如果你绑定了主线程,你会开始注意到缓慢的性能,或者 Android 团队所说的“jank”。我没有一个明确的指导方针可以告诉你什么是昂贵的电话,什么是便宜的电话。不过,我能做的是,向你们展示这两个调用的例子;希望你能了解什么是昂贵的电话和便宜的电话。

清单 15-1 是一个廉价的调用,即使它将文本属性设置为一个计算值。计算很简单,UI 线程不会出一点汗。

button.setOnClickListener {
  txtsecondnumber.setText((2 * 2 * 2).toString())
}

Listing 15-1Set Text Attribute to a Calculated Value: A Cheap Call

清单 15-2 可能看起来很复杂,因为它计算 GCF。如果数字很大怎么办——这对主线程来说不是太繁重了吗?不完全是。清单 15-2 使用欧几里德算法寻找 GCF。该算法以常数时间或 O(1)执行;这是开发人员在谈论算法的时间复杂度或代码完成需要多长时间时使用的另一种行话。O(1)或恒定时间意味着无论输入是大还是小,算法都将执行相同的操作;无论我们找到的 GCF 是 12 和 15 还是 16,848,662 和 24,时间复杂度都没有太大变化。所以,把这个放在主线程里还是挺好的。

注意

算法的时间复杂度可以表示为 O(1)、O(N)、O(N 2 )、O(2 N )或 O(log N),其中 N 代表输入的大小。这是一个叫做大 O 的符号。了解它是有好处的——特别是如果你想写性能代码的话。

button.setOnClickListener {

  val numfno = txtfirstnumber.text.toString().toInt()
  val numsno = txtsecondnumber.text.toString().toInt()

  var numbig = if(numfno > numsno) numfno else numsno
  var numsmall = if(numfno < numsno) numfno else numsno

  var rem = numbig % numsmall

  while(rem != 0) {
    numbig = numsmall
    numsmall = rem
    rem = numbig % numsmall
  }
  Toast.makeText(this@MainActivity, "GCF is $numsmall", Toast.LENGTH_LONG).show()
}

Listing 15-2Calculate GCF: Still a Cheap Call

清单 15-3 被认为是昂贵的,因为它调用网络 I/O。事实上,代码甚至根本不会编译,因为它将导致一个NetworkOnMainThreadException。IDE 甚至不让我们完成编译过程。根据经验,如果您的代码将进行 I/O 调用,无论是本地文件还是网络,您都应该在后台线程中进行。

button.setOnClickListener {
  val url = "https://api.github.com/users/tedhagos"
  println("inside doGetHttp")
  val client = OkHttpClient()
  val request = Request.Builder().url(url).build()
  val response = client.newCall(request).execute()

  val bodystr = response.body().string()
}

Listing 15-3Read Something from GitHub: Expensive Call

清单 15-4 不做任何 I/O,但是函数 killSomeTime 模拟一个昂贵的调用。

button.setOnClickListener {
    killSomeTime()
  }
}

private fun killSomeTime() {
  for (i in 1..20) {
    textView.text = i.toString()
    println("i:$i")
    Thread.sleep(2000)
  }
}

Listing 15-4Do Something That Blocks: Expensive Call

清单 15-4 中的 Thread.sleep 调用完全暴露了代码将会阻塞的情况,但是它可以模拟一些需要 2 秒钟才能完成的事情。乍一看,你可能认为文本视图会每 2 秒更新一次来显示 i 的当前值,但这不会发生,因为运行时已经降低了帧速率。UI 线程无法更新 textView,因为它正在等待线程唤醒和恢复。

想象一下,如果您有一个类似于清单 15-5 的代码—它没有任何 I/O 调用或 Thread.sleep ,但是它不会像您所期望的那样更新文本字段(在循环的第二层)—同样,因为主线程正忙于计算笛卡尔积。

button.setOnClickListener {
  for (i in 1..100000) {
    for (j in 1..10000) {
      txtfirstnumber.setText((i*j).toString())
      for (k in 1..10000) {
        println("i: $i | j: $j | k$k | i*j*k = ${i*j*k}")
      }
    }
  }
}

Listing 15-5Deeply Nested Calculation: Expensive Call

注意

笛卡尔积是一个数学集合,它是其他集合相乘的结果。

在 Android 的早期版本中,在 Project Butter 之前,清单 15-3 、 15-4 和 15-5 中显示的代码可能会导致 ANR 错误(Android 没有响应)。如今,他们可能不再画 ANR 了,但是更大的担忧是 jank。为了避免 jank,我们应该将那些昂贵的调用转移到后台线程。在 Android 中有很多方法可以做到这一点。有些解决方案是在框架层面上找到的,比如 Loader API 或 AsyncTaskLoader 然而,这些东西从 API 28 开始就被弃用了,所以最好远离它们。在后台也有一些低级的方法来完成一些任务,它们是:

  • 线程和可运行线程,来自 Java

  • AsyncTask 是 Android 框架的一部分

  • 处理程序和消息,也是 Android 框架的一部分

  • Anko 的 doAsync 是一个用 Kotin 编写的第三方库

线程和可运行程序

让我们使用清单 15-14 作为我们探索的用例。要运行这些代码,你需要一个类似图 15-1 的用户界面;我们的基本 UI 的 xml 代码在清单 15-6 中。

img/463887_1_En_15_Fig1_HTML.png

图 15-1

我们的基本活动 _ 主要布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
  tools:context=".MainActivity">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:text="Button"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView" />

  <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="32dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 15-6/app/res/layout/activity_main.xml

如果你现在试图运行清单 15-4 ,它将会运行;但是不会跑的很好。您将注意到以下内容:

  1. 你期望文本视图每 2 秒刷新一次,以显示 i 的当前值。不会的。这些帧会被删除,所以你看不到任何用户界面活动。

  2. 但是你会看到 I 的值,因为它在 Logcat 窗口中每 2 秒更新一次。这是因为 println 不受帧率降低的影响——输出是在控制台中,而不是在 UI 中。

  3. 您可能会看到来自运行时的**编排器:**的类似这样的消息

    07-31 15:51:29.646 13403-13403/net.workingdev.ch15scratchasynctask I/Choreographer: Skipped 2402 frames! The application may be doing too much work on its main thread.
    
    

虽然这款应用没有获得 ANR 奖,但速度明显变慢了。你肯定能感觉到一些玩笑。为了解决这个问题,让我们将 janky 代码移到后台线程中。

要创建一个线程并启动它,您需要执行以下操作:

  1. 创建一个实现 Runnable 类型的类。

  2. 任何你想在后台运行的东西,把它放在被覆盖的 run 方法中。

  3. 创建一个线程对象,然后将您刚刚在步骤 1 中创建的 Runnable 对象传递给线程的构造函数。

  4. 调用线程的 start 方法。

  5. 每当变量 i 的值改变时,我们更新 TextView。

在代码中,它看起来像下面这样(参见清单 15-7 )。

class MainActivity : AppCompatActivity() {

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

    button.setOnClickListener {
      val runnable = Worker()
      val thread = Thread(runnable)
      thread.start()
    }
  }

  inner class Worker : Runnable {
    override fun run() {
      killSomeTime()
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      Thread.sleep(2000)
      println("i: $i")
    }
  }
}

Listing 15-7
Threads and Runnables

到目前为止,在本书的第十五章中,你已经知道了匿名对象、lambdas 以及如何链接函数调用。我们应该能做出这样的东西:

| -好的 | 使用 Kotlin lambda 表达式创建一个可运行的匿名对象。它被传递给一个线程类的构造函数。 | | ❷ | 我们不必再编写 **run** 方法了。Runnable 是一个 SAM 类(一个只有一个抽象方法的类)。在 lambda 表达式中使用 SAM 类时,不需要显式编写抽象方法的名称。 | | -你好 | 调用 **start** 将线程踢入高速档。 |
 button.setOnClickListener {
  Thread(Runnable {     ❶ ❷
    killSomeTime()
  }).start()            ❸
}

如果我们只想将 ln 打印到控制台,我们的代码现在应该可以正常工作。但是请记住,我们需要将 TextField 的值设置为当前值 i

不允许后台线程更改 UI 中的任何内容。这个责任只属于 UI 线程。因此,我们需要解决的下一个问题是如何回到 UI 线程,以便我们可以更新 TextView。有几种方法可以做到这一点,但最简单的是调用 Activity 类的 runOnUiThread 方法。

runOnUiThread 方法获取一个 Runnable 对象,并在主线程中执行该 Runnable 对象的代码。清单 15-8 显示了 MainActivity 的完整的、带注释的和解释的代码。

| -好的 | 要创建一个后台线程,需要创建一个 Runnable 类型的实例(thread)并 **start** 它。**线程**构造函数采用 Runnable 类型并执行 **run** 方法中的任何内容。我在这行中使用了一个对象表达式来创建一个 Runnable 类型的实例,而没有创建一个名为子类的*——有点像 Java 的匿名类。* | | ❷ | 我们现在在 Runnable 的 **run** 方法中。我们在后台线程中。 | | -你好 | 别忘了在线程对象上调用 **start** 。 | | (a) | 后台线程的限制之一是它**不能**做任何修改 UI 的事情。任何 UI 修改代码都必须从创建 UI 的原始线程运行——也就是 UI 线程。如果需要从后台线程改变 UI(像这样),可以调用**活动**类的 **runOnUiThread** 方法。它采用一个 Runnable 类型(再次),你可以把所有的 UI 修改代码放在这个 Runnable 类型的 **run** 方法上。 |
import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

    button.setOnClickListener {
      Thread(Runnable {                ❶
        killSomeTime()                 ❷
      }).start()                       ❸
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      runOnUiThread(Runnable{          ❹
        textView.text = i.toString()
      })
      println("i:$i")
      Thread.sleep(2000)
    }
  }
}

Listing 15-8Full Code of MainActivity, With Annotations

当您运行这段代码时,您应该每 2 秒钟看到一次变量 i 的更新值。的编舞也不会再因为丢帧而烦扰我们,因为我们回到了非常流畅的 60 FPS 的速度。

使用处理程序类

与线程不同,Handler 类是 Android 框架的一部分,而不是 Java 的一部分。处理程序对象主要用于管理线程。还记得之前关于将代码放入 MessageQueue 的讨论吗;它在那里等待,直到被拾取和执行——是处理程序进行拾取和执行。

基本思想是获取对主线程处理程序的引用,然后,当我们在后台线程中时(在这里我们不能进行任何 UI 更改),向处理程序对象发送一个消息。使用消息对象在后台线程和主线程之间传递数据。

要使用 Handler 对象,您需要执行以下操作:

  1. 获取与 UI 线程关联的处理程序对象。

  2. 在你代码的某个地方,当你要做一些可能导致 jank 的事情时,在后台线程上运行它。

  3. 当您在后台线程中时,当您需要更改 UI 中的某些内容时,请执行以下操作:

    a.创建一个消息对象,最好的方法是调用消息.获取()

    b.通过调用 sendMessage 方法向 Handler 对象发送消息。消息对象可以携带数据。消息对象的数据属性是一个 Bundle 对象,所以你可以对它使用各种putXXX()方法(例如 putString、putInt、putBundle、putFloat 等)。).

  4. 您可以在 Handler 对象的 handleMessage 回调中更改 UI。

清单 15-9 展示了所有这些是如何在代码中组合在一起的。

| -好的 | 将处理程序对象声明为类的属性。我们需要从我们的两个顶级功能访问它。我们在这里使用 **lateinit** 是因为我们还没有准备好定义对象。 | | ❷ | 我们现在正在定义处理程序对象。我们正在获取与 UI 线程关联的 Handler 对象。 | | -你好 | 在这里修改用户界面是安全的。这是与 UI 线程相关联的处理程序。当我们调用 **sendMessage** 时,运行时将调用 **handleMessage** 回调。此方法的消息参数携带数据。 | | (a) | **kill some**是任何 I/O 或耗时任务的代表。总是在后台线程中运行它,以避免 jank。 | | (一) | 创建一个消息对象。这是我们稍后将发送给处理程序的内容。 | | ❻ | 消息对象的**数据**属性就像一个**包**——你可以把东西放在里面。它就像一本字典,每个条目都是一对——一个键和一个值。我们向 putString()方法传递了两样东西,它们是:1.【计数器】、**键**2.`i.toString(),`**值** | | ❼ | 将消息发送到处理程序对象。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

  lateinit var mhandler: Handler                        ❶

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

    mhandler = object : Handler() {                     ❷
      override fun handleMessage(msg: Message?) {
        textView.text = msg?.data?.getString("counter") ❸
      }
    }

    button.setOnClickListener {
      Thread(Runnable {
        killSomeTime()                                  ❹
      }).start()
    }
  }

  private fun killSomeTime() {
    for (i in 1..20) {
      var msg = Message.obtain()                        ❺
      msg.data.putString("counter", i.toString())       ❻
      mhandler.sendMessage(msg)                         ❼
      Thread.sleep(2000)
    }
  }
}

Listing 15-9Full Listing for MainActivity, Annotated and Explained

当您运行这段代码时,它的性能与我们前面的线程示例一样好。

异步任务

另一种在后台运行代码的方法是使用 AsyncTask 类。AsyncTask 和 Handler 类一样,是 Android 框架的一部分。像处理程序一样,它有一个在后台完成工作的机制,并且它还提供了一个更新 UI 的(更干净的)方法。

要使用 AsyncTask,通常需要执行以下操作:

  1. 扩展 AsyncTask 类。

  2. 覆盖 AsyncTask 的 doInBackground 方法,这样您就可以完成后台工作。

  3. 重写几个 AsyncTask 的生命周期方法,这样就可以更新 UI 并报告后台任务的整体状态。

  4. 创建 AsyncTask 子类的一个实例,并调用execute——这就是如何启动后台操作。

AsyncTask 不如简单线程受欢迎的原因之一是它使用泛型。AsyncTask 类是参数化的。在使用它之前,您必须指定三种类型。清单 15-10 向我们展示了如何创建 AsyncTask 类的子类。

| -好的 | AsyncTask 是一个参数化类。在使用它之前,您必须指定三种类型。这三种类型按出现的顺序如下:**a .**Params。这是您需要传递给 AsyncTask 的信息,以便它可以执行后台任务。它可以是任何东西,比如 URL 列表、视图对象或字符串。为了让它对我们来说更有挑战性,它是一个 *vararg* 参数。通常,开发人员使用此参数来传递视图元素,以便 AsyncTask 可以引用活动的视图对象。但是在我们的例子中,我将使 AsyncTask 成为一个内部类——这样,它可以引用 MainActivity 中的任何视图元素(这就是我使用 **Void** 作为第一个类型参数的原因——我根本不需要它)。**b .**??【进度】??。您希望后台线程传递给 UI 线程的信息类型,以便您可以告诉用户正在发生什么。**c .**结果。您想要指示后台操作结果的种类数据;大多数时候,这不是*真*就是*假*。如果操作成功,则为*真,否则为*假。** | | ❷ | 这是唯一需要覆盖的强制函数。顾名思义,这是您在后台做事情的地方。每当你需要读/写一个文件或一个网络 I/O 时,你会想在这里做。这个函数接受一个 *vararg* **Void** 参数,它对应于我们为类定义的第一个*类型参数*。如果您将第一个类型参数设置为字符串,那么 **doInBackground** 应该接受一个字符串。还要注意,这个方法返回一个布尔值;那是因为我们传递了一个**布尔值**作为第三个参数类型。 | | -你好 | 定期地,你可能想要通知用户你的应用正在进行什么,特别是如果它是一个冗长的操作。 **publishProgress** 方法允许您这样做。当你在 **doInBackground** 里面的时候,你不能对 UI 做任何修改。UI 更改需要发生在 UI 线程上。当您调用 **publishProgress** 时,Android 运行时将调用**onprogress update**——在那里您可以进行 UI 更改。无论您向 **publishProgress** 传递什么参数,onProgressUpdate 都会接收到它。 | | (a) | 当你在这个方法中时,所有的语句都将在 UI 线程上执行。这是您对视图对象进行更改的地方。该方法接受一个字符串参数,因为我们将**字符串**作为 AsyncTask 类的第二个类型参数进行了传递,并且它与该类型参数相对应。在我们从**的背景**方法中调用**的发布进度**后,这个方法将被调用;无论您传递给 **publishProgress** 什么数据,都将由 **onProgressUpdate 接收。** | | (一) | 当 **doInBackground** 完成时,运行时将调用该方法。doInBackground 返回了**结果**参数。 |
AsyncTask<Void, String, Boolean> {                                ❶

  override fun doInBackground(vararg p0: Void?) : Boolean {       ❷
    // statement
    publishProgress("status of anything")                         ❸
  }
  override fun onProgressUpdate(vararg values: String?) {
    // update the UI                                              ❹
  }
  override fun onPostExecute(result: Boolean?) {
    println(result)                                               ❺
  }
}

Listing 15-10Subclassing the AsyncTask

现在我们已经熟悉了 AsyncTask 的结构,让我们看看如何在我们的计数示例中使用它。清单 15-11 显示了在 MainActivity 中使用 AsyncTask 的完整和带注释的代码。

| -好的 | 创建一个**工作者**的实例,然后**执行**它。 | | ❷ | 将 AsyncTask 定义为内部类,这样我们就可以引用封闭 MainActivity 的视图对象。*类型参数*解释如下。**a .作废**。我真的不需要传递任何东西给 AsyncTask,所以,Void。**b .字符串**。方法 **onProgressUpdate** 将更新文本视图。因为我们将使用第二种类型来更新值 TextView,所以 String 似乎是一个不错的选择。**c .布尔**。当我们完成**的后台**时,我们想要设置一个状态来表示成功或失败;布尔似乎是一个很好的选择。 | | -你好 | 我们来告诉用户 *i* 的当前值是多少。onProgressUpdate 采用字符串参数;这就是为什么我们要把 *i* 转换成一个整数。 | | (a) | 这模拟了长度运算。 | | (一) | 既然我们在 UI 线程中,我们可以安全地将 TextView 的*文本*属性设置为当前的 *i* 值。我们只从 **publishProgress** 传递了一个参数,所以如果我们想得到它,它是 **values** 参数的第 0 个元素。 |
import android.os.AsyncTask
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

    button.setOnClickListener {
      Worker().execute()                                           ❶
    }
  }

  inner class Worker : AsyncTask<Void, String, Boolean>() {        ❷

    override fun doInBackground(vararg p0: Void?) : Boolean {
      for (i in 1..20) {
        publishProgress(i.toString())                              ❸
        Thread.sleep(2000)                                         ❹
      }
      return true
    }

    override fun onProgressUpdate(vararg values: String?) {
      textView.text = values[0]                                    ❺
    }

    override fun onPostExecute(result: Boolean?) {
      println(result)
    }
  }
}

Listing 15-11Full Code for MainActivity, Annotated and Explained

与处理程序和线程类一样,AsyncTask 将释放 UI 线程。当你运行这个程序时,它会以 60 帧/秒的速度发出咕噜声。

安科的 doasync

Anko 是 JetBrains(创建 Kotlin 的同一家公司)用 Kotlin 编写的 Android 库。您可以将它用于各种各样的任务,但是对于我们的目的,我们只需要 doAsync 部分。顾名思义,Anko 的 doAsync 将让我们异步或在后台运行代码。

在使用 Anko 之前,您需要将它添加到项目的 Gradle 文件的依赖项中,如清单 15-12 所示。

dependencies {
  ....
  implementation 'org.jetbrains.anko:anko-common:0.9'
}

Listing 15-12/app/build.gradle

使用 doAsync 的语法如清单 15-13 所示。

| -好的 | 在这里,您可以读取或写入大文件,从互联网上下载文件,或者执行需要很长时间才能完成的任务。该块将在后台线程中执行。 |
doAsync {
  // do things in the background  ❶
}

Listing 15-13Syntax for doAsync

下一个挑战是如何回到 UI 线程。请记住,后台线程不允许更改 UI 中的任何内容。Anko 的方法可能是我们在前面章节中讨论的所有其他选项中最简单的。清单 15-14 展示了一个样例代码,它展示了 doAsync 如何在后台运行代码,以及它如何返回到 UI 线程。

清单 15-14 。doAsync 和 activityUiThread

| -好的 | 后台处理。 | | ❷ | 现在,您回到了 UI 线程。就这么简单。无论何时你需要返回 UI 线程,你都可以在**activity ithread**块中完成。 |
doAsync {
  // do things in the background  ❶
  activityUiThread {
    // make changes to the UI     ❷
    textView.text = "Hello"
  }
}

清单 15-15 显示了 MainActivity 的完整代码示例。它使用 Anko 的 doAsync 来执行长时间的计算,然后将一些内容写回 UI。

| -好的 | 让我们设置一个基本的 OnClickListener。这将触发后台任务。 | | ❷ | 让我们从 1 数到 15。 | | -你好 | 这模拟了一个长时间运行的任务。我们的循环大约要进行 15 次,所以完成这个任务总共需要 30 秒。 | | (a) | 让我们告诉用户这个应用是怎么回事。用 *i.* 的当前值更新 TextView 对象 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync

class MainActivity : AppCompatActivity() {

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

    button.setOnClickListener {             ❶
      doAsync {
        for(i in 1..15) {                   ❷
          Thread.sleep(2000)                ❸
          activityUiThread {
            textView.text = i.toString()    ❹
          }
        }
      }
    }
  }
}

Listing 15-15Full Code for MainActivity Using doAsync, Annotated and Explained

doAsync 就像之前的 Thread、Handler 和 AsyncTask 示例一样,应该表现得一样好。当您运行这段代码时,应用将以 60 FPS 的速度流畅运行。

您已经看到了在后台执行任务的四种底层技术。希望代码示例给了您足够的想法,让您可以继续自己的工作。

现实世界的例子

在我们结束这一章之前,让我们研究一些你可能会在项目中用到的东西。让我们使用 GitHub 的公共 API 从 GitHub 获取一些用户信息。GitHub 允许任何人访问 https://api.github.com/users/<username> 。如果您有 GitHub 帐户,请尝试使用您的 GitHub 登录来调用此 URL,这样您就可以熟悉它返回的内容。清单 15-16 显示了使用我自己的 GitHub id (tedhagos)的 HTTP 调用的部分输出。

{
  "login": "tedhagos",
  "id": 1287584,
  "node_id": "MDQ6VXNlcjEyODc1ODQ=",
  "avatar_url": "https://avatars1.githubusercontent.com/u/1287584?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/tedhagos",
  "html_url": "https://github.com/tedhagos",
  "followers_url": "https://api.github.com/users/tedhagos/followers",
  "following_url": "https://api.github.com/users/tedhagos/following{/other_user}",
  "gists_url": "https://api.github.com/users/tedhagos/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/tedhagos/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/tedhagos/subscriptions",
  "organizations_url": "https://api.github.com/users/tedhagos/orgs",
  "repos_url": "https://api.github.com/users/tedhagos/repos",
  "events_url": "https://api.github.com/users/tedhagos/events{/privacy}",
  "received_events_url": "https://api.github.com/users/tedhagos/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Ted Hagos",
  "company": null,
  "blog": "https://workingdev.net",
  "location": null,
  "email": null,
  "hireable": null,
  "bio": "Currently CTO and Data Protection Officer of RenditionDigital International. Sometimes a writer and tech trainer."
}

Listing 15-16Sample JSON Response from GitHub API

我们想要做的如下:

  1. 提示用户输入 GitHub 帐户;是登录 id。我们将使用 EditText 的 hint 属性来告诉用户输入什么。

  2. 使用我们从用户那里获得的登录 id 编写 HTTP 请求。我们可以通过使用低级的 java.net 类来 DIY 我们的方法,但是那会分散我们对主题的注意力,所以我们将使用 OkHttp。这是一个第三方库,但它非常易于使用,最重要的是,易于理解。

  3. 对 GitHub API 进行 HTTP 调用,并在后台线程中运行。我们将在这个项目中使用 Anko 的 doAsync。这是最容易使用的。你不觉得吗?

  4. HTTP 调用返回一个 JSON 对象,如清单 15-16 所示。我们将解析 JSON 消息,只获取 name 属性的值。

  5. 我们将通过使用方法activity ithread返回到 UI thread,在那里,我们将使用 name 属性的值(我们从 JSON 对象获得的值)更新 textView。

表 15-1 显示了演示项目的详细信息。

表 15-1

项目详细信息

|

项目详细信息

|

价值

| | --- | --- | | 应用名称 | CH15GetGitHubInfo | | 公司域 | 使用您的网站名称 | | Kotlin 支架 | 是 | | 波形因数 | 仅限手机和平板电脑 | | 最低 SDK | API 23 棉花糖 | | 活动类型 | 空的 | | 活动名称 | 主要活动 | | 布局名称 | 活动 _ 主要 | | 向后兼容性 | 是的。应用兼容性 |

UI 截图如图 15-2 所示。我们将使用 EditText 获取用户的输入,并使用 TextView 显示返回的 JSON 文件的名称属性。

img/463887_1_En_15_Fig2_HTML.png

图 15-2

CH15GetGitHubInfo 的用户界面

清单 15-17 显示了 activity_main.xml 的完整清单

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
  tools:context=".MainActivity"
  tools:layout_editor_absoluteY="81dp">

  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="Button"
    app:layout_constraintStart_toStartOf="@+id/txtusername"
    app:layout_constraintTop_toBottomOf="@+id/txtusername" />

  <TextView
    android:id="@+id/txtusername"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="TextView"
    android:textSize="30sp"
    app:layout_constraintStart_toStartOf="@+id/txtsearchuser"
    app:layout_constraintTop_toBottomOf="@+id/txtsearchuser" />

  <EditText
    android:id="@+id/txtsearchuser"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="31dp"
    android:layout_marginTop="30dp"
    android:ems="10"
    android:inputType="textPersonName"
    android:text="Name"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

Listing 15-17/app/res/layout/activity_main.xml

在使用 OkHttp 和 Anko 库之前,您需要将它们的依赖项添加到项目的模块级 gradle 文件中。清单 15-18 显示了您需要添加到 /app/build.gradle依赖项部分的内容。

| -好的 | 您需要添加此项才能使用 OkHttp。 | | ❷ | 你需要添加这个,这样你才能使用 Anko 的 doAsync。 |
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.squareup.okhttp:okhttp:2.5.0'   ❶
    implementation 'org.jetbrains.anko:anko-common:0.9' ❷
}

Listing 15-18Add OkHttp and Anko to /app/build.gradle

在 gradle 文件中添加了 Anko 和 OkHttp 之后,您必须同步该文件。点击屏幕右上角的“立即同步”链接,如图 15-3 所示。

img/463887_1_En_15_Fig3_HTML.jpg

图 15-3

编辑后同步 gradle 文件

OkHttp 网站有一个展示基本用法的示例代码——如清单 15-19 所示。它是用 Java 写的,但是很容易改编为我们所用。

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

Listing 15-19Sample Code from 
http://square.github.io/okhttp/

清单 15-20 展示了我们 Kotlin 版本的 OkHttp 代码示例。

private fun fetchGitHubInfo(login_id: String): String {
  val url = https://api.github.com/users/$login_id
  val client = OkHttpClient()
  val request = Request.Builder().url(url).build()
  val response = client.newCall(request).execute()
  val bodystr =  response.body().string() // this can be consumed only once

  return bodystr
}

Listing 15-20Our Kotlin Version of OkHttp Code

够近了。顺便说一句,我希望你注意到了清单 15-20 的倒数第二行——我甚至注释了它。调用 **response.body.string,**时只能消费一次,不能这样调用:

println(response.body.string())            // consumes the content
val bodystr =  response.body().string().   // no more JSON file here

response.body.string 调用不是等幂。你不能重复调用它,并期望它每次调用都返回相同的结果。

现在我们已经得到了我们需要的一切,是时候编写 MainActivity 了。清单 15-21 显示了 MainActivity 的完整和带注释的代码。

| -好的 | Anko 的 **doAsync** 块从这里开始。这个块中的所有东西都将在后台线程中运行。 | | ❷ | 让我们将 **txtsearchuser** EditText 的当前值传递给 **fetchGitHubInfo** ,并将结果 JSON 对象赋给 *mgithubinfo* 变量。 | | -你好 | 让我们用内置的 **JSONObject** 解析 *mgithubinfo* 。 | | (a) | 现在我们需要返回 UI 线程,这样我们就可以将 http 调用的结果写入 UI。 | | (一) | **activity ithread**块让我们回到 UI 线程并做一些更改。我们将 **txtusername** 的**文本**属性设置为 JSON 文件的 name 属性。 |
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.squareup.okhttp.OkHttpClient
import com.squareup.okhttp.Request
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.activityUiThread
import org.jetbrains.anko.doAsync
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

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

    button.setOnClickListener {
      doAsync {                                                           ❶
        val mgithubinfo = fetchGitHubInfo(txtsearchuser.text.toString())  ❷
        val jsonreader = JSONObject(mgithubinfo)                          ❸
        activityUiThread {                                                ❹
          txtusername.text = jsonreader.getString("name")                 ❺
        }
      }
    }
  }

  private fun fetchGitHubInfo(login_id: String): String {
    val url = "https://api.github.com/users/$login_id"
    val client = OkHttpClient()
    val request = Request.Builder().url(url).build()
    val response = client.newCall(request).execute()
    val bodystr =  response.body().string() // this can be consumed only once

    return bodystr
  }

  override fun onResume() {
    super.onResume()

    txtsearchuser.setText("")
    txtsearchuser.setHint("Enter GitHub username")
  }
}

Listing 15-21MainActivity, Annotated and Explained

在运行应用之前,还有一件事要做:我们需要将 INTERNET 权限添加到清单文件中。

| -好的 | 您应该将它添加到项目的 AndroidManifest 文件中。 |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="net.workingdev.ch15getgithubinfo">
  <uses-permission android:name="android.permission.INTERNET"/> ❶
  <application
  ....
  </application>=
</manifest>

Listing 15-22
AndroidManifest.xml

图 15-4 显示了正在运行的应用。

img/463887_1_En_15_Fig4_HTML.jpg

图 15-4

仿真器上的 CH15GetGitHubInfo

章节总结

  • 什么是 jank?当你试图在 UI 线程上做太多事情时,Android 运行时会开始丢帧。当你的应用的 FPS 下降时,用户界面会断断续续,使用起来会很慢,很烦人。这是杰克。

  • **我们如何避免?**不要试图在 UI 线程上做太多。不要:

    • 从大文件中读取,或者向文件中写入大量信息。

    • 连接到网络并从中读取(或写入)。

    • 计算一个复杂的例程在后台线程中做这些事情。

  • 什么是 UI 线程?最初的线程负责在应用中创建(和修改)视图元素。一些开发人员将 UI 线程称为“主线程”

  • 什么是后台线程?任何不是 UI 线程的线程。你通常为你的应用创建一个后台线程。

  • 创建后台线程的方法有哪些?Java 线程、处理程序、AsyncTask 和 Anko 的 doAsync

在下一章中:

  • 我们将了解开发人员日常面临的各种错误。

  • 我们也会得到一些如何避免它们的提示。

  • 如果我们深陷在错误中,我们会知道该怎么做。