คำแนะนำสำหรับสถาปัตยกรรม Android

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

แนวทางปฏิบัติแนะนำด้านล่างจะจัดกลุ่มตามหัวข้อ แต่ละรายการจะมีลําดับความสําคัญที่แสดงถึงระดับการแนะนําของทีม รายการลำดับความสำคัญมีดังต่อไปนี้

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

สถาปัตยกรรมแบบเป็นชั้น

สถาปัตยกรรมแบบเป็นชั้นที่เราแนะนำจะแยกข้อกังวลออกจากกัน โดยขับเคลื่อน UI จากโมเดลข้อมูล เป็นไปตามหลักการแหล่งข้อมูลที่ถูกต้องแห่งเดียว และเป็นไปตามหลักการการไหลของข้อมูลแบบทิศทางเดียว แนวทางปฏิบัติแนะนำเกี่ยวกับ สถาปัตยกรรมแบบเลเยอร์มีดังนี้

คำแนะนำ คำอธิบาย
ใช้ชั้นข้อมูลที่กําหนดไว้อย่างชัดเจน ชั้นข้อมูลจะเปิดเผยข้อมูลแอปพลิเคชันส่วนที่เหลือของแอปและประกอบด้วยตรรกะทางธุรกิจส่วนใหญ่ของแอป
  • คุณควรสร้างที่เก็บแม้ว่าจะมีเพียงแหล่งข้อมูลเดียว
  • ในแอปขนาดเล็ก คุณจะเลือกวางประเภทชั้นข้อมูลในแพ็กเกจหรือโมดูล data ได้
ใช้เลเยอร์ UI ที่กําหนดไว้อย่างชัดเจน เลเยอร์ UI จะแสดงข้อมูลแอปพลิเคชันบนหน้าจอและใช้เป็นจุดหลักในการโต้ตอบกับผู้ใช้
  • ในแอปขนาดเล็ก คุณจะเลือกวางเลเยอร์ข้อมูลประเภทต่างๆ ในuiแพ็กเกจหรือโมดูลได้
ดูแนวทางปฏิบัติแนะนำเพิ่มเติมสำหรับเลเยอร์ UI ได้ที่นี่
ชั้นข้อมูลควรแสดงข้อมูลแอปพลิเคชันโดยใช้ที่เก็บข้อมูล

คอมโพเนนต์ในเลเยอร์ UI เช่น Composable, กิจกรรม หรือ ViewModel ไม่ควรโต้ตอบกับแหล่งข้อมูลโดยตรง ตัวอย่างแหล่งข้อมูล ได้แก่

  • ฐานข้อมูล, Datastore, SharedPreferences, Firebase API
  • ผู้ให้บริการตำแหน่ง GPS
  • ผู้ให้บริการข้อมูลบลูทูธ
  • ผู้ให้บริการสถานะการเชื่อมต่อเครือข่าย
ใช้ Coroutine และ Flow ใช้โครูทีนและโฟลว์เพื่อสื่อสารระหว่างเลเยอร์

ดูแนวทางปฏิบัติแนะนำเพิ่มเติมเกี่ยวกับโคโริวทีนได้ที่นี่

ใช้เลเยอร์โดเมน ใช้เลเยอร์โดเมน กรณีการใช้งาน หากคุณต้องการนําตรรกะทางธุรกิจที่โต้ตอบกับเลเยอร์ข้อมูลใน ViewModel หลายรายการมาใช้ซ้ำ หรือต้องการลดความซับซ้อนของตรรกะทางธุรกิจของ ViewModel บางรายการ

เลเยอร์ UI

บทบาทของเลเยอร์ UI คือการแสดงข้อมูลแอปพลิเคชันบนหน้าจอ และใช้เป็นจุดหลักในการโต้ตอบกับผู้ใช้ แนวทางปฏิบัติแนะนำสำหรับเลเยอร์ UI มีดังนี้

