I take uri from db and would like to show an image:
val uri by viewModel.uri.collectAsState()
Image(
painter = rememberAsyncImagePainter(
ImageRequest
.Builder(LocalContext.current)
.data(data = uri)
.build()
),
contentDescription = ""
)
but it is not loading - it is blank.
When debugging I can see that on recomposition
Image->Uri->uriString is set to correct value - "content://com.android.providers.media.documents/document/image%3A44"
but itis still not visible.
It is only visible if I pick it again using:
val pickMedia =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) {
viewModel.onImageSelected(uri)
} else {
Log.d("PhotoPicker", "No media selected")
}
}
I am reusing the same image in many places and from now on it is visible everywhere (but still no other image is visible until I repick it again).
After closing and reopening app images are not visible again.
Maybe it is some caching? or permissions?
I'm currently getting an index out of bound which I can't figure out why...
Following situation:
Sometimes I update my compose bottom navigation to add an item or to change tint color etc. When the app is running and the update is happening it works. But when i close the app and it starts to initialize the navigation with the new state I get an index out of bound exception on the painterResource...
Would be nice if anyone has a hint for me. Another thing to add is i wire the compose currently to xml but i can't see a problem with that.
Thanks in advance
BottomNavigation(
backgroundColor = backgroundColor,
) {
navigation.navigationItems.forEach { navigationItem ->
val isSelected = selectedItem == navigationItem.destination
val iconResId =
if (isSelected) navigationItem.selectedIconRes else navigationItem.iconRes
val hasBadge = badges.contains(navigationItem.destination)
BottomNavigationItem(
icon = {
if (hasBadge) {
BadgedBox(
modifier = Modifier.background(badgeBackgroundColor),
badge = { Badge(backgroundColor = badgeColor) }
) {
Icon(
painterResource(iconResId),
contentDescription = null
)
}
} else {
Icon(
painterResource(iconResId),
contentDescription = null
)
}
},
selected = isSelected,
onClick = { onItemSelected(navigationItem.destination) },
alwaysShowLabel = true,
selectedContentColor = selectedTintColor,
unselectedContentColor = tintColor
)
}
}
Update:
I found now out why it happens but it’s still strange. The drawable itself produced somehow that error. After putting a different one there it worked all the time. The question is why did it not work after restarting the app but when running on the dynamic change… I have no idea…
Update 2:
After the dynamic change there are artifacts happening within the icons… For example 3rd icon has artifacts of 4th etc. What could cause that? Any idea?
I am integrating Admob's banners in my app, this app runs both in AndroidTV and Android, where it's running in mobile the dimensions must be 320x50(BANNER) which is part of the standard sizes in the docs, but it turns of nowhere to 468x60(FULL_BANNER), this behavior happens oftentimes when you run the app. I am using Jetpack Compose for this, and the add is placed inside a LazyColumn which is the equivalent to a recycler view
My code for the banner is like this:
enum class BannerSize {
NORMAL,
RECTANGLE;
internal fun map(): AdSize = when (this) {
NORMAL -> AdSize.BANNER
RECTANGLE -> AdSize.MEDIUM_RECTANGLE
}
}
/*
This Composable goes inside a lazy column with other composable where I have a
when statement, where I look if the index is 0 or 5 which are the specific
indexes I want to place the ads.
*/
#Composable
internal fun BannerAd(
modifier: Modifier = Modifier,
size: BannerSize = BannerSize.NORMAL,
id: String = "TODO",
pos: String = "TODO",
adId: String,
) {
val isInEditMode = LocalInspectionMode.current
if (!isInEditMode) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(20.dp),
contentAlignment = Alignment.Center
) {
AndroidView(
modifier = modifier
.height(size.map().height.dp)
.width(size.map().width.dp),
factory = { context ->
Napier.d(
tag = "Ads",
message = "Creating Ad, id: $id, adId: $adId, pos: $pos, size: $size"
)
AdView(context).apply {
adListener = object : AdListener() {
override fun onAdFailedToLoad(p0: LoadAdError) {
Napier.e(tag = "Ads", message = p0.message)
}
}
adSize = size.map()
adUnitId = adId
loadAd(
AdRequest.Builder()
.addNetworkExtrasBundle(
AdMobAdapter::class.java,
Bundle().also {
it.putString("pos", pos)
}
)
.build()
)
}
}
)
}
} else {
EditModeText()
}
}
Has someone encountered something like this while integrating ads with Compose? I've looking for some questions related to this, but I haven't found any
Well, after doing a lot research, turns out it was something related to Android view's lifecycle, to avoid this weird behaviour I did what is in this answer
: AdManagerAdView not rendering ad image when off screen in LazyColumn
We must set the ad during on doOnLayout method, and It will fix this problem for now.
In the Coil image library I am trying to get when the image has been successfully loaded and then use Palette to get the colors from the bitmap but I am having a problem where if I use the ImageRequests's target the image does not show in the imageview but the onSuccess completes correctly because I am able to extract the color from the given bitmap
Here is what I have
_contentImage.load(File(image))
{
crossfade(true)
allowHardware(false)
listener(onError = { _, error ->
Log.d(SUBTAG, "Error -> ${error.message}")
},
onSuccess = {_, _ ->
Log.d(SUBTAG, "Content image loaded")
contentImageLoaded = true
checkContentLoaded()
})
target(
onSuccess = { result ->
Palette.from(result.toBitmap()).generate {
if (it != null) {
val paletteColor = it.getDominantColor(color)
_constraintLayout.setBackgroundColor(paletteColor)
}
}
}
)
}
If I comment out the whole target section of the builder the image shows fine but obviously I don't get the palette stuff
Do I have to manually set the drawable to the image if I use the onSuccess like this
target(
onSuccess = { result ->
Palette.from(result.toBitmap()).generate {
if (it != null) {
val paletteColor = it.getDominantColor(color)
_constraintLayout.setBackgroundColor(paletteColor)
}
}
_contentImage.setImageDrawable(result) // Set it here?
}
)
If I do that the image shows but it seems odd to have to manually set it and I feel like it should be set by itself
Not sure what the problem is here
As of 1.2.0-beta01 of androidx.activity:activity-ktx, one can no longer launch the request created using Activity.registerForActivityResult(), as highlighted in the above link under "Behavior Changes" and seen in the Google issue here.
How should an application launch this request via a #Composable function now? Previously, an app could pass the instance of the MainActivity down the chain via using an Ambient and then launch the request easily.
The new behavior can be worked around by, for example, passing a class registering for the activity result down the chain after being instantiated outside of the Activity's onCreate function, and then launch the request in a Composable. However, registering the a callback to be executed after completion cannot be done this way.
One could get around this by creating custom ActivityResultContract, which, at launch, take a callback. However, this would mean that virtually none of the built-in ActivityResultContracts could be used with Jetpack Compose.
TL;DR
How would an app launch an ActivityResultsContract request from a #Composable function?
As of androidx.activity:activity-compose:1.3.0-alpha06, the registerForActivityResult() API has been renamed to rememberLauncherForActivityResult() to better indicate the returned ActivityResultLauncher is a managed object that is remembered on your behalf.
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 Activity Result has two API surfaces:
The core ActivityResultRegistry. This is what actually does the underlying work.
A convenience interface in ActivityResultCaller that ComponentActivity and Fragment implement that ties the Activity Result request to the lifecycle of the Activity or Fragment
A Composable has a different lifetime than the Activity or Fragment (e.g., if you remove the Composable from your hierarchy, it should clean up after itself) and thus using the ActivityResultCaller APIs such as registerForActivityResult() is never the right thing to do.
Instead, you should be using the ActivityResultRegistry APIs directly, calling register() and unregister() directly. This is best paired with the rememberUpdatedState() and DisposableEffect to create a version of registerForActivityResult that works with a Composable:
#Composable
fun <I, O> registerForActivityResult(
contract: ActivityResultContract<I, O>,
onResult: (O) -> Unit
) : ActivityResultLauncher<I> {
// First, find the ActivityResultRegistry by casting the Context
// (which is actually a ComponentActivity) to ActivityResultRegistryOwner
val owner = ContextAmbient.current as ActivityResultRegistryOwner
val activityResultRegistry = owner.activityResultRegistry
// Keep track of the current onResult listener
val currentOnResult = rememberUpdatedState(onResult)
// It doesn't really matter what the key is, just that it is unique
// and consistent across configuration changes
val key = rememberSavedInstanceState { UUID.randomUUID().toString() }
// Since we don't have a reference to the real ActivityResultLauncher
// until we register(), we build a layer of indirection so we can
// immediately return an ActivityResultLauncher
// (this is the same approach that Fragment.registerForActivityResult uses)
val realLauncher = mutableStateOf<ActivityResultLauncher<I>?>(null)
val returnedLauncher = remember {
object : ActivityResultLauncher<I>() {
override fun launch(input: I, options: ActivityOptionsCompat?) {
realLauncher.value?.launch(input, options)
}
override fun unregister() {
realLauncher.value?.unregister()
}
override fun getContract() = contract
}
}
// DisposableEffect ensures that we only register once
// and that we unregister when the composable is disposed
DisposableEffect(activityResultRegistry, key, contract) {
realLauncher.value = activityResultRegistry.register(key, contract) {
currentOnResult.value(it)
}
onDispose {
realLauncher.value?.unregister()
}
}
return returnedLauncher
}
Then it is possible to use this in your own Composable via code such as:
val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {
// Here we just update the state, but you could imagine
// pre-processing the result, or updating a MutableSharedFlow that
// your composable collects
result.value = it
}
// Now your onClick listener can call launch()
Button(onClick = { launcher.launch() } ) {
Text(text = "Take a picture")
}
// And you can use the result once it becomes available
result.value?.let { image ->
Image(image.asImageAsset(),
modifier = Modifier.fillMaxWidth())
}
As of Activity Compose 1.3.0-alpha03 and beyond, there is a new utility function registerForActivityResult() that simplifies this process.
#Composable
fun RegisterForActivityResult() {
val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = registerForActivityResult(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())
}
}
(From the sample given here )
Adding in case if someone is starting a new external intent. In My case, I wanted to launch a google sign-in prompt on click on the button in jetpack compose.
declare your intent launch
val startForResult =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
//do something here
}
}
launch your new activity or any intent.
Button(
onClick = {
//important step
startForResult.launch(googleSignInClient?.signInIntent)
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp),
shape = RoundedCornerShape(6.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Black,
contentColor = Color.White
)
) {
Image(
painter = painterResource(id = R.drawable.ic_logo_google),
contentDescription = ""
)
Text(text = "Sign in with Google", modifier = Modifier.padding(6.dp))
}
#googlesignin
For those who are not getting back a result with the gist provided by #ianhanniballake in my case the returnedLauncher actually captures an already disposed value of the realLauncher.
So while removing the layer of indirection should fix the issue, it's definitely not the optimal way of doing this.
Here's the updated version, until a better solution is found:
#Composable
fun <I, O> registerForActivityResult(
contract: ActivityResultContract<I, O>,
onResult: (O) -> Unit
): ActivityResultLauncher<I> {
// First, find the ActivityResultRegistry by casting the Context
// (which is actually a ComponentActivity) to ActivityResultRegistryOwner
val owner = AmbientContext.current as ActivityResultRegistryOwner
val activityResultRegistry = owner.activityResultRegistry
// Keep track of the current onResult listener
val currentOnResult = rememberUpdatedState(onResult)
// It doesn't really matter what the key is, just that it is unique
// and consistent across configuration changes
val key = rememberSavedInstanceState { UUID.randomUUID().toString() }
// TODO a working layer of indirection would be great
val realLauncher = remember<ActivityResultLauncher<I>> {
activityResultRegistry.register(key, contract) {
currentOnResult.value(it)
}
}
onDispose {
realLauncher.unregister()
}
return realLauncher
}