깨알 개념/Android

[Android] Jetpack Compose - Scaffold

interfacer_han 2024. 9. 5. 18:52

#1 개요

#1-1 Scaffold의 사전적 의미

이미지 출처: https://en.dict.naver.com/#/entry/enko/f85a34c157c64847ad1a1a407d9720a9

Scaffold비계((건설) 높은 곳에서 공사를 할 수 있도록 임시로 설치한 가설물)라는 단어로 번역된다.

 

#1-2 Scaffold in Jetpack Compose

 

Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Scaffold 머티리얼 디자인에서 스캐폴드는 스캐폴드로

developer.android.com

Jetpack Compose에도 Scaffold라는 이름의 객체가 존재한다. 이 Scaffold에 대해 누군가 무엇을 위한 가설물이냐고 묻는다면, 바로 다양한 UI 요소를 위한 가설물이라는 대답을 받게될 것이다. Scaffold는 UI을 위한 가설물이다. 혹은 일종의 UI적 도구들의 모임이라고도 말할 수 있다.

 

#2 Scaffold 사용하기

#2-1 기초 (가설물 세우기)

// package com.example.scaffold

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                /*
                ↓ ↓ ↓
                Scaffold에 '설치'할 UI 컴포넌트들을 선언할 위치
                ↑ ↑ ↑
                */
            ) { innerPadding -> // Scaffold가 계산한, content가 Scaffold의 다른 UI 요소들과 겹치지 않을 정도의 Padding값
                Box(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxSize()
                        .background(Color.LightGray)
                ) {
                    Text(
                        text = "Content Text",
                        modifier = Modifier.align(Alignment.Center),
                        fontSize = 30.sp
                    )
                }
            }
        }
    }
}

Scaffold의 뼈대만 구현했다. #3 ~ #6에서 이 뼈대에 UI 구성 요소를 하나씩 추가해보겠다.

 

#2-2 작동 확인

아무런 UI 구성 요소 없이 뼈대만 구현했기 때문에, Scaffold가 없는 것처럼 보인다.

 

#2-3 세부 구현 (UI적 도구들)

@Composable
public fun Scaffold(
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {}, // #3에서 다룸
    bottomBar: @Composable () -> Unit = {}, // #4에서 다룸
    snackbarHost: @Composable () -> Unit = {}, // #5에서 다룸
    floatingActionButton: @Composable () -> Unit = {}, // #6에서 다룸
    floatingActionButtonPosition: FabPosition = FabPosition.End, // #6에서 다룸
    containerColor: Color = MaterialTheme.colorScheme.background, // #7에서 다룸
    contentColor: Color = contentColorFor(containerColor), // #7에서 다룸
    contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, // #7에서 다룸
    content: @Composable (PaddingValues) -> Unit
): Unit

(참조) Scaffold에 대한 공식 문서.

 

이제, 이 뼈대(Scaffold)에 '설치'할 수 있는 UI 요소들에 대해 살펴보겠다.

 

#3 topBar

#3-1 기초

...
import androidx.compose.material3.TopAppBar

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3Api::class) // androidx.compose.material3.TopAppBar 버전이 2024년 8월 기준 아직 안정화되지 않아서, 이 경고용 어노테이션을 요구한다.
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                topBar = {
                    TopAppBar(
                        title = { Text("TopBar Text") }
                    )
                }
            ) { innerPadding ->
                ... (#2-1 참조)
            }
        }
    }
}

Text 하나만 있는 간단한 TopAppBar.

 

#3-2 작동 확인

 

#3-3 세부 구현

@ExperimentalMaterial3Api
@Composable
public fun TopAppBar(
    title: @Composable () -> Unit, // 제목
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {}, // 제목 왼쪽 버튼
    actions: @Composable() (RowScope.() -> Unit) = {}, // 제목 오른쪽 버튼
    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, // 시스템 UI를 고려한 암시적 패딩 설정
    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), // TopAppBar 속 컴포넌트들의 색 정의
    scrollBehavior: TopAppBarScrollBehavior? = null // 스크롤 시 동작 정의
): Unit

