Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เชี่ยวชาญพื้นฐาน Jetpack Compose: คู่มือสำหรับผู้เริ่มต้นเกี่ยวกับ Composables และ Recomposition

บทช่วยสอนนี้สอน概念พื้นฐานและ терมที่เกี่ยวข้องกับ Jetpack Compose UI Library บน Android

แม้จะเป็น beginner's guide สำหรับ Compose แต่ไม่ใช่สำหรับ Android เรื่องทั่วไป ควรเคยสร้างแอปอย่างน้อย 1-2 ตัว (ไม่จำเป็นต้องเป็น Compose)

ก่อนเริ่ม ผมวางแผนเขียนบทความต่อสำหรับ senior developers จนเจอ series 2 ส่วนของ Leland Richardson Leland ไม่ได้แค่ Software Engineer ทีม Jetpack Compose แต่ยังเป็นนักเขียนที่เก่ง

บทความนี้ standalone เป็น intro สำหรับ Compose basics แต ขอแนะนำอย่างยิ่ง ให้อ่านบทความของเขาหลังได้ประสบการณ์กับ Compose (หรือทันทีถ้าชอบเรียนแบบนั้น)

Key Terms/Concepts ในบทความนี้:

  • ทบทวน View System และ Hierarchy แบบเก่า
  • Composables และความสัมพันธ์กับ Views
  • Recomposition และหลีกเลี่ยงการทำมันอย่างแย่!

Composable คืออะไร?

ส่วนนี้พูดถึงส่วนพื้นฐานที่สุดของ Jetpack Compose library นักพัฒนา Android มาก่อนอาจข้ามไปส่วน "Are Composables Views?"

หากไม่คุ้นเคย View system อ่านส่วนต่อไปจำเป็นเพื่อเข้าใจ Composable

View Hierarchy

ในบริบท Android SDK (libraries สร้าง UI) View คือสิ่งที่ให้โครงสร้างและสไตล์กับแอป

เป็น building block พื้นฐานของ UI แต่ละ block มีข้อมูล:

  • ตำแหน่ง X และ Y เริ่มต้นและสิ้นสุด บอกคอมพิวเตอร์ว่าจะวาด view ที่ไหนบนหน้าจอ
  • ค่าสีและ alpha (transparency)
  • ข้อมูล font, text, symbols, images
  • พฤติกรรมจาก events เช่น user interaction (clicks) หรือการเปลี่ยนแปลงข้อมูลแอป

สำคัญ: View อาจเป็น button (widget) แต่อาจเป็น container ของหน้าจอทั้งหมด ส่วนหน้าจอ หรือ child Views อื่นๆ

Containers เรียกว่า Layouts หรือ Viewgroups ขึ้นกับบริบท แชร์ข้อมูลเหมือน widget แต่มีข้อมูลจัดเรียงและแสดง child Views ที่ nested ไว้

มาถึงส่วนสำคัญ: View Hierarchy สำหรับ Web Developers View Hierarchy คือ Android version ของ Document Object Model (DOM)

สำหรับ Android Developers View Hierarchy คือ virtual representation ของ Views ที่ define ใน XML หรือ programmatically ใน Java/Kotlin

ดู XML file (ไม่ต้องศึกษาใกล้ชิด แค่ดูชื่อ) แล้วใช้ debugger/stepper tool ดูใน memory space ของ Fragment ที่ inflate ไฟล์นี้:

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>

Memory Space of (Fragment)HourView.kt:

เชี่ยวชาญพื้นฐาน Jetpack Compose: คู่มือสำหรับผู้เริ่มต้นเกี่ยวกับ Composables และ Recomposition
ภาพ View Hierarchy

Debugger และ stepper tools เป็นวิธีโปรดเรียนรู้ภายใต้ฮู้ดของโค้ดจาก libraries ต่างๆ ลองทำดู!

จุดประสงค์แสดง XML และ runtime representation เพื่อ demonstrate nested Views ใน XML แปลงเป็น nested View Hierarchy ตอน runtime

หวังว่า model เรียบง่ายนี้ช่วยเปรียบเทียบระบบเก่าและใหม่

Composables คือ Views หรือไม่?

คำถามแรกตอนเริ่ม Compose คำตอบคือ ใช่ และ ไม่

ใช่ ในความหมายว่า Composable เล่น conceptual role เหมือน View ในระบบเก่า อาจเป็น widget เช่น button หรือ container เช่น ConstraintLayout (มี Composable implementation ของ ConstraintLayout)

ไม่ ในความหมาย UI ไม่ได้ represented virtually ใน View Hierarchy (ยกเว้น interoperability) แต่ Compose ไม่ใช้ magic ต้องมีของตัวเองที่คล้าย View Hierarchy

ดูสั้นๆ: Activity ใช้ setContent {…} bind 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 field ของ Activity ภายในมี conceptual replacement: The Composer และ slotTable.

เชี่ยวชาญพื้นฐาน Jetpack Compose: คู่มือสำหรับผู้เริ่มต้นเกี่ยวกับ Composables และ Recomposition

หากต้องการ overview ลึกของ Composer และ slotTable อ่านบทความของ Leland (part 2) มีมากกว่านี้แต่พอสำหรับเริ่มต้น

