1. 准备工作
假设您在拥挤的购物中心,看到了朋友小杰。您可能想挥舞手臂并大声呼喊来引起小杰的注意。借助 Google 的 Nearby Messages API,您可以让应用代用户完成大声呼喊来引起对方注意的环节,让朋友能在相距较近时轻松发现彼此。此 Codelab 教您如何使用 Nearby Messages API 实现近距离的用户互动。为简单起见,在此 Codelab 中,每个用户都将发布其手机的 build 型号:android.os.Build.MODEL
。但实际上,您可以让每位用户向附近的朋友发布他们的 userId
或适合您的用例的其他信息。Nearby Messages API 结合使用互联网连接、蓝牙和其他技术来提供此功能。
前提条件
- 具备 Kotlin 和 Android 开发方面的基础知识
- 了解如何在 Android Studio 中创建和运行应用
- 两台或多台用于运行和测试代码的 Android 设备
学习内容
- 如何将 Nearby 库添加到应用
- 如何向相关方广播消息
- 如何检测来自相关方的消息
- 如何为消息获取 API 密钥
- 电池续航时间最佳做法
所需条件
- 一个用于获取 Google API 密钥的 Google 账号(即 Gmail 地址)
- 最新版本的 Android Studio
- 两台安装有 Google Play 服务(即 Play 商店)的 Android 设备
- 互联网连接(而不是不需要互联网连接的 Nearby Connections API)
构建内容
一个让用户可以发布设备信息并接收附近设备相关信息的 Activity
应用。此应用包含两个用户可以切换的开关:第一个开关用于发现或停止发现附近的消息;第二个开关用于发布或取消发布消息。对于此应用,我们希望发布和发现模式都在 120 秒后停止。为此,我们将稍微更深入地了解一下此 API,然后创建 PublishOptions
和 SubscribeOptions
对象,并利用这两者的 onExpired()
回调来关闭发布和订阅界面开关。
2. 创建 Android Studio 项目
- 启动一个新的 Android Studio 项目。
- 选择 Empty Activity。
- 将项目命名为 Nearby Messages Example,并将语言设置为 Kotlin。
3. 设置代码
- 将最新版本的 Nearby 依赖项添加到您的应用级
build.gradle
文件中。这样您就可以使用 Nearby Messages API 来发送消息和检测来自附近设备的消息。
implementation 'com.google.android.gms:play-services-nearby:18.0.0'
- 在 Android 代码块中将 viewBinding 构建选项设置为
true
,以启用 ViewBinding。
android {
...
buildFeatures {
viewBinding true
}
}
- 点击 Sync Now 或绿色锤子按钮,让 Android Studio 能登记这些 Gradle 更改。
- 添加“Discover nearby devices”和“Share device information”这两个切换开关,以及会包含设备列表的 RecycleView。在
activity_main.xml
文件中,将代码替换为以下内容。
<?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:id="@+id/activity_main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/subscribe_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Discover nearby devices" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/publish_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Share device information" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nearby_msg_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:transcriptMode="alwaysScroll" />
</LinearLayout>
注意:在您自己的项目中,应将值(如 16dp
)更改为资源(如 @dimen/activity_vertical_margin
)。
4. 向应用添加“附近消息”功能
定义变量
- 在主 activity (
MainActivity.kt
) 中,将以下代码段粘贴在onCreate()
函数的上方来定义以下变量。
/**
* For accessing layout variables
*/
private lateinit var binding: ActivityMainBinding
/**
* Sets the time to live in seconds for the publish or subscribe.
*/
private val TTL_IN_SECONDS = 120 // Two minutes.
/**
* Choose of strategies for publishing or subscribing for nearby messages.
*/
private val PUB_SUB_STRATEGY = Strategy.Builder().setTtlSeconds(TTL_IN_SECONDS).build()
/**
* The [Message] object used to broadcast information about the device to nearby devices.
*/
private lateinit var message: Message
/**
* A [MessageListener] for processing messages from nearby devices.
*/
private lateinit var messageListener: MessageListener
/**
* MessageAdapter is a custom class that we will define later. It's for adding
* [messages][Message] to the [RecyclerView]
*/
private lateinit var msgAdapter: MessageAdapter
由于我们希望自定义广播应持续的时间,所以定义了 Strategy
。在此 Codelab 中,我们选择了 120 秒。如果您未指定 Strategy,此 API 会使用默认值。此外,即使我们在此 Codelab 中为发布和订阅使用同一 Strategy
,这也不是必需操作。
- 更改您的
onCreate()
函数,以将 ViewBinding 对象传入setContentView()
。这会显示activity_main.xml
布局文件的内容。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
连接界面按钮
此应用会执行三项操作:轻点一下开关即可发布消息、轻点一下开关即可发现消息,以及在 RecyclerView
中显示消息。
- 我们希望用户发布消息和取消发布消息,以及发现(即订阅)消息。现在,为此创建名为
publish()
、unpublish()
、subscribe()
、unsubscribe()
的桩方法。我们会在后续步骤中创建实现。
private fun publish() {
TODO("Not yet implemented")
}
private fun unpublish() {
TODO("Not yet implemented")
}
private fun subscribe() {
TODO("Not yet implemented")
}
private fun unsubscribe() {
TODO("Not yet implemented")
}
- 用户可以使用 activity 布局中添加的开关来发布消息或发现(即订阅)消息。连接两个
Switches
以调用我们在onCreate()
函数末尾定义的方法。
binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
subscribe()
} else {
unsubscribe()
}
}
binding.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
publish()
} else {
unpublish()
}
}
- 现在,您已添加用于发布和发现消息的界面代码,接下来要设置用于显示和移除消息的
RecyclerView
。RecyclerView
会显示正在主动发布的消息。订阅者会监听消息。发现消息后,订阅者会将其添加到RecyclerView
;当消息消失(即发布者停止发布)后,订阅者会将其从RecyclerView
中移除。
private fun setupMessagesDisplay() {
msgAdapter = MessageAdapter()
with(binding.nearbyMsgRecyclerView) {
layoutManager = LinearLayoutManager(context)
this.adapter = msgAdapter
}
}
class MessageAdapter : RecyclerView.Adapter<MessageAdapter.MessageVH>() {
private var itemsList: MutableList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageVH {
return MessageVH(TextView(parent.context))
}
override fun onBindViewHolder(holder: MessageVH, position: Int) {
holder.bind(getItem(position))
}
override fun getItemCount(): Int = itemsList.size
private fun getItem(pos: Int): String? = if (itemsList.isEmpty()) null else itemsList[pos]
fun addItem(item: String) {
itemsList.add(item)
notifyItemInserted(itemsList.size)
}
fun removeItem(item: String) {
val pos = itemsList.indexOf(item)
itemsList.remove(item)
notifyItemRemoved(pos)
}
inner class MessageVH(private val tv: TextView) : RecyclerView.ViewHolder(tv) {
fun bind(item: String?) {
item?.let { tv.text = it }
}
}
}
- 在
onCreate()
函数的末尾,添加对setupMessagesDisplay()
函数的调用。
override fun onCreate(savedInstanceState: Bundle?) {
...
setupMessagesDisplay()
}
现在,界面全部设置完毕,您可以开始发布消息,让附近的其他设备发现。此时,您的应用应如下所示:
添加发布和发现代码
- 如要发送消息,我们首先需要一个
Message
对象。考虑到这是演示,我们仅发送设备的型号。将下面这段代码添加到onCreate()
函数中,用于创建要发送的消息。
override fun onCreate(savedInstanceState: Bundle?) {
...
// The message being published is simply the Build.MODEL of the device. But since the
// Messages API is expecting a byte array, you must convert the data to a byte array.
message = Message(Build.MODEL.toByteArray(Charset.forName("UTF-8")))
}
- 如要发布其他附近的设备能发现的
Message
,只需调用Nearby.getMessagesClient(activity).publish(message)
。不过,我们建议您更进一步,构建您自己的PublishOptions
对象;这样一来就可以指定自己的自定义Strategy
,并利用PublishCallback
(在已发布的消息失效时发出通知)。在以下代码中,我们创建了一个选项,以便我们可以在已发布的TTL
失效后为用户关闭开关。然后,我们会在调用publish()
时传入此选项。将您的publish()
函数更新为以下内容。
private fun publish() {
val options = PublishOptions.Builder()
.setStrategy(PUB_SUB_STRATEGY)
.setCallback(object : PublishCallback() {
override fun onExpired() {
super.onExpired()
// flick the switch off since the publishing has expired.
// recall that we had set expiration time to 120 seconds
// Use runOnUiThread to force the callback
// to run on the UI thread
runOnUiThread{
binding.publishSwitch.isChecked = false
}
}
}).build()
Nearby.getMessagesClient(this).publish(message, options)
}
每当用户开启 publish 开关时,此代码就会运行。
- 发布操作需要
Message
,而订阅操作需要MessageListener
。但是在这里,我们同样建议您构建一个SubscribeOptions
对象,即使该 API 正常运行并不需要此对象也要构建。例如,通过构建自己的SubscriptionOption
,您可以指定发现模式的持续时间。
将以下 MessageListener
代码添加到 onCreate()
函数中。检测到消息时,监听器会将其添加至 RecyclerView
中。当消息消失时,监听器会将其从 RecyclerView
中移除。
messageListener = object : MessageListener() {
override fun onFound(message: Message) {
// Called when a new message is found.
val msgBody = String(message.content)
msgAdapter.addItem(msgBody)
}
override fun onLost(message: Message) {
// Called when a message is no longer detectable nearby.
val msgBody = String(message.content)
msgAdapter.removeItem(msgBody)
}
}
- 从技术上讲,订阅者不需要
TTL
(或者可以将TTL
设置为无穷大)。但在此 Codelab 中,我们希望发现模式在 120 秒后停止。因此,我们要构建自己的SubscribeOptions
,并使用其onExpired()
回调来关闭订阅界面Switch
。使用以下这段代码更新您的订阅函数。
private fun subscribe() {
val options = SubscribeOptions.Builder()
.setStrategy(PUB_SUB_STRATEGY)
.setCallback(object : SubscribeCallback() {
override fun onExpired() {
super.onExpired()
// flick the switch off since the subscribing has expired.
// recall that we had set expiration time to 120 seconds
// Use runOnUiThread to force the callback
// to run on the UI thread
runOnUiThread {
binding.subscribeSwitch.isChecked = false
}
}
}).build()
Nearby.getMessagesClient(this).subscribe(messageListener, options)
}
- 请务必让您的用户能够关闭信息共享功能。这意味着让发布者能停止发布,并让订阅者能停止订阅。如要停止发布,发布者必须指定要停止发布的消息。因此,如果正在广播 10 条消息,可以停止发布 1 条,留下 9 条。
private fun unpublish() {
Nearby.getMessagesClient(this).unpublish(message)
}
- 一个应用可以同时发布多条消息,但一次只能存在一个
MessageListener
;因此,退订更为通用。如要停止订阅,订阅者必须指定监听器。
private fun unsubscribe() {
Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
- 顺便提一下,虽然此 API 应在客户端进程终止时关闭其进程,但您可能想在
onDestroy()
生命周期方法中停止订阅(并在方便的情况下停止发布)。
override fun onDestroy() {
super.onDestroy()
// although the API should shutdown its processes when the client process dies,
// you may want to stop subscribing (and publishing if convenient)
Nearby.getMessagesClient(this).unpublish(message)
Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
此时,我们的代码应该可以编译了。不过,由于缺少 API 密钥,应用还无法按预期运行。您的 MainActivity
看上去应如下所示。
package com.example.nearbymessagesexample
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.nearbymessagesexample.databinding.ActivityMainBinding
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.messages.Message
import com.google.android.gms.nearby.messages.MessageListener
import com.google.android.gms.nearby.messages.PublishCallback
import com.google.android.gms.nearby.messages.PublishOptions
import com.google.android.gms.nearby.messages.Strategy
import com.google.android.gms.nearby.messages.SubscribeCallback
import com.google.android.gms.nearby.messages.SubscribeOptions
import java.nio.charset.Charset
class MainActivity : AppCompatActivity() {
/**
* For accessing layout variables
*/
private lateinit var binding: ActivityMainBinding
/**
* Sets the time to live in seconds for the publish or subscribe.
*/
private val TTL_IN_SECONDS = 120 // Two minutes.
/**
* Choose of strategies for publishing or subscribing for nearby messages.
*/
private val PUB_SUB_STRATEGY = Strategy.Builder().setTtlSeconds(TTL_IN_SECONDS).build()
/**
* The [Message] object used to broadcast information about the device to nearby devices.
*/
private lateinit var message: Message
/**
* A [MessageListener] for processing messages from nearby devices.
*/
private lateinit var messageListener: MessageListener
/**
* For adding [messages][Message] to the [RecyclerView]
*/
private lateinit var msgAdapter: MessageAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.subscribeSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
subscribe()
} else {
unsubscribe()
}
}
binding.publishSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
publish()
} else {
unpublish()
}
}
setupMessagesDisplay()
// The message being published is simply the Build.MODEL of the device. But since the
// Messages API is expecting a byte array, you must convert the data to a byte array.
message = Message(Build.MODEL.toByteArray(Charset.forName("UTF-8")))
messageListener = object : MessageListener() {
override fun onFound(message: Message) {
// Called when a new message is found.
val msgBody = String(message.content)
msgAdapter.addItem(msgBody)
}
override fun onLost(message: Message) {
// Called when a message is no longer detectable nearby.
val msgBody = String(message.content)
msgAdapter.removeItem(msgBody)
}
}
}
override fun onDestroy() {
super.onDestroy()
// although the API should shutdown its processes when the client process dies,
// you may want to stop subscribing (and publishing if convenient)
Nearby.getMessagesClient(this).unpublish(message)
Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
private fun publish() {
val options = PublishOptions.Builder()
.setStrategy(PUB_SUB_STRATEGY)
.setCallback(object : PublishCallback() {
override fun onExpired() {
super.onExpired()
// flick the switch off since the publishing has expired.
// recall that we had set expiration time to 120 seconds
runOnUiThread {
binding.publishSwitch.isChecked = false
}
runOnUiThread() {
binding.publishSwitch.isChecked = false
}
}
}).build()
Nearby.getMessagesClient(this).publish(message, options)
}
private fun unpublish() {
Nearby.getMessagesClient(this).unpublish(message)
}
private fun subscribe() {
val options = SubscribeOptions.Builder()
.setStrategy(PUB_SUB_STRATEGY)
.setCallback(object : SubscribeCallback() {
override fun onExpired() {
super.onExpired()
runOnUiThread {
binding.subscribeSwitch.isChecked = false
}
}
}).build()
Nearby.getMessagesClient(this).subscribe(messageListener, options)
}
private fun unsubscribe() {
Nearby.getMessagesClient(this).unsubscribe(messageListener)
}
private fun setupMessagesDisplay() {
msgAdapter = MessageAdapter()
with(binding.nearbyMsgRecyclerView) {
layoutManager = LinearLayoutManager(context)
this.adapter = msgAdapter
}
}
class MessageAdapter : RecyclerView.Adapter<MessageAdapter.MessageVH>() {
private var itemsList: MutableList<String> = arrayListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageVH {
return MessageVH(TextView(parent.context))
}
override fun onBindViewHolder(holder: MessageVH, position: Int) {
holder.bind(getItem(position))
}
override fun getItemCount(): Int = itemsList.size
private fun getItem(pos: Int): String? = if (itemsList.isEmpty()) null else itemsList[pos]
fun addItem(item: String) {
itemsList.add(item)
notifyItemInserted(itemsList.size)
}
fun removeItem(item: String) {
val pos = itemsList.indexOf(item)
itemsList.remove(item)
notifyItemRemoved(pos)
}
inner class MessageVH(private val tv: TextView) : RecyclerView.ViewHolder(tv) {
fun bind(item: String?) {
item?.let { tv.text = it }
}
}
}
}
将 Google 提供的 API 密钥添加到清单文件中
Nearby Messages API 包含 Google 为您提供的服务器组件。当您发布消息后,Nearby Messages API 实际上会将消息发送到 Google 服务器,然后订阅者可以从此服务器查询消息。为了让 Google 能识别您的应用,您需要将 Google 提供的 API_KEY
添加到 Manifest.xml
文件中。之后我们就可以运行和操作应用了。
获取 API 密钥的过程分为三个步骤:
- 前往 Google 开发者控制台。
- 点击 + 创建凭据,然后选择 API 密钥*。*
- 复制已创建的 API 密钥并将其粘贴到您的 Android 项目的清单文件中。
在您的清单文件中,在 application 内添加 com.google.android.nearby.messages.API_KEY
元数据项。此文件内容应如下所示。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.nearbymessagesexample">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NearbyMessagesExample">
<meta-data
android:name="com.google.android.nearby.messages.API_KEY"
android:value="ADD_KEY_HERE" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
添加 API 密钥后,在两台或更多设备上运行此应用,以查看设备之间的通信情况。
5. 电池续航时间最佳做法
- 为了保护用户隐私并延长电池续航时间,您应该在用户离开包含必要功用的功能后立即停止发布和订阅。
- 您应使用“附近消息”功能在设备之间建立近程关系,而不是用于持续通信。持续通信时设备的电池电量消耗速度是正常情况的 2.5 到 3.5 倍。
6. 恭喜
恭喜!您现在已经了解如何使用 Nearby Messages API 在附近的设备之间发送和发现消息。
简言之,如要使用 Nearby Messages API,您需要为 play-services-nearby
添加依赖项,还需要从 Google 开发者控制台获取 API 密钥并将其添加到 Manifest.xml
文件中。此 API 需要连接互联网,以便发布者可以将他们的消息发送到 Google 服务器,供订阅者抓取。
- 您已了解如何发送消息
- 您已了解如何通过订阅发现消息
- 您已了解如何使用消息(在本例中,只是在
RecyclerView
中显示了消息)
后续操作
查看我们的博客系列和示例