Jetpack compose how to recompose when coming to foreground - android-jetpack-compose

I'd like to make status bar hidden and I've managed to do it like so using the accompanist library:
val systemUiController = rememberSystemUiController()
systemUiController.isStatusBarVisible = false
The issue is that when the app goes to background and comes to foreground, this piece of code is not run and therefore the status bar is shown again. How can I fix that?
Thanks.

You can use OnLifecycleEvent from this answer.
val systemUiController = rememberSystemUiController()
OnLifecycleEvent { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME,
Lifecycle.Event.ON_START,
-> {
systemUiController.isStatusBarVisible = false
}
else -> Unit
}
}
OnLifecycleEvent:
#Composable
fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) {
val eventHandler = rememberUpdatedState(onEvent)
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
DisposableEffect(lifecycleOwner.value) {
val lifecycle = lifecycleOwner.value.lifecycle
val observer = LifecycleEventObserver { owner, event ->
eventHandler.value(owner, event)
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
}

Related

How to take pictures using only the front camera in jetpack compose?

I am trying to take pictures from front camera using jetpack compose. I tried it using LocalContext.current.getCameraProvider() but it opens only the back camera. I do not want to open the back camera. I tried this to open the camera but I am unable to open the front camera.
I am new to jetpack compose, someone please help me.
This should be the way:
#Composable
fun CameraView(onImageCaptured: (Uri, Boolean) -> Unit, onError: (ImageCaptureException) -> Unit) {
val context = LocalContext.current
var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
val imageCapture: ImageCapture = remember {
ImageCapture.Builder().build()
}
val galleryLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
if (uri != null) onImageCaptured(uri, true)
}
CameraPreviewView(
imageCapture,
lensFacing
) { cameraUIAction ->
when (cameraUIAction) {
is CameraUIAction.OnCameraClick -> {
imageCapture.takePicture(context, lensFacing, onImageCaptured, onError)
}
is CameraUIAction.OnSwitchCameraClick -> {
lensFacing =
if (lensFacing == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT
else
CameraSelector.LENS_FACING_BACK
}
is CameraUIAction.OnGalleryViewClick -> {
if (true == context.getOutputDirectory().listFiles()?.isNotEmpty()) {
galleryLauncher.launch("image/*")
}
}
}
}
}

Jetpack Compose - how to check if keyboard is open or closed

I'm using Jetpack Compose and trying to find a way to detect if the keyboard is open.
I've tried to use the below code, but I get an error stating Unresolved reference: ime. When I click on the recommended imports (the 2 shown below), this error still remains.
import android.view.WindowInsets
import android.view.WindowInsets.Type.ime
#Composable
fun signInView() {
val isVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
}
How can I resolve this?
Add the dependencies for the artifacts you need in the build.gradle file for your app or module:
dependencies {
implementation "androidx.compose.foundation:foundation:1.3.1"
}
android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
kotlinOptions {
jvmTarget = "1.8"
}
}
Example:
#Composable
fun signInView() {
var isVisible by remember { mutableStateOf(false) }
val ime = androidx.compose.foundation.layout.WindowInsets.ime
val navbar = androidx.compose.foundation.layout.WindowInsets.navigationBars
var keyboardHeightDp by remember { mutableStateOf(0.dp) }
val localDensity = LocalDensity.current
LaunchedEffect(localDensity.density) {
snapshotFlow {
ime.getBottom(localDensity) - navbar.getBottom(localDensity)
}.collect {
val currentKeyboardHeightDp = (it / localDensity.density).dp
keyboardHeightDp = maxOf(currentKeyboardHeightDp, keyboardHeightDp)
isVisible = currentKeyboardHeightDp == keyboardHeightDp
}
}
}

Android dataStore with flow not get update after edit

I'm use DataStore with flow but I cant get any update on the flow when editing DataStore.
Store.kt
private class IStore(private val context: Context): Store {
val eventIDKey = stringPreferencesKey("EventID")
override suspend fun setEventID(eventID: String) {
context.dataStoreSettings.edit { settings ->
settings[eventIDKey] = eventID
}
}
override fun getEventID(): Flow<String> {
return context.dataStoreSettings.data.map { settings -> settings[eventIDKey].orEmpty() }
}
}
and manipulate getEventID() with data from room database in event service
EventService.kt
fun getSelectedEventLive() = store.getEventID()
.onEach { Log.d("EventService", "income new event id $it") }
.flatMapConcat { if(it.isNotBlank()) eventDao.get(it) else flowOf(null) }
onEach called when I collect the data but when updated it's not called again and need to close and open the app to show the latest data
MainViewModel.kt
val selectedEvent = eventService.getSelectedEventLive()
.stateIn(viewModelScope, SharingStarted.Lazily, null)
and use on Compose with this
val currentEvent by mainViewModel.selectedEvent.collectAsState()
Maybe I doing wrong or maybe there is something I miss?
Usually, you want to use flow.collect {...}, since Flow is cold and need to know that it is being collected to start producing new values.
// MainViewModel.kt
private val _selectedEvent = MutableStateFlow<TypeOfYourEvent>()
val selectedEvent: StateFlow<TypeOfYourEvent> = _selectedEvent
init {
viewModelScope.launch {
getSelectedEventLive().collect { it ->
_selectedEvent.value = it
}
}
}
This example should be fine with your composable's code, you still can collect selectedEvent as state.
Yeah i found the solusion its works if i change the flatMapConcat with flatMapLatest in EventService.kt
fun getSelectedEventLive() = store.getEventID()
.filterNot { it.isBlank() }
.flatMapLatest { eventDao.get(it) }

Jetpack Compose - Speech Recognition

Do you know how to apply Speech Recognition (SpeechRecognizer) in Jetpack Compose?
Something like this, but in Compose.
I followed the steps in this video:
Added these permissions in the manifest:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Wrote this code in MainActivity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PageUi()
}
}
}
#Composable
fun PageUi() {
val context = LocalContext.current
val talk by remember { mutableStateOf("Speech text should come here") }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = talk,
style = MaterialTheme.typography.h4,
modifier = Modifier
.fillMaxSize(0.85f)
.padding(16.dp)
.background(Color.LightGray)
)
Button(onClick = { askSpeechInput(context) }) {
Text(
text = "Talk", style = MaterialTheme.typography.h3
)
}
}
}
fun askSpeechInput(context: Context) {
if (!SpeechRecognizer.isRecognitionAvailable(context)) {
Toast.makeText(context, "Speech not available", Toast.LENGTH_SHORT).show()
} else {
val i = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
i.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
i.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault())
i.putExtra(RecognizerIntent.EXTRA_PROMPT, "Talk")
//startActivityForResult(MainActivity(),i,102)
}
}
#Preview(showBackground = true)
#Composable
fun PageShow() {
PageUi()
}
But I have no idea how to use startActivityForResult in Compose and do the rest?
And when I test it so far on my phone (or emulator) it always ends up with the toast message!
I am going to explain my own implementation. Let me give you a general idea first, and then I am going to explain each step. So first you need to ask for permissions every time and then if permission is granted then you should start an intent in order to hear what the user says. What the user says is saved on a variable to a View Model. The variable on the View Model is being observed by the composable so you can get the data.
1) Add this to your Manigest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="your.package">
// Add uses-permission
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
[...]
[...]
[...]
// Add above the last line </manifest> like so:
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
</manifest>
2) Create a ViewModel
class ScreenViewModel : ViewModel() {
var textFromSpeech: String? by mutableStateOf(null)
}
You need the ViewModel in order to observe the variable from composable and implement your code logic for clean architecture.
3) Implement asking for permission
In build.gradle add the following:
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
Then create a composable like so:
#ExperimentalPermissionsApi
#Composable
fun OpenVoiceWithPermission(
onDismiss: () -> Unit,
vm: ScreenViewModel,
ctxFromScreen: Context,
finished: () -> Unit
) {
val voicePermissionState = rememberPermissionState(android.Manifest.permission.RECORD_AUDIO)
val ctx = LocalContext.current
fun newIntent(ctx: Context) {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts(
"package",
BuildConfig.APPLICATION_ID, null
)
intent.data = uri
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
ctx.startActivity(intent)
}
PermissionRequired(
permissionState = voicePermissionState,
permissionNotGrantedContent = {
DialogCustomBox(
onDismiss = onDismiss,
dialogBoxState = DialogLogInState.REQUEST_VOICE,
onRequestPermission = { voicePermissionState.launchPermissionRequest() }
)
},
permissionNotAvailableContent = {
DialogCustomBox(
onDismiss = onDismiss,
dialogBoxState = DialogLogInState.VOICE_OPEN_SYSTEM_SETTINGS,
onOpenSystemSettings = { newIntent(ctx) }
)
}
) {
startSpeechToText(vm, ctxFromScreen, finished = finished)
}
}
DialogBox you can create your own custom as I have done or use the standard version, this is up to you and out of the scope of this answer.
On the above code if permission is granted you move automatically to this piece of code: startSpeechToText(vm, ctxFromScreen, finished = finished) which you have to implement next.
4) Implementing Speech Recognizer
fun startSpeechToText(vm: ScreenViewModel, ctx: Context, finished: ()-> Unit) {
val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(ctx)
val speechRecognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
speechRecognizerIntent.putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
)
// Optionally I have added my mother language
speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, "el_GR")
speechRecognizer.setRecognitionListener(object : RecognitionListener {
override fun onReadyForSpeech(bundle: Bundle?) {}
override fun onBeginningOfSpeech() {}
override fun onRmsChanged(v: Float) {}
override fun onBufferReceived(bytes: ByteArray?) {}
override fun onEndOfSpeech() {
finished()
// changing the color of your mic icon to
// gray to indicate it is not listening or do something you want
}
override fun onError(i: Int) {}
override fun onResults(bundle: Bundle) {
val result = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
if (result != null) {
// attaching the output
// to our viewmodel
vm.textFromSpeech = result[0]
}
}
override fun onPartialResults(bundle: Bundle) {}
override fun onEvent(i: Int, bundle: Bundle?) {}
})
speechRecognizer.startListening(speechRecognizerIntent)
}
With this implementation it is very customizable and you do not get this pop up from google. So you can inform the user that his device is listening with your own unique way!
5) Call from your composable the function to start listening:
#ExperimentalPermissionsApi
#Composable
fun YourScreen() {
val ctx = LocalContext.current
val vm: ScreenViewModel = viewModel()
var clickToShowPermission by rememberSaveable { mutableStateOf(false) }
if (clickToShowPermission) {
OpenVoiceWithPermission(
onDismiss = { clickToShowPermission = false },
vm = vm,
ctxFromScreen = ctx
) {
// Do anything you want when the voice has finished and do
// not forget to return clickToShowPermission to false!!
clickToShowPermission = false
}
}
}
So on you code everytime you call clickToShowPermission = true you can start listening what the user says...
use registerForActivityResult(ActivityResultContract, ActivityResultCallback) passing in a androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult object for the ActivityResultContract.
By declaring the StartActivityForResult callback function
val startLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {it ->
//you implement
}
start Intent
startLauncher.launch(intent)
test simple example
#Composable
fun TestStartForResult() {
val content = LocalContext.current
val startLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult())
{
Toast.makeText(content, "Result", Toast.LENGTH_SHORT).show()
}
Button(onClick = {
startLauncher.launch(Intent(content,TestActivity::class.java))
}) {
Text("start")
}
}

