Firebase Barcode scanner using Jetpack compose not working - android

Trying to migrate barcode scanner to Jetpack compose and updating camera and ML Kit dependencies to latest version.
The current shows the camera view correctly, but it is not scanning the barcodes.
The ImageAnalysis analyzer runs only once.
Code
#Composable
fun CameraPreview(
data: CameraPreviewData,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { AndroidViewContext ->
PreviewView(AndroidViewContext).apply {
this.scaleType = PreviewView.ScaleType.FILL_CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Preview is incorrectly scaled in Compose on some devices without this
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
},
update = { previewView ->
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val preview: Preview = Preview.Builder()
.build()
.also {
// Attach the viewfinder's surface provider to preview use case
it.setSurfaceProvider(previewView.surfaceProvider)
}
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val barcodeAnalyser = BarcodeAnalyser { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
logError("Barcode value detected: ${barcodeValue}.")
// Other handling code
}
}
}
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
} catch (exception: Exception) {
logError("Use case binding failed with exception : $exception")
}
}, ContextCompat.getMainExecutor(context))
},
)
}
BarcodeAnalyser
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
val barcodeScanner = BarcodeScanning.getClient(options)
val imageToProcess =
InputImage.fromMediaImage(imageToAnalyze, imageProxy.imageInfo.rotationDegrees)
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
}
}
}
References
https://stackoverflow.com/a/66763853/9636037

Thanks to Adrian's comment.
It worked after the following changes.
In BarcodeAnalyser
Removed imageProxy.close() from addOnSuccessListener and addOnFailureListener. Added it to addOnCompleteListener.
Added imageProxy.close() in else condition as well.
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
// ...Same code
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
// imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
// imageProxy.close()
}
.addOnCompleteListener {
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
} else {
imageProxy.close()
}
}
}

Related

How to stop camera from working when it no longer visible in the compisition?

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))
}
)
}

Android Paging3 - refresh from a ViewModel with Compose

I'm using the Paging 3 library with Jetpack Compose and have just implemented swipe to dismiss on some paged data (using the Material library's SwipeToDismiss composable).
Once a swipe action has completed, I call a method in my ViewModel to send an update to the server (either to mark a message as read or to delete a message). Once this action has taken place, I obviously need to refresh the paging data.
My current approach is to have a call back from my ViewModel function which will then handle the refresh on the LazyPagingItems, but this feels wrong.
Is there a better approach?
My ViewModel basically looks like:
#HiltViewModel
class MessageListViewModel #Inject constructor(
private val repository: Repository
): ViewModel() {
companion object {
private const val TAG = "MessageListViewModel"
}
val messages : Flow<PagingData<Message>> = Pager(
PagingConfig(
enablePlaceholders = false,
)
) {
MessagePagingSource(repository)
}.flow.cachedIn(viewModelScope)
fun markRead(guid: String, onComplete: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(guid)
onComplete()
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $guid", e)
}
}
}
}
And in my Composable for the message list, it looks a bit like the following:
#Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
) {
val messages: LazyPagingItems<MessageSummary> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
Scaffold(
topBar = {
SmallTopAppBar (
title = {
Text(stringResource(R.string.message_list_title))
},
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
items(
items = messages,
key = { it.guid }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.guid) {
messages.refresh()
}
},
)
}
}
}
}
}
}
}
As I say, this does work, it just doesn't quite feel like the cleanest approach.
I'm fairly new to working with flows, so I don't know if there's some other trick I'm missing...
I ended up implementing something like this:
View Model:
class MessageListViewModel #Inject constructor(
private val repository: Repository,
): ViewModel() {
sealed class UiAction {
class MarkReadError(val error: Throwable): UiAction()
class MarkedRead(val id: Long): UiAction()
}
private val _uiActions = MutableSharedFlow<UiAction>()
val uiActions = _uiActions.asSharedFlow()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
)
fun markRead(id: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(id)
_uiActions.emit(UiAction.MarkedRead(id))
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $id", e)
_uiActions.emit(UiAction.MarkReadError(e))
}
}
}
}
View:
#Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
onMarkReadFailed: (String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val messages: LazyPagingItems<Message> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.uiActions.collectLatest {
when (it) {
is MessageListViewModel.UiAction.MarkReadError -> {
val msg = it.error.localizedMessage ?: it.error.message
val message = if (!msg.isNullOrEmpty()) {
context.getString(R.string.error_unknown_error_with_message, msg)
} else {
context.getString(R.string.error_unknown_error_without_message)
}
onMarkReadFailed(message)
}
is MessageListViewModel.UiAction.MarkedRead -> {
messages.refresh()
}
}
}
}
}
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
state = listState,
) {
items(
items = messages,
key = { it.id }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.id)
},
)
}
FadedDivider()
}
messages.apply {
when (loadState.append) {
is LoadState.Loading -> {
item {
LoadingRow(R.string.messages_loading)
}
}
else -> {}
}
}
}
}
}

