โมดูลสถานะที่บันทึกไว้สำหรับ ViewModel   เป็นส่วนหนึ่งของ Android Jetpack

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

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

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

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

ตั้งค่า

ตั้งแต่ Fragment 1.2.0 หรือ Activity 1.1.0 ที่เป็น transitive dependency ของ Fragment 1.2.0 คุณจะรับ SavedStateHandle เป็นอาร์กิวเมนต์คอนสตรัคเตอร์ของ ViewModel ได้

Kotlin

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

จากนั้นคุณจะเรียกข้อมูลอินสแตนซ์ของ ViewModel ได้โดยไม่ต้องมีการกำหนดค่าเพิ่มเติม โรงงาน ViewModel เริ่มต้นจะจัดหา SavedStateHandle ที่เหมาะสมให้กับ ViewModel

Kotlin

class MainFragment : Fragment() {
    val vm: SavedStateViewModel by viewModels()

    ...
}

Java

class MainFragment extends Fragment {
    private SavedStateViewModel vm;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        vm = new ViewModelProvider(this).get(SavedStateViewModel.class);

        ...


    }

    ...
}

เมื่อระบุอินสแตนซ์ ViewModelProvider.Factory ที่กําหนดเอง คุณจะเปิดใช้SavedStateHandleได้โดยขยาย AbstractSavedStateViewModelFactory

การใช้ SavedStateHandle

คลาส SavedStateHandle คือแผนที่คีย์-ค่าที่ช่วยให้คุณเขียนและดึงข้อมูลจากสถานะที่บันทึกไว้ผ่านเมธอด set() และ get() ได้

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

SavedStateHandle ยังมีเมธอดอื่นๆ ที่คุณอาจคาดหวังเมื่อโต้ตอบกับแผนที่คีย์-ค่า ดังนี้

  • contains(String key) - ตรวจสอบว่าคีย์ที่ระบุมีค่าหรือไม่
  • remove(String key) - นำค่าของคีย์ที่ระบุออก
  • keys() - แสดงผล คีย์ทั้งหมดที่อยู่ใน SavedStateHandle

นอกจากนี้ คุณยังดึงค่าจาก SavedStateHandle โดยใช้ที่เก็บข้อมูลที่สังเกตได้ รายการประเภทที่รองรับ ได้แก่

LiveData

ดึงค่าจาก SavedStateHandle ที่รวมอยู่ใน LiveData ที่สังเกตได้โดยใช้ getLiveData() เมื่อค่าของคีย์ได้รับการอัปเดต LiveData จะได้รับค่าใหม่ ส่วนใหญ่แล้ว ระบบจะตั้งค่าเนื่องจากมีการโต้ตอบของผู้ใช้ เช่น การป้อนข้อความค้นหาเพื่อกรองรายการข้อมูล จากนั้นจะใช้ค่าที่อัปเดตนี้เพื่อเปลี่ยนรูปแบบ LiveData ได้

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
        repository.getFilteredData(query)
    }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle savedStateHandle;
    public LiveData<List<String>> filteredData;
    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        this.savedStateHandle = savedStateHandle;
        LiveData<String> queryLiveData = savedStateHandle.getLiveData("query");
        filteredData = Transformations.switchMap(queryLiveData, query -> {
            return repository.getFilteredData(query);
        });
    }

    public void setQuery(String query) {
        savedStateHandle.set("query", query);
    }
}

StateFlow

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

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: StateFlow<List<String>> =
        savedStateHandle.getStateFlow<String>("query")
            .flatMapLatest { query ->
                repository.getFilteredData(query)
            }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

การรองรับสถานะของ Compose เวอร์ชันทดลอง

อาร์ติแฟกต์ lifecycle-viewmodel-compose มี API saveable เวอร์ชันทดลองที่ช่วยให้ SavedStateHandle ทำงานร่วมกับ Saver ของ Compose ได้ เพื่อให้คุณบันทึก State ใดก็ได้ที่บันทึกผ่าน rememberSaveable ด้วย Saver ที่กําหนดเองด้วย SavedStateHandle

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

ประเภทที่รองรับ

