series

Back to Jetpack: Dependencies and transitive relationships of Jetpack’s major components

Changes to using activities and fragments under AdroidX

Can you really use a Fragment? Fragments FAQ and new postures for using fragments on androidx

AndroidX Fragment1.2.2 Source code analysis

Fragment returns the stack preparation section

Jetpack’s Fragment return stack is a Fragment return stack demo

preface

As we all know, activity has a set of onSaveInstanceState-onRestoreInstanceState state saving mechanism, which aims to “system resource recovery” or “configuration change” state saving, to provide users with a better experience

On androidx, the SavedState library is provided to help activities and fragments handle state saving and recovery

This article assumes that you have some understanding of the status Saving mechanism. For this part, go to Saving UI States

For process management on Android, Ian Lake’s Who Lives and Who Dies? Process priorities on Android

The viewModel-SavedState library is introduced in this article. The viewModel-SavedState library is introduced in this article. The viewModel-SavedState library is introduced in this article

Is there anything in software engineering that the middle tier can’t solve

Before we look at the SavedState library we need to talk a little bit about ComponentActivity

As of AndroidX Activity 1.0.0, ComponentActivity becomes a base class for FragmentActivity and AppCompatActivity.

As the saying goes, “Every cause must have fruit”, with a strong curiosity, I checked the reason for the introduction of ComponentActivity.

Can see ComponentActivity inherited androidx.core.app.Com ponentActivity (in fragment library), and initially only LifecycleOwner interface is realized

The inheritance of the activity we created now looks like this:

So back to the original question, why introduce ComponentActivity? Actually look at the class structure of ComponentActivity now the answer is very clear

ComponentActivity implements five interfaces, representing its five roles in addition to the activity. In line with the principle of single function, the authorities set up an intermediate layer to delegate some functions to specialized classes. OnBackPressedDispatcherOwner is we speak fragments return stack (on the back of the Jetpack OnBackPressedDispatcher 】 fragments return stack preparation) mentioned in structure, And the SavedStateRegistryOwner is a member of the main character SavedState that we are going to talk about today

SavedState

The introduction of SavedState

implementation "Androidx. Savedstate: savedstate: 1.0.0."
Copy the code

You don’t need to declare it explicitly, because the Activity library has already introduced it internally. For the dependencies of jetpack components, see back Jetpack for the dependencies and transitions of jetpack’s main components

This is a very small library

Android ViewModels: State Persistence — SavedState

SavedStateProvider

A component that holds state that will be restored and used later

public interface SavedStateProvider {
    @NonNull
    Bundle saveState(a);
}
Copy the code

SavedStateRegistry

A component that manages the SavedStateProvider list, a registry that is bound to its owner’s lifecycle (that is, activities or fragments). Each creation lifecycle owner creates a new instance

Once the owner of the registry is created (for example, after calling the activity’s onCreate(savedInstanceState) method), its performRestore(State) method is called to restore any state saved before the system killed its owner.

void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState) {
    // ...
    if(savedState ! =null) {
        mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY);
    }
    // ...
}
Copy the code

The SavedStateProvider for each registry is identified by the unique key used to register it

private SafeIterableMap<String, SavedStateProvider> mComponents = new SafeIterableMap<>();

public void registerSavedStateProvider(@NonNull String key, @NonNull SavedStateProvider provider) {
    SavedStateProvider previous = mComponents.putIfAbsent(key, provider);
    if(previous ! =null) {
        throw new IllegalArgumentException("SavedStateProvider with the given key is already registered"); }}public void unregisterSavedStateProvider(@NonNull String key) {
    mComponents.remove(key);
}
Copy the code

Once you complete registration, you can pass consumeRestoredStateForKey (key) to use a specific key state of reduction

public Bundle consumeRestoredStateForKey(@NonNull String key) {
    if(mRestoredState ! =null) {
        Bundle result = mRestoredState.getBundle(key);
        // It is cleared after the call, and the second call returns NULL
        mRestoredState.remove(key);
        if (mRestoredState.isEmpty()) {
            mRestoredState = null;
        }
        return result;
    }
    return null;
}
Copy the code

Note that this method retrieves the saved state and then clears its internal reference, meaning that calling it twice with the same key will return NULL on the second call

Once the registry has been restored to its saved state, it is up to the provider to decide whether to require its recovered data. If not, unused restored data will be saved again the next time the owner of the registry is killed by the system

A registered provider can save its state until its owner is killed by the system. When this happens, its Bundle saveState() method is called. For each registered SavedStateProvider, state can be saved like this.

savedState.putBundle(savedStateProviderKey, savedStateProvider.saveState());
Copy the code

The source code for the performSave(outBundle) method is shown below

