简体   繁体   中英

Accessibility focus is reset in Fragments

I have a button on the screen, when I open a new Fragment by clicking on that button with TalkBack and return to the previous Fragment ( fm.popBackStack() ), the button should get accessability focus again. But instead, the focus is reset to the first element on the screen as on the first visit. The same with RecyclerView, when I open a new Fragment by clicking on an item, then when I return, the focus should return to the same item on which it was. It works as expected with activity, but not with fragments. How it can be fixed?

Tested on Android 9, 11 and Fragment 1.3.0, 1.4.0, 1.4.1 versions

在此处输入图像描述

This will be a long answer because this question has various solutions depending on your needs, and I decided to take time and be thorough because accessibility is essential!

tl;dr solution for your problem:

To correctly save the instance state of Fragment you should do the following:

  1. In the fragment, save instance state by overriding onSaveInstanceState() and restore in onActivityCreated() :

class MyFragment extends Fragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ...
    if (savedInstanceState != null) {
        //Restore the fragment's state here
    }
}
...
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    
    //Save the fragment's state here
}
}
  1. And important point, in the activity, you have to save the fragment's instance in onSaveInstanceState() and restore in onCreate()

class MyActivity extends Activity {
    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
            
        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }
}

Deep dive - Managing UI state

You can efficiently save and restore the UI state by dividing the work among the various types of persistence mechanisms. In most cases, each of these mechanisms should store a different kind of data used in the activity, based on the tradeoffs of data complexity, access speed, and lifetime:

  • Local persistence: Stores all data you don't want to lose if you open and close the activity. Example: A collection of song objects, including audio files and metadata. ViewModel : Stores all the data needed to display the associated UI Controller in memory. Example: The song objects of the most recent search and search query.
  • onSaveInstanceState() : Stores a small amount of data needed to quickly reload the activity state if the system stops and then recreates the UI Controller. Instead of storing complex objects here, persist the complex objects in local storage and store a unique ID for these objects in onSaveInstanceState() Example: Storing the most recent search query. As an example, consider an activity that allows you to search through your library of songs. Here's how different events should be handled:

The ViewModel immediately delegates this data locally when the user adds a song. If this newly added song should be shown in the UI, you should also update the data in the ViewModel object to reflect the addition of the song. Remember to do all database inserts off of the main thread.

When the user searches for a song, whatever complex song data you load from the database for the UI Controller should be immediately stored in the ViewModel object. You should also save the search query itself in the ViewModel object.

When the activity goes into the background, the system calls onSaveInstanceState(). You should save the search query in the onSaveInstanceState() bundle. This small amount of data is easy to save. It's also all the information you need to get the activity back into its current state.


Options for preserving UI state

When the user's expectations about UI state do not match default system behaviour, you must save and restore the user's UI state to ensure that the system-initiated destruction is transparent to the user.

Each of the options for preserving UI state vary along the following dimensions that impact the user experience:

ViewModel Saved instance state Persistent storage
Storage location in memory serialized to disk on disk or network
Survives configuration change Yes Yes Yes
Survives system-initiated process death No Yes Yes
Survives user complete activity dismissal/onFinish() No No Yes
Data limitations complex objects are fine, but space is limited by available memory only for primitive types and simple, small objects such as String only limited by disk space or cost/time of retrieval from the network resource
Read/write time quick (memory access only) slow (requires serialization/deserialization and disk access) slow (requires disk access or network transaction)


SavedStateRegistry

Beginning with Fragment 1.1.0, UI controllers, such as an Activity or Fragment, implement SavedStateRegistryOwner and provide a SavedStateRegistry that is bound to that controller. SavedStateRegistry allows components to hook into your UI controller's saved state to consume or contribute to it. For example, the Saved State module for ViewModel uses SavedStateRegistry to create a SavedStateHandle and provide it to your ViewModel objects. You can retrieve the SavedStateRegistry from within your UI controller by calling getSavedStateRegistry() .

Components that contribute to the saved state must implement SavedStateRegistry . SavedStateProvider defines a single method called saveState() . The saveState() method allows your component to return a Bundle containing any state that should be saved from that component. SavedStateRegistry calls this method during the saving state phase of the UI controller's lifecycle.

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

To register a SavedStateProvider , call registerSavedStateProvider() on the SavedStateRegistry , passing a key to associate with the provider's data as well as the provider. The previously saved data for the provider can be retrieved from the saved state by calling consumeRestoredStateForKey() on the SavedStateRegistry , passing in the key associated with the provider's data.

Within an Activity or Fragment, you can register a SavedStateProvider in onCreate() after calling super.onCreate() . Alternatively, you can set a LifecycleObserver on a SavedStateRegistryOwner , which implements LifecycleOwner , and register the SavedStateProvider once the ON_CREATE event occurs. Using a LifecycleObserver lets you decouple the registration and retrieval of the previously saved state from the SavedStateRegistryOwner itself.

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

Use ViewModel to handle configuration changes

ViewModel is ideal for storing and managing UI-related data while actively using the application. It allows quick access to UI data and helps you avoid refetching data from network or disk across rotation, window resizing, and other commonly occurring configuration changes.

ViewModel retains the data in memory, which means it is cheaper to retrieve than data from the disk or the network. A ViewModel is associated with an activity (or some other lifecycle owner) - it stays in memory during a configuration change. The system automatically associates the ViewModel with the new activity instance that results from the configuration change.

ViewModels are automatically destroyed by the system when your user backs out of your activity or fragment or if you call finish() , which means the state will be cleared as the user expects in these scenarios.

Unlike saved instance states, ViewModels are destroyed during a system-initiated process death. This is why you should use ViewModel objects in combination with onSaveInstanceState() (or some other disk persistence), stashing identifiers in savedInstanceState to help view models reload the data after system death.

If you already have an in-memory solution in place for storing your UI state across configuration changes, you may not need to use ViewModel .


Use onSaveInstanceState() as backup to handle system-initiated process death

The onSaveInstanceState() callback stores data needed to reload the state of a UI controller, such as an activity or a fragment, if the system destroys and later recreates that controller.

Saved instance state bundles persist through configuration changes and process death but are limited by storage and speed because onSavedInstanceState() serializes data to disk. Serialization can consume a lot of memory if the serialized objects are complicated. Because this process happens on the main thread during a configuration change, long-running serialization can cause dropped frames and visual stutter.

Do not use onSavedInstanceState() to store large amounts of data, such as bitmaps or complex data structures requiring lengthy serialization or deserialization. Instead, store only primitive types and simple, small objects such as String. As such, use onSaveInstanceState() to store a minimal amount of data necessary, such as an ID, to re-create the data necessary to restore the UI back to its previous state should the other persistence mechanisms fail. Most apps should implement onSaveInstanceState() to handle system-initiated process death.

Depending on your app's use cases, you might not need to use onSaveInstanceState() at all.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM