บทแนะนำนี้จะสอนคุณเกี่ยวกับแนวคิดและข้อกำหนดพื้นฐานบางประการที่เกี่ยวข้องกับไลบรารี Jetpack Compose UI บน Android
แม้ว่านี่จะเป็นคู่มือสำหรับผู้เริ่มต้นใช้งาน Compose แต่จะไม่ใช่คู่มือสำหรับผู้เริ่มต้นใช้งาน Android ดังนั้นคุณควรสร้างแอปพลิเคชันอย่างน้อยหนึ่งหรือสองรายการ (แต่ไม่จำเป็นต้องอยู่ใน Compose)
ก่อนที่เราจะเริ่ม ตอนแรกฉันวางแผนที่จะเขียนบทความต่อเนื่องที่มุ่งเป้าไปที่นักพัฒนาอาวุโสมากขึ้น จนกระทั่งฉันได้พบกับชุดบทความสองส่วนของ Leland Richardson Leland ไม่ใช่แค่วิศวกรซอฟต์แวร์ที่ทำงานในทีม Jetpack Compose แต่ฉันเห็นว่าเขาเป็นนักเขียนที่ยอดเยี่ยมเช่นกัน
แม้ว่าฉันจะรู้สึกว่าบทความของฉันจะยืนหยัดในตัวเองเพื่อเป็นการแนะนำพื้นฐานของ Jetpack Compose ฉันขอแนะนำอย่างยิ่ง คุณอ่านบทความของเขาเมื่อคุณได้รับประสบการณ์เชิงปฏิบัติเกี่ยวกับ Compose (หรือทันทีหากคุณต้องการเรียนรู้วิธีนั้น)
คำอธิบายข้อกำหนด/แนวคิดหลักในบทความนี้:
- การทบทวนโดยย่อของระบบการดูและลำดับชั้นแบบเก่า
- Composables และวิธีที่พวกมันมีความสัมพันธ์กับ Views
- การจัดองค์ประกอบใหม่และวิธีหลีกเลี่ยงการทำมันได้แย่มาก!
อะไรที่สามารถเขียนได้
ในส่วนนี้ เราจะพูดถึงส่วนพื้นฐานที่สุดของไลบรารี Jetpack Compose หากคุณเป็นนักพัฒนาซอฟต์แวร์ Android ที่ช่ำชอง คุณอาจต้องการข้ามไปยังส่วนย่อยที่ชื่อว่า “Are Composables Views?”
หากคุณยังไม่คุ้นเคยกับระบบ View คุณควรอ่านหัวข้อถัดไปเนื่องจากจำเป็นต้องสร้างแรงจูงใจและทำความเข้าใจว่า Composable คืออะไร
ดูลำดับชั้น
ในบริบทของ Android SDK (ไลบรารีที่เราใช้เพื่อสร้างอินเทอร์เฟซผู้ใช้บนแพลตฟอร์มนี้) มุมมองคือสิ่งที่เราใช้เพื่อให้โครงสร้างและรูปแบบแก่แอปพลิเคชันของเรา
เป็นประเภทการสร้างพื้นฐานหรือองค์ประกอบพื้นฐานของอินเทอร์เฟซผู้ใช้ (UI) ที่กำหนด และแต่ละบล็อคการสร้างเหล่านี้จะมีข้อมูลประเภทต่อไปนี้ (เหนือสิ่งอื่นใด):
- ตำแหน่งเริ่มต้นและสิ้นสุด X และ Y ซึ่งบอกคอมพิวเตอร์ว่าต้องวาดมุมมองบนหน้าจออุปกรณ์ที่ไหน
- ค่าสีและอัลฟา (โปร่งใส)
- ข้อมูลแบบอักษร ข้อความ สัญลักษณ์ และรูปภาพ
- พฤติกรรมตามเหตุการณ์ต่างๆ เช่น การโต้ตอบของผู้ใช้ (การคลิก) หรือการเปลี่ยนแปลงในข้อมูลของแอปพลิเคชัน (เพิ่มเติมในภายหลัง)
สิ่งสำคัญคือต้องเข้าใจว่า การดูสามารถเป็นเหมือนปุ่ม (โดยทั่วไปจะเรียกว่า "วิดเจ็ต") แต่ก็สามารถเป็นคอนเทนเนอร์ของทั้งหน้าจอ ส่วนหนึ่งของหน้าจอ หรือสำหรับ View ย่อยอื่นๆ ได้ .
คอนเทนเนอร์ . ดังกล่าว โดยทั่วไปจะเรียกว่า Layouts หรือ Viewgroups ขึ้นอยู่กับบริบท และในขณะที่แบ่งปันข้อมูลประเภทเดียวกันกับวิดเจ็ตส่วนใหญ่ พวกเขายังมีข้อมูลเกี่ยวกับวิธีการจัดเรียงและแสดงมุมมองอื่นๆ ที่ ซ้อน ในตัวมันเอง
ด้วยเหตุนี้ เราจึงมาถึงส่วนสำคัญของการตรวจสอบระบบมุมมองนี้:ลำดับชั้นการดู . สำหรับนักพัฒนาเว็บ ลำดับชั้นการดูคือเวอร์ชันของ Document Object Model (DOM) ของ Android
สำหรับนักพัฒนา Android คุณสามารถมองว่าลำดับชั้นการดูเป็นการแสดงเสมือนของมุมมองทั้งหมดที่คุณกำหนดไว้ในไฟล์ XML หรือโดยทางโปรแกรมใน Java หรือ Kotlin
เพื่อแสดงให้เห็นสิ่งนี้ ให้ดูที่ไฟล์ XML ดังกล่าว (ไม่จำเป็นต้องศึกษาอย่างละเอียด เพียงจดชื่อไว้) จากนั้น ใช้ดีบักเกอร์/เครื่องมือ stepper เราจะดูว่ามันเป็นอย่างไรในพื้นที่หน่วยความจำของ Fragment ซึ่งขยายไฟล์นี้:
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:
เครื่องมือดีบักเกอร์และสเต็ปคือวิธีที่ฉันโปรดปรานในการเรียนรู้เกี่ยวกับสิ่งที่เกิดขึ้นภายใต้ประทุนของโค้ดที่ฉันใช้จากไลบรารีต่างๆ ลองดูสักครั้ง!
จุดประสงค์ในการแสดงไฟล์ XML นี้และสิ่งที่จะเปลี่ยนเป็น กระบวนการ (กระบวนการคือ เป็นเพียงโปรแกรมที่กำลังทำงานอยู่ บนอุปกรณ์) คือการแสดงวิธีที่ Views ที่ซ้อนกันในไฟล์ XML แปลเป็น View Hierarchy ที่ซ้อนกัน ณ รันไทม์
หวังว่าด้วยรูปแบบที่เรียบง่ายแต่เป็นรูปธรรมเกี่ยวกับวิธีการทำงานของระบบเก่า เราจะสามารถเปรียบเทียบกับรูปแบบใหม่ได้
เป็น Composables Views หรือไม่
นี่เป็นหนึ่งในคำถามแรกๆ ที่ฉันถามเมื่อเริ่มทำงานกับ Compose และคำตอบที่ฉันได้รับคือ ใช่ และ ไม่ .
ใช่ ในแง่ที่ว่า Composable ตอบสนอง บทบาทแนวคิดเดียวกันกับมุมมอง ในระบบเก่า Composable อาจเป็นวิดเจ็ต เช่น ปุ่ม หรือคอนเทนเนอร์ เช่น ConstraintLayout (เป็นที่น่าสังเกตว่ามีการนำ ConstraintLayout ไปใช้แบบ Composable)
ไม่ ในแง่ที่ว่า UI ไม่ได้แสดงอยู่ในลำดับชั้นการดูอีกต่อไป (นอกเหนือจากสถานการณ์ที่เกี่ยวข้องกับการทำงานร่วมกัน) อย่างที่กล่าวไปแล้ว การเขียนไม่ได้ใช้เวทมนตร์ในการแสดงและติดตาม UI แบบเสมือนจริง ซึ่งหมายความว่าต้องมีสิ่งที่เป็นของตัวเองซึ่งมีแนวคิดคล้ายกับลำดับชั้นการดู
เรามาดูสิ่งนี้กันโดยย่อ ที่นี่เรามีกิจกรรมที่ใช้ 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
}
}
}
}
ในการเขียน ลำดับชั้นการดูจะถูกแทนที่ด้วยสิ่งที่เราสามารถระบุได้หากเราเจาะลึกลงไปใน mWindow ฟิลด์ของกิจกรรมนี้ ภายในฟิลด์นั้นคือการแทนที่แนวคิดของลำดับชั้นการดู:The Composer
และของมัน slotTable
ณ จุดนี้ หากคุณต้องการภาพรวมโดยละเอียดของ Composer
และ slotTable
ฉันต้องแนะนำอีกครั้งให้คุณอ่านบทความของ Leland (เขาลงรายละเอียดในส่วนที่ 2) ลำดับชั้นการเขียนมีมากกว่า Composer และ slotTable ของมัน แต่นั่นก็เพียงพอแล้วที่จะให้เราเริ่มต้นได้
โดยทั่วไป Jetpack Compose ใช้สิ่งที่เราอาจเรียกว่า Compose Hierarchy (ซึ่งสร้างและจัดการโดยสิ่งต่างๆ เช่น Composer และ slotTable)
อีกครั้ง นี่เป็นแนวคิดเดียวกันกับลำดับชั้นการดู ซึ่งเป็นกลุ่มของออบเจ็กต์ในพื้นที่หน่วยความจำซึ่งเป็นตัวแทนของ UI โดยรวม แต่มีการใช้งานแตกต่างกันมาก
อย่างไรก็ตาม มีความแตกต่างที่สำคัญซึ่งเป็นเรื่องยากที่จะเข้าใจในทางเทคนิค แต่โดยหลักการแล้วเข้าใจง่าย นี่คือวิธีที่ Compose จัดการกับการอัปเดตลำดับชั้นการเขียน:การจัดองค์ประกอบใหม่ .
การจัดองค์ประกอบใหม่:วิธีอัปเดต UI การเขียน
สำหรับเพื่อน ESL ของฉัน คำว่า Compose มาจากภาษาละติน componere ซึ่งหมายถึง "การรวมเข้าด้วยกัน" อย่างคร่าวๆ ผู้ที่แต่งเพลงมักถูกเรียกว่า "ผู้แต่ง" ซึ่งถือได้ว่าเป็นผู้รวบรวมโน้ตที่มาจากเครื่องดนตรีอย่างน้อยหนึ่งชิ้นมาประกอบเป็นเพลง (เพลง)
ประกอบกันก็แสดงว่ามีชิ้นเดียว สิ่งสำคัญคือต้องเข้าใจว่านักพัฒนาซอฟต์แวร์ที่ดีเกือบทุกคนพยายามแบ่งโค้ดออกเป็นส่วนที่เล็กที่สุดที่เหมาะสมที่สุด .
ฉันพูดถึงสมเหตุสมผล เพราะฉันคิดว่าหลักการอย่าง DRY (Don't Repeat Yourself) ควรปฏิบัติตามเฉพาะในขอบเขตที่แก้ปัญหาได้มากกว่าที่สร้างไว้
การนำแนวคิดนี้ไปใช้มีประโยชน์มากมาย ซึ่งมักเรียกว่าโมดูลาร์ (หรือตามที่ฉันชอบ การแยกข้อกังวล หรือ SOC) ฉันทราบดีว่าพวกคุณบางคนที่อ่านข้อความนี้อาจคิดว่าฉันแค่ลอกเลียนสิ่งที่ Leland พูดในบทความของเขา แต่ฉันพูดถึง SOC เป็นหลักการทองของสถาปัตยกรรมซอฟต์แวร์มาหลายปีแล้ว
ที่สิ่งนี้เล่นเป็น Compose เป็นหลักการเดียวกับที่เราเห็นในไลบรารี Javascript ยอดนิยม React . เมื่อทำอย่างถูกต้อง Compose จะ "recompose" เท่านั้น (วาดใหม่ แสดงใหม่ อัปเดต หรืออะไรก็ตาม) Composables (ส่วน/องค์ประกอบของ UI) ที่จำเป็นต้องจัดองค์ประกอบใหม่
นี่เป็นสิ่งสำคัญอย่างมากเมื่อพูดถึงประสิทธิภาพของแอปพลิเคชัน ทั้งนี้เนื่องจากการวาด UI ใหม่ไม่ว่าจะอยู่ในระบบ View แบบเก่าหรือใน Compose นั้นมีค่าใช้จ่ายสูงสำหรับทรัพยากรระบบ
ในกรณีที่คุณไม่ทราบ จุดประสงค์ทั้งหมดของ RecyclerView แบบเก่า (ซึ่งเป็นสิ่งแรกที่ฉันทำการสอนในปี 2016!) คือการใช้รูปแบบ ViewHolder กับรายการข้อมูล วิธีนี้ทำให้ไม่ต้องขยาย (สร้าง) มุมมองใหม่สำหรับแต่ละรายการอย่างต่อเนื่อง
เป้าหมายของฉันในบทความนี้คือการเน้นไปที่ทฤษฎีเป็นส่วนใหญ่ เนื่องจากฉันจะเขียนเนื้อหาเชิงปฏิบัติมากมายในอีกไม่กี่เดือนข้างหน้านี้ อย่างไรก็ตาม ฉันจะปิดท้ายบทความด้วยเรื่องราวจากประสบการณ์ตรงของฉัน ซึ่งจะช่วยให้คุณเข้าใจมากขึ้นว่าการจัดองค์ประกอบใหม่เป็นอย่างไร และ วิธีหลีกเลี่ยงการทำมันได้แย่มาก!
ตัวอย่างนาฬิกาจับเวลา
สำหรับแอปพลิเคชัน Compose เต็มรูปแบบครั้งแรกของฉัน ฉันตัดสินใจสร้าง Sudoku มีสาเหตุหลายประการ รวมถึงความจริงที่ว่าฉันต้องการโครงการที่ไม่มี UI ที่ซับซ้อนอย่างบ้าคลั่ง ฉันยังต้องการโอกาสที่จะเจาะลึกลงไปใน Graph DS และ Algos ซึ่งค่อนข้างเหมาะสำหรับปริศนาซูโดกุ
สิ่งหนึ่งที่ฉันต้องการคือนาฬิกาจับเวลาซึ่งจะคอยติดตามว่าผู้ใช้ใช้เวลานานแค่ไหนในการไขปริศนา:
ตามปกติในอาชีพของฉัน ฉันคาดว่าตัวจับเวลานี้จะเพิ่มได้ง่ายกว่าที่เป็นจริงมาก ฉันยุ่งกับคลาส Chronometer ของ Android และคลาส Java Timer และทั้งคู่ก็นำเสนอปัญหาที่แตกต่างกันแต่ยังคงใช้งานแอปพลิเคชันไม่ได้
ในที่สุดฉันก็ถอยออกมาและตระหนักว่าฉันกำลังเขียนภาษา Kotlin ดังนั้นฉันจึงตั้งค่าตัวจับเวลาที่ใช้ Coroutine ในคลาสตรรกะการนำเสนอของฉัน (มันสมเหตุสมผลที่สุดที่จะวางไว้ที่นั่น) ซึ่งจะอัปเดต 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 - ฉันเขียน VM ของตัวเอง แต่ Compose มีความสามารถในการทำงานร่วมกันได้ดีกับ AAC VM จากสิ่งที่ฉันเห็น) การอ้างอิงถึงฟังก์ชันการโทรกลับซึ่งเป็นสิ่งที่ฉันจะใช้ในการอัปเดต Composables ของฉัน:
class ActiveGameViewModel {
//…
internal var subTimerState: ((Long) -> Unit)? = null
internal var timerState: Long = 0L
//…
internal fun updateTimerState(){
timerState++
subTimerState?.invoke(timerState)
}
//…
}
ส่วนสำคัญมาถึงแล้ว! เราสามารถเรียกการจัดลำดับชั้นการเขียนใหม่ได้โดยใช้คุณลักษณะบางอย่างของการเขียน เช่น remember
ฟังก์ชัน:
var timerState by remember {
mutableStateOf(“”)
}
หากคุณต้องรู้ คุณลักษณะเหล่านี้จะเก็บสถานะของสิ่งที่คุณกำลังจำได้ใน slotTable
. กล่าวโดยย่อ คำว่า state ในที่นี้หมายถึง "สถานะ" ปัจจุบันของข้อมูล ซึ่งเริ่มต้นจากการเป็นเพียงสตริงว่าง
นี่คือสิ่งที่ฉันทำพลาดไป . ฉันได้ดึงตัวจับเวลาแบบง่ายของฉันที่ประกอบเป็นฟังก์ชันของตัวเองได้ (ใช้ SOC) และฉันกำลังส่งผ่าน timerState
เป็นพารามิเตอร์ที่คอมไพล์ได้นั้น
อย่างไรก็ตาม ตัวอย่างข้างต้นอยู่ในพาเรนต์ที่ประกอบได้ของตัวจับเวลา ซึ่งเป็นคอนเทนเนอร์สำหรับส่วนที่ซับซ้อนที่สุดของ UI (ซูโดกุ 9x9 ต้องใช้วิดเจ็ตจำนวนมาก):
@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 ที่มีบทแนะนำหลายร้อยรายการ และเป็นนักเขียนที่กระตือรือร้นบนแพลตฟอร์มต่างๆ
เชื่อมต่อกับฉันทางโซเชียลมีเดีย
คุณสามารถพบฉันบน Instagram ที่นี่และบน Twitter ที่นี่
นอกจากนี้ ฉันต้องการจะชี้ให้เห็นถึงแหล่งข้อมูลเดียวที่ฉันใช้ในการเริ่มต้นกับ Jetpack Compose:ตัวอย่างโค้ดที่ใช้งานได้จากนักพัฒนาที่ดี