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

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

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

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

สถาปัตยกรรมแบบแบ่งเลเยอร์

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

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

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

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับแนวทางปฏิบัติแนะนำสำหรับ Coroutine ได้ที่ แนวทางปฏิบัติแนะนำสำหรับ Coroutine ใน Android

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

เลเยอร์ UI

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

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับแนวทางปฏิบัติแนะนำสำหรับ ViewModel ได้ที่ คำแนะนำเกี่ยวกับสถาปัตยกรรม

ดูข้อมูลเพิ่มเติมเกี่ยวกับประโยชน์ของ ViewModel ได้ที่ ViewModel ในฐานะตัวเก็บสถานะตรรกะทางธุรกิจ

ใช้การรวบรวมสถานะ UI ที่รับรู้ถึงวงจร รวบรวมสถานะ UI จาก UI โดยใช้ตัวสร้างโครูทีนที่เหมาะสมซึ่งรับรู้ถึงวงจร collectAsStateWithLifecycle

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

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

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

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

ViewModel

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

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

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

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

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

  • Composable ระดับหน้าจอ
  • ปลายทางหรือกราฟเมื่อใช้ Jetpack Navigation

สำหรับ Composable ที่ซับซ้อนมากขึ้น หรือ Composable ที่มีลักษณะการทำงานแบบไดนามิกตามสถานะ ให้ใช้ rememberViewModelStoreOwner() เพื่อกำหนดขอบเขต ViewModel ไปยังตำแหน่งที่เรียกใช้ Composable โดยตรง

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

ข้อมูลโค้ดต่อไปนี้แสดงวิธีแสดงสถานะ 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
            )

    // ...
}

วงจร

ทำตามแนวทางปฏิบัติแนะนำสำหรับการทำงานกับวงจรของกิจกรรม

คำแนะนำ คำอธิบาย
ใช้เอฟเฟกต์ที่รับรู้ถึงวงจรใน Composable แทนการลบล้างการเรียกกลับวงจรของ Activity

อย่าลบล้างเมธอดวงจรของ Activity เช่น onResume เพื่อเรียกใช้ฟังก์ชันที่เกี่ยวข้องกับ UI แต่ให้ใช้ LifecycleEffect หรือขอบเขต Coroutine ที่รับรู้ถึงวงจรของ Compose แทน:

  • ใช้ LifecycleStartEffect เพื่อทำงานแบบซิงโครนัสเมื่อกิจกรรมเริ่มต้นและหยุด
  • ใช้ LifecycleResumeEffect เพื่อทำงานแบบซิงโครนัสเมื่อกิจกรรมกลับมาทำงานต่อและหยุดชั่วคราว
  • ใช้ repeatOnLifecycle เพื่อทำงานแบบอะซิงโครนัสเพื่อตอบสนองต่อเหตุการณ์วงจร
  • รวบรวมข้อมูลแบบอะซิงโครนัสจาก Flow โดยใช้ collectAsStateWithLifecycle

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

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

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

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

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

การทดสอบ

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

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

ทดสอบโปรเจ็กต์ เว้นแต่โปรเจ็กต์จะง่ายมาก เช่น แอป "Hello World" โดยให้รวมสิ่งต่อไปนี้เป็นอย่างน้อย

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

ดูข้อมูลเพิ่มเติมได้ที่ สิ่งที่จะทดสอบใน Android และ ทดสอบเลย์เอาต์ Compose

โมเดล

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

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

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

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

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

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

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

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับสถาปัตยกรรม Android ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้

เอกสารประกอบ

เนื้อหา Views