คำแนะนำ คำอธิบาย
ทำตามการไหลของข้อมูลแบบทิศทางเดียว (UDF) ทำตามหลักการ Unidirectional Data Flow (UDF) โดย ViewModels แสดงสถานะ UI โดยใช้รูปแบบผู้สังเกตการณ์และรับการดำเนินการจาก UI ผ่านการเรียกใช้เมธอด
ใช้ ViewModel ของ AAC หากแอปของคุณได้รับประโยชน์จาก ViewModel ดังกล่าว ใช้ ViewModel ของ AAC เพื่อจัดการตรรกะทางธุรกิจ และดึงข้อมูลแอปพลิเคชันเพื่อแสดงสถานะ UI ต่อ UI (Compose หรือ Android View)

ดูแนวทางปฏิบัติแนะนำสำหรับ ViewModel เพิ่มเติมที่นี่

ดูประโยชน์ของ ViewModels ที่นี่

ใช้การเก็บรวบรวมสถานะ UI ที่รับรู้วงจรของลูกค้า รวบรวมสถานะ UI จาก UI โดยใช้โปรแกรมสร้างโคโรทีนซึ่งคำนึงถึงวงจรชีวิตของแอปที่เหมาะสม ได้แก่ repeatOnLifecycle ในระบบ View และ collectAsStateWithLifecycle ใน Jetpack Compose

อ่านเพิ่มเติมเกี่ยวกับ repeatOnLifecycle

อ่านข้อมูลเพิ่มเติมเกี่ยวกับ collectAsStateWithLifecycle

อย่าส่งเหตุการณ์จาก ViewModel ไปยัง UI ประมวลผลเหตุการณ์ทันทีใน ViewModel และสร้างการอัปเดตสถานะโดยเป็นผลมาจากการจัดการเหตุการณ์ ดูข้อมูลเพิ่มเติมเกี่ยวกับเหตุการณ์ UI ได้ที่นี่
ใช้แอปพลิเคชันกิจกรรมเดียว ใช้ Navigation Fragments หรือ Navigation Compose เพื่อไปยังส่วนต่างๆ ระหว่างหน้าจอและ Deep Link ไปยังแอปหากแอปมีมากกว่า 1 หน้าจอ
ใช้ Jetpack Compose ใช้ Jetpack Compose เพื่อสร้างแอปใหม่ๆ สำหรับโทรศัพท์ แท็บเล็ต อุปกรณ์แบบพับได้ และ Wear OS

ข้อมูลโค้ดต่อไปนี้แสดงวิธีรวบรวมสถานะ UI ในลักษณะที่คำนึงถึงวงจร

ยอดดู

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

เขียน

@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
}

ViewModel

ViewModel มีหน้าที่ระบุสถานะ UI และเข้าถึงเลเยอร์ข้อมูล แนวทางปฏิบัติแนะนำบางส่วนสำหรับ ViewModel มีดังนี้

คำแนะนำ คำอธิบาย
ViewModel ไม่ควรเกี่ยวข้องกับวงจรชีวิตของ Android ViewModels ไม่ควรอ้างอิงถึงประเภทใดๆ ที่เกี่ยวข้องกับวงจร อย่าส่ง Activity, Fragment, Context หรือ Resources เป็นทรัพยากร Dependency หากมีบางอย่างต้องใช้ Context ใน ViewModel คุณควรประเมินอย่างละเอียดว่าอยู่ในเลเยอร์ที่เหมาะสมหรือไม่
ใช้โครูทีนและโฟลว์

ViewModel จะโต้ตอบกับเลเยอร์ข้อมูลหรือโดเมนโดยใช้สิ่งต่อไปนี้

  • Kotlin ใช้สำหรับรับข้อมูลแอปพลิเคชัน
  • ฟังก์ชัน suspend เพื่อดำเนินการโดยใช้ viewModelScope
ใช้ ViewModel ที่ระดับหน้าจอ

อย่าใช้ ViewModel ในส่วน UI ที่นํากลับมาใช้ซ้ำได้ คุณควรใช้ ViewModel ในกรณีต่อไปนี้

  • คอมโพเนนต์ระดับหน้าจอ
  • กิจกรรม/ส่วนย่อยในมุมมอง
  • ปลายทางหรือกราฟเมื่อใช้การนำทางของ Jetpack
