Search
castle

상태 기반 UI 구조 이해

  • Compose는 상태 기반 UI
    • 값(state) 변경UI도 같이 변함(업데이트)
    • recomposition : 자동으로 UI 로드
  • mutableStateOf(value) : 상태값을 생성하는 함수
  • remember {...}
    • 컴포저블 함수 재구성 시 상태를 유지
    • 람다 결과를 캐싱 → 컴포저블 상태를 저장
  • val variable = remenber { mutableStateOf(<T>) }
    • variable.value를 통해 값을 읽고 쓸 수 있음
    • variable.value 변경 시 자동으로 UI recomposite
    • by와의 차이점 : by 사용 시 value만 가짐, mutableStateOf 객체의 경우 객체로 사용
@Composable
fun CountEx(){
	val count = remember { mutableStateOf(0) }
	// val countBy by remember { mutableStateOf(0) }
	
	Column {
		Text("count: ${count.value}")
		Button(onClick = { count.value++ }) {
			Text("+1 증가")
		}
		// Text("count: ${countBy}")
		// Button(onClick = { countBy++ }) {
		// 	Text("+1 증가")
		// }		
	}
}

Navigation Compose

  • 라우팅(url) 기반
  • SAA(Sing Activity Architecture)
  • 여러 화면 전환
  • 네비게이션 그래프(NavGraph) 기반으로 전환/이동할 수 있게 해주는 라이브러리
  • 기존 XML의 FragementManager 또는 Intent 역할 → NavController + NavHost

image.png

  • 의존성 필요(Gradle)
    • project build.gradle이 아닌 module build.gradle
// build.gradle.kts (:app)
dependencies {
    val nav_version = "2.9.3"

    implementation("androidx.navigation:navigation-compose:${nav_version}")
}
  • 기타 dependencies
plugins {
  // Kotlin serialization plugin for type safe routes and navigation arguments
  kotlin("plugin.serialization") version "2.0.21"
}

dependencies {
  val nav_version = "2.9.3"

  // Jetpack Compose integration
  implementation("androidx.navigation:navigation-compose:$nav_version")

  // Views/Fragments integration
  implementation("androidx.navigation:navigation-fragment:$nav_version")
  implementation("androidx.navigation:navigation-ui:$nav_version")

  // Feature module support for Fragments
  implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")

  // Testing Navigation
  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")

  // JSON serialization library, works with the Kotlin serialization plugin
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
  • 구성 요소
구성 요소 설명 역할 / 특징
NavController 네비게이션을 제어하는 핵심 객체  
화면 이동 담당 - 현재 화면 상태 추적- navigate(), popBackStack() 등으로 화면 전환 관리  
NavHost 네비게이션 그래프를 표시하는 컨테이너  
화면의 출발점 - NavController와 연결- 어떤 Composable 화면이 보여질지 결정  
NavGraph 화면 간 이동 경로와 구조 정의  
route - 화면들의 관계, 시작 지점(startDestination) 정의  
Composable Destination 실제로 표시되는 화면(Composable 함수) - composable("routeName") { ... } 형태로 정의- 하나의 화면을 네비게이션 목적지로 등록
Route (경로) 각 화면을 식별하는 고유 문자열 - composable("home") 처럼 지정- 이동 시 navController.navigate("home") 로 호출
Arguments 화면 간 데이터 전달 요소 - navArgument 를 사용- 예: composable("detail/{itemId}")
BackStack 이동 기록을 저장하는 스택 구조 - popBackStack() 호출 시 이전 화면으로 돌아감
rememberNavController() Composable 내부에서 NavController 생성 - 상태를 기억(remember)하여 재구성 시에도 NavController 유지

시작 base경로를 설정,
이후 이동 상태를 stack 형태로 저장해 실행 관리
NavController와 탐색 그래프를 연결하는 Composable이다.
현재 NavController의 상태에 따른 Composable 대상을 표시

@Composable
fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
            fadeIn(animationSpec = tween(700))
        },
    exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
            fadeOut(animationSpec = tween(700))
        },
    popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = enterTransition,
    popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = exitTransition,
    sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null
): Unit

Parameters  
navController: NavHostController the navController for this host
startDestination: String the route for the start destination
modifier: Modifier = Modifier The modifier to be applied to the layout.
contentAlignment: Alignment = Alignment.TopStart The Alignment of the AnimatedContent
route: String? = null the route for the graph
enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { fadeIn(animationSpec = tween(700)) } callback to define enter transitions for destination in this host
exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = { fadeOut(animationSpec = tween(700)) } callback to define exit transitions for destination in this host
popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = enterTransition callback to define popEnter transitions for destination in this host
popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = exitTransition callback to define popExit transitions for destination in this host
sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null callback to define the size transform for destinations in this host
builder: NavGraphBuilder.() -> Unit the builder used to construct the graph
  • rememberNavController() : navigation stack
    • NavHostController 객체
    • Composable 함수 내에서 사용
    • 파라미터 전달을 통해 화면 재구성 시에도 instance 유지
// MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplication02Theme {
                    MyAppNavHost()
            }
        }
    }
}

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
) {
    var countMainButton by remember { mutableStateOf(1) }

    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = MAIN_SCREEN_ROOT,
        ) {
            composable(MAIN_SCREEN_ROOT) {
                GreetingMain(
                    name = "Android",
                    modifier = Modifier.padding(innerPadding),
                    navController = navController,
                    count = countMainButton,
                    onIncrementCount = { countMainButton++ },
                )
            }

            composable(SCREEN01_SCREEN_ROOT) {
                Screen(navController = navController)
            }
        }

    }
}

// Screen01.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Screen(navController: NavHostController) {
    Scaffold(modifier = Modifier.fillMaxSize(),
        topBar = { TopAppBar(title = { Text("탑바") }) },
        floatingActionButton = {
            FloatingActionButton(onClick = {}) { Text("+") }
        },
        bottomBar = {
            BottomAppBar {
                Text(
                    "Bottom Bar",
                    modifier = Modifier.padding(16.dp)
                )
            }
        },
    ) { innerPadding ->
        Surface(
            modifier = Modifier.fillMaxSize(0.8f),
            contentColor = Color.Red
        ) {
            Greeting(
                name = "한글",
                modifier = Modifier.padding(innerPadding),
                navController = navController
            )
        }
    }
}

화면 간 데이터 전달

  • / 이용
    • composable("tagetNav/{data}") {val userName = ...}
    • 가능은 하나 보안에 취약(인코딩 등의 과정 필요)
// MyAppNavHost
...        
        NavHost(
            navController = navController,
            startDestination = MAIN_SCREEN_ROOT,
        ) {
            composable("$MAIN_SCREEN_ROOT") { backStackEntry ->
                GreetingMain(
                    name = "",
                    modifier = Modifier.padding(innerPadding),
                    navController = navController,
                    count = countMainButton,
                    onIncrementCount = { countMainButton++ },
                )
            }

            composable("$MAIN_SCREEN_ROOT/{value}") { backStackEntry ->
                val value = backStackEntry.arguments?.getString("value") ?: ""

                GreetingMain(
                    name = value,
                    modifier = Modifier.padding(innerPadding),
                    navController = navController,
                    count = countMainButton,
                    onIncrementCount = { countMainButton++ },
                )
            }

            ...
...

// Greeting02(Screen02)
...
            Button(onClick = { navController.navigate("$MAIN_SCREEN_ROOT/${value.value}") }) {
                Text("홈으로 데이터 전송")
            }
...

image.png

image.png

left
right

C

Contents