การจัดการวงจรด้วยคอมโพเนนต์ที่รับรู้วงจร
เป็นส่วนหนึ่งของ Android Jetpack
จัดทุกอย่างให้เป็นระเบียบอยู่เสมอด้วยคอลเล็กชัน
บันทึกและจัดหมวดหมู่เนื้อหาตามค่ากำหนดของคุณ
คอมโพเนนต์ที่รับรู้วงจรจะดำเนินการตามการเปลี่ยนแปลงสถานะวงจรของคอมโพเนนต์อื่น เช่น กิจกรรมและข้อมูลโค้ด คอมโพเนนต์เหล่านี้ช่วยให้คุณสร้างโค้ดที่มีการจัดระเบียบดีขึ้นและมักจะมีน้ำหนักเบากว่า ซึ่งทำให้ดูแลรักษาได้ง่ายขึ้น
รูปแบบที่พบบ่อยคือการใช้การดำเนินการของคอมโพเนนต์ที่ขึ้นต่อกันในเมธอดวงจรชีวิตของกิจกรรมและฟragment อย่างไรก็ตาม รูปแบบนี้ทำให้การจัดระเบียบโค้ดไม่ดีและทำให้เกิดข้อผิดพลาดจำนวนมาก การใช้คอมโพเนนต์ที่รู้เกี่ยวกับวงจรจะย้ายโค้ดของคอมโพเนนต์ที่ขึ้นต่อกันออกจากเมธอดวงจรและย้ายไปไว้ในคอมโพเนนต์นั้นๆ ได้
แพ็กเกจ androidx.lifecycle
มีคลาสและอินเทอร์เฟซที่ช่วยให้คุณสร้างคอมโพเนนต์ที่รับรู้วงจรได้ ซึ่งเป็นคอมโพเนนต์ที่ปรับลักษณะการทำงานโดยอัตโนมัติตามสถานะวงจรปัจจุบันของกิจกรรมหรือฟragment
คอมโพเนนต์แอปส่วนใหญ่ที่กําหนดไว้ในเฟรมเวิร์ก Android จะมีวงจรอยู่ วงจรจัดการโดยระบบปฏิบัติการหรือโค้ดเฟรมเวิร์กที่ใช้ในกระบวนการ นโยบายเหล่านี้เป็นหัวใจสำคัญของวิธีการทำงานของ Android และแอปพลิเคชันของคุณต้องปฏิบัติตามนโยบายดังกล่าว การไม่ทำเช่นนั้นอาจทริกเกอร์ปัญหาหน่วยความจำรั่วหรือแอปพลิเคชันขัดข้อง
สมมติว่าเรามีกิจกรรมที่แสดงตำแหน่งของอุปกรณ์บนหน้าจอ การใช้งานที่พบบ่อยอาจมีลักษณะดังนี้
internal class MyLocationListener( private val context: Context, private val callback: (Location) -> Unit ) { fun start() { // connect to system location service } fun stop() { // disconnect from system location service } } class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this) { location -> // update UI } } public override fun onStart() { super.onStart() myLocationListener.start() // manage other components that need to respond // to the activity lifecycle } public override fun onStop() { super.onStop() myLocationListener.stop() // manage other components that need to respond // to the activity lifecycle } }
class MyLocationListener { public MyLocationListener(Context context, Callback callback) { // ... } void start() { // connect to system location service } void stop() { // disconnect from system location service } } class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; @Override public void onCreate(...) { myLocationListener = new MyLocationListener(this, (location) -> { // update UI }); } @Override public void onStart() { super.onStart(); myLocationListener.start(); // manage other components that need to respond // to the activity lifecycle } @Override public void onStop() { super.onStop(); myLocationListener.stop(); // manage other components that need to respond // to the activity lifecycle } }
แม้ว่าตัวอย่างนี้ดูดี แต่ในแอปจริง คุณอาจมีการเรียกใช้ที่จัดการ UI และคอมโพเนนต์อื่นๆ มากเกินไปเพื่อตอบสนองต่อสถานะปัจจุบันของวงจร การจัดการคอมโพเนนต์หลายรายการจะเพิ่มโค้ดจำนวนมากลงในเมธอดของวงจร เช่น onStart()
และ onStop()
ซึ่งทำให้ดูแลรักษาได้ยาก
นอกจากนี้ ไม่มีการรับประกันว่าคอมโพเนนต์จะเริ่มทำงานก่อนที่กิจกรรมหรือข้อมูลโค้ดจะหยุดลง โดยเฉพาะอย่างยิ่งหากเราต้องดำเนินการที่ใช้เวลานาน เช่น การตรวจสอบการกําหนดค่าบางอย่างใน onStart()
ซึ่งอาจทำให้เกิดเงื่อนไขการแข่งขันที่เมธอด onStop()
ทำงานเสร็จก่อน onStart()
ทำให้คอมโพเนนต์ทำงานนานกว่าที่จำเป็น
class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this) { location -> // update UI } } public override fun onStart() { super.onStart() Util.checkUserStatus { result -> // what if this callback is invoked AFTER activity is stopped? if (result) { myLocationListener.start() } } } public override fun onStop() { super.onStop() myLocationListener.stop() } }
class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, location -> { // update UI }); } @Override public void onStart() { super.onStart(); Util.checkUserStatus(result -> { // what if this callback is invoked AFTER activity is stopped? if (result) { myLocationListener.start(); } }); } @Override public void onStop() { super.onStop(); myLocationListener.stop(); } }
แพ็กเกจ androidx.lifecycle
มีคลาสและอินเทอร์เฟซที่ช่วยให้คุณจัดการปัญหาเหล่านี้ได้อย่างยืดหยุ่นและแยกส่วน
วงจร
Lifecycle
เป็นคลาสที่มีข้อมูลเกี่ยวกับสถานะวงจรชีวิตของคอมโพเนนต์ (เช่น กิจกรรมหรือฟragment) และอนุญาตให้ออบเจ็กต์อื่นๆ สังเกตสถานะนี้ได้
Lifecycle
ใช้การแจกแจงหลัก 2 รายการเพื่อติดตามสถานะวงจรของลูกค้าสําหรับคอมโพเนนต์ที่เกี่ยวข้อง ดังนี้
- กิจกรรม
- เหตุการณ์ในวงจรที่ส่งมาจากเฟรมเวิร์กและคลาส
Lifecycle
เหตุการณ์เหล่านี้จะแมปกับเหตุการณ์ Callback ในกิจกรรมและข้อมูลโค้ด - รัฐ
- สถานะปัจจุบันของคอมโพเนนต์ที่ออบเจ็กต์
Lifecycle
ติดตาม
ให้คิดว่าสถานะเป็นโหนดของกราฟ และเหตุการณ์เป็นขอบระหว่างโหนดเหล่านี้
คลาสสามารถตรวจสอบสถานะวงจรชีวิตของคอมโพเนนต์ได้โดยการใช้ DefaultLifecycleObserver
และลบล้างเมธอดที่เกี่ยวข้อง เช่น onCreate
, onStart
เป็นต้น จากนั้นคุณสามารถเพิ่มผู้สังเกตการณ์ได้โดยเรียกใช้เมธอด addObserver()
ของคลาส Lifecycle
และส่งอินสแตนซ์ของผู้สังเกตการณ์ ดังที่แสดงในตัวอย่างต่อไปนี้
class MyObserver : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { connect() } override fun onPause(owner: LifecycleOwner) { disconnect() } } myLifecycleOwner.getLifecycle().addObserver(MyObserver())
public class MyObserver implements DefaultLifecycleObserver { @Override public void onResume(LifecycleOwner owner) { connect() } @Override public void onPause(LifecycleOwner owner) { disconnect() } } myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
ในตัวอย่างข้างต้น ออบเจ็กต์ myLifecycleOwner
ใช้อินเทอร์เฟซ LifecycleOwner
ซึ่งอธิบายไว้ในส่วนถัดไป
LifecycleOwner
LifecycleOwner
เป็นอินเทอร์เฟซเมธอดเดียวที่ระบุว่าคลาสมี Lifecycle
โดยจะมีเมธอดเดียว getLifecycle()
ซึ่งคลาสต้องนำมาใช้งาน
หากต้องการจัดการวงจรของกระบวนการแอปพลิเคชันทั้งหมดแทน โปรดดูProcessLifecycleOwner
อินเทอร์เฟซนี้จะแยกการเป็นเจ้าของ Lifecycle
ออกจากคลาสแต่ละคลาส เช่น Fragment
และ AppCompatActivity
และอนุญาตให้เขียนคอมโพเนนต์ที่ทำงานร่วมกับคลาสเหล่านั้นได้ คลาสแอปพลิเคชันที่กำหนดเองใดๆ สามารถใช้อินเทอร์เฟซ LifecycleOwner
ได้
คอมโพเนนต์ที่ใช้ DefaultLifecycleObserver
จะทํางานร่วมกับคอมโพเนนต์ที่ใช้ LifecycleOwner
ได้อย่างราบรื่น เนื่องจากเจ้าของสามารถระบุวงจรชีวิตของคอมโพเนนต์ ซึ่งผู้สังเกตการณ์สามารถลงทะเบียนเพื่อดูได้
สําหรับตัวอย่างการติดตามตําแหน่ง เราสามารถสร้างคลาส MyLocationListener
ให้ใช้งาน DefaultLifecycleObserver
แล้วเริ่มต้นด้วย Lifecycle
ของกิจกรรมในเมธอด onCreate()
วิธีนี้ช่วยให้คลาส MyLocationListener
ทำงานได้ด้วยตัวเอง ซึ่งหมายความว่าจะมีการประกาศตรรกะในการตอบสนองต่อการเปลี่ยนแปลงสถานะวงจรใน MyLocationListener
แทนกิจกรรม การที่คอมโพเนนต์แต่ละรายการจัดเก็บตรรกะของตนเองจะช่วยให้จัดการตรรกะของกิจกรรมและข้อมูลโค้ดได้ง่ายขึ้น
class MyActivity : AppCompatActivity() { private lateinit var myLocationListener: MyLocationListener override fun onCreate(...) { myLocationListener = MyLocationListener(this, lifecycle) { location -> // update UI } Util.checkUserStatus { result -> if (result) { myLocationListener.enable() } } } }
class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, getLifecycle(), location -> { // update UI }); Util.checkUserStatus(result -> { if (result) { myLocationListener.enable(); } }); } }
Use Case ที่พบบ่อยคือหลีกเลี่ยงการเรียกใช้การเรียกกลับบางรายการหาก Lifecycle
ไม่อยู่ในสถานะที่ดีในขณะนี้ ตัวอย่างเช่น หากการเรียกกลับเรียกใช้ธุรกรรมของข้อมูลโค้ดที่แยกส่วนหลังจากบันทึกสถานะกิจกรรมแล้ว ระบบจะทริกเกอร์ข้อขัดข้อง เราจึงไม่ต้องเรียกใช้การเรียกกลับนั้น
คลาส Lifecycle
ช่วยให้ออบเจ็กต์อื่นๆ ค้นหาสถานะปัจจุบันได้ เพื่อให้กรณีการใช้งานนี้ง่ายขึ้น
internal class MyLocationListener( private val context: Context, private val lifecycle: Lifecycle, private val callback: (Location) -> Unit ): DefaultLifecycleObserver { private var enabled = false override fun onStart(owner: LifecycleOwner) { if (enabled) { // connect } } fun enable() { enabled = true if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { // connect if not connected } } override fun onStop(owner: LifecycleOwner) { // disconnect if connected } }
class MyLocationListener implements DefaultLifecycleObserver { private boolean enabled = false; public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { ... } @Override public void onStart(LifecycleOwner owner) { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getCurrentState().isAtLeast(STARTED)) { // connect if not connected } } @Override public void onStop(LifecycleOwner owner) { // disconnect if connected } }
การติดตั้งใช้งานนี้ทำให้คลาส LocationListener
ของเรารองรับวงจรของลูกค้าอย่างสมบูรณ์ หากต้องการใช้ LocationListener
จากกิจกรรมหรือส่วนอื่น ก็เพียงต้องเริ่มต้นใช้งานเท่านั้น การดำเนินการทั้งหมดในการตั้งค่าและการเลิกใช้งานจะจัดการโดยชั้นเรียนเอง
หากไลบรารีมีคลาสที่ต้องทำงานร่วมกับวงจรชีวิตของ Android เราขอแนะนำให้คุณใช้คอมโพเนนต์ที่รับรู้วงจร ลูกค้าไลบรารีผสานรวมคอมโพเนนต์เหล่านั้นได้อย่างง่ายดายโดยไม่ต้องจัดการวงจรด้วยตนเองฝั่งไคลเอ็นต์
การใช้ LifecycleOwner ที่กําหนดเอง
ส่วนต่างๆ และกิจกรรมใน Support Library 26.1.0 ขึ้นไปใช้อินเทอร์เฟซ LifecycleOwner
อยู่แล้ว
หากมีคลาสที่กำหนดเองที่ต้องการสร้าง LifecycleOwner
คุณสามารถใช้คลาส LifecycleRegistry แต่ต้องส่งต่อเหตุการณ์ไปยังคลาสนั้น ดังที่แสดงในตัวอย่างโค้ดต่อไปนี้
class MyActivity : Activity(), LifecycleOwner { private lateinit var lifecycleRegistry: LifecycleRegistry override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleRegistry = LifecycleRegistry(this) lifecycleRegistry.markState(Lifecycle.State.CREATED) } public override fun onStart() { super.onStart() lifecycleRegistry.markState(Lifecycle.State.STARTED) } override fun getLifecycle(): Lifecycle { return lifecycleRegistry } }
public class MyActivity extends Activity implements LifecycleOwner { private LifecycleRegistry lifecycleRegistry; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleRegistry = new LifecycleRegistry(this); lifecycleRegistry.markState(Lifecycle.State.CREATED); } @Override public void onStart() { super.onStart(); lifecycleRegistry.markState(Lifecycle.State.STARTED); } @NonNull @Override public Lifecycle getLifecycle() { return lifecycleRegistry; } }
แนวทางปฏิบัติแนะนำสำหรับคอมโพเนนต์ที่รับรู้วงจรของลูกค้า
- ควบคุม UI (กิจกรรมและส่วนต่างๆ) ให้น้อยที่สุดเท่าที่เป็นไปได้ โดยไม่ควรพยายามรวบรวมข้อมูลของตนเอง แต่ให้ใช้
ViewModel
แทน และสังเกตออบเจ็กต์LiveData
เพื่อแสดงการเปลี่ยนแปลงในมุมมอง - ลองเขียน UI ที่ขับเคลื่อนโดยข้อมูลโดยที่ความรับผิดชอบของเครื่องมือควบคุม UI คืออัปเดตมุมมองเมื่อข้อมูลมีการเปลี่ยนแปลง หรือแจ้งการดําเนินการของผู้ใช้กลับไปยัง
ViewModel
- ใส่ตรรกะข้อมูลในคลาส
ViewModel
ViewModel
ควรทำหน้าที่เป็นเครื่องมือเชื่อมต่อระหว่างตัวควบคุม UI กับส่วนที่เหลือของแอป แต่โปรดทราบว่าViewModel
ไม่ได้มีหน้าที่ดึงข้อมูล (เช่น จากเครือข่าย) แต่ViewModel
ควรเรียกใช้คอมโพเนนต์ที่เหมาะสมเพื่อดึงข้อมูล แล้วส่งผลลัพธ์กลับไปยังตัวควบคุม UI - ใช้การเชื่อมโยงข้อมูลเพื่อรักษาอินเทอร์เฟซที่เรียบร้อยระหว่างมุมมองกับตัวควบคุม UI ซึ่งช่วยให้คุณทำให้มุมมองเป็นแบบประกาศมากขึ้นและลดโค้ดการอัปเดตที่ต้องเขียนในกิจกรรมและข้อมูลโค้ดได้ หากต้องการดำเนินการนี้ในภาษาโปรแกรม Java ให้ใช้ไลบรารีอย่าง Butter Knife เพื่อหลีกเลี่ยงโค้ดที่ซ้ำกันและมีการสร้างแบบนามธรรมที่ดียิ่งขึ้น
- หาก UI มีความซับซ้อน ให้พิจารณาสร้างคลาส presenter เพื่อจัดการการแก้ไข UI งานนี้อาจทําได้ยาก แต่จะช่วยให้ทดสอบคอมโพเนนต์ UI ได้ง่ายขึ้น
- หลีกเลี่ยงการอ้างอิงบริบท
View
หรือActivity
ในViewModel
หากViewModel
มีอายุมากกว่ากิจกรรม (ในกรณีที่มีการเปลี่ยนแปลงการกําหนดค่า) กิจกรรมจะรั่วไหลและระบบเก็บขยะจะไม่จัดการอย่างถูกต้อง - ใช้ Kotlin coroutine เพื่อจัดการงานที่ทำงานต่อเนื่องเป็นเวลานานและการดำเนินการอื่นๆ ที่ทำงานแบบไม่พร้อมกันได้
Use Case สําหรับคอมโพเนนต์ที่ทราบวงจร
คอมโพเนนต์ที่รับรู้วงจรช่วยให้คุณจัดการวงจรได้ง่ายขึ้นอย่างมากในหลายกรณี ตัวอย่างบางส่วนมีดังนี้
- การสลับระหว่างการอัปเดตตำแหน่งแบบหยาบและแบบละเอียด ใช้คอมโพเนนต์ที่รับรู้วงจรเพื่อเปิดใช้การอัปเดตตำแหน่งแบบละเอียดขณะที่แอปตำแหน่งแสดงอยู่ และเปลี่ยนไปใช้การอัปเดตแบบหยาบเมื่อแอปทำงานอยู่เบื้องหลัง
LiveData
เป็นคอมโพเนนต์ที่รับรู้วงจร ซึ่งช่วยให้แอปอัปเดต UI โดยอัตโนมัติเมื่อผู้ใช้เปลี่ยนตำแหน่ง - การหยุดและเริ่มบัฟเฟอร์วิดีโอ ใช้คอมโพเนนต์ที่รับรู้วงจรเพื่อเริ่มบัฟเฟอร์วิดีโอโดยเร็วที่สุด แต่เลื่อนการเล่นจนกว่าแอปจะเริ่มต้นอย่างสมบูรณ์ นอกจากนี้ คุณยังใช้คอมโพเนนต์ที่รับรู้วงจรเพื่อสิ้นสุดการบัฟเฟอร์ได้เมื่อแอปถูกทำลาย
- การเริ่มและหยุดการเชื่อมต่อเครือข่าย ใช้คอมโพเนนต์ที่รับรู้วงจรเพื่อเปิดใช้การอัปเดตแบบเรียลไทม์ (สตรีมมิง) ของข้อมูลเครือข่ายขณะที่แอปอยู่เบื้องหน้า และหยุดชั่วคราวโดยอัตโนมัติเมื่อแอปทำงานอยู่ในเบื้องหลัง
- การหยุดองค์ประกอบที่วาดได้แบบเคลื่อนไหวไว้ชั่วคราวและเล่นต่อ ใช้คอมโพเนนต์ที่รับรู้วงจรเพื่อจัดการการหยุดเนื้อหาที่วาดได้แบบเคลื่อนไหวชั่วคราวเมื่อแอปอยู่เบื้องหลัง และแสดงเนื้อหาที่วาดได้ต่อหลังจากแอปอยู่เบื้องหน้า
การจัดการเหตุการณ์หยุด
เมื่อ Lifecycle
เป็นของ AppCompatActivity
หรือ Fragment
สถานะของ Lifecycle
จะเปลี่ยนเป็น CREATED
และระบบจะส่งเหตุการณ์ ON_STOP
เมื่อมีการเรียก onSaveInstanceState()
ของ AppCompatActivity
หรือ Fragment
เมื่อบันทึกสถานะของ Fragment
หรือ AppCompatActivity
ผ่าน onSaveInstanceState()
ระบบจะถือว่า UI ของ Fragment
หรือ AppCompatActivity
นั้นเปลี่ยนแปลงไม่ได้จนกว่าจะเรียกใช้ ON_START
การพยายามแก้ไข UI หลังจากบันทึกสถานะแล้วมีแนวโน้มที่จะทําให้สถานะการนําทางของแอปพลิเคชันไม่สอดคล้องกัน ด้วยเหตุนี้ FragmentManager
จึงแสดงข้อยกเว้นหากแอปเรียกใช้ FragmentTransaction
หลังจากบันทึกสถานะแล้ว ดูรายละเอียดได้ที่
commit()
LiveData
ป้องกันกรณีขอบเขตนี้ตั้งแต่เริ่มต้นโดยงดเรียกใช้ผู้สังเกตการณ์หาก Lifecycle
ที่เชื่อมโยงของผู้สังเกตการณ์มีค่าไม่น้อยกว่า STARTED
เบื้องหลังคือมีการเรียกใช้ isAtLeast()
ก่อนตัดสินใจเรียกใช้ผู้สังเกตการณ์
ขออภัย วิธีการ onStop()
ของ AppCompatActivity
เรียกว่า after
onSaveInstanceState()
ซึ่งทำให้เกิดช่องว่างที่ไม่อนุญาตให้มีการเปลี่ยนแปลงสถานะของ UI แต่ Lifecycle
ยังไม่ได้ย้ายไปยังสถานะ CREATED
เพื่อป้องกันปัญหานี้ คลาส Lifecycle
ในเวอร์ชัน beta2
และต่ำกว่าจะทําเครื่องหมายสถานะเป็น CREATED
โดยไม่มีการส่งเหตุการณ์เพื่อให้โค้ดที่ตรวจสอบสถานะปัจจุบันได้รับค่าจริง แม้ว่าระบบจะไม่ส่งเหตุการณ์จนกว่าจะมีการเรียกใช้ onStop()
แต่วิธีนี้มีปัญหาสำคัญ 2 ข้อ ได้แก่
- ใน API ระดับ 23 และต่ำกว่า ระบบ Android จะบันทึกสถานะของกิจกรรมแม้ว่ากิจกรรมนั้นจะได้รับบางส่วนจากกิจกรรมอื่นก็ตาม กล่าวคือ ระบบ Android จะเรียก
onSaveInstanceState()
แต่ไม่จำเป็นต้องเรียกonStop()
ซึ่งอาจทำให้เกิดช่วงเวลาที่นานที่ผู้สังเกตการณ์ยังคงคิดว่าวงจรยังมีการใช้งานอยู่ แม้ว่าจะไม่สามารถแก้ไขสถานะ UI ของวงจรได้ก็ตาม - คลาสที่ต้องการแสดงลักษณะการทำงานที่คล้ายกับคลาส
LiveData
ต้องใช้วิธีแก้ปัญหาที่ระบุโดยLifecycle
เวอร์ชันbeta 2
และต่ำกว่า
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการจัดการวงจรด้วยคอมโพเนนต์ที่รับรู้วงจรได้จากแหล่งข้อมูลเพิ่มเติมต่อไปนี้
ตัวอย่าง
- Sunflower เป็นแอปสาธิตที่แสดงแนวทางปฏิบัติแนะนำเกี่ยวกับคอมโพเนนต์สถาปัตยกรรม
Codelabs
บล็อก
แนะนำสำหรับคุณ
ภาพรวมของ LiveData
ใช้ LiveData เพื่อจัดการข้อมูลตามวงจรชีวิต
ใช้โครูทีน Kotlin กับคอมโพเนนต์ที่รับรู้ถึงวงจรชีวิต
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
โมดูลสถานะที่บันทึกไว้สำหรับ ViewModel
โมดูลสำหรับการจัดการสถานะที่บันทึกไว้ในออบเจ็กต์ ViewModel