(참조) TopAppBar에 대한 공식 문서.

 

title: @Composable () -> Unit
제목. 인수의 데이터형 @Composable () -> Unit라는 것에서 보듯 다양한 컴포저블들을 넣을 수 있다. 주로 Text 컴포저블이 들어간다.

navigationIcon: @Composable () -> Unit
TopAppBar의 좌측에 배치될 네비게이션 아이콘을 정의. 주로 햄버거 메뉴 아이콘이나 뒤로가기 버튼 등을 배치된다.

actions: @Composable() (RowScope.() -> Unit)
TopAppBar의 우측에 배치될 액션 버튼들을 정의.

windowInsets: WindowInsets
WindowInsets은 안드로이드 시스템 UI에 대한 정보다. 이를 TopAppBar에 제공해 TopAppBar가 이 올바른 영역에 겹쳐짐 없이 자리잡게 만든다. 기본값은 WindowInsets.systemBars로, 안드로이드 시스템의 상태바 및 Navigation Bar(뒤로가기 버튼, 홈 버튼, 앱 목록 보기 버튼이 있는 바)를 피해서 자리잡도록 만드는 옵션이다.

colors: TopAppBarColors

public constructor TopAppBarColors(
    val containerColor: Color, // 배경색 지정
    val scrolledContainerColor: Color, // 사용자가 화면을 스크롤할 때 TopAppBar의 색 지정
    val navigationIconContentColor: Color, // navigation 버튼들의 색 지정
    val titleContentColor: Color, // title 속 content들의 색 지정
    val actionIconContentColor: Color // action 버튼들의 색 지정
)

TopAppBar 속 다양한 구성 요소들의 색을 정의. scrolledContainerColor: Color는 사용자가 화면 영역(Scaffold의 본문 영역)을 스크롤할 때 TopAppBar가 어떤 색으로 변할지를 정의한다. 예를 들어, 원래는 투명했던 TopAppBar가 스크롤함에 따라 특정 색을 띄게하는 예시가 대표적이다. 기본값은 TopAppBarDefaults.topAppBarColors()다.

scrollBehavior: TopAppBarScrollBehavior?

사용자가 화면 영역(Scaffold의 본문 영역)을 스크롤할 때 TopAppBar가 어떤 동작을 수행할 지 정의한다. 예를 들어, 스크롤에 따라 TopAppBar가 숨겨지거나 다시 나타나는 애니메이션이 대표적이다. 기본값은 null(= 정의되지 않음)이다.

 

#4 bottomBar

#4-1 기초

...
import androidx.compose.material3.BottomAppBar

class MainActivity : ComponentActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                bottomBar = {
                    BottomAppBar(
                        content = { Text("BottomBar Text") }
                    )
                }
            ) { innerPadding ->
                ... (#2-1 참조)
            }
        }
    }
}

