精通安卓应用开发-三-

94 阅读39分钟

精通安卓应用开发(三)

原文:zh.annas-archive.org/md5/23E2C896EDA56175BA900FB6F2E21CF8

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:推送通知和分析

我们将从讨论推送通知开始本章。你将学习如何使用 Google Cloud Messaging 实现带通知的自定义解决方案,包括服务器端和应用程序端。然后,我们将在示例中添加 Parse 的通知。最后,我们将使用NotificationCompat显示自定义通知来结束通知部分。

在本章的后半部分,我们将讨论分析。拥有分析工具来追踪用户在我们应用中的行为对于了解用户的行为模式至关重要,这使我们能够识别模式并改善用户体验。我们将使用 Parse 实现一个示例,并概述市场上最受欢迎的解决方案。

  • 推送通知

    • 使用 GCM 发送和接收

    • 来自 Parse 的通知

    • NotificationCompat

  • 分析

    • 使用 Parse 的分析
  • 错误报告

推送通知

推送通知对于吸引用户并提供实时更新非常重要。它们有助于提醒用户有待处理的事项。例如,在万事达卡创建的**Qkr!**应用中,用户可以在一些餐厅点餐点饮料,如果用户在一段时间后仍未付款,系统会发送通知提醒用户在离开餐厅前需要付款。

当我们需要告诉用户我们有新内容或者其他用户给他们发送了消息时,它们也非常有效。任何在服务器端发生的变化并且需要通知用户的情况都是使用通知的完美场景。

通知也可以从我们自己的应用程序本地发送;例如,我们可以设置一个闹钟并显示通知。它们不一定非要从服务器发送。

它们显示在屏幕顶部的状态栏中,这个地方被称为通知区域。

推送通知

通知所需的最少信息包括一个图标、一个标题和详细文本。随着材料设计的到来,我们可以以不同方式自定义通知;例如,我们可以为它们添加不同的操作:

推送通知

如果我们从屏幕顶部向下滚动,将会显示通知抽屉,我们可以在其中看到所有通知显示的信息:

推送通知

通知不应作为双向通道通信的一部分。如果我们的应用需要与服务器持续通信,如即时通讯应用的情况,我们应该考虑使用套接字、XMPP 或任何其他消息传递协议。理论上,通知是不可靠的,我们无法控制它们何时会被确切接收。

然而,不要滥用通知;这是用户卸载你应用的一个很好的理由。尽量将通知数量降到最低,只在必要时使用。

你可以为通知分配一个优先级,从 Android Lollipop 开始,你可以根据这个优先级过滤你想接收的通知。

这些是处理通知时你应该记住的关键点和概念。在深入了解更多理论知识之前,我们将练习向我们的应用发送通知。

使用 GCM 发送和接收通知

市场上有很多不同的解决方案用于发送推送通知;其中一个是 Parse,它有一个友好的控制面板,任何人都可以轻松地发送推送通知。我们将以 Parse 为例,但首先,了解其内部工作原理以及如何构建我们自己的通知发送系统是有好处的。

GCMGoogle Cloud Messaging)使用推送通知,我们将这些通知发送到我们的手机。谷歌有一些称为 GCM 连接服务器的服务器来处理这个过程。如果我们想发送推送通知,首先需要告诉这些服务器,然后它们会在稍后发送到我们的设备。我们需要创建一个服务器或使用第三方服务器,通过 HTTP 或 XMPP 与 GCM 服务器通信,因为可以使用这两种协议进行通信。

使用 GCM 发送和接收通知

如我们之前所述,由于我们无法控制 GCM 服务器,因此不能精确控制消息的接收时间。GCM 服务器会将消息排队并在设备在线时发送。

要创建我们自己的解决方案,首先需要在 Google 开发者网站上的我们的应用中启用消息传递服务:developers.google.com/mobile/add?platform=android

使用 GCM 发送和接收通知

创建应用后,启用 GCM 消息传递,系统会提供发送者 ID 和服务器 API 密钥。发送者 ID 之前被称为项目编号。

如果我们想接收 GCM 消息,我们需要将我们的客户端(即我们的移动应用)注册到这个项目。为此,我们的应用将使用 GCM API 进行注册并获得一个令牌作为确认。完成此操作后,GCM 服务器将知道你的设备已准备好接收来自这个特定项目/发送者的推送通知。

使用 GCM 发送和接收通知

我们需要添加游戏服务以使用此 API:

 compile "com.google.android.gms:play-services:7.5.+"

通过实例 ID API 进行注册,调用instanceID.getToken方法,并将SenderID作为参数:

InstanceID instanceID = InstanceID.getInstance(this);
String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId),
GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);

我们需要异步调用此方法,并在我们的应用中保持一个布尔变量,以记住我们是否已成功注册。我们的令牌可能会随时间变化,当变化发生时,我们会通过onRefreshToken()回调得知。令牌需要发送到我们的服务器:

@Override
public void onTokenRefresh() {
  //Get new token from Instance ID with the code above
  //Send new token to our Server
}

完成这些后,我们需要创建一个GCMListener并在 Android 清单中添加一些权限:

<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission android:name="com.example.gcm.permission.C2D_MESSAGE"
  android:protectionLevel="signature" />
<uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />

<application ...>
  <receiver
    android:name="com.google.android.gms.gcm.GcmReceiver"
    android:exported="true"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
      <category android:name="com.example.gcm" />
    </intent-filter>
  </receiver>
  <service
    android:name="com.example.MyGcmListenerService"
    android:exported="false" >
    <intent-filter>
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
    </intent-filter>
  </service>
  <service
    android:name="com.example.MyInstanceIDListenerService"
    android:exported="false">
    <intent-filter>
      <action android:name="com.google.android.gms.iid.InstanceID"/>
    </intent-filter>
  </service>
</application>

</manifest>

GCMListener将包含onMessageReceived方法,当我们接收到任何消息时会被调用。

这就是我们需要的客户端方面的全部内容;至于服务器端,由于它完全取决于选择的技术和语言,本书将不详细介绍。在网络上可以轻松找到用于发送通知的 Python、Grails、Java 等不同的代码片段和脚本。

实际上,我们并不需要一个服务器来发送通知,因为我们可以直接与 GCM 进行通信。我们需要做的就是向 gcm-http.googleapis.com/gcm/send 发送一个 POST 请求。这可以通过任何在线 POST 发送服务轻松完成,例如 hurl.it 或 Postman(一个用于发送网络请求的 Google Chrome 扩展程序)。我们的请求需要如下所示:

Content-Type:application/json
Authorization:key="SERVER_API_LEY"
{
  "to" : "RECEIVER_TOKEN"
  "data" : {
    "text":"Testing GCM"
  },
}

使用 GCM 发送和接收通知

继续使用 MasteringAndroidApp,我们将实现 Parse 的推送通知功能。

使用 Parse 的推送通知

对于我们的示例,我们将坚持使用 Parse。主要原因是,我们不需要担心服务器端,而且使用这个解决方案不需要在 Google 开发者控制台创建应用程序。另一个原因是它有一个很好的内置控制面板来发送通知,如果我们提前跟踪了具有不同参数的不同用户,我们还可以针对这些用户。

使用 Parse 的推送通知

使用 Parse,我们不需要创建一个 GCM 监听器。相反,它使用 Parse 库中已经包含的服务,我们只需为此服务注册一个订阅者。我们需要做的就是向我们的应用程序添加权限和接收器,然后就可以开始了:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:protectionLevel="signature" android:name="com.packtub.masteringandroidapp.permission.C2D_MESSAGE" />
<uses-permission android:name="com.packtpub.masteringandroidapp.permission.C2D_MESSAGE" />

确保最后两个权限与您的包名相匹配。接收器需要放在 application 标签内:

<service android:name="com.parse.PushService" />
<receiver android:name="com.parse.ParseBroadcastReceiver">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <action android:name="android.intent.action.USER_PRESENT" />
  </intent-filter>
</receiver>

<receiver android:name="com.parse.ParsePushBroadcastReceiver"
  android:exported="false">
  <intent-filter>
    <action android:name="com.parse.push.intent.RECEIVE" />
    <action android:name="com.parse.push.intent.DELETE" />
    <action android:name="com.parse.push.intent.OPEN" />
  </intent-filter>
</receiver>

<receiver android:name="com.parse.GcmBroadcastReceiver"
  android:permission="com.google.android.c2dm.permission.SEND">
  <intent-filter>
    <action android:name="com.google.android.c2dm.intent.RECEIVE" />
    <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
    <category android:name="com.packtpub.masteringandroidapp" />
  </intent-filter>
</receiver>

</application>

为了监听通知,我们可以在 Application 类的 OnCreate 方法中注册一个订阅者:

ParsePush.subscribeInBackground("", new SaveCallback() {
  @Override
  public void done(com.parse.ParseException e) {
    if (e == null) {
      Log.d("com.parse.push", "successfully subscribed to the broadcast channel.");
    } else {
      Log.e("com.parse.push", "failed to subscribe for push", e);
      }
  }
});

现在,一切就绪。只需进入 Parse 网站,选择 推送 选项卡,然后点击 + 发送推送。您可以指定受众,选择立即发送还是延迟发送以及其他参数。它会跟踪已发送的推送,并指出发送给了哪些人。

使用 Parse 的推送通知

如果您在 已发送推送 列中看到 1,然后查看设备中的通知,那么一切正常。您设备中的通知应如下所示:

使用 Parse 的推送通知

使用 NotificationCompat

目前,我们可以看到由 Parse 接收器创建的默认通知。但是,我们可以设置自己的接收器,并使用 NotificationCompat 创建更美观的通知。这个组件在支持库 v4 中引入,可以显示 Android L 和 M 以及之前版本直到 API 4 的最新功能的通知。

简而言之,我们需要做的是利用NotificationCompat.Builder创建一个通知,并通过NotificationManager.notify()将这个通知传递给系统:

public class MyNotificationReceiver  extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    Notification notification = new NotificationCompat.Builder(context)
    .setContentTitle("Title")
    .setContentText("Text")
    .setSmallIcon(R.drawable.ic_launcher)
    .build();
    NotificationManagerCompat.from(context).notify(1231,notification);
  }

}

这将显示我们的通知。标题、文本和图标是必须的;如果我们不添加这三个属性,通知就不会显示。要开始使用我们的自定义接收器,需要在清单文件中指定我们想要使用的注册,而不是 Parse 推送接收器:

receiver android:name="com.packtpub.masteringandroidapp.MyNotificationReceiver" android:exported="false">
  <intent-filter>
    <action android:name="com.parse.push.intent.RECEIVE" />
    <action android:name="com.parse.push.intent.DELETE" />
    <action android:name="com.parse.push.intent.OPEN" />
  </intent-filter>
</receiver>

我们讨论了如何使用NotificationCompat显示自定义通知。通知有自己的设计指南,它们是材料设计的重要组成部分。建议查看这些指南,并在在应用中使用此组件时牢记它们。

注意

