Computer >> 컴퓨터 >  >> 체계 >> Android

초보자를 위한 Jetpack Compose 튜토리얼 – 컴포저블 및 재구성을 이해하는 방법

이 튜토리얼은 Android의 Jetpack Compose UI 라이브러리와 관련된 몇 가지 기본 개념과 용어를 알려줍니다.

이것은 Compose에 대한 초보자용 가이드이지만 Android에 대한 초보자용 가이드는 아닙니다. 따라서 적어도 한두 개의 애플리케이션을 빌드해야 합니다(Compose에는 없지만 반드시 필요함).

시작하기 전에 처음에는 Leland Richardson의 2부 기사 시리즈를 접할 때까지 더 많은 시니어 개발자를 대상으로 하는 후속 기사를 작성할 계획이었습니다. Leland는 Jetpack Compose 팀에서 일하는 소프트웨어 엔지니어일 뿐만 아니라 훌륭한 작가이기도 합니다.

내 기사가 Jetpack Compose의 기본 사항에 대한 소개로 독립적이라고 생각하지만 강력하게 제안합니다. Compose에 대한 실질적인 경험을 쌓고 나면 그의 기사를 읽게 됩니다(또는 그런 방식으로 배우고 싶다면 즉시).

이 문서에 설명된 주요 용어/개념:

  • 이전 보기 시스템 및 계층 구조에 대한 간략한 검토
  • 컴포저블 및 뷰와 관련하여 컴포저블의 위치
  • 재구성 및 매우 좋지 않은 작업을 피하는 방법!

컴포저블이란 무엇입니까?

이 섹션에서는 Jetpack Compose 라이브러리의 가장 기본적인 부분에 대해 설명합니다. 노련한 Android 개발자라면 "구성 가능한 보기가 있습니까?"라는 제목의 하위 섹션으로 건너뛸 수 있습니다.

View 시스템에 아직 익숙하지 않은 경우 동기를 부여하고 Composable이 무엇인지 이해하는 데 필요하므로 다음 섹션을 읽어야 합니다.

계층 구조 보기

Android SDK(이 플랫폼에서 사용자 인터페이스를 만드는 데 사용하는 라이브러리)의 맥락에서 보기는 애플리케이션에 구조와 스타일을 제공하는 데 사용하는 것입니다.

이것은 주어진 UI(사용자 인터페이스)의 가장 기본적인 유형의 빌딩 블록 또는 요소이며 이러한 각 빌딩 블록에는 다음과 같은 종류의 정보가 포함됩니다.

  • X 및 Y는 장치 화면에서 뷰를 그릴 위치를 컴퓨터에 알려주는 시작 및 끝 위치
  • 색상 및 알파(투명도) 값
  • 글꼴 정보, 텍스트, 기호 및 이미지
  • 사용자 상호작용(클릭) 또는 애플리케이션 데이터 변경(나중에 자세히 설명)과 같은 이벤트를 기반으로 한 동작

보기는 버튼과 비슷할 수 있음을 이해하는 것이 중요합니다. (일반적으로 '위젯'이라고 함)하지만 전체 화면, 화면 일부 또는 다른 하위 보기의 컨테이너일 수도 있습니다. .

이러한 컨테이너 컨텍스트에 따라 일반적으로 레이아웃 또는 뷰 그룹이라고 합니다. 또한 위젯과 대부분의 동일한 정보를 공유하는 동시에 중첩된 다른 보기를 정렬하고 표시하는 방법에 대한 정보도 포함합니다. 그들 안에.

이를 염두에 두고 이 View 시스템 검토의 중요한 부분인 View Hierarchy에 도달했습니다. . 웹 개발자에게 View Hierarchy는 본질적으로 Android 버전의 DOM(Document Object Model)입니다.

Android 개발자의 경우 View Hierarchy를 XML 파일로 정의하거나 Java 또는 Kotlin에서 프로그래밍 방식으로 정의한 모든 View의 가상 표현으로 생각할 수 있습니다.

이를 설명하기 위해 이러한 XML 파일을 살펴보겠습니다(자세히 연구할 필요가 없으며 이름만 메모해 두십시오). 그런 다음 디버거/스테퍼 도구를 사용하여 이 파일을 확장하는 프래그먼트의 메모리 공간에서 어떻게 보이는지 살펴보겠습니다.

fragment_hour_view.xml:

<?xml version=”1.0" encoding=”utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=”https://schemas.android.com/apk/res/android"
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:id=”@+id/root_hour_view_fragment”
xmlns:app=”https://schemas.android.com/apk/res-auto"
>
<androidx.compose.ui.platform.ComposeView
android:id=”@+id/tlb_hour_view”
//...
 />
