Problem with LaunchedEffect in composables of HorizontalPager - android

I'm creating a project with Compose, but I ran into a situation that I couldn't solve.
View Model:
data class OneState(
val name: String = "",
val city: String = ""
)
sealed class OneChannel {
object FirstStepToSecondStep : OneChannel()
object Finish : OneChannel()
}
#HiltViewModel
class OneViewModel #Inject constructor() : ViewModel() {
private val viewModelState = MutableStateFlow(OneState())
val screenState = viewModelState.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = viewModelState.value
)
private val _channel = Channel<OneChannel>()
val channel = _channel.receiveAsFlow()
fun changeName(value: String) {
viewModelState.update { it.copy(name = value) }
}
fun changeCity(value: String) {
viewModelState.update { it.copy(city = value) }
}
fun firstStepToSecondStep() {
Log.d("OneViewModel", "start of method first step to second step")
if (viewModelState.value.name.isBlank()) {
Log.d("OneViewModel", "name is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"name is not empty, first step to second step event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.FirstStepToSecondStep)
}
}
fun finish() {
Log.d("OneViewModel", "start of method finish")
if (viewModelState.value.city.isBlank()) {
Log.d("OneViewModel", "city is empty, nothing should be done")
return
}
Log.d(
"OneViewModel",
"city is not empty, finish event will be send for composable"
)
viewModelScope.launch {
_channel.send(OneChannel.Finish)
}
}
}
This ViewModel has a MutableStateFlow, a StateFlow to be collected on composable screens and a Channel/Flow for "one time events".
The first two methods are to change a respective state and the last two methods are to validate some logic and then send an event through the Channel.
Composables:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun FirstStep(
viewModel: OneViewModel,
nextStep: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("FirstStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.FirstStepToSecondStep -> {
Log.d("FirstStep (Composable)", "first step to second step action")
nextStep()
}
else -> Log.d(
"FirstStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.name,
onValueChange = { viewModel.changeName(value = it) },
placeholder = { Text(text = "Type our name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = { viewModel.firstStepToSecondStep() }
) {
Text(text = "Next Step")
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SecondStep(
viewModel: OneViewModel,
prevStep: () -> Unit,
finish: () -> Unit
) {
val state by viewModel.screenState.collectAsState()
LaunchedEffect(key1 = Unit) {
Log.d("SecondStep (Composable)", "start of launched effect block")
viewModel.channel.collect { channel ->
when (channel) {
OneChannel.Finish -> {
Log.d("SecondStep (Composable)", "finish action //todo")
finish()
}
else -> Log.d(
"SecondStep (Composable)",
"another action that should be ignored in this scope"
)
}
}
}
Column(modifier = Modifier.fillMaxSize()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
value = state.city,
onValueChange = { viewModel.changeCity(value = it) },
placeholder = { Text(text = "Type our city name") }
)
Spacer(modifier = Modifier.weight(weight = 1F))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Button(
modifier = Modifier.weight(weight = 1F),
onClick = prevStep
) {
Text(text = "Previous Step")
}
Button(
modifier = Modifier.weight(weight = 1F),
onClick = { viewModel.finish() }
) {
Text(text = "Finish")
}
}
}
}
#OptIn(ExperimentalPagerApi::class)
#Composable
fun OneScreen(viewModel: OneViewModel = hiltViewModel()) {
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(initialPage = 0)
val pages = listOf<#Composable () -> Unit>(
{
FirstStep(
viewModel = viewModel,
nextStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage + 1)
}
}
)
},
{
SecondStep(
viewModel = viewModel,
prevStep = {
coroutineScope.launch {
pagerState.animateScrollToPage(page = pagerState.currentPage - 1)
}
},
finish = {}
)
}
)
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.weight(weight = 1F),
state = pagerState,
count = pages.size,
userScrollEnabled = false
) { index ->
pages[index]()
}
HorizontalPagerIndicator(
modifier = Modifier
.padding(vertical = 16.dp)
.align(alignment = Alignment.CenterHorizontally),
pagerState = pagerState,
activeColor = MaterialTheme.colorScheme.primary
)
}
}
OneScreen has a HorizontalPager (from the Accompanist library) which receives two other composables, FirstStep and SecondStep, these two composables have their own LaunchedEffect to collect any possible event coming from the View Model.
Dependencies used:
implementation 'androidx.navigation:navigation-compose:2.5.2'
implementation 'com.google.dagger:hilt-android:2.43.2'
kapt 'com.google.dagger:hilt-android-compiler:2.43.2'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation 'com.google.accompanist:accompanist-pager:0.25.1'
implementation 'com.google.accompanist:accompanist-pager-indicators:0.25.1'
The problem:
After typing something in the name field and clicking to go to the next step, the flow happens normally. When clicking to go back to the previous step, it also works normally. But now when clicking to go to the next step again, the collect in the LaunchedEffect of the FirstStep is not collected, instead the collect in LaunchedEffect of the SecondStep is, resulting in no action, and if click again, then collect in FirstStep works.
Some images that follow the logcat:
when opening the app
after typing something and clicking to go to the next step
going back to the first step
clicking to go to next step (problem)
clicking for the second time (works)