ใช้คลาสตัวเก็บสถานะแบบธรรมดาในคอมโพเนนต์ UI ที่นํากลับมาใช้ซ้ำได้ ใช้คลาสตัวเก็บสถานะแบบธรรมดาเพื่อจัดการความซับซ้อนในคอมโพเนนต์ UI ที่นํากลับมาใช้ซ้ำได้ ซึ่งจะทำให้ยกระดับและควบคุมสถานะจากภายนอกได้
อย่าใช้ AndroidViewModel ใช้คลาส ViewModel ไม่ใช่ AndroidViewModel คุณไม่ควรใช้คลาส Application ใน ViewModel แต่ให้ย้ายการพึ่งพาไปยัง UI หรือชั้นข้อมูลแทน
แสดงสถานะ UI ViewModel ควรแสดงข้อมูลต่อ UI ผ่านพร็อพเพอร์ตี้เดียวที่เรียกว่า uiState หาก UI แสดงข้อมูลหลายรายการที่ไม่เกี่ยวข้องกัน VM สามารถแสดงพร็อพเพอร์ตี้สถานะ UI หลายรายการ
  • คุณควรทำให้ uiState เป็น StateFlow
  • คุณควรสร้าง uiState โดยใช้โอเปอเรเตอร์ stateIn ที่มีนโยบาย WhileSubscribed(5000) (ตัวอย่าง) หากข้อมูลมาในรูปแบบสตรีมข้อมูลจากเลเยอร์อื่นๆ ของลําดับชั้น
  • สําหรับกรณีที่ง่ายขึ้นซึ่งไม่มีสตรีมข้อมูลจากชั้นข้อมูล คุณสามารถใช้ MutableStateFlow ที่แสดงเป็น StateFlow แบบคงที่ (ตัวอย่าง) ได้
  • คุณเลือกให้ ${Screen}UiState เป็นคลาสข้อมูลที่อาจมีข้อมูล ข้อผิดพลาด และสัญญาณการโหลดได้ คลาสนี้อาจเป็นคลาสที่ปิดอยู่ได้หากสถานะต่างๆ เป็นแบบไม่รวมกัน

ข้อมูลโค้ดต่อไปนี้แสดงวิธีแสดงสถานะ UI จาก ViewModel

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

วงจร

แนวทางปฏิบัติแนะนำบางส่วนสำหรับการใช้ Android lifecycle มีดังนี้

คำแนะนำ คำอธิบาย
อย่าลบล้างเมธอดวงจรของ Activity หรือ Fragment อย่าลบล้างเมธอดอายุการใช้งาน เช่น onResume ในกิจกรรมหรือ Fragment ให้ใช้ LifecycleObserver แทน หากแอปต้องทํางานเมื่อวงจรมาถึง Lifecycle.State บางรายการ ให้ใช้ repeatOnLifecycle API

ข้อมูลโค้ดต่อไปนี้แสดงวิธีดำเนินการตามสถานะวงจรของลูกค้า

ยอดดู

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

เขียน

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

จัดการทรัพยากร Dependency

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

คำแนะนำ คำอธิบาย
ใช้การแทรก Dependency ใช้แนวทางปฏิบัติแนะนำเกี่ยวกับ Dependency Injection โดยหลักๆ แล้วจะใช้การแทรกเครื่องมือสร้าง เมื่อเป็นไปได้
กำหนดขอบเขตไปยังคอมโพเนนต์เมื่อจำเป็น กำหนดขอบเขตเป็นคอนเทนเนอร์ทรัพยากร Dependency เมื่อประเภทมีข้อมูลที่เปลี่ยนแปลงได้ซึ่งจำเป็นต้องแชร์ หรือประเภทการเริ่มต้นมีต้นทุนสูงและใช้กันอย่างแพร่หลายในแอป
ใช้ Hilt ใช้ Hilt หรือการฉีด Dependency ด้วยตนเองในแอปง่ายๆ ใช้ Hilt หากโปรเจ็กต์มีความซับซ้อนมาก เช่น หากคุณมีสิ่งต่อไปนี้
  • หน้าจอหลายหน้าจอที่มี ViewModel - การผสานรวม
  • การใช้งาน WorkManager - การผสานรวม
  • การใช้การนำทางขั้นสูง เช่น ViewModels ที่กำหนดขอบเขตไปยังกราฟการนำทาง ซึ่งก็คือการผสานการทำงาน

