การวางเลเยอร์สถาปัตยกรรมของ Jetpack Compose

หน้านี้แสดงภาพรวมระดับสูงของเลเยอร์สถาปัตยกรรมที่ประกอบขึ้นเป็น Jetpack Compose และหลักการสำคัญที่ใช้ในการออกแบบนี้

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

  • ใช้ระดับนามธรรมที่เหมาะสมเพื่อสร้างแอปหรือไลบรารี
  • ทำความเข้าใจว่าเมื่อใดที่คุณสามารถ "ลดระดับ" ไปยังระดับที่ต่ำลงเพื่อการควบคุมหรือการปรับแต่งที่มากขึ้น
  • ลดทรัพยากร Dependency

เลเยอร์

เลเยอร์หลักของ Jetpack Compose มีดังนี้

รูปที่ 1 เลเยอร์หลักของ Jetpack Compose

แต่ละเลเยอร์สร้างขึ้นจากระดับที่ต่ำกว่า โดยรวมฟังก์ชันการทำงานเพื่อสร้างคอมโพเนนต์ระดับที่สูงขึ้น แต่ละเลเยอร์จะสร้างขึ้นบน API สาธารณะของเลเยอร์ที่ต่ำกว่าเพื่อยืนยันขอบเขตของโมดูล และช่วยให้คุณแทนที่เลเยอร์ใดก็ได้หากจำเป็น มาดูเลเยอร์เหล่านี้จากล่างขึ้นบนกัน

รันไทม์
โมดูลนี้มีพื้นฐานของรันไทม์ Compose เช่น remember, mutableStateOf, คำอธิบายประกอบ @Composable และ SideEffect คุณอาจพิจารณาสร้างโดยตรงบนเลเยอร์นี้หากต้องการเพียงความสามารถในการจัดการต้นไม้ของ Compose ไม่ใช่ UI
UI
เลเยอร์ UI ประกอบด้วยหลายโมดูล ( ui-text, ui-graphics, ui-tooling ฯลฯ) โมดูลเหล่านี้ใช้พื้นฐานของชุดเครื่องมือ UI เช่น LayoutNode, Modifier, ตัวแฮนเดิลอินพุต, เลย์เอาต์ที่กำหนดเอง และการวาด คุณอาจพิจารณาสร้างต่อยอดจากเลเยอร์นี้หากต้องการเพียงแนวคิดพื้นฐานของชุดเครื่องมือ UI เท่านั้น
Foundation
โมดูลนี้มีองค์ประกอบพื้นฐานที่ไม่ขึ้นอยู่กับระบบการออกแบบสำหรับ Compose UI เช่น Row และ Column, LazyColumn, การจดจำท่าทางสัมผัสบางอย่าง เป็นต้น คุณอาจพิจารณาสร้างระบบการออกแบบของคุณเองโดยต่อยอดจากเลเยอร์พื้นฐาน
วัสดุ
โมดูลนี้ใช้ระบบการออกแบบ Material Design สำหรับ Compose UI โดยมีระบบการกำหนดธีม คอมโพเนนต์ที่มีสไตล์ การแสดงผลริปเปิล และไอคอน สร้างต่อยอดจากเลเยอร์นี้เมื่อใช้ Material Design ในแอป

หลักการออกแบบ

หลักการชี้นำสำหรับ Jetpack Compose คือการจัดเตรียมฟังก์ชันการทำงานขนาดเล็กที่มุ่งเน้นซึ่งสามารถประกอบ (หรือเขียน) เข้าด้วยกัน แทนที่จะเป็นคอมโพเนนต์แบบโมโนลิธิกเพียงไม่กี่รายการ แนวทางนี้มีข้อดีหลายประการ

ควบคุม

คอมโพเนนต์ระดับสูงมักจะทำงานให้คุณได้มากกว่า แต่จะจำกัดปริมาณการควบคุมโดยตรงที่คุณมี หากต้องการควบคุมมากขึ้น คุณสามารถ "เลื่อนลง" เพื่อใช้คอมโพเนนต์ระดับต่ำกว่าได้

เช่น หากต้องการสร้างภาพเคลื่อนไหวให้กับสีของคอมโพเนนต์ คุณอาจใช้ API animateColorAsState ดังนี้

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

อย่างไรก็ตาม หากคุณต้องการให้คอมโพเนนต์เริ่มต้นเป็นสีเทาเสมอ คุณจะทำไม่ได้ด้วย API นี้ แต่คุณสามารถใช้ API ระดับต่ำกว่าแทนได้ ดังนี้ Animatable

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

API ระดับสูงกว่า animateColorAsState สร้างขึ้นจาก API ระดับต่ำกว่า Animatable การใช้ API ระดับต่ำกว่าจะซับซ้อนกว่าแต่ให้การควบคุมมากกว่า เลือกระดับของนามธรรมที่เหมาะกับความต้องการของคุณมากที่สุด

การปรับแต่ง

