This method should only be accessed from tests or within private scope - kotlin

How I can Resolve this warning:
This method should only be accessed from tests or within private scope
It appear after I upgrade the android studio to the latest version!!!
#Composable
fun AppBody() {
val navController = rememberNavController()
val context = LocalContext.current
val width = LocalConfiguration.current.screenWidthDp
Scaffold(
bottomBar = {
AndroidView(
factory = {
AdView(it).apply {
setAdSize(
AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
context,
width
)
)
adUnitId = "*****"
loadAd(AdRequest.Builder().build())
}
},
)
},
content = {
Box(Modifier.padding(it)) {
AppNavHostController(navController = navController)
}
}
)
}

Related

How to override SnackbarData in Jetpack Compose?

I need a custom parameter in the snackbar, but showSnackbar only allows passing three parameters (message, actionLabel and duration). I need to pass an Enum to decide which status to show.
#Composable
fun BaseScreen(
viewModel: BaseViewModel?,
content: #Composable () -> Unit
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope: CoroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
CustomSnackbar(
message = snackbarData.message,
// I can't get custom parameter
status = snackbarData.status
)
}
)
},
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
viewModel?.showSnackbar = { message ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
// I can't pass custom parameter
status = SnackbarStatusEnum.DANGER
)
}
}
content()
}
}
}
You can use something different.
Use a variable(state) to set the status and use a condition inside the Snackbar to change backgroundColor and actions.
Something like:
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { data ->
// custom snackbar with the custom colors
Snackbar(
backgroundColor = if (status) Color.Red else Color.Yellow,
//contentColor = ...,
snackbarData = data
)
}
},
)

Coroutines not working in jetpack Compose

I use the following way to get network data.
I start a network request in a coroutine but it doesn't work, the pagination load is not called.
But if I call the network request through the init method in the ViewModel I can get the data successfully.
#Composable
fun HomeView() {
val viewModel = hiltViewModel<CountryViewModel>()
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
Scaffold {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 96.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier = Modifier.fillMaxSize()) {
items(pagingItems) { countryGroup ->
if (countryGroup == null) return#items
Text(text = "Hello", modifier = Modifier.height(100.dp))
}
}
}
}
#HiltViewModel
class CountryViewModel #Inject constructor() : ViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
init {
getCountryList() // can work
}
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
}
I don't understand what's the difference between the two calls, why doesn't it work?
Any helpful comments and answers are greatly appreciated.
I solved the problem, the coroutine was used twice in the code above, which caused network data to not be fetched.
A coroutine is used here:
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
Here is another coroutine:
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
current usage:
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
viewModel.getCountryList()
execute.value = false
}

How to eliminate passing View Model to this Jetpack Compose Kotlin Home Screen?

I'm trying to clean-up my code and eliminate View Models where not necessary.
I'd like to be able to access itemList, filtering, and itemListFiltered in my HomeScreen function without explicitly passing the MainViewModel as a parameter, but I can't figure out how to do it.
I tried using itemList = model::itemList but Android Studio gives error: Type mismatch. Required: List<Int> Found: KProperty0<SnapshotStateList<Int>>
View Model
class MainViewModel : ViewModel() {
val itemList = mutableStateListOf<Int>()
val filtering = mutableStateOf<Boolean>(false)
val itemListFiltered = mutableStateListOf<Int>()
fun addItem(item: Int) {
itemList.add(item)
}
fun clearItemList() {
itemList.clear()
}
fun filterListGreaterThan(greaterThan: Int) {
itemListFiltered.clear()
itemList.forEach { item ->
if (item > greaterThan) itemListFiltered.add(item)
}
}
init {
clearItemList()
} // End Initializer
}
Home Screen Scaffolding
#Composable
fun HomeScreen(
model: MainViewModel // <-- How do I eliminate this
) {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopBar(scope, scaffoldState) },
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(
onAddItem = model::addItem
) },
drawerContent = {
DrawerContent(
onAddItem = model::addItem,
onResetList = model::clearItemList
) },
drawerGesturesEnabled = true,
content = {
Content(
itemList = model.itemList, // <-- How do I pass this itemList without model?
onAddItem = model::addItem
) },
bottomBar = { BottomBar() }
)
}
Can you help me figure out how to do that?
Thanks for your help!
Pass it just like that...
model.itemsList
The :: symbol doesn't work how you seem to think it does. Consider it to be used for static non-changing variables/methods. Like you could use model::addItem or model::clearItemList since they are just public methods and do not hold a value.

