New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.13.0: Automatic explicit parent scope hierarchy management #112

Merged
merged 6 commits into from Sep 10, 2018
View
@@ -1,5 +1,22 @@
# Change log
-Simple Stack 1.13.0 (2018-09-10)
--------------------------------
- ADDED: Adds `ScopeKey.Child` interface, which allows the definition of *explicit* parent hierarchy of a given scope.
Even by default, there is an implicit hierarchy between screens: it is possible to look up services defined by previous keys.
However, there are times when we must define scopes that are supersets of multiple screens. In this case, we know we are on a given screen, within a given state, and we require a superscope to exist that is shared across multiple screens.
In this case, the key can define an explicit parent hierarchy of scopes. These scopes are created before the key's own scope (assuming the key is also a ScopeKey).
The parent scopes are only destroyed after all their children are destroyed.
`lookupService()` prefers explicit parents, however will also continue to seek the service across implicit parents, and their explicit parent chain as well.
*This feature is a bit complex, so if you see any unexpected behavior, report it as soon as possible. There are however multiple unit tests to verify that behavior is generally as expected.*
-Simple Stack 1.12.3 (2018-09-02)
--------------------------------
- CHANGE: When `lookupService` cannot find the service, the exception message is improved (and tells you what *could* be wrong in your configuration).
View
@@ -68,7 +68,7 @@ In order to use Simple Stack, you need to add jitpack to your project root gradl
and add the compile dependency to your module level gradle.
compile 'com.github.Zhuinden:simple-stack:1.12.3'
compile 'com.github.Zhuinden:simple-stack:1.13.0'
## How does it work?
@@ -454,6 +454,17 @@ public boolean hasService(@NonNull ScopeKey scopeKey, @NonNull String serviceTag
return getManager().hasService(scopeKey, serviceTag);
}
/**
* Returns if a service is bound to the scope identified by the provided tag.
*
* @param scopeTag the tag of the scope
* @param serviceTag the service tag
* @return whether the service is bound in the given scope
*/
public boolean hasService(@NonNull String scopeTag, @NonNull String serviceTag) {
return getManager().hasService(scopeTag, serviceTag);
}
/**
* Returns the service bound to the scope of the {@link ScopeKey} by the provided tag.
*
@@ -467,6 +478,19 @@ public boolean hasService(@NonNull ScopeKey scopeKey, @NonNull String serviceTag
return getManager().getService(scopeKey, serviceTag);
}
/**
* Returns the service bound to the scope identified by the provided tag.
*
* @param scopeTag the tag of the scope
* @param serviceTag the service tag
* @param <T> the type of the service
* @return the service
*/
@NonNull
public <T> T getService(@NonNull String scopeTag, @NonNull String serviceTag) {
return getManager().getService(scopeTag, serviceTag);
}
/**
* Attempts to look-up the service in all currently existing scopes, starting from the last added scope.
* Returns whether the service exists in any scopes.
@@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -67,7 +68,7 @@ static String getScopesTag() {
return SCOPES_TAG;
}
private String activeScope = null;
private Object previousTopKeyWithAssociatedScope = null;
private final StateChanger managedStateChanger = new StateChanger() {
@Override
@@ -83,23 +84,63 @@ public void stateChangeComplete() {
History<Object> newState = stateChange.getNewState();
// activation/deactivation
ScopeKey topScopeKey = null;
Object newTopKeyWithAssociatedScope = null;
for(int i = 0, size = newState.size(); i < size; i++) {
Object key = newState.fromTop(i);
if(key instanceof ScopeKey) {
topScopeKey = (ScopeKey) key;
if(key instanceof ScopeKey || key instanceof ScopeKey.Child) {
newTopKeyWithAssociatedScope = key;
break;
}
}
String newScopeTag = null;
if(topScopeKey != null) {
newScopeTag = topScopeKey.getScopeTag();
Set<String> scopesToDeactivate = new LinkedHashSet<>();
Set<String> scopesToActivate = new LinkedHashSet<>();
if(previousTopKeyWithAssociatedScope != null) {
if(previousTopKeyWithAssociatedScope instanceof ScopeKey) {
ScopeKey scopeKey = (ScopeKey) previousTopKeyWithAssociatedScope;
scopesToDeactivate.add(scopeKey.getScopeTag());
}
if(previousTopKeyWithAssociatedScope instanceof ScopeKey.Child) {
ScopeKey.Child child = (ScopeKey.Child) previousTopKeyWithAssociatedScope;
ScopeManager.checkParentScopes(child);
List<String> parentScopes = child.getParentScopes();
for(int i = parentScopes.size() - 1; i >= 0; i--) {
scopesToDeactivate.add(parentScopes.get(i));
}
}
}
if(newTopKeyWithAssociatedScope != null) {
if(newTopKeyWithAssociatedScope instanceof ScopeKey.Child) {
ScopeKey.Child child = (ScopeKey.Child) newTopKeyWithAssociatedScope;
ScopeManager.checkParentScopes(child);
scopesToActivate.addAll(child.getParentScopes());
}
if(newTopKeyWithAssociatedScope instanceof ScopeKey) {
ScopeKey scopeKey = (ScopeKey) newTopKeyWithAssociatedScope;
scopesToActivate.add(scopeKey.getScopeTag());
}
}
previousTopKeyWithAssociatedScope = newTopKeyWithAssociatedScope;
// do not deactivate scopes that exist at this time
Iterator<String> scopeToActivate = scopesToActivate.iterator();
while(scopeToActivate.hasNext()) {
String activeScope = scopeToActivate.next();
if(scopesToDeactivate.contains(activeScope)) {
scopesToDeactivate.remove(activeScope); // do not deactivate an already active scope
scopeToActivate.remove(); // if the previous already contains it, then it is already activated.
// we should make sure we never activate the same service twice.
}
}
if(activeScope == null /* no active scope */
|| (activeScope != null && !activeScope.equals(newScopeTag)) /* new active scope */) {
String previousScopeTag = activeScope;
activeScope = newScopeTag;
scopeManager.dispatchActivation(previousScopeTag, newScopeTag);
if(!scopesToActivate.isEmpty() || !scopesToDeactivate.isEmpty()) { // de-morgan is an ass, but the unit tests don't lie
scopeManager.dispatchActivation(scopesToDeactivate, scopesToActivate);
}
// scope eviction + scoped
@@ -257,22 +298,42 @@ public void reattachStateChanger() {
* Note that if you use {@link BackstackDelegate} or {@link com.zhuinden.simplestack.navigator.Navigator}, then there is no need to call this method manually.
*/
public void finalizeScopes() {
if(activeScope != null) {
String previousScopeTag = activeScope;
activeScope = null;
if(previousTopKeyWithAssociatedScope != null) {
Set<String> scopesToDeactivate = new LinkedHashSet<>();
if(previousTopKeyWithAssociatedScope instanceof ScopeKey.Child) {
ScopeKey.Child child = (ScopeKey.Child) previousTopKeyWithAssociatedScope;
ScopeManager.checkParentScopes(child);
scopesToDeactivate.addAll(child.getParentScopes());
}
if(previousTopKeyWithAssociatedScope instanceof ScopeKey) {
ScopeKey scopeKey = (ScopeKey) previousTopKeyWithAssociatedScope;
scopesToDeactivate.add(scopeKey.getScopeTag());
}
//noinspection ConstantConditions
scopeManager.dispatchActivation(previousScopeTag, activeScope);
scopeManager.dispatchActivation(scopesToDeactivate, Collections.<String>emptySet());
}
List<Object> history = backstack.getHistory();
History<Object> history = backstack.getHistory();
Set<String> scopeSet = new LinkedHashSet<>();
for(Object key : history) {
for(int i = 0, size = history.size(); i < size; i++) {
Object key = history.fromTop(i);
if(key instanceof ScopeKey) {
scopeSet.add(((ScopeKey) key).getScopeTag());
}
if(key instanceof ScopeKey.Child) {
ScopeKey.Child child = (ScopeKey.Child) key;
for(String parent : child.getParentScopes()) {
if(scopeSet.contains(parent)) {
scopeSet.remove(parent); // needed to setup the proper order
}
scopeSet.add(parent);
}
}
}
List<String> scopes = new ArrayList<>(scopeSet);
Collections.reverse(scopes);
for(String scope : scopes) {
scopeManager.destroyScope(scope);
}
@@ -2,10 +2,38 @@
import android.support.annotation.NonNull;
import java.util.List;
/**
* Inheriting from {@link ScopeKey} allows defining that our key belongs to a given scope, that a scope is associated with it.
*/
public interface ScopeKey {
/**
* Defines the tag of the scope this key defines the existence of.
*
* @return the tag of the scope
*/
@NonNull
String getScopeTag();
/**
* Inheriting from {@link Child} enables defining an explicit parent hierarchy, thus ensuring that
* even if the scopes are not defined by the tags of any existing keys in the {@link Backstack}'s current {@link History},
* the scopes will still be created, bound and shall exist.
*
* During {@link BackstackManager#lookupService(String)}, the explicit parents are traversed first, and implicit parents second. Implicit scope inheritance means that the previous keys' scopes will be traversed as well.
*
* If a {@link Child} is the top-most scope, then its explicit scopes are activated as well, so their services have {@link com.zhuinden.simplestack.ScopedServices.Activated#onScopeActive(String)} called if they implement {@link com.zhuinden.simplestack.ScopedServices.Activated}.
*/
public interface Child {
/**
* Defines the hierarchy of the parent scope that ought to exist as explicit parents of this key.
*
* The order of the items matters: the top-most scope is the first, the bottom-most parent is the last.
*
* @return the list of scope tags that ought to serve as the key's hierarchy of explicit parents
*/
@NonNull
List<String> getParentScopes();
}
}
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.