관리 메뉴

나만을 위한 블로그

[Android Compose] 화면 이동 구현하는 법 본문

Android/Compose

[Android Compose] 화면 이동 구현하는 법

참깨빵위에참깨빵 2023. 7. 16. 00:57
728x90
반응형

※ 이 포스팅의 예제 코드는 안드로이드 스튜디오 플라밍고 버전에서 작성했기 때문에 구버전 안드로이드 스튜디오를 쓰는 경우 컴파일 에러가 발생하는 import문이 있을 수 있다.

 

Compose를 사용하지 않는다면 인텐트와 startActivity()를 써서 다른 액티비티로 이동할 수 있었다.

하지만 Compose에선 startActivity() 대신 네비게이션 라이브러리를 써서 화면 이동을 구현한다. 코드를 보기 전에 왜 Compose에선 이딴이런 식으로 화면 이동을 구현해야 하는 것인가?

이 글을 보는 사람이라면 Compose가 선언형 방식으로 UI를 구성한다는 건 알고 있을 것이다. 이 특징 때문에 네비게이션 라이브러리를 사용하는 것이다.

안드로이드의 전통적인 화면 전환 방법인 startActivity()는 명령형 프로그래밍 패러다임을 따르는 함수다. 그래서 선언형 프로그래밍 패러다임인 Compose와는 잘 맞지 않는다. 안드로이드 공식문서 중에는 아예 Compose에서 이동을 구현하려면 네비게이션을 사용하는 것을 전제에 깔고 설명하는 문서도 있다.

 

https://developer.android.com/jetpack/compose/navigation?hl=ko 

 

Compose를 통해 이동  |  Jetpack Compose  |  Android Developers

Compose를 통해 이동 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Navigation 구성요소는 Jetpack Compose 애플리케이션을 지원합니다. Navigation 구성요소의 인프라

developer.android.com

 

물론 네비게이션을 쓰면 여러 장점이 있긴 하지만 사람에 따라선 그냥 프래그먼트 쓰는 게 낫다는 생각도 할 수 있을 정도로 준비 작업 등 여러가지가 귀찮다.

다행히 Compose에서 화면 이동 시 네비게이션을 구현하는 것은 생각보다 어렵지 않고 간단하기 때문에 방법만 잘 알아두면 확장 함수로든 뭘로든 만들어서 쓰면 될 것이다. 그러나 화면 이동 하나 구현하려면 라이브러리 추가해야 하는 건 매우 귀찮기 때문에 그냥 프로젝트 만들 때 기본으로 추가되게 업데이트 해주면 좋겠다.

 

이제 시작한다. 먼저 앱 gradle에 의존성을 추가한다.

 

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

 

그리고 화면 이동 시 어떤 경로로 이동할지 알아야 하기 때문에 enum을 만든다.

 

enum class TodoScreen {
    Start,
    Write,
    Edit
}

 

이 enum은 URL과 비슷한 것이다. URL이 웹사이트의 다른 페이지에 매핑되는 것처럼 경로는 대상에 매핑되서 고유 식별자 역할을 하게 된다. 

아래는 MainActivity의 코드다.

 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.composetodo.ui.theme.ComposeTodoTheme
import com.example.composetodo.ui.theme.WriteTodoScreen

