ABOUT ME

-

  • 핵심만 골라 배우는 젯팩 컴포즈 Study 3주차
    Android 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 람다를 제공하면 컴포즈는 이 항목이 콘텐츠가 다를 뿐 동일한 항목임을 인지.
    • 해당 값만 리컴포즈 되어 효율적으로 사용.

     

    반응형

    댓글

Designed by Me.