How to navigate to Detail View clicking in LazyColumn item with JetPack Compose?

Viewed 2135

I am trying to create an app with JetPack Compose (first time) and I am having some problem with the navigation. The application has a bottomBar with 3 items that navigates to the selected screen.

This works, the problem is when I try to access one of the items of a LazyColumn that is in one of the screens. I would like to navigate to another screen (Profile) where the data of the selected item is displayed but I can't find a way to do it. No matter how I try to do it, I always get this "@Composable invocations can only happen from the context of a @Composable function".

Could someone help me by explaining how to do it? What I want is to learn how and why, not just copy.

Thanks

MainActivity

class MainActivity : ComponentActivity() {

    @ExperimentalFoundationApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        setContent {
            val systemUiController = rememberSystemUiController()

            SideEffect {
                systemUiController.setStatusBarColor(color = PrimaryDark)
            }
            AppTheme() {


                MainScreen()

            }

        }
    }

    @ExperimentalFoundationApi
    @Composable
    fun MainScreen() {
        val navController = rememberNavController()
        val navigationItems = listOf(Obras, Talleres, Ajustes)
        Scaffold(bottomBar = {
            BottomNavigationBar(
                navController = navController,
                items = navigationItems
            )
        }) {

            NavigationHost(navController)
        }
    }
} 

NavigationHost.kt

@ExperimentalFoundationApi
@Composable
fun NavigationHost(navController: NavHostController) {


    NavHost(navController = navController, startDestination = Obras.route) {
        composable(Obras.route) {
            Pantalla1(navigateToProfile = { authorId ->
                navController.navigate("${Profile.route}/$authorId")
            })

        }
        composable(Talleres.route) {
            Pantalla2()
        }

        composable(Ajustes.route) {
            Pantalla3()
        }

        composable(
            Profile.route + "/{authorId}",
            arguments = listOf(navArgument("authorId") { type = NavType.StringType })
        ) { backStackEntry ->
            val authorId = backStackEntry.arguments!!.getString("authorId")!!
            Profile(authorId)
        }

    }
}

Pantalla1.kt

typealias AuthorId = String

@ExperimentalCoroutinesApi
@Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {


    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(
                paddingValues = PaddingValues(

                    bottom = 50.dp
                )
            ),
    ) {

        AutoresInfo(navigateToProfile)
    }

}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
    var autoresList by remember {
        mutableStateOf<List<Autor>?>(null)
    }

    JetFirestore(path = { collection("autores") },
        queryOnCollection = { orderBy("nombre", Query.Direction.ASCENDING) },

        onRealtimeCollectionFetch = { value, exception ->
            autoresList = value.getListOfObjects()
        }) {
        autoresList?.let {
            val grouped = it.groupBy { it.nombre[0] }
            LazyColumn(

                modifier = Modifier.fillMaxSize()

            ) {


                grouped.forEach { (initial, autoresForInitial) ->
                    stickyHeader {
                        StickyHeaderAutores(initial = initial.toString())
                    }

                    items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
                        Surface(modifier = Modifier.clickable { navigateToProfile(autor.nombre) }) {
                            @OptIn(coil.annotation.ExperimentalCoilApi::class)
                            AutorCard(autor)
                        }


                    }
                }


            }
        } ?: Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            CircularProgressIndicator(
                color = Color.Red,
                modifier = Modifier

                    .size(50.dp)

            )
        }

    }


}

2 Answers

Step 1. Add argument to your profile navigation route. Check out documentation about navigating with arguments

composable(
    Profile.route + "/{authorId}",
    arguments = listOf(navArgument("authorId") { type = NavType.StringType })
) { backStackEntry ->
    val authorId = backStackEntry.arguments!!.getString("authorId")!!
    Profile(authorId)
}

Step 2. You need to pass down navigateToProfile function from your NavigationHost. You can replace AuthorId with Int(or an other type) if it's not String:

typealias AuthorId = String

@Composable
fun NavigationHost(navController: NavHostController){
    NavHost(navController = navController, startDestination = Obras.route) {
        composable(Obras.route) {
            Pantalla1(navigateToProfile = { authorId ->
                navController.navigate("${Profile.route}/$authorId")
            })
        }
        ... 
}

@Composable
fun Pantalla1(navigateToProfile: (AuthorId) -> Unit) {
    ...
        AutoresInfo(navigateToProfile)
    ...
}

@Composable
fun AutoresInfo(navigateToProfile: (AuthorId) -> Unit) {
    ...
    items(autoresForInitial, key = { autor -> autor.nombre }) { autor ->
        Surface(modifier = Modifier.clickable {
            navigateToProfile(author.id)
        }) {
            AutorCard(autor)
        }
    }
    ...
}

Step 3. In you profile composable you need to fetch author by id. Not sure what's JetFirestore, you probably should use it.

@Composable
fun Profile(id: AuthorId) {
    JetFirestore(
        // fetch author by id
    )
}

create a sealed class that contains the route like below

sealed class Screen(val route:String){
    object MyListScreen:Screen("my_list_screen")
    object MyDetailScreen:Screen("my_detail_screen")
}

create listItem with Row that receive a list and an event of click like below

@Composable
fun MyListItem(
    yourList: YourList,
    onItemClick:(yourList) -> Unit
) {
    Row(
       modifier = Modifier
           .fillMaxWidth()
           .clickable {
               onItemClick(yourList)
           }
    ){
   //something here
}

Inside your listScreen in LazyColoumn you can do this

 LazyColumn(modifier = Modifier.fillMaxSize()){
            items (state.yourfreshlist){mylist->
               MyListItem(mylist = mylist, onItemClick = {
                 navController.navigate(Screen.MyDetailScreen.route+"/${mylist.something}")
               }
               )
            }
    }

your host will be like this

NavHost(
            navController = navController,
            startDestination = Screen.MyListScreen.route
        ) {
            composable(
                route = Screen.MyListScreen.route
            ) {
                MyListScreen(navController)
            }
            composable(
                route = Screen.MyDetailScreen.route + "/{anything here id for example}"
            ) {
                MyDetailScreen()
            }
        }

For more information, you can browse the docs

Related