I want my webview to not respond to any touches; I just want it to display content(hence the touchListener returning true). The parent Surface composable should handle the touches. But it doesn't work.
#OptIn(ExperimentalMaterialApi::class)
#Preview
#Composable
fun MyComposable() {
Surface(
modifier = Modifier.size(250.dp).padding(10.dp),
onClick = { Log.e("TEST", "Clicked") },
elevation = 5.dp
) {
AndroidView(
factory = { context ->
WebView(context).apply {
this.setOnTouchListener { _, _ -> true }
}
},
update = {
it.loadUrl("https://stackoverflow.com/")
}
)
}
}
Currently the only solution that works is having an empty Surface composable on top of the WebView to capture the clicks.
#OptIn(ExperimentalMaterialApi::class)
#Preview
#Composable
fun MyComposable() {
Surface(
modifier = Modifier.size(250.dp).padding(10.dp),
// onClick = { Log.e("TEST", "Clicked") }, // Removed
elevation = 5.dp
) {
AndroidView(
factory = { context ->
WebView(context).apply {
this.setOnTouchListener { _, _ -> true }
}
},
update = {
it.loadUrl("https://stackoverflow.com/")
}
)
Surface(
onClick = { Log.e("TEST", "clicked") },
color = Color.Transparent,
modifier = Modifier.fillMaxSize()
) { }
}
}
But this solution seems a bit hacky. Better solutions are welcome.
Related
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 2 screens which both have their own Scaffold and TopAppBar. When I navigate between them using the Jetpack Navigation Compose library, the app bar flashes. Why does it happen and how can I get rid of this?
Code:
Navigation:
#Composable
fun TodoNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = TodoScreen.TodoList.name,
modifier = modifier
) {
composable(TodoScreen.TodoList.name) {
TodoListScreen(
onTodoEditClicked = { todo ->
navController.navigate("${TodoScreen.AddEditTodo.name}?todoId=${todo.id}")
},
onFabAddNewTodoClicked = {
navController.navigate(TodoScreen.AddEditTodo.name)
}
)
}
composable(
"${TodoScreen.AddEditTodo.name}?todoId={todoId}",
arguments = listOf(
navArgument("todoId") {
type = NavType.LongType
defaultValue = -1L
}
)
) {
AddEditTodoScreen(
onNavigateUp = {
navController.popBackStack()
},
onNavigateBackWithResult = { result ->
navController.navigate(TodoScreen.TodoList.name)
}
)
}
}
}
Todo list screen Scaffold with TopAppBar:
#Composable
fun TodoListBody(
todos: List<Todo>,
todoExpandedStates: Map<Long, Boolean>,
onTodoItemClicked: (Todo) -> Unit,
onTodoCheckedChanged: (Todo, Boolean) -> Unit,
onTodoEditClicked: (Todo) -> Unit,
onFabAddNewTodoClicked: () -> Unit,
onDeleteAllCompletedConfirmed: () -> Unit,
modifier: Modifier = Modifier,
errorSnackbarMessage: String = "",
errorSnackbarShown: Boolean = false
) {
var menuExpanded by remember { mutableStateOf(false) }
var showDeleteAllCompletedConfirmationDialog by rememberSaveable { mutableStateOf(false) }
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text("My Todos") },
actions = {
IconButton(
onClick = { menuExpanded = !menuExpanded },
modifier = Modifier.semantics {
contentDescription = "Options Menu"
}
) {
Icon(Icons.Default.MoreVert, contentDescription = "Show menu")
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(
onClick = {
showDeleteAllCompletedConfirmationDialog = true
menuExpanded = false
},
modifier = Modifier.semantics {
contentDescription = "Option Delete All Completed"
}) {
Text("Delete all completed")
}
}
}
)
},
[...]
Add/edit screen Scaffold with TopAppBar:
#Composable
fun AddEditTodoBody(
todo: Todo?,
todoTitle: String,
setTitle: (String) -> Unit,
todoImportance: Boolean,
setImportance: (Boolean) -> Unit,
onSaveClick: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text(todo?.let { "Edit Todo" } ?: "Add Todo") },
actions = {
IconButton(onClick = onSaveClick) {
Icon(Icons.Default.Save, contentDescription = "Save Todo")
}
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
},
) { innerPadding ->
BodyContent(
todoTitle = todoTitle,
setTitle = setTitle,
todoImportance = todoImportance,
setImportance = setImportance,
modifier = Modifier.padding(innerPadding)
)
}
}
The flashing is caused by the default cross-fade animation in more recent versions of the navigation-compose library. The only way to get rid of it right now (without downgrading the dependency) is by using the Accompanist animation library:
implementation "com.google.accompanist:accompanist-navigation-animation:0.20.0"
And then replace the normal NavHost with Accompanist's AnimatedNavHost, replace rememberNavController() with rememberAnimatedNavController() and disable the transitions animations:
AnimatedNavHost(
navController = navController,
startDestination = bottomNavDestinations[0].fullRoute,
enterTransition = { _, _ -> EnterTransition.None },
exitTransition = { _, _ -> ExitTransition.None },
popEnterTransition = { _, _ -> EnterTransition.None },
popExitTransition = { _, _ -> ExitTransition.None },
modifier = modifier,
) {
[...}
}
I think I found an easy solution for that issue (works on Compose version 1.4.0).
My setup - blinking
All of my screens have their own toolbar wrapped in the scaffold:
// Some Composable screnn
Scaffold(
topBar = { TopAppBar(...) }
) {
ScreenContent()
}
Main activity which holds the nav host is defined like that:
// Activity with NavHost
setContent {
AppTheme {
NavHost(...) { }
}
}
Solution - no blinking!
Wrap you NavHost in activity in a Surface:
setContent {
AppTheme {
Surface {
NavHost(...) { }
}
}
}
Rest of the screens stay the same. No blinking and transition animation between destinations is almost the same like it was with fragments (subtle fade in/fade out). So far I haven't found any negative side effects of that.
It is the expected behaviour. You are constructing two separate app bars for both the screens so they are bound to flash. This is not the correct way. The correct way would be to actually put the scaffold in your main activity and place the NavHost as it's content. If you wish to modify the app bar, create variables to hold state. Then modify them from the Composables. Ideally, store then in a viewmodel. That is how it is done in compose. Through variables.
Thanks
I got the same issue having a "scaffold-per-screen" architecture. What helped, to my surprise, was lowering androidx.navigation:navigation-compose version to 2.4.0-alpha04.
With the newer library implementation "com.google.accompanist:accompanist-navigation-animation:0.24.1-alpha"
you need to have the AnimatedNavHost like this
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None },
modifier = Modifier.padding(innerPadding)
Also
Replace rememberNavController() with rememberAnimatedNavController()
Replace NavHost with AnimatedNavHost
Replace import androidx.navigation.compose.navigation with import com.google.accompanist.navigation.animation.navigation
Replace import androidx.navigation.compose.composable with import com.google.accompanist.navigation.animation.composable
In order not to blink (or to slide if you have AnimatedNavHost) you should put the TopAppBar in the activity and outside the NavHost, otherwise the TopAppBar is just part of the screen and makes transitions like every other screen element:
// Activity with your navigation host
setContent {
MyAppTheme {
Scaffold(
topBar = { TopAppBar(...) }
) { padding ->
TodoNavHost(padding, ...) { }
}
}
}
From the Scaffold containing the TopAppBar comes the padding parameter, that you should pass to the NavHost and use it in the screen like you have done in your example
Alternative to removing Animation you can change animations for example:
#Composable
private fun ScreenContent() {
val navController = rememberAnimatedNavController()
val springSpec = spring<IntOffset>(dampingRatio = Spring.DampingRatioMediumBouncy)
val tweenSpec = tween<IntOffset>(durationMillis = 2000, easing = CubicBezierEasing(0.08f, 0.93f, 0.68f, 1.27f))
...
) { innerPadding ->
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = springSpec) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = springSpec) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = tweenSpec) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = tweenSpec) },
modifier = Modifier.padding(innerPadding)
) {}
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
},
)
}