<com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView
android:id=”@+id/vqht_one”
//...
/>
<com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView
android:id=”@+id/vqht_two”
//...
/>
<com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView
android:id=”@+id/vqht_three”
//...
/>
<com.wiseassblog.samsaradayplanner.ui.managehourview.HourToggleView
android:id=”@+id/vqht_four”
//...
/>
</androidx.constraintlayout.widget.ConstraintLayout>

(Fragment)HourView.kt의 메모리 공간:

초보자를 위한 Jetpack Compose 튜토리얼 – 컴포저블 및 재구성을 이해하는 방법
보기 계층의 이미지

디버거와 스테퍼 도구는 내가 다양한 라이브러리에서 사용하는 코드의 내부에서 무슨 일이 일어나고 있는지 배울 수 있는 가장 좋아하는 방법 중 일부입니다. 한 번 시도해 보세요!

이 XML 파일과 프로세스 에서 변환되는 내용을 보여주는 목적 (프로세스는 실행 중인 프로그램 장치에서) XML 파일의 중첩된 보기가 런타임에 중첩된 보기 계층으로 변환되는 방법을 보여주기 위한 것입니다.

이전 시스템의 작동 방식에 대한 간단하지만 구체적인 모델을 통해 새 시스템과 비교할 수 있기를 바랍니다.

컴포저블 보기입니까?

이것은 내가 Compose를 사용하기 시작했을 때 내가 처음으로 물은 질문 중 하나였으며 내가 도달한 대답은 둘 다 입니다. 아니요 .

, 컴포저블이 뷰와 동일한 개념적 역할을 수행한다는 의미에서 오래된 시스템에서. 컴포저블은 버튼과 같은 위젯이거나 ConstraintLayout과 같은 컨테이너일 수 있습니다(사용 가능한 ConstraintLayout의 컴포저블 구현이 있다는 점에 주목할 가치가 있습니다).

아니요 , UI가 더 이상 View Hierarchy에서 가상으로 표현되지 않는다는 점에서(상호 운용성과 관련된 상황은 제외). 즉, 작성은 UI를 가상으로 표현하고 추적하기 위해 마술을 사용하지 않습니다. 즉, View Hierarchy와 개념적으로 유사한 자체 항목이 있어야 합니다.

이것에 대해 아주 간략하게 살펴보자. 여기에 setContent {…}을 사용하는 활동이 있습니다. Composable을 자신에 바인딩하는 함수:

ActiveGameActivity.kt:

class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer {
private lateinit var logic: ActiveGameLogic
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val viewModel = ActiveGameViewModel()
    setContent {
        ActiveGameScreen(
            onEventHandler = {
                logic.onEvent(it)
            },
            viewModel
        )
    }
    logic = buildActiveGameLogic(this, viewModel, applicationContext)
}
//…
}

ActiveGameScreen.kt:

@Composable
fun ActiveGameScreen(
    onEventHandler: ((ActiveGameEvent) -> Unit),
    viewModel: ActiveGameViewModel
) {
    //...

    GraphSudokuTheme {
        Column(
            Modifier
                .background(MaterialTheme.colors.primary)
                .fillMaxHeight()
        ) {
            ActiveGameToolbar(
                clickHandler = {
                    onEventHandler.invoke(
                        ActiveGameEvent.OnNewGameClicked
                    )
                }
            )

            Box {
              //content
            }
        }
    }
}

Compose에서 View Hierarchy는 mWindow 를 깊이 파고들면 찾을 수 있는 것으로 대체됩니다. 이 활동의 ​​필드입니다. 해당 필드 내에는 보기 계층 구조의 개념적 대체가 있습니다. Composer slotTable .

초보자를 위한 Jetpack Compose 튜토리얼 – 컴포저블 및 재구성을 이해하는 방법

이 시점에서 Composer 및 해당 slotTable , 나는 당신이 Leland의 기사를 읽을 것을 다시 제안해야 합니다(그는 2부에서 자세히 설명합니다). Composer 및 해당 slotTable보다 Compose Hierarchy에 더 많은 것이 있지만 시작하기에 충분할 것입니다.

일반적으로 Jetpack Compose는 Compose Hierarchy(Composer 및 해당 slotTable로 구성되고 관리됨)라고 부를 수 있는 것을 사용합니다.

다시 말하지만, 이는 UI를 집합적으로 나타내는 메모리 공간의 개체 묶음인 View 계층 구조와 동일한 개념적 아이디어이지만 매우 다르게 구현됩니다.

그러나 기술적으로 이해하기는 어렵지만 원칙적으로는 이해하기 쉬운 중요한 차이점이 있습니다. 이것은 Compose가 Compose 계층에 대한 업데이트를 처리하는 방식입니다. Recomposition .

재구성:Compose UI 업데이트 방법