JetpackCompose LazyList extension function stale data

I want to make without paging lib load more functionality with JetpackCompose LazyColumn. All works fine except I am sending from parent isLoading value that is coming from ViewModel. But in my extension function this value never change. Any advice? Here is my code:
TileListView
#Composable
fun LazyListState.OnBottomReached(
onEndReachedThreshold : Int = 0,
onLoadMore : suspend () -> Unit,
isLoading: Boolean = false
) {
require(onEndReachedThreshold >= 0) { "buffer cannot be negative, but was $onEndReachedThreshold" }
// Is Loading does not update from parent
val shouldLoadMore = remember {
derivedStateOf {
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
?:
return#derivedStateOf true
lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - onEndReachedThreshold
}
}
LaunchedEffect(shouldLoadMore){
snapshotFlow { shouldLoadMore.value}
.collect { if (it) {
onLoadMore()
} }
}
}
#Composable
fun TileListView(tiles: List<Tile>,
loadMore: suspend () -> Unit,
isLoading: Boolean,
onEndReachedThreshold: Int = 0,) {
val listState = rememberLazyListState()
val tileViewModel: TileViewModel = hiltViewModel()
LazyColumn(state = listState) {
items(tiles) { tile ->
tileViewModel.tileViewFactory.constructTileView(tile)
}
}
listState.OnBottomReached(onEndReachedThreshold, {
loadMore()
}, isLoading )
}
And here is where I render it
#Composable
fun Feed() {
val feedViewModel: FeedViewModel = hiltViewModel()
val tiles = feedViewModel.state.collectAsState()
val isLoading = feedViewModel.isLoading.collectAsState()
val coroutineScope = rememberCoroutineScope()
TileListView(
tiles = tiles.value,
{
coroutineScope.launch {
feedViewModel.loadMore()
}
}, isLoading = isLoading.value, onEndReachedThreshold = 0
)
}

Screen scrolls to the top (Jetpack Compose Pagination)

I am trying to do pagination in my application. First, I'm fetching 20 item from Api (limit) and every time i scroll down to the bottom of the screen, it increase this number by 20 (nextPage()). However, when this function is called, the screen goes to the top, but I want it to continue where it left off. How can I do that?
Here is my code:
CharacterListScreen:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val state = characterListViewModel.state.value
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
val listState = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
itemsIndexed(state.characters) { index, character ->
characterListViewModel.onChangeRecipeScrollPosition(index)
if ((index + 1) >= limit) {
characterListViewModel.nextPage()
}
CharacterListItem(character = character)
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
CharacterListViewModel:
#HiltViewModel
class CharacterListViewModel #Inject constructor(
private val characterRepository: CharacterRepository
) : ViewModel() {
val state = mutableStateOf(CharacterListState())
val limit = mutableStateOf(20)
var recipeListScrollPosition = 0
init {
getCharacters(limit.value, Constants.HEADER)
}
private fun getCharacters(limit : Int, header : String) {
characterRepository.getCharacters(limit, header).onEach { result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun incrementLimit() {
limit.value = limit.value + 20
}
fun onChangeRecipeScrollPosition(position: Int){
recipeListScrollPosition = position
}
fun nextPage() {
if((recipeListScrollPosition + 1) >= limit.value) {
incrementLimit()
characterRepository.getCharacters(limit.value, Constants.HEADER).onEach {result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
}
CharacterListState:
data class CharacterListState(
val isLoading : Boolean = false,
var characters : List<Character> = emptyList(),
val error : String = ""
)
I think the issue here is that you are creating CharacterListState(isLoading = true) while loading. This creates an object with empty list of elements. So compose renders an empty LazyColumn here which resets the scroll state. The easy solution for that could be state.value = state.value.copy(isLoading = true). Then, while loading, the item list can be preserved (and so is the scroll state)
Not sure if you are using the LazyListState correctly. In your viewmodel, create an instance of LazyListState:
val lazyListState: LazyListState = LazyListState()
Pass that into your composable and use it as follows:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = characterListViewModel.lazyListState) {
itemsIndexed(state.characters) { index, character ->
}
}
}
}

How to initialize camera in compose

I want to get a camera view by compose androidView, but the following code seems not work for me.
#Composable
fun CameraPreviewScreen() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val previewView = remember { PreviewView(context) }
AndroidView(viewBlock = { previewView }) {
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(it.surfaceProvider)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
}
}
The snapshot below.
The report error.
Permission Denial: can't use the camera
cannot open camera "0" without camera permission (code 1)
Since your problem seems related to camera permission, you can use Permissions APIs from Accompanist library to ask the user for permissions.
The docs show an example with camera
#Composable
private fun FeatureThatRequiresCameraPermission(
navigateToSettingsScreen: () -> Unit
) {
// Track if the user doesn't want to see the rationale any more.
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
}

Categories

Resources