dependencies {
// Stream Chat SDK
implementation("io.getstream:stream-chat-android-compose:$stream_version")
implementation("io.getstream:stream-chat-android-offline:$stream_version")
// Google Play Services for location
implementation("com.google.android.gms:play-services-location:$play_services_version")
}
Location Sharing
Introduction
Stream’s Chat SDK allows users to share their location with other members of a channel.
This guide demonstrates how to implement a location-sharing feature where users can send their location to a chat channel and display it using a custom attachment component with maps.
Location Picker | Static Location | Live Location |
---|---|---|
![]() | ![]() | ![]() |
The implementation uses Jetpack Compose for the UI and the Stream Chat Android SDK for chat functionality.
Prerequisites
- Stream Chat SDK: Ensure you have integrated the Stream Chat Compose SDK into your project. Follow the Compose In-App Messaging Tutorial for setup instructions.
- Permissions: Add location permissions to your app.
- Dependencies: Include the necessary dependencies for location services.
1: Set Up Dependencies
Add the following dependencies to your app/build.gradle file to enable Stream Chat and location services:
Replace $stream_version
with the latest version from the Stream Chat Android GitHub releases.
Sync your project to ensure dependencies are resolved.
2: Request Location Permissions
To access the user’s location, add the following permissions to your AndroidManifest.xml
:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Ensure permissions are requested before accessing location services. Check out more about location permissions in the Android documentation.
3: Send Device Location Updates
To send device location updates, create a simple class that handles location updates and sends them to the chat channel.
This class will use the FusedLocationProviderClient
to get location updates and the ChatClient.updateLiveLocation
to update a started live location message.
/**
* Consider using a foreground service for continuous location updates while the app is in the background.
*/
class SharedLocationService(private val context: Context) : LocationCallback() {
private val locationClient = LocationServices.getFusedLocationProviderClient(context)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val chatClient by lazy { ChatClient.instance() }
@Volatile
private var activeLiveLocations: List<Location> = emptyList()
// Track whether we are currently receiving updates
@Volatile
private var isReceivingUpdates = false
var currentDeviceId: String = UnknownDeviceId
private set
// Call this when the user connects to the client
fun start() {
scope.launch { currentDeviceId = /* Get current device if logic */ }
// Fetch user's active live locations
chatClient.queryActiveLocations().enqueue()
// Listen for changes in current user's active live locations
chatClient.globalStateFlow
.flatMapLatest { it.currentUserActiveLiveLocations }
.onEach { userActiveLiveLocations ->
activeLiveLocations = userActiveLiveLocations.toList()
if (userActiveLiveLocations.isEmpty()) {
// No active locations, stop receiving updates
locationClient.removeLocationUpdates(this)
isReceivingUpdates = false
} else {
if (!isReceivingUpdates) {
// If we are not already receiving updates, request location updates
val hasPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (hasPermission) {
val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
5000L, // Location updates internal in millis
).build()
locationClient.requestLocationUpdates(request, this, Looper.getMainLooper())
isReceivingUpdates = true
} else {
// Log or handle the case where location permission is not granted
}
} else {
// Already receiving updates, no need to request again
}
}
}
.launchIn(scope)
}
override fun onLocationResult(result: LocationResult) {
val locationsToUpdate = activeLiveLocations
// Check if we have any active live locations to update
for (deviceLocation in result.locations) {
locationsToUpdate
.filterNot { it.endAt?.before(Date()) ?: false } // Filter out expired locations
.forEach { activeLiveLocation ->
chatClient.updateLiveLocation(
messageId = activeLiveLocation.messageId,
latitude = deviceLocation.latitude,
longitude = deviceLocation.longitude,
deviceId = currentDeviceId,
).enqueue { /* Handle success or error */ }
}
}
}
// Call this when the user disconnects from the client
fun stop() {
// Stop receiving location updates
locationClient.removeLocationUpdates(this)
scope.cancel()
}
}
Check out SharedLocationService for a complete implementation.
4: Add Location Picker
To allow users to select a location, you can create a location picker dialog or screen. This example uses a custom attachment picker tab.
internal class LocationPickerTabFactory(
private val viewModelFactory: SharedLocationViewModelFactory,
) : AttachmentsPickerTabFactory {
override val attachmentsPickerMode: AttachmentsPickerMode =
CustomPickerMode()
override fun isPickerTabEnabled(channel: Channel): Boolean =
channel.config.sharedLocationsEnabled
@Composable
override fun PickerTabIcon(isEnabled: Boolean, isSelected: Boolean) {
Icon(
imageVector = Icons.Rounded.ShareLocation,
contentDescription = "Share Location",
tint = when {
isEnabled -> ChatTheme.colors.textLowEmphasis
else -> ChatTheme.colors.disabled
},
)
}
@Composable
override fun PickerTabContent(
onAttachmentPickerAction: (AttachmentPickerAction) -> Unit,
attachments: List<AttachmentPickerItemState>,
onAttachmentsChanged: (List<AttachmentPickerItemState>) -> Unit,
onAttachmentItemSelected: (AttachmentPickerItemState) -> Unit,
onAttachmentsSubmitted: (List<AttachmentMetaData>) -> Unit,
) {
LocationPicker(
viewModelFactory = viewModelFactory,
onDismiss = { onAttachmentPickerAction(AttachmentPickerBack) },
)
}
}
Then set up the LocationPickerTabFactory
in the ChatTheme
:
val attachmentsPickerTabFactories = AttachmentsPickerTabFactories.defaultFactories() +
LocationPickerTabFactory(viewModelFactory = SharedLocationViewModelFactory(cid))
ChatTheme(
attachmentsPickerTabFactories = attachmentsPickerTabFactories,
)
LocationPicker
uses SharedLocationViewModel
to handle the following:
- Send a static location through the
ChatClient.sendStaticLocation
function when the user clicks the “Send Current Location” button. - Start a live location through the
ChatClient.startLiveLocationSharing
function when the user clicks the “Share Live Location” button.
LocationPicker
renders a map using Leaflet.js through a WebView
.
Check out LocationPickerTabFactory, LocationPicker, and SharedLocationViewModel for a complete implementation.
5: Display Shared Locations
To display shared locations in the chat, you can override the MessageItemCenterContent
component factory and create a custom component that renders the location on a map.
class CustomChatComponentFactory(
private val delegate: ChatComponentFactory = object : ChatComponentFactory {},
) : ChatComponentFactory by deltegate {
@Composable
override fun ColumnScope.MessageItemCenterContent(
messageItem: MessageItemState,
onLongItemClick: (Message) -> Unit,
onPollUpdated: (Message, Poll) -> Unit,
onCastVote: (Message, Poll, Option) -> Unit,
onRemoveVote: (Message, Poll, Vote) -> Unit,
selectPoll: (Message, Poll, PollSelectionType) -> Unit,
onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit,
onClosePoll: (String) -> Unit,
onAddPollOption: (poll: Poll, option: String) -> Unit,
onGiphyActionClick: (GiphyAction) -> Unit,
onQuotedMessageClick: (Message) -> Unit,
onLinkClick: ((Message, String) -> Unit)?,
onUserMentionClick: (User) -> Unit,
onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit,
) {
val message = messageItem.message
// Check if the message has a shared location and is not deleted
if (message.hasSharedLocation() && !message.isDeleted()) {
val location = requireNotNull(message.sharedLocation)
SharedLocationItem(
modifier = Modifier.widthIn(max = ChatTheme.dimens.messageItemMaxWidth),
message = message,
location = location,
onMapClick = { url -> onLinkClick?.invoke(message, url) },
onMapLongClick = { onLongItemClick(message) },
)
} else {
// Fallback to the default implementation
with(delegate) {
MessageItemCenterContent(
messageItem = messageItem,
onLongItemClick = onLongItemClick,
onGiphyActionClick = onGiphyActionClick,
onQuotedMessageClick = onQuotedMessageClick,
onLinkClick = onLinkClick,
onUserMentionClick = onUserMentionClick,
onMediaGalleryPreviewResult = onMediaGalleryPreviewResult,
onPollUpdated = onPollUpdated,
onCastVote = onCastVote,
onRemoveVote = onRemoveVote,
selectPoll = selectPoll,
onAddAnswer = onAddAnswer,
onClosePoll = onClosePoll,
onAddPollOption = onAddPollOption,
)
}
}
}
}
When the shared location has an endAt
date, it is considered a live location sharing.
In this guide, stoping a live location sharing is handled in the SharedLocationViewModel
by calling the ChatClient.stopLiveLocationSharing
function.
Then set up the CustomChatComponentFactory
in the ChatTheme
:
val chatComponentFactory = CustomChatComponentFactory()
ChatTheme(
chatComponentFactory = chatComponentFactory,
)
Learn more about Component Factory.
Check out SharedLocationItem for a complete implementation.
Summary
In this guide, you learned how to implement a location-sharing feature in your Stream Chat application using Jetpack Compose. You set up dependencies, requested location permissions, sent device location updates, added a location picker, and displayed shared locations in the chat.