Reputation: 637
I hope you all are doing very well.
I'm very new to Kotlin so please be patient with me.
I created a single page in jetpack compose, as you can see in the image below, it needs decorations but at least I want to implement the functionality. Simple app interface with lazy column at the left and data display at the right.
at the left is a lazy column that display the a list of items from an arraylist. the lazy column has cards within that is clickable.
What I need is that once the use click on an item from the column the rest of the item details are displayed in the second composable at the right side.
the code looks like this
@Composable
fun Greeting() {
Row(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.fillMaxHeight()
.width(150.dp)
.background(color = Color.LightGray)
) {
items(photoList) { item ->
ProfileCard(photo = item) { <--- i need the item into the other composable.
// photoContent(
// id = item.id,
// owner = item.owner,
// secret = item.secret,
// server = item.server,
// farm = item.farm,
// title = item.title,
// ispublic = item.ispublic,
// isfriend = item.isfriend,
// isfamily = item.isfamily,
// url_s = item.url_s,
// height_s = item.height_s,
// width_s = item.width_s
// )
}
}
}
photoContent(
id = "",
owner = "",
secret = "",
server = "",
farm = 0,
title = "",
ispublic = 0,
isfamily = 0,
isfriend = 0,
url_s = "",
height_s = 0,
width_s = 0
)
}
}
this click function is from the displayed card
@Composable
fun ProfileCard(photo: photo, clickAction: () -> Unit) {
Card( //We changed the default shape of Card to make a cut in the corner.
modifier = Modifier
.padding(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth()
.wrapContentHeight(
align = Alignment.Top
)
.clickable { clickAction.invoke() }, //<-- moving the action to main composable.
elevation = 8.dp,
backgroundColor = Color.White // Uses Surface color by default, so we have to override it.
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
ProfileContent(photo, Alignment.Start)
}
}
}
I tried several ways to pass the data into the other composable but I always get the error:
@composable invocations can only happen from the context of an @composable function
and I can't remove the @Composable
annotation because it has composable contents...
How can I pass the data this way? or it is not possible without Navigation? but that requires the user to open another page not the current one...
Upvotes: 3
Views: 7768
Reputation: 6863
Listen kid you need to change your way of thinking up here.
Ok let's dig in
Jetpack Compose has state at its heart, and state is held in form of variables (mainly). If a variable is used as state in many places, you must ensure that it is maintained well. Do not allow willy-nilly modifications and reads. Hence, the proper way to store state is inside a ViewModel
.
So start up by creating a VM like
class ProfileViewModel : ViewModel() {
/*This variable shall store the photo,
Which I plan to use as an ID for the Profile Card.*/
var selectedProfile by mutableStateOf(Photo(/*Default Arguments*/)) //Store state as mutableStateOf to ensure proper recompostions
private set //Do not allow external modification, i.e., outside the ViewModel
//We'll be modifying the variable through this method
fun onSelectProfile(photo: Photo) {
selectedProfile = Photo
}
}
This is just a simple demonstration so I'll restrict the viewmodel to this.
Moving on to the activity, we initialize the viewmodel
imports ...
class MainActivity : ComponentActivity() {
private val profileViewModel by viewModels<ProfileViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Greeting(
selectedProfile = profileViewModel.selectedProfile
onSelectProfile = profileViewModel::onSelectProfile
)
}
As is clear I've made a couple modifications to your Greeting Composable. Let's see what they are:-
I've essentially added two parameters. Now learn this. In any scenario where you need to modify the state stored somewhere other than where it is used, and you also need to modify it at times, you must pass two parameters into the composable - One to Read the Value, and one to write the value. Makes sense doesn't it. That is what I have done here.
We pass the value down from the place where it is stored, providing it to the composable. And just to allow the composable to trigger modifications, we pass a onChange
parameter, which is linked all the way up to the storage place. In this case, we are linking your Greeting Composable's onSelectProfile
to the onSelectProfile
defined in the viewmodel. Hence, the caller of the onSelectProfile
is the Greeting Composable, but the method is called finally inside the viewmodel, as it passes up. When the viewmodel's onSelectProfile
is executed, the selectedProfile
variable gets updated (see the method in the viewmodel, it updates the variable). Now, the Composable is reading the variable, hence, the composable itself gets the updated value of the variable (since we are using mutableStateOf
, a recomposition is triggered).
Hence, look at the Composable
@Composable
fun Greeting(
selectedProfile: Photo,
onSelectProfile: (photo: Photo) -> Unit
) {
Row(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier
.fillMaxHeight()
.width(150.dp)
.background(color = Color.LightGray)
) {
items(photoList) { item ->
ProfileCard(photo = item, clickAction = onSelectProfile(item)) //We pass the photo like that
}
}
photoContent(
id = selectedProfile.id,
owner = selectedProfile.,
secret = selectedProfile.<respectiveProperty>,
server = selectedProfile.<respectiveProperty>,
farm = selectedProfile.<respectiveProperty>,
title = selectedProfile.<respectiveProperty>,
ispublic = selectedProfile.<respectiveProperty>,
isfamily = selectedProfile.<respectiveProperty>,
isfriend = selectedProfile.<respectiveProperty>,
url_s = selectedProfile.<respectiveProperty>,
height_s = selectedProfile.<respectiveProperty>,
width_s = selectedProfile.<respectiveProperty>
)
}
}
You see this is what you have been doing essentially. You created a clickAction()
as a parameter didn't you? You were going through the same process, you just needed to continue upwards.
You see, here events are flowing up the heirarchy, leading up to the viewmodel, which then passes down the updated value of the variable (and hence, state) to the Composables. In any system where the events flow up, and state flows down, we call it a Uni-Directional Data Flow, and mind you, this is at the heart of Compose. There are some rules for establishing this data flow. Read the Docs and take the Compose State Codelab to learn about it, although the concept has been made quite clear here.
Thanks!
Side Note: There are other ways to initialize the ViewModel. Using a factory is recommended. Check the docs for that. Good luck learning coding.
Upvotes: 2
Reputation: 23964
Basically, what you need is create a state for your selection. See this example:
Let's say this class is your Photo
class...
data class MyUser(
val name: String,
val surname: String
)
Then, define your screen like this:
@Composable
fun MyScreen(users: List<MyUser>) {
// This state will control the selection
var selectedUser by remember {
mutableStateOf<MyUser?>(null)
}
Row(Modifier.fillMaxSize()) {
LazyColumn(
Modifier
.weight(.3f)
.background(Color.Gray)) {
items(users) {
// This would be your ProfileCard
Text(
text = "${it.name} - ${it.surname}",
modifier = Modifier
.clickable {
// when you click on an item,
// the state is updated
selectedUser = it
}
.padding(16.dp)
)
}
}
Column(Modifier.weight(.7f)) {
// if the selection is not null, display it...
// this would be your PhotoContent
selectedUser?.let {
Text(it.name)
Text(it.surname)
}
}
}
}
Calling this function like this...
@Composable
fun Greeting() {
// Just a fake user list...
val users = (1..50).map { MyUser("User $it", "Surname $it") }
MyScreen(users = users)
}
will be like this
Upvotes: 1