I'm developing an app that enables users to capture photos using the built-in camera of their phones and then display the captured photo within the app using an AsyncImage object from Coli. The initial capture works as expected, but subsequent attempts to capture additional photos result in the displayed image remaining unchanged from the first one. It fails to update with the newly captured picture.
I attempted to place the getUriForFile function within the button so that the URI would be refreshed every time the button is clicked. However, the issue escalated to the point where the displayed image becomes empty after the second capture. I suspect that the problem may stem from the way I assigned the URI value, but despite spending an entire day grappling with it, I haven't been able to resolve it.
Here are the codes that I'm working with:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MyApp()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
val context = LocalContext.current
val uri = context.createImageFile()
var imageUri by remember {
mutableStateOf<Uri>(Uri.EMPTY)
}
// Take photo
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
imageUri = uri
}
// Camera Permission
val cameraPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
if (it) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Scaffold (
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(R.string.title),
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
},
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = colorResource(R.color.theme)
)
)
}
) {
innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
// Create AsyncImage object if image exists
if (imageUri.path?.isNotEmpty() == true) {
AsyncImage(
model = imageUri,
modifier = Modifier.fillMaxWidth(),
contentDescription = stringResource(R.string.image_content_description)
)
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = dimensionResource(R.dimen.padding_vertical))
) {
Text(
text = stringResource(R.string.upload),
style = MaterialTheme.typography.titleLarge
)
// Take photo
Button(
onClick = {
val permissionCheckResult =
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(uri)
} else {
// Request a permission
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
},
shape = RoundedCornerShape(dimensionResource(R.dimen.button_radius)),
colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.theme)),
modifier = Modifier
.width(dimensionResource(R.dimen.button_width))
.height(dimensionResource(R.dimen.button_height))
.padding(top = dimensionResource(R.dimen.button_padding))
) {
Image(
painter = painterResource(R.drawable.photo_camera),
contentDescription = null,
modifier = Modifier.weight(0.5f)
)
Text(
text = stringResource(R.string.take_photo),
style = MaterialTheme.typography.titleLarge,
color = Color.Black,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1.5f)
)
}
}
}
}
}
private fun Context.createImageFile(): Uri {
val directory = File(filesDir, "images")
if (!directory.exists()) {
directory.mkdirs()
}
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val file = File.createTempFile(
"image_${timeStamp}_",
".png",
directory
)
return FileProvider.getUriForFile(
Objects.requireNonNull(this),
packageName + ".FileProvider",
file
)
}
I've managed to find a solution, so I'll share it here.
Initially, I made the uri
variable mutable thanks to Akshay's suggestion. This allows the uri
value to change every time the button is clicked. However, the issue persisted.
Upon further investigation, I came across this post that shed light on the problem. It turns out that passing the cameraLauncher
into the cameraPermissionLauncher
resulted in the camera launching with the same uri each time it checked for permission.
Here is the working solution:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MyApp()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
val context = LocalContext.current
var uri by rememberSaveable {
mutableStateOf<Uri>(Uri.EMPTY)
}
var imageUri by rememberSaveable {
mutableStateOf<Uri>(Uri.EMPTY)
}
// Take photo
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success) {
imageUri = uri
} else {
Toast.makeText(context, "Failed to capture photo", Toast.LENGTH_SHORT).show()
}
}
// Camera Permission
val cameraPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Scaffold (
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(R.string.title),
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
},
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = colorResource(R.color.theme)
)
)
}
) {
innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
// Create AsyncImage object if image exists
AnimatedVisibility(visible = imageUri.toString().isNotEmpty()) {
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageUri)
.crossfade(true)
.build(),
modifier = Modifier.fillMaxWidth(),
contentDescription = stringResource(R.string.image_content_description)
)
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = dimensionResource(R.dimen.padding_vertical))
) {
Text(
text = stringResource(R.string.upload),
style = MaterialTheme.typography.titleLarge
)
// Take photo
Button(
onClick = {
val permissionCheckResult =
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
uri = context.createImageFile()
cameraLauncher.launch(uri)
} else {
// Request a permission
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
},
shape = RoundedCornerShape(dimensionResource(R.dimen.button_radius)),
colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.theme)),
modifier = Modifier
.width(dimensionResource(R.dimen.button_width))
.height(dimensionResource(R.dimen.button_height))
.padding(top = dimensionResource(R.dimen.button_padding))
) {
Image(
painter = painterResource(R.drawable.photo_camera),
contentDescription = null,
modifier = Modifier.weight(0.5f)
)
Text(
text = stringResource(R.string.take_photo),
style = MaterialTheme.typography.titleLarge,
color = Color.Black,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1.5f)
)
}
}
}
}
}
private fun Context.createImageFile(): Uri {
val directory = File(filesDir, "images")
if (!directory.exists()) {
directory.mkdirs()
}
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val file = File.createTempFile(
"image_${timeStamp}_",
".png",
directory
)
return FileProvider.getUriForFile(
Objects.requireNonNull(this),
packageName + ".FileProvider",
file
)
}