제 ESL 친구들에게 Compose라는 단어는 라틴어 compone에서 왔습니다. , 대략적으로 "조립하다"를 의미합니다. 음악을 쓰는 사람을 종종 "작곡가"라고 하며, 하나 이상의 악기에서 나오는 음표를 하나의 작곡(노래)으로 만드는 사람으로 생각할 수 있습니다.

모으는 것은 개별 조각이 있음을 의미합니다. 거의 모든 훌륭한 소프트웨어 개발자는 자신의 코드를 가장 작은 합당한 부분으로 나누기 위해 최소한 어느 정도 노력한다는 점을 이해하는 것이 중요합니다. .

합리적이라고 언급함 , DRY(Don't Repeat Yourself)와 같은 원칙은 그들이 만드는 것보다 더 많은 문제를 해결하는 정도까지만 따라야 한다고 생각하기 때문입니다.

이 개념을 적용하면 많은 이점이 있습니다. 이 개념은 종종 모듈성(또는 제가 선호하는 방식으로 관심사 분리(SOC))이라고 합니다. 이 글을 읽는 여러분 중 일부는 제가 Leland가 그의 기사에서 말한 것을 그대로 따라하고 있다고 생각할 수도 있다는 것을 알고 있습니다. 그러나 저는 이미 수년 동안 SOC를 소프트웨어 아키텍처의 황금 원칙으로 이야기해 왔습니다.

이것이 Compose에서 작동하는 위치는 인기 있는 Javascript 라이브러리 React에서 볼 수 있는 것과 동일한 원칙입니다. . 제대로 완료되면 Compose는 재구성해야 하는 구성 가능 항목(UI의 일부/요소)만 "재구성"(다시 그리기, 다시 렌더링, 업데이트 등)합니다.

이것은 응용 프로그램의 성능과 관련하여 매우 중요합니다. 이전 View 시스템에서든 Compose에서든 UI를 다시 그리는 것은 시스템 리소스에 비용이 많이 들기 때문입니다.

모르는 경우를 대비하여 이전 RecyclerView(2016년에 처음으로 튜토리얼을 만든 것입니다!)의 전체 목적은 ViewHolder 패턴을 데이터 목록에 적용하는 것이었습니다. 이렇게 하면 각 목록 항목에 대해 새 보기를 지속적으로 확장(만들기)할 필요가 없습니다.

이 기사의 목표는 대부분 이론에 초점을 맞추는 것이었습니다. 앞으로 몇 달 동안 많은 실용적인 내용을 쓸 것이기 때문입니다. 그러나 재구성이 작동하는 방식과 잘못된 작업을 피하는 방법을 더 잘 이해하는 데 도움이 될 제 직접적인 경험의 이야기로 기사를 마무리하겠습니다.

스톱워치 예

첫 번째 완전한 Compose 응용 프로그램의 경우 Sudoku를 구축하기로 결정했습니다. 엄청나게 복잡한 UI가 없는 프로젝트를 원했다는 사실을 포함하여 여러 가지 이유가 있습니다. 또한 스도쿠 퍼즐에 매우 적합한 Graph DS와 Algos에 대해 자세히 알아볼 수 있는 기회를 원했습니다.

내가 원했던 한 가지는 사용자가 퍼즐을 완성하는 데 걸린 시간을 추적하는 스톱워치였습니다.

초보자를 위한 Jetpack Compose 튜토리얼 – 컴포저블 및 재구성을 이해하는 방법
그래프 스도쿠 퍼즐

내 직업에서 종종 그렇듯이 이 타이머는 실제보다 훨씬 쉽게 추가할 수 있을 것으로 예상했습니다. Android의 Chronometer 클래스와 Java Timer 클래스를 만지작거리다가 둘 다 서로 다르지만 여전히 애플리케이션을 깨는 문제를 제시했습니다.

결국 나는 한 발 물러서서 내가 Kotlin으로 작성하고 있다는 것을 깨달았습니다. 그래서 저는 프레젠테이션 로직 클래스에서 코루틴 기반 타이머를 설정했습니다(이를 거기에 넣는 것이 가장 합리적이었습니다). 그러면 매초마다 뷰 모델이 업데이트됩니다.

Class ActiveGameLogic(…):…{
//…
inline fun startCoroutineTimer(
    delayMillis: Long = 0,
    repeatMillis: Long = 1000,
    crossinline action: () -> Unit
) = launch {
    delay(delayMillis)
    if (repeatMillis > 0) {
        while (true) {
            action()
            delay(repeatMillis)
        }
    } else {
        action()
    }
}
private fun onStart() =
launch {
    gameRepo.getCurrentGame(
    { puzzle, isComplete ->
        viewModel.initializeBoardState(
            puzzle,
            isComplete
    )
        if (!isComplete) timerTracker = startCoroutineTimer {
            viewModel.updateTimerState()
        }
    },{
        container?.onNewGameClick()
    })
}
//…
}

ViewModel(AAC가 아님 – 내 자신의 VM을 작성합니다. 하지만 Compose는 이미 내가 볼 수 있는 AAC VM과 우수한 상호 운용성을 가지고 있습니다.) 콜백 함수에 대한 참조를 노출했는데, 이것이 내 Composable을 업데이트하는 데 사용할 것입니다.

class ActiveGameViewModel {
    //…
    internal var subTimerState: ((Long) -> Unit)? = null
    internal var timerState: Long = 0L
    //…
    internal fun updateTimerState(){
        timerState++
        subTimerState?.invoke(timerState)
    }
//…
}

이제 중요한 부분이 나옵니다! remember 와 같은 compose의 특정 기능을 사용하여 Compose Hierarchy의 재구성을 트리거할 수 있습니다. 기능:

var timerState by remember {
    mutableStateOf(“”)
}

알고 있어야 하는 경우 이 기능은 기억하고 있는 모든 항목의 상태를 slotTable에 저장합니다. . 간단히 말해서 여기서 상태라는 단어는 데이터의 현재 "상태"를 의미하며, 이는 단순히 빈 문자열로 시작합니다.

여기서 내가 망친 부분이 있습니다. . 내 간단한 타이머를 자체 기능(SOC 적용)으로 가져왔고 timerState 을 전달했습니다. 해당 컴포저블에 대한 매개변수로.

그러나 위의 스니펫은 UI의 가장 복잡한 부분을 위한 컨테이너인 타이머의 구성 가능한 상위 요소에 있었습니다(9x9 Sudoku에는 많은 위젯이 필요함).

@Composable
fun GameContent(
    onEventHandler: (ActiveGameEvent) -> Unit,
    viewModel: ActiveGameViewModel
) {
    Surface(
        Modifier
            .wrapContentHeight()
            .fillMaxWidth()
    ) {
        BoxWithConstraints(Modifier.background(MaterialTheme.colors.primary)) {
            //…
            ConstraintLayout {
                val (board, timer, diff, inputs) = createRefs()
                var isComplete by remember {
                    mutableStateOf(false)
                }
                var timerState by remember {
                    mutableStateOf("")
                }
                viewModel.subTimerState = {
                    timerState = it.toTime()
                }
                viewModel.subIsCompleteState = { isComplete = it }
            //…Sudoku board
            //Timer
                Box(Modifier
                    .wrapContentSize()
                    .constrainAs(timer) {
                        top.linkTo(board.bottom)
                        start.linkTo(parent.start)
                    }
                    .padding(start = 16.dp))
                {
                    TimerText(timerState)
                }
            //…difficulty display
            //…Input buttons
            }
        }
    }
}
@Composable
fun TimerText(timerState: String) {
    Text(
        text = timerState,
        style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary)
    )
}