你可以在以下链接找到指导方针:developer.android.com/design/patterns/notifications.html

数据分析的重要性

了解用户如何使用你的应用非常重要。分析帮助我们理解哪些屏幕访问最多,用户在我们的应用中购买哪些产品,以及为什么某些用户在注册过程中退出,同时获取有关性别、位置、年龄等信息。

我们甚至可以追踪应用中用户遇到的崩溃问题,以及设备型号、Android 版本、崩溃日志等信息。

这些数据帮助我们改善用户体验,例如,如果我们发现用户的行为并不像我们预期的那样。它有助于定义我们的产品;如果我们的应用中有不同的功能,我们可以确定哪个功能使用最多。它帮助我们了解受众,这对市场营销是有益的。通过崩溃报告,更容易保持应用无错误和崩溃。

我们将以 Parse 为例,开始追踪一些事件。

使用 Parse 进行数据分析

在不添加任何额外代码的情况下,仅通过我们已经在使用的Parse.init()方法,在 Parse 控制台的分析标签下就能看到一些统计数据。

使用 Parse 进行数据分析

受众部分,我们可以看到每日、每周和每月的活跃安装量和活跃用户数。这有助于我们了解有多少用户以及他们中有多少是活跃的。如果我们想知道有多少用户卸载了应用,可以查看留存部分。

我们将追踪一些事件和崩溃,以在这两个部分显示信息,但首先,我们将看看资源管理器。如果你点击左侧的资源管理器按钮,你应该看到以下选项:

使用 Parse 进行数据分析

这将显示一个表格,我们可以从中看到过滤我们应用数据的各种选项。一旦开始追踪事件和动作,这里将会有更多的列,我们将能够创建复杂的查询。

默认情况下,如果我们点击运行查询,我们会看到以下表格图像:

使用 Parse 的分析

它显示了在默认列下可用的所有信息;目前不需要额外的列。我们可以看到所有不同的请求类型以及操作系统、操作系统版本和我们应用程序的版本。

我们可以使用过滤器来生成不同的输出。一些有趣的输出可能是,例如,按应用程序版本排序和分组,以便了解有多少人在使用每个版本。

如果我们使用相同的 Parse 数据库用于不同的平台,比如安卓和 iOS,我们可以按平台进行过滤。

这是一个按操作系统版本过滤的示例,我们可以看到我们的用户当前正在使用的所有安卓版本:

使用 Parse 的分析

为了收集更多关于应用程序何时以及被打开频率的数据,我们可以在欢迎屏幕或第一个活动的onCreate方法中添加以下行。

ParseAnalytics.trackAppOpenedInBackground(getIntent());

这是一个我们可以跟踪的事件示例,但通常说到事件跟踪,我们指的是自定义事件。例如,如果我们想跟踪哪个职位报价访问量最大,我们将在JobOfferDetailActivity中跟踪带有访问文章标题的事件。我们还可以在点击行打开报价的onlick监听器中跟踪此事件。这方面没有固定的规则;实现可能有所不同。但是,我们需要知道目标是当报价被查看时跟踪事件。

OfferDetailActivityOnCreate方法中选择跟踪事件的选项的代码将与以下代码类似:

public class OfferDetailActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_offer_detail);

    String job_title = getIntent().getStringExtra("job_title");

    Map<String, String> eventParams = new HashMap<>();
    eventParams.put("job_title", job_title);
    ParseAnalytics.trackEventInBackground("job_visited", eventParams);

trackEventInBackground方法启动一个后台线程来为我们创建网络上传请求。参数作为具有最多八个的Map字符串发送。

如果我们在不同时间访问不同的报价,并进入分析浏览器部分,我们可以轻松创建一个查询来查看每个职位报价被打开的次数。

使用 Parse 的分析

通过按维度分组数据,这些维度包括我们随事件跟踪发送的不同参数,并使用计数的聚合,我们可以得到每个职位报价被访问的次数。

接下来,我们将看看如何利用这种事件跟踪的优势,将 Parse 作为错误报告工具。

错误报告

当我们的应用程序分发时报告崩溃对于维护一个无错误和崩溃的应用程序至关重要。市场上有成百上千种安卓设备,即使在发布应用程序时最好的质量保证人员或测试员也可能在不同情况下犯错,最终导致应用程序崩溃。

我们需要假设我们的应用程序将会崩溃。我们必须尽可能编写最好的代码,但如果发生崩溃,我们需要有工具来报告并修复它。

Parse 允许我们使用以下代码跟踪错误:

Map<String, String> dimensions = new HashMap<String, String>(); dimensions.put('code', Integer.toString(error.getCode())); ParseAnalytics.trackEvent('error', dimensions);

然而,这种解决方案仅能让我们追踪到受控代码块的错误。例如,假设我们有一个网络请求,它返回了一个错误。这种情形可以轻松处理;我们只需追踪来自服务器的错误响应事件。

当我们的应用程序中出现NullPointerException时,就会出现问题,这是因为我们遇到了一个由于无法在代码中检测到的意外情况而导致的崩溃。例如,如果一个职位的图片链接为 null,而我尝试读取链接而不检查属性是否为 null,我将得到NullPointerException,应用程序将崩溃。

如果我们不能控制发生错误的代码部分,我们该如何追踪呢?幸运的是,市场上我们有工具可以帮我们做到这一点。HockeyApp 是一个帮助分发测试版本并收集实时崩溃报告的工具。这是一个在网页面板上显示我们应用程序错误报告的工具。它真的很容易集成;我们只需要在库中添加以下内容:

compile 'net.hockeyapp.android:HockeySDK:3.5.0-b.4'

然后,我们需要调用以下方法来报告错误:

CrashManager.register(this, APP_ID);

APP_ID将在你将 APK 上传到 Hockey 或在你手动在 Hockey 网站上创建新应用程序时找到。

错误报告

一旦我们知道App_ID并注册了崩溃报告,如果我们遇到崩溃,我们将看到一个带有发生次数列表的界面,如下面的截图所示:

错误报告

我们将以分析的话题结束,Parse 只是众多选择之一;同样常见的还有使用 Google Analytics,它包含在 Google Play 服务库中。Google Analytics 允许我们创建更复杂的报告,例如漏斗追踪,以查看在漫长的注册过程中我们失去了多少用户,我们还可以在不同的图表和直方图中查看数据。

如果你属于一个大机构,可以看看 Adobe Omniture。它是一个企业工具,能帮助你将不同的事件作为变量进行追踪,并创建公式来展示这些变量。它还允许你将移动分析数据与其他部门(如销售、市场营销和客户服务)的数据结合起来。根据我的个人经验,我见过的使用 Omniture 的公司通常会有专人全职负责分析研究。在这种情况下,开发者需要知道的只是如何实现 SDK 和追踪事件;创建复杂报告不是开发者的任务。

总结

在本章中,你学习了如何为我们的应用程序添加通知。我们使用 Parse 实现了推送通知,并讨论了如何使用 Google Cloud Messaging 创建我们自己的自定义通知服务,包括客户端所需的所有代码和测试服务器端的工具。在章节的后半部分,我们介绍了分析,解释了它们的重要性,并用 Parse 跟踪事件。在分析领域,一个重要的方面是错误报告。我们还使用 Parse 和 HockeyApp 跟踪了应用程序中的错误。最后,我们概览了其他分析解决方案,例如 Google Analytics 和 Adobe Omniture。

在下一章中,我们将使用位置服务,并学习如何将MapView添加到我们的示例中,显示带有位置标记的谷歌地图。

第十章:位置服务

在本章中,我们将学习如何使用 Google 的地图片段向我们的应用程序添加地图视图。我们将在地图上添加标记,用于指出感兴趣的位置。

为了做到这一点,我们还将讨论如何在 Google 开发者控制台创建项目,并设置我们的应用程序以使用 Google Play Services SDK,这是在任何 Android 应用程序中使用 Google 服务的必要条件。

Parse 中的每个工作机会都有一个位置字段;基于此,我们将在地图上显示标记。

  • 配置项目

    • 获取 Google Maps API 密钥

    • 配置AndroidManifest.xml

  • 添加地图

    • 为 ViewPager 创建片段

    • 实现地图片段

  • 添加标记

    • 从 Parse 检索数据

    • 为每个位置显示一个标记

  • 添加标题

配置项目

为了使我们能够使用 Google Play Service API,我们需要使用 Google Play Services SDK 配置我们的项目。如果你还没有安装,请进入 Android SDK 管理器并获取 Google Play Service SDK。

既然我们的应用使用了 Google Play 服务,为了测试应用,你必须确保在以下设备之一上运行应用:

  1. 带有 Google Play 商店的 Android 2.3 或更高版本的 Android 设备(推荐)。

  2. 已设置 Google Play 服务的模拟器。如果你使用 Genymotion,Google Play 服务默认不会安装:配置项目

我们需要让 Google Play 服务 API 对应用可用。

打开应用的build.gradle文件,并在依赖项下添加play-services库。添加到build.gradle文件的行应该类似于这样:

compile 'com.google.android.gms:play-services:7.8.0'

确保你将其更改为最新版本的play-services,并在发布新版本时更新。

保存文件并点击与 Gradle 文件同步项目

获取 API 密钥

为了使用 Google Maps API,我们需要在 Google 开发者控制台注册我们的项目,并获得一个 API 密钥,然后将其添加到我们的应用中。

首先,我们需要获取我们唯一应用程序的 SHA-1 指纹。我们可以从调试证书发布证书中获得。

  • 调试 证书在完成调试构建时会自动创建。此证书仅应用于目前正在测试的应用。不要使用调试证书发布应用程序。

  • 发布 证书是在完成发布构建时生成的。也可以使用keytool程序创建证书。当应用准备发布到 Play 商店时,必须使用此证书。

显示调试证书指纹

  • 找到名为debug.keystore的调试密钥库文件。这个文件通常位于 Android 虚拟设备文件所在的目录中:

    • OS X 和 Linux~/.android/

    • Windows Vista 和 Windows 7C:\Users\your_user_name\.android\

  • 要显示 SHA-1 指纹,请打开终端或命令提示符窗口,并输入以下内容:

    • OS X 和 Linux:我们使用命令 keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

    • Windows Vista 和 Windows 7:我们使用命令 keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

输入命令并按下回车键后,您将看到类似这样的输出:

Alias name: androiddebugkey
Creation date: Dec 16, 2014
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 32f30c87
Valid from: Tue Dec 16 11:35:40 CAT 2014 until: Thu Dec 08 11:35:40 CAT 2044
Certificate fingerprints:
         MD5:  7E:06:3D:45:D7:1D:48:FE:96:88:18:20:0F:09:B8:2A
         SHA1: BD:24:B2:7C:DA:67:E5:80:78:1D:75:8C:C6:66:B3:D0:63:3E:EE:84
         SHA256: E4:8C:BD:4A:24:CD:55:5C:0E:7A:1E:B7:FC:A3:9E:60:28:FB:F7:20:C6:C0:E9:1A:C8:13:29:A6:F2:10:42:DB
         Signature algorithm name: SHA256withRSA
         Version: 3

