The composable host an AndroidView that is a FragmentContainerView which has multiple child Fragments on back press of the FragmentContainerView we want to close the #Game composable.
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
You can control if your Game composable is part of the composition from its parent composable with some state and a simple if statement.
To change the state on back press you can use the BackHandler composable.
A working example:
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
BackHandler(enabled = gameIsActive) {
gameIsActive = false
}
if (gameIsActive) {
Game()
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
If you will have to close the game from some other handler(s) from inside the Game composable then taking this approach might be better
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
if (gameIsActive) {
Game(onClose = { gameIsActive = false })
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null, onClose: () -> Unit) {
BackHandler(enabled = true) {
// this way you can even pass some result back if you parametrize
// this callback, for example won/lost/draw/quit.
onClose()
}
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
// call onClose() from some other handler
}
}
Related
I want to avoid multiple function call when LaunchEffect key triggers.
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
when first composition isEnableState and viewModel.uiState both will trigger twice and call viewModel.scanState(bluetoothAdapter).
isEnableState is a Boolean type and viewModel.uiState is sealed class of UI types.
var uiState by mutableStateOf<UIState>(UIState.Initial)
private set
var isEnableState by mutableStateOf(false)
private set
So how can we handle idiomatic way to avoid duplicate calls?
Thanks
UPDATE
ContentStateful
#Composable
fun ContentStateful(
context: Context = LocalContext.current,
viewModel: ContentViewModel = koinViewModel(),
) {
LaunchedEffect(key1 = viewModel.isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
LaunchedEffect(viewModel.previous) {
viewModel.changeDeviceSate()
}
ContentStateLess{
viewModel.isEnableState = false
}
}
ContentStateLess
#Composable
fun ContentStateLess(changeAction: () -> Unit) {
Button(onClick = { changeAction() }) {
Text(text = "Click On me")
}
}
ContentViewModel
class ContentViewModel : BaseViewModel() {
var uiState by mutableStateOf<UIState>(UIState.Initial)
var isEnableState by mutableStateOf(false)
fun scanState(bluetoothAdapter: BluetoothAdapter) {
if (isEnableState && isInitialOrScanningUiState()) {
// start scanning
} else {
// stop scanning
}
}
private fun isInitialOrScanningUiState(): Boolean {
return (uiState == UIState.Initial || uiState == UIState.ScanningDevice)
}
fun changeDeviceSate() {
if (previous == BOND_NONE && newState == BONDING) {
uiState = UIState.LoadingState
} else if (previous == BONDING && newState == BONDED) {
uiState = UIState.ConnectedState(it)
} else {
uiState = UIState.ConnectionFailedState
}
}
}
scanState function is start and stop scanning of devices.
I guess the answer below would work or might require some modification to work but logic for preventing double clicks can be used only if you wish to prevent actions happen initially within time frame of small interval. To prevent double clicks you you set current time and check again if the time is above threshold to invoke click callback. In your situation also adding states with delay might solve the issue.
IDLE, BUSY, READY
var launchState by remember {mutableStateOf(IDLE)}
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
if(launchState != BUSY){
viewModel.scanState(bluetoothAdapter)
if(launchState == IDLE){ launchState = BUSY)
}
}
LaunchedEffect(launchState) {
if(launchState == BUSY){
delay(50)
launchState = READY
}
}
I'm trying to send a variable value from my ViewModel to my composable screen. I tried using the debugger to find out where it gets stuck. It seems like it sends the value but never actually receives it.
This is the code I'm using:
NewEvent.kt
#Composable
fun NewEvent(
viewModel: NewEventViewModel = viewModel(),
navController: NavController
){
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
}
changeEventType() gets called here
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}
NewEventViewModel.kt
private val newEventTypeChannel = Channel<EventType>()
val newEventType = newEventTypeChannel.receiveAsFlow()
fun changeEventType(newEventType: EventType){
viewModelScope.launch {
newEventTypeChannel.send(newEventType)
}
}
I downloaded a sample project from GitHub using this exact implementation and it worked, I'm not sure what I'm missing here.
If you want to display toast message, you don't have to create separate composable function for it. Since Toast is dynamic and does not need to be recomposed, you don't need composable function for it.
It would be more clear and better to implement it like this (in one composable function)
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}
I am creating an app that makes use of a physical button on the device.
This button will have a different functionality depending on the screen that is active.
With Activities what I would do would be to have an Activity for each screen and in each one I would override the onKeyDown function. How would I do this with a single activity that navigates between different Jetpack Compose screens?
According to the Android documentation the correct way would be something like this...
Box(modifier = Modifier
.onKeyEvent {
Log.e("Pressed", it.nativeKeyEvent.keyCode.toString())
true
}
.focusable()
.fillMaxSize()
.background(Color.Gray)
) {
// All screen components
}
But this only works when one of the elements on the screen is focused and what I require is that it always works or not, is there a way to achieve this?
One option is to keep the focus on the view so that the Modifier.onKeyEvent always works. Note that it may conflict with other focusable views, like TextField, so all these views should be children(at any level) of this always-focusable view.
val focusRequester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }
Box(
Modifier
.focusRequester(focusRequester)
.onFocusChanged {
hasFocus = it.hasFocus
}
.focusable()
.onKeyEvent {
TODO()
}
) {
}
if (!hasFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
Another option is to create compositional local handlers and pass events from your activity:
class MainActivity : AppCompatActivity() {
private val keyEventHandlers = mutableListOf<KeyEventHandler>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalKeyEventHandlers provides keyEventHandlers) {
// your app
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return keyEventHandlers.reversed().any { it(keyCode, event) } || super.onKeyDown(keyCode, event)
}
}
val LocalKeyEventHandlers = compositionLocalOf<MutableList<KeyEventHandler>> {
error("LocalKeyEventHandlers is not provided")
}
typealias KeyEventHandler = (Int, KeyEvent) -> Boolean
#Composable
fun ListenKeyEvents(handler: KeyEventHandler) {
val handlerState = rememberUpdatedState(handler)
val eventHandlers = LocalKeyEventHandlers.current
DisposableEffect(handlerState) {
val localHandler: KeyEventHandler = {
handlerState.value(it)
}
eventHandlers.add(localHandler)
onDispose {
eventHandlers.remove(localHandler)
}
}
}
Usage:
ListenKeyEvents { code, event ->
TODO()
}
We have the below compose which works well.
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
But if we have the timerStartStop passed through the function parameter, I cannot do as below
#Composable
fun MyInnerControl(timerStartStop: Boolean) {
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I can pass as below, but then I have to change my timerStartStop to timerStartStop.value
#Composable
fun MyInnerControl(timerStartStop: MutableState<Boolean>) {
Button(onClick = {
timerStartStop.value = !timerStartStop.value
}) {
Text(if (timerStartStop.value) "Stop" else "Start")
}
}
Is there any way to have the MutableState<Boolean> passed over the argument, yet within the function we can use the delegate approach, i.e. using timerStartStop instead of timerStartStop.value?
Jetpack Compose tells you to respect Single Source of Truth principle, which means that you can not pass state as argument if you want to change that state inside your function.
So generally you have to choose one of two approaches:
Pass both timerStartStop variable and onClick callback
#Composable
fun MyInnerControl(timerStartStop: Boolean, onClick: ()->Unit) {
Button(onClick = onClick) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
Pass none of these
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I think it's better to apply proper state hoisting methods and give MyInnerControl the ability to both read and edit the parameter in the following way.
#Composable
fun ParentComposable() {
val isTimerRunning by remember { mutableStateOf(false) } }
MyInnerControl(
isTimerRunning = isTimerRunning,
flipTimerState {
isTimerRunning = !isTimerRunning
}
}
}
#Composable
fun MyInnerControl(
isTimerRunning: Boolean,
flipTimerState: () -> Unit,
) {
Button(
onClick = { flipTimerState() }
) {
Text(if (isTimerRunning) "Stop" else "Start")
}
}
I'm building a jetpack compose app and I want my view model to tell my compose function to display a snack bar by sending it an event. I have read multiple blog posts about the Single Live Event case with Kotlin and I tried to implement it with Compose and Kotlin Flow. I managed to send the event from the view model (I see it in the logs) but I don't know how to receive it in the composable function. Can someone help me figure it out please? Here is my implementation.
class HomeViewModel() : ViewModel() {
sealed class Event {
object ShowSheet : Event()
object HideSheet : Event()
data class ShowSnackBar(val text: String) : Event()
}
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow: Flow<Event> = eventChannel.receiveAsFlow()
fun showSnackbar() {
Timber.d("Show snackbar button pressed")
viewModelScope.launch {
eventChannel.send(Event.ShowSnackBar("SnackBar"))
}
}
}
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
) {
val context = LocalContext.current
val scaffoldState = rememberScaffoldState()
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val lifecycleOwner = LocalLifecycleOwner.current
val eventsFlowLifecycleAware = remember(viewModel.eventsFlow, lifecycleOwner) {
eventsFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
LaunchedEffect(sheetState, scaffoldState.snackbarHostState) {
eventsFlowLifecycleAware.onEach {
when (it) {
HomeViewModel.Event.ShowSheet -> {
Timber.d("Show sheet event received")
sheetState.show()
}
HomeViewModel.Event.HideSheet -> {
Timber.d("Hide sheet event received")
sheetState.hide()
}
is HomeViewModel.Event.ShowSnackBar -> {
Timber.d("Show snack bar received")
scaffoldState.snackbarHostState.showSnackbar(
context.getString(it.resId)
)
}
}
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Text("Sheet")
}
) {
Button(
onClick = {
viewModel.showSheet()
}
) {
Text("Show SnackBar")
}
}
}
For reference, I've used these blog posts:
Android SingleLiveEvent Redux with Kotlin Flow
A safer way to collect flows from Android UIs
Ok, I was using the wrong approach, I must not send events, I must update the view state and check if I should show the snackbar when recomposing. Something like that:
You store the SnackBar state in the view model
class HomeViewModel: ViewModel() {
var isSnackBarShowing: Boolean by mutableStateOf(false)
private set
private fun showSnackBar() {
isSnackBarShowing = true
}
fun dismissSnackBar() {
isSnackBarShowing = false
}
}
And in the view you use LaunchedEffect to check if you should show the snackbar when recomposing the view
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
) {
val onDismissSnackBarState by rememberUpdatedState(newValue = onDismissSnackBar)
if (isSnackBarShowing) {
val snackBarMessage = "Message"
LaunchedEffect(isSnackBarShowing) {
try {
when (scaffoldState.snackbarHostState.showSnackbar(
snackBarMessage,
)) {
SnackbarResult.Dismissed -> {
}
}
} finally {
onDismissSnackBarState()
}
}
}
Row() {
Text(text = "Hello")
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
viewModel.showSnackBar()
}
) {
Text(text = "Show SnackBar")
}
}
}
I think you have to collect eventsFlowLifecycleAware as a state to trigger a Composable correctly.
Try removing the LaunchedEffect block, and using it like this:
val event by eventsFlowLifecycleAware.collectAsState(null)
when (event) {
is HomeViewModel.Event.ShowSnackBar -> {
// Do stuff
}
}