Skip to content

5. Scoping, data or state sharing between screens

Gabor Varadi edited this page Sep 7, 2020 · 4 revisions

Defining scopes with ScopeKey

If a given key exists within the navigation history, then if it implements ScopeKey, its associated scope will be automatically created.

To determine the services that are to be bound to this scope, bindServices(ServiceBinder) is called (in ScopedServices).

A key can also define additional shared scopes using ScopeKey.Child.

Setting the scoped services

You can set scoped services on either Backstack, or Navigator (which sets it on the Backstack automatically).

Navigator.configure() 
         .setScopedServices(new DefaultServiceProvider())
         .install(...);

or

backstack.setScopedServices(new DefaultServiceProvider());
backstack.setup(...)

When using the DefaultServiceProvider, then the key should implement DefaultServiceProvider.HasServices to specify what services it wants to bind to its scope.

Service lifecycle

The binder is called only if the scope does not exist yet, so services for a given scope tag are only bound once.

The services are destroyed when there are no more keys with the associated scope tag found in the new history.

Registered

To be notified of when the service is created (and restored), and destroyed, it is possible to implement ScopedServices.Registered.

Activated

To be notified of when the service's scope becomes the topmost scope (or stops being the topmost scope), it is possible to implement ScopedServices.Activated.

Bundleable

To automatically persist/restore the state of a service to StateBundle to survive across process death, the service can implement Bundleable. (Yes, its state will be persisted/restored automatically.)

Service lookup

To find a service, we must find it by the service tag that it was registered with.

MyService service = Navigator.lookupService(context, serviceTag);

or

MyService service = backstack.lookupService(context, serviceTag);

When using the lookupService method, Simple-Stack assumes a parent-child hierarchy across all scopes that exist within the navigation history. This means that you can find the service on a given scope tag even if it was bound for a previous scope. This allows sharing data across screens without having to parcel them (however, it is recommended to use an observable holder, like BehaviorRelay or LiveData).

What service lookup actually does

The test in

        /*
         *                    PARENT0
         *            PARENT1
         *     PARENT2       PARENT3         PARENT4
         *   CHILD1 CHILD2    CHILD3     CHILD4   CHILD5
         */
        backstackManager.setup(
                History.of(
                        new ChildKey("C1", History.of("P0", "P1", "P2")),
                        new ChildKey("C2", History.of("P0", "P1", "P2")),
                        new ChildKey("C3", History.of("P0", "P1", "P3")),
                        new ChildKey("C4", History.of("P0", "P4")),
                        new ChildKey("C5", History.of("P0", "P4"))
                )
        );

is testing the following setup:

flow-test dot

Note:

  • red arrows show implicit scopes, created if the given key in the history is a ScopeKey

  • black arrows represent explicit scopes, created if the given key in history is a ScopeKey.Child, and describes its parents with a List<String>

In which a traversal from the active-most scope (CHILD5) is the following order:

CHILD5 -> PARENT4 -> CHILD4 -> CHILD3 -> PARENT3 -> PARENT1 -> PARENT0 -> CHILD2 -> PARENT2 -> CHILD1

Because lookup prefers explicit parents, but it eventually goes to implicit parents, their explicit parents, then next implicit, and so on; but it doesn't check the same scope tag twice.

Global scope

It is possible to add services to the global scope using one of the following:

Navigator.configure() 
         .setGlobalServices(GlobalServices.builder()
              .add(...)
              .build())
         .install(...);

or

backstack.setGlobalServices(GlobalServices.builder()
              .add(...)
              .build())
backstack.setup(...)

It is also possible to pass a GlobalServices.Factory instead of a GlobalServices. However, this should not be an anonymous implementation, as that would cause memory leak (it is retained across config changes).

class GlobalsFactory: GlobalServices.Factory {
    ...
}

Navigator.configure() 
         .setGlobalServices(GlobalsFactory())
         .install(...)

The global scope acts as a "shared parent" that is checked as the last, final scope for any lookup (either via implicit or explicit parent scopes). Therefore, the global scope is a shared parent to all scopes.