创建 Google 开发者控制台项目

访问console.developers.google.com/project,如果您还没有账户,请创建一个账户。首先,使用您想要的名字创建一个新项目。项目创建后,执行以下步骤:

  1. 在左侧边栏中,点击APIs & auth,然后选择APIs选项:创建 Google 开发者控制台项目

  2. 选择Google Maps Android API并启用它。

  3. 打开凭据,然后点击**[创建新密钥]**。

  4. 选择Android 密钥,输入您的SHA-1指纹,然后是您的项目包名称,两者之间用分号分隔,如下所示:

    BD:24:B2:7C:DA:67:E5:80:78:1D:75:8C:C6:66:B3:D0:63:3E:EE:84;com.packtpub.masteringandroidapp
    
  5. 完成此操作后,您将能够像以下截图一样查看凭据:创建 Google 开发者控制台项目

配置 AndroidManifest.xml

既然我们已经获得了 Android 应用的 API 密钥,我们需要将其添加到AndroidManifest.xml文件中。

打开您的AndroidManifest.xml文件,并在<application>元素下添加以下代码作为子元素:

<meta-data
  android:name="com.google.android.geo.API_KEY"
  android:value="API_KEY"/>

value属性中的API_KEY替换为 Google 开发者控制台给出的 API 密钥。

我们还需要在AndroidManifest中添加其他几个设置。如下设置 Google Play 服务的版本:

<meta-data
  android:name="com.google.android.gms.version"
  android:value="@integer/google_play_services_version" />

如下设置必要的权限:

  • INTERNET:此权限用于从 Google Maps 服务器下载地图数据。

  • ACCESS_NETWORK_STATE:这将允许 API 检查连接状态,以确定是否能够下载数据。

  • WRITE_EXTERNAL_STORAGE:这将允许 API 缓存地图数据。

  • ACCESS_COARSE_LOCATION:这允许 API 使用 Wi-Fi 或移动数据获取设备的位置。

  • ACCESS_FINE_LOCATION:这将比ACCESS_COARSE_LOCATION提供更精确的位置,并且还将使用 GPS 以及 Wi-Fi 或移动数据。看看以下代码:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    

您还需要设置 OpenGL ES。地图 API 使用 OpenGL ES 来渲染地图,因此需要安装它才能显示地图。为了通知其他服务这一需求,并防止不支持 OpenGL 的设备在 Google Play 商店显示您的应用,请在您的AndroidManifest.xml文件中的<manifest>标签下添加以下内容:

<uses-feature
  android:glEsVersion="0x00020000"
  android:required="true"/>

您当前的AndroidManifest.xml文件应类似于以下代码:

<?xml version="1.0" encoding="UTF-8"?>
<manifest  package="com.packtpub.masteringandroidapp">
  <uses-feature android:glEsVersion="0x00020000" android:required="true" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <application android:name=".MAApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme">
    <activity android:name=".SplashActivity" android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:name=".MainActivity" android:label="@string/title_activity_main" />
    <activity android:name=".OfferDetailActivity" android:label="@string/title_activity_offer_detail" />
    <provider android:name=".MAAProvider" android:authorities="com.packtpub.masteringandroidapp.MAAProvider" />
    <meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSyC9o7cLdk_MIX_aQhaOLvoqYywK61bN0PQ" />
    <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
  </application>
</manifest>

添加地图

既然我们的应用程序已经配置好了可以使用地图服务,那么我们可以开始讨论如何在应用程序中添加一个可视化的地图。对于地图,我们将创建另一个 Fragment,它将被加载到ViewPager的第二个页面。

有两种方法可以显示谷歌地图;一个MapFragmentMapView对象可以表示它。

添加 Fragment

在我们的fragments目录中创建一个名为MyMapFragment的新 Java 类。这个类应该继承Fragment类型。然后,重写OnCreateView方法,并让它返回fragment_my_map的填充视图:

package com.packtpub.masteringandroidapp.fragments;

import/**
* Created by Unathi on 7/29/2015.
*/
public class MyMapFragment extends Fragment {

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_my_map, container, false);

    return view;
  }
}

接下来,为 Fragment 创建布局文件,并将其命名为fragment_my_map。将布局的根元素设置为FrameLayout。我们将在布局中暂时添加TextView以验证其是否有效。fragment_my_map.xml文件的代码应类似于以下这样:

<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout  android:layout_width="match_parent" android:layout_height="match_parent">
  <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="This is a TextView" android:layout_gravity="center" android:textSize="25dp" />
</FrameLayout>

将我们的 Fragment 添加到应用程序的最后一步将是编辑MyPagerAdapter.java文件,将其作为第二个页面显示。为此,将getItem方法中的第二个 case 更改为返回MyMapFragment的实例,并将getPageTitle方法中第二个 case 的页面标题更改为返回MAP

@Override
public Fragment getItem(int i) {
  switch (i) {
    case 0 :
    return new ListFragment();
    case 1 :
    return new MyMapFragment();
    case 2 :
    return new SettingsFragment();
    default:
    return null;
  }
}

@Override
public CharSequence getPageTitle(int position) {
  switch (position) {
    case 0 :
    return "LIST";
    case 1 :
    return "MAP";
    case 2 :
    return "SETTINGS";
    default:
    return null;
  }
}

现在,当你运行应用程序时,ViewPager的第二个页面应该被我们新的 Fragment 替换。

添加 Fragment

实现 MapFragment

我们现在将使用MapFragment在我们的应用程序上显示地图。我们可以通过添加一个带有android:namecom.google.android.gms.maps.MapFragment<fragment>布局来实现这一点。这样做将自动将MapFragment添加到activity中:

以下是fragment_my_map.xml的代码:

<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout  android:layout_width="match_parent" android:layout_height="match_parent">
  <fragment android:name="com.google.android.gms.maps.MapFragment" android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

接下来,为了能够处理我们添加到布局中的MapFragment,我们需要使用FragmentManager,我们从getChildFragmentManager获取它,通过findFragmentById来找到。这将在OnCreateView方法中完成:

FragmentManager fm = getChildFragmentManager();
mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
if (mapFragment == null) {
  mapFragment = SupportMapFragment.newInstance();
  fm.beginTransaction().add(R.id.map, mapFragment).commit();
}

我们将把我们的 Fragment 分配给SupportMapFragment,而不是仅仅MapFragment,这样应用程序就可以支持低于12的 Android API 级别。使用以下代码:

以下是MyMapFragment.java的代码:

public class MyMapFragment extends Fragment{

  private SupportMapFragment mapFragment;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_my_map, container, false);

    FragmentManager fm = getChildFragmentManager();
    mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
    if (mapFragment == null) {
      mapFragment = SupportMapFragment.newInstance();
      fm.beginTransaction().add(R.id.map, mapFragment).commit();
    }

    return view;
  }

}

现在,当我们运行应用程序时,地图将在屏幕上显示。

实现 MapFragment

添加标记

现在谷歌地图已经可见,但它还没有为用户显示任何有用的数据。为了实现这一点,我们将添加地图标记来指示用户感兴趣的点。这些将是不同职位提供的位置,我们将从 Parse 数据库下载这些位置。

我们还将学习如何将用于在地图上标记点的图标更改为自定义图像,并在标记上加上标题。这将使我们的应用程序看起来更有趣、更具有信息性。

从 Parse 检索数据

在我们能够显示所有标记之前,我们需要从 Parse 下载所有必要的数据。

MyMapFragment.java 中,我们将使用 ParseQuery 获取位置列表,并使用它来获取每个职位在显示之前的相关信息。执行以下步骤:

  • 创建一个名为 googleMap 的私有成员变量,其类型为 GoogleMap,并重写 onResume() 方法。

  • onResume() 中,检查 googleMap 是否为空;如果是,这意味着我们还没有向当前地图实例添加标记。如果 googleMap 为空,从我们已经创建的 MapFragment 分配地图。这是使用 getMap() 实现的:

    if (googleMap == null) {
    
      googleMap = MapFragment.getMap();
    
    }
    
  • 创建一个 ParseQuery,它将检索我们 Parse 数据库中 JobOffer 表的所有数据。使用 findInBackground() 函数和 FindCallback,这样我们就可以在数据下载完成后开始处理数据。使用以下代码:

    ParseQuery<JobOffer> query = ParseQuery.getQuery("JobOffer");
    query.findInBackground(new FindCallback<JobOffer>() {
      @Override
      public void done(List<JobOffer> list, ParseException e) {
    
      }
    });
    

为每个位置显示标记

现在,我们将遍历从 Parse 收到的职位列表,并使用 addMarker()googleMap 添加标记。执行以下步骤:

  1. findInBackground 执行完毕后,创建一个 ParseGeoPoint 变量和一个循环,该循环将遍历列表中的每个项目。我们将使用 ParseGeoPoint 变量来存储来自 Parse 数据库的坐标:

    ParseGeoPoint geoPoint = new ParseGeoPoint();
    
    for(int i =0;i<list.size();i++){
    
    }
    
  2. 在循环内,从列表中获取 GeoPoint 数据并将其保存到我们的 ParseGeoPoint 变量中:

     geoPoint = list.get(i).getParseGeoPoint("coordinates");
    
  3. 最后,在每次迭代中使用 addMarker()googleMap 添加标记:

    googleMap.addMarker(new MarkerOptions()
    .position(new LatLng(geoPoint.getLatitude(), geoPoint.getLongitude())));
    

你的 MyMapFragment.java 文件应类似于以下内容:

public class MyMapFragment extends Fragment{

  private SupportMapFragment mapFragment;
  private GoogleMap googleMap;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_my_map, container, false);

    FragmentManager fm = getChildFragmentManager();
    mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
    if (mapFragment == null) {
      mapFragment = SupportMapFragment.newInstance();
      fm.beginTransaction().add(R.id.map, mapFragment).commit();
    }

    return view;
  }

  @Override
  public void onResume() {
    super.onResume();

    if (googleMap == null) {
      googleMap = mapFragment.getMap();

      ParseQuery<JobOffer> query = ParseQuery.getQuery("JobOffer");
      query.findInBackground(new FindCallback<JobOffer>() {
        @Override
        public void done(List<JobOffer> list, ParseException e) {

          ParseGeoPoint geoPoint;

          for(int i =0;i<list.size();i++){
            geoPoint = list.get(i).getParseGeoPoint("coordinates");

            googleMap.addMarker(new MarkerOptions()
            .position(new LatLng(geoPoint.getLatitude(), geoPoint.getLongitude())));
          }

        }
      });

    }
  }
}

这些标记现在应该在应用中可见:

为每个位置显示标记

向标记添加标题

地图上标记的实用性不仅仅在于显示一个点,而且还在于为用户提供一种简单且易于访问的方式来获取此位置的信息。我们将通过在点击标记时显示标题来实现这一点。

这可以通过简单地在我们的 addMarker() 方法中添加 .title(string) 来实现:

