ภาพรวม ViewModel ส่วนหนึ่งของ Android Jetpack
คลาส 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
ในครั้งแรกที่ระบบเรียกใช้เมธอด 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
คลาสได้ที่แหล่งข้อมูลต่อไปนี้
เอกสาร
ตัวอย่าง
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ใช้ Kotlin Coroutine กับคอมโพเนนต์ที่รับรู้ถึงวงจร
- บันทึกสถานะ UI
- โหลดและแสดงข้อมูลแบบแบ่งหน้า