레시피 앱에 navigation 추가해 디테일 페이지로 이동해보기
네비게이션을 사용하기 위해선 디팬던시 필요
Kotlin 1.8.22 버전에 사용할 수 있는 navigation 버전은 2.5.3
implementation 'androidx.navigation:navigation-compose:2.5.3'
매개변수로 category를 받는 데테일페이지 만들기
- 스크롤 텍스트 만들기
Text 컴포넌트 modifier에 스크롤 속성을 가지고 있다
rememberScrollState이 스크롤이 어떤 상태 인지를 알려준다Text( text = category.strCategoryDescription, textAlign = TextAlign.Justify, //부드러운 줄바꿈 해줌 color = Color.Black, modifier = Modifier .verticalScroll(rememberScrollState()) //텍스트 스크롤 rememberScrollState이 스크롤이 어떤 상태 인지를 알려준다 .padding(top = 4.dp) )
package com.lullulalal.myrecipeapp
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
//디테일 뷰
@Composable
fun RecipeDetailScreen(category:Category) {
Column(modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = category.strCategory,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom =4.dp)
)
Image(
painter = rememberAsyncImagePainter(category.strCategoryThumb),
contentDescription = "${category.strCategory} Thumbnail",
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f)) // 1:1
//스크롤 텍스트 만들기
Text(
text = category.strCategoryDescription,
textAlign = TextAlign.Justify, //부드러운 줄바꿈 해줌
color = Color.Black,
modifier = Modifier
.verticalScroll(rememberScrollState()) //텍스트 스크롤 rememberScrollState이 스크롤이 어떤 상태 인지를 알려준다
.padding(top = 4.dp)
)
}
}
매개 변수로 이동할 페이지를 알려줘야함
이동 시 Category를 넘겨줘야함
- clickable 속상 사용하기
item 클릭 시 이동 해당 아이템의 디테일 페이지로 이동하도록 한다
Column에 modifier에 clickable 속성을 가지고 있음Column(modifier = Modifier .padding(8.dp) .fillMaxSize() .clickable { navigateTorecipeDetailScreen(category) }, //clickable은 modifier의 속성 중 하나 horizontalAlignment = Alignment.CenterHorizontally ) {...}
package com.lullulalal.myrecipeapp
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter
//사용자한테 보여질 UI 담당 파일
@Composable
fun RecipeScreen(modifier: Modifier = Modifier,
navigateTorecipeDetailScreen: (Category) -> Unit) {
val recipeViewModel : MainViewModel = viewModel() //데이터 가져오는 역할
val viewState by recipeViewModel.categoriesState
//getValue import -> state의 값을 바로 가져올 수 있도록 해주는 애
//ui 그리기용
Box(modifier = Modifier.fillMaxSize()) {
//로딩중 표시
when {
viewState.loading -> {
CircularProgressIndicator(modifier.align(Alignment.Center))
}
viewState.error != null -> {
Text(text = "Error")
}
else -> {
//제대로 로딩됐을 때
CategorScreen(categories = viewState.list, navigateTorecipeDetailScreen)
}
}
}
}
//리스트 항목을 생성하기 위한 view
@Composable
fun CategorScreen(categories : List<Category>,
navigateTorecipeDetailScreen: (Category) -> Unit) {
//LazyVerticalGrid => 항목을 격자 무늬로 배치
LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize()) {
//아이템과 ui 연결
items(categories) {
category ->
CategoyItem(category = category, navigateTorecipeDetailScreen)
}
}
}
//각 아이템 생김새 ui
@Composable
fun CategoyItem(category: Category,
navigateTorecipeDetailScreen: (Category) -> Unit) {
Column(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
.clickable { navigateTorecipeDetailScreen(category) }, //clickable은 modifier의 속성 중 하나
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = rememberAsyncImagePainter(category.strCategoryThumb),
contentDescription = category.strCategoryDescription,
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f)) // 1:1
Text(
text = category.strCategory,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(top=4.dp)
)
}
}
sealed이란?
- 서브 클래스의 자료형 중 하나를 따르는 자료형인 것을 미리 알 수 있을 때 사용함
- 런타임이 아니라 컴파일할 때 일치하는 자료형으로 제한하여 안정성을 보장함
- 이를 통해 실수로 경로의 이름을 틀리거나 한 경우 경로를 호출할 때 문제가 발생하지 않도록 할 수 있다
- 일종의 제네릭 같은거..라고 할 수 있나..
package com.lullulalal.myrecipeapp
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
sealed class Screen(val route:String) {
//경로 탐색을 담당하는 클래스
//1. 매개변수로 val route:String 전달
//2. 이동할 화면 만큼 객체(Object)를 만든다
// 이동할 각 화면을 상수로 만드는데 sealed Screen 클래스를 이용해서 객체로 만들고 경로 이름을 전달하고 있음
object RecipeScreen:Screen("recipeScreen")
object RecipeDetailScreen:Screen("recipeDetailScreen")
}
화면 이동 담당할 @Composable을 가진 파일 생성하고 NavHost를 사용하여 화면간 이동을 엮어준다
@Composable
fun ReciprApp(navController: NavHostController) {...}
이 파일에서 데이터를 전달해 줄것이기 때문에 기존 RecipeScreen 파일에 변수로 사용한 val viewState by recipeViewModel.categoriesState 삭제
//데이터 전달하기 위해 데이터 담당 부름
val recipeViewModel : MainViewModel = viewModel() //데이터 가져오는 역할
val viewState by recipeViewModel.categoriesState
//getValue import -> state의 값을 바로 가져올 수 있도록 해주는 애
//viewState를 이용해서 다른 화면으로 이동 뿐 아니라 RecipeScreen을 보여줌
package com.lullulalal.myrecipeapp
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter
//사용자한테 보여질 UI 담당 파일
@Composable
fun RecipeScreen(modifier: Modifier = Modifier,
viewState:MainViewModel.RecipeState,
navigateTorecipeDetailScreen: (Category) -> Unit) {
val recipeViewModel : MainViewModel = viewModel() //데이터 가져오는 역할
// val viewState by recipeViewModel.categoriesState
// -- 기존에 바로 viewState를 만들어서 사용한 대신 ReciprApp에서 전달해주기 위해 삭제
//ui 그리기용
Box(modifier = Modifier.fillMaxSize()) {
//로딩중 표시
when {
viewState.loading -> {
CircularProgressIndicator(modifier.align(Alignment.Center))
}
viewState.error != null -> {
Text(text = "Error")
}
else -> {
//제대로 로딩됐을 때
CategorScreen(categories = viewState.list, navigateTorecipeDetailScreen)
}
}
}
}
//리스트 항목을 생성하기 위한 view
@Composable
fun CategorScreen(categories : List<Category>,
navigateTorecipeDetailScreen: (Category) -> Unit) {
//LazyVerticalGrid => 항목을 격자 무늬로 배치
LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize()) {
//아이템과 ui 연결
items(categories) {
category ->
CategoyItem(category = category, navigateTorecipeDetailScreen)
}
}
}
//각 아이템 생김새 ui
@Composable
fun CategoyItem(category: Category,
navigateTorecipeDetailScreen: (Category) -> Unit) {
Column(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
.clickable { navigateTorecipeDetailScreen(category) }, //clickable은 modifier의 속성 중 하나
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = rememberAsyncImagePainter(category.strCategoryThumb),
contentDescription = category.strCategoryDescription,
modifier = Modifier
.fillMaxSize()
.aspectRatio(1f)) // 1:1
Text(
text = category.strCategory,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(top=4.dp)
)
}
}
5-4-1) NavHost에 초기 화면 설정
NavHost(navController = navController, startDestination = Screen.RecipeScreen.route) {...}
5-4-2) 데이터 전달하기
1.composable에 경로 설정
2.이동할 화면 설정 RecipeScreen
3.상세화면으로 보내줄 데이터 저장 : navController.currentBackStackEntry?.savedStateHandle?.set("cat",it)
4.상세 화면으로 이동 : navController.navigate
composable(route = Screen.RecipeScreen.route) { //1.경로 설정
RecipeScreen(viewState=viewState, navigateTorecipeDetailScreen = {
//2. 이 부분이 현재 화면에서 상세 화면으로 데이터를 전달하는 역할을 한다
//3.상세화면으로 보내줄 데이터 저장
//navController.currentBackStackEntry => 현재 화면의 상태를 나타내는 부분 탐색 지점을 얻음
//savedStateHandle => 다른 화면 간에 데이터를 전달하는 역할을 한다
//set("cat",it) => savedStateHandle에 키-값 쌍을 저장함
navController.currentBackStackEntry?.savedStateHandle?.set("cat",it)
//4.상세 화면으로 이동
navController.navigate(Screen.RecipeDetailScreen.route)
})
}
5-4-3)데이터 받기
1.composable에 경로 설정
2.전달 받은 it:NavBackStackEntry -> previousBackStackEntry를 통해 전달받은 카테고리를 얻어옴
3.실제로 detail화면으로 데이터 전달하기
composable(route = Screen.RecipeDetailScreen.route) {
//1. 전달 받은 it:NavBackStackEntry -> previousBackStackEntry를 통해 전달받은 카테고리를 얻어옴
val category = navController.previousBackStackEntry?.savedStateHandle?.
get<Category>("cat") ?: Category("","","","")
//2. 실제로 detail로 전달하기
RecipeDetailScreen(category = category)
}
package com.lullulalal.myrecipeapp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@Composable
fun ReciprApp(navController: NavHostController) {
//NavHostController
//NavHost에서 사용하기 위해 매개변수로 받는다
//데이터 전달하기 위해 데이터 담당 부름
val recipeViewModel : MainViewModel = viewModel() //데이터 가져오는 역할
val viewState by recipeViewModel.categoriesState
//getValue import -> state의 값을 바로 가져올 수 있도록 해주는 애
//viewState를 이용해서 다른 화면으로 이동 뿐 아니라 RecipeScreen을 보여줌
//startDestination에 경로를 가진 Screen 클래스 사용하기
NavHost(navController = navController, startDestination = Screen.RecipeScreen.route) {
composable(route = Screen.RecipeScreen.route) { //1.경로 설정
RecipeScreen(viewState=viewState, navigateTorecipeDetailScreen = {
//2. 이 부분이 현재 화면에서 상세 화면으로 데이터를 전달하는 역할을 한다
//3.상세화면으로 보내줄 데이터 저장
//navController.currentBackStackEntry => 현재 화면의 상태를 나타내는 부분 탐색 지점을 얻음
//savedStateHandle => 다른 화면 간에 데이터를 전달하는 역할을 한다
//set("cat",it) => savedStateHandle에 키-값 쌍을 저장함
navController.currentBackStackEntry?.savedStateHandle?.set("cat",it)
//4.상세 화면으로 이동
navController.navigate(Screen.RecipeDetailScreen.route)
})
}
composable(route = Screen.RecipeDetailScreen.route) {
//1. 전달 받은 it:NavBackStackEntry -> previousBackStackEntry를 통해 전달받은 카테고리를 얻어옴
val category = navController.previousBackStackEntry?.savedStateHandle?.
get<Category>("cat") ?: Category("","","","")
//2. 실제로 detail로 전달하기
RecipeDetailScreen(category = category)
}
}
}
객체를 한 화면에서 다른 화면으로 전달하려면 직렬화-역직렬화 해야됨 이를 도와주는 것이 Parcelable 키워드이다
이것을 사용함으로써 saveStateHandel에 객체 전체를 저장, 전달 할 수 있도록 해준다
- 직렬화 : 데이터(객체)를 이동할 때 형태에 맞게 안전하게 이동시키기 위한 것, 직렬화는 기본적으로 객체를 스트링으로 만듦
- 역직렬화 : 데이터(객체)가 목적지에 도착하면 원래의 형태로 바꾸는 것, 다시 객체로 변함
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
}
1.@Parcelize 붙이기
2.Parcelable 붙여주기
package com.lullulalal.myrecipeapp
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Category(
val idCategory : String,
val strCategory : String,
val strCategoryThumb : String,
val strCategoryDescription : String,
):Parcelable
data class CategoriesResponse(val categories:List<Category>)
참고)
https://tutorials.eu/mastering-screen-navigation-with-kotlin-day-10-android-14-masterclass/