googleMap.addMarker(new MarkerOptions()
.position(new LatLng(geoPoint.getLatitude(), geoPoint.getLongitude()))
.title(list.get(i).getTitle()));

现在我们有一个完全功能的地图显示,当用户点击标记时,它会在标记上方显示一个标题,如下面的图片所示:

向标记添加标题

总结

在本章中,你学习了如何向我们的应用添加地图。这需要我们在 Google Developer Console 上创建一个项目,并配置我们的应用以访问必要的 API。一旦我们的应用完全配置好,我们就继续将地图添加到我们选择的视图中。我们讨论了在另一个片段(MyMapFragment 中的 MapFragment)中处理片段。尽管单个 MapFragment 可以通过代码单独添加,但将其放在带有布局的另一个片段中,如果我们需要,可以让我们有可能向页面添加其他 UI 控件,例如 FloatingActionButton。最后,我们通过显示从 Parse 下载的位置的标记和信息,使地图变得有用。

在下一章中,你将学习如何调试和测试我们的应用程序。

第十一章:安卓上的调试与测试

在本章中,你将学习如何在 Android 中进行调试,这是一个在开发应用程序时查找和解决问题的基本实践,可以节省时间。

我们将学习如何创建自动化测试,可以测试按钮的点击或单个方法的结果。这是一组你可以在 Android Studio 中运行的测试,以确保每次开发新功能时,不会破坏现有的功能。

你还将学习如何使用Robolectric进行单元测试和 Espresso 进行集成测试。

在本章的最后,我们将讨论如何使用 Monkey 进行数百万次随机点击测试 UI,如何通过应用录制点击序列,以及如何使用 MonkeyTalk 配置基于这些记录的测试。

  • 日志和调试模式

  • 测试

    • 使用 Robolectric 进行单元测试

    • 使用 Espresso 进行集成测试

  • UI 测试

    • 使用 MonkeyRunner 进行随机点击

    • 使用 MonkeyTalk 记录点击

  • 持续集成

日志和调试模式

如果不提及日志以及如何通过调试解决问题,我们无法完成这本书。如果你知道如何解决自己的问题,那么在 Android 上开发不仅仅是复制粘贴 Stack Overflow 的内容。

调试模式和日志是帮助开发者定位问题的机制。随着时间的推移,每个开发者都会进步并减少使用这些技术的频率,但一开始,应用中充满了日志是很常见的。我们不希望用户在应用发布后能够看到日志,也不希望手动移除日志并在发布新版本时再次添加。我们将看看如何避免这种情况。

处理日志

Log 类用于打印实时可在LogCat中读取的消息和错误。以下是记录消息的示例:

Log.i("MyActivity", "Example of an info log");

Log类有五种方法,它们用于在日志上设置不同的优先级。这允许我们在LogCat中按优先级进行过滤。在某些情况下,我们会显示不同的日志,例如,查看每次请求下载的工作机会数量。如果我们的应用崩溃了,此时错误类型的日志是我们的优先事项,我们希望隐藏其他优先级较低的日志,以便尽快找到错误。

五个优先级分别是(从低到高):详细、调试、信息、警告和错误。(Log.v , Log.d, Log.i, Log.w, 和 Log.e)

我们可以通过日志窗口顶部的栏按进程进行过滤。我们可以按优先级和关键词进行过滤,并且可以创建自定义过滤器,按标签、进程 ID 等进行过滤。

处理日志

如果日志不显示或者它们是旧的且不刷新,尝试打开右边的下拉菜单,选择 过滤,然后再次选择仅显示选定应用程序。这会强制控制台刷新。

处理日志

为了完成日志,我们将创建一个包装器,并使用第三方库,以便只需更改布尔值就能在项目中禁用所有日志。为此,我们只需创建一个具有与Log类相同方法的类,这些方法依赖于这个布尔值:

public class MyLogger {

  static final boolean LOG = false;

  public static void i(String tag, String string) {
    if (LOG) android.util.Log.i(tag, string);
  }

  public static void e(String tag, String string) {
    if (LOG) android.util.Log.e(tag, string);
  }
  …

我们每次想要编写日志时都需要使用这个包装器——使用MyLogger.d()而不是Log.d()。这样,如果我们更改MyLogger类中的布尔值LOG,它将同时停止我们项目中的所有日志。

建议使用BuildConfing.DEBUG变量的值:

static final boolean LOG = BuildConfing.DEBUG; 

如果我们的应用处于调试模式,这将成立,发布应用时将变为假。因此,我们不需要记住在发布模式下关闭日志,也没有日志对最终用户显示的风险。

使用 Timber,日志包装器

Timber 是由 Jake Wharton 创建的日志包装器,它将日志提升到了一个高级水平,允许我们使用日志树概念来拥有不同的日志行为。看看以下代码:

compile 'com.jakewharton.timber:timber:3.1.0'

使用 Timber 的优点之一是,我们不需要在同一个活动中多次编写日志标签:

Timber.tag("LifeCycles");
Timber.d("Activity Created");

我们的树可以有不同的行为;例如,我可能想在发布模式下禁用日志,但我仍然想处理错误;所以,我会种植一个错误树,将错误报告给 Parse:

if (BuildConfig.DEBUG) {
  Timber.plant(new Timber.DebugTree());
} else {
  Timber.plant(new CrashReportingTree());
}

/** A tree which logs important information for crash reporting. */
private static class CrashReportingTree extends Timber.Tree {
  @Override protected void log(int priority, String tag, String message, Throwable t) {
    if (priority == Log.VERBOSE || priority == Log.DEBUG) {
      return;
    }
    //Track error to parse.com
  }
}

调试我们的应用

日志可用于开发过程中查找问题,但如果我们掌握了调试模式,会发现这个做法要快得多。

当我们处于调试模式时,可以在代码中设置断点。通过这些断点,我们指定一个代码行,我们希望执行在此处停止,以显示那一刻的变量值。要设置断点,只需在左侧边栏上双击:

调试我们的应用

我们在获取工作机会的方法的响应中设置了一个调试点。我们可以从顶部栏启动调试模式:

调试我们的应用

如果我们在调试模式下运行应用,当达到这一点时,Android Studio 将暂停执行:

调试我们的应用

Android Studio 会自动提示调试器窗口,我们可以在执行点查看变量。从前面的截图中,我们可以看到工作机会列表,并导航查看每个机会内部的内容。

这里的重要特性是左侧的绿色播放按钮,它将继续执行我们的应用到下一个断点,以及红色方块,它退出调试模式并继续执行应用。

我们还有不同的控件可用于转到下一行,进入一个方法,或离开方法。例如,假设我们在以下命令的第一行有一个断点:

MasteringAndroidDAO.getInstance().clearDB(getActivity());
MasteringAndroidDAO.getInstance().storeOffers(getActivity(), jobOffersList);

在这种情况下,单步跳过(指向下方的蓝色箭头)将把我们的执行移到下一行。如果我们点击单步进入(指向右下角的蓝色箭头),我们将进入getInstace()方法。结合这些控件,我们可以实时控制流程。

解释了调试模式之后,我们现在可以继续讨论自动化测试。

在 Android 上测试

任何新功能在完成之前都需要先进行测试。我们作为开发者,曾多次掉入在未先编写通过测试的情况下提交代码更改的陷阱,结果发现在未来的迭代中预期行为被破坏了。

我们通过艰难的方式了解到,编写测试可以提高我们的生产力,提高代码质量,并帮助我们更频繁地发布。因此,Android 提供了几种工具,以帮助我们从早期阶段开始测试应用程序。

在接下来的两节中,我们将讨论我最喜欢的设置:用 Robolectric 进行单元测试,用 Espresso 进行集成测试。

使用 Robolectric 的单元测试

直到 Robolectric 出现,编写单元测试意味着我们必须在真实设备或模拟器上运行它们。这个过程可能需要几分钟,因为 Android 构建工具必须打包测试代码,将其推送到连接的设备,然后运行它。

Robolectric 通过使我们能够在工作站的 JVM 中运行单元测试,而无需 Android 设备或模拟器,从而缓解了这个问题。

要使用 Gradle 包含 Robolectric,我们可以在build.gradle文件中添加以下依赖:

testCompile "org.robolectric:robolectric:3.0"

我们使用testCompile来指定我们希望此依赖包含在测试项目中。对于测试项目,默认源目录是src/test

Robolectric 配置

在撰写本文时,Robolectric 3.0 版本支持以下 Android SDK:

  • Jelly Bean,SDK 版本 16

  • Jelly Bean MR1,SDK 版本 17

  • Jelly Bean MR2,SDK 版本 18

  • KitKat,SDK 版本 19

  • Lollipop,SDK 版本 21

默认情况下,测试将针对AndroidManifest文件中定义的targetSdkVersion运行。如果你想针对不同的 SDK 版本运行测试,或者你当前的targetSdkVersion不被 Robolectric 支持,你可以手动覆盖它,通过位于src/test/resources/robolectric.properties的属性文件,内容如下:

robolectric.properties
sdk=<SDK_VERSION>

我们的第一单元测试

我们将从设置一个非常简单且常见的场景开始:一个带有登录按钮的欢迎活动,该按钮使用户导航到登录活动。欢迎活动的布局如下:

<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout  android:layout_width="match_parent" android:layout_height="match_parent">
  <Button android:id="@+id/login" android:text="Login" android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout>

WelcomeActivity类中,我们只需将登录按钮设置为启动登录活动:

public class WelcomeActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.welcome_activity);

    View button = findViewById(R.id.login);
    button.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
      }
    });
  }
}

为了测试这一点,我们可以确保通过发送正确的Intent来启动LoginActivity。因为 Robolectric 是一个单元测试框架,LoginActivity实际上不会启动,但我们能够检查框架是否捕获了正确的意图。

首先,我们将在src/test/java/路径中正确的包内创建测试文件WelcomeActivityTest.java。Robolectric 依赖于 JUnit 4,因此我们将从指定 Robolectric 的 Gradle 测试运行器和一些额外的配置开始,框架将使用这些配置来查找AndroidManifest资源和资产。运行以下命令:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)

现在,我们可以编写我们的第一个测试。我们将从创建并使欢迎活动进入前台开始:

WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);

既然我们已经有了WelcomeActivity的实例,点击登录按钮就变得简单了:

activity.findViewById(R.id.login).performClick();

最后,我们必须验证框架捕获了将启动LoginActivity的意图:

Intent expectedIntent = new Intent(activity, LoginActivity.class);
assertThat(shadowOf(activity).getNextStartedActivity(), is(equalTo(expectedIntent)));

shadowOf静态方法返回一个ShadowActivity对象,该对象存储了与当前测试活动的大部分交互。我们需要使用@Test注解,这告诉 JUnit 该方法可以作为测试用例运行。将所有内容放在一起,我们得到以下测试类(WelcomeActivityTest.java):

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class WelcomeActivityTest {

  @Test
  public void loginPress_startsLoginActivity() {
    WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);

    activity.findViewById(R.id.login).performClick();

    Intent expectedIntent = new Intent(activity, LoginActivity.class);
    assertThat(shadowOf(activity).getNextStartedActivity(), is(equalTo(expectedIntent)));
  }
}