void performSave(@NonNull Bundle outBundle) {
    Bundle components = new Bundle();
    
    // 1. Save the unused state
    if(mRestoredState ! =null) {
        components.putAll(mRestoredState);
    }
    
    // 2. Use SavedStateProvider to save the state
    for (Iterator<Map.Entry<String, SavedStateProvider>> it = mComponents.iteratorWithAdditions(); it.hasNext(); ) {
        Map.Entry<String, SavedStateProvider> entry1 = it.next();
        components.putBundle(entry1.getKey(), entry1.getValue().saveState());
    }
    
    // 3. Save the bundle to the outBundle object
    outBundle.putBundle(SAVED_COMPONENTS_KEY, components);
}
Copy the code

Perform state saving to merge all unused states with those provided by the registry. This outBundle is the bundle passed in to the activity’s onSaveInstanceState.

SavedStateRegistryController

A component that wraps SavedStateRegistry and allows it to be controlled through its two main methods: performRestore(savedState) and performSave(outBundle). These two methods will be handled internally by methods in SavedStateRegistry.

public final class SavedStateRegistryController {
    private final SavedStateRegistryOwner mOwner;
    private final SavedStateRegistry mRegistry;

    public void performRestore(@Nullable Bundle savedState) {
        // ...
        mRegistry.performRestore(lifecycle, savedState);
    }

    public void performSave(@NonNull Bundle outBundle) { mRegistry.performSave(outBundle); }}Copy the code

SavedStateRegistryOwner

Component that holds SavedStateRegistry. By default, both ComponentActivity and Fragment in the AndroidX package implement this interface.

public interface SavedStateRegistryOwner extends LifecycleOwner {
    @NonNull
    SavedStateRegistry getSavedStateRegistry(a);
}
Copy the code

The state of the Activity is saved

One thing we need to be clear about here is what state does an activity save?

You can refer to the official documentation for this part

Simply put, the state of an activity can be divided into view state and member state

By default, the system uses the Bundle instance state to store information about each View object in the activity layout (for example, text values entered into EditText or the scrolling position of recyclerView). Therefore, if the Activity instance is destroyed and recreated, the layout state reverts to its previous state without requiring you to perform any code. (Note that the view whose state needs to be restored requires an ID.)

This part of the logic is implemented in the onSaveInstanceState method of the activity

About when saveInstanceState() is called

  • TargetSdkVersion 28 and later: executed after onStop()
  • TargetSdkVersion greater than 11 and less than 28: executed before onStop()
  • TargetSdkVersion less than 11: executed before onPause()

SavedStateRegistryOwner (); SavedStateRegistryOwner (); SavedStateRegistryOwner

public class ComponentActivity extends androidx.core.app.ComponentActivity implements SavedStateRegistryOwner {

    private final SavedStateRegistryController mSavedStateRegistryController = SavedStateRegistryController.create(this);
  
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSavedStateRegistryController.performRestore(savedInstanceState);
        // ...
    }
  
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        // ...
        // Call onSaveInstanceState to save the view state
        super.onSaveInstanceState(outState);
        mSavedStateRegistryController.performSave(outState);
    }
  
    @NonNull
    @Override
    public final SavedStateRegistry getSavedStateRegistry(a) {
        returnmSavedStateRegistryController.getSavedStateRegistry(); }}Copy the code

Its internal hold SavedStateRegistryController instance mSavedStateRegistryController, Query the saved state using the Controller’s performRestore method in the activity’s onCreate method. Use the Controller’s performSave method in onSaveInstanceState to save the state

In addition to view state and member state, an activity is also responsible for saving its internal fragment state. The onSaveInstanceState method of FragmentActivity saves the state of its internal fragment and restores the saved fragment in the onCreate method. This explains the problem of overlapping fragments if not handled properly

Fragment state is saved

Androidx Fragment uses the FragmentStateManager to handle fragment state saving

There are four ways to save the correlation

  • saveState
  • saveBasicState
  • saveViewState
  • saveInstanceState

The invocation chain is that the activity indirectly calls the FragmentManager’s saveAllState via the FragmentController, followed by the subsequent save method

Fragment state can be divided into view state, member state, and Child Fragment state

Regarding view state, the FragmentStateManager provides the saveViewSate method, which calls in two places:

  1. Called when the activity or parent Fragment triggers state saving, as described above
  2. The fragment is about to enteronDestroyViewCalled during the life cycle, with its location atFragmentManagerInside the moveToState method, which explains why the replace operation added to the return stack automatically restores the view state when it returns

Member state is handled by the state mechanism in the activity, as described in the previous section

About child fragments, fragments of the onCreate method will be called restoreChildFragmentState child fragments to restore the state, And call performSaveInstanceState in the saveBasicState method in the FragmentStateManager to save the state of the Child Fragment

Viewmodel-SavedState