Text 하나만 있는 간단한 BottomAppBar. NavigationBar를 BottomAppBar 자리에 넣을 수도 있다 (#4-3 참조).

 

 

#4-2 작동 확인

 

#4-3 세부 구현

@OptIn(markerClass = {androidx. compose. material3.ExperimentalMaterial3Api::class})
@Composable
public fun BottomAppBar(
    modifier: Modifier = Modifier,
    containerColor: Color = BottomAppBarDefaults.containerColor, // 배경색 지정
    contentColor: Color = contentColorFor(containerColor), // BottomAppBar 속 컴포넌트들의 기본색 지정
    tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation, // 색을 통한 시각적 계층화의 정도값
    contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding, // 명시적 패딩 설정
    windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets, // 시스템 UI를 고려한 암시적 패딩 설정
    content: @Composable() (RowScope.() -> Unit)
): Unit

(참조) BottomAppBar에 대한 공식 문서.

 

containerColor: Color
BottomAppBar의 배경 색상 설정. 기본값은 BottomAppBarDefaults.containerColor이다.


contentColor: Color
BottomAppBar 속 content들의 기본 색상을 지정. 기본값은 contentColorFor(containerColor)다.

tonalElevation: Dp
UI 요소가 2개 이상 중첩되어있을 때 색(tone)을 강조함으로써 UI들을 시각적으로 계층화한다. Dp 단위로 값을 설정한다. 기본값은 BottomAppBarDefaults.ContainerElevation다.

contentPadding: PaddingValues
BottomAppBar 내부의 Padding(여백)을 설정. 기본값은 BottomAppBarDefaults.ContentPadding이다.

windowInsets: WindowInsets
WindowInsets은 안드로이드 시스템 UI에 대한 정보다. 이를 BottomAppBar에 제공해 BottomAppBar가 이 올바른 영역에 겹쳐짐 없이 자리잡게 만든다. 기본값은 WindowInsets.systemBars로, 안드로이드 시스템의 상태바 및 Navigation Bar(뒤로가기 버튼, 홈 버튼, 앱 목록 보기 버튼이 있는 바)를 피해서 자리잡도록 만드는 옵션이다.

 

@Composable
public fun NavigationBar(
    modifier: Modifier = Modifier,
    containerColor: Color = NavigationBarDefaults.containerColor, // 배경색 지정
    contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), // NavigationBar 속 컴포넌트들의 기본색 지정
    tonalElevation: Dp = NavigationBarDefaults.Elevation, // 색을 통한 시각적 계층화의 정도값
    windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, // 시스템 UI를 고려한 암시적 패딩 설정
    content: @Composable() (RowScope.() -> Unit)
): Unit

(참조) NavigationBar에 대한 공식 문서.

 

containerColor: Color

NavigationBar의 배경 색상 설정. 기본값은 NavigationBarDefaults.containerColor다.


contentColor: Color

NavigationBar 속 content들의 기본 색상을 지정. 기본값은 MaterialTheme.colorScheme.contentColorFor(containerColor)다.


tonalElevation: Dp

UI 요소가 2개 이상 중첩되어있을 때 색(tone)을 강조함으로써 UI들을 시각적으로 계층화한다. Dp 단위로 값을 설정한다. 기본값은 NavigationBarDefaults.Elevation다.


windowInsets: WindowInsets

WindowInsets은 안드로이드 시스템 UI에 대한 정보다. 이를 NavigationBar에 제공해 NavigationBar가 이 올바른 영역에 겹쳐짐 없이 자리잡게 만든다. 기본값은 WindowInsets.systemBars로, 안드로이드 시스템의 상태바 및 Navigation Bar(뒤로가기 버튼, 홈 버튼, 앱 목록 보기 버튼이 있는 바)를 피해서 자리잡도록 만드는 옵션이다. NavigationBarNavigation Bar는 서로 다른 종류의 객체다. 전자는 앱을 탐색하기 위한 것, 후자는 시스템 자체를 탐색하기 위한 것이다.

 

content: @Composable() (RowScope.() -> Unit)

NavigationBar는 반드시 3개 이상 5개 이하의 NavigationBarItem을 content로서 보유해야 한다는 제약 조건이 있다. NavigationBar 및 NavigationBarItem 구현은 이 게시글에서 다룬다.

 

#5 snackbarHost

#5-1 구현

...
import androidx.compose.material3.Button
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Snackbar를 위한 CoroutineScope와 State
            val scope = rememberCoroutineScope()
            val snackbarHostState = remember { SnackbarHostState() }

            Scaffold(
                modifier = Modifier.fillMaxSize(),
                snackbarHost = {
                    SnackbarHost(
                        hostState = snackbarHostState,
                        snackbar = { snackbarData ->
                            Snackbar (
                                modifier = Modifier.padding(12.dp),
                                containerColor = Color.Magenta,
                            ) {
                                Text(
                                    text = snackbarData.visuals.message,
                                    color = Color.White
                                )
                            }
                        }
                    )
                }
            ) { innerPadding ->
                Box(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxSize()
                        .background(Color.LightGray)
                ) {
                    Button(
                        modifier = Modifier.align(Alignment.Center),
                        onClick = {
                            scope.launch {
                                snackbarHostState.showSnackbar(
                                    message = "Button Clicked",
                                    duration = SnackbarDuration.Short
                                )
                            }
                        }
                    ) {
                        Text("Button", fontSize = 30.sp)
                    }
                }
            }
        }
    }
}

본 게시글에서, Scaffold의 content: @Composable (PaddingValues) -> Unit 부분이 #2-1와 다른 유일한 코드다. SnackbarHostState는 Snackbar의 On(나타남)/Off(사라짐)을 관리하는 객체다. Recomposition에 의해 On/Off 상태 정보가 초기화되어서는 안되기 때문에 remember { ... }로 감싼다.

 

또, SnackbarHostState.showSnackbar()를 위치시킬 장소인 rememberCoroutineScope도 선언한다. rememberCoroutineScope는 해당 코루틴 스코프가 선언된 컴포저블의 생명주기와 결합된 코루틴 스코프다. 해당 컴포저블이 아무리 Recomposition되어도 영향을 받지 않는 코루틴 영역을 전개한다. rememberCoroutineScope는 해당 스코프와 생명주기가 겹합된 컴포저블이 화면에서 완전히 사라져야만 같이 사라진다. 즉 일반적인 CoroutineScope와 비교하면, 코루틴의 생명주기를 별도로 관리할 필요가 없다는 이점을 가진다.

 

rememberCoroutineScope는 UI 생성의 비동기적 처리를 목적으로 설계되었다. 그래서 rememberCoroutineScope의 기본 스레드는 메인 스레드다. 메인 스레드는 안드로이드에서 UI를 그릴 수 있는 유일한 스레드로, 주어진 할 일이 많기 때문에 동기적인 코드를 추가하면 안 그래도 바쁜 메인 스레드에 부담을 주게 된다. 이 때 rememberCoroutineScope를 사용하면 메인 스레드의 막힘(Blocking)없는 동작을 보장할 수 있다. 그렇기에 UI에 스낵바를 표시하는 코드인 SnackbarHostState.showSnackbar()를 코루틴 영역에 넣음으로써, 비동기적으로 실행하는 것이다 (같은 스레드 내에서 서로 다른 2개 이상의 작업을 비동기적으로 수행한다는 말은 코루틴에서 모순이 아니다 (이 게시글의 #4 참조)).

 

#5-2 작동 기제

#5-1 속 Snackbar 코드의 작동 기제다.

 

#5-3 작동 확인

버튼을 누르면 Snackbar가 뜬다.

 

#5-4 세부 구현

@Composable
public fun SnackbarHost(
    hostState: SnackbarHostState, // Snackbar의 On/Off 관리자
    modifier: Modifier = Modifier,
    snackbar: @Composable (SnackbarData) -> Unit = { Snackbar(it) } // Snackbar의 모양 설정
): Unit

(참조) SnackbarHost에 대한 공식 문서.

 

hostState: SnackbarHostState

스낵바의 상태(State)를 관리. 즉, 스낵바가 '숨겨져 있는 상태'인지, '표시되고 있는 상태'인지를 관리. SnackbarHostState.showSnackbar()를 하면 스낵바가 표시되고, SnackbarHostState.currentSnackbarData?.dismiss()로 표시 중인 스낵바를 숨길 수 있다. 하지만, 스낵바를 명시적으로 숨기는 코드는 거의 쓰이지 않는다고 한다. 스낵바는 표시되고 일정 시간이 지나면 (토스트 메시지처럼) 알아서 숨겨지기 때문이다.

 

snackbar: @Composable (SnackbarData) -> Unit

@Composable
public fun Snackbar(
    modifier: Modifier = Modifier,
    action: @Composable() (() -> Unit)? = null, // Snackbar에 포함될 작업 버튼 지정 (null이면 표시 안 됨)
    dismissAction: @Composable() (() -> Unit)? = null, // Snackbar를 닫는 버튼 지정 (null이면 표시 안 됨)
    actionOnNewLine: Boolean = false, // action(작업 버튼)을 새로운 줄에 표시할 지 여부 설정
    shape: Shape = SnackbarDefaults.shape, // 모양 지정
    containerColor: Color = SnackbarDefaults.color, // 배경색 지정
    contentColor: Color = SnackbarDefaults.contentColor, // Snackbar 속 컴포넌트들의 기본색 지정
    actionContentColor: Color = SnackbarDefaults.actionContentColor, // action(작업 버튼)의 기본색 지정
    dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor, // dismissAction(닫기 버튼)의 기본색 지정
    content: @Composable () -> Unit
): Unit

Snackbar의 모양을 정의한다.

 

SnackbarHostState.showSnackbar()

public final suspend fun showSnackbar(
    message: String, // Snackbar에 표시할 문자열
    actionLabel: String? = null, // Snackbar에 포함될 텍스트 모양 작업 버튼 지정 (null이면 표시 안 됨)
    withDismissAction: Boolean = false, // Snackbar를 닫는 버튼 유무 설정
    duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration. Short else SnackbarDuration. Indefinite // Snackbar 표시 지속 시간 설정
): SnackbarResult

hostState가 사용할 확장 함수다.

 

#6 floatingActionButton

#6-1 기초

...
import android.widget.Toast
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = {
                            Toast.makeText(this@MainActivity, "FAB Clicked", Toast.LENGTH_SHORT).show()
                        }
                    ) {
                        Text(
                            "F A B"
                        )
                    }
                },
                floatingActionButtonPosition = FabPosition.End
            ) { innerPadding ->
                ... (#2-1 참조)
            }
        }
    }
}

클릭 시 토스트 메시지를 띄우는 플로팅 액션 버튼.

 

#6-2 작동 확인

플로팅 액션 버튼을 누르면 토스트 메시지가 뜬다.

 

#6-3 세부 구현

@Composable
public fun FloatingActionButton(
    onClick: () -> Unit, // 클릭 시 동작 정의
    modifier: Modifier = Modifier,
    shape: Shape = FloatingActionButtonDefaults.shape, // 모양 지정
    containerColor: Color = FloatingActionButtonDefaults.containerColor, // 배경색 지정
    contentColor: Color = contentColorFor(containerColor), // FloatingActionButton 속 컴포넌트들의 기본색 지정
    elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), // 그림자 효과를 통한 시각적 계층화의 정도값
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, // 버튼 동작 커스텀(Custom)
    content: @Composable () -> Unit
): Unit

(참조) FloatingActionButton에 대한 공식 문서.

 

shape: Shape

FloatingActionButton의 모양을 정의. 기본값은 FloatingActionButtonDefaults.shape다.


containerColor: Color

FloatingActionButton의 배경 색상 설정. 기본값은 FloatingActionButtonDefaults.containerColor이다.


contentColor: Color

FloatingActionButton 속 content들의 기본 색상을 지정. 기본값은 contentColorFor(containerColor)다.


elevation: FloatingActionButtonElevation

그림자 깊이를 나타내는 인수로, 음영 효과(= 공중에 떠 있는 듯한 효과)를 통해 입체감을 준다. Dp 단위로 값을 설정한다.


interactionSource: MutableInteractionSource

버튼을 누를 때 발생하는 애니메이션 효과나 색상 변화 등을 제어하는 인수. FloatingActionButton이 사용자와 상호작용하고 반응하는 방식을 커스텀할 때 사용한다.

 

#7 기타

#7-1 기초

...
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                containerColor = MaterialTheme.colorScheme.background, // Scaffold의 배경 색상
                contentColor = contentColorFor(MaterialTheme.colorScheme.background), // 내부 content들의 기본 색
                contentWindowInsets = WindowInsets.systemBars // 상태바 위치를 고려한 Scaffold의 자리
            ) { innerPadding ->
                ... (#2-1 참조)
            }
        }
    }
}

