When I am using these 2 composables, if I click on the back button, the app is closing, which is expected.
#Composable
fun Greeting(modifier: Modifier = Modifier, name: String) {
val focusRequester by remember { mutableStateOf(FocusRequester()) }
Container(modifier = modifier) {
Column(Modifier.padding(start = 24.dp)) {
Button(onClick = { /*TODO*/ }) {
Text("1")
}
Button(modifier = modifier.focusRequester(focusRequester), onClick = { /*TODO*/ }) {
Text("2")
}
Button(onClick = { /*TODO*/ }) {
Text("3")
}
Button(onClick = { /*TODO*/ }) {
Text("4")
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
#Composable
fun Container(modifier: Modifier = Modifier, content: #Composable () -> Unit) {
val focusManager = LocalFocusManager.current
Box {
Box {
content()
}
}
}
But, if I make a change on the Container composable (I set it as focusable) :
#Composable
fun Container(modifier:Modifier = Modifier, content: #Composable () -> Unit) {
val focusManager = LocalFocusManager.current
Box {
Box(modifier = Modifier.focusable()){
content()
}
}
}
I have to press the back button twice to have the application to exit (on the first click, it removes the focus, and on the second click, it exit the app).
It seems strange to me that the back button is interfering with focus management, but I suppose that once you set your composable focusable, you have to handle the back button manually ?
All I can think of is that when you do not set any Modifier, the default behavior is focusable(false) , that would explain the different behavior in both cases, and make sense in mobile perspective.
Related
I want to use verticalArrangement = Arrangement.SpaceBetween in Column to align view top and bottom. I did this before without any problem. Now I added AnimatedVisibility to look good animation when my condition true. But It overlapping the view. I can't understand that things.
PairContentStateLess
#Composable
fun PairContentStateLess(
viewModel: XyzPairViewModel,
scanning: State<Boolean>,
onResume: () -> Unit,
onPause: () -> Unit,
tryAgainAction: () -> Unit,
openSettingAction: () -> Unit,
onResumeScan: () -> Unit,
onPauseScan: () -> Unit,
) {
AnimatedVisibility(visible = true) {
AppBarScaffold() {
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = if (viewModel.isBluetoothEnabled && scanning.value) {
Arrangement.Top
} else {
Arrangement.SpaceBetween
}
) {
PairScreenImage()
PairDeviceDescription()
if (viewModel.isBluetoothEnabled) {
PressAndHoldDescription()
WaitingToPair(scanning.value)
UnableToPair(scanning.value)
} else {
BluetoothTurnOnWarning()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView { tryAgainAction() }
OpenDeviceSettingsButtonView { openSettingAction() }
}
}
}
}
}
I am only adding UnableToPair code now. If I am using it working fine without any problem.
UnableToPair
fun ColumnScope.UnableToPair(scanning: Boolean) {
if (!scanning) {
WarningBoxView()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView {
}
}
}
UI look like this
Now main problems come
when I tried to use AnimatedVisibility. I don't know what happened in the WarningBoxView and TryAgainButtonView.
#Composable
fun ColumnScope.UnableToPair(scanning: Boolean) {
AnimatedVisibility (!scanning) {
WarningBoxView()
Spacer(modifier = Modifier.weight(1f))
TryAgainButtonView {
}
}
}
UI look like this and I hide sensitive data from black box in image. Please notice only button and warning box.
Thanks
use column inside animated visibility again
I am using MutableStateFlow UI State in jetpack compose. I am not getting proper flow of my UI event. In UI state there is Empty, Loading, Success and Error state. I setup a Empty state when I initialise a variable. When I am starting to call api before that I am trigger Loading state. On that basis I am triggering Success or Error event.
Note: I am not adding imports and package name. If you want to see full code please click a name of class you will redirect to my repository.
MainActivityViewModel.kt
class MainActivityViewModel(private val resultRepository: ResultRepository) : ViewModel() {
val stateResultFetchState = MutableStateFlow<ResultFetchState>(ResultFetchState.OnEmpty)
fun getSportResult() {
viewModelScope.launch {
stateResultFetchState.value = ResultFetchState.IsLoading
val result = resultRepository.getSportResult()
delay(5000)
result.handleResult(
onSuccess = { response ->
if (response != null) {
stateResultFetchState.value = ResultFetchState.OnSuccess(response)
} else {
stateResultFetchState.value = ResultFetchState.OnEmpty
}
},
onError = {
stateResultFetchState.value =
ResultFetchState.OnError(it.errorResponse?.errorMessage)
}
)
}
}
}
MainActivity.kt
internal lateinit var networkConnection: NetworkConnection
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
networkConnection = NetworkConnection(application)
setContent {
SportsResultTheme {
SetupConnectionView()
}
}
}
}
#Composable
fun SetupConnectionView() {
val isConnected = networkConnection.observeAsState()
if (isConnected.value == true) {
NavigationGraph()
} else {
NoInternetView()
}
}
#Composable
fun NoInternetView() {
Box(
modifier = Modifier
.fillMaxSize()
.background(getBackgroundColor()),
contentAlignment = Center,
) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.nointernet))
LottieAnimation(
composition,
iterations = LottieConstants.IterateForever
)
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SetupMainActivityView(
viewModel: MainActivityViewModel = koinViewModel(),
navigateToNext: (state: String) -> Unit,
) {
Scaffold(topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
backgroundColor = getBackgroundColor(),
elevation = 0.dp
)
}, content = { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(getBackgroundColor())
.padding(padding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
viewModel.getSportResult()
}) {
Text(text = stringResource(id = R.string.get_result))
}
}
})
when (val state = viewModel.stateResultFetchState.collectAsState().value) {
is ResultFetchState.OnSuccess -> {
navigateToNext("loading $state")
}
is ResultFetchState.IsLoading -> {
LoadingFunction()
}
is ResultFetchState.OnError -> {}
is ResultFetchState.OnEmpty -> {}
}
}
#Composable
fun LoadingFunction() {
Column(
modifier = Modifier
.fillMaxSize()
.background(getBackgroundColor()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
}
I am adding my navigation graph so you will clearly see what I am trying to do.
NavigationGraph.kt
#Composable
internal fun NavigationGraph() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Home.route) {
composable(ScreenRoute.Home.route) {
SetupMainActivityView { state ->
navController.navigate(ScreenRoute.Result.route + "/{$state}")
}
}
composable(
ScreenRoute.Result.route + "/{state}",
arguments = listOf(
navArgument("state") { type = NavType.StringType }
)
) { backStackEntry ->
ResultScreen(backStackEntry.arguments?.getString("state").orEmpty())
}
}
}
ResultScreen.kt
#Composable
fun ResultScreen(state: String) {
Log.e("TAG", "ResultScreen: $state" )
}
Actual Output
when you click on Button it started Loading screen. After Loading screen my Button screen appears than my Result Screen appears. You can see in my video.
Button Screen -> Loading Screen -> Again Button Screen -> Result Screen.
Expected Output
Button Screen -> Loading Screen -> Result Screen.
My Github project link. Can you guys guide me what I am doing wrong here. Many Thanks
UPDATE
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SetupMainActivityView(
viewModel: MainActivityViewModel = koinViewModel(),
navigateToNext: (nearestResult: ArrayList<NearestResult>) -> Unit,
) {
Scaffold(topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
backgroundColor = getBackgroundColor(),
elevation = 0.dp
)
}, content = { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(getBackgroundColor()),
contentAlignment = Center
) {
when (val state = viewModel.stateResultFetchState.collectAsState().value) {
is ResultFetchState.OnSuccess -> {
LaunchedEffect(Unit) {
navigateToNext(state.nearestResult)
}
}
is ResultFetchState.IsLoading -> {
LoadingFunction()
}
is ResultFetchState.OnError,
is ResultFetchState.OnEmpty -> {
ActivityContent(viewModel)
}
}
}
})
}
After doing this my ResultScreen calling twice. Is it normal?
During loading you overlap the button view with the loading view, but when you succeed you remove the loading view, so the button view appears for the transition.
Depending on the expected behavior, you can move your when inside the content, and display content only on empty/error - it might make sense to leave the option to click back to cancel the request.
content = { padding ->
Box(Modifier.fillMaxSize().padding(padding).background(getBackgroundColor())) {
when (val state = viewModel.stateResultFetchState.collectAsState().value) {
is ResultFetchState.OnSuccess -> {
LaunchedEffect(Unit){
navigateToNext("loading $state")
}
}
is ResultFetchState.IsLoading -> {
LoadingFunction()
}
is ResultFetchState.OnError, is ResultFetchState.OnEmpty -> {
YourContent()
}
}
}
})
Or add LoadingFunction() inside ResultFetchState.OnSuccess, so that this view doesn't disappear from the screen during the transition.
is ResultFetchState.OnSuccess -> {
LaunchedEffect(Unit){
navigateToNext("loading $state")
}
LoadingFunction()
}
Also see this answer for why calling navigateToNext as you do is unsafe and why I've added LaunchedEffect.
I'm trying to convert my old XML layout to #Composable classes in a test app I made, but I encountered a problem with my "loading" screen.
The app has a button to fetch quotes from a free API and, when clicked, a loading screen should appear on top of the page, effectively blocking possible further interactions with the button.
The loading screen was previously RelativeLayout with a ProgressBar inside.
Now with Compose I cannot manage to have this loading screen to be "on top" because the buttons still show above it and remain clickable.
The same "wrong" behaviour can also be reproduced with XML layouts when using MaterialButtons, whereas with AppCompatButtons the issue is solved.
Is there a way to make this work in compose?
p.s. here is my solution with Compose
#Composable
fun QuoteButton(text: String, onClick: () -> Unit) {
Button(
onClick,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 5.dp)
) {
Text(text = text)
}
}
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
content()
} else {
content()
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
QuoteLoading(isLoading = loadingState) {
Column {
QuoteDisplay(textState)
QuoteButton(getString(R.string.button_fetch_quote)) {
viewModel.setEvent(Event.GetQuote)
}
QuoteButton(getString(R.string.button_save_quote)) {
viewModel.setEvent(Event.SaveQuote)
}
QuoteButton(getString(R.string.button_clear_quotes)) {
viewModel.setEvent(Event.ClearQuote)
}
}
}
}
}
}
}
}
private val DarkColorPalette = darkColors(
primary = Color(0xFFBB86FC),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
private val LightColorPalette = lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
#Composable
fun ComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
MaterialTheme(
colors = if (darkTheme) DarkColorPalette else LightColorPalette,
content = content
)
}
First of all put your progress bar in a dialogue that is not cancellable by any input except loading has been finished.
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnBackPress = false,
dismissOnClickOutside = false),
content = {
CircularProgressIndicator()
}
)
}
content()
} else {
content()
}
What i am trying to do is to close the TopBar dropdown menu after clicking the dropdown item. It can be easily done, if i am putting the dropdown items directly inside the dropdown menu. But here i am trying to separate it as a composable for readability.
Here is my TopAppBar
#Composable
fun TopBar(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
event: (AdminLaunchEvents) -> Unit,
navController: NavHostController
) {
val openDialog = remember { mutableStateOf(false) }
TopAppBar(
title = {
Text(text = "Main App Admin Area", fontSize = 18.sp)
},
actions = {
OverflowMenu() {
SettingsDropDownItem(onClick = {})
ModeDropDownItem(onClick = {})
LogoutDropDownItem(onClick = {
openDialog.value = true
})
}
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
)
if (openDialog.value) {
LogOutComponent(openDialog = openDialog, event = event,navController = navController)
}
}
And this is the OverFlowMenu composable which contains the DropDown Menu
#Composable
fun OverflowMenu(content: #Composable () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = {
showMenu = !showMenu
}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "More",
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
content()
}
}
Now given below is the DropDownItem.
#Composable
fun SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = onClick) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
What i am trying to do is, when i click the SettingsDroDownItem, i need to capture the click event in the OverFlowMenu composable to make the showMenu false, so as the hide the DropdownMenu. I can get the click event in the TopAppBar, but how to get it on DropDownMenu.
How to do that?
The first option is moving showMenu state out of OverflowMenu, as this is not the only composable which depends on the value. Something like this:
OverFlowMenu:
#Composable
fun OverflowMenu(showMenu: Bool, setShowMenu: (Bool) -> Unit, content: #Composable () -> Unit) {
// ...
}
TopBar:
actions = {
var (showMenu, setShowMenu) = remember { mutableStateOf(false) }
OverflowMenu(showMenu, setShowMenu) {
SettingsDropDownItem(onClick = {
openDialog.value = true
setShowMenu(false)
})
}
},
An other options is creating something like OverflowMenuScope, and running SettingsDropDownItem on this scope so it can close the menu itself:
OverflowMenu:
interface OverflowMenuScope {
fun closeMenu()
}
#Composable
fun OverflowMenu(content: #Composable OverflowMenuScope.() -> Unit) {
var showMenu by remember { mutableStateOf(false) }
val scope = remember {
object: OverflowMenuScope {
override fun closeMenu() {
showMenu = false
}
}
}
//...
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
scope.content()
}
}
SettingsDropDownItem:
#Composable
fun OverflowMenuScope.SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = {
closeMenu()
onClick()
}) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}