-
-
Save sanskar10100/eeff4187c12dec8510ad895ad72a2a6a to your computer and use it in GitHub Desktop.
Maps Sample demonstrating integration of Google Maps, Places, and Geocoder API with Jetpack Compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package dev.sanskar.maps.ui.location | |
import android.Manifest | |
import android.content.Context | |
import android.content.IntentSender | |
import android.location.Geocoder | |
import android.location.LocationManager | |
import android.os.Bundle | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.animation.AnimatedVisibility | |
import androidx.compose.animation.ExperimentalAnimationApi | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.foundation.shape.RoundedCornerShape | |
import androidx.compose.material.Button | |
import androidx.compose.material.Icon | |
import androidx.compose.material.OutlinedTextField | |
import androidx.compose.material.Surface | |
import androidx.compose.material.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.platform.ComposeView | |
import androidx.compose.ui.platform.ViewCompositionStrategy | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.unit.dp | |
import androidx.core.location.LocationManagerCompat | |
import androidx.fragment.app.Fragment | |
import androidx.fragment.app.viewModels | |
import com.google.accompanist.permissions.ExperimentalPermissionsApi | |
import com.google.accompanist.permissions.rememberMultiplePermissionsState | |
import com.google.android.gms.common.api.ResolvableApiException | |
import com.google.android.gms.location.LocationRequest | |
import com.google.android.gms.location.LocationServices | |
import com.google.android.gms.location.LocationSettingsRequest | |
import com.google.android.gms.maps.CameraUpdateFactory | |
import com.google.android.gms.maps.model.CameraPosition | |
import com.google.android.libraries.places.api.Places | |
import com.google.maps.android.compose.GoogleMap | |
import com.google.maps.android.compose.MapProperties | |
import com.google.maps.android.compose.MapUiSettings | |
import com.google.maps.android.compose.rememberCameraPositionState | |
import com.ikanshy.oshm.BuildConfig | |
import com.ikanshy.oshm.R | |
import com.ikanshy.oshm.utils.clickWithRipple | |
import dagger.hilt.android.AndroidEntryPoint | |
import kotlinx.coroutines.launch | |
class LocationFragment : Fragment() { | |
private val viewModel by viewModels<LocationViewModel>() | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle?, | |
): View { | |
return ComposeView(requireContext()).apply { | |
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | |
setContent { | |
LocationScreen() | |
} | |
} | |
} | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
viewModel.fusedLocationClient = | |
LocationServices.getFusedLocationProviderClient(requireActivity()) | |
Places.initialize(requireContext().applicationContext, BuildConfig.MAPS_API_KEY) | |
viewModel.placesClient = Places.createClient(requireContext()) | |
viewModel.geoCoder = Geocoder(requireContext()) | |
} | |
@OptIn(ExperimentalPermissionsApi::class, ExperimentalAnimationApi::class) | |
@Composable | |
fun LocationScreen(modifier: Modifier = Modifier) { | |
val locationPermissionState = rememberMultiplePermissionsState( | |
listOf( | |
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION | |
) | |
) | |
LaunchedEffect(locationPermissionState.allPermissionsGranted) { | |
if (locationPermissionState.allPermissionsGranted) { | |
if (locationEnabled()) { | |
viewModel.getCurrentLocation() | |
} else { | |
viewModel.locationState = LocationState.LocationDisabled | |
} | |
} | |
} | |
AnimatedContent( | |
viewModel.locationState | |
) { state -> | |
when (state) { | |
is LocationState.NoPermission -> { | |
Column { | |
Text("We need location permission to continue") | |
Button(onClick = { locationPermissionState.launchMultiplePermissionRequest() }) { | |
Text("Request permission") | |
} | |
} | |
} | |
is LocationState.LocationDisabled -> { | |
Column { | |
Text("We need location to continue") | |
Button(onClick = { requestLocationEnable() }) { | |
Text("Enable location") | |
} | |
} | |
} | |
is LocationState.LocationLoading -> { | |
Text("Loading Map") | |
} | |
is LocationState.Error -> { | |
Column { | |
Text("Error fetching your location") | |
Button(onClick = { viewModel.getCurrentLocation() }) { | |
Text("Retry") | |
} | |
} | |
} | |
is LocationState.LocationAvailable -> { | |
val cameraPositionState = rememberCameraPositionState { | |
position = CameraPosition.fromLatLngZoom(state.cameraLatLang, 15f) | |
} | |
val mapUiSettings by remember { mutableStateOf(MapUiSettings()) } | |
val mapProperties by remember { mutableStateOf(MapProperties(isMyLocationEnabled = true)) } | |
val scope = rememberCoroutineScope() | |
LaunchedEffect(viewModel.currentLatLong) { | |
cameraPositionState.animate(CameraUpdateFactory.newLatLng(viewModel.currentLatLong)) | |
} | |
LaunchedEffect(cameraPositionState.isMoving) { | |
if (!cameraPositionState.isMoving) { | |
viewModel.getAddress(cameraPositionState.position.target) | |
} | |
} | |
Box( | |
modifier = Modifier.fillMaxSize() | |
) { | |
GoogleMap(modifier = Modifier.fillMaxSize(), | |
cameraPositionState = cameraPositionState, | |
uiSettings = mapUiSettings, | |
properties = mapProperties, | |
onMapClick = { | |
scope.launch { | |
cameraPositionState.animate(CameraUpdateFactory.newLatLng(it)) | |
} | |
}) | |
Icon( | |
painter = painterResource(id = R.drawable.ic_marker), | |
contentDescription = null, | |
modifier = Modifier | |
.size(24.dp) | |
.align(Alignment.Center) | |
) | |
Surface( | |
modifier = Modifier | |
.align(Alignment.BottomCenter) | |
.padding(8.dp) | |
.fillMaxWidth(), | |
color = Color.White, | |
shape = RoundedCornerShape(8.dp) | |
) { | |
Column( | |
modifier = Modifier.padding(16.dp), | |
horizontalAlignment = Alignment.CenterHorizontally | |
) { | |
AnimatedVisibility( | |
viewModel.locationAutofill.isNotEmpty(), | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(8.dp) | |
) { | |
LazyColumn( | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
items(viewModel.locationAutofill) { | |
Row(modifier = Modifier | |
.fillMaxWidth() | |
.padding(16.dp) | |
.clickWithRipple { | |
viewModel.text = it.address | |
viewModel.locationAutofill.clear() | |
viewModel.getCoordinates(it) | |
}) { | |
Text(it.address) | |
} | |
} | |
} | |
Spacer(Modifier.height(16.dp)) | |
} | |
OutlinedTextField( | |
value = viewModel.text, onValueChange = { | |
viewModel.text = it | |
viewModel.searchPlaces(it) | |
}, modifier = Modifier | |
.fillMaxWidth() | |
.padding(8.dp) | |
) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
private fun locationEnabled(): Boolean { | |
val locationManager = | |
requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager | |
return LocationManagerCompat.isLocationEnabled(locationManager) | |
} | |
private fun requestLocationEnable() { | |
activity?.let { | |
val locationRequest = LocationRequest.create() | |
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest) | |
LocationServices.getSettingsClient(it).checkLocationSettings(builder.build()) | |
.addOnSuccessListener { | |
if (it.locationSettingsStates?.isLocationPresent == true) { | |
viewModel.getCurrentLocation() | |
} | |
}.addOnFailureListener { | |
if (it is ResolvableApiException) { | |
try { | |
it.startResolutionForResult(requireActivity(), 999) | |
} catch (e: IntentSender.SendIntentException) { | |
e.printStackTrace() | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package dev.sanskar.maps.ui.location | |
import android.annotation.SuppressLint | |
import android.location.Geocoder | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateListOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.setValue | |
import androidx.lifecycle.ViewModel | |
import androidx.lifecycle.viewModelScope | |
import com.google.android.gms.location.FusedLocationProviderClient | |
import com.google.android.gms.location.Priority | |
import com.google.android.gms.maps.model.LatLng | |
import com.google.android.libraries.places.api.model.Place | |
import com.google.android.libraries.places.api.net.FetchPlaceRequest | |
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest | |
import com.google.android.libraries.places.api.net.PlacesClient | |
import dagger.hilt.android.lifecycle.HiltViewModel | |
import javax.inject.Inject | |
import kotlinx.coroutines.Job | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.launch | |
sealed class LocationState { | |
object NoPermission : LocationState() | |
object LocationDisabled : LocationState() | |
object LocationLoading : LocationState() | |
data class LocationAvailable(val cameraLatLang: LatLng) : LocationState() | |
object Error : LocationState() | |
} | |
data class AutocompleteResult( | |
val address: String, | |
val placeId: String, | |
) | |
@HiltViewModel | |
class LocationViewModel @Inject constructor() : ViewModel() { | |
lateinit var fusedLocationClient: FusedLocationProviderClient | |
lateinit var placesClient: PlacesClient | |
lateinit var geoCoder: Geocoder | |
var locationState by mutableStateOf<LocationState>(LocationState.NoPermission) | |
val locationAutofill = mutableStateListOf<AutocompleteResult>() | |
var currentLatLong by mutableStateOf(LatLng(0.0, 0.0)) | |
private var job: Job? = null | |
fun searchPlaces(query: String) { | |
job?.cancel() | |
locationAutofill.clear() | |
job = viewModelScope.launch { | |
val request = FindAutocompletePredictionsRequest.builder().setQuery(query).build() | |
placesClient.findAutocompletePredictions(request).addOnSuccessListener { response -> | |
locationAutofill += response.autocompletePredictions.map { | |
AutocompleteResult( | |
it.getFullText(null).toString(), it.placeId | |
) | |
} | |
}.addOnFailureListener { | |
it.printStackTrace() | |
println(it.cause) | |
println(it.message) | |
} | |
} | |
} | |
fun getCoordinates(result: AutocompleteResult) { | |
val placeFields = listOf(Place.Field.LAT_LNG) | |
val request = FetchPlaceRequest.newInstance(result.placeId, placeFields) | |
placesClient.fetchPlace(request).addOnSuccessListener { | |
if (it != null) { | |
currentLatLong = it.place.latLng!! | |
} | |
}.addOnFailureListener { | |
it.printStackTrace() | |
} | |
} | |
@SuppressLint("MissingPermission") | |
fun getCurrentLocation() { | |
locationState = LocationState.LocationLoading | |
fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) | |
.addOnSuccessListener { location -> | |
locationState = | |
if (location == null && locationState !is LocationState.LocationAvailable) { | |
LocationState.Error | |
} else { | |
currentLatLong = LatLng(location.latitude, location.longitude) | |
LocationState.LocationAvailable( | |
LatLng( | |
location.latitude, | |
location.longitude | |
) | |
) | |
} | |
} | |
} | |
var text by mutableStateOf("") | |
fun getAddress(latLng: LatLng) { | |
viewModelScope.launch { | |
val address = geoCoder.getFromLocation(latLng.latitude, latLng.longitude, 1) | |
text = address?.get(0)?.getAddressLine(0).toString() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Sanskar, you could pass me the full github please, since I really like this work you did and I would like to know everything you did to be able to implement it in one of my projects. Greetings