เหตุการณ์ UI

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

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

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

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

แผนผังการตัดสินใจเหตุการณ์ UI

แผนภาพต่อไปนี้แสดงแผนผังการตัดสินใจเพื่อหาแนวทางที่ดีที่สุด การจัดการ Use Case ของเหตุการณ์ที่เฉพาะเจาะจง ส่วนที่เหลือของคู่มือนี้จะอธิบายสิ่งเหล่านี้ อย่างละเอียด

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

จัดการเหตุการณ์ของผู้ใช้

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

ตัวอย่างต่อไปนี้แสดงวิธีใช้ปุ่มต่างๆ เพื่อขยาย UI (ตรรกะ UI) และรีเฟรชข้อมูลบนหน้าจอ (ตรรกะธุรกิจ) ให้ทำดังนี้

ยอดดู

class LatestNewsActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLatestNewsBinding
    private val viewModel: LatestNewsViewModel by viewModels()

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

        // The expand details event is processed by the UI that
        // modifies a View's internal state.
        binding.expandButton.setOnClickListener {
            binding.expandedSection.visibility = View.VISIBLE
        }

        // The refresh event is processed by the ViewModel that is in charge
        // of the business logic.
        binding.refreshButton.setOnClickListener {
            viewModel.refreshNews()
        }
    }
}

เขียน

@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {

    // State of whether more details should be shown
    var expanded by remember { mutableStateOf(false) }

    Column {
        Text("Some text")
        if (expanded) {
            Text("More details")
        }

        Button(
          // The expand details event is processed by the UI that
          // modifies this composable's internal state.
          onClick = { expanded = !expanded }
        ) {
          val expandText = if (expanded) "Collapse" else "Expand"
          Text("$expandText details")
        }

        // The refresh event is processed by the ViewModel that is in charge
        // of the UI's business logic.
        Button(onClick = { viewModel.refreshNews() }) {
            Text("Refresh data")
        }
    }
}

เหตุการณ์ผู้ใช้ใน RecyclerViews

หากมีการสร้างการดำเนินการเพิ่มเติมจากโครงสร้าง UI เช่น ใน RecyclerView หรือ View ที่กำหนดเอง ViewModel ยังควรเป็นผู้ใช้ที่มีการจัดการรายเดียว กิจกรรม

ตัวอย่างเช่น สมมติว่ารายการข่าวทั้งหมดจาก NewsActivity มีบุ๊กมาร์ก ViewModel จำเป็นต้องทราบรหัสของรายการข่าวที่บุ๊กมาร์กไว้ วันและเวลา ผู้ใช้บุ๊กมาร์กรายการข่าว แต่อะแดปเตอร์ RecyclerView จะไม่เรียกใช้ ได้เปิดเผยฟังก์ชัน addBookmark(newsId) จาก ViewModel ซึ่งจะต้องมี การพึ่งพา ViewModel แต่ ViewModel จะแสดงออบเจ็กต์สถานะแทน ที่เรียกว่า NewsItemUiState ซึ่งมีการติดตั้งใช้งานสำหรับจัดการ กิจกรรม:

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    val publicationDate: String,
    val onBookmark: () -> Unit
)

class LatestNewsViewModel(
    private val formatDateUseCase: FormatDateUseCase,
    private val repository: NewsRepository
)
    val newsListUiItems = repository.latestNews.map { news ->
        NewsItemUiState(
            title = news.title,
            body = news.body,
            bookmarked = news.bookmarked,
            publicationDate = formatDateUseCase(news.publicationDate),
            // Business logic is passed as a lambda function that the
            // UI calls on click events.
            onBookmark = {
                repository.addBookmark(news.id)
            }
        )
    }
}

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

รูปแบบการตั้งชื่อสำหรับฟังก์ชันเหตุการณ์ของผู้ใช้

ในคู่มือนี้ ฟังก์ชัน ViewModel ที่จัดการเหตุการณ์ของผู้ใช้จะได้รับการตั้งชื่อด้วย กริยาตามการกระทำที่ดำเนินการ เช่น addBookmark(id) หรือ logIn(username, password)

จัดการเหตุการณ์ ViewModel

