[Android/Flutter 교육] 특강 9일차

MSU·2024년 4월 19일

Android-Flutter

목록 보기
72/85
post-thumbnail

컴포즈에서는 프래그먼트라는 화면 단위가 없고
xml기반에서의 화면 이동과는 다른 개념
다른 형태의 화면으로 재구성해줌
하나의 화면 단위를 Screen이라고 부른다(Fragment와 같이 별개의 클래스가 있는 건 아님)

라이브러리를 추가해준다

implementation("androidx.navigation.navigation-compose:2.7.7")

MainActivity는 처음은 아래와 같이 지워준다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetPackExampleTheme {

            }
        }
    }
}

각 화면 이름을 저장하는 enum class를 만들어준다.

// 각 화면을 저장할 이름
enum class ScreenName(){
    MainScreen,
    InputScreen,
    OutputScreen
}

화면 네비게이션을 관리하는 객체를 생성하고 보여줄 화면을 등록해준다

// 애플리케이션 메인 코드
@Composable
fun MemoApp(modifier: Modifier = Modifier){
    // 화면 네비게이션을 관리하는 객체
    val navController = rememberNavController()
    // 보여줄 화면을 등록한다.
    NavHost(
        // 화면 네비게이션을 관리하는 객체
        navController = navController,
        // 첫 화면의 이름
        startDestination = ScreenName.MainScreen.name){

        // 사용할 화면을 등록해준다.
        // 메인 화면
        composable(
            route = ScreenName.MainScreen.name
        ){

        }
        // 입력 화면
        composable(
            route = ScreenName.InputScreen.name
        ){

        }
        // 결과 화면
        composable(
            route = ScreenName.OutputScreen.name
        ){

        }
    }
}

route는 화면 이름을 지정해주며, 새로운 화면을 보여주고 싶을 때 지정하는 이름이 route에 등록되어 있는 화면을 보여주게 된다.

이와 같이 MainActivity에서는 화면 별 이름을 지정하고 네비게이션 컨트롤러를 만든 다음에 화면 별로 준비한 코드가 동작하도록 해주기만 하면 된다.

새로운 화면을 만들 때에는 kt파일을 만들어준다.

import androidx.compose.runtime.Composable

@Composable
fun MainScreen(navHostController: NavHostController) {

}

또한 Material 3를 사용할 때는 실험적 요소를 사용하기 위한 어노테이션을 붙여준다

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController

// Material3가 아직 완성 버전이 아니기 때문에
// 실험적 요소(미완성된 UI 요소)를 사용할 때 반드시 붙여줘야 하는 어노테이션
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navHostController: NavHostController) {

}

상단 툴바는 아래와 같이 배치한다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 목록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                )
            )
        }
    ) {

    }
}

화면 본문의 레이아웃은 Column으로 아래와 같이 설정한다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 목록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                )
            )
        }
    ) {
        // 위에서 아래방향으로 배치하는 레이아웃
        Column(
            // fillMaxSize : 화면의 크기를 단말기 전체 화면으로 설정한다.
            // padding : 여백. Scaffold의 it 안에는 상단 툴바 만큼의 여백이 설정되어 있다.
            // background : 배경 색상
            modifier = Modifier.fillMaxSize().padding(it).background(Color.White)
        ) {

        }
    }
}

다시 MainActivity로 돌아가 MainScreen이 구성되도록 셋팅해준다.

        // 메인 화면
        composable(
            route = ScreenName.MainScreen.name
        ){
            // MainScreen이 구성되도록 호출한다.
            MainScreen(navHostController = navController)
        }

실행을 하면 아래와 같이 메인 화면이 나오는 것을 확인할 수 있다.

두번째 화면도 만들어준다.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.navigation.NavHostController

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InputScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 등록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                ),
                // 네비게이션 아이콘
                navigationIcon = {
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(
                            // 표시할 아이콘
                            imageVector = Icons.Filled.ArrowBack,
                            // 설명 문자열(화면에 나타나지는 않음)
                            contentDescription = "뒤로가기",
                            // 아이콘 색상
                            tint = Color.Black
                        )
                    }
                }
            )
        }
    ) {
        // 위에서 아래방향으로 배치하는 레이아웃
        Column(
            // fillMaxSize : 화면의 크기를 단말기 전체 화면으로 설정한다.
            // padding : 여백. Scaffold의 it 안에는 상단 툴바 만큼의 여백이 설정되어 있다.
            // background : 배경 색상
            modifier = Modifier
                .fillMaxSize()
                .padding(it)
                .background(Color.White)
        ) {

        }
    }
}

