Android-jetpack Compose

jackpack compose - Navigation : 정의, 개발 절차 샘플

slow333 2023. 3. 30. 14:37
jetpack navigateion component

1. Nav.Controller - (Central API) navigation이 수행할 내용을 정의 

     navigation.navigate(route)

2. Nav.Host - 개별 navigation graph item을 host 함

 사용자가 새로운 화면으로 navigate 할 때  navHost는 개별 destination(composable)을 교체 함

3. Navigation Graph - destination, screen, composable 관련 정보를 보관함

NavGraph는 destination, screen, composable 와 관련 정보 모두를 map out 한다.

 

프로젝트에 우선 navigation, screen package를 만들고 작업

======================================================================

navigation 시에 특정 영역 클릭 시에 해당 값을 받아서 navigation에서 이 값을 활용 특정 페이지로 이동하는 방법

1. data class 생성

data class Movie(val id: String,
                 val title: String,
                 val year: String,
                 val genre: String,
                 val director: String,
                 val actors: String,
                 val plot: String,
                 val poster: String,
                 val images: List<String>,
                 val rating: String)

2. data list 생성

fun getMovies(): List<Movie> {
   return listOf(
      Movie(id = idList.uuidList[0],
         title = "Avatar",
         year = "2009",
         genre = "Action, Adventure, Fantasy",
         director = "James Cameron",
         actors = "Sam Worthington, Zoe Saldana, Sigourney Weaver, Stephen Lang",
         plot = "A paraplegic marine dispatched to the moon Pandora on a unique mission becomes torn between following his orders and protecting the world he feels is his home.",
         poster = "http://ia.media-imdb.com/images/M/MV5BMTYwOTEwNjAzMl5BMl5BanBnXkFtZTcwODc5MTUwMw@@._V1_SX300.jpg",
         images = listOf("https://images-na.ssl-images-amazon.com/images/M/MV5BMjEyOTYyMzUxNl5BMl5BanBnXkFtZTcwNTg0MTUzNA@@._V1_SX1500_CR0,0,1500,999_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BNzM2MDk3MTcyMV5BMl5BanBnXkFtZTcwNjg0MTUzNA@@._V1_SX1777_CR0,0,1777,999_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BMTY2ODQ3NjMyMl5BMl5BanBnXkFtZTcwODg0MTUzNA@@._V1_SX1777_CR0,0,1777,999_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BMTMxOTEwNDcxN15BMl5BanBnXkFtZTcwOTg0MTUzNA@@._V1_SX1777_CR0,0,1777,999_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYxMDg1Nzk1MV5BMl5BanBnXkFtZTcwMDk0MTUzNA@@._V1_SX1500_CR0,0,1500,999_AL_.jpg"),
         rating = "7.9"),

      Movie(id = idList.uuidList[1],
         title = "300",
         year = "2006",
         genre = "Action, Drama, Fantasy",
         director = "Zack Snyder",
         actors = "Gerard Butler, Lena Headey, Dominic West, David Wenham",
         plot = "King Leonidas of Sparta and a force of 300 men fight the Persians at Thermopylae in 480 B.C.",
         poster = "http://ia.media-imdb.com/images/M/MV5BMjAzNTkzNjcxNl5BMl5BanBnXkFtZTYwNDA4NjE3._V1_SX300.jpg",
         images = listOf("https://images-na.ssl-images-amazon.com/images/M/MV5BMTMwNTg5MzMwMV5BMl5BanBnXkFtZTcwMzA2NTIyMw@@._V1_SX1777_CR0,0,1777,937_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQwNTgyNTMzNF5BMl5BanBnXkFtZTcwNDA2NTIyMw@@._V1_SX1777_CR0,0,1777,935_AL_.jpg",
            "https://images-na.ssl-images-amazon.com/images/M/MV5BMTc0MjQzOTEwMV5BMl5BanBnXkFtZTcwMzE2NTIyMw@@._V1_SX1777_CR0,0,1777,947_AL_.jpg"

         ),
         rating = "7.7"),

3. navigation 이름 지정을 위한 enum class 생성(안해도 되나 확장성을 위해...)

enum class MovieScreens {
   HomeScreen,
   DetailScreen;
   companion object {
      fun fromRoute(route: String?) : MovieScreens
      = when(route?.substringBefore("/")) { // "/" 까지를 잘라 내서 버림
         HomeScreen.name -> HomeScreen
         DetailScreen.name -> DetailScreen
         null -> HomeScreen
         else -> throw java.lang.IllegalArgumentException("경로($route)를 알수 없음...")
      }
   }
}

4. enum class를 활용해서 Navigation 구성

navController 생성(제어) → navHost 구성(콘테이너) → navGraph 구성(네비게이션 전체 지도를 갖음)

@Composable
fun MovieNavigation(){
   val navController = rememberNavController()  // navigation controller
   NavHost(navController = navController,  // navHost
      startDestination = MovieScreens.HomeScreen.name
   ){ 
   // nav graph 생성
      composable(MovieScreens.HomeScreen.name){
         HomeScreen(getMovies(), navController = navController)
      }
      composable(
      // 외부에서 인자를 받아서 화면 구성을 위한 설정
         route = MovieScreens.DetailScreen.name + "/{fromHomeScreen}",
         arguments = listOf(navArgument(name = "fromHomeScreen") { type = NavType.StringType})
      ){
         DetailsScreen(
            navController = navController,
            // 위에서 받은 값을 활용해서 화면 구성을 위한 인수를 전달
            it.arguments?.getString("fromHomeScreen"))
//         Log.d("backstack","${UUID.randomUUID().toString()}")
      }
   }
}

* 기본 화면은 간단하나 확장(이벤트를 받아서 화면이동을 위한 내용은 복잡함)

5. navigation에 인수를 전달하기 위한 개별 화면 composable 

@Composable
fun MovieCardView(
   movie: Movie = getMovies()[0],
   onItemClick: (String) -> Unit = {}  // 클릭 시 네비게이션에 전달할 람다
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
      modifier = Modifier
         .padding(8.dp)
         .fillMaxWidth()
         .clickable { onItemClick("") }, // 네비게이션에 전달하는 클릭 이벤트
      elevation = 6.dp,
      border = BorderStroke(2.dp, color = MaterialTheme.colors.surface),
      shape = RoundedCornerShape(6.dp)
   ) {
      Row(
         modifier = Modifier.padding(8.dp),
         verticalAlignment = Alignment.CenterVertically
      ) {
         Surface(
            modifier = Modifier
               .padding(10.dp)
               .size(100.dp),
            shape = CircleShape,
            elevation = 9.dp
         ) {
            val painter = rememberAsyncImagePainter(
               model = ImageRequest.Builder(LocalContext.current)
                  .data(movie.images[0])
                  .crossfade(true)
                  .build(),
               contentScale = ContentScale.Crop
            )
//            Image(painter = rememberImagePainter(data = movie.images[0]), contentDescription = "poster",
//            contentScale = ContentScale.Crop)
         } // coil

         Column(
            modifier = Modifier.padding(start = 4.dp),
            horizontalAlignment = Alignment.Start,
            verticalArrangement = Arrangement.Top
         ) {
            Text(
               text = "제목 : ${movie.title}",
               style = TextStyle(color = Color.Blue, fontSize = 20.sp)
            )
            Text(
               text = "출시년: ${movie.year}",
               style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.primary)
            )
            AnimatedVisibility (visible = expanded) {
               Column(horizontalAlignment = Alignment.Start,
                  verticalArrangement = Arrangement.Top) {
                  Text( buildAnnotatedString {
                     withStyle(style = SpanStyle(
                        color = Color.Blue, fontSize = 12.sp )) {
                        append("Plot: ")
                     }
                     withStyle(style = SpanStyle(color = Color.DarkGray,
                     fontSize = 10.sp,
                     fontWeight = FontWeight.Bold )  ){
                        append(movie.plot)
                     }
                  }, modifier = Modifier.padding(6.dp))
                  Divider(modifier = Modifier.padding(5.dp))
                  Text(text = "Director : ${movie.director}", style = MaterialTheme.typography.caption)
                  Text(text = "Actor : ${movie.actors}", style = MaterialTheme.typography.caption)
                  Text(text = "Rating : ${movie.rating}", style = MaterialTheme.typography.caption)
               }
            }

            Icon(
               imageVector = if(!expanded) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp,
               contentDescription = "down",
               modifier = Modifier
                  .size(25.dp)
                  .clickable { expanded = !expanded },
               tint = Color.DarkGray
            )
         }
      }
   }
}

