Computer >> คอมพิวเตอร์ >  >> ระบบ >> Android

บทแนะนำการเขียน Jetpack สำหรับผู้เริ่มต้น – วิธีทำความเข้าใจส่วนประกอบและการจัดองค์ประกอบใหม่

บทแนะนำนี้จะสอนคุณเกี่ยวกับแนวคิดและข้อกำหนดพื้นฐานบางประการที่เกี่ยวข้องกับไลบรารี 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:

บทแนะนำการเขียน Jetpack สำหรับผู้เริ่มต้น – วิธีทำความเข้าใจส่วนประกอบและการจัดองค์ประกอบใหม่
รูปภาพของลำดับชั้นการดู

เครื่องมือดีบักเกอร์และสเต็ปคือวิธีที่ฉันโปรดปรานในการเรียนรู้เกี่ยวกับสิ่งที่เกิดขึ้นภายใต้ประทุนของโค้ดที่ฉันใช้จากไลบรารีต่างๆ ลองดูสักครั้ง!

จุดประสงค์ในการแสดงไฟล์ 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

บทแนะนำการเขียน Jetpack สำหรับผู้เริ่มต้น – วิธีทำความเข้าใจส่วนประกอบและการจัดองค์ประกอบใหม่

ณ จุดนี้ หากคุณต้องการภาพรวมโดยละเอียดของ 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 ซึ่งค่อนข้างเหมาะสำหรับปริศนาซูโดกุ

สิ่งหนึ่งที่ฉันต้องการคือนาฬิกาจับเวลาซึ่งจะคอยติดตามว่าผู้ใช้ใช้เวลานานแค่ไหนในการไขปริศนา:

บทแนะนำการเขียน Jetpack สำหรับผู้เริ่มต้น – วิธีทำความเข้าใจส่วนประกอบและการจัดองค์ประกอบใหม่
ตัวต่อกราฟิกซูโดกุ

ตามปกติในอาชีพของฉัน ฉันคาดว่าตัวจับเวลานี้จะเพิ่มได้ง่ายกว่าที่เป็นจริงมาก ฉันยุ่งกับคลาส 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:ตัวอย่างโค้ดที่ใช้งานได้จากนักพัฒนาที่ดี