메인 화면에서 입력 화면으로 전환할 수 있도록 버튼을 추가해준다

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 목록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                ),
                // 툴바의 메뉴
                actions = {
                    IconButton(
                        onClick = {
                            // InputScreen이 보여지도록 한다.
                            navHostController.navigate(ScreenName.InputScreen.name)
                        }
                    ) {
                        Icon(
                            imageVector = Icons.Filled.Add,
                            contentDescription = "메모 추가",
                            tint = Color.Black
                        )
                    }
                }
            )
        }
    )

InputScreen으로 전환해주는 코드를 설정해준다.

// 애플리케이션 메인 코드
@Composable
fun MemoApp(modifier: Modifier = Modifier){
    // 화면 네비게이션을 관리하는 객체
    val navController = rememberNavController()
    // 보여줄 화면을 등록한다.
    NavHost(
        // 화면 네비게이션을 관리하는 객체
        navController = navController,
        // 첫 화면의 이름
        startDestination = ScreenName.MainScreen.name){

        // 사용할 화면을 등록해준다.
        // route : 화면 이름을 지정한다.
        // 새로운 화면을 보여주고 싶을 때 지정하는 이름이 route에 등록되어 있는 화면을 보여주게 된다.

        // 메인 화면
        composable(
            route = ScreenName.MainScreen.name
        ){
            // MainScreen이 구성되도록 호출한다.
            MainScreen(navHostController = navController)
        }
        // 입력 화면
        composable(
            route = ScreenName.InputScreen.name
        ){
            // InputScreen이 구성되도록 호출한다.
            InputScreen(navHostController = navController)
        }
        // 결과 화면
        composable(
            route = ScreenName.OutputScreen.name
        ){

        }
    }
}

실행해서 메인화면에서 등록 버튼을 눌러주면 입력 화면이 나타난다.

뒤로가기 기능은 네비게이션 컨트롤러에서 popBackStack()을 호출하면 된다.

                // 네비게이션 아이콘
                navigationIcon = {
                    IconButton(
                        onClick = {
                            // 이전 화면이 보이도록 한다.
                            navHostController.popBackStack()
                        }
                    ) {
                        Icon(
                            // 표시할 아이콘
                            imageVector = Icons.Filled.ArrowBack,
                            // 설명 문자열(화면에 나타나지는 않음)
                            contentDescription = "뒤로가기",
                            // 아이콘 색상
                            tint = Color.Black
                        )
                    }
                }