coil을 활용하여 url(string)을 통해 이미지을 가지고 옴.

그래들 import 필요 :    implementation "io.coil-kt:coil-compose:2.2.2"

coil 1.x 에서 사용하는 rememberImagePainter가 2.x 에서는 rememberAsyncImagePainter 로 변경됨

 

6. Lazy 화면을 구성하고, 네비게이션에 클릭 이벤트(값을 전달)를 위한 composable

@Composable
private fun MovieLazyView(movieList: List<Movie> = getMovies(), navController: NavController) {
   Surface(color = MaterialTheme.colors.background) {
      LazyColumn(modifier = Modifier.padding(6.dp)) {
         items(items = movieList) {movie ->
            MovieCardView(movie = movie) {
               navController.navigate(
                  MovieScreens.DetailScreen.name + "/${movie.id}")
            //여기서 backstackentry를 만들어서 네비게이션에 가서 관련 페이지를 찾음
            }
         }
      }
   }
}

 ==> 여기서 생성한 nav controller의 전달 값을 네비게이션에서 받아서 nav graph를 검색하여 해당되는 화면을 랜더링함

7. 네비게이션에서 받은 인수를 활용해서 화면을 생성하는 composable

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun DetailsScreen(navController: NavController,
                  fromNav: String?) {
   val newMovieList = getMovies().filter {
      it.id == fromNav
   }
   Scaffold(
      topBar = {
         MyTopAppBar(
            navIcon = Icons.Default.ArrowBack,
            desc = "Go back",
            title = "Movie Detail",
            navController = navController,
            modifier = Modifier
               .padding(vertical = 8.dp)
               .clickable { navController.popBackStack() }
         )
      },
   ) {
      Column() {
         Surface(
            modifier = Modifier
               .padding(6.dp)
               .fillMaxSize()
         ) {
            Column(horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Top) {

               MovieCardView(movie = newMovieList.first())
               Spacer(modifier = Modifier.height(8.dp))
               Divider()
               Text(text = newMovieList[0].id)
               Text(text = newMovieList[0].genre)
               Text(text = newMovieList[0].actors)
               Divider()
               Text("Movie Images")
               RowImages(newMovieList)
            }
         }
      }
   }
}

이벤트 전달 순서(순환 구조로 좀 혼란 스러움)

5(lazy에 전달한 단일 화면 구성, 클릭 람다를 hoisting)

    => 주의 사항 : 전체 리스트에서 첫번째 것만 전달

       (lazy에서는 전체 리스트 중에서 하나 씩 뽑아 내므로 결국 하나의 개별 화면 구성만 있으면 됨)

-> 6(list의 원하는 값을 전달)

-> 4(네비게이션에서 해당되는 화면을 찾아서 전달 받은 값을 다시 해당 화면에 전달)

->  7(네비게이션에서 받은 값을 활용해서 filter를 적용해서 list에서 원하는 list를 찾아서 화면 구성에 활용)

=========================================================

네비게이션을 위한 impot 항목

    implementation "androidx.navigation:navigation-compose:2.5.3"

 

기타 : 확장 아이콘을 위해서는 그래들 impot 필요

implementation "androidx.compose.material:material-icons-extended:1.4.0"