Saher Al-Sous
Saher Al-Sous

Reputation: 637

Jetpack Compose: passing data from lazy column into another composable

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

Answers (2)

Richard Onslow Roper
Richard Onslow Roper

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

nglauber
nglauber

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

enter image description here

Upvotes: 1

Related Questions