การดำเนินการของ UI ที่มาจากเหตุการณ์ ViewModel—ViewModel ควร จะส่งผลให้เกิดการอัปเดตสถานะ UI ช่วงเวลานี้ สอดคล้องกับหลักการของ Unidirectional Data โฟลว์ ซึ่งจะทำให้เหตุการณ์เกิดซ้ำได้หลังจาก เปลี่ยนแปลงการกำหนดค่า และรับประกันว่าการดำเนินการ UI จะไม่หายไป (ไม่บังคับ) คุณสามารถทำให้เหตุการณ์เกิดซ้ำหลังจากที่กระบวนการสิ้นสุดลงได้ หากคุณใช้แท็ก ที่บันทึกไว้ โมดูลสถานะ

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

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

data class LoginUiState(
    val isLoading: Boolean = false,
    val errorMessage: String? = null,
    val isUserLoggedIn: Boolean = false
)

UI นี้จะตอบสนองต่อการเปลี่ยนแปลงสถานะ isUserLoggedIn และไปยัง ปลายทางที่ถูกต้องตามต้องการ:

ยอดดู

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
    /* ... */
}

class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()

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

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    if (uiState.isUserLoggedIn) {
                        // Navigate to the Home screen.
                    }
                    ...
                }
            }
        }
    }
}

เขียน

class LoginViewModel : ViewModel() {
    var uiState by mutableStateOf(LoginUiState())
        private set
    /* ... */
}

@Composable
fun LoginScreen(
    viewModel: LoginViewModel = viewModel(),
    onUserLogIn: () -> Unit
) {
    val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)

    // Whenever the uiState changes, check if the user is logged in.
    LaunchedEffect(viewModel.uiState)  {
        if (viewModel.uiState.isUserLoggedIn) {
            currentOnUserLogIn()
        }
    }

    // Rest of the UI for the login screen.
}

การใช้เหตุการณ์จะทำให้เกิดการอัปเดตสถานะ

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

// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
    val news: List<News> = emptyList(),
    val isLoading: Boolean = false,
    val userMessage: String? = null
)

ViewModel จะอัปเดตสถานะ UI ดังนี้เมื่อตรรกะทางธุรกิจ ต้องแสดงข้อความชั่วคราวใหม่แก่ผู้ใช้:

ยอดดู

class LatestNewsViewModel(/* ... */) : ViewModel() {

    private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    fun refreshNews() {
        viewModelScope.launch {
            // If there isn't internet connection, show a new message on the screen.
            if (!internetConnection()) {
                _uiState.update { currentUiState ->
                    currentUiState.copy(userMessage = "No Internet connection")
                }
                return@launch
            }

            // Do something else.
        }
    }

    fun userMessageShown() {
        _uiState.update { currentUiState ->
            currentUiState.copy(userMessage = null)
        }
    }
}

เขียน

class LatestNewsViewModel(/* ... */) : ViewModel() {

    var uiState by mutableStateOf(LatestNewsUiState())
        private set

    fun refreshNews() {
        viewModelScope.launch {
            // If there isn't internet connection, show a new message on the screen.
            if (!internetConnection()) {
                uiState = uiState.copy(userMessage = "No Internet connection")
                return@launch
            }

            // Do something else.
        }
    }

    fun userMessageShown() {
        uiState = uiState.copy(userMessage = null)
    }
}

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

ยอดดู

class LatestNewsActivity : AppCompatActivity() {
    private val viewModel: LatestNewsViewModel by viewModels()

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

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    uiState.userMessage?.let {
                        // TODO: Show Snackbar with userMessage.

                        // Once the message is displayed and
                        // dismissed, notify the ViewModel.
                        viewModel.userMessageShown()
                    }
                    ...
                }
            }
        }
    }
}

เขียน

@Composable
fun LatestNewsScreen(
    snackbarHostState: SnackbarHostState,
    viewModel: LatestNewsViewModel = viewModel(),
) {
    // Rest of the UI content.

    // If there are user messages to show on the screen,
    // show it and notify the ViewModel.
    viewModel.uiState.userMessage?.let { userMessage ->
        LaunchedEffect(userMessage) {
            snackbarHostState.showSnackbar(userMessage)
            // Once the message is displayed and dismissed, notify the ViewModel.
            viewModel.userMessageShown()
        }
    }
}

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

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

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

ยอดดู

class LoginActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLoginBinding
    private val viewModel: LoginViewModel by viewModels()

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

        binding.helpButton.setOnClickListener {
            navController.navigate(...) // Open help screen
        }
    }
}

เขียน

@Composable
fun LoginScreen(
    onHelp: () -> Unit, // Caller navigates to the right screen
    viewModel: LoginViewModel = viewModel()
) {
    // Rest of the UI

    Button(onClick = onHelp) {
        Text("Get help")
    }
}

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