@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTodoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val navController = rememberNavController()
                    Scaffold(
                        topBar = { TopAppBar(
                            title = { Text("ComposeTodo") }
                        )},
                        floatingActionButtonPosition = FabPosition.End,
                        floatingActionButton = {
                            FloatingActionButton(onClick = {
                                navController.navigate(TodoScreen.Write.name)
                            }) {
                                Image(painter = painterResource(id = R.drawable.baseline_add_24), contentDescription = null)
                            }
                        }
                    ) { paddingValues ->
                        NavHost(navController = navController, startDestination = TodoScreen.Start.name) {
                            composable(route = TodoScreen.Start.name) {
                                MainScreen(paddingValues = paddingValues)
                            }
                            composable(route = TodoScreen.Write.name) {
                                WriteTodoScreen(title = "", content = "", paddingValues = paddingValues)
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun MainScreen(
    paddingValues: PaddingValues
) {
    val scrollState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = paddingValues.calculateTopPadding())
            .verticalScroll(scrollState)
    ) {
        repeat(10) {
            TodoCard(title = "제목 $it", content = "내용 $it")
            Spacer(modifier = Modifier.height(8.dp))
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    ComposeTodoTheme {
    }
}

 

painterResource() 안의 드로어블이 없어서 컴파일 에러가 날 수 있다. 대충 더하기 기호 모양의 드로어블을 추가하고 그 이름을 넣어주면 된다.

자잘한 코드들 다 쳐내고 화면 이동 관련 코드부터 확인한다.

 

val navController = rememberNavController()

 

rememberNavController()를 통해 화면 이동에 필요한 클래스를 얻어 온다. 그리고 Scaffold의 후행 람다 안에 NavHost를 넣는다.

 

NavHost(navController = navController, startDestination = TodoScreen.Start.name) {
    composable(route = TodoScreen.Start.name) {
        MainScreen(paddingValues = paddingValues)
    }
    composable(route = TodoScreen.Write.name) {
        WriteTodoScreen(title = "", content = "", paddingValues = paddingValues)
    }
}

 

NavHost는 지정된 경로 기반으로 다른 컴포저블 대상을 표시하는 컴포저블이다. 위 코드에선 경로가 Write라면 Todo를 작성하는 화면을 표시한다. 아래는 NavHost 문법을 간략화한 그림이다.

 

 

navController에는 rememberNavController()의 값을 갖고 있는 navController 참조를 넣으면 된다. startDestination에는 앱에서 NavHost를 처음 표시할 때 보여줄 컴포저블을 정의하는 문자열 경로다.

그리고 그 밑에 composable() {}을 사용해서 route 매개변수에 Start, Write를 각각 대입한 다음, 후행 람다 안에서 컴포저블 함수를 호출하고 있다. 즉 지정된 enum 값을 받을 때마다 특정 컴포저블을 호출해서 화면에 표시하는 것이다. 이렇게 함으로써 기존 화면은 애니메이션 처리되면서 사라지고 새로운 화면이 핸드폰에 표시되는 것이다.

 

현재 MainScreen()을 표시하고 있으니 FAB를 클릭하면 작성하는 화면으로 이동하도록 해야 한다. 이렇게 하려면 아래처럼 작성한다.

 

FloatingActionButton(onClick = {
    navController.navigate(TodoScreen.Write.name)
}

 

navigate()는 하나의 매개변수만 받는데 어떤 경로로 이동할 것인지를 받는다. 이 경로는 먼저 정의했던 enum class 안의 값들이다. 경로가 NavHost의 composable() 호출 중 하나와 일치하면 그 화면으로 이동한다.

 

NavHost(navController = navController, startDestination = TodoScreen.Start.name) {
    composable(route = TodoScreen.Start.name) {
        MainScreen(paddingValues = paddingValues)
    }
    composable(route = TodoScreen.Write.name) { // <- 여기로 이동
        WriteTodoScreen(title = "", content = "", paddingValues = paddingValues)
    }
}

 

Write 경로가 입력되면 WriteTodoScreen()이 호출되면서 MainScreen()이 사라지고 WriteTodoScreen()이 표시된다.

startActivity()를 쓰면 기존 액티비티를 새 액티비티가 덮는 것처럼, 네비게이션을 쓰면 기존 컴포저블을 새 컴포저블이 덮는 것이라고 이해하면 된다.

 

 

참고한 사이트)

 

https://developer.android.com/codelabs/basic-android-kotlin-compose-navigation?hl=ko#0 

 

Compose를 사용하여 화면 간 이동  |  Android Developers

Cupcake 앱에 Navigation 구성요소를 추가하여 앱의 흐름을 구성하고 여러 화면 간에 데이터를 탐색하고 전달합니다.

developer.android.com

 

https://stackoverflow.com/questions/69186351/jetpack-compose-using-new-activity-or-new-navigation

 

Jetpack Compose, using new Activity or new Navigation?

I'm here looking for the best practices in Jetpack Compose, what is more best practice, work using new Activity (by using intent) startActivity(Intent(this, AnotherActivity::class.java)) or using

stackoverflow.com

 

반응형
Comments