이로 인해 상당한 지연과 응답 없음이 발생했습니다. 디버거를 많이 사용하면서 그 이유를 알 수 있었습니다. 내 timerState 변수가 부모 Composable 내부에서 생성되고 업데이트되었을 때 UI의 전체 부분에 대한 재구성이 트리거되었습니다. 모든. 하나의. 틱.

적절한 코드를 TimerText 로 이동한 후 구성 가능, 매우 원활하게 작동:

@Composable
fun TimerText(viewModel: ActiveGameViewModel) {
    var timerState by remember {
        mutableStateOf("")
    }

    viewModel.subTimerState = {
        timerState = it.toTime()
    }

    Text(
        text = timerState,
        style = activeGameSubtitle.copy(color = MaterialTheme.colors.secondary)
    )
}

재구성에 대한 실제 이해와 이를 잘못 수행하는 가장 큰 방법 중 하나를 제공했기를 바랍니다.

불필요한 재구성을 피하는 것은 성능에 매우 중요합니다. 그리고 지금까지는 메모리 상태를 별도의 컴포저블에 유지하는 수준까지 SOC를 엄격하게 적용하는 것이 표준 관행이 되어야 할 것 같습니다.

리소스 및 지원

이 기사가 마음에 들면 소셜 미디어에 공유하고 여기에서 freeCodeCamp에 대한 다른 기사를 확인하십시오. 또한 수백 개의 튜토리얼이 있는 YouTube 채널을 운영하고 있으며 다양한 플랫폼에서 활발하게 활동하고 있습니다.

소셜 미디어에서 나와 소통하기

여기 인스타그램과 여기 트위터에서 저를 찾을 수 있습니다.

또한 Jetpack Compose를 시작하는 데 사용한 단일 리소스:우수한 개발자의 작업 코드 샘플을 지적하고 싶습니다.