The problem is that HorizontalPager creates both the current page and the next page. When current page is FirstStep, both collectors are active and will be triggered sequentially.
Let's look at the three jump attempts on the first page. The first attempt is received by collector in FirstStep and successfully jumps to the second page. The second attempt is received by collector in SecondStep and fails. The third attempt succeeds again.
Actually, HorizontalPager is LazyRow, so this should be the result of LazyLayout's place logic.
To solve this problem, I suggest merging the two LaunchedEffect and moving it into OneScreen. In fact, the viewmodel should all be moved to the top of the OneScreen, for cleaner code.
At last, here is my simplified code if you want try it.
#Composable
fun Step(index: Int, flow: Flow<String>, onSwitch: () -> Unit, onSend: () -> Unit) {
LaunchedEffect(Unit) {
println("LaunchEffect$index")
flow.collect { println("Step$index:$it") }
}
Column {
Text(text = index.toString(), style = MaterialTheme.typography.h3)
Button(onClick = onSwitch) { Text(text = "Switch Page") }
Button(onClick = onSend) { Text(text = "Send") }
}
}
#OptIn(ExperimentalPagerApi::class)
#Composable
fun Test() {
val channel = remember { Channel<String>() }
val flow = remember { channel.receiveAsFlow() }
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState()
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
count = 4,
userScrollEnabled = false,
) { index ->
Step(index = index, flow = flow,
onSwitch = {
scope.launch { pagerState.scrollToPage((index + 1) % pagerState.pageCount) }
},
onSend = {
scope.launch { channel.send("Test") }
}
)
}
}
If you keep click send button at first page, it will print:

Related

Scrolling is quite laggy in my implementation using jetpack compose paging

I tried to implement pagination in my app using compose. Here you can find full code : https://github.com/alirezaeiii/TMDb-Compose
First I am showing a loading indicator, and then loads items from TMDb API. So I have following composable method :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
As you see in the else section it calls LazyTMDbItemGrid composable function :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
In order to load images in the ImageView asynchronously, I have following function :
#Composable
private fun BoxScope.TMDbItemPoster(posterUrl: String?, tmdbItemName: String) {
val painter = rememberAsyncImagePainter(
model = posterUrl,
error = rememberVectorPainter(Icons.Filled.BrokenImage),
placeholder = rememberVectorPainter(Icons.Default.Movie)
)
val colorFilter = when (painter.state) {
is AsyncImagePainter.State.Loading, is AsyncImagePainter.State.Error -> ColorFilter.tint(
MaterialTheme.colors.imageTint
)
else -> null
}
val scale =
if (painter.state !is AsyncImagePainter.State.Success) ContentScale.Fit else ContentScale.FillBounds
Image(
painter = painter,
colorFilter = colorFilter,
contentDescription = tmdbItemName,
contentScale = scale,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
)
}
The problem is when I am scrolling very fast in the part of screen that images are not loaded yet, it is quite laggy. Based on my research there is no problem with my image loading using coil library, but I do not know why it is laggy. Do you have any suggestion about this?
Addenda :
I have reported the issue here : https://issuetracker.google.com/issues/264847068
You can try to refer to this issue address on github to solve it. At present, Coil is combined with Compose's LazyVerticalGrid and LazyColumn, and the sliding sense is lagging:
https://github.com/coil-kt/coil/issues/1337