运行单元测试

在能够运行单元测试之前,我们需要在 Android Studio 中选择正确的测试工件。为此,我们将打开构建变体工具栏,并选择单元测试工件,如下面的截图所示:

运行单元测试

现在,从项目窗口中,我们可以通过右键点击测试类并选择运行选项来运行测试。确保项目路径中没有空格;否则,Robolectric 在执行单元测试之前会抛出异常。

运行单元测试

我们还可以从命令行运行单元测试。为此,使用带有--continue选项的test任务命令调用:

./gradlew test --continue

如果我们已经配置了持续集成系统,例如 Jenkins、Travis 或 wercker,这个选项是理想的。

这就是 Robolectric 部分的结尾。接下来,我们将讨论使用Espresso进行集成测试。

使用 Espresso 进行集成测试

由于 Android 本身的特点以及市场上的大量设备,我们无法确定发布应用时它可能会如何表现。

我们自然会尽可能在许多不同的设备上手动测试我们的应用,这是一个繁琐的过程,每次发布时都必须重复。在本节中,我们将简要讨论 Espresso 以及如何编写将在真实设备上运行的测试。

Espresso 配置

在编写我们的第一个集成测试之前,我们需要安装并配置测试环境。执行以下步骤:

  1. 从 Android SDK 管理器中,我们需要在Extras文件夹中选择并安装Android 支持仓库,如下面的截图所示:Espresso 配置

  2. 创建用于集成测试代码的文件夹;这应该位于app/src/androidTest

  3. 我们还需要在项目的build.gradle中指定一些依赖项。使用以下代码:

    dependencies {
      androidTestCompile 'com.android.support.test:runner:0.3'
      androidTestCompile 'com.android.support.test:rules:0.3'
      androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
      androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2'
    }
    

最近,Android 添加了对 JUnit 4 风格测试案例的支持。要使用它,我们将在build.gradle文件中将AndroidJUnitRunner添加为默认的测试仪器运行器:

android {
  defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  }
}

编写集成测试

对于这个例子,我们将从 Robolectric 停下的地方继续;我们将为LoginActivity编写一个测试。对于这个活动,我们将设置一个简单的布局,包含两个EditTexts和一个登录按钮。运行以下代码(activity_login.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <EditText
    android:id="@+id/input_username"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="textEmailAddress" />

  <EditText
    android:id="@+id/input_password"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="textPassword" />

  <Button
    android:id="@+id/button_signin"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/signin"/>
</LinearLayout>

LoginActivity中,当用户点击登录按钮时,我们将使用以下代码(LoginActivity.java)将凭据发送到闪屏活动:

public class LoginActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_login);

    final EditText inputUsername = (EditText) findViewById(R.id.input_username);
    final EditText inputPassword = (EditText) findViewById(R.id.input_password);

    Button buttonLogin = (Button) findViewById(R.id.button_signin);

    buttonLogin.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        startActivity(new Intent(getApplicationContext(), SplashActivity.class)
        .putExtra("username", inputUsername.getText().toString())
        .putExtra("password", inputPassword.getText().toString()));
        finish();
      }
    });
  }
}

对于这个测试,我们将在两个输入字段中输入用户凭据,并验证我们是否在意图中正确捆绑它们。

首先,我们将在src/test/androidTest/路径中正确的包内创建LoginActivityTest.java测试文件。我们将使用 JUnit 4,因此首先会指定AndroidJUnit4测试运行器。使用以下命令:

@RunWith(AndroidJUnit4.class)

与 Robolectric 的另一区别是,在 Espresso 中,我们需要指定一个规则来准备被测试的活动。为此,使用以下命令:

@Rule
public IntentsTestRule<LoginActivity> mActivityRule =
  new IntentsTestRule<>(LoginActivity.class);

现在,我们可以开始编写测试。首先,我们需要在两个输入字段中输入登录详情:

String expectedUsername = "mastering@android.com";
String expectedPassword = "electric_sheep";

onView(withId(R.id.input_username)).perform(typeText(expectedUsername));
onView(withId(R.id.input_password)).perform(typeText(expectedPassword));

然后,我们将通过点击登录按钮发送意图:

onView(withId(R.id.button_signin)).perform(click());

最后,我们必须验证捕获的意图是否包含登录凭据:

intended(hasExtras(allOf(
  hasEntry(equalTo("username"), equalTo(expectedUsername)),
  hasEntry(equalTo("password"), equalTo(expectedPassword)))));

把所有内容放在一起,我们将拥有以下测试类(LoginActivityTest.java):

@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {

  @Rule
  public IntentsTestRule<LoginActivity> mActivityRule =
  new IntentsTestRule<>(LoginActivity.class);

  @Test
  public void loginButtonPressed_sendsLoginCredentials() {
    String expectedUsername = "mastering@android.com";
    String expectedPassword = "electric_sheep";

    onView(withId(R.id.input_username)).perform(typeText(expectedUsername));
    onView(withId(R.id.input_password)).perform(typeText(expectedPassword));

    onView(withId(R.id.button_signin)).perform(click());

    intended(hasExtras(allOf(
    hasEntry(equalTo("username"), equalTo(expectedUsername)),
    hasEntry(equalTo("password"), equalTo(expectedPassword)))));
  }
}

运行集成测试

与我们使用 Robolectric 所做的类似,要运行集成测试,我们需要在 Android Studio 中切换到正确的测试工件(Test Artifact)。为此,我们将打开构建变种(Build Variants)工具栏并选择Android Instrumentation Tests工件:

运行集成测试

现在,从项目窗口中,我们可以通过右键点击测试类并选择运行选项来运行测试。

我们还可以从命令行运行集成测试。为此,我们将调用connectedCheck(或cC)任务:

./gradlew cC

如果我们有连接设备或模拟器的持续集成系统,使用命令行是首选方式。当我们编写足够的集成测试后,可以轻松地在数百台真实设备上部署和运行它们,使用如Testdroid之类的服务。

运行集成测试

从 UI 角度进行测试

我们现在将进行的测试与使用应用程序的人可能会进行的测试类似。实际上,在拥有**质量保证(QA)**的的公司中,人们将这些工具作为手动测试的补充。

UI 测试也可以自动化,但它们与单元测试和集成测试不同;这些是在屏幕上执行的操作,从点击按钮到使用记录的事件完成注册过程。

我们将从使用The Monkey进行压力测试开始。

启动 The Monkey

The Monkey 是一个可以从命令行通过 ADB 启动的程序。它在我们的设备或模拟器中生成随机事件,并使用一个种子,我们可以重现相同的随机事件。为了说明,让我们考虑一个带数字的例子。假设我执行了 Monkey,它产生了 1 到 10 的随机数字;如果我再次启动它,我会得到不同的数字。当我使用一个种子(这个种子是一个数字)执行 The Monkey 时,我得到了一组不同的 1 到 10 的数字,如果我用相同的种子再次启动它,我会得到相同的数字。这很有用,因为如果我们使用一个种子来生成随机事件并遇到崩溃,我们可以修复这个崩溃,并再次运行相同的种子,以确保我们解决了问题。

这些随机事件可以是从点击和滚动手势到系统级别事件(如音量增大、音量减小、截图等)的变化。我们可以限制事件的数量和类型,以及运行它的程序包。

终端中的基本语法是以下命令:

$ adb shell monkey [options] <event-count>

如果你从未使用过 ADB,你可以在 Android SDK 目录中的platform-tools文件夹里找到它,无论它安装在你的系统中的哪个位置:

../sdk/platform-tools/adb

当我们打开终端并导航到这个目录时,我们可以编写以下代码行:

adb shell monkey -p com.packtpub.masteringandroidapp -v 500

当你尝试使用adb而输出是command not found时,你可以使用adb kill-serveradb start-server和如果是 Linux 或 Mac 系统,使用./adb(点斜杠 adb)来重启adb

我们可以将事件数量增加到5000,或者产生无限事件,但通常建议设置一个数字限制;否则,你可能需要重启设备来停止 The Monkey。当你执行命令时,你将能够看到产生的随机事件,并且它会指示所使用的种子,以便如果你想重复相同的事件链,可以知道种子:

启动 The Monkey

根据应用程序的不同,我们可能需要调整事件之间的时间,使用节流毫秒属性来模拟真实用户。

使用下一个测试工具,我们将进行一种不同类型的 UI 测试,目的是跟随一个流程。例如,如果我们有一个由三个屏幕组成的注册流程,每个屏幕都有不同的表单,我们想要记录一个测试,用户填写表单并通过这三个屏幕逻辑地继续。在这种情况下,The Monkey 实际上并不太有帮助;在大量的事件中,它最终会用随机字符填写所有输入字段,并点击按钮进入下一个屏幕,但这并不是我们真正想要的。

使用 MonkeyTalk 记录 UI 测试

记录一系列测试(如注册过程)的目的是为了保存这些测试,以便在我们对代码进行更改时能够再次运行它。我们可能需要修改注册过程的网络请求,而不改变 UI,所以这些测试非常完美。我们可以在完成修改后直接运行它们,而无需手动完成注册或填写表单。这里我们并不是在偷懒;如果我们有数百个测试,这对于一个人来说将是大量的工作。此外,通过自动化测试,我们可以确保事件序列始终如一。

MonkeyTalk是一个免费且开源的工具,有两个版本;在我们的示例中,我们将使用社区版。

注意

有关社区版和专业版的比较列表,可以在他们的网站www.cloudmonkeymobile.com/monkeytalk上查看。

MonkeyTalk 可以在真实设备和模拟器上使用。它通过在录制模式下记录一系列事件来工作:

使用 MonkeyTalk 录制 UI 测试

一旦我们通过点击工具中的录制进入这个录制模式,所有的事件将以执行的动作用到的参数的顺序被记录下来。在上面的截图中,我们可以看到点击TextView并在上面输入一些内容是如何被记录为两个事件的。

我们可以在一个脚本文件中创建这个,MonkeyTalk 将会重现它;因此,我们有了在不录制的情况下创建自己的事件序列的选项。对于前面的事件,我们将编写如下脚本:

Input username tap
Input username enterText us

如果我们点击立即播放按钮,我们将在任何设备上看到所有这些步骤的执行。我们可以在 Android 手机上录制脚本,然后在 iOS 设备上播放它们。

除了录制和播放脚本,我们还可以有验证命令。例如,如果我们有一个清除所有输入字段的按钮,我们可以在脚本执行过程中使用currentValue添加一个验证命令:

Input username tap
Input username enterText us
Input clearform click
Input currentvalue ""

这将报告执行过程中验证的结果,这样我们就能检查我们的所有验证是否正确通过。例如,点击清除表单的按钮就需要一个点击监听器来清除每个输入文本。如果由于某种原因我们进行了修改,元素的 ID 发生了变化,MonkeyTalk 测试会通过一个失败的命令验证来报告这个问题。