ระบบจะบันทึกและกู้คืนข้อมูลที่เก็บไว้ใน SavedStateHandle เป็น Bundle พร้อมกับ savedInstanceState ที่เหลือสำหรับกิจกรรมหรือข้อมูลโค้ด

ประเภทที่รองรับโดยตรง

โดยค่าเริ่มต้น คุณสามารถเรียกใช้ set() และ get() ใน SavedStateHandle สำหรับประเภทข้อมูลเดียวกันกับ Bundle ดังที่แสดงด้านล่าง

การรองรับประเภท/คลาส การรองรับอาร์เรย์
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

หากคลาสไม่ได้ขยายมาจากคลาสใดคลาสหนึ่งในรายการข้างต้น ให้พิจารณาทำให้คลาสแบ่งพาร์ติชันได้โดยการใส่แอตทริบิวต์ @Parcelize ของ Kotlin หรือใช้ Parcelable โดยตรง

การบันทึกคลาสที่แบ่งออกเป็นแปลงไม่ได้

หากคลาสไม่ได้ใช้ Parcelable หรือ Serializable และไม่สามารถแก้ไขให้ใช้อินเทอร์เฟซใดอินเทอร์เฟซหนึ่งดังกล่าวได้ คุณจะบันทึกอินสแตนซ์ของคลาสนั้นลงใน SavedStateHandle โดยตรงไม่ได้

ตั้งแต่วงจร 2.3.0-alpha03 เป็นต้นไป SavedStateHandle จะช่วยให้คุณบันทึกออบเจ็กต์ใดก็ได้โดยระบุตรรกะของคุณเองสำหรับการบันทึกและกู้คืนออบเจ็กต์เป็น Bundle โดยใช้เมธอด setSavedStateProvider() SavedStateRegistry.SavedStateProvider เป็นอินเทอร์เฟซที่กําหนดวิธีเดียว saveState() ซึ่งแสดงผล Bundle ที่มีสถานะที่ต้องการบันทึก เมื่อSavedStateHandleพร้อมที่จะบันทึกสถานะแล้ว ก็จะเรียก saveState() เพื่อดึงข้อมูล Bundle จาก SavedStateProvider และบันทึก Bundle สำหรับคีย์ที่เกี่ยวข้อง

ลองดูตัวอย่างแอปที่ขอรูปภาพจากแอปกล้องผ่านACTION_IMAGE_CAPTURE Intent โดยส่งไฟล์ชั่วคราวสำหรับตำแหน่งที่กล้องควรจัดเก็บรูปภาพ TempFileViewModel จะรวมตรรกะในการสร้างไฟล์ชั่วคราวนั้น

Kotlin

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel() {
    }


    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }
}

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

Kotlin

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        savedStateHandle.setSavedStateProvider("temp_file",
            new TempFileSavedStateProvider());
    }
    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }
    }
}

หากต้องการกู้คืนข้อมูล File เมื่อผู้ใช้กลับมา ให้ดึงข้อมูล temp_file Bundle จาก SavedStateHandle Bundle นี้เหมือนกับsaveTempFile()ที่ได้จาก saveTempFile()ซึ่งมีเส้นทางแบบสัมบูรณ์ จากนั้นจะใช้ Absolute Path เพื่อสร้างอินสแตนซ์ File ใหม่ได้

Kotlin

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        Bundle tempFileBundle = savedStateHandle.get("temp_file");
        if (tempFileBundle != null) {
            tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle);
        }
        savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider());
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }

        @Nullable
        private static File restoreTempFile(Bundle bundle) {
            if (bundle.containsKey("path") {
                return File(bundle.getString("path"));
            }
            return null;
        }
    }
}

SavedStateHandle ในการทดสอบ

หากต้องการทดสอบ ViewModel ที่ใช้ SavedStateHandle เป็น Dependency ให้สร้างอินสแตนซ์ใหม่ของ SavedStateHandle ด้วยค่าทดสอบที่ต้องการ แล้วส่งค่านั้นไปยังอินสแตนซ์ ViewModel ที่คุณทดสอบ

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

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

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

Codelabs