โดยทั่วไป Jetpack Compose ใช้ Compose Hierarchy (ประกอบและจัดการโดย Composer และ slotTable)

แนวคิดเหมือน View hierarchy แต่ implement แตกต่างมาก

ความแตกต่างสำคัญ: วิธี Compose จัดการ updates ของ Compose Hierarchy: Recomposition

Recomposition: อัปเดต Compose UI

สำหรับเพื่อน ESL คำว่า Compose มาจาก latin componere ประมาณ "put together" ผู้แต่งเพลงเรียกว่า "Composer" คือคนรวมโน้ตจากเครื่องดนตรีหนึ่งหรือหลายเครื่องเป็น composition (เพลง)

"Put together" บ่งชี้ว่ามีชิ้นส่วนแยกกัน นักพัฒนาที่ดีพยายามแยกโค้ดเป็น smallest reasonable parts

กล่าว reasonable เพราะคิดว่า principles เช่น DRY (Don't Repeat Yourself) ควรปฏิบัติแค่แก้ปัญหามากกว่าสร้างปัญหา

มีประโยชน์มากจากการแยกส่วน (modularity หรือ Separation of Concerns - SOC) รู้ว่าบางคนคิดว่าผมก็อปจาก Leland แต่ผมพูดถึง SOC เป็น Golden Principle Of Software Architecture มาหลายปีแล้ว

ที่ Compose เล่นร่วมกับ principle เดียวกับ React: เมื่อทำถูกต้อง Compose จะ "recompose" (redraw, re-render, update) เฉพาะ Composables ที่จำเป็น

สำคัญมากต่อ performance เพราะ redraw UI ทั้งในระบบเก่าและ Compose ใช้ resources มาก

RecyclerView (บทช่วยสอนแรกของผม 2016!) ใช้ ViewHolder pattern สำหรับ list data หลีกเลี่ยงการ inflate Views ใหม่สำหรับแต่ละ item

เป้าหมายเน้น theory จะเขียน practical content เดือนหน้า แต่จบด้วย story จากประสบการณ์ตรง ช่วยเข้าใจ recomposition และ หลีกเลี่ยงการทำมันอย่างแย่!

ตัวอย่าง Stopwatch

แอป Compose แรกของผมคือ Sudoku เหตุผลหลายอย่าง รวมถึงอยากโปรเจกต์ที่ UI ไม่ง่ายเกินไป และ deep dive Graph DS และ Algos ที่เหมาะกับ Sudoku

อยากมี Stopwatch นับเวลาแก้ปริศนา:

เชี่ยวชาญพื้นฐาน Jetpack Compose: คู่มือสำหรับผู้เริ่มต้นเกี่ยวกับ Composables และ Recomposition
Graph Sudoku puzzle

คิดว่าจะง่าย แต่จริงๆ ยาก มุ่งมั่นกับ Chronometer และ Java Timer ทั้งคู่มีปัญหาแตกต่างแต่ทำให้แอพพัง

ถอยหลังคิดว่าเขียน Kotlin จึงตั้ง Coroutine timer ใน presentation logic class (เหมาะสมที่สุด) อัปเดต viewmodel ทุกวินาที:

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 - เขียนเอง แต่ Compose มี interoperability ดีกับ AAC VMs) expose references ของ callback functions เพื่ออัปเดต Composables:

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

ส่วนสำคัญ! ทริกเกอร์ recomposition ของ Compose Hierarchy ด้วย features เช่น remember:

var timerState by remember { mutableStateOf("") }

features นี้เก็บ state ใน slotTable state ที่นี่หมายถึง "state" ปัจจุบันของข้อมูล เริ่มต้นเป็น empty String

ที่นี่ผมพลาด ดึง Timer composable ออกมาเป็น function แยก (apply SOC) ส่ง timerState เป็น parameter

แต่ snippets ด้านบนอยู่ใน parent composable ของ timer ซึ่งเป็น container สำหรับ UI สับซ้อนที่สุด (9x9 Sudoku ต้องใช้ widgets เยอะ):

@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) ) }

ทำให้ lag และ unresponsive ด้วย debugger หาสาเหตุ: เพราะ timerState สร้างและอัปเดตใน parent Composable ทริกเกอร์ recomposition ทั้งส่วนนั้น ทุกๆ tick

หลังย้ายโค้ดที่เกี่ยวข้องไปใน TimerText composable ทำงานลื่นไหล:

@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) ) }

หวังว่าให้ working understanding ของ recomposition และหนึ่งในวิธีทำผิดที่สุด

หลีกเลี่ยง unnecessary recompositions สำคัญมากต่อ performance จนถึงตอนนี้ การ apply SOC อย่างเข้มงวด จนระดับเก็บ remember state ใน composables แยกกัน ควรเป็น standard practice

Resources & Support

ถ้าชอบบทความนี้ แชร์บนโซเชียลมีเดีย และดูบทความอื่นๆ ของผมบน freeCodeCamp ที่นี่ มี YouTube channel หลายร้อย tutorials และเป็นนักเขียน active บนหลาย platform

ติดต่อผมบนโซเชียลมีเดีย

หาผมได้ที่ Instagram และ Twitter

ชี้ให้เห็น single resource ที่ใช้เริ่มต้น Jetpack Compose: Working code samples from good developers.