如果每次我们在应用中做出更改时,有一个工具能为我们运行这些 UI 测试,以及单元测试和集成测试,那岂不是很棒?这个解决方案是存在的,它被称为持续集成

持续集成

我们的目的不是解释如何构建一个持续集成系统,因为这超出了本书的范围,而且通常不是 Android 开发者的职责来设置这个环境。但是,你应该了解它是什么以及它是如何工作的,因为它与 Android 直接相关。

一套良好的自动化测试套件与 CI 或持续集成解决方案结合使用总是更好的。这个解决方案将允许我们在每次代码更改时构建和测试我们的应用程序。

这就是大多数拥有大型项目的公司的工作方式。如果他们有一个开发团队,代码通常会在一个仓库中共享,并且他们会构建一个与仓库相连的持续集成(CI)系统。每当开发者在仓库中进行更改并提交时,都会执行测试集合,如果结果成功,就会构建一个新的 Android 可执行文件(APK)。

这样做是为了尽量减少问题的风险。在一个需要多年开发,不同人员参与的大型应用程序中,新开发人员在不破坏或更改现有功能的情况下开始进行更改是不可能的。这是因为项目中的所有人并不都了解所有代码的用途,或者代码过于复杂,修改一个组件可能会影响其他组件。

注意

如果你有兴趣实施这个解决方案,我们可以为你推荐Jenkins,其最初名为 Hudson,详情请访问wiki.jenkins-ci.org/display/JENKINS/Meet+Jenkins

持续集成

除了测试和构建我们的应用程序外,Jenkins 还将生成一个测试覆盖率报告,这将使我们能够了解单元测试和集成测试覆盖了我们代码的百分比。

总结

在本章中,我们开始学习如何在我们的应用程序中以高级方式使用日志,并快速概述了调试过程。我们解释了什么是测试,以及如何分别使用 Robolectric 和 Espresso 创建单元测试和集成测试。

我们还创建了 UI 测试,从使用 The Monkey 进行压力测试开始,然后生成随机事件,后来开始使用 MonkeyTalk 进行测试,记录可以再次播放以验证输出的事件流程。最后,我们讨论了持续集成,了解公司如何为 Android 应用程序将测试和构建系统集成在一起。

在下一章,也就是本书的最后一章中,我们将了解如何使我们的应用程序盈利,如何使用不同的构建风味构建应用程序,以及混淆代码,使其准备好上传到应用商店。

第十二章:营收化、构建过程和发布

这是本书的最后一章;我们需要完成的是实现应用的盈利化,生成不同的版本,并将其发布和上传到 Play 商店。

我们将通过创建不同的构建类型来完成构建过程,并生成不带广告的付费版本和带广告的免费版本的应用。所有这些都在同一个项目中,但将作为两个不同的应用导出。

一旦构建过程完成,我们将开始实施广告,并解释关于广告盈利的关键点;这将使我们的应用能够产生收入。

最后,我们将发布应用,并使用发布证书对我们的 APK 进行签名,混淆代码以防止被反编译。我们将上传到 Play 商店,并介绍在应用发布过程中需要注意的关键点。

  • 构建变体

  • 营收化

    • 广告盈利的关键点

    • 添加广告

  • 发布

    • 混淆和签名

    • 使用 Gradle 导出

  • 上传到 Play 商店

使用构建变体

为了解释在 Android 上通过广告实现盈利的机制,我们将在应用中添加广告,但在这一步之前,我们会设置一个构建过程,允许我们导出两个版本:付费版本和免费版本。这种策略在 Play 商店中很常见(提供一个带有广告的免费版本和一个不带广告的付费版本),这样所有用户都可以免费使用该应用,但不喜欢广告并希望支持应用的用户可以选择购买付费版本。

实现这一策略还有第二种方法,即只创建一个版本,并在应用内提供购买附加组件以移除广告的选项,通过应用内购买产品来实现。这种方式的缺点是,您的应用在 Play 商店中不会列为免费应用;它会被归类为“提供应用内购买”,因此可能有些用户对此感到不适应,或者家长不允许孩子使用付费应用或包含支付的应用。第二个问题是应用内购买不容易实现;这个过程非常复杂,涉及许多步骤,包括设置服务、在 Play 商店中创建产品、从应用中消费这些产品,以及设置一个测试环境,我们可以在不产生费用的前提下测试购买。

使用构建变体

构建变体是构建类型和产品风味的组合。

如果我们有构建类型 AB,以及产品风味 12,那么结果将产生以下构建变体:

A 1
A 2
B 1
B 2

为了更好地理解这一点,我们可以了解构建类型和构建风味是什么,以及如何创建它们。

创建构建类型

构建类型允许我们为调试或发布目的配置应用程序的打包。

让我们先看看我们的build.gradle文件:

buildTypes {
  release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

build.gradle文件中,我们可以看到发布构建类型有两个属性,我们将在本章末尾解释它们。

默认情况下,我们有两种构建类型:调试(debug)发布(release)。即使我们没有看到调试构建类型,所有变体也将在发布和调试模式下生成。

我们可以创建更多具有不同参数的构建类型;我们可以使用的参数之一是:

  • 签名配置

  • 调试签名标志

  • 更改版本名称或包名后缀

这意味着我们可以使用不同类型的证书来启用或禁用调试模式,并且可以有不同的包名。

构建类型并不是用来创建我们应用程序的不同版本,比如演示版或完整版,免费或付费等等。为此,我们有产品风味。每个构建类型都应用于每个构建风味,就像我们之前看到的,创建一个构建变体。

产品风味

我们将创建两个产品风味,并在build.gradle中使用以下代码声明它们:

productFlavors {
  paid {
    applicationId "com.packtpub.masteringandroidapp"
  }
  free {
    applicationId "com.packtpub.masteringandroidapp.free"
  }
}

我们有一个付费风味,这是没有广告的应用程序,还有一个名为*免费(free)*的风味,这是带有广告的免费版本。对于每个产品风味,我们可以在项目的../src/级别创建一个文件夹。我们的付费版本默认是主要的,所以不需要为其创建文件夹。

产品风味

这样,我们就可以为每个构建版本使用不同的类和资源,甚至可以有不同的AndroidManifest.xml文件。我们的应用程序将在main文件夹中共享付费版本和免费版本的通用代码,在free文件夹中包含特定于广告的代码。

要在不同的版本之间切换,我们只需更改构建变体窗口中的下拉菜单,如下面的屏幕截图所示:

产品风味

选择一个构建变体后,我们可以运行应用程序或导出它,相应地运行或导出所选的风味。这些可以配置为具有不同的包名和不同的版本名称。

现在,我们将看看如何向免费版本添加特定代码,这些代码不会包含在主付费版本中。

安卓应用的货币化

我们将描述通过应用程序赚钱的三种常见方式。

首先,我们可以在 Play Store 中为应用程序定价销售。在某些情况下,为你的应用收费比提供带有广告或应用内产品的免费应用更有意义。如果你为少数用户创造了具有大价值的应用,你绝对应该考虑这个选项。例如,如果我们发布一个为建筑师专业设计房屋的应用,我们会知道我们的应用不会被数百万用户下载;它是针对寻求高质量软件的特定目标受众。我们无法通过广告获得足够的利润,而我们的用户将愿意为使他们的工作更轻松的软件支付一笔不错的费用。要求用户预先支付费用总是存在风险的;即使用户可以选择退还应用,他/她可能也不够吸引人去尝试。这就是为什么我们应该考虑第二种模式。

第二种模式被称为免费增值模式。我们发布一个免费的应用程序,但其中包括应用内购买。同样以设计房屋的应用为例,我们可以提供三种免费设计,以便当用户对我们的产品感到满意时,我们可以要求他/她购买一次性许可或订阅以继续使用该应用。这在游戏中非常常见,你可以为你的角色购买物品。正是在游戏中,我们可以看到这个模式也可以与第三种模式结合以获得尽可能最大的收益。

盈利化的第三种模式是广告模式;我们在应用中放置广告,当用户点击它们时,我们获得收入。我们可以使用不同类型的广告——从全屏广告到底部的小横幅。我们将关注这个模式。实施它比你想象的要容易。但在实施之前,我们需要解释诸如CPC每次点击成本)、CTR点击通过率)、填充率等术语,这将帮助我们选择一个好的广告平台和提供商。这对于理解指标并能够阅读图表以了解应用中的广告表现也是必要的。在不同的地方放置广告可能会改变收入;然而,我们需要在不烦扰用户的情况下最大化收入。如果我们为用户提供以小额费用通过应用内产品或无广告的付费版本移除广告的选项,我们可以增加广告的数量。如果用户知道他们有选择,这对他们来说是最好的。如果他们选择与广告共存,这是他们的决定,它不会像我们在没有移除选项的情况下放置大量广告那样让他们感到烦恼。

广告盈利化的关键点

我们将解释理解广告盈利化如何运作的基础知识。在这个业务中有一些带有缩写的概念,初学者可能会感到困惑。

一旦我们在广告平台上注册,我们就会看到一个关于我们应用的统计数据报告页面。以下就是来自广告网络AdToApp的仪表板示例:

广告盈利的关键点

在这里,我们可以看到请求、填充率、展示量、点击量、CTR、eCPM 和收入。让我们逐一考虑它们。

请求是指我们的应用向广告网络请求广告的次数。例如,如果我们决定在应用开始时添加一个全屏广告,那么每次启动应用时,都会向服务器发送一个请求以获取广告。

我们的应用中并没有实际的广告;我们拥有的是一个占位符、一个框架和一个AdView,它将由广告网络提供的内容填充。有时,在请求时刻广告网络可能没有广告给我们,这就是下一个概念重要的原因。

填充率是通过已投放广告数量除以请求广告数量得出的百分比。例如,如果我们启动应用十次,只收到五次广告返回,那么我们的填充率就是 50%。在一个好的广告网络中,我们希望填充率是 100%。我们希望展示尽可能多的广告,并且有好的 CPC。

CPC,即每次点击成本,是指用户在我们的应用中每次点击广告时我们能赚到的钱;这个数值越高,我们获得的收入越多。广告商决定广告的 CPC。一些广告商可能愿意为每次点击支付更多的费用。

许多低 CPC 的点击并不一定比少数高 CPC 的点击更好。这就是为什么我们拥有的广告质量很重要。

展示量是指广告向用户展示的次数。在之前的例子中,如果有十次广告请求,其中五次失败,我们就会有五次展示量。如果用户没有点击,展示量不会产生收入。

点击量是指用户点击广告的次数。这是基于 CPC 产生收入的部分。因此,五次点击,每次点击 0.5 美元,将会产生 5x0.5,即 2.5 美元。

