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
리스트와 그리드
- 표준 리스트는 초기화 시점에 리스트에 포함된 모든 아이템을 만듬. 메모리 부족과 성능 제한 유발.
- 지연 리스트는 사용자가 스크롤을 하면 표시 영역에서 벗어나는 아이템들은 파괴하고 리소스를 확보, 아이템들은 표시되는 시점에 만들어짐. 성능 저하 없이 표시 가능.
- LazyColumn, LazyRow
- LazyListScope의 item() 함수를 호출해 지연 리스트에 개별 아이템 추가 가능.
- items() 함수를 호출하면여러 아이템을 한번에 추가 가능.
- itemsIndexed() 함수를 이용하면 아이템의 콘텐츠와 인덱스 값을 함께 얻을 수 있음.
LazyColumn {
item {
MyListItem()
}
}
- 화면에 표시되는 영역에 맞는 아이템만 접근 가능. Row, Column 기반의 리스트를 스크롤 하게 하려면 ScrollState를 이용해 스크롤 활성화 가능.
- 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 람다를 제공하면 컴포즈는 이 항목이 콘텐츠가 다를 뿐 동일한 항목임을 인지.
- 해당 값만 리컴포즈 되어 효율적으로 사용.