I created a road using Navigation (with Jetpack Compose) as so
composable(CatLifeScreen.AddCatFormScreen.route + "?catId={catId}",
arguments = listOf(
navArgument(name = "catId") {
nullable = true
}
)) {
bottomBarState.value = false
val catId = it.arguments?.getInt("catId")
AddEditCatFormBody(
idOfCat = catId,
navController = navController,
addEditCatViewModel = addEditCatViewModel,
)
If I don't add any argument, the road works good
onClick = { navController.navigate(CatLifeScreen.AddCatFormScreen.route) }
If I define an argument it.arguments?.getInt("catId") is 0 when my passed argument is not 0.
IconButton(onClick = { navController.navigate(CatLifeScreen.AddCatFormScreen.route + "?catId=$idOfCat") } //idOfCat = 1
Does anyone have a lead to follow or something? I've been stuck for a long time now
Finally found the answer.
For anyone stuck like I was:
Put the type of the data you want to receive
Integer value can't be nullable (app will crash if you do).(see documentation https://developer.android.com/guide/navigation/navigation-pass-data#supported_argument_types ). In my case, I don't specify the type
Fetch key in arguments using getString(key) and cast it to Int (only if you don't specify the type because you want it to be nullable. You probably could deal with it differently (like using -1 when you don't have a value to pass, instead of null)
composable(CatLifeScreen.AddCatFormScreen.route + "?catId={catId}",
arguments = listOf(
navArgument(name = "catId") {
nullable = true // I don't specify the type because Integer can't be nullable
}
)) {
bottomBarState.value = false
val catId = it.arguments?.getString("catId")?.toInt() // Fetch key using getString() and cast it
AddEditCatFormBody(
idOfCat = catId,
navController = navController,
addEditCatViewModel = addEditCatViewModel,
)
}
try typing the argument type, like this;
arguments = listOf(
navArgument("catId") {
type = NavType.IntType
nullable = true
}
)
Related
I'm running into a problem when trying to navigate with argument in my very first compose project
Error:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/transaction_detail/{1} } cannot be found in the navigation graph NavGraph...
My NavGraph:
#Composable
fun SetupNavGraph(
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = HomeDestination.route,
) {
composable(route = HomeDestination.route) {
HomeScreen(
navigateToItemEntry = { navController.navigate(TransactionEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${TransactionDetailDestination.route}/{$it}")
}
)
}
//detail screen route
composable(
route = TransactionDetailDestination.routeWithArgs,
arguments = listOf(
navArgument(TransactionDetailDestination.transactionIdArg) {
type = NavType.IntType
}
)
) {
val id = it.arguments?.getInt(TransactionDetailDestination.transactionIdArg)!!
TransactionDetailScreen(id)
}
}
}
My transaction detail screen:
object TransactionDetailDestination : NavigationDestination {
override val route = "transaction_detail"
override val title = "Transaction Detail Screen"
const val transactionIdArg = "transactionId"
val routeWithArgs = "$route/{$transactionIdArg}"
}
#Composable
fun TransactionDetailScreen(id: Int) {
Scaffold {
TransactionDetailBody(paddingValues = it, id = id)
}
}
#Composable
fun TransactionDetailBody(
paddingValues: PaddingValues,
id: Int
) {
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "$id", fontSize = 100.sp)
...
}
}
I can see that the problem is the route to transaction detail destination, but I don't know where to correct. I'm looking forward to every suggestion!
By research on internet a lot a realize that when specify the route to go, in my case, always like this:
//'it' is the argument we need to send
//rule: 'route/value1/value2...' where 'value' is what we trying to send over
navController.navigate("${TransactionDetailDestination.route}/$it")
The string of the route we need to extract the argument(s) from:
//notice the naming rule: 'route/{arg1}/{arg2}/...'
val routeWithArgs = "${route}/{${transactionIdArg}}"
Only be doing the above the compiler will understand the argument you are trying to send and receive. My mistake not reading carefully. Hope it helps!
I think you didn't declare your destination argument in your graph like this
composable("transaction_detail/{id}")
according to this documentation
What is the correct way of passing arguments to startDestination of a nested navigation graph? See this example:
private const val featureGraphRoute = "feature_graph"
private const val firstRouteArg = "intArgument"
private const val firstRoute = "first_route/{$firstRouteArg}"
private const val secondRoute = "second_route"
fun NavController.navigateToFeatureGraph(argument:Int, navOptions: NavOptions? = null) {
//TODO: pass the argument
this.navigate(featureGraphRoute, navOptions)
}
fun NavGraphBuilder.featureGraph() {
navigation(
route = featureGraphRoute,
startDestination = firstRoute
) {
composable(
route = firstRoute,
arguments = listOf(
navArgument(firstRouteArg){
type = NavType.IntType
}
)
) { backStackEntry ->
FirstRoute(
argument = backStackEntry.arguments?.getInt(firstRouteArg)
)
}
composable(route = firstRoute) {
SecondRoute()
}
}
}
Adding the same argument to the featureGraphRoute does seem to work but only if using NavType.StringType. Otherwise app crashes with exception:
java.lang.IllegalArgumentException: Wrong argument type for 'intArgument' in argument bundle. integer expected.
EDIT:
I somehow missed the fact that NavGraphBuilder.navigation has an overload that takes arguments. Moving the navArgument declaration up one level does prevent the crash.
So I have Two ViewModels in my Calculator App in which I all reference in my Compose NavGraph so I can use the same ViewModel instance. I set a Boolean State(historyCheck) in the first ViewModel and I set it too true to "Clear" the History I set which is a history of Calculations I am trying to retrieve from both ViewModels. The issue now is that the Boolean State "strCalcViewModel.historyCheck" changes before the variable above it get's assigned which then makes the 'if' statement I setup fail which in turns makes the whole Implementation also fail as it is always set to false.
This is my code Below...
My Compose NavGraph.
#Composable
fun ComposeNavigation(
navController: NavHostController,
) {
/**
* Here We declare an Instance of our Two ViewModels, their states and History States. This is because we don't want to have the same States for the two Screens.
*/
val strCalcViewModel = viewModel<CalculatorViewModel>()
val sciCalcViewModel = viewModel<ScientificCalculatorViewModel>()
val strCalcState = strCalcViewModel.strState
val sciCalcState = sciCalcViewModel.sciState
val strHistoryState = strCalcViewModel.historyState
val sciHistoryState = sciCalcViewModel.historyState
// This holds our current available 'HistoryState' based on where the Calculation was performed(Screens) by the USER.
var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
if(strCalcViewModel.historyCheck) {
currHistory = strHistoryState
strCalcViewModel.historyCheck = false // this gets assigned before the 'currHistory' variable above thereBy making the the if to always be false
} else {
currHistory = sciHistoryState
}
NavHost(
navController = navController,
startDestination = "main_screen",
) {
composable("main_screen") {
MainScreen(
navController = navController, state = strCalcState, viewModel = strCalcViewModel
)
}
composable("first_screen") {
FirstScreen(
navController = navController, state = sciCalcState, viewModel = sciCalcViewModel
)
}
composable("second_screen") {
SecondScreen(
navController = navController, historyState = currHistory, viewModel = strCalcViewModel
)
}
}
}
Then my ViewModel
private const val TAG = "CalculatorViewModel"
class CalculatorViewModel : ViewModel() {
var strState by mutableStateOf(CalculatorState())
// This makes our state accessible by outside classes but still readable
private set
var historyState by mutableStateOf(CalculatorHistoryState())
private set
private var leftBracket by mutableStateOf(true)
private var check = 0
var checkState by mutableStateOf(false)
var historyCheck by mutableStateOf(false)
// Function to Register our Click events
fun onAction(action : CalculatorAction) {
when(action) {
is CalculatorAction.Number -> enterNumber(action.number)
is CalculatorAction.Decimal -> enterDecimal()
is CalculatorAction.Clear -> {
strState = CalculatorState()
check = 0
}
is CalculatorAction.ClearHistory -> checkState = true
is CalculatorAction.Operation -> enterStandardOperations(action.operation)
is CalculatorAction.Calculate -> performStandardCalculations()
is CalculatorAction.Delete -> performDeletion()
is CalculatorAction.Brackets -> enterBrackets()
}
}
// We are Basically making the click events possible by modifying the 'state'
private fun performStandardCalculations() {
val primaryStateChar = strState.primaryTextState.last()
val primaryState = strState.primaryTextState
val secondaryState = strState.secondaryTextState
if (!(primaryStateChar == '(' || primaryStateChar == '%')) {
strState = strState.copy(
primaryTextState = secondaryState
)
strState = strState.copy(secondaryTextState = "")
// Below, we store our Calculated Values in the History Screen after it has been Calculated by the USER.
historyState = historyState.copy(
historySecondaryState = secondaryState
)
historyState = historyState.copy(
historyPrimaryState = primaryState
)
historyCheck = true // this is where I assign it to true when I complete my Calculations and pass it to the history State
} else {
strState = strState.copy(
secondaryTextState = "Format error"
)
strState = strState.copy(
color = ferrari
)
}
}
}
You're checking the if condition and assigning a new value to your viewModel variable in the Compose function, it's not correct! you should use side-effects
LaunchedEffect(strCalcViewModel.historyCheck) {
if(strCalcViewModel.historyCheck) {
currHistory = strHistoryState
strCalcViewModel.historyCheck = false
} else {
currHistory = sciHistoryState
}}
Whenever there is a new change in strCalcViewModel.historyCheck this block will run
you can check out here for more info Side-effects in Compose
Based on Sadegh.t's Answer I got it working but didn't write it the exact same way and used a different Implementation which I will post now.
I Still used a side-effect but instead of checking for a change in the "historyCheck", I checked for a change in the 'State' itself and also instead of using a Boolean variable, I used the State itself for the basis of the Condition. So here is my answer based on Sadegh.t's original answer.
var currHistory by remember { mutableStateOf(CalculatorHistoryState()) }
LaunchedEffect(key1 = strCalcState) {
if(strCalcState.secondaryTextState.isEmpty()) {
currHistory = strHistoryState
}
}
LaunchedEffect(key1 = sciCalcState) {
if(sciCalcState.secondaryTextState.isEmpty()) {
currHistory = sciHistoryState
}
}
I have a MutableStateFlow<List<AttendanceState>>,
var attendanceStates = MutableStateFlow<List<AttendanceState>>(arrayListOf())
private set
My AttendanceState data class.
data class AttendanceState (
var memberId: Int,
var memberName: String = "",
var isPresent: Boolean = false,
var leaveApplied: Boolean = false
)
The list is rendered by a LazyColumn
The LazyColumn contains Checkboxes.
If i update the checkbox, the event is propagated to the ViewModel and from there I'm changing the value in the list
attendanceStates.value[event.index].copy(leaveApplied = event.hasApplied)
attendanceStates.value = attendanceStates.value.toList()
But this is not updating the LazyColumn.
Snippet of my implementation:
val attendances by viewModel.attendanceStates.collectAsState()
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
Log.e("Attendance","Lazy Column Recomposition")
items(attendances.size) { index ->
AttendanceCheckBox(attendanceState = attendances[index], modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), onAttendanceStatusChangeListener = { viewModel.onEvent(AttendanceEvent.IsPresentStatusChanged(index, it)) }, onLeaveAppliedStatusChangeListener = { viewModel.onEvent(AttendanceEvent.IsLeaveAppliedStatusChanged(index, it)) })
}
}
Re-composition is not happening.
Try this:
viewModelScope.launch {
val helper = ArrayList(attendanceStates.value)
helper[event.index] = helper[event.index].copy(leaveApplied = event.hasApplied)
attendanceStates.emit(helper)
}
Changing an item's properties will not trigger a StateFlow, you have to replace the whole item with the changed item and emit a new list.
I would recommend SnapshotStateList instead of a standard List, this will guarantee an update without having to create a new instance of it like what you would do with an ordinary List, assuming you call AttendanceState instance copy() and updating at least one of its properties with a different value.
var attendanceStates = MutableStateFlow<SnapshotStateList>(mutableStateListOf())
private set
I would also recommend changing the way you use your LazyColumn where items are mapped by their keys not just by their index position,
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
items(attendances, key = {it.memberId}) {
AttendanceCheckBox(...)
}
}
and if you still need the index position.
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
itemsIndexed(attendances, key = { index, item ->
item.memberId
}) { item, index ->
AttendanceCheckBox(...)
}
}
You should also use update when updating your StateFlow instead of modifying its value directly to make it concurrently safe.
attendanceStates.update { list ->
val idx = event.idx
list[idx] = list[idx].copy(leaveApplied = event.hasApplied)
list
}
Is it possible within Jetpack Compose to use a sealed class to display strings with different values in their placeholders? I got confused when trying to figure out what to use for the Text objects. i.e. text = stringResource(id = it.?)
strings.xml
<string name="size_placeholder">Size %1$d</string>
<string name="sizes_placeholder_and_placeholder">Sizes %1$d and %2$d</string>
MainActivity.kt
sealed class Clothes {
data class FixedSizeClothing(val size: String, val placeholder: String): Clothes()
data class MultiSizeClothing(val sizes: String, val placeholders: List<String>): Clothes()
}
#Composable
fun ClothesScreen() {
val clothingItems = remember { listOf(
Clothes.FixedSizeClothing(itemSize = stringResource(id = R.string.size), itemPlaceholder = "8"),
Clothes.MultiSizeClothing(itemSizes = stringResource(id = R.string.sizes), itemPlaceholders = listOf("0", "2"))
)
}
Scaffold(
topBar = { ... },
content = { it ->
Row {
LazyColumn(
modifier = Modifier.padding(it)
) {
items(items) {
Column() {
Text(
text = stringResource(id = it.?)
)
Text(
text = stringResource(id = it.?)
)
}
}
}
}
},
containerColor = MaterialTheme.colorScheme.background
)
expected result
This is more a question about how to check for the type of a subclass of a sealed class (or sealed interface). Just to avoid any confusion, it should be made clear that these are Kotlin features and are not related to Jetpack Compose.
But yes, they can be used inside Composables as well or anywhere you want, really.
You would use a when (...) expression on the value of your sealed class to determine what to do based on the (sub)type of your sealed class (it works the same for sealed interfaces). Inside the when expression you then use the is operator to check for different subtypes.
val result = when (it) {
is Clothes.FixedSizeClothing -> {
// it.size and it.placeholder are accessible here due to Kotlin smart cast
// do something with them...
// last line will be returned as the result
}
is Clothes.MultiSizeClothing -> {
// it.sizes and it.placeholders are accessible here due to Kotlin smart cast
// do something with them...
// last line will be returned as the result
}
In situations when you don't need the result you just omit the val result = part. Note that the name result is arbitrary, pick whatever best describes the value you are creating.
The advantage of this type of the when expression is that it will give you a warning (and in future versions an error) if you forget one of the subtypes inside the when expression. This means the when expression is always exhaustive when there is no warning present, i.e. the Kotlin compiler checks at compile-time for all subtypes of the specific sealed class that are defined inside your whole codebase and makes sure that you are accounting for all of them inside the when expression.
For more on sealed classes inside a when expression see https://kotlinlang.org/docs/sealed-classes.html#sealed-classes-and-when-expression
In your case, you would do the same to generate the text value that you would then pass into the Text(text = ...) composable.
Scaffold(
topBar = { ... },
content = { it ->
Row {
LazyColumn(
modifier = Modifier.padding(it)
) {
items(clothingItems) {
val text = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = R.string.size, it.placeholder)
is Clothes.MultiSizeClothing ->
stringResource(id = R.string.sizes, it.placeholders[0], it.placeholders[1])
}
Text(text = text)
}
}
}
},
containerColor = MaterialTheme.colorScheme.background
)
I used the stringResource(#StringRes id: Int, vararg formatArgs: Any): String version of the call above to construct the final text. See here for other options available in Compose https://developer.android.com/jetpack/compose/resources
If you do want to store the presentation String inside your data classes as you are trying to do in your example you could store the resource id instead of the resolved String.
Also since you are using integer placeholders (%1$d) in your resource strings, the type of your size and sizes values can be Int. So all together that would be something like this
import androidx.annotation.StringRes
sealed class Clothes {
data class FixedSizeClothing(#StringRes val size: Int, val placeholder: Int): Clothes()
data class MultiSizeClothing(#StringRes val sizes: Int, val placeholders: List<Int>): Clothes()
}
And then when you define the items you would not call stringResource(id = ...) anymore and your size and sizes values are just integers.
val clothingItems = remember {
listOf(
Clothes.FixedSizeClothing(size = R.string.size, placeholder = 8),
Clothes.MultiSizeClothing(sizes = R.string.sizes, placeholders = listOf(0, 2))
)
}
The added benefit of this is that now you do not need a Composable context (or a Context or a Resources reference) to create instances of your Clothes sealed class.
Then when declaring the UI, you would do something like this
LazyColumn(
modifier = Modifier.padding(it)
) {
items(clothingItems) {
val text = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = it.size, it.placeholder)
is Clothes.MultiSizeClothing ->
stringResource(id = it.sizes, it.placeholders[0], it.placeholders[1])
}
Text(text = text)
}
}