I was trying to find out how to request permissions in Jetpack Compose. Found an article in official documentation, but I couldn't figure out how to use it in my case; there's also an answer on Stack Overflow, but I simply couldn't understand it.
I will appreciate if you show some of your examples with explanation, or help me understand the code from the answer that I mentioned.
For my case, it was quite simple, I just made a composable and called it in my MainActivity like this, in setContent:
checkNotificationPolicyAccess(notificationManager, this)
Basically, if the permission is not granted, I show a Dialog.
#Composable
fun checkNotificationPolicyAccess(
notificationManager: NotificationManager,
context: Context
): Boolean {
if (notificationManager.isNotificationPolicyAccessGranted) {
return true
} else {
NotificationPolicyPermissionDialog(context)
}
return false
}
OK, I understood it.
You need to implement this composable and use it when you need to use a feature that needs the permissions: you pass in an array of permissions, a requestCode (any Int), and two lambdas with composables: onGranted, which is used when the permissions were granted, and onDenied composable in the other case.
#Composable
fun PermissionsRequest(
permissions: Array<out String>,
requestCode: Int,
onGranted: #Composable () -> Unit,
onDenied: #Composable () -> Unit,
onDeniedPermanently: (#Composable () -> Unit)? = null,
rational: (#Composable () -> Unit)? = null,
awaitResult: (#Composable () -> Unit)? = null,
) {
val permissionHandler = AmbientPermissionHandler.current
val (permissionResult, setPermissionResult) = remember(permissions) {
mutableStateOf<PermissionResult?>(null)
}
LaunchedEffect(Unit) {
setPermissionResult(permissionHandler.requestPermissions(requestCode, permissions))
}
when (permissionResult) {
is PermissionResult.PermissionGranted -> onGranted()
is PermissionResult.PermissionDenied -> onDenied()
is PermissionResult.PermissionDeniedPermanently -> onDeniedPermanently?.invoke()
is PermissionResult.ShowRational -> rational?.invoke()
null -> awaitResult?.invoke()
}
}
Also you need to implement an ambient. As I understand it, it is used to pass the value down the composables children. In our case AmbientPermissionHandler is going to be passed with its value — PermissionHandler — to the PermissionsRequest from Providers composable.
val AmbientPermissionHandler = ambientOf<PermissionHandler>()
The PermissionHandler implementation that would be passed to PermissionRequest composable as an AmbientPermissionHandler using Providers.
class PermissionHandler(private val context: AppCompatActivity) {
suspend fun requestPermissions(
requestCode: Int,
permissions: Array<out String>
): PermissionResult {
return PermissionManager.requestPermissions(context, requestCode, *permissions)
}
}
And then you use it like this:
class MainActivity : AppCompatActivity() {
private val permissionHandler = PermissionHandler(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Providers(
AmbientPermissionHandler provides permissionHandler
) {
PermissionsRequest(
permissions = arrayOf(Manifest.permission.READ_SMS),
requestCode = PERMISSION_REQUEST_CODE,
onGranted = { /* Here goes the composables when the permission is granted */ },
onDenied = { /* Is used when the permission is denied */ }
)
}
}
}
}
Initialize PermissionHandler in MainActivity and then provide it in Providers inside the setContent.
To use PermissionManager and LaunchedEffect you need those dependencies:
implementation 'com.sagar:coroutinespermission:2.0.3'
implementation 'androidx.compose.runtime:runtime:1.0.0-alpha11'
And thanks to 2jan222 for the sample code.
you don't need any additional library for request permission
//define permission in composable fun
val getPermission = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
//permission accepted do somthing
} else {
//permission not accepted show message
}
}
//i used SideEffect to launch permission request when screen recomposed
//you can call it inside a button click without SideEffect
SideEffect {
getPermission.launch(Manifest.permission.YOUR_PERMISSION_REQEUST)
}
and if you wanted to request multiple permission use this:
ActivityResultContracts.RequestMultiplePermissions()
Related
Im trying to implement permission handling for healthconnect. This is done in the funciton checkPermissionAndRun (strongly inspired by the documentation), however I'm not sure what the difference between the two else{...} code segments is.Both check if the permission is given already, but why do we need 2 of those? Furthermore, is it alright if I call the checkpermissionandrun function once in the oncreate, or should i do this also when interacting with HealthConnect?
class MainActivity : AppCompatActivity() {
// build a set of permissions for required data types
val PERMISSIONS =
setOf(
HealthPermission.createReadPermission(HeartRateRecord::class),
HealthPermission.createWritePermission(HeartRateRecord::class),
HealthPermission.createReadPermission(StepsRecord::class),
HealthPermission.createWritePermission(StepsRecord::class)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val hcManager = HealthConnectManager(this)
checkPermissionsAndRun(hcManager.healthConnectClient, PERMISSIONS)
}
private fun checkPermissionsAndRun(client: HealthConnectClient, PERMISSIONS: Set<HealthPermission>) {
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
val requestPermissions =
registerForActivityResult(requestPermissionActivityContract) { granted ->
if (granted.containsAll(PERMISSIONS)) {
//Permission granted text 1
Toast.makeText(applicationContext, "permissions granted 1", Toast.LENGTH_SHORT).show()
} else {
// Lack of required permissions, But what is different to the coroutine else below?
}
}
lifecycleScope.launch {
val granted = client.permissionController.getGrantedPermissions(PERMISSIONS)
if (granted.containsAll(PERMISSIONS)) {
//Permission Granted text 2
Toast.makeText(applicationContext, "permissions granted 2", Toast.LENGTH_SHORT).show()
} else {
//lack of required permissions
requestPermissions.launch(PERMISSIONS)
}
}
}
}
Thank you in advance
I'm a junior Android developer and trying to build a Facebook-like social media app. My issue is that when I bookmark a post in Screen B and the action succeeds, (1) I want to launch an API request in Screen A while in Screen B and (2) update the bookmarked icon ONLY for that particular post.
For the second part of the issue, I tried these two solutions.
I relaunched a manual API request on navigating back to Screen A. This updates the whole list when there's only one small change, hence very inefficient.
I built another URL route to fetch that updated post only and launched it on navigating back to Screen A. But to insert the newly updated post at the old index, the list has to be mutable and I ain't sure this is a good practice.
Please help me on how to solve this issue or similar issues. I'm not sure if this should be done by passing NavArg to update locally and then some or by using web sockets. Thanks in advance.
data class ScreenAState(
val posts: List<Post> = emptyList(),
val isLoading: Boolean = false)
data class ScreenBState(
val post: PostDetail? = null,
val isBookmarked: Boolean? = null)
data class Post(
val title: String,
val isBookMarked: Boolean,
val imageUrl: String)
data class PostDetail(
val title: String,
val content: String,
val isBookMarked: Boolean,
val imageUrl: String)
I suggest you continue with using your logic that will update your list on return from screen B to screen A, but instead of using simple list, you could use:
https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList
This list is designed for what you need I think. Update just that one element.
In mean time, you can change that item from list to some loading dummy item, if you want to have loading like view while you wait for API call to finish.
The problem is how to handle data consistency, which is not directly related to jetpack compose. I suggest you solve this problem at the model level. Return flow instead of static data in the repository, and use collectAsState in the jetpack compose to monitor data changes.
It's hard to give an example, because it depends on the type of Model layer. If it's a database, androidx's room library supports returning flow; if it's a network, take a look at this.
https://gist.github.com/FishHawk/6e4706646401bea20242bdfad5d86a9e
Triggering a refresh is not a good option. It is better to maintain an ActionChannel in the repository for each list that is monitored. use the ActionChannel to modify the list locally to notify compose of the update.
For example, you can make a PagedList if the data layer is network. With onStart and onClose, channels can be added or removed from the repository, thus giving the repository the ability to update all the observed lists.
sealed interface RemoteListAction<out T> {
data class Mutate<T>(val transformer: (MutableList<T>) -> MutableList<T>) : RemoteListAction<T>
object Reload : RemoteListAction<Nothing>
object RequestNextPage : RemoteListAction<Nothing>
}
typealias RemoteListActionChannel<T> = Channel<RemoteListAction<T>>
suspend fun <T> RemoteListActionChannel<T>.mutate(transformer: (MutableList<T>) -> MutableList<T>) {
send(RemoteListAction.Mutate(transformer))
}
suspend fun <T> RemoteListActionChannel<T>.reload() {
send(RemoteListAction.Reload)
}
suspend fun <T> RemoteListActionChannel<T>.requestNextPage() {
send(RemoteListAction.RequestNextPage)
}
class RemoteList<T>(
private val actionChannel: RemoteListActionChannel<T>,
val value: Result<PagedList<T>>?,
) {
suspend fun mutate(transformer: (MutableList<T>) -> MutableList<T>) =
actionChannel.mutate(transformer)
suspend fun reload() = actionChannel.reload()
suspend fun requestNextPage() = actionChannel.requestNextPage()
}
data class PagedList<T>(
val list: List<T>,
val appendState: Result<Unit>?,
)
data class Page<Key : Any, T>(
val data: List<T>,
val nextKey: Key?,
)
fun <Key : Any, T> remotePagingList(
startKey: Key,
loader: suspend (Key) -> Result<Page<Key, T>>,
onStart: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
onClose: ((actionChannel: RemoteListActionChannel<T>) -> Unit)? = null,
): Flow<RemoteList<T>> = callbackFlow {
val dispatcher = Dispatchers.IO.limitedParallelism(1)
val actionChannel = Channel<RemoteListAction<T>>()
var listState: Result<Unit>? = null
var appendState: Result<Unit>? = null
var value: MutableList<T> = mutableListOf()
var nextKey: Key? = startKey
onStart?.invoke(actionChannel)
suspend fun mySend() {
send(
RemoteList(
actionChannel = actionChannel,
value = listState?.map {
PagedList(
appendState = appendState,
list = value,
)
},
)
)
}
fun requestNextPage() = launch(dispatcher) {
nextKey?.let { key ->
appendState = null
mySend()
loader(key)
.onSuccess {
value.addAll(it.data)
nextKey = it.nextKey
listState = Result.success(Unit)
appendState = Result.success(Unit)
mySend()
}
.onFailure {
if (listState?.isSuccess != true)
listState = Result.failure(it)
appendState = Result.failure(it)
mySend()
}
}
}
var job = requestNextPage()
launch(dispatcher) {
actionChannel.receiveAsFlow().flowOn(dispatcher).collect { action ->
when (action) {
is RemoteListAction.Mutate -> {
value = action.transformer(value)
mySend()
}
is RemoteListAction.Reload -> {
job.cancel()
listState = null
appendState = null
value.clear()
nextKey = startKey
mySend()
job = requestNextPage()
}
is RemoteListAction.RequestNextPage -> {
if (!job.isActive) job = requestNextPage()
}
}
}
}
launch(dispatcher) {
Connectivity.instance?.interfaceName?.collect {
if (job.isActive) {
job.cancel()
job = requestNextPage()
}
}
}
awaitClose {
onClose?.invoke(actionChannel)
}
}
And in repository:
val postListActionChannels = mutableListOf<RemoteListActionChannel<Post>>()
suspend fun listPost() =
daoFlow.filterNotNull().flatMapLatest {
remotePagingList(
startKey = 0,
loader = { page ->
it.mapCatching { dao ->
/* dao function, simulate network operation, return List<Post> */
dao.listPost(page)
}.map { Page(it, if (it.isEmpty()) null else page + 1) }
},
onStart = { postListActionChannels.add(it) },
onClose = { postListActionChannels.remove(it) },
)
}
suspend fun markPost(title: String) =
oneshot {
/* dao function, simulate network operation, return Unit */
it.markPost(title)
}.onSuccess {
postListActionChannels.forEach { ch ->
ch.mutate { list ->
list.map {
if (it.title == title && !it.isBookMarked)
it.copy(isBookMarked = true)
else it
}.toMutableList()
}
}
}
Right now I have an Event class in the ViewModel that is exposed as a Flow this way:
abstract class BaseViewModel() : ViewModel() {
...
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()
fun sendEvent(event: Event) {
viewModelScope.launch {
eventChannel.send(event)
}
}
sealed class Event {
data class NavigateTo(val destination: Int): Event()
data class ShowSnackbarResource(val resource: Int): Event()
data class ShowSnackbarString(val message: String): Event()
}
}
And this is the composable managing it:
#Composable
fun SearchScreen(
viewModel: SearchViewModel
) {
val events = viewModel.eventsFlow.collectAsState(initial = null)
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
Column(
modifier = Modifier
.padding(all = 24.dp)
) {
SearchHeader(viewModel = viewModel)
SearchContent(
viewModel = viewModel,
modifier = Modifier.padding(top = 24.dp)
)
when(events.value) {
is NavigateTo -> TODO()
is ShowSnackbarResource -> {
val resources = LocalContext.current.resources
val message = (events.value as ShowSnackbarResource).resource
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getString(message)
)
}
}
is ShowSnackbarString -> {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = (events.value as ShowSnackbarString).message
)
}
}
}
}
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
I followed the pattern for single events with Flow from here.
My problem is, the event is handled correctly only the first time (SnackBar is shown correctly). But after that, seems like the events are not collected anymore. At least until I leave the screen and come back. And in that case, all events are triggered consecutively.
Can't see what I'm doing wrong. When debugged, events are sent to the Channel correctly, but seems like the state value is not updated in the composable.
Rather than placing your logic right inside composable place them inside
// Runs only on initial composition
LaunchedEffect(key1 = Unit) {
viewModel.eventsFlow.collectLatest { value ->
when(value) {
// Handle events
}
}
}
And also rather than using it as state just collect value from flow in LaunchedEffect block. This is how I implemented single event in my application
Here's a modified version of Jack's answer, as an extension function following new guidelines for safer flow collection.
#Composable
inline fun <reified T> Flow<T>.observeWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
noinline action: suspend (T) -> Unit
) {
LaunchedEffect(key1 = Unit) {
lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
}
}
}
Usage:
viewModel.flow.observeWithLifecycle { value ->
//Use the collected value
}
I'm not sure how you manage to compile the code, because I get an error on launch.
Calls to launch should happen inside a LaunchedEffect and not composition
Usually you can use LaunchedEffect which is already running in the coroutine scope, so you don't need coroutineScope.launch. Read more about side effects in documentation.
A little kotlin advice: when using when in types, you don't need to manually cast the variable to a type with as. In cases like this, you can declare val along with your variable to prevent Smart cast to ... is impossible, because ... is a property that has open or custom getter error:
val resources = LocalContext.current.resources
val event = events.value // allow Smart cast
LaunchedEffect(event) {
when (event) {
is BaseViewModel.Event.NavigateTo -> TODO()
is BaseViewModel.Event.ShowSnackbarResource -> {
val message = event.resource
snackbarHostState.showSnackbar(
message = resources.getString(message)
)
}
is BaseViewModel.Event.ShowSnackbarString -> {
snackbarHostState.showSnackbar(
message = event.message
)
}
}
}
This code has one problem: if you send the same event many times, it will not be shown because LaunchedEffect will not be restarted: event as key is the same.
You can solve this problem in different ways. Here are some of them:
Replace data class with class: now events will be compared by pointer, not by fields.
Add a random id to the data class, so that each new element is not equal to another:
data class ShowSnackbarResource(val resource: Int, val id: UUID = UUID.randomUUID()) : Event()
Note that the coroutine LaunchedEffect will be canceled when a new event occurs. And since showSnackbar is a suspend function, the previous snackbar will be hidden to display the new one. If you run showSnackbar on coroutineScope.launch (still doing it inside LaunchedEffect), the new snackbar will wait until the previous snackbar disappears before it appears.
Another option, which seems cleaner to me, is to reset the state of the event because you have already reacted to it. You can add another event to do this:
object Clean : Event()
And send it after the snackbar disappears:
LaunchedEffect(event) {
when (event) {
is BaseViewModel.Event.NavigateTo -> TODO()
is BaseViewModel.Event.ShowSnackbarResource -> {
val message = event.resource
snackbarHostState.showSnackbar(
message = resources.getString(message)
)
}
is BaseViewModel.Event.ShowSnackbarString -> {
snackbarHostState.showSnackbar(
message = event.message
)
}
null, BaseViewModel.Event.Clean -> return#LaunchedEffect
}
viewModel.sendEvent(BaseViewModel.Event.Clean)
}
But in this case, if you send the same event while the previous one has not yet disappeared, it will be ignored as before. This can be perfectly normal, depending on the structure of your application, but to prevent this you can show it on coroutineScope as before.
Also, check out the more general solution implemented in the JetNews compose app example. I suggest you download the project and inspect it starting from location where the snackbar is displayed.
https://github.com/Kotlin-Android-Open-Source/Jetpack-Compose-MVI-Coroutines-Flow/blob/master/core-ui/src/main/java/com/hoc/flowmvi/core_ui/rememberFlowWithLifecycle.kt
#Suppress("ComposableNaming")
#Composable
fun <T> Flow<T>.collectInLaunchedEffectWithLifecycle(
vararg keys: Any?,
lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
collector: suspend CoroutineScope.(T) -> Unit
) {
val flow = this
val currentCollector by rememberUpdatedState(collector)
LaunchedEffect(flow, lifecycle, minActiveState, *keys) {
withContext(Dispatchers.Main.immediate) {
lifecycle.repeatOnLifecycle(minActiveState) {
flow.collect { currentCollector(it) }
}
}
}
}
class ViewModel {
val singleEvent: Flow<E> = eventChannel.receiveAsFlow()
}
#Composable fun Demo() {
val snackbarHostState by rememberUpdatedState(LocalSnackbarHostState.current)
val scope = rememberCoroutineScope()
viewModel.singleEvent.collectInLaunchedEffectWithLifecycle { event ->
when (event) {
SingleEvent.Refresh.Success -> {
scope.launch {
snackbarHostState.showSnackbar("Refresh successfully")
}
}
is SingleEvent.Refresh.Failure -> {
scope.launch {
snackbarHostState.showSnackbar("Failed to refresh")
}
}
is SingleEvent.GetUsersError -> {
scope.launch {
snackbarHostState.showSnackbar("Failed to get users")
}
}
is SingleEvent.RemoveUser.Success -> {
scope.launch {
snackbarHostState.showSnackbar("Removed '${event.user.fullName}'")
}
}
is SingleEvent.RemoveUser.Failure -> {
scope.launch {
snackbarHostState.showSnackbar("Failed to remove '${event.user.fullName}'")
}
}
}
}
}
Here's a modified version of Soroush Lotfi answer making sure we also stop flow collection whenever the composable is not visible anymore: just replace the LaunchedEffect with a DisposableEffect
#Composable
inline fun <reified T> Flow<T>.observeWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
noinline action: suspend (T) -> Unit
) {
DisposableEffect(Unit) {
val job = lifecycleOwner.lifecycleScope.launch {
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
}
onDispose {
job.cancel()
}
}
}
I am trying to implement sign-in hints in my Android app using Jetpack Compose, but this API requires an Activity to work.
fun showPhoneNumberHint(activity: Activity) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val intent = Auth.CredentialsApi.getHintPickerIntent(apiClient, hintRequest)
val requestCode = 12345
try {
startIntentSenderForResult(activity, intent.intentSender, requestCode, null, 0, 0, 0, null)
} catch (exception: SendIntentException) {
// Error handling
}
}
So I guess that I'll have to pass the Activity object all the way down to the Composable where it's needed, which doesn't seem very clean but it should work.
But now the result of the hint will be received in the Activity's onActivityResult() and I'm not sure what the right way is to get it back to the Composable where it's needed.
Is there some clean/standard/alternative way to do this? Preferably I'd just keep all of this logic contained inside the Composable.
I ended up using rememberLauncherForActivityResult in combination with the ActivityResultContracts.StartIntentSenderForResult() contract to listen for the result. This returns a launcher that can be used to start the intent.
Instead of Auth.CredentialsApi, which requires the deprecated GoogleApiClient, I'm now using the Credentials.getClient. For this I still needed an Activity which I got using LocalContext.current.
val phoneNumberHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
if (it.resultCode != RESULT_OK) {
return#rememberLauncherForActivityResult
}
val credential: Credential? = it.data?.getParcelableExtra(Credential.EXTRA_KEY)
val hintResult = credential?.id
if (hintResult !== null) {
phoneNumber = hintResult
}
}
val context = LocalContext.current
LaunchedEffect(Unit) {
val hintRequest: HintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()
val phoneNumberHintIntent = Credentials.getClient(context)
.getHintPickerIntent(hintRequest)
phoneNumberHintLauncher.launch(
IntentSenderRequest.Builder(phoneNumberHintIntent)
.build()
)
}
Activity.onActivityResult() is deprecated and you shouldn't use it even without compose. You should use the Activity Result APIs introduced in AndroidX Activity and Fragment.
The Activity Result APIs provide a registerForActivityResult() API for registering the result callback. registerForActivityResult() takes an ActivityResultContract and an ActivityResultCallback and returns an ActivityResultLauncher which you’ll use to launch the other activity.
Example without compose:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
val selectButton = findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Pass in the mime type you'd like to allow the user to select
// as the input
getContent.launch("image/*")
}
}
In compose use rememberLauncherForActivityResult() instead of registerForActivityResult:
val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
result.value = it
}
Button(onClick = { launcher.launch() }) {
Text(text = "Take a picture")
}
result.value?.let { image ->
Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
The problem with the API you're trying to use is it requires the use of onActivityResult. So, you have no other option but to use it. Try opening an issue on github requesting to update their API.
I trying to implement google map with help of google compose sample project calls Crane in here:
https://github.com/android/compose-samples/tree/main/Crane
I went with same implementation and using MapViewUtils to implement lifeCycler for map and prevent re-compose stuff and more... I put all android map key and also permissions on manifest,
But my code getting crash on start of map:
This the point I wanna show map:
#Composable
fun MapScreen(latitude: String, longitude: String) {
// The MapView lifecycle is handled by this composable. As the MapView also needs to be updated
// with input from Compose UI, those updates are encapsulated into the MapViewContainer
// composable. In this way, when an update to the MapView happens, this composable won't
// recompose and the MapView won't need to be recreated.
val mapView = rememberMapViewWithLifecycle()
MapViewContainer(mapView, latitude, longitude)
}
#Composable
private fun MapViewContainer(
map: MapView,
latitude: String,
longitude: String
) {
// var zoom by savedInstanceState { InitialZoom }
AndroidView({ map }) { mapView ->
// Reading zoom so that AndroidView recomposes when it changes. The getMapAsync lambda
mapView.getMapAsync {
val position = LatLng(latitude.toDouble(), longitude.toDouble())
it.addMarker(
MarkerOptions().position(position)
)
it.moveCamera(CameraUpdateFactory.newLatLng(position))
}
}
}
And this is inside Util class:
#Composable
fun rememberMapViewWithLifecycle(): MapView {
val context = ContextAmbient.current
val mapView = remember {
MapView(context).apply {
id = R.id.map
}
}
// Makes MapView follow the lifecycle of this composable
val lifecycleObserver = rememberMapLifecycleObserver(mapView)
val lifecycle = LifecycleOwnerAmbient.current.lifecycle
onCommit(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
return mapView
}
#Composable
private fun rememberMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
remember(mapView) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) //Crashes here
Lifecycle.Event.ON_START -> mapView.onStart()
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
Lifecycle.Event.ON_STOP -> mapView.onStop()
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> throw IllegalStateException()
}
}
}
And I'm getting this crash:
2020-11-05 12:16:09.282 2665-3383/com.google.android.gms.persistent E/ModuleIdSetter: exception when setting module id
java.lang.IllegalStateException: Unable to get current module info in ModuleManager created with non-module Context
at com.google.android.chimera.config.ModuleManager.getCurrentModule(:com.google.android.gms#202414022#20.24.14 (040700-319035315):2)
at aewd.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):4)
at aewg.b(:com.google.android.gms#202414022#20.24.14 (040700-319035315):9)
at aeso.a(Unknown Source:0)
at rpm.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):0)
at rlv.c(:com.google.android.gms#202414022#20.24.14 (040700-319035315):1)
at rlt.b(:com.google.android.gms#202414022#20.24.14 (040700-319035315):1)
at rok.b(:com.google.android.gms#202414022#20.24.14 (040700-319035315):6)
at rok.c(:com.google.android.gms#202414022#20.24.14 (040700-319035315):6)
at rok.b(:com.google.android.gms#202414022#20.24.14 (040700-319035315):10)
at rok.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):17)
at rok.g(:com.google.android.gms#202414022#20.24.14 (040700-319035315):3)
at sdr.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):2)
at scr.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):10)
at sci.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):0)
at scl.handleMessage(:com.google.android.gms#202414022#20.24.14 (040700-319035315):28)
at android.os.Handler.dispatchMessage(Handler.java:107)
at aekz.a(:com.google.android.gms#202414022#20.24.14 (040700-319035315):2)
at aekz.dispatchMessage(:com.google.android.gms#202414022#20.24.14 (040700-319035315):14)
at android.os.Looper.loop(Looper.java:214)
at android.os.HandlerThread.run(HandlerThread.java:67)
You need to ask permission to access the user's location, and make sure you have it before showing the map. You can use a variable with LiveData and ViewModel that is updated on permission granted, here's a part of a example:
class MainViewModel : ViewModel() {
private val _permissionGranted = MutableLiveData(false)
val permissionGranted = _permissionGranted
fun onPermissionGranted() = _permissionGranted.postValue(true)
// ...
}
class MainActivity : AppCompatActivity() {
private val mainViewModel by viewModels<MainViewModel>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val permissionGranted = mainViewModel.permissionGranted.observeAsState()
if (permissionGranted) {
// logic to show your map
} else {
// logic to ask for permission
}
}
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
// check if it's your request
mainViewModel.onPremissionGranted()
}
// ...
}
You can have more info on asking for permissions here: https://developer.android.com/training/permissions/requesting
Get rid of the return type from " fun rememberMapViewWithLifecycle(): MapView " to " fun rememberMapViewWithLifecycle() "
It's is clear that your composable function is returning a MapView, while it shouldn't return a type in order to be considered a composable function.
I quote from " https://developer.android.com/jetpack/compose/mental-model"
"""
The function doesn't return anything. Compose functions that emit UI do not need to return anything, because they describe the desired screen state instead of constructing UI widgets.
"""