Jetpack Compose Custom CameraView 촬영-프리뷰 구현하기

현재 서비스중인 앱의 카메라 기능은 전부 XML 기반의 커스텀뷰를 만들어서 사용중이다.

Compose를 공부하는 기회에 해당 부분을 Compose로 구현해 봤다.


정말 간단한 기능의 카메라 촬영, 프리뷰 기능만을 구현한 코드를 기록해 두려한다.

코드의 가독성이나 이슈가될 만한 부분들은 상황에 맞게 조절하여 사용하시면 됩니다.


@Composable
fun CameraComponent(
    modifier: Modifier = Modifier,
    selfMode: Boolean,
    takeAction: Boolean,
    receiveImageUrl: (Bitmap?) -> Unit
) {
    val cameraSelector = if (selfMode) {
        CameraSelector.DEFAULT_FRONT_CAMERA
    } else {
        CameraSelector.DEFAULT_BACK_CAMERA
    }
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val imageCapture = remember { ImageCapture.Builder().build() }
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
    val preview: androidx.camera.core.Preview = androidx.camera.core.Preview.Builder().build()

    val cameraViewModel: CameraComponentModel = hiltViewModel()
    val imageBitmap by cameraViewModel.imageBitmap.collectAsState()
    var processing: Boolean by remember { mutableStateOf(false) }

    // 카메라 바인딩
    // cameraProvider를 통해 ProcessCameraProvider를 비동기적으로 가져와 카메라의 생명주기를 현재 카메라 컴포넌트 생명주기와 바인딩
    // preview, imageCapture를 lifecycleOwner에 바인딩하여 카메라 미리보기와 이미지 캡쳐를 구현
    // 카메라 세션 준비가 완료되면 프리뷰를 화면에 표시
    LaunchedEffect(cameraProviderFuture, cameraSelector) {
        cameraProviderFuture.addListener({
            try {
                cameraProvider.unbindAll()

                cameraProvider.bindToLifecycle(
                    lifecycleOwner,
                    cameraSelector,
                    preview,
                    imageCapture
                )
            } catch (exc: Exception) {
                Timber.e("Camera binding failed")
            }
        }, ContextCompat.getMainExecutor(context))
    }

    // takeAction 상태가 true로 변경될 때, imageCapture의 takePicture 메서드를 호출하여 이미지 캡쳐
    // 촬영 성공 시, onCaptureSuccess 콜백에서 이미지를 비트맵으로 변환하고, 사용자 지정 작업 수행
    // 실패 시, onError 콜백에서 실패 이유를 로그로 출력하고, 사용자 지정 작업 수행
    LaunchedEffect(takeAction, imageBitmap) {
        if (takeAction && !processing) {
            imageCapture.takePicture(
                ContextCompat.getMainExecutor(context),
                object : ImageCapture.OnImageCapturedCallback() {
                    override fun onCaptureSuccess(image: ImageProxy) {
                        processing = true
                        cameraViewModel.processImageCapture(image, selfMode)
                    }

                    override fun onError(exception: ImageCaptureException) {
                        Timber.e("Image capture failed ${exception.message}")
                        receiveImageUrl(null)
                    }
                }
            )
        }
        if (imageBitmap != null) {
            receiveImageUrl(imageBitmap)
            cameraViewModel.clearImageBitmap()
            processing = false
        }
    }

    AndroidView(
        modifier = modifier.clipToBounds(),
        factory = { ctx ->
            PreviewView(ctx).apply {
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }
        },
        update = { previewView ->
            preview.setSurfaceProvider(previewView.surfaceProvider)
        }
    )
}


카메라 비율을 일정하게 고정시키고 싶으면 clipToBounds() 부분을 지우면 된다.

Image를 프레임에 맞춰 자르기 위해 modifier.clipToBounds() 를 통해 조절해 봤다.

Jetpack Compose Text 2편

저번 Text 설명 1편에 이어서 2편을 작성해보도록 하겠습니다.

Text는 크게 색다로운 내용이 있다긴 보다 되짚어보는 정리정도 되는거 같습니다. 

2편 같은 경우 사용자 상호작용 개념이 있어서 1편보다는 좀더 좋은 기능을 설명하고 있습니다.


1. 사용자 상호작용

 사용자 상호작용을 사용 설정할 수 있는 다양한 API를 설명합니다.

 - 여러 Composable 레이아웃에서 더 유연하게 텍스트를 선택할 수 있음

 - Text Composable 의 부분에 제어자를 추가할 수 없어서 Text의 사용자 상호작용은 다른 Composable 레이아웃과 다름


1) 텍스트 선택하기

기본적으로 Composable은 선택할 수 없습니다.


텍스트 선택 기능을 사용 설정하려면 Text를 SelectionContainer Composable로 래핑해야 합니다.

@Composable
fun SelectableText() {
    SelectionContainer {
        Text("This text is selectable")
    }
}
특정 부분만 선택 표시를 중지 할 수도 있습니다. 

선택 불가능한 부분을 DisableSelection Composable로 래핑해서 사용할 수 있습니다.

@Composable
fun PartiallySelectableText() {
    SelectionContainer {
        Column {
            Text("This text is selectable")
            Text("This one too")
            Text("This one as well")
            
            DisableSelection {
                Text("But not this one")
                Text("Neither this one")
            }
            
            Text("But again, you can select this one")
            Text("And this one too")
        }
    }
}


2) 텍스트에서 클릭 위치 가져오기

Text의 클릭을 수신 대기하려면 clickable  추가하면 됩니다.

Text Composable 내에서 클릭 위치를 가져오려면  ClickableText를 Text Compsable 대신 사용합니다.

@Composable
fun SimpleClickableText() {
    ClickableText(
        text = AnnotatedString("Click Me"),
        onClick = { offset ->
            Log.d("ClickableText", "$offset -th character is clicked.")
        }
    )
}


3) 주석(tag)이 추가된 클릭

사용자가 Text Composable을 클릭할 때 특정 단어에 연결된 하이퍼링크 같이 Text 값에 정보를 추가할 수 있습니다.

- tag(String), annotation(String), 텍스트 범위를 매개변수로 사용하는 주석을 추가해야 함

- AnnotatedString에서 이러한 주석을 태그 또는 텍스트 범위로 필터링

- buildAnnotatedString은 AnnotatedString 객체를 만드는 inline function이다.

 buildAnnotatedString을 이용해 만들어진 AnnotatedString은 하나의 Text 안에서 여러 스타일을 적용할 수 있도록 하는 정보를 담은 객체로 AnnotatedString 속에는 텍스트의 스타일에 대한 정보를 담은 text, spanStyles, paragraphStyles, annotations 객체들이 저장되어 있다. 

@Composable
fun AnnotatedClickableText() {
    val uriHandler = LocalUriHandler.current

    val annotatedText = buildAnnotatedString {
        append("Click ")

        // We attach this *URL* annotation to the following content
        // until `pop()` is called
        pushStringAnnotation(tag = "URL", annotation = "https://developer.android.com")
        withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
            append("here")
        }

        pop()
    }

    ClickableText(
        text = annotatedText,
        onClick = { offset ->
            // We check if there is an *URL* annotation attached to the text
            // at the clicked position
            annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
                .firstOrNull()?.let { annotation ->
                    Log.d("Clicked URL", annotation.item)
                    uriHandler.openUri(annotation.item)
                }
        }
    )
}



2. 텍스트 입력 및 수정

- TextField를 사용하면 유저가 텍스트를 입력하거나 수정할 수 있습니다.

- 디자인이 Material TextField 또는 OutlineTextField를 호출하는 경우 TextField를 사용하는 할것을 권장

- 머티리얼 사양의 장식이 필요하지 않은 디자인을 빌드하는 경우 BasicTextField를 사용해야함


(1) TextField는 머티리얼 디자인 가이드라인을 따라 구현됨

   - 기본 스타일이 채워짐

   - OutlinedTextField는 위 스타일 버전입니다.


(2) BasicTextField는 유저가 키보드를 통해 텍스트를 수정할 수 있지만 힌트나 자리표시자 등등은 제공하지 않음


예제1. TextField

@Composable
fun SimpleFilledTextFieldSample() {
    var text by remember { mutableStateOf("Hello") }

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}


예제2. outlinedTextField

@Composable
fun SimpleOutlinedTextFieldSample() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}

1) TextField 스타일 지정

TextField 및 BasicTextField는 커스텀 할 수 있는 많은 공통 매개변수를 공유합니다. 


TextField의 전체 목록은 아래링크에서 확인할 수 있습니다.

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt;l=149?hl=ko



다음은 자주 사용하는 매개변수 목록입니다.

- singleLine

- maxLines

- textStyle

@Composable
fun StyledTextField() {
    var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }

    TextField(
        value = value,
        onValueChange = { value = it },
        label = { Text("Enter text") },
        maxLines = 2,
        textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
        modifier = Modifier.padding(20.dp)
    )
}


2) 키보드 옵션

TextField를 사용하면 키보드 레이아웃과 같은 키보드 구성 옵션을 설정하거나 키보드에서 지원하는 경우


이래 지원되는 키보드 옵션들 입니다.

capitalization

autoCorrect

keyboardType

imeAction


위 자세한 설명은 이 링크를 참조 

https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/KeyboardOptions?hl=ko#KeyboardOptions(androidx.compose.ui.text.input.KeyboardCapitalization,kotlin.Boolean,androidx.compose.ui.text.input.KeyboardType,androidx.compose.ui.text.input.ImeAction) 



3) 형식 지정

- TextField를 사용하면 입력 값에 형식을 설정할 수 있습니다. 

- 비밀번호를 나타내는 문자를 *로 바꾸거나 신용카드 번호의 4자리마다 하이픈을 삽입할 수 있습니다.

@Composable
fun PasswordTextField() {
    var password by rememberSaveable { mutableStateOf("") }

    TextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Enter password") },
        visualTransformation = PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
    )
}


4) Focus 처리

많은 사용자들이 앱에서 텍스트를 타이핑을 한 후 키보드가 아닌 다른 곳을 터치하여 키보드를 닫는 UX에 익숙할 것입니다. 

이전까지는 이 처리를 위해 Activity에서 currentFocus를 이용해 Focus를 해제하는 방식을 사용했습니다.

//Activity에서 currentFocus를 이용해 Focus를 해제하는 방식
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    if (currentFocus is EditText) {
        SoftInputUtils.hideSoftInput(currentFocus)
        currentFocus!!.clearFocus()
    }
    return takeDispatchTouchEvent(ev)
}


그렇지만 TextField는 Composable 함수이기 때문에 위와 같이 특정 객체인지 판단할 수 없었습니다. 

그래서 TextField의 포커싱 여부를 판단하는 변수를 하나 두어 처리해야 했습니다.

어렵지 않은 작업이었지만 이 처리를 하면서 명령형 UI와 선언형 UI의 차이가 조금씩 체감이 되었습니다.

var isTextFieldFocused = false

@Composable
fun MainContent(vm: MainViewModel) {
    val focusRequester by remember { mutableStateOf(FocusRequester()) }

    BasicTextField(
        value = ...,
        modifier = Modifier
            .focusRequester(focusRequester = focusRequester)
            .onFocusChanged {
                isTextFieldFocused = it.isFocused
            }
    )
}

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    if (isTextFieldFocused) {
        SoftInputUtils.hideSoftInput(currentFocus)
        currentFocus!!.clearFocus()
    }
    return takeDispatchTouchEvent(ev)
}


위의 방식은 단점이 있다. TextField 가 늘어날 때마다 변수를 새로 생성해 주어야 한다.

다음의 방식으로 해결이 가능하다.

TextField가 아닌, 그것을 감싸고 있는 전체 화면의 터치 이벤트를 캐치해서 포커스를 해제하는 방법이 있다.

detectTapGestures로 태핑을 감지하여 포커스를 해제하면 된다.

Composable을 깔끔하게 쓰기 위해 Modifier의 Extension에 함수 추가

// 터치 이벤트 감지하여 포커스 해제
fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier {
    return this.pointerInput(Unit) {
        detectTapGestures(onTap = {
            doOnClear()
            focusManager.clearFocus()
        })
    }
}


그리고 TextField를 Column으로 감싸주고, Column을 터치할 때 위에 만들어준 함수로 포커스를 해제하도록 만들었다.

@Composable
fun MainContent() {
    val focusRequester by remember { mutableStateOf(FocusRequester()) }
    val focusManager = LocalFocusManager.current
    var text by remember { mutableStateOf("안뇽") }

    Column(modifier = Modifier
        .fillMaxSize()
        .addFocusCleaner(focusManager) //최상단 전체화면의 터치이벤트 감지하여 해제
    ) {
        TextField(
            value = text,
            onValueChange = {
                text = it
            },
            modifier = Modifier
                .focusRequester(focusRequester = focusRequester)
        )
    }
}




아래 링크에서 더 다양한 예제들을 볼 수 있습니다. 

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/VisualTransformationSamples.kt?hl=ko



**정리

- Compose 의 클릭영역은 SelectionContainer Composable로 TextComposable를 감싸서 만들면 사용할 수 있게 됨 

- SelectionContainer 안에서 클릭영역 허용하지 않을 공간을 DisableSelection Composable로 설정할 수 있음

- Text대신 CilckableText Composable 를 사용하면 유저가 클릭한 좌표를 가져올 수 있음 

- 추가된 클릭 표시를 하려면 AnnotatedString를 사용해서 tag , anotation 매개변수를 설정하면 됨 

- TextField 사용하면 유저가 글을 수정할 수 있게끔 됨 (xml의 editText 대신) 

- TextField는 기본 제공되는 스타일이 존재 , BaseTextField는 힌트나 자리표시자 등등을 제공하지 않는 기본 형식

- BaseTextField는 머트리얼 디자인 가이드를 따르지 않고 쓸때 사용하기를 권장

- TextField에서 사용자가 글을 입력시 표시되면 글 형식을 지정할 수 있음 (ex: 패스워드면 **)

- TextField에서 키보드 옵션 설정 가능 


참고

https://developer.android.com/develop/ui/compose/text/user-interactions?hl=ko


Jetpack Compose Text 1편

 이번에는 Compose의 텍스트에 관해 정리를 해봤습니다.

- Compose는 텍스트를 표시하고 사용자 입력을 처리하는 기본 구조인 BasicText 및 BasicTextField를 제공함

- 상위 수준에서 Compose는 Text 및 TextField를 제공하며 머티리얼 디자인를 따름

(위 Composable은 디자인과 분위기가 Android 사용자에게 적합하며 쉽게 커스텀 할 수 있는 옵션이 포함되어 있으므로 사용을 권장함)


1. 기본 Text 표시

텍스트를 표시하는 가장 기본적인 방법은 Text Composable을 사용하는 것입니다.

@Composable
fun SimpleText() {
    Text("Hello World")
    
    //문자열 리소스를 사용하는 예제
    Text(stringResource(R.string.hello_world))
}


2. Text Style 지정

Text Composable에는 콘텐츠의 스타일을 지정할 수 있는 여러 매개변수가 있습니다.

이 매개변수 중 하나를 설정할 때마다 전체 텍스트 값에 스타일이 적용됩니다. 

- color, fontSize, fontStyle, fontWeight, textAlign


언어 텍스트 방향에 따라 Text 이동하는 TextAlign.Start와 TextAlign.End를 사용하는 것을 권장함

(TextAlign.Left, TextAlign.Right 은 가급적 제한적 사용)

@Composable
fun SimpleText() {
    
    //텍스트 색상 변경
    Text("Hello World", color = Color.Blue)
    
    //텍스트 크기 변경
    Text("Hello World", fontSize = 30.sp)
    
    //텍스트를 기울임꼴로 설정
    Text("Hello World", fontStyle = FontStyle.Italic)
    
    //텍스트를 굵게 표시
    Text("Hello World", fontWeight = FontWeight.Bold)
    
    //텍스트 정렬
    Text("Hello World", textAlign = TextAlign.Center, modifier = Modifier.width(150.dp))
}


3. Text font

- Text의 fontFamily 매개변수를 사용하면 Composable에 사용되는 글꼴을 관리할 수 있음

- 기본적으로 Serif, Sans-serif, 고정폭 및 필기체 글꼴 모음이 포함

- fontFamily 속성을 사용하여 res/fonts 폴더에 정의된 맞춤 font로 작업할 수 있음



@Composable
fun SimpleText() {
    
    Text("Hello World", fontFamily = FontFamily.Serif)
    
    Text("Hello World", fontFamily = FontFamily.SansSerif)
}



- fontFamily를 Text 컴포저블에 전달할 수 있습니다.

- fontFamily에 수동으로 fontWeight를 설정하여 텍스트에 적합한 가중치를 선택할 수 있습니다.

val firaSansFamily = FontFamily(
    Font(R.font.firasans_light, FontWeight.Light),
    Font(R.font.firasans_regular, FontWeight.Normal),
    Font(R.font.firasans_italic, FontWeight.Normal, FontStyle.Italic),
    Font(R.font.firasans_medium, FontWeight.Medium),
    Font(R.font.firasans_bold, FontWeight.Bold)
)

Column {
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Italic)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Medium)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Bold)
}



4. Text Composable 내의 여러 스타일 설정 

동일한 Text 컴포저블 내에서 여러 스타일을 설정하려면 AnnotatedString을 사용해야 합니다.

AnnotatedString은 다음 항목이 포함된 데이터 클래스입니다.


- Text 값

- SpanStyleRange의 List: 텍스트 값 내 위치 범위가 포함되는 인라인 스타일 지정과 동일함

- ParagraphStyleRange의 List: 텍스트 정렬, 텍스트 방향, 행 간격, 텍스트 들여쓰기 스타일 지정


TextStyle은 Text 컴포저블에서 사용되며

SpanStyle 및 ParagraphStyle은 AnnotatedString에서 사용됩니다.


SpanStyle과 ParagraphStyle의 차이점은

ParagraphStyle은 전체 단락에 적용할 수 있고,  SpanStyle은 문자 수준에서 적용할 수 있다는 것입니다.

@Composable
fun MultipleStylesInText() {
    Text(buildAnnotatedString {
        withStyle(style = SpanStyle(color = Color.Blue)) {
            append("H")
        }
        append("ello ")

        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) {
            append("W")
        }
        append("orld")
    })
}



@Composable
fun ParagraphStyle() {
    Text(buildAnnotatedString {
        withStyle(style = ParagraphStyle(lineHeight = 30.sp)) {
        withStyle(style = SpanStyle(color = Color.Blue)) {
            append("Hello\n")
        }
        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold,
            color = Color.Red)) {
            append("World\n")
        }
        append("Compose")
        }
    })
}




5. Text 최대 줄 수

- Text Composable에 표시되는 줄 수를 제한하려면 maxLines 매개변수를 설정

@Composable
fun LongText() {
    Text("hello ".repeat(50), maxLines = 2)
}


6. Text 오버플로

- 긴 텍스트를 제한할 때 표시된 텍스트가 잘린 경우에만 표시되는 텍스트 오버플로를 표시할 수 있음

- textOverflow 매개변수를 설정하면됨

@Composable
fun OverflowedText() {
    Text("Hello Compose ".repeat(50), maxLines = 2, overflow = TextOverflow.Ellipsis)
}


**정리

- Compose 의 Text Composable은 기본적으로 머티리얼 디자인 가이드에 따라 구현이 됨

- Text Composable의 매개변수로 스타일을 지정할 수 있음 크기, 색깔, 기울기, 정렬, 굵기,최대 줄수, 오버플로우 등등

- RTL, LTR 설정 컨텍스트에 따라 정렬이 자연적으로 달라지게 되어있음 (Left,Right 보다 Start,End 사용을 권장)

- font도 fontFamily parameter로 쉽게 설정가능, fontWeight parameter로 가중치 수동으로 설정가능

- Text Composable 내의 글자마다 설정을 다르게 하고 싶으면 AnnotatedString를 사용

=> ParagraphStyle은 전체단락 , SpanStyle은 문자 수준 적용


Jetpack Navigation + BottomNavigationBar 구현하기

1. Jetpack Navigation


사실 Jetpack Navigation에 대해서는 이전에도 한번 포스트를 쓴 적이 있다.

Navigation은 크게 3가지 요소로 이루어져 있다.


NavController

- 대상(즉, 앱의 화면) 간 이동을 담당하는데, 탐색 동작을 처리하는 역할

- 대상을 탐색하고 딥 링크를 처리하며 대상 백 스택을 관리하는 등의 여러 메소드를 제공


NavGraph

- 이동할 컴포저블 대상의 매핑을 담당

- 앱 내의 화면 연결을 보여주는 일종의 지도 역할을 한다.


NavHost

- 현재 탐색 대상(navigation destination)이 포함된 UI 요소

- NavGraph의 현재 대상을 표시하는 컨테이너 역할을 하는 컴포저블


2. BottomNavigationBar 구현하기

이제 위에서 설명한 Navigation과 Compose를 활용해 BottomNavigationBar를 구현해보자.

gradle에 라이브러리 추가

dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.7")
}


Bottom Navigation으로 이동할 화면 생성

필자는 여기서 Home, Rating, Profile 3개의 화면을 만들어주었다.


@Composable
fun HomeScreen() {
    Text(text = "Home")
}

@Composable
fun RatingScreen() {
    Text(text = "Rating")
}

@Composable
fun ProfileScreen() {
    Text(text = "Profile")
}


BottomNavItem 생성.

Bottom Navigation으로 이동할 item 객체 클래스 BottomNavItem 클래스를 생성하고,

그 자식으로 Home, Rating, Profile 을 추가한다.

sealed class BottomNavItem(
    @StringRes val title: Int,
    @DrawableRes val icon: Int,
    val screenRoute: String
) {
    object Home : BottomNavItem(R.string.home, R.drawable.ic_home, NavScreens.Home.name)
    object Rating : BottomNavItem(R.string.rating, R.drawable.ic_rating, NavScreens.Rating.name)
    object Profile : BottomNavItem(R.string.profile, R.drawable.ic_profile, NavScreens.Profile.name)
}


NavHost, NavGraph 생성.

NavHost 함수의 NavGraphBuilder 를 통해서 NavGraph를 생성할 수 있다.

@Composable
private fun MyNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        ...

        composable(route = NavScreens.Home.name) {
            HomeScreen(
                onSearchClicked = { navController.navigate(NavScreens.Search.name) }
            )
        }

        composable(route = NavScreens.Rating.name) {
            RatingScreen()
        }

        composable(route = NavScreens.Profile.name) {
            ProfileScreen()
        }

        composable(route = NavScreens.Search.name) {
            SearchScreen()
        }
    }
}


Bottom Navigation 생성
@Composable
private fun MyBottomNavigation(
    modifier: Modifier = Modifier,
    navController: NavHostController
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Rating,
        BottomNavItem.Profile
    )

    NavigationBar {
        items.forEach { item ->
            NavigationBarItem(
                selected = currentRoute == item.screenRoute,
                label = {
                    Text(
                        text = stringResource(id = item.title),
                        style = TextStyle(
                            fontSize = 12.sp
                        )
                    )
                },
                icon = {
                    Icon(
                        painter = painterResource(id = item.icon),
                        contentDescription = stringResource(id = item.title)
                    )
                },
                onClick = {
                    navController.navigate(item.screenRoute) {
                        navController.graph.startDestinationRoute?.let {
                            popUpTo(it) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },
            )
        }
    }

}


BottomNavigationBar를 그리는 앱 화면 구성

@Composable
fun MyApp() {
    val navController = rememberNavController()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            ...
        },
        bottomBar = {
            MyBottomNavigation(navController = navController)
        }
    ) {
        Box(modifier = Modifier.padding(it)) {
            MyNavHost(
                navController = navController,
                startDestination = NavScreens.Home.name
            )
        }
    }
}



Jetpack Compose ConstraintLayout

Compose에 대한 내용을 언급하던 중에 ConstraintLayout 이란 항목이 갑자기 튀어나옵니다. 

한껏 Column, Row, Box란 view group 역할을 하는 composable을 한껏 제공해 주고 나서 말이죠. 

게다가 ConstraintLayout은 기존 View group 방식에서 view group의 중첩, 다시 말해 layout의 depth가 깊어지는 걸 막기 위해 2017년에 새롭게 등장한 layout입니다.

CustomLayout에서 이미 봤듯이 Compose에서는 measure가 한 번만 가능하기 때문에 View의 depth가 깊어지더라도 문제가 발생하지 않습니다. 

다만 Compose에서의 ConstraintLayout은 뷰의 구조가 복잡할 때, 여러 view들 간의 상호관계에 따른 배치를 좀 더 쉽게 가능하도록 하는 목적으로 사용됩니다.

추후 설명할 Compose의 constraint의 사용법은 기존 view system에서 사용하던 ConstraintLayout과 거의 동일합니다. (코드로 표현하는 방법이 다를 뿐) 다만 목적은 "기존의 view system에서 사용된 것과는 다르다" 정도만 이해하고 있으면 이번 포스트팅은 어렵지 않게 클리어 가능할 것으로 생각됩니다.

이 글은 Android developer 공식 사이트에서 제공하는 문서를 기반으로 의역, 번역하였습니다.



Gradle dependency

Constraint layout을 compose에서 사용하기 위해서는  따로 dependency가 필요합니다. 여기서 주의해야 할 점은 기본 compose의 버전을 따라가지 않는다는 겁니다.

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"


현재 최신 compose 버전은 1.6.7이며, 이 constraintlayout (1.0.1)을 쓰려면 kotlin은 2.0.0을 써야 합니다

정리하면 기본 compose 버전과는 다르게 진행된다는 것과 constraintlayout을 사용하기 위해서 kotlin 버전도 호환성 있는 버전으로 맞춰서 사용해야 한다는 점을 유념해 두어야 합니다.


ConstraintLayout의 기본적인 사용

Constraint layout에서 view들 간의 관계는 ID로 표현합니다. 

Compose에서는 ID를 createRef(), createRefs()로 생성합니다.

- createRefs() 또는 createRef()를 사용하여, ConstraintLayout 내에서 각 컴포저블에 대한 참조를 선언

- 이 과정은 xml에서 ConstraintLayout내에서의 참조를 위해 각 레이아웃에 id를 부여하는 작업과 유사


생성된 ID를 각 Compose에 부여하기 위 해어 constrainAs() modifier를 제공하며, lambda block안에서 다른 compose와의 관계를 정의합니다.

lambda block에서 관계를 정의할 때는 linkTo()를 이용합니다.

Compose에서도 parent를 사용할 수 있으며 ConstraintLayout을 나타냅니다.


이제 버튼을 하나 만들고, 그 아래 text를 배치하는 간단한 형태를 구현해 보겠습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the two composables
        // in the ConstraintLayout's body
        val (button1, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button 1")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
        })

    }
}


먼저 각 compose에서 사용할 reference값 (기존 view system에서는 ID)를 생성합니다.

constrainAs()로 reference를 부여한 후에 해당 block안에서 linkTo로 관계를 정의합니다. 

이미 기존 view system의 constraint layout에 익숙한 개발자라면 표현방법이 다를 뿐 동일한 형태로 사용한다는 걸 알 수 있습니다. 따라서 이해하기가 다른 compose 섹션보다는 쉽습니다.


여기서 Text버튼이 쏠려 있으니 가운데 정렬을 위해서 centerHorizontallyTo()를 이용해 보겠습니다. 

이 function은 start와 end를 각각 parent의 양쪽 끝 edge로 선언하는 것과 동일한 효과를 갖습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        ...

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            // Centers Text horizontally in the ConstraintLayout
            centerHorizontallyTo(parent)
        })

    }
}






parent의 중간에 위치하도록 Text를 설정했지만 결국 button1 기준으로 중간에 위치한것처럽 보입니다. 

이는 constratinlayout 자체가 기본적으로 본인이 가진 contents를 wrap 한 사이즈를 사용하기 때문입니다. 

다르게 말해 자기가 가진 자식 composables을 표현할 수 있는 최소한의 크기만을 사용합니다.

물론 전체 화면으로 확장해서 사용하기 위해 이미 다른 포스팅에서 여러 번 사용했던 modifier의 fillMaxSize 또는 size를 이용하면 됩니다.



GuideLine

가이드라인은 컴포저블을 위치하게 할 수 있는 보이지 않는 뷰입니다.

가이드라인은 float, offset, dp를 사용하여 크기/위치를 설정할 수 있습니다.


아래와 같은 방식으로 가이드라인을 선언 할 수 있습니다.


val guideLineFromTop5f = createGuidelineFromTop(0.5f)

val guideLineFromTop20dp = createGuidelineFromTop(20.dp)

val guideLineFromEnd20dp = createGuidelineFromEnd(20.dp)

각각, 부모의 Top에서 50%, 20dp, End에서 20dp 만큼 떨어진 부분에 가이드라인이 생성됩니다.


선언 된 가이드라인은 다른 컴포저블들을 ConstraintLayout 내에서 위치시키는데 이용될 수 있습니다.

@Composable
fun GuideConstraintLayoutContent() {
    ConstraintLayout (modifier = Modifier.size(200.dp)) {
        val (text1, text2) = createRefs()

        val guideLineFromTop5f = createGuidelineFromTop(0.5f)
        val guideLineFromTop20dp = createGuidelineFromTop(20.dp)
        val guideLineFromEnd20dp = createGuidelineFromEnd(20.dp)

        Text(
            "50% from top",
            modifier = Modifier.constrainAs(text1) {
                top.linkTo(guideLineFromTop5f)
            }
        )

        Text(
            "Top 20dp End 20dp",
            modifier = Modifier.constrainAs(text2) {
                top.linkTo(guideLineFromTop20dp)
                end.linkTo(guideLineFromEnd20dp)
            }
        )

    }
}





Barrier

Barrier는 가이드라인과 같이 보이지 않는 뷰로서, 다른 컴포저블을 위치시키는데 도움을 줍니다.


하지만 가이드라인이 부모를 기준으로 위치했다면 Barrier는 다른 컴포넌트를 기준으로 유동적으로 위치하게 할 수 있습니다.


아래는 text1, text2중 더 end가 긴 쪽에 가이드라인을 선언하는 코드입니다.


val endBarrier = createEndBarrier(text1, text2)

End 대신 Top, Start, Bottom등을 넣어서 원하는 위치에 배리어를 생성할 수 있습니다.


이를 통해 아래와 같은 뷰를 만들 수 있습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Creates references for the three composables
        // in the ConstraintLayout's body
        val (button1, button2, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button1) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button 1")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button1.bottom, margin = 16.dp)
            centerAround(button1.end)            
        })

        val barrier = createEndBarrier(button1, text)
        
        Button(
            onClick = { /* Do something */ },
            modifier = Modifier.constrainAs(button2) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(barrier)
            }
        ) {
            Text("Button 2")
        }
    }
}



이번에는 reference를 세 개 만들고 버튼 두 개와 Text를 하나 추가 했습니다.

Button1의 위치를 기준으로 Text는 button1의 아래에 위치시키고, Text의 중간이 button1의 끝이 오는 부분에 위치시켰습니다. 추가적으로 button1과 text의 끝 부분에 barrier를 만들고 button2의 시작은 barrier부터 하도록 연결합니다.




barrier 또는 그 이외의 다른 helper들은 constraintAs 영역 안에서 생성할 수 없습니다. 

따라서 위 샘플 코드처럼 constraintAs의 lambda 영역 밖에서 선언해야 합니다.



Chain

기존 xml에서의 ConstraintLayout 안에서 여러 레이아웃을 app:layout_constraintVertical_chainStyle 등의 코드를 통해 체이닝하여 사용하였습니다.

이는 Jetpack Compose에서도 마찬가지로 사용 가능합니다.

createHorizontalChain(button1, button2, chainStyle = ChainStyle.Packed)

우선, createHorizontalChain()을 사용해 체인을 선언해야합니다. createVerticalChain()을 통해 세로로 체이닝 할 수도 있습니다.

chainStyle는 각 컴포저블을 어떤 형태로 체이닝할지 결정하는 요소입니다.

특히, ChainStyle.Packed()는 bias 설정을 통해 어느 한 쪽으로 쏠리게 할 수 있습니다.

아래는 전체 코드이며, ChainStyle에 따라 위의 결과 중 선택한 style에 따른 결과를 얻을 수 있습니다.

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout (modifier = Modifier
        .fillMaxWidth()
        .height(200.dp)) {
        val (button1, button2, text) = createRefs()

        createHorizontalChain(button1, button2, chainStyle = ChainStyle.Packed) // SpreadInside, Spread, Packed

        Button(
            onClick = { },
            modifier = Modifier.padding(8.dp).constrainAs(button1) {
                top.linkTo(parent.top, margin = 8.dp)
            }
        ) {
            Text("Button 1")
        }

        Button(
            onClick = { },
            modifier = Modifier.padding(8.dp).constrainAs(button2) {
                top.linkTo(parent.top, margin = 8.dp)
            }
        ) {
            Text("Button 2")
        }

        Text(
            "Text",
            modifier = Modifier.constrainAs(text) {
                top.linkTo(button1.bottom, margin = 16.dp)
                centerHorizontallyTo(parent)
            }
        )
    }
}





Jetpack Compose LazyColumn / LazyRow (=RecyclerView)

이번 포스팅은 Compose의 리스팅 기능을 살펴보고자 한다.


기존 Xml방식은 List를 주로 RecyclerView 혹은 ListView로 많이 구현을 했을것이다.

Compose에서는 List를 어떻게 구현해야할지 살펴보겠습니다.


기본적인 Column 또는 Row를 사용하여 각 아이템의 콘텐츠를 표시할 수 있음

verticalScroll() Modifier를 사용하여 Column을 스크롤 가능하게 만들 수 있습니다. 

하지만 아이템 갯수만큼 UI가 미리 만들어져 있기 때문에 많은 size의 아이템을 표시해야 하는 경우 성능문제가 발생할 수 있음

@Composable
fun MessageList(messages: List<Message>) {
    Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}


안드로이드 Jetpack Compose의 LazyColumn를 이용한 RecyclerView 구현은 기존 XML 방식에 비해서 코드가 간결하여 간단하게 개발할 수 있다.

또한, 아이템 간의 간격을 추가할 때도 Arrangement.spacedBy() 함수를 통해 설정할 수 있으며 별도의 Adapter 클래스가 필요하지 않은 것도 편하게 느껴집니다.

RecyclerView를 만들기 위해선 ViewHolder, Adapter 클래스도 만들어주고 또 xml에서 각 아이템의 레이아웃도 만들어줘야하고 할 게 굉장히 많았는데, Compose의 LazyColumn, LazyRow를 이용해서 구현하니 비교도 안될 정도로 간단하게 구현할 수 있었다.

LazyColumn, LazyRow는 기존의 RecyclerView와 동일하게 리스트에 속한 모든 View를 한번에 그리지 않고 스크롤하면서 화면에 보여지게 될 때만 그리게 함으로써 리소스 사용을 최적화하기 위한 용도로 만들어졌다.

Compose에서 제공하는 LazyRow를 사용하면 가로 방향으로 무한 스크롤이 되는 RecyclerView를 상당히 간단하게 구현할 수 있게 됐습니다.


- LazyColumn은 세로로 스크롤되는 목록 생성

- LazyRow는 가로로 스크롤되는 목록 생성

- LazyGrid (LazyVerticalGrid, LazyHorizontalGrid..)는 그리드 형태의 스크롤 되는 목록 생성




Android 공식 홈페이지 예제 코드로 RecyclerView의 기능을 얼마나 간단하게 짤 수 있는지 살펴보자.

LazyColumn (세로 스크롤 LazyList)

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}


LazyColumn 에서 아이템을 추가하기 위해선 LazyList Scope 블록안에 추가하고자 하는 아이템을 넣으면 된다. (LazyRow, LazyGrid도 동일)


아이템을 추가하는 방법은 크게 2가지 케이스다

1. item 블록을 사용해 하나 추가

2. items 블록을 사용해 여러 개 추가

//item의 인덱스가 필요할 때는 itemsIndexed 를 사용하면 된다.
@Composable
fun MessageList(messages: List<Message>) {
    
    LazyRow(
        modifier = Modifier.fillMaxSize(),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            itemsIndexed(messages) { index, message ->
                MessageRow(
                    id = index,
                    title = message.title,
                    contents = message.contents
                )
            }
        })
}



콘텐츠 가장자리 주변에 패딩을 추가해야 하는 경우가 있다.

지연 구성요소를 사용하면 일부 PaddingValues을 contentPadding 매개변수에 전달하여 이 작업을 지원할 수 있다.

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}


Arrangement.spacedBy()를 사용하면 아이템 마다 간격을 줄 수 있음

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}



고정 헤더(실험용)

(주의: 실험용 API는 향후 변경되거나 완전히 삭제될 수 있음)


'고정 헤더' 패턴은 그룹화된 데이터 목록을 표시할 때 유용함

LazyColumn이 있는 고정 헤더를 표시하려면 헤더 콘텐츠를 제공하는 실험용 stickyHeader() 함수를 사용

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}


흔히 사용하던 스티키 헤더 방식은 다음과 같은방식으로 가능

val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}




그리드(실험용)

(주의: 실험용 API는 향후 변경되거나 완전히 삭제될 수 있음)


LazyVerticalGrid Composable은 아이템을 그리드로 표시하기 위한 실험용 지원 기능을 제공

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}


스크롤 위치 제어

LazyListState는 스크롤 위치를 '즉시' 스냅하는 scrollToItem() 및 애니메이션을 사용하여 스크롤하는 animateScrollToItem() 함수를 통해 이 기능을 지원한다.

scrollToItem() 및 animateScrollToItem()은 모두 suspend 함수입니다. 

코루틴에서 호출해야 합니다.

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

    LazyColumn(state = listState) {
        // ...
    }

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}



페이징 (Paging)

아이템 양이 많으면 Paging 라이브러리를 사용해서 필요한 만큼 아이템을 불러와서 표시할 수 있음.

Paging 3.0 이상에서는 androidx.paging:paging-compose 라이브러리를 통해 Compose 지원 기능을 제공 

페이징된 콘텐츠 목록을 표시하려면 collectAsLazyPagingItems() 확장 함수를 사용한 다음,

반환된 LazyPagingItems를 LazyColumn의 items()에 전달하면 된다.

import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(lazyPagingItems) {message ->
            message?.let {
                MessageRow(message)
            }
        }
    }
}





Jetpack Compose BackHandler (= onBackPressed) 뒤로가기

안드로이드에서 기존에 뒤로가기 버튼을 눌렀을 때 동작을 커스텀하려면 onBackPressed() 메소드를 오버라이딩 해야 한다.

Jetpack Compose 에서는 어떻게 구현해야 할까?

BackHandler 라는 내장함수를 사용하면 된다.

BackHandler의 enabled 프로퍼티는 BackHandler를 활성화할지 여부를 결정한다.(기본값은 true)


BackHandler를 사용하여 다음과 같은 작업을 수행할수 있다.

- 뒤로가기 버튼을 사용하여 이전화면으로 돌아가기 / 종료하기

- 뒤로가기 버튼을 사용하여 작업을 취소하기

- 뒤로가기 버튼을 사용하여 특정 기능을 활성화 또는 비활성화 하기


BackHandler(enabled = true, onBack= {
    // 뒤로가기 버튼이 눌렸을때 수행할 작업을 정의
    ...
})
// 뒤로가기 두 번 눌렀을 때 앱 종료
@Composable
fun BackOnPressed() {
    val context = LocalContext.current
    var backPressedState by remember { mutableStateOf(true) }
    var backPressedTime = 0L

    BackHandler(enabled = backPressedState) {
        if(System.currentTimeMillis() - backPressedTime <= 400L) {
            // 앱 종료
            (context as Activity).finish()
        } else {
            backPressedState = true
            Toast.makeText(context, "한 번 더 누르시면 앱이 종료됩니다.", Toast.LENGTH_SHORT).show()
        }
        backPressedTime = System.currentTimeMillis()
    }
}
//특정 화면(screen)에서만 종료팝업 띄우기
@Composable
fun BackOnPressed2() {
    val exitDialog = remember { mutableStateOf(false) }

    BackHandler(enabled = (currentRoute(navController) == Screen.Home.route)) {
        exitDialog.value = true
    }

    ...
    
    if (exitDialog.value) {
        ExitAlertDialog(navController, {
            exitDialog.value = it
        }, {
            activity?.finish()
        })

    }
}
@Composable
fun ExitAlertDialog(navController: NavController, cancel: (isOpen: Boolean) -> Unit,  ok: () -> Unit) {
    val openDialog = remember { mutableStateOf(true) }
    if (currentRoute(navController = navController) == Screen.Home.route && openDialog.value) {
        AlertDialog(
            onDismissRequest = {
            },
            // below line is use to display title of our dialog
            // box and we are setting text color to white.
            title = {
                Text(
                    text = stringResource(R.string.close_the_app),
                    fontWeight = FontWeight.Bold,
                    fontSize = 18.sp
                )
            },
            text = {
                Text(text = stringResource(R.string.do_you_want_to_exit_the_app), fontSize = 16.sp)
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                        ok()
                    }) {
                    Text(
                        stringResource(R.string.yes),
                        fontWeight = FontWeight.Bold,
                        style = TextStyle(color = Color.Black)
                    )
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                        cancel(false)
                    }) {
                    Text(
                        stringResource(R.string.no),
                        fontWeight = FontWeight.Bold,
                        style = TextStyle(color = Color.Black)
                    )
                }
            },
        )
    }
}


Jetpack Compose Scaffold

 본 게시글은 Material3를 기준으로 작성되었습니다.


Scaffold

Material Design에서 사용되는 Composable function으로 다양한 구성요소와 기타 화면 요소를 제공합니다.

Material Design 가이드라인을 따르며, 앱의 상단 앱바, 하단 탐색 막대, 콘텐츠 영역 등을 설정할 수 있습니다.


Scaffold 구성 요소

TopAppBar

상단 앱바는 앱의 제목, 액션 버튼, 메뉴 등을 표시할 수 있는 영역입니다. TopAppBar를 사용하여 앱바의 콘텐츠와 동작을 정의할 수 있습니다.

BottomAppBar

하단 탐색 막대는 일반적으로 탐색 메뉴나 액션 버튼을 표시하는 데 사용됩니다. BottomAppBar를 사용하여 하단 막대의 콘텐츠와 동작을 정의할 수 있습니다.

FloatingActionButton

부동 액션 버튼은 일반적으로 앱에서 가장 중요한 작업을 표시하기 위해 사용됩니다. FloatingActionButton을 사용하여 부동 액션 버튼의 모양과 동작을 정의할 수 있습니다.

Content

콘텐츠 영역은 앱의 주요 내용을 표시하는 영역입니다. Content를 사용하여 콘텐츠 영역을 정의할 수 있으며, 보통은 Column이나 Row와 같은 다른 컴포넌트를 사용하여 구성합니다.


[Error] **Content padding parameter it is not used**

만약 Scaffold의 구성요소로 content{ } 부분에 에러가 발생하실 수 있습니다.

Compose 1.2.0부터는 Scaffold 내의 content에 padding value를 적용해야 합니다.


* floatingActionButtonPosition

- floating버튼의 위치를 지정할 수 있습니다. 가능한 위치는 Center와 End입니다.


* isFloatingActionButtonDocked

- floating버튼을 BottomBar의 중간에 걸치고자 한다면 true 값을, 아니면 false값을 사용하시면 됩니다.


* navigationIcon

- TopAppBar의 시작(왼쪽) 부분에 배치되는 아이콘입니다. 주로 메뉴 버튼 또는 네비게이션 아이콘으로 사용됩니다. navigationIcon은 TopAppBar에 단일 아이콘만 배치됩니다.


AppBar

상단 앱 바나 하단 앱 바(네비게이션 바)를 제공합니다.

Scaffold(
	topBar = {
    	TopAppBar { /* ... */ }
    },
    bottomBar = {
    	BottomAppBar { /* ... */ }
        // 혹은
        // NavigationBar() { /* ... */ }
    }
) {
	/* ... */
}



BottomAppBar

BottomAppBar를 사용하여 하단 막대의 콘텐츠와 동작을 정의할 수 있습니다.

Scaffold(
    bottomBar = {
        BottomAppBar(
            backgroundColor = Color.Blue
        ) {
            IconButton(onClick = {}) {
                Icon(Icons.Default.Favorite, contentDescription = "Favorite")
            }
            IconButton(onClick = {}) {
                Icon(Icons.Default.Add, contentDescription = "Add")
            }
        }
    }

) {
	/* ... */
}



FloatingActionButton

플로팅 작업 버튼을 제공합니다.

floatingActionButtonPosition 을 이용하여 가로 위치를 조정할 수 있습니다.

Scaffold(
	floatingActionButtonPosition = FabPosition.End,
	floatingActionButton = {
    	FloatingActionButton(onClick = { /* ... */ }) {
        	/* ... */
        }
    },
    
    // bottomBar가 있을 시 isFloatingActionButtonDocked 을 이용하여 겹치게 만들 수 있습니다.
    isFloatingActionButtonDocked = true,
    bottomBar = { /* ... */ }
) {
	/* ... */
}



Content

콘텐츠 영역은 앱의 주요 내용을 표시하는 영역입니다. Content를 사용하여 콘텐츠 영역을 정의할 수 있으며, 보통은 Column이나 Row와 같은 다른 컴포넌트를 사용하여 구성합니다.

Scaffold(
    content = {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .podding(it)
        ) { /* ... */ }
    }
) {
	/* ... */
}



Snackbar

val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(
    snackbarHost = { SnackbarHost(snackbarHostState) },
	floatingActionButtonPosition = FabPosition.End,
	floatingActionButton = {
    	FloatingActionButton(onClick = { 
	        coroutineScope.launch {
    	    	snackbarHostState.showSnackbar("Show Snackbar")
        	}
        }) {
        	/* ... */
        }
    }
) {
    SnackbarHost(
        hostState = snackBarHostState,
        /* ... */
    )
}



Jetpack Compose Composable의 Lifecycle

  이 페이지에서는 컴포저블의 수명 주기에 관해 알아보며 Compose에서 컴포저블에 재구성이 필요한지를 결정하는 방법을 살펴봅니다. 수명 주기 개요 상태 관리 문서 에 설명된 대로 컴포지션은 앱의 UI를 설명하고 컴포저블을 실행하여 생성됩니다. 컴포...