ภาพรวม ViewModel   ส่วนหนึ่งของ Android Jetpack

ลองใช้ Kotlin Multiplatform
Kotlin Multiplatform ช่วยให้แชร์ตรรกะทางธุรกิจกับแพลตฟอร์มอื่นๆ ได้ ดูวิธีตั้งค่าและทำงานกับ ViewModel ใน KMP

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

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

ประโยชน์ของ ViewModel

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

ประโยชน์หลักของคลาส ViewModel มี 2 ประการ ได้แก่

  • ซึ่งช่วยให้คุณคงสถานะ UI ไว้ได้
  • ซึ่งให้สิทธิ์เข้าถึงตรรกะทางธุรกิจ

ความต่อเนื่อง

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

ขอบเขต

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

คลาสต่างๆ เป็นคลาสย่อยโดยตรงหรือโดยอ้อมของอินเทอร์เฟซ ViewModelStoreOwner คลาสย่อยโดยตรงคือ ComponentActivity, Fragment และ NavBackStackEntry ดูรายการคลาสย่อยทางอ้อมทั้งหมดได้ในViewModelStoreOwnerข้อมูลอ้างอิง

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

ดูข้อมูลเพิ่มเติมได้ที่ส่วนวงจรของ ViewModel ด้านล่าง

SavedStateHandle

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

การเข้าถึงตรรกะทางธุรกิจ

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

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

Jetpack Compose

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

สิ่งสำคัญที่สุดที่ต้องคำนึงถึงเมื่อใช้ ViewModel กับ Compose คือ คุณไม่สามารถกำหนดขอบเขต ViewModel ให้กับ Composable ได้ เนื่องจาก Composable ไม่ใช่ ViewModelStoreOwner อินสแตนซ์ 2 รายการของ Composable เดียวกันใน Composition หรือ Composable 2 รายการที่แตกต่างกันซึ่งเข้าถึง ViewModel ประเภทเดียวกัน ภายใต้ ViewModelStoreOwner เดียวกันจะได้รับอินสแตนซ์ เดียวกันของ ViewModel ซึ่งมักจะไม่ใช่ลักษณะการทำงานที่คาดไว้

หากต้องการรับประโยชน์ของ ViewModel ใน Compose ให้โฮสต์แต่ละหน้าจอใน Fragment หรือ Activity หรือใช้ Compose Navigation และใช้ ViewModel ในฟังก์ชันที่ใช้ร่วมกันได้ ให้ใกล้กับปลายทางการนำทางมากที่สุด เนื่องจากคุณสามารถกำหนดขอบเขต ViewModel ให้กับปลายทางการนำทาง กราฟการนำทาง กิจกรรม และ Fragment ได้

ดูข้อมูลเพิ่มเติมได้ที่คู่มือเกี่ยวกับการย้ายสถานะสำหรับ Jetpack Compose

ใช้ ViewModel

ต่อไปนี้คือตัวอย่างการใช้งาน ViewModel สำหรับหน้าจอที่ อนุญาตให้ผู้ใช้ทอยลูกเต๋า

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

จากนั้นคุณจะเข้าถึง ViewModel จากกิจกรรมได้ดังนี้

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

ใช้โครูทีนกับ ViewModel

ViewModel รองรับโครูทีนของ Kotlin โดยสามารถเก็บงานแบบอะซิงโครนัสได้ในลักษณะเดียวกับการเก็บสถานะ UI

ดูข้อมูลเพิ่มเติมได้ที่ใช้ Kotlin Coroutines กับ Android Architecture Components

วงจรการใช้งานของ ViewModel

วงจรของ ViewModel จะเชื่อมโยงกับขอบเขตโดยตรง ViewModel จะยังคงอยู่ในหน่วยความจำจนกว่า ViewModelStoreOwner ที่กำหนดขอบเขตไว้ จะหายไป ซึ่งอาจเกิดขึ้นในบริบทต่อไปนี้

  • ในกรณีของกิจกรรม เมื่อกิจกรรมสิ้นสุดลง
  • ในกรณีของ Fragment เมื่อมีการแยก
  • ในกรณีของรายการการนำทาง เมื่อนำออกจากสแต็กย้อนกลับ

ซึ่งทำให้ ViewModel เป็นโซลูชันที่ยอดเยี่ยมสำหรับการจัดเก็บข้อมูลที่ยังคงอยู่ แม้จะมีการเปลี่ยนแปลงการกำหนดค่า

รูปที่ 1 แสดงสถานะวงจรต่างๆ ของกิจกรรมเมื่อมีการหมุนเวียนและสิ้นสุด ภาพยังแสดงอายุการใช้งานของ ViewModel ข้างวงจรชีวิตของกิจกรรมที่เกี่ยวข้องด้วย ไดอะแกรมนี้แสดงสถานะของกิจกรรม สถานะพื้นฐานเดียวกันนี้ใช้กับ วงจรของ Fragment

แสดงวงจรของ ViewModel เมื่อกิจกรรมเปลี่ยนสถานะ

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

การล้างทรัพยากร Dependency ของ ViewModel

ViewModel จะเรียกใช้เมธอด onCleared เมื่อ ViewModelStoreOwner ทำลาย ViewModel ในช่วงวงจรของ ViewModel ซึ่งจะช่วยให้คุณล้างงานหรือการอ้างอิงที่เกิดขึ้นตามวงจรของ ViewModel ได้

ตัวอย่างต่อไปนี้แสดงทางเลือกแทน viewModelScope viewModelScope เป็น CoroutineScope ในตัวที่ ติดตามวงจรของ ViewModel โดยอัตโนมัติ ViewModel ใช้เพื่อ ทริกเกอร์การดำเนินการที่เกี่ยวข้องกับธุรกิจ หากต้องการใช้ขอบเขตที่กำหนดเองแทน viewModelScope เพื่อให้ทดสอบได้ง่ายขึ้น ViewModel จะรับ CoroutineScope เป็นการอ้างอิงในตัวสร้างได้ เมื่อ ViewModelStoreOwner ล้าง ViewModel เมื่อสิ้นสุดวงจรของ ViewModel นั้น ViewModel จะยกเลิก CoroutineScope ด้วย

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

ตั้งแต่เวอร์ชัน 2.5 ของวงจรขึ้นไป คุณสามารถส่งออบเจ็กต์อย่างน้อย 1 รายการCloseable ไปยังตัวสร้างของ ViewModel ซึ่งจะปิดโดยอัตโนมัติเมื่อล้างอินสแตนซ์ ViewModel

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

แนวทางปฏิบัติแนะนำ

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

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

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

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

คำแนะนำเกี่ยวกับสถาปัตยกรรมแอป Android แนะนำให้สร้างคลาสที่เก็บ เพื่อจัดการฟังก์ชันเหล่านี้

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

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

เอกสาร

ตัวอย่าง