Skip to content

Instantly share code, notes, and snippets.

@sanskar10100
Last active January 20, 2024 20:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sanskar10100/eeff4187c12dec8510ad895ad72a2a6a to your computer and use it in GitHub Desktop.
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
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()
}
}
}
}
}
}
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()
}
}
}
@ProjectsYM
Copy link

ProjectsYM commented Jan 20, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment