Gadgets might not all the time be related to the web. Regardless of that, Close by Connections permits Android gadgets inside shut proximity to attach in a peer-to-peer vogue enabling the alternate of knowledge. This permits use circumstances resembling native multiplayer gaming, offline knowledge transfers and controlling an Android TV utilizing a telephone or pill.
Internally, Close by Connections combines and abstracts options, resembling Bluetooth and Wi-Fi, to create an easy-to-use API. Close by Connections allows/disables these options as wanted and restores the gadget to its earlier state as soon as the app isn’t utilizing the API anymore. This lets you focus in your particular area with out the fear of integrating advanced networking code.
On this tutorial, you’ll study:
- What Advertisers and Discoverers are.
- About promoting your telephone for Close by Connections.
- How one can set up a connection between an advertiser and a discoverer.
- How one can ship and obtain payloads.
Getting Began
All through this tutorial, you’ll work with a TicTacToe recreation. In a single gadget, a participant will host the match; in one other, a second participant will hook up with the host, and the sport will begin. The sport will let every participant know whose flip it’s.
Use the Obtain Supplies button on the high or backside of this tutorial to obtain the starter challenge.
Though you can run the starter challenge utilizing an emulator, later within the tutorial, you’ll want bodily gadgets as a result of, at present, Close by Connections API requires bodily gadgets to work.
As soon as downloaded, open the starter challenge in Android Studio 2021.2.1 or newer. Construct and run, and also you’ll see the next display screen:
You’ll see that you would be able to select to both host a match or uncover an current one. Nonetheless, it doesn’t really do both of these issues, you’re going to repair that.
![]() |
![]() |
Assessment the challenge to familiarize your self with the information:
- HomeScreen.kt: Let’s you select to host or uncover a recreation.
- WaitingScreen.kt: You’ll discover the app’s screens after selecting to host or uncover.
- GameScreen.kt: This incorporates screens associated to the sport.
- TicTacToe.kt: Fashions a TicTacToe recreation.
- TicTacToeRouter.kt: This lets you navigate between screens.
- TicTacToeViewModel.kt: This orchestrates the interactions between the screens, the sport, and later, with the Close by Connections shopper.
Setting Up Dependencies and Permissions
To make use of the Close by Connections API, you will need to first add a dependency. Open your app’s construct.gradle file and add the next dependency:
implementation 'com.google.android.gms:play-services-nearby:18.3.0'
Sync your challenge so Android Studio can obtain the dependency.
Now open your AndroidManifest.xml and add the next permissions:
<uses-permission android:identify="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:identify="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:identify="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:identify="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:identify="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<uses-permission android:identify="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:identify="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:identify="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:identify="android.permission.BLUETOOTH_SCAN" />
A few of these are harmful permissions, subsequently you’ll have to request person consent. Open MainActivity and assign REQUIRED_PERMISSIONS
contained in the companion object
as follows:
val REQUIRED_PERMISSIONS =
if (Construct.VERSION.SDK_INT >= Construct.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else if (Construct.VERSION.SDK_INT >= Construct.VERSION_CODES.Q) {
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
}
You’ll want these imports:
import android.Manifest
import android.os.Construct
The exercise already has the code to request these permissions from the person.
Now that you just’ve added the wanted dependency, you can begin utilizing the Close by Connections shopper that you just’ll have a look at within the subsequent part.
Getting the Connection Shopper
To get the shopper for Close by Connections, you’ll be able to merely name:
Close by.getConnectionsClient(context)
Since you’ll use it contained in the ViewModel, open TicTacToeViewModel and replace the constructor with the next:
class TicTacToeViewModel(personal val connectionsClient: ConnectionsClient)
Subsequent, open TicTacToeViewModelFactory and replace it like this:
class TicTacToeViewModelFactory(
personal val connectionsClient: ConnectionsClient
) : ViewModelProvider.Manufacturing facility {
override enjoyable <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TicTacToeViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return TicTacToeViewModel(connectionsClient) as T
...
For each information, you’ll have to import the next:
import com.google.android.gms.close by.connection.ConnectionsClient
Lastly, open MainActivity and modify the viewModel
property like this:
personal val viewModel: TicTacToeViewModel by viewModels {
TicTacToeViewModelFactory(Close by.getConnectionsClient(applicationContext))
}
Make certain to import the next:
import com.google.android.gms.close by.Close by
Now your ViewModel and related manufacturing facility courses have the ConnectionsClient
occasion supplied. You’re prepared to start out utilizing it and set up a connection!
Selecting a Technique
Now you’ll select a connection technique primarily based on how the gadgets want to attach.
Examine the next desk to grasp the options:
Technique | Request N outgoing connections | Obtain M incoming connections |
---|---|---|
P2P_CLUSTER | N=MANY | M=MANY |
P2P_STAR | N=1 | M=MANY |
P2P_POINT_TO_POINT | N=1 | M=1 |
You’ll use P2P_CLUSTER when a tool can each request outgoing connections to different gadgets and obtain incoming connections from different gadgets. When you want a star-shaped topology the place there’s a central internet hosting gadget, and the remaining will hook up with it, you’ll use P2P_STAR.
On this case, since you’ll join between two gadgets, you’ll use P2P_POINT_TO_POINT. Open TicTacToeViewModel and add the next fixed:
personal companion object {
...
val STRATEGY = Technique.P2P_POINT_TO_POINT
}
You’ll have to import:
import com.google.android.gms.close by.connection.Technique
It’s essential to notice that each Advertiser and Discoverer, which you’ll find out about later, have to make use of the identical technique.
To set the technique, replace startHosting()
with the next code:
enjoyable startHosting() {
Log.d(TAG, "Begin promoting...")
TicTacToeRouter.navigateTo(Display screen.Internet hosting)
val advertisingOptions = AdvertisingOptions.Builder().setStrategy(STRATEGY).construct()
}
This begins the promoting code and units the technique to P2P sort that you just outlined earlier. You’ll get again an choices variable that you just’ll use later to arrange the promoting connection.
Additionally, replace startDiscovering()
with the next:
enjoyable startDiscovering() {
Log.d(TAG, "Begin discovering...")
TicTacToeRouter.navigateTo(Display screen.Discovering)
val discoveryOptions = DiscoveryOptions.Builder().setStrategy(STRATEGY).construct()
}
Just like the promoting code, this units up the invention choices to make use of the identical P2P technique.
Within the following sections, you’ll study what Advertisers and Discoverers are and the way they alternate knowledge.
Making ready Your Gadgets
To begin exchanging knowledge between two gadgets, one in every of them, the Advertiser, has to promote itself in order that the opposite gadget, the Discoverer, can request a connection.
Promoting
To begin promoting, replace startHosting()
with the next:
enjoyable startHosting() {
Log.d(TAG, "Begin promoting...")
TicTacToeRouter.navigateTo(Display screen.Internet hosting)
val advertisingOptions = AdvertisingOptions.Builder().setStrategy(STRATEGY).construct()
// 1
connectionsClient.startAdvertising(
localUsername, // 2
BuildConfig.APPLICATION_ID, // 3
connectionLifecycleCallback, // 4
advertisingOptions // 5
).addOnSuccessListener {
// 6
Log.d(TAG, "Promoting...")
localPlayer = 1
opponentPlayer = 2
}.addOnFailureListener {
// 7
Log.d(TAG, "Unable to start out promoting")
TicTacToeRouter.navigateTo(Display screen.House)
}
}
Let’s see what’s happening right here:
- Name
startAdvertising()
on the shopper. - That you must go an area endpoint identify.
- You set
BuildConfig.APPLICATION_ID
for service ID since you desire a Discoverer to search out you with this distinctive id. - Calls to the
connectionLifecycleCallback
strategies happen when establishing a reference to a Discoverer. - You go the choices containing the technique beforehand configured.
- As soon as the shopper efficiently begins promoting, you set the native participant as participant 1, and the opponent will probably be participant 2.
- If the shopper fails to promote, it logs to the console and returns to the house display screen.
These are the imports you want:
import com.google.android.gms.close by.connection.AdvertisingOptions
import com.yourcompany.android.tictactoe.BuildConfig
Add a property named connectionLifecycleCallback
with the next content material:
personal val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
override enjoyable onConnectionInitiated(endpointId: String, data: ConnectionInfo) {
Log.d(TAG, "onConnectionInitiated")
}
override enjoyable onConnectionResult(endpointId: String, decision: ConnectionResolution) {
Log.d(TAG, "onConnectionResult")
when (decision.standing.statusCode) {
ConnectionsStatusCodes.STATUS_OK -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
}
ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED")
}
ConnectionsStatusCodes.STATUS_ERROR -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_ERROR")
}
else -> {
Log.d(TAG, "Unknown standing code ${decision.standing.statusCode}")
}
}
}
override enjoyable onDisconnected(endpointId: String) {
Log.d(TAG, "onDisconnected")
}
}
When a Discoverer requests a connection, the Advertiser’s ConnectionLifecycleCallback.onConnectionInitiated()
will fireplace. In a later part, you’ll add code to this methodology callback. When there’s a connection change that happens, the ConnectionLifecycleCallback.onConnectionResult()
fires. You’ll deal with three particular connection standing sorts: OK, rejected and error. There’s additionally a catch-all for the some other unknown standing code that’s returned.
You’ll want the next imports:
import com.google.android.gms.close by.connection.ConnectionLifecycleCallback
import com.google.android.gms.close by.connection.ConnectionInfo
import com.google.android.gms.close by.connection.ConnectionResolution
import com.google.android.gms.close by.connection.ConnectionsStatusCodes
Discovering
The Discoverer is the gadget that wishes to find an Advertiser to request a connection.
To begin discovering, replace the next methodology:
enjoyable startDiscovering() {
Log.d(TAG, "Begin discovering...")
TicTacToeRouter.navigateTo(Display screen.Discovering)
val discoveryOptions = DiscoveryOptions.Builder().setStrategy(STRATEGY).construct()
// 1
connectionsClient.startDiscovery(
BuildConfig.APPLICATION_ID, // 2
endpointDiscoveryCallback, // 3
discoveryOptions // 4
).addOnSuccessListener {
// 5
Log.d(TAG, "Discovering...")
localPlayer = 2
opponentPlayer = 1
}.addOnFailureListener {
// 6
Log.d(TAG, "Unable to start out discovering")
TicTacToeRouter.navigateTo(Display screen.House)
}
}
That is what’s happening:
- You name
startDiscovery()
on the shopper. - You set
BuildConfig.APPLICATION_ID
for service ID since you wish to discover an Advertiser this distinctive ID. - Calls to the
endpointDiscoveryCallback
strategies happen when establishing a reference to an Advertiser. - You go the choices containing the technique beforehand configured.
- As soon as the shopper efficiently begins discovering you set the native participant as participant 2, the opponent will probably be participant 1.
- If the shopper fails to find, it logs to the console and returns to the house display screen.
Add this import:
import com.google.android.gms.close by.connection.DiscoveryOptions
Add a property named endpointDiscoveryCallback
with the next content material:
personal val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
override enjoyable onEndpointFound(endpointId: String, data: DiscoveredEndpointInfo) {
Log.d(TAG, "onEndpointFound")
}
override enjoyable onEndpointLost(endpointId: String) {
Log.d(TAG, "onEndpointLost")
}
}
You additionally have to import these:
import com.google.android.gms.close by.connection.EndpointDiscoveryCallback
import com.google.android.gms.close by.connection.DiscoveredEndpointInfo
When a Discoverer finds an Advertiser, the Discoverer’s EndpointDiscoveryCallback.onEndpointFound()
will probably be referred to as. You’ll add code to this methodology callback within the following part.
Establishing a Connection
After discovering an Advertiser, the Discoverer has to request a connection. Replace EndpointDiscoveryCallback.onEndpointFound()
with the next code:
override enjoyable onEndpointFound(endpointId: String, data: DiscoveredEndpointInfo) {
Log.d(TAG, "onEndpointFound")
Log.d(TAG, "Requesting connection...")
// 1
connectionsClient.requestConnection(
localUsername, // 2
endpointId, // 3
connectionLifecycleCallback // 4
).addOnSuccessListener {
// 5
Log.d(TAG, "Efficiently requested a connection")
}.addOnFailureListener {
// 6
Log.d(TAG, "Didn't request the connection")
}
}
Let’s assessment step-by-step:
- You name
requestConnection()
on the shopper. - That you must go an area endpoint identify.
- Cross the
endpointId
you’ve simply discovered. - Calls to the
connectionLifecycleCallback
strategies happen later when the connection initiates with the Advertiser. - As soon as the shopper efficiently requests a connection, it logs to the console.
- If the shopper fails, it logs to the console.
The Advertiser and Discoverer want to simply accept the connection, each will get notified through ConnectionLifecycleCallback.onConnectionInitiated()
, so replace the code with this:
override enjoyable onConnectionInitiated(endpointId: String, data: ConnectionInfo) {
Log.d(TAG, "onConnectionInitiated")
Log.d(TAG, "Accepting connection...")
connectionsClient.acceptConnection(endpointId, payloadCallback)
}
That you must present a payloadCallback
, which incorporates strategies that’ll execute later when the gadgets alternate knowledge. For now, simply create a property with the next content material:
personal val payloadCallback: PayloadCallback = object : PayloadCallback() {
override enjoyable onPayloadReceived(endpointId: String, payload: Payload) {
Log.d(TAG, "onPayloadReceived")
}
override enjoyable onPayloadTransferUpdate(endpointId: String, replace: PayloadTransferUpdate) {
Log.d(TAG, "onPayloadTransferUpdate")
}
}
That you must import these:
import com.google.android.gms.close by.connection.PayloadCallback
import com.google.android.gms.close by.connection.Payload
import com.google.android.gms.close by.connection.PayloadTransferUpdate
After accepting, ConnectionLifecycleCallback.onConnectionResult()
notifies either side of the brand new connection. Replace its code to the next:
override enjoyable onConnectionResult(endpointId: String, decision: ConnectionResolution) {
Log.d(TAG, "onConnectionResult")
when (decision.standing.statusCode) {
ConnectionsStatusCodes.STATUS_OK -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
opponentEndpointId = endpointId
Log.d(TAG, "opponentEndpointId: $opponentEndpointId")
newGame()
TicTacToeRouter.navigateTo(Display screen.Recreation)
}
...
If the standing code is STATUS_OK
, you save the opponentEndpointId
to ship payloads later. Now you’ll be able to navigate to the sport display screen to start out enjoying!
Construct and run the appliance on two bodily gadgets, click on Host on one in every of them and Uncover on the opposite one. After a couple of seconds, it’s best to see the sport board on every gadget:
![]() |
![]() |
Utilizing a Payload
Sending
That you must ship the participant place to the opposite gadget everytime you make a transfer. Modify sendPosition()
with the next code:
personal enjoyable sendPosition(place: Pair<Int, Int>) {
Log.d(TAG, "Sending [${position.first},${position.second}] to $opponentEndpointId")
connectionsClient.sendPayload(
opponentEndpointId,
place.toPayLoad()
)
}
Right here, you’re utilizing the opponentEndpointId
you beforehand saved to ship the place. That you must convert the place, which is a Pair
to a Payload
object. To do this, add the next extension to the top of the file:
enjoyable Pair<Int, Int>.toPayLoad() = Payload.fromBytes("$first,$second".toByteArray(UTF_8))
Import this:
import kotlin.textual content.Charsets.UTF_8
You’ve now transformed the pair right into a comma separated string which is transformed to a ByteArray
that’s lastly used to create a Payload
.
Receiving
To obtain this payload, replace the PayloadCallback.onPayloadReceived()
with this:
override enjoyable onPayloadReceived(endpointId: String, payload: Payload) {
Log.d(TAG, "onPayloadReceived")
// 1
if (payload.sort == Payload.Kind.BYTES) {
// 2
val place = payload.toPosition()
Log.d(TAG, "Acquired [${position.first},${position.second}] from $endpointId")
// 3
play(opponentPlayer, place)
}
}
That is what’s happening:
- You examine if the payload sort is
BYTES
. - You exchange again the
Payload
to a placePair
object. - Instruct the sport that the opponent has performed this place.
Add the extension to transform a Payload
to a Pair
place to the top of the file:
enjoyable Payload.toPosition(): Pair<Int, Int> {
val positionStr = String(asBytes()!!, UTF_8)
val positionArray = positionStr.cut up(",")
return positionArray[0].toInt() to positionArray[1].toInt()
}
Construct and run the appliance on two gadgets and begin enjoying!
Clearing Connections
When the Advertiser and Discoverer have discovered one another, it’s best to cease promoting and discovering. Add the next code to the ConnectionLifecycleCallback.onConnectionResult()
:
override enjoyable onConnectionResult(endpointId: String, decision: ConnectionResolution) {
Log.d(TAG, "onConnectionResult")
when (decision.standing.statusCode) {
ConnectionsStatusCodes.STATUS_OK -> {
Log.d(TAG, "ConnectionsStatusCodes.STATUS_OK")
connectionsClient.stopAdvertising()
connectionsClient.stopDiscovery()
...
That you must disconnect the shopper each time one participant decides to exit the sport. Add the next to make sure the shopper is stopped each time the ViewModel is destroyed:
override enjoyable onCleared() {
stopClient()
tremendous.onCleared()
}
Replace goToHome()
as follows:
enjoyable goToHome() {
stopClient()
TicTacToeRouter.navigateTo(Display screen.House)
}
Add the code for stopClient()
as follows:
personal enjoyable stopClient() {
Log.d(TAG, "Cease promoting, discovering, all endpoints")
connectionsClient.stopAdvertising()
connectionsClient.stopDiscovery()
connectionsClient.stopAllEndpoints()
localPlayer = 0
opponentPlayer = 0
opponentEndpointId = ""
}
Right here you’re additionally calling stopAllEndpoints()
which is able to make sure the disconnection of the shopper.
If you wish to disconnect from a selected endpoint you need to use disconnectFromEndpoint(endpointId)
.
Lastly, each time an Advertiser or Discoverer executes stopAllEndpoints()
(or disconnectFromEndpoint(endpointId)
) the counterpart will probably be notified through ConnectionLifecycleCallback.onDisconnected()
, so replace it as follows:
override enjoyable onDisconnected(endpointId: String) {
Log.d(TAG, "onDisconnected")
goToHome()
}
Construct and run the app on each gadgets. Begin a brand new recreation and press the again button on any gadget. You’ll discover that the sport ends on each gadgets and takes you again to the house display screen.
Congratulations! You’ve simply discovered the fundamentals of Android’s Close by Connections API.
The place to Go From Right here?
You’ll be able to obtain the ultimate model of the challenge utilizing the Obtain Supplies button on the high or backside of this tutorial.
As a problem, you’ll be able to let extra gamers hook up with the host and use a much bigger board, the TicTacToe mannequin already permits that. Listed here are a couple of ideas that may assist you:
- You’ll want to decide on one other connection technique.
- Let the host resolve the board measurement.
- As a result of the host doesn’t know what number of opponents will join beforehand, you’ll have to set the native and opponent participant numbers when the sport begins.
- As an alternative of instantly beginning the sport each time an opponent joins, watch for a couple of to affix and let the host resolve when to start out.
- You’ll want a brand new payload to sign to all of the opponents that the host has began a brand new recreation.
You could find the answer within the supplies.
Listed here are some nice references to study extra concerning the topic:
- You could find the official documentation right here.
- Right here you’ll discover the API reference.
- When you appreciated this, you may wish to construct a ‘rock, paper and scissors’ multiplayer recreation, simply comply with alongside this codelab.
- For different Close by use circumstances, examine this weblog sequence.
Be at liberty to share your suggestions and findings or ask any questions within the feedback under or within the boards. We hope you loved this tutorial!