JetpackCompose Navigation Nested Graphs cause "ViewModelStore should be set before setGraph call" exception

I am trying to apply Jetpack Compose navigation into my application.
My Screens: Login/Register screens and Bottom navbar screens(call, chat, settings).
I already found out that the best way to do this is using nested graphs.
But I keep getting ViewModelStore should be set before setGraph call exception. However, I don't think this is the right exception.
My navigation is already in the latest version. Probably my nested graph logic is not right.
Requirement:
I want to be able to navigate from the Login or Register screen to any BottomBar Screen & reverse
#Composable
fun SetupNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
NavHost(
navController = navController,
startDestination = BOTTOM_BAR_GRAPH_ROUTE,
route = ROOT_GRAPH_ROUTE
) {
loginNavGraph(navController = navController, userViewModel)
bottomBarNavGraph(navController = navController, userViewModel)
}
}
NavGraph.kt
fun NavGraphBuilder.loginNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.LoginScreen.route,
route = LOGIN_GRAPH_ROUTE
) {
composable(
route = Screen.LoginScreen.route,
content = {
LoginScreen(
navController = navController,
loginViewModel = userViewModel
)
})
composable(
route = Screen.RegisterScreen.route,
content = {
RegisterScreen(
navController = navController,
loginViewModel = userViewModel
)
})
}
}
LoginNavGraph.kt
fun NavGraphBuilder.bottomBarNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.AppScaffold.route,
route = BOTTOM_BAR_GRAPH_ROUTE
) {
composable(
route = Screen.AppScaffold.route,
content = {
AppScaffold(
navController = navController,
userViewModel = userViewModel
)
})
}
}
BottomBarNavGraph.kt
#Composable
fun AppScaffold(
navController: NavHostController,
userViewModel: UserViewModel
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
bottomBar = {
BottomBar(mainNavController = navController)
},
scaffoldState = scaffoldState,
) {
NavHost(
navController = navController,
startDestination = NavigationScreen.EmergencyCallScreen.route
) {
composable(NavigationScreen.EmergencyCallScreen.route) {
EmergencyCallScreen(
navController = navController,
loginViewModel = userViewModel
)
}
composable(NavigationScreen.ChatScreen.route) { ChatScreen() }
composable(NavigationScreen.SettingsScreen.route) {
SettingsScreen(
navController = navController,
loginViewModel = userViewModel
)
}
}
}
}
AppScaffold.kt
#Composable
fun BottomBar(mainNavController: NavHostController) {
val items = listOf(
NavigationScreen.EmergencyCallScreen,
NavigationScreen.ChatScreen,
NavigationScreen.SettingsScreen,
)
BottomNavigation(
elevation = 5.dp,
) {
val navBackStackEntry by mainNavController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.map {
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = it.icon),
contentDescription = it.title
)
},
label = {
Text(
text = it.title
)
},
selected = currentRoute == it.route,
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(alpha = 0.4f),
onClick = {
mainNavController.navigate(it.route) {
mainNavController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
restoreState = true
launchSingleTop = true
}
},
)
}
}
}
BottomBar.kt
const val ROOT_GRAPH_ROUTE = "root"
const val LOGIN_GRAPH_ROUTE = "login_register"
const val BOTTOM_BAR_GRAPH_ROUTE = "bottom_bar"
sealed class Screen(val route: String) {
object LoginScreen : Screen("login_screen")
object RegisterScreen : Screen("register_screen")
object AppScaffold : Screen("app_scaffold")
}
Screen.kt
sealed class NavigationScreen(val route: String, val title: String, #DrawableRes val icon: Int) {
object EmergencyCallScreen : NavigationScreen(
route = "emergency_call_screen",
title = "Emergency Call",
icon = R.drawable.ic_phone
)
object ChatScreen :
NavigationScreen(
route = "chat_screen",
title = "Chat",
icon = R.drawable.ic_chat)
object SettingsScreen : NavigationScreen(
route = "settings_screen",
title = "Settings",
icon = R.drawable.ic_settings
)
}
NavigationScreen.kt
After struggling some time with this issue, I made my way out by using two separated NavHost. It might not be the right way to do it but it works at the moment. You can find the example source code here:
https://github.com/talhaoz/JetPackCompose-LoginAndBottomBar
Hope they make the navigation easier on upcoming releases.
Nesting of NavHost is not allowed. It results in ViewModelStore should be set before setGraph call Exception. Generally, the bottom nav is outside of the NavHost, which is what the docs show. The recommended approach is a single NavHost, where you hide and show your bottom nav based on what destination you are on.
One NavHost, one NavHostController. Create a new NavHostController in front of the nested NavHost on AppScaffold.
Have similar issue when implement this common UI pattern:
HomePage(with BottomNavigationBar), this page is hosted by Inner nav controller
click some links of one page
navigate to a new page (with new Scaffold instance). This page is hosted by Outer nav controller.
Kinda hacked this issue by using 2 NavHost with 2 navController instance.
Basic idea is using some msg channel to tell the outer nav controller, a Channel in my case.
private val _pages: Channel<String> = Channel()
var pages = _pages.receiveAsFlow()
#Composable
fun Route() {
val navController1 = rememberNavController()
LaunchedEffect(true) {
pages.collect { page ->
navController1.navigate("detail")
}
}
NavHost(navController = navController1, startDestination = "home") {
composable("home") { MainPage() }
composable("detail") { DetailPage() }
}
}
#Composable
fun MainPage() {
val navController2 = rememberNavController()
val onTabSelected = { tab: String ->
navController2.navigate(tab) {
popUpTo(navController2.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
Scaffold(topBar = { TopAppBar(title = { Text("Home Title") }) },
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController2.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab1" } == true,
onClick = { onTabSelected("tab1") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab1") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab2" } == true,
onClick = { onTabSelected("tab2") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab2") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab3" } == true,
onClick = { onTabSelected("tab3") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab3") }
)
}
}
) { value ->
NavHost(navController = navController2, startDestination = "tab1") {
composable("tab1") { Home() }
composable("tab2") { Text("tab2") }
composable("tab3") { Text("tab3") }
}
}
}
class HomeViewModel: ViewModel()
#Composable
fun Home(viewModel: HomeViewModel = HomeViewModel()) {
Button(
onClick = {
viewModel.viewModelScope.launch {
_pages.send("detail")
}
},
modifier = Modifier.padding(all = 16.dp)
) {
Text("Home", modifier = Modifier.padding(all = 16.dp))
}
}
#Composable
fun DetailPage() {
Scaffold(topBar = { TopAppBar(title = { Text("Detail Title") }) }) {
Text("Detail")
}
}
Cons:
App needs to maintain UI stack information.
It's even harder to cope with responsive layout.
use rememberNavController() for your function
fun YourFunction(
navController: NavHostController = rememberNavController()
)
In my case, I had to create nav controller (for bottom bar) with in home screen.
#AndroidEntryPoint
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
Theme {
Surface(modifier = Modifier.fillMaxSize()) {
AppContainer()
}
}
}
}
}
#Composable
fun AppContainer() {
val mainNavController = rememberNavController()
// This was causing the issue. I moved this to HomeScreen.
// val bottomNavController = rememberNavController()
Box(
modifier = Modifier.background(BackgroundColor)
) {
NavGraph(mainNavController)
}
}
#Composable
fun HomeScreen(mainNavController: NavController) {
val bottomBarNavController = rememberNavController()
}

Compose navigation title is not updating

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks