Android

핵심만 골라 배우는 젯팩 컴포즈 Study 3주차

가짜 개발자 2023. 1. 28. 20:37


Chapter 27 ~ 36

 

커스텀 레이아웃

fun Modifier.<커스텀 레이아웃 이름> (
		// 선택적 파라미터
) = layout { measurable, constraints ->
		// 요소의 위치와 크기를 조정할 코드
}
  • Box, Row, Column 같은 내장 레이아웃 컴포넌트 만으로 대응하기 어려운 레이아웃을 만들어야 할 때 사용.
  • 커스텀 레이아웃 모디파이어는 위의 표준 구문을 이용해 만들 수 있음.
  • 싱글 패스 측정 : 커스텀 레이아웃을 개발 할 때는 모디파이어가 호출될 때마다 자식을 측정하는 규칙이 적용.
    • 사용자 인터페이스 트리 계층을 신속하고 효율적으로 렌더링하기 위해 중요.
    • 변경된 요소만 측정.
    • 컴포즈가 빠른 이유 중 하나.
  • layout 수정자를 이용해 요소가 측정되고 배치되는 방식을 수정 할 수 있음.
    • measurable : 측정할 요소.
    • constraints : 부모로부터 받은 min, max의 width, height 범위.
fun Modifier.exampleLayout(
    x: Int,
    y: Int
) = layout { measurable, constraints ->
		// measure를 호출해 배치 가능한 크기 전달 받음.
    val placeable = measurable.measure(constraints)

    layout(placeable.width, placeable.height) {
        placeable.placeRelative(x, y)
    }
}
  • measure의 반환된 객체인 Placeable 은 측정된 width, height 값을 가짐.
  • layout() 메서드는 placeable 값으로부터 높이와 폭을 전달함. 또한, 자식의 위치를 지정하는 layout() 메서드에 후행 람다를 전달해아 함.
  • 후행 람다 안에서 Placeable 객체의 placeRelative() 메서드 호출을 통해 자식 요소의 위치가 지정됨.
    • placeRelative : 레이아웃 방향이 변경될 때 자동으로 미러링하는 경우 사용.
  • 새로운 위치는 부모가 정의한 기본 좌표에 따라 상대적으로 계산됨.
  • placeable 객체로부터 자식의 폭을 받아서 fraction 파라미터값을 곱함. 결과는 부동소수점이라 정숫값으로 반올림.
  • 정렬 선을 오른쪽으로 옮기는 것은 자식을 왼쪽으로 옮기는 것과 같으므로 x 값을 음수로 바꿈.
  • 수직 위치는 변경되지 않았으므로 y값은 0으로 설정.
fun Modifier.exampleLayout(
    fraction: Float
) = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)

    val x = -(placeable.width * fraction).roundToInt()

    layout(placeable.width, placeable.height) {
        placeable.placeRelative(x, y = 0)
    }
}

베이스라인

  • Text 컴포저블은 텍스트 콘텐츠 베이스라인을 따라 정렬할 수 있음.
  • FirstBaseline, LastBaseline 정렬 선은 Text 컴포넌트 안에 포함된 텍스트 콘텐츠의 첫 번째 행과 마지막 행의 바닥선에 해당.
  • 모든 컴포저블이 베이스라인 정렬을 지원하지는 않음. 정렬이 AlignmentLine.Unsepcified와 동일하지 않은지 확인해보면됨.
// Check the composable has a first baseline
if (placeable[FirstBaseline] == AlignmentLine.Unspecified) {
       
}

Chapter 28

커스텀 레이아웃 구문

@Composable
fun DoNothingLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = 0)
            }
        }
    }
}
  • layout 수정자는 호출하는 컴포저블만 변경.
  • 여러 컴포저블을 측정하고 배치하려면 Layout 컴포저블 사용.
    • modifier와 content가 최소 기본 파라미터로 넘어가야함.
  • view system에서 ViewGroup을 상속받아 measure / layout을 구현해야 했지만 Compose에서는 Layout 하나로 구현할 수 있음.
  • Layout() 컴포저블을 호출하고, 해당 컴포저블 뒤에 람다를 받음. 이 람다는 measurables, contraints 2개의 파라미터를 전달받음.
  • 마지막으로 layout() 함수를 호출하고 부모가 할당할 수 있는 최대 높이와 폭의 값을 전달.
  • 커스텀 레이아웃은 자식 요소를 재배치하지 않음. 스택으로 쌓이게 됨.

Chapter 29

ConstraintLayout

  • 모디파이어의 **constraintAs()**를 사용하여 생성한 ID를 Compose로 사용 가능.
    • 제약 : 정렬과 위치를 조정하는 일련의 규칙
    • 마진 : 고정된 거리를 지정하는 제약의 한 형태.
    • 반대 제약 : 동일한 축을 따라 한 컴포저블이 가진 2개의 제약.
    • 제약 편향 : 반대 제약 상태에서 컴포넌트의 위치를 조정 하기 위해 사용.
    • 체인 : 하나의 그룹으로 정의된 2개 이상의 컴포저블을 포함하는 레이아웃의 동작 방법 제공.
    • 체인 스타일 : 체인의 레이아웃 동작을 설정.
    • 가이드라인 : 시각적 도우미 역할.
    • 배리어 : 가상의 뷰로 레이아웃 안에 표시되도록 제한할 때 사용.
  • 크기 설정하기
    • preferredWrapContent : constraints의 내부 공간에 맞춰 wrap content를 함.
      • contaraint 내부 공간에 맞추면서 기본 크기도 적용할 수 있음 e.g. Dimemsion.preferredWrapContent.atLeast(100.dp)
    • wrapContent: constraints 내부 공간과는 상관없이 item을 감쌀 수 있을 만큼을 크기로 가짐.
    • fillToConstraints: constraint에 선언된 값만큼 constraint 내부를 확장해서 채움.
    • preferredValue: constraint내부 공간에서 preferredValue에 선언된 dp 값만큼을 크기로 가짐.
    • value: constraint와 상관없이 선언된 dp값만큼을 크기로 가짐.
ConstraintLayout {
        val text1 = createRef()
        val (button, text1, text2) = createRefs()
    }
  • createRef() 함수를 호출해서 하나의 참조를 생성하고 그 결과를 상수에 할당 할 수 있음.
  • **createRefs()**를 호출해 한번에 여러 참조를 생성할 수 있음.
Text(text = "Hello", modifier = Modifier.constrainAs(text1) {
            // 제약들
        }
  • 기존 ContsraintLayout의 app:layout_constraintTop_toTopOf 처럼 뷰들의 ID를 참조하여 연결하는 방식을 Compose-ConstraintLayout에서 linkTo를 통해 사용.

Chapter 30

제약 집합을 이용해 제약 연결 끊기

  • 화면 구성에 따라 다르게 ConstrinatLayout을 구성할 때 이를 분리시켜 사용 가능.
  • 이 분리된 제약들은 ConstraintLayout에 전달하면 컴포저블 자식들에 제약을 적용 가능.
  • 모디파이어 선언을 중복하지 않고 재사용할 수 있는 제약 집합을 만들 수 있음.
  • ConstraintSet을 ConstraintLayout 생성 시 파라미터로 전달.
  • ConstraintLayout에서는 ConstraintSet에서 정의해놓은 구성에서 ID만 입력해 사용가능.
@Composable
fun MainScreen() {
    ConstraintLayout(Modifier.size(width = 200.dp, height = 200.dp)) {
        val constraints = myConstraintSet(margin = 8.dp)

        ConstraintLayout(constraints, Modifier.size(width = 200.dp, height = 200.dp)) {
            MyButton(
                text = "Button1",
                Modifier
                    .size(200.dp)
                    .layoutId("button1")
            )
        }
    }
}

private fun myConstraintSet(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button1 = createRefFor("button1")
        constrain(button1) {
            linkTo(parent.top, parent.bottom, topMargin = 8.dp, bottomMargin = 8.dp)
            linkTo(parent.start, parent.end, startMargin = 8.dp, endMargin = 8.dp)
            width = Dimension.fillToConstraints
            height = Dimension.fillToConstraints
        }
    }
}

Chapter 31

IntrinsicSize

  • 컴포즈 규칙 중 하나는 하위 요소를 한 번만 측정해야 함. 두 번 측정하면 런타임 예외 발생.
  • 하지만, 하위 요소를 측정하기 전에 미리 정보가 필요한 경우 IntrisicSize를 통해 알아올 수 있음.
  • 두번 측정하는건 아님. 하위 요소는 실제로 측정되기 전에 Intrinsic measurement를 요청 받음. 이 정보를 가지고 부모는 하위 요소를 측정할 때 계산함.
  • IntrinsicSize는 가장 넓은 자식이 가질 수 있는 최댓값, 최솟값에 관한 정보를 부모에게 제공함.
.
.
   Column(Modifier.width(200.dp).padding(5.dp)) {
        Column(Modifier.width(IntrinsicSize.Max)){
.
..
.
   Column(Modifier.width(200.dp).padding(5.dp)) {
        Column(Modifier.width(IntrinsicSize.Min)) {
.
  • 하나 이상의 자식들의 크기가 동적으로 변경될 때 매우 유용.
  • Text 컴포저블을 예로 내재적 최대 값은 그 컴포저블이 표시하는 텍스트의 길이와 같음.
  • 높이 제한이 없다고 가정하면 Text 컴포넌트가 필요하는 최소 폭은 텍스트 문자열에서 가장 긴 단어의 길이와 같음.

Chapter 32

코루틴과 LaunchedEffect

  • 코루틴
    • 코루틴은 자신이 실행된 스레드를 정지시키지 않으면서 비동기적으로 실행되는 비동기적인 코드 블록.
    • 전통적인 다중 스레딩 기법보다 훨씬 단순하고 효율적인 비동기 태스크 수행 접근 방식을 제공.
  • LaunchedEffect
    • 컴포저블에서 컴포지션이 일어날 때 suspend fun을 실행해주는 컴포저블.
    • 리컴포지션이 일어날 때마다 LaunchedEffect가 취소되고 다시 수행된다면 비효율적.
    • 그래서 key를 기준값으로 두어 key가 바뀔 때만 LaunchedEffect의 suspend fun을 취소하고 재실행.
    • key 값이 변경되면 LaunchedEffect는 현재 코루틴을 취소하고 새로운 코루틴 실행.
    • LaunchedEffect 컴포저블이 호출되면, 해당 코루틴은 즉시 실행되고 비동기 코드 수행을 시작. 부모 컴포저블이 완료되는 즉시, 해당 LaunchedEffect 인스턴스와 코루틴은 파기됨.
@Composable
fun Greeting(name: String) {
    val coroutineScope = rememberCoroutineScope()
    
    LaunchedEffect(key1 = Unit) {
        coroutineScope.launch { 
            // async code
        }
    }
}
  • SideEffect
    • 리컴포지션 마다 호출됨.
    • key 파라미터를 받지 않으며, 부모 컴포저블이 재구성될 때마다 실행.
    • 성공적인 리컴포지션이 발생할 때만 호출되어야 하는 작업에 사용.
@Composable
fun Greeting(name: String) {
    val coroutineScope = rememberCoroutineScope()

    SideEffect {
        coroutineScope.launch {
            // async code
        }
    }
}

Chapter 33

리스트와 그리드

  • 표준 리스트는 초기화 시점에 리스트에 포함된 모든 아이템을 만듬. 메모리 부족과 성능 제한 유발.
    • Row, Column
  • 지연 리스트는 사용자가 스크롤을 하면 표시 영역에서 벗어나는 아이템들은 파괴하고 리소스를 확보, 아이템들은 표시되는 시점에 만들어짐. 성능 저하 없이 표시 가능.
    • LazyColumn, LazyRow
    • LazyListScope의 item() 함수를 호출해 지연 리스트에 개별 아이템 추가 가능.
    • items() 함수를 호출하면여러 아이템을 한번에 추가 가능.
    • itemsIndexed() 함수를 이용하면 아이템의 콘텐츠와 인덱스 값을 함께 얻을 수 있음.
    LazyColumn {
    	item {
    		MyListItem()
    	}
    }
    
  • 화면에 표시되는 영역에 맞는 아이템만 접근 가능. Row, Column 기반의 리스트를 스크롤 하게 하려면 ScrollState를 이용해 스크롤 활성화 가능.
    • LazyList, LazyRow는 스크롤 기본 제공.
    • 프로그래밍적 스크롤
      • animateScrollTo(value: Int) : 애니메이션을 이용해 지정한 위치까지 부드럽게 스크롤.
      • scrollTo(value: Int) : 지정한 위치까지 곧바로 스크롤.
      • 리스트의 끝을 나타내는 위치는 모호함. scroll 상태 인스턴스의 maxValue 프로퍼티를 통해 최대 스크롤 위치 얻어 낼 수 있음.
    • LazyColumn, LazyRows 리스트를 프로그래밍적으로 스크롤 할 때는 LazyListState 인스턴스가 제공하는 함수들 호출.
      • rememberLazyListState() 함수를 호출해 얻을 수 있음.
      • animateScrollToItem(index: Int) : 지정한 리스트 아이템까지 부드럽게 스크롤.
      • scrollToItem(index: Int) : 지정한 리스트 아이템까지 곧바로 스크롤.
      • firstVisibleItemIndex : 리스트에서 현재 가장 처음에 보이는 아이템의 인덱스.
      val listState = rememberLazyState()
      LazyColumn(state = listState) { }
      
    • ScrollState 및 LazyListState를 이용할 때는 재구성을 통해 기억되는 CoroutineScope 인스턴스에 접근해야 함.
      • remeberCoroutineScope를 호출.
      val coroutineScope = rememberCoroutineScope()
      coroutineScope.launch {
      	scrollState.animateScrollTo(scrollState.maxValue)
      }
      
  • val scrollState = rememberScrollState() Column(Modifier.verticalScroll(scrollState)) { repeat(100) { MyListItem() } }
  • 지연 리스트에서만 이용할 수 있는 stickyHeader() 함수를 이용해 스크롤되는 동안 화면에서 계속 표시 가능.
  • 그리드 레이아웃
    • 지연 그리드는 LazyVerticalGrid 컴포저블을 이용.
    • 형태는 cells 파라미터를 통해 제어. 이 파라미터는 적응 모드, 고정 모드로 설정 가능.
    • 적응 모드는 그리드가 이용할 수 있는 공간에 맞게 행과 열의 수를 계산. 이때 아이템 사이의 공간은 최소 지정 셀 크기가 됨.
    • 고정 모드에서는 표시할 행의 수를 전달하면 이용할 수 있는 공간의 폭을 채우기 위해 각 열의 폭을 동일 크기로 조정.
    LazyVerticalGrid(
            cells = GridCells.Adaptive(minSize = 60.dp),
            state = rememberLazyListState(),
            contentPadding = PaddingValues(10.dp)
        ) {
            items(30) { index ->
                Card(
                    backgroundColor = Color.Blue, modifier = Modifier
                        .padding(5.dp)
                        .fillMaxSize()
                ) {
                    Text(
                        text = "$index",
                        fontSize = 35.sp,
                        color = Color.White,
                        textAlign = TextAlign.Center
                    )
                }
            }
        }
    
LazyVerticalGrid(
        cells = GridCells.Fixed(3),
        state = rememberLazyListState(),
        contentPadding = PaddingValues(10.dp)
    ) {
        items(30) { index ->
            Card(
                backgroundColor = Color.Blue, modifier = Modifier
                    .padding(5.dp)
                    .fillMaxSize()
            ) {
                Text(
                    text = "$index",
                    fontSize = 35.sp,
                    color = Color.White,
                    textAlign = TextAlign.Center
                )
            }
        }
    }

Chapter 34

컴포즈 리스트 튜토리얼

@Composable
fun ColumnList() {
    val scrollState = rememberScrollState()
    val coroutineScope = rememberCoroutineScope()

    Column {
        Row {
            Button(
                onClick = {
                    coroutineScope.launch {
                        scrollState.animateScrollTo(0)
                    }
                }, modifier = Modifier
                    .weight(0.5f)
                    .padding(2.dp)
            ) {
                Text(text = "Top")
            }
            Button(
                onClick = {
                    coroutineScope.launch {
                        scrollState.animateScrollTo(scrollState.maxValue)
                    }
                }, modifier = Modifier
                    .weight(0.5f)
                    .padding(2.dp)
            ) {
                Text(text = "End")
            }
        }

        Column(Modifier.verticalScroll(scrollState)) {
            repeat(500) {
                Text(
                    text = "List Item $it",
                    style = MaterialTheme.typography.h4,
                    modifier = Modifier.padding(5.dp)
                )
            }
        }
    }
}

Chapter 35

지연 리스트 튜토리얼

@Composable
fun MyListItem(item: String, onItemClick: (String) -> Unit) {
    Card(
        Modifier
            .padding(8.dp)
            .fillMaxWidth()
            .clickable { onItemClick(item) },
        shape = RoundedCornerShape(10.dp),
        elevation = 5.dp
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            ImageLoader(item = item)
            Spacer(modifier = Modifier.width(8.dp))
            Text(
                text = item,
                style = MaterialTheme.typography.h6,
                modifier = Modifier.padding(8.dp)
            )
        }
    }
}

@Composable
fun ImageLoader(item: String) {
    val url =
        "<https://www.ebookfrenzy.com/book_examples/car_logos/>" + item.substringBefore(" ") + "_logo.png"

    Image(
        painter = rememberImagePainter(url),
        contentDescription = "car image",
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(75.dp)
    )
}

@Composable
fun MainScreen(itemArray: Array) {
    val context = LocalContext.current
    val onListItemClick = { text: String ->
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
    }
    LazyColumn {
        items(itemArray) { model ->
            MyListItem(item = model, onItemClick = onListItemClick)
        }
    }
}

Chapter 36

지연 리스트가 제공하는 스티키 헤더와 스크롤 식별

@Composable
fun MainScreen(itemArray: Array<out String>) {
    val context = LocalContext.current
    val groupedItems = itemArray.groupBy { it.substringBefore(' ') }
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    val displayButton = listState.firstVisibleItemIndex > 5

    val onListItemClick = { text: String ->
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
    }

    Box {
        LazyColumn(state = listState, contentPadding = PaddingValues(bottom = 40.dp)) {
            groupedItems.forEach { (manufacturer, models) ->
                stickyHeader {
                    Text(
                        text = manufacturer,
                        color = Color.White,
                        modifier = Modifier
                            .background(Color.Gray)
                            .padding(5.dp)
                            .fillMaxWidth()
                    )
                }
                items(models) { model ->
                    MyListItem(item = model, onItemClick = onListItemClick)
                }
            }
        }
        AnimatedVisibility(visible = displayButton, Modifier.align(Alignment.BottomCenter)) {
            OutlinedButton(
                onClick = { coroutineScope.launch { listState.scrollToItem(0) } },
                border = BorderStroke(1.dp, Color.Gray),
                shape = RoundedCornerShape(50),
                colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.DarkGray),
                modifier = Modifier.padding(5.dp)
            ) {
                Text(text = "Top")
            }
        }
    }
}

Q) 컴포즈는 일반적으로 안드로이드 뷰 시스템보다 빠르다고 한다. 하지만 느리다. 느려진 원인을 어떻게 찾을 수 있을까?

  • 컴포즈는 디버그 버전에서 내부적으로 많은 작업이 진행됨, 런타임에서 바이트 코드를 변환하기 때문에 느림.
  • 컴포즈는 라이브러리로 배포되기 때문에 앱이 시작될 때 로드되어야 해서 비용이 발생.

Q) 컴포즈를 쓰면서 느리게 렌더링 되는 다양한 케이스들을 생각해보자.

  • LazyColumn을 사용할 때 느리게 렌더링 되는 케이스.

Q) 렌더링이 느린 케이스들에 대해서 각각 개선 방법을 생각해보자.

LazyColumn {
        items(
            items = messages,
            key = { message ->
                // Return a stable + unique key for the item
                message.id
            }
        ) { message ->
            // Display entry here
            MessageRow(message)
        }
    }item에 key를 세팅. 
  • 리사이클러뷰의 DiffUtil과 유사한 동작.
  • key 람다를 제공하면 컴포즈는 이 항목이 콘텐츠가 다를 뿐 동일한 항목임을 인지.
  • 해당 값만 리컴포즈 되어 효율적으로 사용.

 

반응형