화면 전환 애니메이션은 아래와 같이 설정할 수 있다.

        // 메인 화면
        composable(
            route = ScreenName.MainScreen.name,
            // 화면 전환 애니메이션 설정
            // https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ko
            // A 화면에서 B화면으로 이동한다고 가정한다.
            // 다음 화면으로 전환 될 때 B 화면에 적용되는 애니메이션
            enterTransition = {
                slideInHorizontally(
                    // 화면의 초기 위치
                    initialOffsetX = {it},
                    // 애니메이션 부가 설정
                    // durationMillis : 애니메이션 동작 시간
                    // delayMillis : 애니메이션 대기 시간
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면으로 전환 될 때 A 화면에 적용되는 애니메이션
            exitTransition = {
                // slideOutHorizontally()
                fadeOut(
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면에서 돌아올 때 A 화면에 적용되는 애니메이션
            popEnterTransition = {
                // slideInHorizontally()
                fadeIn(
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면에서 돌아올 때 B 화면에 적용되는 애니메이션
            popExitTransition = {
                slideOutHorizontally(
                    targetOffsetX = {it},
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            }
        ){
            // MainScreen이 구성되도록 호출한다.
            MainScreen(navHostController = navController)
        }

나머지 화면에도 동일하게 애니메이션을 적용해주면 된다.

참고 링크
https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ko

컴포즈에서는 리사이클러뷰 대신 lazyColumn을 사용한다.

            // Column은 위에서 아래 방향으로 화면 요소들을 배치하기 위해 사용한다.
            // Row는 좌측에서 우측 방향으로 화면 요소들을 배치하기 위해 사용한다.
            // Box는 겹쳐지게 화면 요소들을 배치하기 위해 사용한다.
            // 위의 3개는 배치한 화면 요소들이 단말기 화면 밖으로 벗어난다고 해도 모두 생성된다.
            // Lazy로 시작하는 것들도 위와 유사하지만 화면 상에 보이지 않는 화면 요소들은
            // 생성이 대기하고 있다가 보이게 되는 순간에 생성된다.
            // 보였다가 사라진 요소들은 사용 대기상태가 되고 재사용된다.(리사이클러뷰와 동일)
            LazyColumn{
                // 리스트
                // 100개의 항목을 가진 리스트를 생성한다.
                // LazyColumn 안에 있기 때문에 보이지 않는 항목들은 생성이 대기
                // 사라진 항목들은 새롭게 나타난 항목들을 위해 재사용된다.
                // it에는 몇번째 항목인지 값이 들어온다
                items(100){
                    // 항목 하나의 모양을 구성한다.
                    Column(
                        modifier = Modifier
                            .padding(10.dp)
                            .fillMaxSize()
                    ) {
                        Text(text = "항목 입니다 : $it")
                    }
                    Divider()
                }
            }

항목을 클릭하면 내용을 보여줄 화면을 만들어준다

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ResultScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 목록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                ),
                // 네비게이션 아이콘
                navigationIcon = {
                    IconButton(
                        onClick = {
                            // 이전 화면이 보이도록 한다.
                            navHostController.popBackStack()
                        }
                    ) {
                        Icon(
                            // 표시할 아이콘
                            imageVector = Icons.Filled.ArrowBack,
                            // 설명 문자열(화면에 나타나지는 않음)
                            contentDescription = "뒤로가기",
                            // 아이콘 색상
                            tint = Color.Black
                        )
                    }
                }
            )
        }
    ) {
        // 위에서 아래방향으로 배치하는 레이아웃
        Column(
            // fillMaxSize : 화면의 크기를 단말기 전체 화면으로 설정한다.
            // padding : 여백. Scaffold의 it 안에는 상단 툴바 만큼의 여백이 설정되어 있다.
            // background : 배경 색상
            modifier = Modifier
                .fillMaxSize()
                .padding(it)
                .background(Color.White)
        ) {

            Text(text = "제목 : 제목입니다")

            // 여백
            Spacer(modifier = Modifier.padding(top = 10.dp))

            Text(text = "내용 : 내용입니다")

        }
    }
}

메인 화면에서 항목을 눌렀을 때 결과 화면이 보여지도록 한다

                items(100){
                    // 항목 하나의 모양을 구성한다.
                    Column(
                        modifier = Modifier
                            .padding(10.dp)
                            .fillMaxSize()
                            // 항목을 눌렀을 때
                            .clickable {
                                // 결과 화면이 보이도록 한다.
                                navHostController.navigate(ScreenName.OutputScreen.name)
                            }
                    ) {
                        Text(text = "항목 입니다 : $it")
                    }
                    Divider()
                }

네비게이션 컨트롤러의 결과 화면 부분을 셋팅한다

        // 결과 화면
        composable(
            route = ScreenName.OutputScreen.name,
            // 화면 전환 애니메이션 설정
            // https://developer.android.com/develop/ui/compose/animation/composables-modifiers?hl=ko
            // A 화면에서 B화면으로 이동한다고 가정한다.
            // 다음 화면으로 전환 될 때 B 화면에 적용되는 애니메이션
            enterTransition = {
                slideInHorizontally(
                    // 화면의 초기 위치
                    initialOffsetX = {it},
                    // 애니메이션 부가 설정
                    // durationMillis : 애니메이션 동작 시간
                    // delayMillis : 애니메이션 대기 시간
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면으로 전환 될 때 A 화면에 적용되는 애니메이션
            exitTransition = {
                // slideOutHorizontally()
                fadeOut(
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면에서 돌아올 때 A 화면에 적용되는 애니메이션
            popEnterTransition = {
                // slideInHorizontally()
                fadeIn(
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            },
            // 다음 화면에서 돌아올 때 B 화면에 적용되는 애니메이션
            popExitTransition = {
                slideOutHorizontally(
                    targetOffsetX = {it},
                    animationSpec = tween(
                        durationMillis = 200,
                        delayMillis = 200
                    )
                )
            }
        ){
            // ResultScreen이 구성되도록 호출한다.
            ResultScreen(navHostController = navController)
        }

입력 화면은 아래와 같이 구성한다.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InputScreen(navHostController: NavHostController) {

    Scaffold(
        // 상단 툴바
        topBar = {
            TopAppBar(
                // 툴바에 표시될 타이틀
                title = {
                    Text(text = "메모 등록")
                },
                // 툴바의 색상
                colors = TopAppBarDefaults.topAppBarColors(
                    // 툴바 색상
                    containerColor = Color.White,
                    // 툴바의 타이틀 색상
                    titleContentColor = Color.Black
                ),
                // 네비게이션 아이콘
                navigationIcon = {
                    IconButton(
                        onClick = {
                            // 이전 화면이 보이도록 한다.
                            navHostController.popBackStack()
                        }
                    ) {
                        Icon(
                            // 표시할 아이콘
                            imageVector = Icons.Filled.ArrowBack,
                            // 설명 문자열(화면에 나타나지는 않음)
                            contentDescription = "뒤로가기",
                            // 아이콘 색상
                            tint = Color.Black
                        )
                    }
                },
                // 우측 상단 메뉴
                actions = {
                    IconButton(
                        onClick = {
                            navHostController.popBackStack()
                        }
                    ) {
                        Icon(
                            imageVector = Icons.Filled.Done,
                            contentDescription = "완료",
                            tint = Color.Black
                        )
                    }
                }
            )
        }
    ) {
        // 위에서 아래방향으로 배치하는 레이아웃
        Column(
            // fillMaxSize : 화면의 크기를 단말기 전체 화면으로 설정한다.
            // padding : 여백. Scaffold의 it 안에는 상단 툴바 만큼의 여백이 설정되어 있다.
            // background : 배경 색상
            modifier = Modifier
                .fillMaxSize()
                .padding(it)
                .background(Color.White)
        ) {
            // 제목 입력 요소와 연결되어 있는 데이터 관리 요소
            val subjectTextState = remember {
                mutableStateOf("")
            }
            // 제목 입력 요소
            TextField(
                // TextField가 관리하는 값
                // 데이터 관리 요소를 지정해준다.
                value = subjectTextState.value,
                // 사용자가 입력한 값이 변경되었을 때
                onValueChange ={
                    subjectTextState.value = it
                },
                // hint
                placeholder = {
                    Text(text = "제목을 입력해주세요")
                },
                // 한 줄 입력 요소로 설정한다.
                singleLine = true,
                // 가로 길이
                modifier = Modifier.fillMaxWidth(),
                // 색상
                colors = TextFieldDefaults.colors(
                    // 포커스가 주어졌을 때의 색상(투명색으로 설정)
                    focusedContainerColor = Color.Transparent,
                    // 포커스가 사렺ㅆ을 때의 배경 색상(투명색으로 설정)
                    unfocusedContainerColor = Color.Transparent,
                ),
                // 좌측의 아이콘
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Filled.AccountCircle,
                        contentDescription = "제목",
                    )
                },
                // 우측의 아이콘
                trailingIcon = {
                    IconButton(
                        onClick = {
                            // 제목 TextField에 연결되어 있는 데이터 관리요소에
                            // 길이가 0인 문자열을 넣어준다.
                            subjectTextState.value = ""
                        }
                    ) {
                        Icon(imageVector = Icons.Filled.Clear, contentDescription = "초기화")
                    }
                }
            )
            
            // 여백
            Spacer(modifier = Modifier.padding(top = 10.dp))

            // 내용 입력 요소와 연결되어 있는 데이터 관리 요소
            val contentTextState = remember {
                mutableStateOf("")
            }
            // 제목 입력 요소
            TextField(
                // TextField가 관리하는 값
                // 데이터 관리 요소를 지정해준다.
                value = contentTextState.value,
                // 사용자가 입력한 값이 변경되었을 때
                onValueChange ={
                    contentTextState.value = it
                },
                // hint
                placeholder = {
                    Text(text = "내용을 입력해주세요")
                },
                // 가로 길이
                modifier = Modifier.fillMaxWidth(),
                // 색상
                colors = TextFieldDefaults.colors(
                    // 포커스가 주어졌을 때의 색상(투명색으로 설정)
                    focusedContainerColor = Color.Transparent,
                    // 포커스가 사렺ㅆ을 때의 배경 색상(투명색으로 설정)
                    unfocusedContainerColor = Color.Transparent,
                ),
                // 좌측의 아이콘
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Filled.AccountBox,
                        contentDescription = "내용",
                    )
                },
                // 우측의 아이콘
                trailingIcon = {
                    IconButton(
                        onClick = {
                            // 제목 TextField에 연결되어 있는 데이터 관리요소에
                            // 길이가 0인 문자열을 넣어준다.
                            contentTextState.value = ""
                        }
                    ) {
                        Icon(imageVector = Icons.Filled.Clear, contentDescription = "초기화")
                    }
                }
            )
            
        }
    }
}

profile
안드로이드공부

0개의 댓글