containerColor: Color

Scaffold의 배경 색상 설정. 기본값은 MaterialTheme.colorScheme.background이다.


contentColor: Color

Scaffold 속 content들의 기본 색상을 지정. 기본값은 contentColorFor(MaterialTheme.colorScheme.background)다.


contentWindowInsets: WindowInsets

WindowInsets은 안드로이드 시스템 UI에 대한 정보다. 이를 Scaffold에 제공해 Scaffold가 이 올바른 영역에 겹쳐짐 없이 자리잡게 만든다. 기본값은 WindowInsets.systemBars로, 안드로이드 시스템의 상태바 및 Navigation Bar(뒤로가기 버튼, 홈 버튼, 앱 목록 보기 버튼이 있는 바)를 피해서 자리잡도록 만드는 옵션이다. Scaffold 속의 모든 content들에 전역적이고 일괄적으로 적용된다. 여기서 말하는 content는 topBar, bottomBar을 말하는 게 아니라, Scaffold의 마지막 매개변수인 content: @Composable (PaddingValues) -> Unit를 의미한다 (#2-3 참조).

 

#8 합산

#8-1 코드

// package com.example.scaffold

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.BottomAppBar
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.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {

    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Snackbar를 위한 CoroutineScope와 State
            val scope = rememberCoroutineScope()
            val snackbarHostState = remember { SnackbarHostState() }

            Scaffold(
                modifier = Modifier.fillMaxSize(),
                // #3
                topBar = {
                    TopAppBar(
                        title = { Text("TopBar Text") }
                    )
                },
                // #4
                bottomBar = {
                    BottomAppBar(
                        content = { Text("BottomBar Text") }
                    )
                },
                // #5
                snackbarHost = {
                    SnackbarHost(
                        hostState = snackbarHostState,
                        snackbar = { snackbarData ->
                            Snackbar(
                                modifier = Modifier.padding(12.dp),
                                containerColor = Color.Magenta,
                            ) {
                                Text(
                                    text = snackbarData.visuals.message,
                                    color = Color.White
                                )
                            }
                        }
                    )
                },
                // #6
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = {
                            scope.launch {
                                snackbarHostState.showSnackbar(
                                    // snackbarData 보내기
                                    message = "FAB Clicked",
                                    duration = SnackbarDuration.Short
                                )
                            }
                        }
                    ) {
                        Text(
                            "F A B"
                        )
                    }
                },
                floatingActionButtonPosition = FabPosition.End,
                // #7
                containerColor = MaterialTheme.colorScheme.background, // Scaffold의 배경 색상
                contentColor = contentColorFor(MaterialTheme.colorScheme.background), // 내부 content들의 기본 색
                contentWindowInsets = WindowInsets.systemBars // 상태바 위치를 고려한 Scaffold의 자리
            ) { innerPadding -> // Scaffold가 계산한, content가 Scaffold의 다른 UI 요소들과 겹치지 않을 정도의 Padding값
                Box(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxSize()
                        .background(Color.LightGray)
                ) {
                    Text(
                        text = "Content Text",
                        modifier = Modifier.align(Alignment.Center),
                        fontSize = 30.sp
                    )
                }
            }
        }
    }
}

#3 ~ #7의 모든 코드를 합쳤다. 합친 김에 약간의 변경점을 넣었는데, 플로팅 액션 버튼을 클릭하면 Snackbar가 뜨게 수정했다. #7 부분은 생략해도 상관없다. 생략했다면 암시적으로 적용되었을 기본값을 그대로 썼기 때문이다.

 

#8-2 작동 확인

플로팅 액션 버튼을 누르면 Snackbar가 뜬다.

 

#9 요약

최근 안드로이드 앱 UI의 정형화된 모습은 Scaffold의 영향도 일부 있을 것 같다는 생각이 든다. 그만큼 쓰기 편하다.

 

#10 완성된 앱

 

android-practice/jetpack-compose/Scaffold at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com