Skip to content

Commit f9fd021

Browse files
authored
feat: Improved Place Details Compose sample (#964)
* * Enhance map screen with live location and improved UX * Implement immersive mode and enhance map interaction Key changes: - **Live Location Tracking**: - **Improved Map Interaction & UX**: - When a place is selected, a `Circle` is now drawn on the map to visually highlight its location. - The camera animates to a position slightly offset to the south of the selected place, preventing the details view from obscuring the marker. - The app now supports an edge-to-edge display by setting a transparent status bar. - **Architecture & Initialization**: - A custom `PlaceDetailsComposeApplication` class has been added to handle application-wide setup. - The application now performs a startup check to ensure the Google Maps API key is correctly configured, throwing a `RuntimeException` if it's missing or invalid. - Google Maps initialization is now handled explicitly in `MainActivity`, displaying a progress indicator until the map is ready. - **Dependency Updates**: - Upgraded several dependencies, including Places SDK to `5.0.0`, Maps Compose to `6.12.1`, and various Kotlin and Jetpack Compose libraries. - Added new dependencies for `maps-compose-widgets`, `maps-utils-ktx`, and `material-icons-extended` to support the new features. * fix: fix deprecated places fields * chore: Fix typos and update map ID placeholder This commit addresses several minor issues and inconsistencies across the projects. Key changes: - **Gradle Wrapper**: Corrected a typo in the `distributionSha256Sum` by removing a trailing 'n' in the `gradle-wrapper.properties` files for all modules. - **Map ID Placeholder**: Updated the placeholder value for the map ID from `UNSET_MAP_ID` to `YOUR_MAP_ID`. This change is reflected in: - `PlaceDetailsCompose/app/src/main/res/values/strings.xml` - `PlaceDetailsCompose/local.defaults.properties` - The validation logic within `PlaceDetailsComposeApplication.kt`. - **Code Style**: Renamed the `Context` variable from `ctx` to `context` in `PlaceDetailsView.kt` for improved clarity. * chore: update gradle-wrapper.jar files\nAdd missing POST_NOTIFICATIONS permission
1 parent 32f2c9f commit f9fd021

37 files changed

+1075
-578
lines changed

PlaceDetailsCompose/app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ dependencies {
124124
implementation(libs.places) // The SDK for the Places UI Kit (PlaceDetails fragments).
125125
implementation(libs.play.services.location) // Needed for the FusedLocationProviderClient to get the device's location.
126126
implementation(libs.maps.compose)
127+
implementation(libs.maps.compose.widgets)
128+
implementation(libs.maps.utils.ktx)
127129
implementation(libs.material) // For Material Design components (used in XML layouts).
128130

129131
// --- Jetpack Compose ---
@@ -132,6 +134,7 @@ dependencies {
132134
implementation(platform(libs.androidx.compose.bom)) // The Compose Bill of Materials (BOM) ensures all Compose libraries use compatible versions.
133135
implementation(libs.androidx.ui.tooling.preview) // For displaying @Preview composables in Android Studio.
134136
implementation(libs.androidx.ui.viewbinding)
137+
implementation(libs.androidx.material.icons.extended)
135138
debugImplementation(libs.androidx.ui.tooling) // Provides tools for inspecting Compose UIs.
136139

137140
// --- Testing Libraries ---

PlaceDetailsCompose/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2020

2121
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
2222
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
23+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
2324

2425
<application
26+
android:name=".PlaceDetailsComposeApplication"
2527
android:allowBackup="true"
2628
android:dataExtractionRules="@xml/data_extraction_rules"
2729
android:fullBackupContent="@xml/backup_rules"

PlaceDetailsCompose/app/src/main/java/com/example/placedetailscompose/MainActivity.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,23 @@ import android.widget.Toast
2020
import androidx.activity.compose.setContent
2121
import androidx.activity.enableEdgeToEdge
2222
import androidx.appcompat.app.AppCompatActivity
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.fillMaxSize
25+
import androidx.compose.material3.CircularProgressIndicator
26+
import androidx.compose.runtime.LaunchedEffect
27+
import androidx.compose.runtime.SideEffect
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.ui.Alignment
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.platform.LocalContext
32+
import androidx.core.view.WindowCompat
33+
import androidx.core.view.WindowInsetsCompat
34+
import androidx.core.view.WindowInsetsControllerCompat
2335
import com.example.placedetailscompose.ui.map.MapScreen
2436
import com.example.placedetailscompose.ui.theme.PlaceDetailsComposeTheme
2537
import com.google.android.libraries.places.api.Places
38+
import com.google.maps.android.compose.internal.InitializationState
39+
import com.google.maps.android.compose.internal.LocalGoogleMapsInitializer
2640

2741
class MainActivity : AppCompatActivity() {
2842
override fun onCreate(savedInstanceState: Bundle?) {
@@ -48,8 +62,35 @@ class MainActivity : AppCompatActivity() {
4862

4963
enableEdgeToEdge()
5064
setContent {
65+
val googleMapsInitializer = LocalGoogleMapsInitializer.current
66+
val initializationState by googleMapsInitializer.state
67+
val window = this.window
68+
val insetsController = WindowCompat.getInsetsController(window, window.decorView)
69+
70+
SideEffect {
71+
insetsController.hide(WindowInsetsCompat.Type.systemBars())
72+
insetsController.systemBarsBehavior =
73+
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
74+
}
75+
76+
if (initializationState != InitializationState.SUCCESS) {
77+
val context = LocalContext.current
78+
LaunchedEffect(Unit) {
79+
googleMapsInitializer.initialize(context)
80+
}
81+
}
82+
5183
PlaceDetailsComposeTheme {
52-
MapScreen()
84+
if (initializationState == InitializationState.SUCCESS) {
85+
MapScreen()
86+
} else {
87+
Box(
88+
modifier = Modifier.fillMaxSize(),
89+
contentAlignment = Alignment.Center
90+
) {
91+
CircularProgressIndicator()
92+
}
93+
}
5394
}
5495
}
5596
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.placedetailscompose
18+
19+
import android.app.Application
20+
import android.content.pm.PackageManager
21+
import android.util.Log
22+
import android.widget.Toast
23+
import java.util.Objects
24+
import kotlin.text.isBlank
25+
26+
/**
27+
* `PlaceDetailsComposeApplication` is a custom Application class.
28+
*
29+
* This class is responsible for application-wide initialization and setup,
30+
* such as checking for the presence and validity of the API key during the
31+
* application's startup.
32+
*
33+
* It extends the [Application] class and overrides the [.onCreate]
34+
* method to perform these initialization tasks.
35+
*/
36+
class PlaceDetailsComposeApplication : Application() {
37+
38+
override fun onCreate() {
39+
super.onCreate()
40+
checkApiKey()
41+
}
42+
43+
/**
44+
* Checks if the API key for Google Maps is properly configured in the application's metadata.
45+
*
46+
* This method retrieves the API key from the application's metadata, specifically looking for
47+
* a string value associated with the key "com.google.android.geo.API_KEY".
48+
* The key must be present, not blank, and not set to the placeholder value "DEFAULT_API_KEY".
49+
*
50+
* If any of these checks fail, a Toast message is displayed indicating that the API key is missing or
51+
* incorrectly configured, and a RuntimeException is thrown.
52+
*/
53+
private fun checkApiKey() {
54+
try {
55+
val appInfo =
56+
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
57+
val bundle = Objects.requireNonNull(appInfo.metaData)
58+
59+
val apiKey =
60+
bundle.getString("com.google.android.geo.API_KEY") // Key name is important!
61+
62+
if (apiKey == null || apiKey.isBlank() || apiKey == "DEFAULT_API_KEY") {
63+
Toast.makeText(
64+
this,
65+
"API Key was not set in secrets.properties",
66+
Toast.LENGTH_LONG
67+
).show()
68+
throw RuntimeException("API Key was not set in secrets.properties")
69+
}
70+
} catch (e: PackageManager.NameNotFoundException) {
71+
Log.e(TAG, "Package name not found.", e)
72+
throw RuntimeException("Error getting package info.", e)
73+
} catch (e: NullPointerException) {
74+
Log.e(TAG, "Error accessing meta-data.", e) // Handle the case where meta-data is completely missing.
75+
throw RuntimeException("Error accessing meta-data in manifest", e)
76+
}
77+
}
78+
79+
/**
80+
* Retrieves the map ID from the BuildConfig or string resource.
81+
*
82+
* @return The valid map ID or null if no valid map ID is found.
83+
*/
84+
val mapId: String? by lazy {
85+
if (BuildConfig.MAP_ID != "YOUR_MAP_ID") {
86+
BuildConfig.MAP_ID
87+
} else if (getString(R.string.map_id) != "YOUR_MAP_ID") {
88+
getString(R.string.map_id)
89+
} else {
90+
Log.w(TAG, "Map ID is not set. See README for instructions.")
91+
Toast.makeText(this, "Map ID is not set. Some features may not work. See README for instructions.", Toast.LENGTH_LONG)
92+
.show()
93+
null
94+
}
95+
}
96+
97+
companion object {
98+
private const val TAG = "ApiDemoApplication"
99+
}
100+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.example.placedetailscompose.repository
16+
17+
import android.annotation.SuppressLint
18+
import android.content.Context
19+
import android.location.Location
20+
import android.os.Looper
21+
import com.google.android.gms.location.FusedLocationProviderClient
22+
import com.google.android.gms.location.LocationCallback
23+
import com.google.android.gms.location.LocationRequest
24+
import com.google.android.gms.location.LocationResult
25+
import com.google.android.gms.location.LocationServices
26+
import com.google.android.gms.location.Priority
27+
import kotlinx.coroutines.channels.awaitClose
28+
import kotlinx.coroutines.flow.Flow
29+
import kotlinx.coroutines.flow.callbackFlow
30+
31+
class LocationRepository(context: Context) {
32+
33+
private val fusedLocationClient: FusedLocationProviderClient =
34+
LocationServices.getFusedLocationProviderClient(context)
35+
36+
@SuppressLint("MissingPermission")
37+
fun getDeviceLocation(): Flow<Location> = callbackFlow {
38+
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 10000L)
39+
.setWaitForAccurateLocation(false)
40+
.setMinUpdateIntervalMillis(5000L)
41+
.build()
42+
43+
val locationCallback = object : LocationCallback() {
44+
override fun onLocationResult(locationResult: LocationResult) {
45+
locationResult.lastLocation?.let { trySend(it) }
46+
}
47+
}
48+
49+
fusedLocationClient.requestLocationUpdates(
50+
locationRequest,
51+
locationCallback,
52+
Looper.getMainLooper()
53+
)
54+
55+
awaitClose {
56+
fusedLocationClient.removeLocationUpdates(locationCallback)
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)