เลเยอร์ UI (มุมมอง)

แนวคิดและการใช้งาน Jetpack Compose

บทบาทของ UI คือการแสดงข้อมูลแอปพลิเคชันบนหน้าจอ รวมถึงเป็นจุดหลักของการโต้ตอบของผู้ใช้ เมื่อใดก็ตามที่ข้อมูลมีการเปลี่ยนแปลง ไม่ว่าจะเป็นเนื่องจากการโต้ตอบของผู้ใช้ (เช่น การกดปุ่ม) หรืออินพุตภายนอก (เช่น การตอบสนองของเครือข่าย) UI ควรได้รับการอัปเดตเพื่อแสดงการเปลี่ยนแปลงเหล่านั้น กล่าวคือ UI เป็นการแสดงภาพของสถานะแอปพลิเคชันที่ดึงมาจากชั้นข้อมูล

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

แสดงสถานะ UI

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

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = 
}

วิธีทั่วไปในการสร้างสตรีม UiState คือการแสดงสตรีมที่เปลี่ยนแปลงได้ เป็นสตรีมที่เปลี่ยนแปลงไม่ได้จาก ViewModel เช่น การแสดง MutableStateFlow<UiState> เป็น StateFlow<UiState>

class NewsViewModel(...) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

จากนั้น ViewModel จะแสดงเมธอดที่เปลี่ยนแปลงสถานะภายใน โดยเผยแพร่การอัปเดตเพื่อให้ UI ใช้ ตัวอย่างเช่น ในกรณีที่ต้องดำเนินการแบบอะซิงโครนัส คุณสามารถเปิดใช้โครูทีนได้โดยใช้ viewModelScope และ อัปเดตสถานะที่เปลี่ยนแปลงได้เมื่อดำเนินการเสร็จสมบูรณ์

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

ใช้สถานะ UI

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

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

แสดงการดำเนินการที่อยู่ระหว่างดำเนินการ

วิธีง่ายๆ ในการแสดงสถานะการโหลดในคลาส UiState คือการใช้ฟิลด์บูลีน ดังนี้

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

ค่าของแฟล็กนี้แสดงถึงการมีหรือไม่มีแถบความคืบหน้าใน UI

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

ภาพเคลื่อนไหว

หากต้องการให้การเปลี่ยนผ่านการนำทางระดับบนสุดเป็นไปอย่างราบรื่น คุณอาจต้องรอให้หน้าจอที่ 2 โหลดข้อมูลก่อนที่จะเริ่มภาพเคลื่อนไหว เฟรมเวิร์กมุมมอง Android มีฮุกเพื่อหน่วงเวลาการเปลี่ยนผ่านระหว่างปลายทางของ Fragment ด้วย postponeEnterTransition() และ startPostponedEnterTransition() API API เหล่านี้ช่วยให้มั่นใจได้ว่าองค์ประกอบ UI ในหน้าจอที่ 2 (โดยปกติจะเป็นรูปภาพที่ดึงมาจากเครือข่าย) พร้อมที่จะแสดงก่อนที่ UI จะแสดงภาพเคลื่อนไหวการเปลี่ยนผ่านไปยังหน้าจอนั้น