ยอดดู

class LoginActivity : AppCompatActivity() {
    private val viewModel: LoginViewModel by viewModels()

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

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { uiState ->
                    if (uiState.isUserLoggedIn) {
                        // Navigate to the Home screen.
                    }
                    ...
                }
            }
        }
    }
}

เขียน

@Composable
fun LoginScreen(
    onUserLogIn: () -> Unit, // Caller navigates to the right screen
    viewModel: LoginViewModel = viewModel()
) {
    Button(
        onClick = {
            // ViewModel validation is triggered
            viewModel.login()
        }
    ) {
        Text("Log in")
    }
    // Rest of the UI

    val lifecycle = LocalLifecycleOwner.current.lifecycle
    val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
    LaunchedEffect(viewModel, lifecycle)  {
        // Whenever the uiState changes, check if the user is logged in and
        // call the `onUserLogin` event when `lifecycle` is at least STARTED
        snapshotFlow { viewModel.uiState }
            .filter { it.isUserLoggedIn }
            .flowWithLifecycle(lifecycle)
            .collect {
                currentOnUserLogIn()
            }
    }
}

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

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

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

ยอดดู

// Key that identifies the `validationInProgress` state in the Bundle
private const val DOB_VALIDATION_KEY = "dobValidationKey"

class DobValidationFragment : Fragment() {

    private var validationInProgress: Boolean = false
    private val viewModel: DobValidationViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = // ...
        validationInProgress = savedInstanceState?.getBoolean(DOB_VALIDATION_KEY) ?: false

        binding.continueButton.setOnClickListener {
            viewModel.validateDob()
            validationInProgress = true
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState
                .flowWithLifecycle(viewLifecycleOwner.lifecycle)
                .collect { uiState ->
                    // Update other parts of the UI ...

                    // If the input is valid and the user wants
                    // to navigate, navigate to the next screen
                    // and reset `validationInProgress` flag
                    if (uiState.isDobValid && validationInProgress) {
                        validationInProgress = false
                        navController.navigate(...) // Navigate to next screen
                    }
                }
        }

        return binding
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBoolean(DOB_VALIDATION_KEY, validationInProgress)
    }
}

เขียน

class DobValidationViewModel(/* ... */) : ViewModel() {
    var uiState by mutableStateOf(DobValidationUiState())
        private set
}

@Composable
fun DobValidationScreen(
    onNavigateToNextScreen: () -> Unit, // Caller navigates to the right screen
    viewModel: DobValidationViewModel = viewModel()
) {
    // TextField that updates the ViewModel when a date of birth is selected

    var validationInProgress by rememberSaveable { mutableStateOf(false) }

    Button(
        onClick = {
            viewModel.validateInput()
            validationInProgress = true
        }
    ) {
        Text("Continue")
    }
    // Rest of the UI

    /*
     * The following code implements the requirement of advancing automatically
     * to the next screen when a valid date of birth has been introduced
     * and the user wanted to continue with the registration process.
     */

    if (validationInProgress) {
        val lifecycle = LocalLifecycleOwner.current.lifecycle
        val currentNavigateToNextScreen by rememberUpdatedState(onNavigateToNextScreen)
        LaunchedEffect(viewModel, lifecycle) {
            // If the date of birth is valid and the validation is in progress,
            // navigate to the next screen when `lifecycle` is at least STARTED,
            // which is the default Lifecycle.State for the `flowWithLifecycle` operator.
            snapshotFlow { viewModel.uiState }
                .filter { it.isDobValid }
                .flowWithLifecycle(lifecycle)
                .collect {
                    validationInProgress = false
                    currentNavigateToNextScreen()
                }
        }
    }
}

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

Use Case อื่นๆ

หากคุณคิดว่ากรณีการใช้งานเหตุการณ์ UI ไม่สามารถแก้ไขได้ด้วยการอัปเดตสถานะ UI อาจต้องพิจารณาวิธีไหลของข้อมูลในแอปอีกครั้ง ลองพิจารณาวิธีต่อไปนี้ หลักการ:

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

ตัวอย่าง

ตัวอย่าง Google ต่อไปนี้แสดงเหตุการณ์ UI ใน เลเยอร์ UI ศึกษาคู่มือเพื่อดูคำแนะนำนี้ในสถานการณ์จริง