2020-01-22, ViewModel-savedState 1.0.0 official release, 02-05 official release 2.2.0

 implementation "Androidx. Lifecycle: lifecycle - viewmodel - savedstate: 2.2.0." "
Copy the code

You don’t need to import the library manually because the Fragment library already imports it internally

UI State under Jetpack MVVM is usually held and stored by the ViewModel, so this module appears. After configuring this module, the ViewModel object will receive the SavedStateHandle object (key-value mapping) through its constructor. Allows you to save the state and query the saved state. These values persist after the system terminates the process and are available through the same object.

Android ViewModels: State Persistence — SavedState

SavedStateHandle

Internally holds a map of the key-values of the saved state, allowing states to be read and written that persist even after the application process is killed

SavedStateHandle is passed in through the ViewModel constructor, and the main main methods are as follows

  • T get(String key)
  • MutableLiveData getLiveData(String key)
  • void set(String key, T value)

SavedStateHandle also contains an instance of SavedStateProvider, which is used to help the ViewModel owner save state

AbstractSavedStateViewModelFactory

An implementation ViewModelFactory. KeyedFactory ViewModel Factory, it will create a request and instantiate the ViewModel SavedStateHandle associated

public abstract class AbstractSavedStateViewModelFactory extends ViewModelProvider.KeyedFactory {
  
    private final SavedStateRegistry mSavedStateRegistry;
  
    // Default state used when the saved state is empty
    private final Bundle mDefaultArgs;

    @Override
    public final <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
        // Read the saved state
        Bundle restoredState = mSavedStateRegistry.consumeRestoredStateForKey(key);
      
        // Create a handle that holds the state
        SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, mDefaultArgs);
        
        // ... 
      
        / / create the viewModel
        T viewmodel = create(key, modelClass, handle);
      
        // ... 

        returnviewmodel; }}Copy the code

SavedStateViewModelFactory

The realization of a AbstractSavedStateViewModelFactory

public final class SavedStateViewModelFactory extends AbstractSavedStateVMFactory {

    public SavedStateViewModelFactory(@NonNull Application application, @NonNull SavedStateRegistryOwner owner) {
        this(application, owner, null);
    }

    public SavedStateViewModelFactory(@NonNull Application application, @NonNull SavedStateRegistryOwner owner, @Nullable Bundle defaultArgs) {
        mSavedStateRegistry = owner.getSavedStateRegistry();
        mLifecycle = owner.getLifecycle();
        mDefaultArgs = defaultArgs;
        mApplication = application;
        mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    
	public <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
        boolean isAndroidViewModel = AndroidViewModel.class.isAssignableFrom(modelClass);
        Constructor<T> constructor;
        if (isAndroidViewModel) {
            constructor = findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE);
        } else {
            constructor = findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE);
        }
        // doesn't need SavedStateHandle
        if (constructor == null) {
            return mFactory.create(modelClass);
        }

        SavedStateHandleController controller = SavedStateHandleController.create(
                mSavedStateRegistry, mLifecycle, key, mDefaultArgs);        
        T viewmodel;
        if (isAndroidViewModel) {
            viewmodel = constructor.newInstance(mApplication, controller.getHandle());
        } else {
            viewmodel = constructor.newInstance(controller.getHandle());
        }
        viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
        return viewmodel;
        / /...}}Copy the code

The working process

ViewModelProvider(this).get(MyViewModel::class.java)
Copy the code

Create a ViewModel instance in your activity and pass this (SavedStateRegistryOwner) as a parameter that can access its SavedStateRegistry, If there is no incoming factory will pass the activity to rewrite getDefaultViewModelProviderFactory method to get the default factory. Factory then takes the saved state, wraps it in SavedStateHandle, and passes it to the ViewModel. The ViewModel can read and write to the Handle

When the activity’s onSaveInstanceState(outState) method is called, its SavedStateRegistry performSave(outState) method is executed, All saveState methods within the SavedStateProvider are executed, and once executed, outState contains the saved state

When the app is restarted, the activity and a new Registry are created, the activity’s onCreate(savedInstanceState) method is called, Then Registry’s performRestore(savedInstanceState) will be called to restore the previously saved state

The state saves the correct posture

The ViewModel constructor adds the SavedStateHandle argument and saves the data you want to save using the Handle

class WithSavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    private val key = "key"
    fun setValue(value: String) = state.set(key, value)
    fun getValue(a): LiveData<String> = state.getLiveData(key)
}
Copy the code

Don’t need to rewriteonSaveInstanceState/onRestoreInstanceStatemethods

The Demo address

SavedState is only suitable for saving lightweight data. For heavyweight operations, please consider using persistent schemes such as SP and database

About me

I am a Fly_with24

  • The Denver nuggets
  • Jane’s book
  • Github