การทดสอบ

แนวทางปฏิบัติแนะนำบางส่วนสําหรับการทดสอบมีดังนี้

คำแนะนำ คำอธิบาย
รู้สิ่งที่จะทดสอบ

คุณควรทดสอบโปรเจ็กต์เป็นอย่างน้อยด้วยสิ่งต่อไปนี้ เว้นแต่โปรเจ็กต์จะเรียบง่ายพอๆ กับแอป Hello World

  • ทดสอบ ViewModel รวมถึงโฟลว์ด้วย Unit Test
  • เอนทิตีชั้นข้อมูลการทดสอบ 1 หน่วย กล่าวคือ ที่เก็บและแหล่งข้อมูล
  • การทดสอบการไปยังส่วนต่างๆ ของ UI ที่มีประโยชน์สำหรับการทดสอบแบบย้อนกลับใน CI
ชอบปลอมเพื่อล้อเลียน อ่านเพิ่มเติมได้ในใช้ Test Double ในเอกสารประกอบของ Android
ทดสอบ StateFlow เมื่อทดสอบ StateFlow

สำหรับข้อมูลเพิ่มเติม โปรดดูสิ่งที่ต้องทดสอบในคู่มือ DAC ของ Android

รุ่น

คุณควรปฏิบัติตามแนวทางปฏิบัติแนะนำเหล่านี้เมื่อพัฒนาโมเดลในแอป

คำแนะนำ คำอธิบาย
สร้างโมเดลต่อเลเยอร์ในแอปที่ซับซ้อน

ในแอปที่ซับซ้อน ให้สร้างโมเดลใหม่ในเลเยอร์หรือคอมโพเนนต์ต่างๆ ตามความเหมาะสม ลองดูตัวอย่างต่อไปนี้

  • แหล่งข้อมูลระยะไกลสามารถแมปโมเดลที่ได้รับผ่านเครือข่ายไปยังคลาสที่เรียบง่ายขึ้นโดยมีเพียงข้อมูลที่แอปต้องการ
  • รีโพซิทอรีสามารถแมปโมเดล DAO กับคลาสข้อมูลที่เรียบง่ายขึ้นโดยใช้เฉพาะข้อมูลที่เลเยอร์ UI ต้องการ
  • ViewModel สามารถรวมโมเดลชั้นข้อมูลในคลาส UiState

รูปแบบการตั้งชื่อ

เมื่อตั้งชื่อโค้ดเบส คุณควรคำนึงถึงแนวทางปฏิบัติแนะนำต่อไปนี้

คำแนะนำ คำอธิบาย
วิธีการตั้งชื่อ
ไม่บังคับ
เมธอดควรเป็นวลีที่มีคำกริยา เช่น makePayment()
การตั้งชื่อพร็อพเพอร์ตี้
ไม่บังคับ
พร็อพเพอร์ตี้ควรเป็นวลีนาม เช่น inProgressTopicSelection
ตั้งชื่อสตรีมข้อมูล
ไม่บังคับ
เมื่อคลาสแสดงสตรีม Flow, LiveData หรือสตรีมอื่นๆ รูปแบบการตั้งชื่อจะเป็น get{model}Stream() เช่น getAuthorStream(): Flow<Author> หากฟังก์ชันแสดงผลรายการโมเดล ชื่อโมเดลควรเป็นพหูพจน์ getAuthorsStream(): Flow<List<Author>>
การตั้งชื่อการติดตั้งใช้งานอินเทอร์เฟซ
ไม่บังคับ
ชื่อสำหรับการติดตั้งใช้งานอินเทอร์เฟซควรสื่อความหมาย ใช้ Default เป็นคำนำหน้าหากไม่พบชื่อที่ดีกว่า เช่น สําหรับอินเทอร์เฟซ NewsRepository คุณอาจมี OfflineFirstNewsRepository หรือ InMemoryNewsRepository หากไม่เห็นชื่อที่เป็นประโยชน์ ให้ใช้ DefaultNewsRepository การใช้งานจำลองควรขึ้นต้นด้วย Fake เช่น FakeAuthorsRepository