Location Sharing With Custom Attachments on Android

Stream Chat allows you to add custom data to many of its API’s objects. Let’s take a look at a use case for adding custom data to attachments.

Stream's Android Chat SDK supports sending custom attachments with messages. In this tutorial, you'll learn how to send location data as a custom attachment.

Note: This tutorial assumes you already know the basics of the Stream API. To get started, check out the Android In-App Messaging Tutorial, and take a look at the Android SDK on GitHub.

Getting the Current Location

Before you send your attachment, first you'll need to get the current location of the user. The implementation for getting the current location is already set up, and you can see it in the sample project for this tutorial.

There's the LocationUtils file which has a method with extends the FusedLocationProviderClient class. The extension method returns a callbackFlow with the location data.

To get the location you can collect the results in your Activity as shown below:

1lifecycleScope.launch {
2    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
3        fusedLocationClient.locationFlow().collect {
4            currentLocation = LatLng(it.latitude, it.longitude)
5        }
6    }
7}

Here, you're getting location from FusedLocationProviderClient as a Flow, using the locationFlow() extension method. You're also collecting the results in a safe way using the Lifecycle methods.

You now have the user's current location. In the next section, you'll see how to add location coordinates as custom attachments.

API key setup

To be able to load a map, you'll need to have your project on the Google Maps Platform console. From the console, you can get an API Key for your app which enables your app to access all map functionalities. Read more about this in the official documentation.

Once you have the API Key, you can add it in the local.properties file as:

1googleMapsKey="YOUR_API_KEY"

Adding Location as a Custom Attachment

To add the location to your custom attachment, you need to create an Attachment object as shown below.

1// 1
2val attachment = Attachment(
3    type = "location",
4    extraData = mutableMapOf("latitude" to currentLocation.latitude, "longitude" to currentLocation.longitude),
5)
6
7// 2
8val message = Message(
9    cid = channelId,
10    text = "My current location",
11    attachments = mutableListOf(attachment),
12)

To explain what the code above does:

  1. Here, you're creating an attachment with the custom location type. You'll use this key later to recognize and display attachments like this. You're also passing in the latitude and longitude from your location coordinates using the extraData parameter which allows you to add arbitrary key-value pairs to an attachment.
  2. You're adding your location attachment to a new Message using the attachments property.

With this, you can send your message with this custom attachment.

Adding A Map Preview

The Android SDK renders previews for attachments like images and files by default. For custom attachments, you'll override the AttachmentViewFactory class, which allows you to create and render your custom view for attachments.

First, create a layout for your custom attachment:

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3    xmlns:app="http://schemas.android.com/apk/res-auto"
4    android:layout_width="match_parent"
5    android:layout_height="match_parent">
6
7    <com.google.android.gms.maps.MapView
8        android:id="@+id/mapView"
9        android:layout_width="match_parent"
10        android:layout_height="200dp"
11        app:layout_constraintStart_toStartOf="parent"
12        app:layout_constraintEnd_toEndOf="parent"
13        app:layout_constraintTop_toTopOf="parent"/>
14
15</androidx.constraintlayout.widget.ConstraintLayout>

The layout has a MapView for displaying the location on the map.

Next you'll create the LocationAttachmentViewFactory which extends AttachmentViewFactory. This is how the class looks like:

1class LocationAttachmentViewFactory(
2    private val lifecycleOwner: LifecycleOwner
3): AttachmentViewFactory() {
4    // 1
5    override fun createAttachmentView(
6        data: MessageListItem.MessageItem,
7        listeners: MessageListListenerContainer,
8        style: MessageListItemStyle,
9        parent: ViewGroup
10    ): View {
11        // 2
12        val location = data.message.attachments.find { it.type == "location" }
13        return if (location != null) {
14            val lat = location.extraData["latitude"] as Double
15            val long = location.extraData["longitude"] as Double
16            val latLng = LatLng(lat, long)
17            // 3
18            createLocationView(parent, latLng)
19        } else {
20            super.createAttachmentView(data, listeners, style, parent)
21        }
22    }
23
24    private fun createLocationView(parent: ViewGroup, location: LatLng): View {
25        val binding = LocationAttachementViewBinding
26            .inflate(LayoutInflater.from(parent.context), parent, false)
27
28        // 4
29        val mapView = binding.mapView
30        mapView.onCreate(Bundle())
31        // 5
32        mapView.getMapAsync { googleMap ->
33            googleMap.setMinZoomPreference(18f)
34            googleMap.moveCamera(CameraUpdateFactory.newLatLng(location))
35        }
36
37        // 6
38        lifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
39            @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
40            fun destroyMapView(){
41                mapView.onDestroy()
42            }
43
44            @OnLifecycleEvent(Lifecycle.Event.ON_START)
45            fun startMapView(){
46               mapView.onStart()
47            }
48
49            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
50            fun resumeMapView(){
51                mapView.onResume()
52            }
53
54            @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
55            fun stopMapView(){
56                mapView.onStop()
57            }
58        })
59
60        return binding.root
61    }
62
63}

Here's a breakdown of all the code above:

  1. This the method responsible for rendering all attachment UI. In this method, you only need to change the UI for attachments that have a location. The other attachments remain unchanged, and can use the default implementation.
  2. Here, you're getting the location data that you passed on your message using the locationkey that you defined earlier.
  3. You're calling the createLocationView which is responsible for inflating the view.
  4. Here you're initializing the mapView.
  5. You're calling the getMapAsync() method. This method sets a callback object which is triggered when the GoogleMap instance is ready for use. When that's invoked, you're updating the map with the a zoom level and moving it to the location you added to your attachment.
  6. You're adding a LifecycleObserver. This is for calling the different MapView lifecycle methods depending on the lifecycle state of LocationAttachmentViewFactory. For example you're supposed to destoy the MapView when the view has been destoyed. You achieve this by calling mapView.onDestroy() when you receive the ON_DESTROY lifecycle event.

With the custom factory created, you now need to set up MessageListView to use it. You do this as shown in the code below:

1binding.messageListView.setAttachmentViewFactory(LocationAttachmentViewFactory(lifecycleOwner = this))

You pass the Activity as the lifecycleOwner for the LocationAttachmentViewFactory. This hooks the map behaviours to the lifecycle of the current Activity.

With this, your app is ready to send and also preview custom location attachments. For the project, the action button for sending the location is on the options menu as shown in the image below.

You'll use the menu options to send the user's current location from the app to Stream Android Chat SDK. Once you tap on the location icon at the top right, it sends a message with the text: "My Current location" (the Message object prepared earlier).

As seen from the image above, the attachment shows a map and TextView. The map shows the location of the coordinates sent as custom attachments.

Conclusion

You've seen how easy it is to add a location as a custom attachment. You can now enrich your chat with location sharing.

The the full sample project with examples in this tutorial on GitHub.

You could take this idea further from here to implement live, continous location sharing as well, by editing the already sent message as the location of your device is updated. This would require some additional bookkeeping and lifecycle management, but Stream's Chat SDK supports it with its editing and realtime notification features.

You can learn more about the Android SDK by checking out its GitHub repository, and by taking a look at the documentation.

You can also go through the Message List View Custom Attachments sections that explain more about custom attachments.