การประกอบคอมโพเนนต์ระดับสูงขึ้นจากองค์ประกอบที่เล็กลงจะช่วยให้ปรับแต่งคอมโพเนนต์ได้ง่ายขึ้นมากหากจำเป็น ตัวอย่างเช่น ลองพิจารณาการใช้งาน Button ที่เลเยอร์ Material มีให้

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Button ประกอบขึ้นจาก 4 คอมโพเนนต์ ได้แก่

  1. วัสดุ Surface ที่ให้พื้นหลัง รูปร่าง การจัดการการคลิก ฯลฯ

  2. CompositionLocalProvider ซึ่งจะเปลี่ยนอัลฟาของเนื้อหาเมื่อเปิดหรือปิดใช้ปุ่ม

  3. ProvideTextStyle จะตั้งค่ารูปแบบข้อความเริ่มต้นที่จะใช้

  4. Row มีนโยบายเลย์เอาต์เริ่มต้นสำหรับเนื้อหาของปุ่ม

เราได้ละเว้นพารามิเตอร์และความคิดเห็นบางส่วนเพื่อให้โครงสร้างชัดเจนขึ้น แต่คอมโพเนนต์ทั้งหมดมีโค้ดเพียงประมาณ 40 บรรทัดเท่านั้น เนื่องจากเป็นเพียงการประกอบคอมโพเนนต์ 4 รายการนี้เพื่อใช้ปุ่ม คอมโพเนนต์อย่าง Button มีแนวคิดที่ชัดเจนเกี่ยวกับพารามิเตอร์ที่แสดง โดยสร้างสมดุลระหว่างการเปิดใช้การปรับแต่งทั่วไปกับการเพิ่มขึ้นอย่างรวดเร็วของพารามิเตอร์ที่อาจทำให้คอมโพเนนต์ใช้งานยากขึ้น ตัวอย่างเช่น คอมโพเนนต์ Material มีการปรับแต่งที่ระบุไว้ในระบบการออกแบบ Material Design ซึ่งช่วยให้ปฏิบัติตามหลักการของ Material Design ได้ง่าย

อย่างไรก็ตาม หากต้องการปรับแต่งนอกเหนือจากพารามิเตอร์ของคอมโพเนนต์ คุณสามารถ "เลื่อนลง" ไปยังระดับหนึ่งและแยกคอมโพเนนต์ได้ เช่น Material Design ระบุว่าปุ่มควรมีพื้นหลังสีทึบ หากต้องการพื้นหลังแบบไล่ระดับสี พารามิเตอร์ Button จะไม่รองรับตัวเลือกนี้ ในกรณีนี้ คุณสามารถใช้การติดตั้งใช้งาน Material Button เป็นข้อมูลอ้างอิงและสร้างคอมโพเนนต์ของคุณเองได้ ดังนี้

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

การติดตั้งใช้งานข้างต้นยังคงใช้คอมโพเนนต์จากเลเยอร์ Material เช่น แนวคิดของ Material เกี่ยวกับอัลฟ่าของเนื้อหาปัจจุบันและรูปแบบข้อความปัจจุบัน อย่างไรก็ตาม ระบบจะแทนที่ Surface ของวัสดุด้วย Row และจัดรูปแบบเพื่อให้ได้รูปลักษณ์ที่ต้องการ

หากไม่ต้องการใช้แนวคิด Material เลย เช่น หากสร้างระบบการออกแบบที่กำหนดเอง คุณสามารถเลือกใช้เฉพาะคอมโพเนนต์เลเยอร์พื้นฐานได้ ดังนี้

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose จะสงวนชื่อที่ง่ายที่สุดไว้สำหรับคอมโพเนนต์ระดับสูงสุด ตัวอย่างเช่น androidx.compose.material.Text สร้างขึ้นจาก androidx.compose.foundation.text.BasicText ซึ่งจะช่วยให้คุณสามารถระบุการติดตั้งใช้งานของคุณเองด้วยชื่อที่ค้นพบได้มากที่สุด หากต้องการแทนที่ระดับที่สูงขึ้น

การเลือก Abstraction ที่เหมาะสม

ปรัชญาของ Compose ในการสร้างคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้แบบเป็นเลเยอร์หมายความว่าคุณไม่ควรใช้ Building Block ระดับต่ำเสมอไป คอมโพเนนต์ระดับสูงจำนวนมากไม่เพียงแต่มีฟังก์ชันการทำงานที่มากกว่าเท่านั้น แต่ยังมักใช้แนวทางปฏิบัติแนะนำ เช่น การรองรับการช่วยเหลือพิเศษด้วย

ตัวอย่างเช่น หากต้องการเพิ่มการรองรับท่าทางสัมผัสลงในคอมโพเนนต์ที่กำหนดเอง คุณสามารถสร้างคอมโพเนนต์นี้ตั้งแต่ต้นโดยใช้ Modifier.pointerInput แต่ก็มีคอมโพเนนต์ระดับสูงอื่นๆ ที่สร้างขึ้นจากคอมโพเนนต์นี้ซึ่งอาจเป็นจุดเริ่มต้นที่ดีกว่า เช่น Modifier.draggable, Modifier.scrollable หรือ Modifier.swipeable

ตามกฎแล้ว คุณควรสร้างบนคอมโพเนนต์ระดับสูงสุดซึ่งมีฟังก์ชันการทำงานที่คุณต้องการ เพื่อให้ได้รับประโยชน์จากแนวทางปฏิบัติแนะนำที่รวมอยู่ด้วย

ดูข้อมูลเพิ่มเติม

ดูตัวอย่าง Jetsnack เพื่อดูตัวอย่างการสร้างระบบการออกแบบที่กำหนดเอง