Jetpack Compose: Bottom bar navigation not responding after deep-linking

I have setup a bottom bar in my new Jetpack Compose app with 2 destinations. I have tried to follow the samples from Google.
So for example it looks something like this:
#Composable
fun MyBottomBar(navController: NavHostController) {
val items = listOf(
BottomNavigationScreen.ScreenA,
BottomNavigationScreen.ScreenB
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation {
items.forEach { screen ->
BottomNavigationItem(
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
icon = { Icon(imageVector = screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.label)) }
)
}
}
}
This all works fine and I'm able to navigate between the two destinations. However, I also have a deep-link to ScreenB. Once this has been invoked, pressing the ScreenA button seems to do nothing (If I add logging I can see that currentDestination is being repeatedly set to ScreenB) but pressing back returns to the startDestination of ScreenA.
My workaround at the moment is to remove the restoreState = true line from the sample code.
My suspicion is that something about the deep-link is being persisted and although it tries to go to ScreenA the navigation component says that it's got a deep-link pointing to ScreenB so it just goes there. I've tried resetting the activity intent so that it has no flags and no data in the intent, I've even tried changing the intent action type but all to no avail.
I am using Compose 1.0.0-rc02 and Compose Navigation 2.4.0-alpha04.
Am I doing something wrong or is this a bug?
I know you got this code from the official documentation, but I don't think it works well for bottom navigation. It keeps the elements in the navigation stack, so pressing the back button from ScreenB will take you back to ScreenA, which does not seem to me to be the right behavior in this case.
That's why it's better to remove all elements from the stack, so that only one of the tabs is always left. And with saveState you won't lose state anyway. This can be done as follows:
fun NavHostController.navigateBottomNavigationScreen(screen: BottomNavigationScreen) = navigate(screen.route) {
val navigationRoutes = BottomNavigationScreen.values()
.map(BottomNavigationScreen::route)
val firstBottomBarDestination = backQueue
.firstOrNull { navigationRoutes.contains(it.destination.route) }
?.destination
if (firstBottomBarDestination != null) {
popUpTo(firstBottomBarDestination.id) {
inclusive = true
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
And use it like this:
BottomNavigationItem(
onClick = {
navController.navigateBottomNavigationScreen(screen)
},
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
icon = { Icon(imageVector = screen.icon, contentDescription = null) },
label = { Text(screen.label) }
)
For the same reasons, I wouldn't use deep link navigation in this case. Instead, you process them manually. You can use a view model to not re-process deep link if you leave bottom navigation view and come back:
class DeepLinkProcessingViewModel : ViewModel() {
private var deepLinkProcessed = false
fun processDeepLinkIfAvailable(context: Context): String? {
if (!deepLinkProcessed) {
val activity = context.findActivity()
val intentData = activity?.intent?.data?.toString()
deepLinkProcessed = true
return intentData
}
return null
}
}
And using this view model you can calculate start destination like this:
val context = LocalContext.current
val deepLinkProcessingViewModel = viewModel<DeepLinkProcessingViewModel>()
val startDestination = rememberSaveable(context) {
val deepLink = deepLinkProcessingViewModel.processDeepLinkIfAvailable(context)
if (deepLink == "example://playground") {
// deep link handled
BottomNavigationScreen.ScreenB.route
} else {
// default start destination
BottomNavigationScreen.ScreenA.route
}
}
NavHost(navController = navController, startDestination = startDestination) {
...
}
Or, if you have several navigation elements in front of the bottom navigation and you don't want to lose them with a deep link, you can do it as follows:
val navController = rememberNavController()
val context = LocalContext.current
val deepLinkProcessingViewModel = viewModel<DeepLinkProcessingViewModel>()
LaunchedEffect(Unit) {
val deepLink = deepLinkProcessingViewModel.processDeepLinkIfAvailable(context) ?: return#LaunchedEffect
if (deepLink == "example://playground") {
navController.navigateBottomNavigationScreen(BottomNavigationScreen.ScreenB)
}
}
NavHost(
navController = navController,
startDestination = BottomNavigationScreen.ScreenA.route
) {
findActivity:
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
Looks like it's finally fixed in the 2.4.0-beta02 release; so it was a bug after all.
I was able to add the saveState and restoreState commands back into my BottomBar (as per the documentation) and following a deep-link I was now still able to click the initial destination.

Resources