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.
Related
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:
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
My app contains 2 different layouts for different device types. I want different actions to be performed e.g. 1st, 2nd and 3rd items to navigate to different screens, 4th item to show a Toast, the 5th item to launch an email composer intent, etc. Is there way to find out which index of the array was clicked and making these click events reusable rather than creating the same click event twice?
array in strings.xml
<string-array name="array_main">
<item>#string/breads</item>
<item>#string/cakes</item>
<item>#string/condiments</item>
<item>#string/desserts</item>
<item>#string/snacks</item>
<item>#string/contact us</item>
</string-array>
MainActivity.kt
class ActivityMain : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HomeScreen(navController = rememberNavController())
}
}
}
}
#Composable
fun ComposeNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home_screen"
) {
composable("home_screen") {
HomeScreen(navController = navController)
}
composable("second_screen") {
SecondScreen(navController = navController)
}
composable("third_screen") {
ThirdScreen(navController = navController)
}
}
}
#Composable
fun HomeScreen(navController: NavController) {
val windowInfo = rememberWindowInfo()
if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact) {
Scaffold(
topBar = {...},
content = {
MyLazyColumn(lazyItems = resources.getStringArray(R.array.array_main))
},
)
} else {
Scaffold(
topBar = {...},
content = {
MyLazyVerticalGrid(lazyItems = resources.getStringArray(R.array.array_main))
},
)
}
}
}
MyLazyColumn.kt
#Composable
fun MyLazyColumn(lazyItems: Array<String>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
) {
items(lazyItems) { item ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
}) {
Text(
text = item,
)
}
}
}
}
MyLazyVerticalGrid.kt
fun MyLazyVerticalGrid(lazyItems: Array<String>) {
val lazyGridState = rememberLazyGridState()
LazyVerticalGrid(
state = lazyGridState,
columns = GridCells.Fixed(2),
content = {
items(lazyItems.size) { index ->
OutlinedButton(
modifier = Modifier.padding(7.dp),
onClick = {/*TODO*/ }) {
Text(text = lazyItems[index]
)
}
}
}
)
}
Since we're using Compose, we can take advantage of Kotlin.
Instead of creating a string-array resource, we can create an enum class with all the choices like this:
enum class Choices(#StringRes val textResId: Int) {
Breads(R.string.breads),
Cakes(R.string.cakes),
Condiments(R.string.condiments),
Desserts(R.string.desserts),
Snacks(R.string.snacks),
ContactUs(R.string.contact_us),
}
Then, we can setup your LazyColumn and LazyGrid like this:
#Composable
fun MyLazyColumn(
lazyItems : Array<Choice>,
onClickItem: (Choice) -> Unit
) {
//...
items(lazyItems) { choice ->
//...
Text(text = stringResource(choice.textResId))
Button(
//...
onClick = { onClickItem(choice) }
)
//...
}
//...
}
#Composable
fun MyLazyGrid(
lazyItems : Array<Choice>,
onClickItem: (Choice) -> Unit
) {
// ...
items(lazyItems) { choice ->
Text(text = stringResource(choice.textResId))
Button(
onClick = { onClickItem(choice) }
)
}
// ...
}
To reuse the ClickListener, we can create a lambda object and reuse it:
#Composable
fun HomeScreen(/* ... */) {
val listener: (Choice) -> Unit = { choice ->
when(choice) {
Breads -> {}
Cakes -> {}
Condiments -> {}
Desserts -> {}
Snacks -> {}
ContactUs -> {}
}
}
MyLazyGrid(
lazyItems = arrayOf(
/* Choice items you'd add to you list */
),
onClickItem = listener
)
MyLazyColumn(
lazyItems = arrayOf(
/* Choice items you'd add to you list */
),
onClickItem = listener
)
}
Just store your click-codeblocks as variables.
val firstItemClicker = { /* Add Actions Here */ }
val secondItemClicker = { /* and So on */ }
Use it like
FirstItem(
Modifier.clickable { firstItemClicker() }
)
Or,
Button(
onclick = { firstItemClicker() }
){
Text("Click Recyler")
}
I am trying to create an app with JetPack Compose (first time) and I am having some problem with the navigation. The application has a bottomBar with 3 items that navigates to the selected screen.
This works, the problem is when I try to access one of the items of a LazyColumn that is in one of the screens. I would like to navigate to another screen (Profile) where the data of the selected item is displayed but I can't find a way to do it. No matter how I try to do it, I always get this "#Composable invocations can only happen from the context of a #Composable function".
Could someone help me by explaining how to do it? What I want is to learn how and why, not just copy.
Thanks
MainActivity
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(color = PrimaryDark)
}
AppTheme() {
MainScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun MainScreen() {
val navController = rememberNavController()
val navigationItems = listOf(Obras, Talleres, Ajustes)
Scaffold(bottomBar = {
BottomNavigationBar(
navController = navController,
items = navigationItems
)
}) {
NavigationHost(navController)
}
}
}
NavigationHost.kt
#ExperimentalFoundationApi
#Composable
fun NavigationHost(navController: NavHostController) {
NavHost(navController = navController, startDestination = Obras.route) {
composable(Obras.route) {
Pantalla1(navigateToProfile = { authorId ->
navController.navigate("${Profile.route}/$authorId")
})
}
composable(Talleres.route) {
Pantalla2()
}
composable(Ajustes.route) {
Pantalla3()
}
composable(
Profile.route + "/{authorId}",
arguments = listOf(navArgument("authorId") { type = NavType.StringType })
) { backStackEntry ->
val authorId = backStackEntry.arguments!!.getString("authorId")!!
Profile(authorId)
}
}
}
Pantalla1.kt
typealias AuthorId = String
#ExperimentalCoroutinesApi
#Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(
paddingValues = PaddingValues(
bottom = 50.dp
)
),
) {
AutoresInfo(navigateToProfile)
}
}
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
var autoresList by remember {
mutableStateOf<List<Autor>?>(null)
}
JetFirestore(path = { collection("autores") },
queryOnCollection = { orderBy("nombre", Query.Direction.ASCENDING) },
onRealtimeCollectionFetch = { value, exception ->
autoresList = value.getListOfObjects()
}) {
autoresList?.let {
val grouped = it.groupBy { it.nombre[0] }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
grouped.forEach { (initial, autoresForInitial) ->
stickyHeader {
StickyHeaderAutores(initial = initial.toString())
}
items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
Surface(modifier = Modifier.clickable { navigateToProfile(autor.nombre) }) {
#OptIn(coil.annotation.ExperimentalCoilApi::class)
AutorCard(autor)
}
}
}
}
} ?: Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
color = Color.Red,
modifier = Modifier
.size(50.dp)
)
}
}
}
Step 1. Add argument to your profile navigation route. Check out documentation about navigating with arguments
composable(
Profile.route + "/{authorId}",
arguments = listOf(navArgument("authorId") { type = NavType.StringType })
) { backStackEntry ->
val authorId = backStackEntry.arguments!!.getString("authorId")!!
Profile(authorId)
}
Step 2. You need to pass down navigateToProfile function from your NavigationHost. You can replace AuthorId with Int(or an other type) if it's not String:
typealias AuthorId = String
#Composable
fun NavigationHost(navController: NavHostController){
NavHost(navController = navController, startDestination = Obras.route) {
composable(Obras.route) {
Pantalla1(navigateToProfile = { authorId ->
navController.navigate("${Profile.route}/$authorId")
})
}
...
}
#Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {
...
AutoresInfo(navigateToProfile)
...
}
#Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
...
items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
Surface(modifier = Modifier.clickable {
navigateToProfile(author.id)
}) {
AutorCard(autor)
}
}
...
}
Step 3. In you profile composable you need to fetch author by id. Not sure what's JetFirestore, you probably should use it.
#Composable
fun Profile(id: AuthorId) {
JetFirestore(
// fetch author by id
)
}
create a sealed class that contains the route like below
sealed class Screen(val route:String){
object MyListScreen:Screen("my_list_screen")
object MyDetailScreen:Screen("my_detail_screen")
}
create listItem with Row that receive a list and an event of click like below
#Composable
fun MyListItem(
yourList: YourList,
onItemClick:(yourList) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onItemClick(yourList)
}
){
//something here
}
Inside your listScreen in LazyColoumn you can do this
LazyColumn(modifier = Modifier.fillMaxSize()){
items (state.yourfreshlist){mylist->
MyListItem(mylist = mylist, onItemClick = {
navController.navigate(Screen.MyDetailScreen.route+"/${mylist.something}")
}
)
}
}
your host will be like this
NavHost(
navController = navController,
startDestination = Screen.MyListScreen.route
) {
composable(
route = Screen.MyListScreen.route
) {
MyListScreen(navController)
}
composable(
route = Screen.MyDetailScreen.route + "/{anything here id for example}"
) {
MyDetailScreen()
}
}
For more information, you can browse the docs
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
},
)
}