I create the CameraPreview in layout resource file, and i want to show this file in AndroidView by clicking the navigation icon button .
// navigation icon button
Icon(
vectorResource(id = R.drawable.ic_photo),
modifier = Modifier.clickable(onClick = /* show CameraPreview */)
)
// compose CameraPreview
#Composable
fun CameraPreview() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
AndroidView(viewBlock = { ctx ->
/* get the CameraPreview */
})
}
// xml file
<androidx.camera.view.PreviewView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Create an instance of PreviewView in your composable itself:
#Composable
fun CameraPreview() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val previewView = remember { PreviewView(context) }
AndroidView(viewBlock = { previewView }) {
// Initialize your camera here
}
}
Related
I've got a bottomSheetScaffold, which contains a BottomSheet
That BottomSheet uses device's Camera, where I use CameraX alongside with Google's MLkit for bar scanning
Let's consider permission is accepted
What happens (Not correct): once I expand the bottomsheet upward, I show the CameraPreview, show camera preview, and ImageAnalyzer which analyzes the preview image.
Now the bottomSheet is expanded, the camera preview is visible and working as expected
then I collapse the bottomSheet, but the camera is still working (analyzer as well,
imageAnalysis.clearAnalyzer() clear the analyzing part)
The outcome: is not correct behavior I intended
so How can I stop camera from working, and using resources once the bottomSheetState is collapsed, and only allow camera when bottomSheetState is Expanded
How it works(Wrong):
The problem I got is, camera is binded to the lifecycle of the activity, and not the composable itself, when re-composition happens, it still consider the camera live, since it's not attached to the composition lifecycle
How does Composition work:
Code:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetContent(
modifier: Modifier = Modifier,
bottomSheetState: BottomSheetState
) {
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
) {
PeekBar()
ScanningSerialTextTitle(modifier)
if (bottomSheetState.isExpanded) {
CameraBox(modifier)
} else {
EmptyBox()
}
}
}
#Composable
fun EmptyBox(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(color = Color.DarkGray)
)
}
#OptIn(ExperimentalPermissionsApi::class)
#Composable
fun CameraBox(modifier: Modifier = Modifier) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifeCycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
cameraPermissionState.launchPermissionRequest()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
})
cameraPermissionState.handlePermissionCases(
ShouldShowRationaleContent = {
ShouldShowRationaleContent(cameraPermissionState = cameraPermissionState)
},
PermissionDeniedPermanentlyContent = {
PermissionDeniedPermanentContent()
}) {
val context = LocalContext.current
val barCodeVal = remember { mutableStateOf("") }
CameraPreview(onBarcodeDetected = { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
barCodeVal.value = barcodeValue
Toast.makeText(context, barcodeValue, Toast.LENGTH_SHORT).show()
}
}
}, onBarcodeFailed = {}, onBarcodeNotFound = {})
}
}
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector = buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
try {
cameraProvider.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
camera.cameraControl.enableTorch(true)
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
private fun initPreviewView(androidViewContext: Context): PreviewView {
val previewView = PreviewView(androidViewContext).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
return previewView
}
private fun buildPreview(): Preview {
return Preview.Builder().build()
}
private fun buildImageAnalysis(imageAnalysisStrategy: Int): ImageAnalysis {
return ImageAnalysis.Builder()
.setBackpressureStrategy(imageAnalysisStrategy)
.build()
}
private fun buildCameraSelector(cameraLens: Int): CameraSelector {
return CameraSelector.Builder()
.requireLensFacing(cameraLens)
.build()
}
What I tried:
I tried passing down the state of BottomSheetState to the composable, and checking for state, which should triggers re-composition, but since I'm using Android's Camera as View, this doesn't solve the problem
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when it de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
I found a solution, where I used DisposableEffect to shut the camera when composable is removed from composition
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when the composable de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
I'm trying to implement https://google.github.io/accompanist/navigation-material/ and i want to expand modelsheet to custom height or more than half screen but i don't have any idea how to achieve it
Currently ModelBottomSheet
Wish to expand like this
Instead of using the show method of the ModalBottomSheetState, You can use the animateTo method. The show method will default to a half screen size modal. The animateTo(ModalBottomSheetValue.Expanded) will expand to the full size of the content. In the example i've used a BoxWithConstrains to get the screen size and set the size of the modal content to 80%.
I hope this helps!
#Composable
#Preview
fun BottomSheetDemo() {
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
BoxWithConstraints {
val sheetHeight = this.constraints.maxHeight * 0.8f
val coroutineScope = rememberCoroutineScope()
Column {
Button(onClick = {
coroutineScope.launch { modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded) }
}) {
Text(text = "Expand")
}
Button(onClick = {
coroutineScope.launch { modalBottomSheetState.animateTo(ModalBottomSheetValue.Hidden) }
}) {
Text(text = "Collapse")
}
}
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Red,
sheetState = modalBottomSheetState,
sheetContent = {
Box(modifier = Modifier.height(with(LocalDensity.current) { sheetHeight.toDp() })) {
Text(text = "This is some content")
}
}
) {}
}
}
EDIT:
If you want to use the material navigation, you will need a custom extension function. The difference in this function with the original is the skipHalfExpanded parameter. This on will make it possible to create bottom sheets larger then half screen.
#Composable
fun rememberBottomSheetNavigator(
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec
): BottomSheetNavigator {
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
animationSpec = animationSpec,
skipHalfExpanded = true
)
return remember(sheetState) {
BottomSheetNavigator(sheetState = sheetState)
}
}
The implementation itself will be something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val bottomSheetNavigator = rememberBottomSheetNavigator()
val navController = rememberNavController(bottomSheetNavigator)
ModalBottomSheetLayout(bottomSheetNavigator) {
NavHost(navController, "home") {
composable(route = "home") {
Home(navController)
}
bottomSheet(route = "sheet") {
ModalDemo()
}
}
}
}
}
}
#Composable
fun Home(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
Column {
Button(onClick = {
coroutineScope.launch { navController.navigate("sheet") }
}) {
Text(text = "Expand")
}
}
}
#Composable
fun ModalDemo() {
Column(Modifier.fillMaxWidth().height(700.dp).background(Color.Red), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "This is some content")
}
}
I have got two problems, scrolling through LazyColumn or VerticalPager with AndroidView filling the whole screen as a child item lags the screen and the scrolling behavior for a couple of milliseconds as well as overlapping items. In my code the AndroidView creates PlayerView, I also tried to replace PlayerView with a TextView to check maybe the problem is with PlayerView itself. I can't seem to find the root of the problem exactly, maybe with the AndroidView or the implementation of the VerticalPager itself, or maybe because it fills the whole screen?
ViewScreen
#OptIn(ExperimentalPagerApi::class)
#Composable
fun VideoScreen() {
val pagerState = rememberPagerState()
Box {
VerticalPager(
count = videos.size,
state = pagerState,
horizontalAlignment = Alignment.CenterHorizontally,
itemSpacing = 10.dp
) { index ->
VideoPlayer(
vid = videos[index],
shouldPlay = false
)
}
}
}
VideoPlayer
#Composable
fun VideoPlayer(
vid: Video,
shouldPlay: Boolean
) {
val exoPlayer = rememberExoPlayerWithLifecycle(vid.url)
val playerView = rememberPlayerView(exoPlayer)
AndroidView(
factory = { playerView },
modifier = Modifier,
update = {
exoPlayer.playWhenReady = shouldPlay
}
)
DisposableEffect(key1 = true) {
onDispose {
exoPlayer.release()
}
}
}
rememberExoPlayerWithLifecycle
#Composable
fun rememberExoPlayerWithLifecycle(
url: String
): ExoPlayer {
val context = LocalContext.current
val exoPlayer = remember(url) {
ExoPlayer.Builder(context).build().apply {
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
repeatMode = Player.REPEAT_MODE_ONE
setHandleAudioBecomingNoisy(true)
val defaultDataSource = DefaultHttpDataSource.Factory()
val source = ProgressiveMediaSource.Factory(defaultDataSource)
.createMediaSource(MediaItem.fromUri(url))
setMediaSource(source)
prepare()
}
}
var appInBackground by remember {
mutableStateOf(false)
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner, appInBackground) {
val lifecycleObserver = getExoPlayerLifecycleObserver(exoPlayer, appInBackground) {
appInBackground = it
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
return exoPlayer
}
rememberPlayerView
#Composable
fun rememberPlayerView(exoPlayer: ExoPlayer): PlayerView {
val context = LocalContext.current
val playerView = remember {
PlayerView(context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
useController = false
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
player = exoPlayer
setShowBuffering(SHOW_BUFFERING_ALWAYS)
}
}
DisposableEffect(key1 = true) {
onDispose {
playerView.player = null
}
}
return playerView
}
build.gradle (Project)
buildscript {
ext {
compose_version = '1.3.0-beta01'
}
}
plugins {
...
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
...
build.gradle (Module)
...
dependencies {
...
//Pager
implementation "com.google.accompanist:accompanist-pager:0.26.2-beta"
//Media 3
def mediaVersion = "1.0.0-beta02"
implementation "androidx.media3:media3-exoplayer:$mediaVersion"
implementation "androidx.media3:media3-ui:$mediaVersion"
...
}
Overlapping Items
Use AspectRatioFrameLayout.RESIZE_MODE_FIT
I was having the same issue and update verticalpager to 0.25.1 exoplater to 2.18.1 and used a StyledPlayerView instead of PlayerView and it seems to be working.
I am migrating my multiple activity app to single activity app for compose.
I have created a composable Home which contains a Top app bar with a title as shown below:
#Composable
fun Home() {
val navController = rememberNavController()
var actionBarTitle by rememberSaveable { mutableStateOf("Home") }
var actionBarSubtitle by rememberSaveable { mutableStateOf("") }
Scaffold(topBar = {
Header(title = actionBarTitle, subTitle = actionBarSubtitle,
onBackPress = { navController.popBackStack() },
showInfo = true, onActionClick = {
navController.navigate(Screen.Info.route)
}, modifier = Modifier.fillMaxWidth())
}) {
AppNavigation(navController = navController, onNavigate = { title, subtitle ->
actionBarTitle = title
actionBarSubtitle = subtitle
})
}
onNavigate is triggered whenever I use navController.navigate for any screen as shown below:
onNavigate("Top up", "Please topm up with minimum of X amount")
navController.navigateTo(Screen.TopUp.route)
My question is when I use backpress I don't know to which screen composable I will be navigated to, so how can I call onNavigate to change the title.
You can observe the navigation changes using the currentBackstackEntryFlow.
#Composable
fun Home() {
val context = LocalContext.current
val navController = rememberNavController()
...
LaunchedEffect(navController) {
navController.currentBackStackEntryFlow.collect { backStackEntry ->
// You can map the title based on the route using:
actionBarTitle = getTitleByRoute(context, backStackEntry.destination.route)
}
}
...
}
Of course, you would need write this getTitleByRoute() to get the correct title in according to the navigation route.
It would be something like:
fun getTitleByRoute(context: Context, route:String): String {
return when (route) {
"Screen1" -> context.getString(R.string.title_screen_1)
// other cases
else -> context.getString(R.string.title_home)
}
}
1. Use LiveData to change the Screen Title while using Composable
implementation "androidx.compose.runtime:runtime-livedata:1.2.0-beta02"
2. Create ViewModel Class
class MainViewModel: ViewModel() {
private var _screenTitle = MutableLiveData("")
val screenTitle: LiveData<String>
get() = _screenTitle
fun setTitle(newTitle: String) {
_screenTitle.value = newTitle
}
}
3. In Your Activity Class
setContent {
Surface(color = MaterialTheme.colors.onPrimary) {
LoadMainScreen()
}
}
// Observe ScreenTitle
#Composable
fun LoadMainScreen(mainViewModel: MainViewModel = viewModel()){
val title: String by mainViewModel.screenTitle.observeAsState("")
Scaffold(
topBar = {
TopAppBar(title = { title?.let { Text(it) } },
navigationIcon = {
Icon(
imageVector = Icons.Default.Menu,
contentDescription = "Menu",
tint = Color.White
)
}
)
}
)
}
4. Change the Title Value from Screen
#Composable
fun ScreenOne(mainViewModel: MainViewModel) {
LaunchedEffect(Unit){
mainViewModel.setTitle("One")
}
}
#Composable
fun ScreenTwo(mainViewModel: MainViewModel) {
LaunchedEffect(Unit){
mainViewModel.setTitle("Two")
}
}
You can get the label of the current destination from navHostcontrollor, just use it as the title
val navController = rememberNavController()
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val title = currentBackStackEntry?.destination?.label
The default composable function is implemented as follows
/**
* Add the [Composable] to the [NavGraphBuilder]
*
* #param route route for the destination
* #param arguments list of arguments to associate with destination
* #param deepLinks list of deep links to associate with the destinations
* #param content composable for the destination
*/
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: #Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
overload it:
fun NavGraphBuilder.composable(
route: String,
label: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: #Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
this.label = label
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
You can use it this way:
composable("route", "title") {
...
}
I have this composable that used to work fine, but now after some libraries update it doesn't.
I'm using a ViewModel to save an image returned from ActivityResultContracts.TakePicture() and show it in an Image within a Box.
The PhotoButton composable is working fine and return the correct image url, but the image string in my main composable is always null.
SamplePage.kt
#Composable
fun SamplePage(navController: NavController) {
val inputViewModel = InputViewModel()
val context = LocalContext.current
Column{
InputFields(inputViewModel, navController, setPerm)
}
}
#Composable
fun InputFields(inputViewModel: InputViewModel, navController: NavController) {
val image: String by inputViewModel.image.observeAsState("")
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(contentAlignment = Alignment.Center) {
val painter = rememberImagePainter(data = image)
Image(
painter = painter,
contentScale = ContentScale.FillWidth,
contentDescription = null
)
if (painter.state !is ImagePainter.State.Success) {
Icon(
painter = painterResource(id = R.drawable.icon),
contentDescription = null
)
}
}
PhotoButton() {
inputViewModel.onImageChange(it)
}
}
}
}
class InputViewModel : ViewModel() {
private val _image: MutableLiveData<String> = MutableLiveData("")
val image: LiveData<String> = _image
fun onImageChange(newImage: String) {
_image.value = newImage
}
}
PhotoButton.kt
#Composable
fun PhotoButton(onValChange: ((String) -> Unit)?){
val context = LocalContext.current
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(storageDir, "picFromCamera")
val uri = FileProvider.getUriForFile(
context,
context.packageName.toString() + ".provider",
file
)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
if (onValChange != null) {
onValChange(uri.toString())
}
}
FAB() {
launcher.launch(uri)
}
}
You're creating a new view model on each recomposition:
val inputViewModel = InputViewModel()
Instead you should use viewModel(): it'll create a new view model on the first call, and store it for the future calls:
val inputViewModel = viewModel<InputViewModel>()
Check out more about view models usage in compose state documentation.