CTR,即点击通过率,是通过点击量除以展示量得出的百分比。如果我们有 100 次广告展示并获得一次点击,我们的 CTR 将是 1%。这个数值通常在 5%以下;用户不会点击他们看到的每一个广告,如果你通过强制用户点击广告来作弊,比如Admob这样的广告平台可能会取消你的账户和支付。假设我们在应用开始时显示一个对话框,要求用户点击广告以继续使用我们的应用。这将基本上给我们带来 100%的 CTR;每次展示都会有点击,这是不允许的。在任何情况下,我们都不可以推广点击。

广告提供商希望他们的广告能被对其感兴趣的人看到;他们不希望为那些对广告不感兴趣、一秒钟后就会关闭广告的人的点击付费。可能的情况是,你有一个高的点击率(CTR),因为你在应用中有一个好的广告位置,而且每个用户都对广告感兴趣。如果发生这种情况,你将不得不向你的广告网络解释,或者像Admob这样的广告网络可能会关闭你的账户。但我们也不应该对他们太不公平;他们这样做是因为他们发现很多人试图破坏规则,这样一个庞大的公司无法专注于个人,所以他们需要有客观的筛选机制。

其他广告网络公司在这方面更加灵活;他们通常会为你分配一个代理人,你可以通过 Skype 或电子邮件频繁联系他,如果遇到任何问题,他们通常会通知你。

eCPM 代表“每千次展示的有效成本”。它是通过将总收入除以总展示次数(以千为单位)来计算的。这基本上是通过一个数字快速了解你表现如何的方法——非常有助于比较广告网络。这个数字通常在030 到 3之间。

我们需要考虑的是,这并不包括填充率。它是每千次展示的成本,而不是每千次请求的成本。一个 50%填充率的三美元 eCPM 与 100%填充率的一块半美元 eCPM 是相同的。

使一个广告网络变得优秀的是高填充率和高 eCPM。我们需要这两个指标都高;如果点击费用昂贵但填充率不足,广告将不会产生任何收入,因为它们根本不会被展示。

AdToApp的团队制作了一张很好的图来解释这一点:

广告盈利的关键点

这张图展示了我们一直在讨论的内容;一个高 eCPM 但低填充率的优质广告网络被表现为一座高但空荡荡、灯光熄灭的大楼。

理论部分我们已经讲完了,现在可以开始集成广告解决方案了;在这种情况下,我们将选择 AdToApp。

使用 AdToApp 添加广告

没有办法知道哪个广告提供商更适合你;你能做的最好的事情就是尝试不同的广告提供商,并查看统计数据。

根据我们的经验,我们喜欢使用 AddToApp,除了因为它良好的投放效果外,它的集成过程非常简单,即使你的应用中已经有其他网络广告,也可以轻松加入。因此,衡量其性能真的很容易。

在这本书中使用它与MasteringAndroidApp非常合适,因为它允许我们使用不同类型的广告,包括全屏广告、横幅、视频等等。

有超过 20 个不同广告网络的调解器,因此包含他们的 SDK,我们将能够访问许多保证高填充率的广告。关于他们的 eCPM,他们会分析哪个网络能为你带来更好的结果;因此,如果他们可以从多个网络投放广告,他们将投放效果更好的广告。

通过 AdToApp 添加广告

我们可以开始创建一个账户,地址是adtoapp.com/?r=OZ-kU-W9Q2qeMmdJsaj3Ow

创建账户后,我们将使用我们应用的包名创建一个应用。

通过 AdToApp 添加广告

我们将点击 SDK 按钮以下载他们的 SDK 并获得集成配置值。

通过 AdToApp 添加广告

集成非常直接;SDK 将包含一个AdToAppSDK.jar文件,我们需要将其复制到libs目录中。我们还需要在build.gradle中添加 Google Play 服务和支持库v7,但我们已经有了这些。

我们需要在清单中添加基本权限,我们已经有这些了,使用以下代码:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

然后,我们需要在清单中添加额外的必要资源,可以从同一网站复制,它包含我们账户的密钥。你可以在以下截图所示的第一部分找到它们:

通过 AdToApp 添加广告

最后,我们可以看看如何实现插屏广告和横幅广告或激励广告。激励广告是游戏中弹出的一种广告,提示观看此视频并获得(金币、宝石等)。是否观看这些广告完全取决于用户是否想要奖励:

通过 AdToApp 添加广告

如果我们选择插屏广告和横幅广告,我们需要根据是否只需要视频广告、只需要图片(横幅)广告,或者插屏中同时需要图片和视频广告来初始化它们。

在网站上,根据你想要的广告类型,将显示必要的代码。

SDK 非常灵活;我们可以更进一步,设置回调以了解横幅广告何时加载和点击。这使我们能够跟踪广告中的点击次数,并验证它们与 AdToApp 控制台中的是否相同,从而使过程透明化。

如果我们需要额外的帮助,我们可以在 SDK 中激活日志,它将在出现任何问题时通知我们。

现在,记住我们在本节开始时提到的最佳实践,即最大化广告数量而不过多打扰用户,并在你的应用中实施这些实践,开始获得收益!

通过 AdToApp 添加广告

将我们的应用发布到 Play 商店

最后,我们的应用准备好了!这是开发新应用时最棒的瞬间;是时候将其上传到 Play 商店,获取用户的反馈,并希望获得成千上万的下载量。

我们需要将应用导出为 APK 文件;为了上传到 Play 商店,它必须使用发布证书进行签名。这一点非常重要;一旦应用程序用证书签名,如果我们将其上传到 Play 商店,并在将来想要上传新版本,它必须使用相同的证书进行签名。

我们在发布过程中会创建这个证书。它需要一个别名和密码,请确保你记住这些细节并将证书文件保存在安全的地方。否则,假设你的应用得到了好的评分和大量的下载,当你想要更新版本时,如果没有证书或忘记了密码,那就无法更新。在这种情况下,你不得不以上传一个具有不同包名的全新应用,并且从零下载和零评分开始。

代码混淆

在发布应用时,还需要考虑的另一个重要事项是代码混淆。如果我们导出应用而不混淆代码,任何人都可以下载 APK 并反编译它,使他们能够看到你的代码,如果其中有 Parse IDs、服务器访问细节、GCM 项目编号等,这可能会成为安全问题。

我们可以使用 Proguard 来混淆代码。Proguard 是 Android 构建系统中包含的一个工具。它混淆、缩小和优化代码,移除未使用的代码,并重命名类、字段和方法,以防止逆向工程。

注意类和方法的重命名,这可能会影响你的崩溃和错误报告,因为堆栈追踪将会被混淆。然而,这不是问题,因为我们可以在发布应用时保存一个映射文件,用它可以重新追踪,这将允许我们将崩溃和报告转换成可读的、未被混淆的代码。

要激活 Proguard,我们需要在 buildTypes 中将 minifyEnabled 属性设置为 true。你可以执行以下代码来实现这一点:

buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

在我们的项目中,有一个 proguard-rules.pro 文件,我们可以在混淆时考虑添加规则。例如,如果我们混淆一些第三方库,它们可能无法正常工作,并且不混淆这些库是没有风险的,因为它们不是我们创建的;我们只是将它们添加到我们的项目中。

代码混淆

为了防止第三方库被混淆,我们可以添加 -keep 规则以及 -dontwarn 规则来忽略警告。例如,我们添加了 calligraphy 以使用自定义字体;这样我们可以在混淆期间忽略它:

# DONT OBFUSCATE EXTERNAL LIBRARIES

# CALLIGRAPHY
-dontwarn uk.co.chrisjenx.calligraphy.**
-keep class uk.co.chrisjenx.calligraphy.** {*;}
# TIMBER
-dontwarn timber.log.**
-keep class timber.log.** {*;}

使用 keep 和包名,我们将保留该包内的所有类。

我们将在调试模式下添加 Proguard,故意创建一个崩溃,看看混淆后的堆栈追踪是什么样子:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setVisibility(int)' on a null object reference
            at com.packtpub.masteringandroidapp.SplashActivity.onCreate(Unknown Source)

我们可以将这个stracktrace复制到文本文件中,然后前往app/build/outputs/mapping/product_flavor_name/ release_or_debug/mapping.txt获取我们的mapping.txt文件。

考虑我们在<sdk_root>/tools/proguard执行以下代码的 retrace 命令:

retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

在这种情况下,我们将有正确的行号显示崩溃,如下所示:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setVisibility(int)' on a null object reference
at com.packtpub.masteringandroidapp.SplashActivity.onCreate(SplashActivity.java:21)
at android.app.Activity.performCreate(Activity.java:6289)

记得在每次发布应用时保存mapping.txt的副本;每次发布时这个文件都会被覆盖,因此在每次发布时保存文件非常重要。或者,如果你有一个仓库,并且你为每次发布标记提交,你可以回退并再次生成相同的发布,理论上这将具有相同的映射文件。

既然我们的应用已经针对逆向工程进行了保护,我们可以继续发布流程。

导出应用

当我们导出应用程序时,我们要做的是在发布模式下创建一个 APK 文件并用证书签名。这个证书是 Play Store 中应用属于我们的证明,有了它,我们可以像之前解释的那样上传同一个应用。这次我们将导出应用并创建一个证书。

要导出我们的应用程序,有两种方法:一种是在 Android Studio 中使用 Gradle 和终端,第二种是使用 Android Studio 中的向导。我们将介绍两种方法,但首先使用第二种方法创建证书。

导航到构建 | 生成签名的 Apk;你会看到一个与以下类似的对话框:

导出应用

如果我们之前导出过这个应用并创建了证书,那么只需选择路径,输入别名和密码,这将使用现有证书签署导出一个新版本的应用。

对我们来说,这是第一次导出MasteringAndroidApp,因此我们将点击创建新的…。在下一个屏幕上,我们需要选择保存证书的路径,这是一个.keystore文件。

我们还需要为keystore和一个证书内的别名设置密码。对于有效日期,100 年应该是可以的;如果你的应用比你活得更久,那就不是你的问题了!最后,在这里至少需要一个字段填写个人信息:

导出应用

最后,它会询问我们想要导出哪个版本的应用,并且会创建.apk文件,同时指出文件的路径。

这种方法很直接,但还有一种自动化的方法可以使用命令行和 Gradle 导出应用;例如,如果我们想用 Jenkins 构建应用,这会非常有用。

为此,我们需要在build.gradle中添加一个签名配置,这样当自动生成应用时,它会知道要使用哪个keystore以及哪个别名和密码。以下代码将有助于实现这一点:

signingConfigs {
  release {
    storeFile file("certificate.keystore")
    storePassword "android"
    keyAlias "android"
    keyPassword "android"
  }
}

不用说,这可能导致安全问题;密码写在build.gradle中,证书文件包含在我们的项目中。如果我们这样做,我们需要确保项目安全。如果这是一个关注点,你可以使用以下代码在运行时读取密码和别名:

storePassword new String(System.console().readPassword("\n\$ Enter keystore password: "))
keyAlias System.console().readLine("\n\$ Enter key alias: ")
keyPassword new String(System.console().readPassword("\n\$ Enter key password: "))

当我们运行生成签名 APK 的命令时,系统会要求我们输入密码别名和别名密码。我们可以使用以下代码行来完成这个操作:

>./gradlew assembleRelease

导出应用

应用导出后,我们可以继续最后一步:上传到 Play 商店。

将我们的应用上传到 Play 商店

要发布应用,我们需要一个 Google 开发者账户。如果你没有,可以从play.google.com/apps/publish/获取一个。

创建发布商账户

创建发布商账户的第一步是输入基本信息,并阅读并接受开发者分销协议。第二步是支付 25 美元的开发许可费用以创建账户。这是我们发布应用所需支付的全部费用,只需一次性支付——一次付费,终身许可。考虑到 iOS 上每年要支付 99 美元,我们不应该抱怨。

最后的第三步需要开发者的名字,该名字将显示在我们的应用程序名称下方。以下是 Google Inc 的示例:

创建发布商账户

我们还需要电子邮件、手机号码以及可选的网站。根据谷歌的说法,这是为了在有人需要就发布的内容联系我们时使用。

Google Play Developer 控制台

当我们打开发布商账户时,如果我们还没有发布任何应用,我们将看到开发者控制台四个主要功能,如下面的图片所示:

Google Play Developer 控制台

第一个选项是发布一个 Android 应用,这也是我们将在书中遵循的选项。然而,在此之前,我们将快速描述其他需要考虑的选项。

第二个选项是关于 Google Play 游戏服务。如果你开发了一个游戏,希望玩家保存并提交他们的分数,并有一个分数排名,你将需要一个服务器来存储这些分数并检索它们,甚至可能需要玩家用户名和登录。游戏服务为我们完成这些工作。

它提供了一个 API,跨游戏共享,并与用户的 Google 账户关联,我们可以管理排行榜和成就。它甚至提供了实现多人游戏(包括实时多人和回合制)的 API 和基础设施。

左侧底部的第三个选项是关于分享开发者控制台的。我们可能希望允许其他开发者更新应用。例如,在公司中,这将有助于那些负责设置应用名称、描述、图片和总体市场营销的人员,以及其他负责应用上传和开发的人员。我们可以配置对控制台和特定应用的访问权限。

Google Play 开发者控制台

第四个也是最后一个选项是商家账户;如果我们想要销售付费应用或应用内产品,就需要这个。这是来自付费应用的商家账户示例;我们可以看到完成的支付和取消的支付。如果用户购买了我们的应用,他/她在两小时内可以申请退款,如果他/她不喜欢它的话。

Google Play 开发者控制台

因为我们还没有发布应用,所以我们看到了一个包含四个主要选项的空白开发者控制台;如果我们有已发布的应用,我们会看到这样的界面。在这种情况下,发布按钮位于顶部:

Google Play 开发者控制台

在初始屏幕上,我们可以看到不同的应用,无论它们是免费还是付费,活跃安装数和总安装数。活跃安装意味着目前拥有该应用并且下载后没有卸载的人数。总安装数意味着应用被安装的所有次数的总计。

我们还可以查看评分和崩溃次数。如果我们点击应用并进入详细视图,可以查看更多详细信息,比如用户的评论和错误崩溃报告。

发布应用

继续上传过程,当我们点击**+ 添加**新应用时,会被要求输入名称和默认语言。在此之后,我们可以选择通过上传 APK 或准备商店列表来开始流程。

发布应用

这包括两个不同的过程:一个是上传 APK 文件,另一个是设置应用的标题、描述、图片、是否付费等——所有在 Play 商店中展示的不同选项。

让我们先从上传 APK 文件和不同的测试组开始。

上传 APK 文件

请记住,当我们上传 APK 时,我们应用的包名在 Play 商店中必须是唯一的;如果我们想要更新之前由我们发布的 app,并且使用初始下载签名的证书与新的 APK 签名证书相同,那么我们可以上传具有现有包名的 APK。

当我们点击上传 APK时,首先注意到的三个不同标签页的名称分别为:生产环境测试版Alpha 版

上传 APK 文件

我们可以在两个测试组以及生产环境中发布我们的应用程序。生产意味着它在 Play 商店中发布;它是公开的,对所有人可见。一段时间以来,这曾是开发者控制台中的唯一选项,直到他们增加了分阶段推出的功能。

分阶段推出允许我们将应用程序发布给一组有限的用户。为了选择这些用户,我们有不同的选项;我们可以通过电子邮件邀请这些用户,分享链接,或者创建一个 Google 群组或 G+社区,邀请用户加入该群组,并将应用程序的链接分享给他们。只有这些用户才会在 Play 商店中看到应用程序。这对于在应用程序向全世界发布之前从一些用户那里获得反馈非常有用,当然,也可以防止应用程序在生产环境中出现错误和差评。我们还可以选择在生产环境中发布我们应用程序的用户百分比;例如,如果我们有百万用户,我们可以首先向 10%的用户发布,并在进行大规模发布之前再次确认一切是否正常。

我们的应用程序可以在不同的阶段有不同的版本;例如,我们可以发布版本 1.0.0,1.0.1 进行 beta 测试,1.0.2 进行 alpha 测试。我们可以从 alpha 阶段滚动到 beta 阶段,从 beta 阶段滚动到生产阶段,但我们不能回滚。

我们现在要解释的概念非常重要。一旦我们发布了应用程序的一个版本,我们就无法回到之前的发布版本。可能会出现这样的情况:我们在 Play 商店中有一个应用程序的正常运行的版本,我们开发了一个新版本,在我们的设备上运行良好,我们认为它已经准备好上传了。现在是周五下午,我们不想进行测试,因为我们会想,“哦,我相信它没问题。我只是改了两行代码,不会影响任何东西”。我们上传了版本 1.0.4。几小时后,我们开始收到来自 Play 商店的崩溃报告。这是恐慌的时刻;我们现在能做的唯一事情是撤销当前应用程序的发布,以防止更多损害,并尽快开始修复。然而,如果修复不容易,最明智的做法是再次生成最后一个已知正常工作的版本(1.0.3),将版本号和代码增加到 1.0.5,并将其上传到 Play 商店。

然而,这可能会变得更糟;如果我们有一个数据库,并且其结构从 1.0.3 更改为 1.0.4,如果我们的代码还没有准备好接受从 1.0.4 降级到 1.0.3(更名为 1.0.5)的数据库,我们知道我们整个周末都要工作,只是为了在周一早上被解雇。总之,我们的观点是,预防胜于治疗;因此,使用分阶段推出,在发布之前进行所有必要的测试,并避免在周五下午发布,以防万一。

准备商店列表

对于开发者来说,准备商店列表可能是最无聊的部分,但为了发布应用程序,这是必须要完成的;有一些我们不能跳过的必填资产和字段。

首先,我们需要为我们的应用准备一个标题,一个最多 80 个字符的简短描述和一个最多 4000 个字符的长描述。标题将是我们搜索应用时首先看到的内容;简短描述可以在例如浏览应用时的平板电脑上看到。这是我们应用的elevator pitch,我们需要在这里描述其主要功能。

准备商店列表

长描述将在我们查看此应用的详细视图时显示。为了在更多搜索中出现并获得可见性,在描述中识别并添加与应用相关的关键词是很好的做法。使用不相关关键词吸引下载是被 Google 禁止的,如果你这样做,你将在开发者控制台收到警告,并且你的应用在重新获得批准和发布之前需要做出一些更改。

在这一点上,我们可以选择国际化我们应用的列表,重复这些字段,用我们想要的任何语言,它们将根据用户的语言自动显示在不同的语言中。

下一步是开发图形,我们需要在这里进行截图。截图可以通过设备上的按键组合轻松完成;例如,在三星 Galaxy 3 上,这是通过同时按下音量减菜单键完成的。也可以通过在 Android Studio 中选择 Android 视图中的相机图标来获取截图。

准备商店列表

除了截图,我们还需要一个 512 x 512 高分辨率的图标;这必须与上传版本中我们应用所使用的图标相同或非常相似,否则会收到警告。因此,最好始终以 512 x 512 的尺寸创建图标,然后将其缩小以用于我们的应用。反其道而行将导致放大后图像质量变差。以下是图标显示的一个示例:

准备商店列表

我们需要准备的最后一张图片是功能图。这是一张 1024 x 500 的图片,展示了我们应用的特点。这是在我们应用在 Google Play 上展示时会被用到的图片。它将在 Play 商店应用中展示;如果我们有促销视频,即使视频没有播放,功能图也会显示。

准备商店列表

我们需要继续进行分类;根据我们的应用是否为游戏或应用程序,我们需要选择不同的类别。如果你不确定选择哪个类别,可以在 Play 商店查看类似你应用的 app。

此后,我们需要选择内容评级;从 2015 年 5 月开始,每个应用都需要有新的评级系统。根据谷歌的说法,这个新的内容评级为向用户传达熟悉且与本地相关的内容评级提供了一种简单方式,并通过针对你的内容定位合适的目标受众来帮助提高应用参与度,具体内容可参考support.google.com/googleplay/android-developer/answer/188189

我们的联系方式会自动填写,所以我们还需要做的就是接受隐私政策,然后点击定价与分发

准备商店列表

在这里,我们决定应用是免费还是付费;这一步无法撤销。如果应用是付费的,我们可以设定一个价格,谷歌会将它转换成不同国家的不同货币;尽管如此,我们可以为每个国家设定不同的价格。我们可以选择加入不同的开发者群体;例如,如果我们开发了一个儿童应用,我们可以将其包含在为家庭设计中。这将增加我们在儿童专区被突出显示的机会,并分发到与儿童应用相关的第三方网络。

在这一部分,我们还可以选择我们希望应用分发的国家。这也可以用作首次发布应用时的分阶段发布策略。

准备商店列表

完成以上所有步骤后,我们将能够通过点击右上角的发布来发布我们的应用。

准备商店列表

如果按钮不可用,你可以点击为什么我不能发布?,它将在左侧列出要求。应用发布后,可能需要几个小时才能在 Play 商店中显示。确定应用是否已发布的最简单方法是使用包名在 URL 中导航到我们的应用。在我们的例子中,URL 将是 play.google.com/store/apps/details?id=com.packtpub.masteringandroidapp

就这样!我们从初学者到更高级别完成了这本书,拥有足够的知识来上传一个设计精良、构建完善、向下兼容并实现盈利的应用。

我们祝愿你的应用成功,并希望你能打造出下一个《愤怒的小鸟》或下一个 WhatsApp!

注意

感谢购买并完成这本书。对于建议、改进或有任何反馈,请毫不犹豫地联系我 <Antonio@suitapps.com> 或在 Twitter 上关注我 @AntPachon

总结

在本书的最后一章,我们开始学习如何创建应用的不同版本,通过结合构建类型与产品风味来获得构建变体。

之后,我们学习了如何对我们的应用程序进行货币化,添加了不同类型的广告,并解释了广告货币化的关键要点。

我们还从 Android Studio 和使用 Gradle 命令行导出了应用程序,进行了混淆并使用发布证书进行了签名。

最后,我们在 Play 商店上传并发布了我们的应用程序。