Composables can only be invoked from the context of a composable context - android

I'm building an application jetpack compose , after fetch some data from online source , i want to pass an id to as extras to the next screen so that i can call the next request api , but i'm facing two issues , the first issue is that showing me an error that composables can only be invoked from a composable context and the second issue is that i'm not sure wether i'm writing the correct code for calling the next screen , i appreciate any help , Thank you .
This is my code
val lazyPopularMoviesItems = movies.collectAsLazyPagingItems()
LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(lazyPopularMoviesItems.itemCount) { index ->
lazyPopularMoviesItems[index]?.let {
Card(elevation = 8.dp, modifier = Modifier
.height(200.dp)
.padding(10.dp)
.clickable {
// This is the function i want to call and pass extras with it
DetailsScreen(movieViewModel = movieViewModel, movieId = it.id)
}
.clip(RoundedCornerShape(8.dp))) {
Column {
Image(
painter = rememberImagePainter("http://image.tmdb.org/t/p/w500/" + it.backdrop_path),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.height(150.dp)
)
Text(
modifier = Modifier
.height(50.dp)
.padding(3.dp)
.fillMaxWidth(),
text = it.title,
fontSize = 15.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
textAlign = TextAlign.Center,
color = androidx.compose.ui.graphics.Color.Black
)
}
}
}
}
}
MainActivity Code
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val movieViewModel : MovieViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
Scaffold(
backgroundColor = Color.Blue.copy(0.1f),
topBar = { TopAppBar(title = {Text(text = "Movie Flex")}, backgroundColor = Color.White, elevation = 10.dp)},
bottomBar = {
val items = listOf(
BarItems.Popular,
BarItems.Playing,
BarItems.Top,
BarItems.Upcoming
)
BottomNavigation(backgroundColor = Color.Gray) {
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = item.title)},
label = { Text(text = item.title)},
selectedContentColor = Color.White,
alwaysShowLabel = true,
selected = false,
unselectedContentColor = Color.White.copy(0.5f),
onClick = {
navController.navigate(item.route){
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
})
}
}
},
content = {
ScreenNavigation(navController,movieViewModel)
},
)
}
}
}
#OptIn(ExperimentalCoilApi::class)
#Composable
fun ScreenNavigation(navController: NavHostController,movieViewModel: MovieViewModel){
NavHost(navController = navController, startDestination = BarItems.Popular.route){
composable(route = BarItems.Popular.route){
PopularScreen(movies = movieViewModel.getPopular(), movieViewModel = movieViewModel)
}
composable(route = BarItems.Playing.route){
PlayingScreen(movies = movieViewModel.getPlaying())
}
composable(route = BarItems.Top.route){
TopRatedScreen(movies = movieViewModel.getTopRated())
}
composable(route = BarItems.Upcoming.route){
UpcomingScreen(movies = movieViewModel.getUpcoming())
}
}
}
Navigation Routing
sealed class BarItems(var route : String , var icon : Int , var title : String) {
object Popular : BarItems("popular", R.drawable.ic_baseline_remove_red_eye_24,"Popular")
object Playing : BarItems("playing",R.drawable.ic_baseline_remove_red_eye_24,"Playing")
object Top : BarItems("top",R.drawable.ic_baseline_remove_red_eye_24,"Top")
object Upcoming : BarItems("upcoming",R.drawable.ic_baseline_remove_red_eye_24,"Upcoming")
}

As you've pointed out, we'll need two things:
Handle the navigation. You can use navigation-compose. Have a look at the documentation
Trigger the navigation with either a LaunchedEffect or by launching a coroutine.

Related

New card item appearse only after phone rotation

I am writing a small gallery app for my cat. It has a button by clicking on which a new PhotoItem is added to the displayed list, but it appears only after phone rotation and I want it to appear on the screen right after button was clicked.
Right now everything is stored in a mutableList inside savedStateHandle.getStateFlow but I also tried regular MutableStateFlow and mutableStateOf and it didn't help. I havent really used jatpack compose and just can't figure what to do (
App
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun BebrasPhotosApp() {
val galaryViewModel = viewModel<GalaryViewModel>()
val allPhotos by galaryViewModel.loadedPics.collectAsState()
Scaffold(topBar = { BebraTopAppBar() }, floatingActionButton = {
FloatingActionButton(
onClick = { galaryViewModel.addPicture() },
backgroundColor = MaterialTheme.colors.onBackground
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Photo",
tint = Color.White,
)
}
}) {
LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
items(allPhotos) {
PhotoItem(bebra = it)
}
}
}
}
ViewModel
class GalaryViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val loadedPics = savedStateHandle.getStateFlow(
"pics", initialValue = mutableListOf<Bebra>(
Bebra(R.string.photo_1, R.string.desc_1, R.drawable.bebra_pic_1, R.string.add_desc_1),
Bebra(R.string.photo_2, R.string.desc_2, R.drawable.bebra_pic_2, R.string.add_desc_2),
Bebra(R.string.photo_3, R.string.desc_3, R.drawable.bebra_pic_3, R.string.add_desc_3)
)
)
fun addPicture() {
val additionalBebraPhoto = Bebra(
R.string.photo_placeholder,
R.string.desc_placeholder,
R.drawable.placeholder_cat,
R.string.add_desc_placeholder
)
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
}
PhotoItem
#Composable
fun PhotoItem(bebra: Bebra, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(elevation = 4.dp, modifier = modifier
.padding(8.dp)
.clickable { expanded = !expanded }) {
Column(
modifier = modifier
.padding(8.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Text(
text = stringResource(id = bebra.PicNumber),
style = MaterialTheme.typography.h1,
modifier = modifier.padding(bottom = 8.dp)
)
Text(
text = stringResource(id = bebra.PicDesc),
style = MaterialTheme.typography.body1,
modifier = modifier.padding(bottom = 8.dp)
)
Image(
painter = painterResource(id = bebra.Picture),
contentDescription = stringResource(id = bebra.PicDesc),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(256.dp)
.clip(RoundedCornerShape(12))
)
if (expanded) {
BebraAdditionalDesc(bebra.additionalDesc)
}
}
}
}
Bebra Data class
data class Bebra(
#StringRes val PicNumber: Int,
#StringRes val PicDesc: Int,
#DrawableRes val Picture: Int,
#StringRes val additionalDesc: Int
)
So, I am also not super familiar with JC, but from first glance it looks like your method, addPicture() - which is called when the user taps on the button, does not update the state, therefore there's no recomposition happening, so the UI does not get updated.
Check:
fun addPicture() {
// ...
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
So here you are basically adding a new item to savedStateHandle, which I assume does not trigger a recomposition.
What I think you need to do, is to update loadedPics, somehow.
However, loadedPics is a StateFlow, to be able to update it you would need a MutableStateFlow.
For simplicity, this is how you would do it if you were operating with a list of strings:
// declare MutableStateFlow that can be updated and trigger recomposition
val _loadedPics = MutableStateFlow(
savedStateHandle.get<MutableList<String>>("pics") ?: mutableListOf()
)
// use this in the JC layout to listen to state changes
val loadedPics: StateFlow<List<String>> = _loadedPics
// addPicture:
val prevList = _loadedPics.value
prevList.add("item")
_loadedPics.value = prevList // triggers recomposition
// here you probably will want to save the item in the
// `savedStateHandle` as you already doing.

Saving current route in bottom navigation - Compose

I have a NavigationBar and each of the items have their own separate navGraphs. One of the NavGraphs has a following scheme:
sealed class ShopDestination(val name: String) {
object Shop : ShopDestination("shop") {
object Category: ShopDestination("${super.name}/category")
class Listing(listingId: Long? = null) : ShopDestination("${super.name}/listing/${listingId ?: ("{" + ShopArguments.ListingId.name + "}")}")
class Product(productId: Long? = null) : ShopDestination("${super.name}/${productId ?: ("{" + RouteArguments.ProductId.name + "}")}")
}
and here's the graph:
.shopNavGraph(navController: NavController) {
navigation(
route = ShopDestination.Shop.name,
startDestination = ShopDestination.Shop.Category.name
) {
composable(ShopDestination.Shop.Category.name) {
ShopScreen({ navController.navigate(ShopDestination.Shop.Listing(it).name) })
}
composable(
route = ShopDestination.Shop.Listing().name,
arguments = listOf(
navArgument(ShopArguments.ListingId.name) {
type = NavType.LongType
}
),
) {
ListingComposable(
contract = hiltViewModel<ListingViewModel>(),
goBack = { navController.popBackStack() },
goToProduct = { navController.navigate(ShopDestination.Shop.Product(it).name) },
)
}
composable(
route = ShopDestination.Shop.Product().name, arguments = listOf(navArgument(
RouteArguments.ProductId.name
) {
type = NavType.LongType
})
) {
ProductScreen(
{ navController.popBackStack() },
{ navController.navigate(ShopDestination.Shop.Product(it).name) })
}
}
When a user click on a different NavigationBarItem and then goes back the shopNavGraph starts from the startDestination. I would like the NavigationBar to save the backstack and once the user clicks back on it, start from where it left of (for example Product). How do I achieve that? Here's also the NavigationBar if it helps in any way:
NavigationBar(containerColor = MaterialTheme.colorScheme.primary) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route ?: ""
items.forEach {
NavigationBarItem(
colors = NavigationBarItemDefaults.colors(indicatorColor = Color.Transparent),
icon = {
Row(verticalAlignment = CenterVertically) {
Icon(
imageVector = it.icon,
contentDescription = it.name,
tint = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(id = it.titleResId).uppercase(),
modifier = Modifier.padding(start = dimensionResource(id = R.dimen.margin_normal)),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium
)
}
},
alwaysShowLabel = true,
selected = currentRoute.contains(it.name),
onClick = {
navController.navigate(it.name)
}

How can I navigation to other screens in Jetpack Compose?

My code is below.
RootNavGraph.kt
#Composable
fun RootNavGraph(navController: NavHostController = rememberNavController()) {
NavHost(
navController = navController,
route = rootRoute,
startDestination = authGraphRoutePattern
) {
authGraph(
navigateToHome = {
navController.popBackStack()
navController.navigateToAppBarGraph()
}
authOtherScreen { navController.popBackStack() }
}
appBarGraph()
supplementSearchScreen()
}
}
SupplementSearch.kt
const val supplementSearchRoute = "supplement_search_route"
fun NavController.navigateToSupplementSearch(navOptions: NavOptions? = null) {
this.navigate(supplementSearchRoute, navOptions)
}
fun NavGraphBuilder.supplementSearchScreen() {
composable(route = supplementSearchRoute) {
SupplementSearchRoute()
}
}
authGraph() is for different login.
appBarGraph() is for bottom navigation menus.
As far as I read this Navigation document, I can place screens in NavHost like this
SomeAScreen()
AGraph()
BGraph()
SomeBScreen()
SomeCScreen()
SomeDScreen()
CGraph()
But I get NPE when I call like this:
#Composable
fun AddSupplementItem(
addSupplement: Vitamin.AddSupplement
) {
val isClicked = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.width(97.dp)
.clickable {
isClicked.value = !isClicked.value
},
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
model = addSupplement.imageUrl,
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.aspectRatio(1.46f)
.clip(RoundedCornerShape(16.dp)),
contentDescription = addSupplement.name,
contentScale = ContentScale.Crop
)
Text(
text = addSupplement.name,
fontSize = 13.sp,
color = Color.Gray
)
}
if (isClicked.value) {
val navController = rememberNavController()
navController.navigateToSupplementSearch() // NullPointException
}
}
It seems like SupplementSearchScreen is not registered to the graph.
Should I keep passing navcontroller from NavHost to that Screen?
NavHost(...){
// otherGraphs()
supplementSearchScreen(navController)
}
But it didn't work.
And also,
// the parent composable function is
fun SupplementGrid(vitaminList: List<Vitamin>) {
// list of vitamin item. and also use AddSupplement()
}
// And also it has parent composable function
#Composable
fun SupplementLayout(feedType: FeedType, supplements: List<Vitamin>) {
// call SupplementGrid()
}
// and finally,
#Composable
fun NutritionScreen(
// it uses LazyColumn and one of item is SupplementLayout()
)
How can I solve this issue??

Composable is recomposing endlessly after flow collect

My composable is recomposing endlessly after flow collect and navigating to a new screen.
I can't understand why.
I'm using Firebase for Auth with Email and Password.
I had to put some Log.i to test my function and my composable, and yes, my Main composable (SignUp) is recomposing endlessly after navigating.
ViewModel
// Firebase auth
private val _signUpState = mutableStateOf<Resources<Any>>(Resources.success(false))
val signUpState: State<Resources<Any>> = _signUpState
fun firebaseSignUp(email: String, password: String) {
job = viewModelScope.launch(Dispatchers.IO) {
firebaseAuth.firebaseSignUp(email = email, password = password).collect {
_signUpState.value = it
Log.i("balito", "polipop")
}
}
}
fun stop() {
job?.cancel()
}
SignUp
#Composable
fun SignUp(
navController: NavController,
signUpViewModel: SignUpViewModel = hiltViewModel()
) {
val localFocusManager = LocalFocusManager.current
Log.i("salut", "salut toi")
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(16.dp)
.background(color = PrimaryColor)
) {
BackButton(navController = navController)
Spacer(modifier = Modifier.height(30.dp))
Text(
text = stringResource(id = R.string.sinscrire),
fontFamily = visby,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
color = Color.White
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.prenez_votre_sante_en_main),
fontFamily = visby,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
color = Grey
)
Spacer(modifier = Modifier.height(20.dp))
Email(signUpViewModel = signUpViewModel, localFocusManager = localFocusManager)
Spacer(modifier = Modifier.height(16.dp))
Password(signUpViewModel = signUpViewModel, localFocusManager = localFocusManager)
Spacer(modifier = Modifier.height(30.dp))
Button(value = stringResource(R.string.continuer), type = Type.Valid.name) {
localFocusManager.clearFocus()
signUpViewModel.firebaseSignUp(signUpViewModel.emailInput.value, signUpViewModel.passwordInput.value)
}
Spacer(modifier = Modifier.height(16.dp))
Button(value = stringResource(R.string.inscription_avec_google), type = Type.Other.name) {
}
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
ClickableTextInfo(stringResource(id = R.string.deja_un_compte_se_connecter), onClick = {})
}
}
Response(navController = navController, signUpViewModel = signUpViewModel)
DisposableEffect(key1 = signUpViewModel.signUpState.value == Resources.success(true)) {
onDispose {
signUpViewModel.stop()
Log.i("fin", "fin")
}
}
}
#Composable
private fun Response(
navController: NavController,
signUpViewModel: SignUpViewModel
) {
when (val response = signUpViewModel.signUpState.value) {
is Resources.Loading<*> -> {
//WaitingLoaderProgress(loading = true)
}
is Resources.Success<*> -> {
response.data.also {
Log.i("lolipop", "lolipopi")
if (it == true) {
navController.navigate(Screen.SignUpConfirmation.route)
}
}
}
is Resources.Failure<*> -> {
// response.throwable.also {
// Log.d(TAG, it)
// }
}
}
}
During navigation transition recomposition happens multiple times because of animations, and you call navController.navigate on each recomposition.
You should not cause side effects or change the state directly from the composable builder, because this will be performed on each recomposition, which is not expected in cases like animation.
Instead you should use side effects. In your case, LaunchedEffect should be used.
if (response.data) {
LaunchedEffect(Unit) {
Log.i("lolipop", "lolipopi")
navController.navigate(Screen.SignUpConfirmation.route)
}
}

How to change icon if selected and unselected in android jetpack compose for NavigationBar like selector we use in xml for selected state?

I want to use outlined and filled icons based on selected state in NavigationBar just like google maps app, using jetpack compose. In case of xml we use selector so what do we use for compose ?
Here is my code ->
MainActivity.kt
#ExperimentalMaterial3Api
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Handle the splash screen transition.
installSplashScreen()
setContent {
MyApp()
}
}
}
#ExperimentalMaterial3Api
#Composable
fun MyApp() {
MyTheme {
val items = listOf(
Screen.HomeScreen,
Screen.MusicScreen,
Screen.ProfileScreen
)
val navController = rememberNavController()
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
NavigationBarItem(
icon = {
Icon(
screen.icon_outlined,
contentDescription = screen.label.toString()
)
},
label = { Text(stringResource(screen.label)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController,
startDestination = Screen.HomeScreen.route,
Modifier.padding(innerPadding)
) {
composable(route = Screen.HomeScreen.route) {
HomeScreen()
}
composable(route = Screen.MusicScreen.route) {
MusicScreen()
}
composable(route = Screen.ProfileScreen.route) {
ProfileScreen()
}
}
}
}
}
#ExperimentalMaterial3Api
#Preview(
showBackground = true, name = "Light mode",
uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL
)
#Preview(
showBackground = true, name = "Night mode",
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
)
#Composable
fun DefaultPreview() {
MyApp()
}
Screen.kt
sealed class Screen(
val route: String,
#StringRes val label: Int,
val icon_outlined: ImageVector,
val icon_filled: ImageVector
) {
object HomeScreen : Screen(
route = "home_screen",
label = R.string.home,
icon_outlined = Icons.Outlined.Home,
icon_filled = Icons.Filled.Home
)
object MusicScreen : Screen(
route = "music_screen",
label = R.string.music,
icon_outlined = Icons.Outlined.LibraryMusic,
icon_filled = Icons.Filled.LibraryMusic,
)
object ProfileScreen : Screen(
route = "profile_screen",
label = R.string.profile,
icon_outlined = Icons.Outlined.AccountCircle,
icon_filled = Icons.Filled.AccountCircle,
)
}
HomeScreen.kt
#Composable
fun HomeScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Surface(color = MaterialTheme.colorScheme.background) {
Text(
text = "Home",
color = Color.Red,
fontSize = MaterialTheme.typography.displayLarge.fontSize,
fontWeight = FontWeight.Bold
)
}
}
}
#Preview(
showBackground = true, name = "Light mode",
uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL
)
#Preview(
showBackground = true, name = "Night mode",
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
)
#Composable
fun HomeScreenPreview() {
HomeScreen()
}
Do I still need to use selector xml or there is alternative way in jetpack compose?
Yeah, you just make a simple if statement based on selected state, like this:
items.forEach { screen ->
val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true
NavigationBarItem(
icon = {
Icon(
if (selected) screen.icon_filled else screen.icon_outlined,
contentDescription = screen.label.toString()
)
},
selected = selected,
)
}

Categories

Resources