Ui state not working correctly in jetpack compose

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.

Loading spinner not working correctly in next screen in jetpack compose

I am new in Compose Navigation. I have Button and when I clicked, I called the function in Viewmodel and trigger loading event with using StateFlow. So I called the next screen through navigation and calling loading spinner. I used delay(5000) to show spinner more before getting data but spinner is loading after the data is loaded. Can someone guide me.
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)
}
)
}
}
}
SetupMainActivityView.kt
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SetupMainActivityView(
viewModel: MainActivityViewModel = koinViewModel(),
navigateToNext: () -> 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 -> {}
is ResultFetchState.IsLoading -> {
navigateToNext()
}
is ResultFetchState.OnError -> {}
is ResultFetchState.OnEmpty -> {}
}
}
My whole project link. Can someone guide me how can I show loading spinner after loading the next screen. Thanks
UPDATE
NavigationGraph.kt
#Composable
internal fun NavigationGraph() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Home.route) {
composable(ScreenRoute.Home.route) {
SetupMainActivityView{
navController.navigate(ScreenRoute.Result.route)
}
}
composable(ScreenRoute.Result.route) {
ResultScreen()
}
}
}
ResultScreen.kt
#Composable
fun ResultScreen() {
CircularProgressIndicator()
}
please check my repository if you need more code. I added my github link above. Thanks
I can't see your code handling the Spinner. Anyway, a general idea to handle these kinda situations is
val state = remember{mutableStateOf<ResultFetchState>(ResultFetchState.EMPTY)}
if(state == ResultFetchState.LOADING){
//show spinner
Spinner()
}
...
state.value = viewModel.stateResultFetchState.collectAsState().value

How to detect swipe in HorizontalPager in Jetpack compose?

How I can detect when user swiped from one tab to second and etc. in my HorizontalPager()?
val pagerState = rememberPagerState(initialPage = 0)
HorizontalPager(count = TabCategory.values().size, state = pagerState) { index ->
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.onBackground)
) {
when (TabCategory.values()[index]) {
TabCategory.Opinion -> { }
TabCategory.Information -> { }
TabCategory.Videos -> { }
}
}
}
In your viewmodel, create a pagerState and monitor its currentPage:
class MyViewModel : ViewModel() {
val pagerState = PagerState()
init {
viewModelScope.launch {
snapshotFlow { pagerState.currentPage }.collect { page ->
// Page is the index of the page being swiped.
}
}
}
}
In your composable, use the pagerState:
HorizontalPager(
state = myViewModel.pagerState,
) { page ->
}
A PagerState has an InteractionSource that keeps track of that stuff. The method collectIsDraggedAsState returns a State<Boolean> you can subscribe to that exposes its current value via .value.
This seems to be testing well.
#Composable
fun ExampleComposable(){
val pagerState=rememberPagerState()
val isDragged=pagerState.interactionSource.collectIsDraggedAsState()
HorizontalPager(count = injector.snippets.size,
state = pagerState,
){index->
MyFancyExampleComposable(index)
if (isDragged.value){
respondToPageSwipeOrPartialPageSwipe()
}
}
}

How to use by delegate for mutableState yet make it passable to another function?

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

Categories

Resources