From 7c5f5e10644b6c786670ce411c9ede317ba8f1a7 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sun, 15 Nov 2015 13:13:27 +1100 Subject: [PATCH 1/9] Retain controller state until navigation is fully done --- ChangeLog.md | 3 ++ build.gradle | 2 +- .../shipdream/lib/android/mvc/MvcGraph.java | 29 ++++++++++++++++++ .../mvc/controller/NavigationController.java | 30 +++++++++++++++++++ .../internal/NavigationControllerImpl.java | 22 ++++++++++++-- .../lib/android/mvc/view/EventRegister.java | 8 ++--- .../lib/android/mvc/view/MvcActivity.java | 10 ++++++- .../com/shipdream/lib/poke/ScopeCache.java | 19 ++++++++++++ 8 files changed, 114 insertions(+), 9 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 909deb6..6b05c17 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,6 @@ +Version: 1.4.1 +* Navigation will retain the state of all injected state managed objects including controllers until the next fragment is ready and shown. + Version: 1.4.0 * Refactor of class AndroidMvc so that controllers can access the MvcGraph * EventBusV2V is injectable same as EventBusC2V and EventBusC2C diff --git a/build.gradle b/build.gradle index 18c29d9..f265f70 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ ext { version = [ major: 1, minor: 4, - patch : 0 + patch : 1 ] libGroup = 'com.shipdream' libVersion = "${version.major}.${version.minor}.${version.patch}" diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java index 7e6557d..417b692 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java @@ -41,6 +41,9 @@ import com.shipdream.lib.poke.exception.ProviderConflictException; import com.shipdream.lib.poke.exception.ProviderMissingException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.HashMap; @@ -76,6 +79,7 @@ *

*/ public class MvcGraph { + private Logger logger = LoggerFactory.getLogger(getClass()); ScopeCache singletonScopeCache; DefaultProviderFinder defaultProviderFinder; List stateManagedObjects = new ArrayList<>(); @@ -105,6 +109,11 @@ public void onFreed(Provider provider) { if (obj instanceof Disposable) { ((Disposable) obj).onDisposed(); } + + if (obj instanceof BaseControllerImpl) { + logger.trace("--Controller freed - '{}'.", + obj.getClass().getSimpleName()); + } } } @@ -378,6 +387,22 @@ public void restoreAllStates(StateKeeper stateKeeper) { } } + /** + * Internal use. Don't call it in app directly. + */ + public void retainCachedObjects() { + //Retain all cached items before navigation. + singletonScopeCache.retainAllCachedItems(); + } + + /** + * Internal use. Don't call it in app directly. + */ + public void releaseCachedItems() { + //Release all cached items after the fragment navigated to is ready to show. + singletonScopeCache.releaseAllCachedItems(); + } + /** * Dependencies for all controllers */ @@ -481,6 +506,7 @@ public Provider findProvider(Class type, Annotation qualifier) throws } private static class MvcProvider extends ProviderByClassType { + private final Logger logger = LoggerFactory.getLogger(MvcGraph.class); private List stateManagedObjects; public MvcProvider(List stateManagedObjects, Class type, Class implementationClass) { @@ -500,6 +526,9 @@ public void onInjected(Object object) { BaseControllerImpl controller = (BaseControllerImpl) object; controller.onConstruct(); unregisterOnInjectedListener(this); + + logger.trace("++Controller injected - '{}'.", + object.getClass().getSimpleName()); } }); } diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/NavigationController.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/NavigationController.java index 15b7412..6caa270 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/NavigationController.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/NavigationController.java @@ -16,9 +16,11 @@ package com.shipdream.lib.android.mvc.controller; +import com.shipdream.lib.android.mvc.MvcGraph; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.event.BaseEventC2C; import com.shipdream.lib.android.mvc.event.ValueChangeEventC2V; +import com.shipdream.lib.poke.Consumer; /** * Controller to navigate among different fragments in the SAME activity. @@ -30,6 +32,20 @@ public interface NavigationController extends BaseController + * Forward navigating will automatically manage continuity of state before and after the + * navigation is performed. This is useful when the next navigation location needs be configured + * with some initial state.
+ * + * For example, when navigate to a fragment called TimerFragment which counts down from an + * initial time value. We can create a TimerController[TimerModel] with the initial time + * value and inject it into TimerFragment. Before we navigate to TimerFragment, we can set the + * initial time value in TimerController[TimerModel] either by an injected field in current + * object who is calling this method or use {@link MvcGraph#use(Class, Consumer)} to inject and + * set the value on the fly. This value will be carried on to TimerFragment when it's created + * and ready to show. + *

+ * * @param sender Who wants to navigate * @param locationId The id of the location navigate to */ @@ -43,6 +59,20 @@ public interface NavigationController extends BaseController + * Forward navigating will automatically manage continuity of state before and after the + * navigation is performed. This is useful when the next navigation location needs be configured + * with some initial state.
+ * + * For example, when navigate to a fragment called TimerFragment which counts down from an + * initial time value. We can create a TimerController[TimerModel] with the initial time + * value and inject it into TimerFragment. Before we navigate to TimerFragment, we can set the + * initial time value in TimerController[TimerModel] either by an injected field in current + * object who is calling this method or use {@link MvcGraph#use(Class, Consumer)} to inject and + * set the value on the fly. This value will be carried on to TimerFragment when it's created + * and ready to show. + *

+ * * @param sender Who wants to navigate * @param locationId The id of the location navigate to * @param clearTopToLocationId Null if all history locations want to be cleared otherwise, the diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java index 1928e4e..982b713 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java @@ -16,6 +16,7 @@ package com.shipdream.lib.android.mvc.controller.internal; +import com.shipdream.lib.android.mvc.Injector; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -94,10 +95,25 @@ private void doNavigateTo(Object sender, String locationId, boolean clearTop, getModel().setCurrentLocation(currentLoc); String lastLocId = lastLoc == null ? null : lastLoc.getLocationId(); + + /** + * Retain all cached state managed objects. They will be retained until the fragment is + * created and ready to show. They will be released by the view ready call back by the + * fragment navigating to. + * fragment.registerOnViewReadyListener(new Runnable() { + @Override + public void run() { + Injector.getGraph().releaseCachedItems(); + fragment.unregisterOnViewReadyListener(this); + } + }); + */ + Injector.getGraph().retainCachedObjects(); + postC2VEvent(new EventC2V.OnLocationForward(sender, lastLoc, currentLoc, clearTop, clearedTopToLocation)); - logger.debug("Nav Controller: Forward: {} -> {}", lastLocId, currentLoc.getLocationId()); + logger.trace("Nav Controller: Forward: {} -> {}", lastLocId, currentLoc.getLocationId()); } dumpHistory(); @@ -115,7 +131,7 @@ public void navigateBack(Object sender) { getModel().setCurrentLocation(previousLoc); postC2VEvent(new EventC2V.OnLocationBack(sender, currentLoc, previousLoc, false)); - logger.debug("Nav Controller: Backward: {} -> {}", currentLoc.getLocationId(), + logger.trace("Nav Controller: Backward: {} -> {}", currentLoc.getLocationId(), previousLoc == null ? "null" : previousLoc.getLocationId()); checkAppExit(sender); @@ -158,7 +174,7 @@ public void navigateBack(Object sender, String toLocationId) { if(success) { getModel().setCurrentLocation(currentLoc); postC2VEvent(new EventC2V.OnLocationBack(sender, previousLoc, currentLoc, true)); - logger.debug("Nav Controller: Backward: {} -> {}", currentLoc.getLocationId(), + logger.trace("Nav Controller: Backward: {} -> {}", currentLoc.getLocationId(), previousLoc.getLocationId()); checkAppExit(sender); diff --git a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/EventRegister.java b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/EventRegister.java index 3407c8d..a51a24d 100644 --- a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/EventRegister.java +++ b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/EventRegister.java @@ -50,10 +50,10 @@ void registerEventBuses() { eventBusC2V.register(androidComponent); eventBusV2V.register(androidComponent); eventsRegistered = true; - logger.debug("+Event bus registered for view - '{}'.", + logger.trace("+Event bus registered for view - '{}'.", androidComponent.getClass().getSimpleName()); } else { - logger.debug("!Event bus already registered for view - '{}' and its controllers.", + logger.trace("!Event bus already registered for view - '{}' and its controllers.", androidComponent.getClass().getSimpleName()); } } @@ -66,10 +66,10 @@ void unregisterEventBuses() { eventBusC2V.unregister(androidComponent); eventBusV2V.unregister(androidComponent); eventsRegistered = false; - logger.debug("-Event bus unregistered for view - '{}' and its controllers.", + logger.trace("-Event bus unregistered for view - '{}' and its controllers.", androidComponent.getClass().getSimpleName()); } else { - logger.debug("!Event bus already unregistered for view - '{}'.", + logger.trace("!Event bus already unregistered for view - '{}'.", androidComponent.getClass().getSimpleName()); } } diff --git a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java index 6f81e08..ab4954c 100644 --- a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java +++ b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java @@ -24,6 +24,7 @@ import android.support.v7.app.AppCompatActivity; import android.view.View; +import com.shipdream.lib.android.mvc.Injector; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.StateManaged; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -396,7 +397,7 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e FragmentTransaction transaction = fm.beginTransaction(); String fragmentTag = getFragmentTag(event.getCurrentValue().getLocationId()); - MvcFragment fragment; + final MvcFragment fragment; try { fragment = (MvcFragment) new ReflectUtils.newObjectByType(fragmentClass).newInstance(); fragment.injectDependencies(); @@ -431,6 +432,13 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e logger.trace("Cleared fragment back stack up to {}", tagPopTo); } + fragment.registerOnViewReadyListener(new Runnable() { + @Override + public void run() { + Injector.getGraph().releaseCachedItems(); + fragment.unregisterOnViewReadyListener(this); + } + }); transaction.replace(getContentLayoutResId(), fragment, fragmentTag); transaction.addToBackStack(fragmentTag); transaction.commit(); diff --git a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java index cb4cb33..a80ac43 100644 --- a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java +++ b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java @@ -31,6 +31,7 @@ public static class CachedItem { Class type; T instance; Annotation qualifier; + Provider provider; } protected Map cache = new HashMap<>(); @@ -43,6 +44,7 @@ T get(Provider provider) throws ProvideException { item = new CachedItem<>(); item.type = provider.type(); item.instance = provider.createInstance(); + item.provider = provider; if(item.instance == null) { String qualifierName = (provider.getQualifier() == null) ? "null" : provider.getQualifier().getClass().getName(); throw new ProvideException(String.format("Provider (type: %s, qualifier: " + @@ -72,4 +74,21 @@ public void removeCache(Class type, Annotation qualifier) { cache.remove(PokeHelper.makeProviderKey(type, qualifier)); } + /** + * Retain references of all cached items + */ + public void retainAllCachedItems() { + for (CachedItem cachedItem : cache.values()) { + cachedItem.provider.retain(); + } + } + + /** + * Release references of all cached items + */ + public void releaseAllCachedItems() { + for (CachedItem cachedItem : cache.values()) { + cachedItem.provider.release(); + } + } } From 8ea1a6d03fac14c33da102f8ecece7b11004bbb7 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sun, 15 Nov 2015 16:49:32 +1100 Subject: [PATCH 2/9] Link sample code to remind how to setup unit test --- extension/service-core/build.gradle | 6 +++- extension/service-mediastore/build.gradle | 6 +++- .../shipdream/lib/android/mvc/Injector.java | 3 ++ .../shipdream/lib/android/mvc/MvcGraph.java | 9 ++++++ .../com/shipdream/lib/poke/ScopeCache.java | 29 +++++++++++++++++-- .../internal/TestControllerBase.java | 22 +++++++++----- 6 files changed, 63 insertions(+), 12 deletions(-) diff --git a/extension/service-core/build.gradle b/extension/service-core/build.gradle index 2ea8079..57556bb 100644 --- a/extension/service-core/build.gradle +++ b/extension/service-core/build.gradle @@ -21,6 +21,10 @@ plugins { apply plugin: 'java' apply plugin: 'maven' +configurations { + provided +} + task sourceJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' @@ -50,7 +54,7 @@ sourceSets { } dependencies { - compile rootProject.ext.lib.androidMinSdk + provided rootProject.ext.lib.androidMinSdk testCompile rootProject.ext.lib.junit testCompile rootProject.ext.lib.mokito diff --git a/extension/service-mediastore/build.gradle b/extension/service-mediastore/build.gradle index d656af8..3537458 100644 --- a/extension/service-mediastore/build.gradle +++ b/extension/service-mediastore/build.gradle @@ -16,6 +16,10 @@ apply plugin: 'java' +configurations { + provided +} + task sourceJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' @@ -45,5 +49,5 @@ sourceSets { } dependencies { - compile rootProject.ext.lib.androidMinSdk + provided rootProject.ext.lib.androidMinSdk } \ No newline at end of file diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java index 615d592..4660fd8 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java @@ -23,6 +23,9 @@ public static void configGraph(MvcGraph.BaseDependencies dependencies) { * @return */ public static MvcGraph getGraph() { + if (mvcGraph == null) { + throw new RuntimeException("In unit testing, the graph needs to be mocked before running tests. See how the graph is prepared by TestControllerBase#prepareGraph() in https://github.com/kejunxia/AndroidMvc/blob/master/samples/note/core/src/test/java/com/shipdream/lib/android/mvc/samples/note/controller/internal/TestControllerBase.java"); + } return mvcGraph; } } diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java index 417b692..ad7d04f 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java @@ -46,6 +46,7 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -403,6 +404,14 @@ public void releaseCachedItems() { singletonScopeCache.releaseAllCachedItems(); } + /** + * Gets all cached items this cache still manages + * @return The collection of cached times + */ + public Collection getAllCachedInstances() { + return singletonScopeCache.getCachedItems(); + } + /** * Dependencies for all controllers */ diff --git a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java index a80ac43..45ab90c 100644 --- a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java +++ b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java @@ -19,6 +19,7 @@ import com.shipdream.lib.poke.exception.ProvideException; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,22 @@ public static class CachedItem { T instance; Annotation qualifier; Provider provider; + + public Class getType() { + return type; + } + + public T getInstance() { + return instance; + } + + public Annotation getQualifier() { + return qualifier; + } + + public Provider getProvider() { + return provider; + } } protected Map cache = new HashMap<>(); @@ -75,7 +92,7 @@ public void removeCache(Class type, Annotation qualifier) { } /** - * Retain references of all cached items + * Retains references of all cached items */ public void retainAllCachedItems() { for (CachedItem cachedItem : cache.values()) { @@ -84,11 +101,19 @@ public void retainAllCachedItems() { } /** - * Release references of all cached items + * Releases references of all cached items */ public void releaseAllCachedItems() { for (CachedItem cachedItem : cache.values()) { cachedItem.provider.release(); } } + + /** + * Gets all cached items this cache still manages + * @return The collection of cached times + */ + public Collection getCachedItems() { + return cache.values(); + } } diff --git a/samples/note/core/src/test/java/com/shipdream/lib/android/mvc/samples/note/controller/internal/TestControllerBase.java b/samples/note/core/src/test/java/com/shipdream/lib/android/mvc/samples/note/controller/internal/TestControllerBase.java index f4d7ad8..f4bd1d0 100644 --- a/samples/note/core/src/test/java/com/shipdream/lib/android/mvc/samples/note/controller/internal/TestControllerBase.java +++ b/samples/note/core/src/test/java/com/shipdream/lib/android/mvc/samples/note/controller/internal/TestControllerBase.java @@ -16,6 +16,7 @@ package com.shipdream.lib.android.mvc.samples.note.controller.internal; +import com.shipdream.lib.android.mvc.Injector; import com.shipdream.lib.android.mvc.MvcGraph; import com.shipdream.lib.android.mvc.controller.BaseController; import com.shipdream.lib.android.mvc.controller.internal.BaseControllerImpl; @@ -33,14 +34,14 @@ public abstract class TestControllerBase ext protected EventBus eventBusC2C; protected EventBus eventBusC2V; protected ExecutorService executorService; - private MvcGraph mvcGraph; protected Controller controllerToTest; - @Before - public void setUp() throws Exception { + private void prepareGraph() { eventBusC2C = new EventBusImpl(); eventBusC2V = new EventBusImpl(); - mvcGraph = new MvcGraph(new MvcGraph.BaseDependencies() { + executorService = mock(ExecutorService.class); + + Injector.configGraph(new MvcGraph.BaseDependencies() { @Override public EventBus createEventBusC2C() { return eventBusC2C; @@ -56,16 +57,21 @@ public ExecutorService createExecutorService() { return executorService; } }); - executorService = mock(ExecutorService.class); - registerDependencies(mvcGraph); + } + + @Before + public void setUp() throws Exception { + prepareGraph(); + + registerDependencies(Injector.getGraph()); controllerToTest = createTestingController(); - mvcGraph.inject(controllerToTest); + Injector.getGraph().inject(controllerToTest); ((BaseControllerImpl)controllerToTest).onConstruct(); } @After public void tearDown() throws Exception { - mvcGraph.release(controllerToTest); + Injector.getGraph().release(controllerToTest); } protected void registerDependencies(MvcGraph mvcGraph) { From 2c2fa5696be792b80ccb8c5a31ac21b7947965cd Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sun, 15 Nov 2015 17:07:01 +1100 Subject: [PATCH 3/9] Fix unit tests --- extension/service-core/build.gradle | 1 + .../internal/TestPreferencesServiceImpl.java | 1 - extension/service-mediastore/build.gradle | 3 +- .../mvc/controller/BaseControllerTest.java | 58 +++++++++++-------- .../internal/TestCounterController.java | 28 +++++---- 5 files changed, 54 insertions(+), 37 deletions(-) diff --git a/extension/service-core/build.gradle b/extension/service-core/build.gradle index 57556bb..afda7c8 100644 --- a/extension/service-core/build.gradle +++ b/extension/service-core/build.gradle @@ -23,6 +23,7 @@ apply plugin: 'maven' configurations { provided + compile.extendsFrom provided } task sourceJar(type: Jar) { diff --git a/extension/service-core/src/test/java/com/shipdream/lib/android/mvc/service/internal/TestPreferencesServiceImpl.java b/extension/service-core/src/test/java/com/shipdream/lib/android/mvc/service/internal/TestPreferencesServiceImpl.java index 4c80c9b..cad7d01 100644 --- a/extension/service-core/src/test/java/com/shipdream/lib/android/mvc/service/internal/TestPreferencesServiceImpl.java +++ b/extension/service-core/src/test/java/com/shipdream/lib/android/mvc/service/internal/TestPreferencesServiceImpl.java @@ -16,7 +16,6 @@ package com.shipdream.lib.android.mvc.service.internal; - import android.content.Context; import com.shipdream.lib.android.mvc.service.PreferenceService; diff --git a/extension/service-mediastore/build.gradle b/extension/service-mediastore/build.gradle index 3537458..69033e0 100644 --- a/extension/service-mediastore/build.gradle +++ b/extension/service-mediastore/build.gradle @@ -18,6 +18,7 @@ apply plugin: 'java' configurations { provided + compile.extendsFrom provided } task sourceJar(type: Jar) { @@ -49,5 +50,5 @@ sourceSets { } dependencies { - provided rootProject.ext.lib.androidMinSdk + provided rootProject.lib.androidMinSdk } \ No newline at end of file diff --git a/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/controller/BaseControllerTest.java b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/controller/BaseControllerTest.java index c5e9599..02aa5b2 100644 --- a/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/controller/BaseControllerTest.java +++ b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/controller/BaseControllerTest.java @@ -16,9 +16,10 @@ package com.shipdream.lib.android.mvc.controller; -import com.shipdream.lib.android.mvc.event.bus.internal.EventBusImpl; -import com.shipdream.lib.android.mvc.event.bus.EventBus; +import com.shipdream.lib.android.mvc.Injector; import com.shipdream.lib.android.mvc.MvcGraph; +import com.shipdream.lib.android.mvc.event.bus.EventBus; +import com.shipdream.lib.android.mvc.event.bus.internal.EventBusImpl; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; @@ -26,21 +27,47 @@ import org.apache.log4j.PatternLayout; import org.junit.Before; import org.junit.BeforeClass; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.util.concurrent.ExecutorService; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; public class BaseControllerTest { - protected MvcGraph graph; protected EventBus eventBusC2C; protected EventBus eventBusC2V; protected ExecutorService executorService; + private void prepareGraph() { + eventBusC2C = new EventBusImpl(); + eventBusC2V = new EventBusImpl(); + executorService = mock(ExecutorService.class); + + Injector.configGraph(new MvcGraph.BaseDependencies() { + @Override + public EventBus createEventBusC2C() { + return eventBusC2C; + } + + @Override + public EventBus createEventBusC2V() { + return eventBusC2V; + } + + @Override + public ExecutorService createExecutorService() { + return executorService; + } + }); + } + + protected MvcGraph graph; + + @Before + public void setUp() throws Exception { + prepareGraph(); + graph = Injector.getGraph(); + } + @BeforeClass public static void beforeClass() { ConsoleAppender console = new ConsoleAppender(); //create appender @@ -53,23 +80,6 @@ public static void beforeClass() { Logger.getRootLogger().addAppender(console); } - @Before - public void setUp() throws Exception { - eventBusC2C = new EventBusImpl(); - eventBusC2V = new EventBusImpl(); - executorService = mock(ExecutorService.class); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Runnable runnable = (Runnable) invocation.getArguments()[0]; - runnable.run(); - return null; - } - }).when(executorService).submit(any(Runnable.class)); - - graph = new MvcGraph(new ControllerDependencies(this)); - } - static class ControllerDependencies extends MvcGraph.BaseDependencies { private BaseControllerTest baseControllerTest; diff --git a/samples/simple/src/test/java/com/shipdream/lib/android/mvc/samples/simple/controller/internal/TestCounterController.java b/samples/simple/src/test/java/com/shipdream/lib/android/mvc/samples/simple/controller/internal/TestCounterController.java index f6a89cf..39c0fb4 100644 --- a/samples/simple/src/test/java/com/shipdream/lib/android/mvc/samples/simple/controller/internal/TestCounterController.java +++ b/samples/simple/src/test/java/com/shipdream/lib/android/mvc/samples/simple/controller/internal/TestCounterController.java @@ -16,6 +16,7 @@ package com.shipdream.lib.android.mvc.samples.simple.controller.internal; +import com.shipdream.lib.android.mvc.Injector; import com.shipdream.lib.android.mvc.MvcGraph; import com.shipdream.lib.android.mvc.controller.NavigationController; import com.shipdream.lib.android.mvc.event.bus.EventBus; @@ -54,24 +55,21 @@ public static void beforeClass() { Logger.getRootLogger().addAppender(console); } - //Dependencies of base controllers - protected EventBus eventBusC2C; - protected EventBus eventBusC2V; - protected ExecutorService executorService; //The graph used to inject private MvcGraph mvcGraph; private CounterControllerImpl counterController; - @Before - public void setUp() throws Exception { - //create instance of CounterController - counterController = new CounterControllerImpl(); + protected EventBus eventBusC2C; + protected EventBus eventBusC2V; + protected ExecutorService executorService; - //Prepare dependencies for injection + private void prepareGraph() { eventBusC2C = new EventBusImpl(); eventBusC2V = new EventBusImpl(); - mvcGraph = new MvcGraph(new MvcGraph.BaseDependencies() { + executorService = mock(ExecutorService.class); + + Injector.configGraph(new MvcGraph.BaseDependencies() { @Override public EventBus createEventBusC2C() { return eventBusC2C; @@ -87,7 +85,15 @@ public ExecutorService createExecutorService() { return executorService; } }); - executorService = mock(ExecutorService.class); + } + + @Before + public void setUp() throws Exception { + prepareGraph(); + mvcGraph = Injector.getGraph(); + + //create instance of CounterController + counterController = new CounterControllerImpl(); //inject dependencies into controller mvcGraph.inject(counterController); From 105a228737ed77f2d2c42f1241ee5e7ce5096ba3 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Mon, 16 Nov 2015 20:13:51 +1100 Subject: [PATCH 4/9] Fields with same name in both super and current class will be released correctly --- .../shipdream/lib/android/mvc/MvcGraph.java | 23 +++++- .../internal/NavigationControllerImpl.java | 2 +- .../lib/android/mvc/view/MvcActivity.java | 13 +++- .../java/com/shipdream/lib/poke/Graph.java | 78 +++++++++++++------ .../java/com/shipdream/lib/poke/Provider.java | 24 +++--- .../com/shipdream/lib/poke/ScopeCache.java | 18 ----- .../lib/poke/TestCircularDependencies.java | 75 +++++++----------- .../lib/poke/TestInjectionReferenceCount.java | 55 ++++++++++--- 8 files changed, 168 insertions(+), 120 deletions(-) diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java index ad7d04f..7b1d2c1 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java @@ -388,20 +388,35 @@ public void restoreAllStates(StateKeeper stateKeeper) { } } + private List cachedInstancesBeforeNavigation = new ArrayList<>(); + /** * Internal use. Don't call it in app directly. */ - public void retainCachedObjects() { + public void retainCachedObjectsBeforeNavigation() { + cachedInstancesBeforeNavigation.clear(); //Retain all cached items before navigation. - singletonScopeCache.retainAllCachedItems(); + Collection cachedItems = singletonScopeCache.getCachedItems(); + for (ScopeCache.CachedItem cachedItem : cachedItems) { + Provider provider = cachedItem.getProvider(); + if (provider != null) { + cachedInstancesBeforeNavigation.add(provider); + provider.retain(); + } + } } /** * Internal use. Don't call it in app directly. */ - public void releaseCachedItems() { + public void releaseCachedItemsAfterNavigation() { //Release all cached items after the fragment navigated to is ready to show. - singletonScopeCache.releaseAllCachedItems(); + for (Provider provider : cachedInstancesBeforeNavigation) { + if (provider != null) { + provider.release(); + } + } + cachedInstancesBeforeNavigation.clear(); } /** diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java index 982b713..7311bd5 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java @@ -108,7 +108,7 @@ public void run() { } }); */ - Injector.getGraph().retainCachedObjects(); + Injector.getGraph().retainCachedObjectsBeforeNavigation(); postC2VEvent(new EventC2V.OnLocationForward(sender, lastLoc, currentLoc, clearTop, clearedTopToLocation)); diff --git a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java index ab4954c..0758a0a 100644 --- a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java +++ b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java @@ -405,15 +405,14 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e throw new RuntimeException("Failed to instantiate fragment: " + fragmentClass.getName(), e); } + MvcFragment lastFrag = null; if (!event.isClearHistory()) { - MvcFragment lastFrag = null; if (event.getLastValue() != null && event.getLastValue().getLocationId() != null) { lastFrag = (MvcFragment) fm.findFragmentByTag( getFragmentTag(event.getLastValue().getLocationId())); } if (lastFrag != null) { lastFrag.onPushingToBackStack(); - lastFrag.releaseDependencies(); } } else { NavLocation clearTopToLocation = event.getLocationWhereHistoryClearedUpTo(); @@ -432,10 +431,18 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e logger.trace("Cleared fragment back stack up to {}", tagPopTo); } + final MvcFragment finalLastFrag = lastFrag; fragment.registerOnViewReadyListener(new Runnable() { @Override public void run() { - Injector.getGraph().releaseCachedItems(); + //Release reference count to pair the retaining by NavigationControllerImpl + // with Injector.getGraph().retainCachedObjectsBeforeNavigation(); + Injector.getGraph().releaseCachedItemsAfterNavigation(); + + if (finalLastFrag != null) { + finalLastFrag.releaseDependencies(); + } + fragment.unregisterOnViewReadyListener(this); } }); diff --git a/library/poke/src/main/java/com/shipdream/lib/poke/Graph.java b/library/poke/src/main/java/com/shipdream/lib/poke/Graph.java index 5ddfeb5..d1e62a7 100644 --- a/library/poke/src/main/java/com/shipdream/lib/poke/Graph.java +++ b/library/poke/src/main/java/com/shipdream/lib/poke/Graph.java @@ -46,7 +46,7 @@ public abstract class Graph { private List monitors; private String revisitedNode = null; private Set visitedInjectNodes = new LinkedHashSet<>(); - private Map> visitedFields = new HashMap<>(); + private Map>> visitedFields = new HashMap<>(); /** * Register {@link OnFreedListener} which will be called when the provider @@ -153,7 +153,7 @@ public void inject(Object target, Class injectAnnotation) monitors.get(i).onInject(target); } } - doInject(target, null, null, injectAnnotation); + doInject(target, null, null, null, injectAnnotation); visitedInjectNodes.clear(); revisitedNode = null; visitedFields.clear(); @@ -281,25 +281,29 @@ public void use(Class type, Annotation qualifier, } else { T newInstance = provider.get(); - doInject(newInstance, type, qualifier, injectAnnotation); + doInject(newInstance, null, type, qualifier, injectAnnotation); instance = newInstance; } provider.retain(); - if (provider.totalReference() == 1) { + if (provider.getReferenceCount() == 1) { provider.notifyInjected(instance); } consumer.consume(instance); - doRelease(instance, type, qualifier, injectAnnotation); + //Clear visiting records + visitedFields.clear(); + visitedInjectNodes.clear(); + + doRelease(instance, null, type, qualifier, injectAnnotation); provider.release(); checkToFreeProvider(provider); } @SuppressWarnings("unchecked") - private void doInject(Object target, Class targetType, Annotation targetQualifier, + private void doInject(Object target, Field targetField, Class targetType, Annotation targetQualifier, Class injectAnnotation) throws ProvideException, ProviderMissingException, CircularDependenciesException { boolean circularDetected = false; @@ -339,16 +343,17 @@ private void doInject(Object target, Class targetType, Annotation targetQualifie ReflectUtils.setField(target, field, impl); - boolean firstTimeInject = provider.totalReference() == 1; - if (!isFieldVisited(impl, field)) { - doInject(impl, fieldType, fieldQualifier, injectAnnotation); + boolean firstTimeInject = provider.getReferenceCount() == 1; + boolean visited = isFieldVisited(target, targetField, field); + if (!visited) { + doInject(impl, field, fieldType, fieldQualifier, injectAnnotation); } if (firstTimeInject) { provider.notifyInjected(impl); } - recordVisitField(impl, field); + recordVisitField(target, targetField, field); } } clazz = clazz.getSuperclass(); @@ -375,13 +380,13 @@ public void release(Object target, Class injectAnnotation) monitors.get(i).onRelease(target); } } - doRelease(target, null, null, injectAnnotation); + doRelease(target, null, null, null, injectAnnotation); visitedInjectNodes.clear(); revisitedNode = null; visitedFields.clear(); } - private void doRelease(Object target, Class targetType, Annotation targetQualifier, + private void doRelease(Object target, Field targetField, Class targetType, Annotation targetQualifier, final Class injectAnnotation) throws ProviderMissingException { Class clazz = target.getClass(); @@ -403,9 +408,10 @@ private void doRelease(Object target, Class targetType, Annotation targetQualifi Provider provider = getProvider(fieldType, fieldQualifier); boolean stillReferenced = provider.getReferenceCount(target, field) > 0; - if (!isFieldVisited(target, field) && stillReferenced) { - recordVisitField(target, field); - doRelease(fieldValue, fieldType, fieldQualifier, injectAnnotation); + boolean fieldVisited = isFieldVisited(target, targetField, field); + if (!fieldVisited && stillReferenced) { + recordVisitField(target, targetField, field); + doRelease(fieldValue, field, fieldType, fieldQualifier, injectAnnotation); provider.release(target, field); @@ -424,7 +430,7 @@ private void doRelease(Object target, Class targetType, Annotation targetQualifi } private void checkToFreeProvider(Provider provider) { - if (provider.totalReference() == 0) { + if (provider.getReferenceCount() == 0) { if (onProviderFreedListeners != null) { int listenerSize = onProviderFreedListeners.size(); for (int k = 0; k < listenerSize; k++) { @@ -436,18 +442,44 @@ private void checkToFreeProvider(Provider provider) { } } - private void recordVisitField(Object object, Field field) { - Set fields = visitedFields.get(object); + /** + * Records the field of a target object is visited + * @param object The field holder + * @param objectField The field which holds the object in its parent + * @param field The field of the holder + */ + private void recordVisitField(Object object, Field objectField, Field field) { + Map> bag = visitedFields.get(object); + if (bag == null) { + bag = new HashMap<>(); + visitedFields.put(object, bag); + } + Set fields = bag.get(objectField); + + String objectFiledKey = objectField == null ? "" : objectField.toGenericString(); + if(fields == null) { fields = new HashSet<>(); - visitedFields.put(object, fields); + bag.put(objectFiledKey, fields); } - fields.add(field.getName()); + fields.add(field.toGenericString()); } - private boolean isFieldVisited(Object object, Field field) { - Set fields = visitedFields.get(object); - return fields != null && fields.contains(field.getName()); + /** + * Indicates whether the field of a target object is visited + * @param object The field holder + * @param objectField The field which holds the object in its parent + * @param field The field of the holder + */ + private boolean isFieldVisited(Object object, Field objectField, Field field) { + Map> bag = visitedFields.get(object); + if (bag == null) { + return false; + } + + String objectFiledKey = objectField == null ? "" : objectField.toGenericString(); + Set fields = bag.get(objectFiledKey); + return fields != null && fields.contains(field); } private boolean recordVisit(Class classType, Annotation qualifier) { diff --git a/library/poke/src/main/java/com/shipdream/lib/poke/Provider.java b/library/poke/src/main/java/com/shipdream/lib/poke/Provider.java index 613d1c7..3dadacd 100644 --- a/library/poke/src/main/java/com/shipdream/lib/poke/Provider.java +++ b/library/poke/src/main/java/com/shipdream/lib/poke/Provider.java @@ -61,13 +61,13 @@ public interface OnFreedListener { ScopeCache scopeCache; private Annotation qualifier; - private Map> owners = new HashMap<>(); + Map> owners = new HashMap<>(); private int totalRefCount = 0; int getReferenceCount(Object owner, Field field) { Map fields = owners.get(owner); if (fields != null) { - Integer count = fields.get(field.getName()); + Integer count = fields.get(field.toGenericString()); if(count != null) { return count; } @@ -79,7 +79,7 @@ int getReferenceCount(Object owner, Field field) { /** * Increase reference count. */ - void retain() { + public void retain() { totalRefCount++; } @@ -96,19 +96,19 @@ void retain(Object owner, Field field) { owners.put(owner, fields); } - Integer count = fields.get(field.getName()); + Integer count = fields.get(field.toGenericString()); if (count == null) { - fields.put(field.getName(), 1); + fields.put(field.toGenericString(), 1); } else { count++; - fields.put(field.getName(), count); + fields.put(field.toGenericString(), count); } } /** * Decrease reference count. */ - void release() { + public void release() { totalRefCount--; } @@ -119,14 +119,14 @@ void release() { */ void release(Object owner, Field field) { Map fields = owners.get(owner); - if(fields!= null) { + if(fields != null) { release(); - Integer count = fields.get(field.getName()); + Integer count = fields.get(field.toGenericString()); if(--count > 0) { - fields.put(field.getName(), count); + fields.put(field.toGenericString(), count); } else { - fields.remove(field.getName()); + fields.remove(field.toGenericString()); } } @@ -144,7 +144,7 @@ void freeCache() { } } - int totalReference() { + public int getReferenceCount() { return totalRefCount; } diff --git a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java index 45ab90c..f7f94d1 100644 --- a/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java +++ b/library/poke/src/main/java/com/shipdream/lib/poke/ScopeCache.java @@ -91,24 +91,6 @@ public void removeCache(Class type, Annotation qualifier) { cache.remove(PokeHelper.makeProviderKey(type, qualifier)); } - /** - * Retains references of all cached items - */ - public void retainAllCachedItems() { - for (CachedItem cachedItem : cache.values()) { - cachedItem.provider.retain(); - } - } - - /** - * Releases references of all cached items - */ - public void releaseAllCachedItems() { - for (CachedItem cachedItem : cache.values()) { - cachedItem.provider.release(); - } - } - /** * Gets all cached items this cache still manages * @return The collection of cached times diff --git a/library/poke/src/test/java/com/shipdream/lib/poke/TestCircularDependencies.java b/library/poke/src/test/java/com/shipdream/lib/poke/TestCircularDependencies.java index 978ca52..06f1299 100644 --- a/library/poke/src/test/java/com/shipdream/lib/poke/TestCircularDependencies.java +++ b/library/poke/src/test/java/com/shipdream/lib/poke/TestCircularDependencies.java @@ -24,8 +24,6 @@ import org.junit.Assert; import org.junit.Test; -import java.lang.annotation.Annotation; - import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -190,26 +188,26 @@ public void test_should_inject_and_release_correctly_on_multiple_objects() throw Provider powerProvider = graph.getProvider(Power.class, null); Provider driverProvider = graph.getProvider(Driver.class, null); Provider robotProvider = graph.getProvider(Robot.class, null); - - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); + Assert.assertFalse(scopeCache.cache.isEmpty()); graph.inject(factory2, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 6); - assertReferenceCount(driverProvider, Driver.class, null, 6); - assertReferenceCount(robotProvider, Robot.class, null, 4); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.release(factory2, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.release(factory1, MyInject.class); Assert.assertTrue(scopeCache.cache.isEmpty()); + + Assert.assertTrue(powerProvider.owners.isEmpty()); + Assert.assertEquals(0, powerProvider.getReferenceCount()); + + Assert.assertTrue(driverProvider.owners.isEmpty()); + Assert.assertEquals(0, driverProvider.getReferenceCount()); + + Assert.assertTrue(robotProvider.owners.isEmpty()); + Assert.assertEquals(0, robotProvider.getReferenceCount()); } @Test @@ -226,25 +224,25 @@ public void test_should_inject_and_release_correctly_even_with_same_cached_objec Provider driverProvider = graph.getProvider(Driver.class, null); Provider robotProvider = graph.getProvider(Robot.class, null); - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.inject(factory, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 6); - assertReferenceCount(driverProvider, Driver.class, null, 6); - assertReferenceCount(robotProvider, Robot.class, null, 4); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.release(factory, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.release(factory, MyInject.class); Assert.assertTrue(scopeCache.cache.isEmpty()); + + Assert.assertTrue(powerProvider.owners.isEmpty()); + Assert.assertEquals(0, powerProvider.getReferenceCount()); + + Assert.assertTrue(driverProvider.owners.isEmpty()); + Assert.assertEquals(0, driverProvider.getReferenceCount()); + + Assert.assertTrue(robotProvider.owners.isEmpty()); + Assert.assertEquals(0, robotProvider.getReferenceCount()); } @Test @@ -260,23 +258,14 @@ public void test_should_inject_and_release_correctly_on_multiple_objects_even_wi Provider powerProvider = graph.getProvider(Power.class, null); Provider driverProvider = graph.getProvider(Driver.class, null); Provider robotProvider = graph.getProvider(Robot.class, null); - - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); + Assert.assertFalse(scopeCache.cache.isEmpty()); final Factory factory2 = new Factory(); graph.inject(factory2, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 6); - assertReferenceCount(driverProvider, Driver.class, null, 6); - assertReferenceCount(robotProvider, Robot.class, null, 4); Assert.assertFalse(scopeCache.cache.isEmpty()); graph.inject(factory1, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 9); - assertReferenceCount(driverProvider, Driver.class, null, 9); - assertReferenceCount(robotProvider, Robot.class, null, 6); Assert.assertFalse(scopeCache.cache.isEmpty()); Assert.assertNotNull(factory1); @@ -300,9 +289,6 @@ public void test_should_inject_and_release_correctly_on_multiple_objects_even_wi Assert.assertNotNull(((RobotImpl)((DriverImpl) factory2.driver).robot).power); graph.release(factory1, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 6); - assertReferenceCount(driverProvider, Driver.class, null, 6); - assertReferenceCount(robotProvider, Robot.class, null, 4); Assert.assertFalse(scopeCache.cache.isEmpty()); Assert.assertNotNull(factory1); @@ -326,9 +312,6 @@ public void test_should_inject_and_release_correctly_on_multiple_objects_even_wi Assert.assertNotNull(((RobotImpl)((DriverImpl) factory2.driver).robot).power); graph.release(factory1, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); Assert.assertFalse(scopeCache.cache.isEmpty()); Assert.assertNotNull(factory1); @@ -346,9 +329,6 @@ public void test_should_inject_and_release_correctly_on_multiple_objects_even_wi Assert.assertNotNull(((RobotImpl)((DriverImpl) factory2.driver).robot).power); graph.release(factory1, MyInject.class); - assertReferenceCount(powerProvider, Power.class, null, 3); - assertReferenceCount(driverProvider, Driver.class, null, 3); - assertReferenceCount(robotProvider, Robot.class, null, 2); Assert.assertFalse(scopeCache.cache.isEmpty()); Assert.assertNotNull(factory1); @@ -367,14 +347,15 @@ public void test_should_inject_and_release_correctly_on_multiple_objects_even_wi graph.release(factory2, MyInject.class); Assert.assertTrue(scopeCache.cache.isEmpty()); - } - private void assertReferenceCount(Provider provider, Class type, Annotation qualifier, int count) { - if (provider != null) { - Assert.assertEquals(provider.totalReference(), count); - } else { - Assert.assertEquals(0, count); - } + Assert.assertTrue(powerProvider.owners.isEmpty()); + Assert.assertEquals(0, powerProvider.getReferenceCount()); + + Assert.assertTrue(driverProvider.owners.isEmpty()); + Assert.assertEquals(0, driverProvider.getReferenceCount()); + + Assert.assertTrue(robotProvider.owners.isEmpty()); + Assert.assertEquals(0, robotProvider.getReferenceCount()); } private void prepareInjection(ScopeCache scopeCache, SimpleGraph graph) throws ProviderConflictException { diff --git a/library/poke/src/test/java/com/shipdream/lib/poke/TestInjectionReferenceCount.java b/library/poke/src/test/java/com/shipdream/lib/poke/TestInjectionReferenceCount.java index 41057ec..4e68a58 100644 --- a/library/poke/src/test/java/com/shipdream/lib/poke/TestInjectionReferenceCount.java +++ b/library/poke/src/test/java/com/shipdream/lib/poke/TestInjectionReferenceCount.java @@ -114,13 +114,44 @@ public void referenceCountCanReduceCascadinglyFromRoot() throws ProvideException Provider containerProvider = graph.getProvider(Container.class, null); Provider fruitProvider = graph.getProvider(Fruit.class, null); - Assert.assertTrue(containerProvider.totalReference() == 1); - Assert.assertTrue(fruitProvider.totalReference() == 2); + Assert.assertTrue(containerProvider.getReferenceCount() == 1); + Assert.assertTrue(fruitProvider.getReferenceCount() == 2); graph.release(house, MyInject.class); Assert.assertTrue(component.getScopeCache().cache.isEmpty()); } + static class Mansion extends House { + @MyInject + private Container container; + } + + @Test + public void should_be_able_to_release_inherited_fields_with_same_name() throws ProvideException, ProviderConflictException, + CircularDependenciesException, ProviderMissingException { + SimpleGraph graph = new SimpleGraph(); + Component component = new TestComp2(); + graph.register(component); + + Mansion mansion = new Mansion(); + graph.inject(mansion, MyInject.class); + + Assert.assertNotNull(mansion.container); + Assert.assertNotNull(((Fridge) mansion.container).a); + Assert.assertNotNull(((Fridge) mansion.container).b); + Assert.assertTrue(((Fridge) mansion.container).a == ((Fridge) mansion.container).b); + + Provider containerProvider = graph.getProvider(Container.class, null); + Provider fruitProvider = graph.getProvider(Fruit.class, null); + + //container has been referenced twice by the fields Mansion.container and Mansion.House.container + Assert.assertEquals(2, containerProvider.getReferenceCount()); + Assert.assertEquals(4, fruitProvider.getReferenceCount()); + + graph.release(mansion, MyInject.class); + Assert.assertTrue(component.getScopeCache().cache.isEmpty()); + } + static class Kitchen { @MyInject private Container container; @@ -153,8 +184,8 @@ public void referenceCountCanReduceCascadinglyFromSubNode() throws ProvideExcept Provider containerProvider = graph.getProvider(Container.class, null); Provider fruitProvider = graph.getProvider(Fruit.class, null); - Assert.assertEquals(1, containerProvider.totalReference()); - Assert.assertEquals(4, fruitProvider.totalReference()); + Assert.assertEquals(1, containerProvider.getReferenceCount()); + Assert.assertEquals(4, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(kitchen.container); @@ -162,14 +193,14 @@ public void referenceCountCanReduceCascadinglyFromSubNode() throws ProvideExcept Assert.assertNotNull(kitchen.bOnFloor); Assert.assertNotNull(fridge.a); Assert.assertNotNull(fridge.b); - Assert.assertEquals(1, containerProvider.totalReference()); - Assert.assertEquals(2, fruitProvider.totalReference()); + Assert.assertEquals(1, containerProvider.getReferenceCount()); + Assert.assertEquals(2, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(fridge.a); Assert.assertNotNull(fridge.b); - Assert.assertEquals(1, containerProvider.totalReference()); - Assert.assertEquals(2, fruitProvider.totalReference()); + Assert.assertEquals(1, containerProvider.getReferenceCount()); + Assert.assertEquals(2, fruitProvider.getReferenceCount()); } @Test @@ -186,16 +217,16 @@ public void releaseInjectedFieldsShouldSetThemNull() throws ProvideException, Pr Provider fruitProvider = graph.getProvider(Fruit.class, null); graph.release(kitchen.container, MyInject.class); - Assert.assertEquals(1, containerProvider.totalReference()); - Assert.assertEquals(2, fruitProvider.totalReference()); + Assert.assertEquals(1, containerProvider.getReferenceCount()); + Assert.assertEquals(2, fruitProvider.getReferenceCount()); graph.release(kitchen.container, MyInject.class); Assert.assertNotNull(kitchen.aOnFloor); Assert.assertNotNull(kitchen.bOnFloor); - Assert.assertTrue(containerProvider.totalReference() == 1); - Assert.assertTrue(fruitProvider.totalReference() == 2); + Assert.assertTrue(containerProvider.getReferenceCount() == 1); + Assert.assertTrue(fruitProvider.getReferenceCount() == 2); graph.release(kitchen, MyInject.class); From 89a464200e8e780a8eaea6c740ef2af1a36edeca Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Mon, 16 Nov 2015 20:26:59 +1100 Subject: [PATCH 5/9] Hide help methods of the frameworks out from MvcGraph --- .../shipdream/lib/android/mvc/MvcGraph.java | 40 --------------- .../lib/android/mvc/MvcGraphHelper.java | 50 +++++++++++++++++++ .../internal/NavigationControllerImpl.java | 3 +- .../lib/android/mvc/view/MvcActivity.java | 4 +- 4 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java index 7b1d2c1..744614b 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java @@ -46,7 +46,6 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -388,45 +387,6 @@ public void restoreAllStates(StateKeeper stateKeeper) { } } - private List cachedInstancesBeforeNavigation = new ArrayList<>(); - - /** - * Internal use. Don't call it in app directly. - */ - public void retainCachedObjectsBeforeNavigation() { - cachedInstancesBeforeNavigation.clear(); - //Retain all cached items before navigation. - Collection cachedItems = singletonScopeCache.getCachedItems(); - for (ScopeCache.CachedItem cachedItem : cachedItems) { - Provider provider = cachedItem.getProvider(); - if (provider != null) { - cachedInstancesBeforeNavigation.add(provider); - provider.retain(); - } - } - } - - /** - * Internal use. Don't call it in app directly. - */ - public void releaseCachedItemsAfterNavigation() { - //Release all cached items after the fragment navigated to is ready to show. - for (Provider provider : cachedInstancesBeforeNavigation) { - if (provider != null) { - provider.release(); - } - } - cachedInstancesBeforeNavigation.clear(); - } - - /** - * Gets all cached items this cache still manages - * @return The collection of cached times - */ - public Collection getAllCachedInstances() { - return singletonScopeCache.getCachedItems(); - } - /** * Dependencies for all controllers */ diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java new file mode 100644 index 0000000..984655f --- /dev/null +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java @@ -0,0 +1,50 @@ +package com.shipdream.lib.android.mvc; + +import com.shipdream.lib.poke.Provider; +import com.shipdream.lib.poke.ScopeCache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class MvcGraphHelper { + private static List cachedInstancesBeforeNavigation = new ArrayList<>(); + + /** + * Internal use. Don't call it in app directly. + */ + public static void retainCachedObjectsBeforeNavigation(MvcGraph mvcGraph) { + cachedInstancesBeforeNavigation.clear(); + //Retain all cached items before navigation. + Collection cachedItems = mvcGraph.singletonScopeCache.getCachedItems(); + for (ScopeCache.CachedItem cachedItem : cachedItems) { + Provider provider = cachedItem.getProvider(); + if (provider != null) { + cachedInstancesBeforeNavigation.add(provider); + provider.retain(); + } + } + } + + /** + * Internal use. Don't call it in app directly. + */ + public static void releaseCachedItemsAfterNavigation() { + //Release all cached items after the fragment navigated to is ready to show. + for (Provider provider : cachedInstancesBeforeNavigation) { + if (provider != null) { + provider.release(); + } + } + cachedInstancesBeforeNavigation.clear(); + } + + /** + * Internal use. Gets all cached items this cache still manages + * @return The collection of cached times + */ + public static Collection getAllCachedInstances(MvcGraph mvcGraph) { + return mvcGraph.singletonScopeCache.getCachedItems(); + } + +} diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java index 7311bd5..c305f6f 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java @@ -17,6 +17,7 @@ package com.shipdream.lib.android.mvc.controller.internal; import com.shipdream.lib.android.mvc.Injector; +import com.shipdream.lib.android.mvc.MvcGraphHelper; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -108,7 +109,7 @@ public void run() { } }); */ - Injector.getGraph().retainCachedObjectsBeforeNavigation(); + MvcGraphHelper.retainCachedObjectsBeforeNavigation(Injector.getGraph()); postC2VEvent(new EventC2V.OnLocationForward(sender, lastLoc, currentLoc, clearTop, clearedTopToLocation)); diff --git a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java index 0758a0a..5f6cb7e 100644 --- a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java +++ b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java @@ -24,7 +24,7 @@ import android.support.v7.app.AppCompatActivity; import android.view.View; -import com.shipdream.lib.android.mvc.Injector; +import com.shipdream.lib.android.mvc.MvcGraphHelper; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.StateManaged; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -437,7 +437,7 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e public void run() { //Release reference count to pair the retaining by NavigationControllerImpl // with Injector.getGraph().retainCachedObjectsBeforeNavigation(); - Injector.getGraph().releaseCachedItemsAfterNavigation(); + MvcGraphHelper.releaseCachedItemsAfterNavigation(); if (finalLastFrag != null) { finalLastFrag.releaseDependencies(); From cd50dc38824b49ca906f0114cbdcbe5c141bbba5 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Tue, 17 Nov 2015 20:57:13 +1100 Subject: [PATCH 6/9] Log how many uncleaned injected objects are remained on app UI exits --- .../shipdream/lib/android/mvc/MvcGraph.java | 1 + ...GraphHelper.java => __MvcGraphHelper.java} | 19 ++++++++-------- .../internal/NavigationControllerImpl.java | 4 ++-- .../lib/android/mvc/view/MvcActivity.java | 22 ++++++++++++++++--- .../note/android/src/main/assets/logback.xml | 2 +- 5 files changed, 32 insertions(+), 16 deletions(-) rename library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/{MvcGraphHelper.java => __MvcGraphHelper.java} (72%) diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java index 744614b..379dca9 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraph.java @@ -79,6 +79,7 @@ *

*/ public class MvcGraph { + List cachedInstancesBeforeNavigation = new ArrayList<>(); private Logger logger = LoggerFactory.getLogger(getClass()); ScopeCache singletonScopeCache; DefaultProviderFinder defaultProviderFinder; diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/__MvcGraphHelper.java similarity index 72% rename from library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java rename to library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/__MvcGraphHelper.java index 984655f..30016be 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphHelper.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/__MvcGraphHelper.java @@ -3,24 +3,23 @@ import com.shipdream.lib.poke.Provider; import com.shipdream.lib.poke.ScopeCache; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; - -public class MvcGraphHelper { - private static List cachedInstancesBeforeNavigation = new ArrayList<>(); +/** + * Helper class to work with MvcGraph. Internal use only. Don't use it in your app. + */ +public class __MvcGraphHelper { /** * Internal use. Don't call it in app directly. */ public static void retainCachedObjectsBeforeNavigation(MvcGraph mvcGraph) { - cachedInstancesBeforeNavigation.clear(); + mvcGraph.cachedInstancesBeforeNavigation.clear(); //Retain all cached items before navigation. Collection cachedItems = mvcGraph.singletonScopeCache.getCachedItems(); for (ScopeCache.CachedItem cachedItem : cachedItems) { Provider provider = cachedItem.getProvider(); if (provider != null) { - cachedInstancesBeforeNavigation.add(provider); + mvcGraph.cachedInstancesBeforeNavigation.add(provider); provider.retain(); } } @@ -29,14 +28,14 @@ public static void retainCachedObjectsBeforeNavigation(MvcGraph mvcGraph) { /** * Internal use. Don't call it in app directly. */ - public static void releaseCachedItemsAfterNavigation() { + public static void releaseCachedItemsAfterNavigation(MvcGraph mvcGraph) { //Release all cached items after the fragment navigated to is ready to show. - for (Provider provider : cachedInstancesBeforeNavigation) { + for (Provider provider : mvcGraph.cachedInstancesBeforeNavigation) { if (provider != null) { provider.release(); } } - cachedInstancesBeforeNavigation.clear(); + mvcGraph.cachedInstancesBeforeNavigation.clear(); } /** diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java index c305f6f..3c25c2c 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/controller/internal/NavigationControllerImpl.java @@ -17,7 +17,7 @@ package com.shipdream.lib.android.mvc.controller.internal; import com.shipdream.lib.android.mvc.Injector; -import com.shipdream.lib.android.mvc.MvcGraphHelper; +import com.shipdream.lib.android.mvc.__MvcGraphHelper; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -109,7 +109,7 @@ public void run() { } }); */ - MvcGraphHelper.retainCachedObjectsBeforeNavigation(Injector.getGraph()); + __MvcGraphHelper.retainCachedObjectsBeforeNavigation(Injector.getGraph()); postC2VEvent(new EventC2V.OnLocationForward(sender, lastLoc, currentLoc, clearTop, clearedTopToLocation)); diff --git a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java index 5f6cb7e..84ffa1b 100644 --- a/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java +++ b/library/android-mvc/src/main/java/com/shipdream/lib/android/mvc/view/MvcActivity.java @@ -24,7 +24,8 @@ import android.support.v7.app.AppCompatActivity; import android.view.View; -import com.shipdream.lib.android.mvc.MvcGraphHelper; +import com.shipdream.lib.android.mvc.Injector; +import com.shipdream.lib.android.mvc.__MvcGraphHelper; import com.shipdream.lib.android.mvc.NavLocation; import com.shipdream.lib.android.mvc.StateManaged; import com.shipdream.lib.android.mvc.controller.NavigationController; @@ -40,8 +41,10 @@ import javax.inject.Inject; public abstract class MvcActivity extends AppCompatActivity { + private Logger logger = LoggerFactory.getLogger(getClass()); private static final String FRAGMENT_TAG_PREFIX = "__--AndroidMvc:Fragment:"; private DelegateFragment delegateFragment; + boolean toPrintAppExitMessage = false; String getDelegateFragmentTag() { return FRAGMENT_TAG_PREFIX + getDelegateFragmentClass().getName(); @@ -70,6 +73,17 @@ protected void onCreate(Bundle savedInstanceState) { } } + @Override + protected void onDestroy() { + super.onDestroy(); + + if (toPrintAppExitMessage && logger.isTraceEnabled()) { + logger.trace("App Exits(UI): {} injected objects are still cached.", + __MvcGraphHelper.getAllCachedInstances(Injector.getGraph()).size()); + toPrintAppExitMessage = false; + } + } + void performSuperBackKeyPressed () { super.onBackPressed(); } @@ -437,7 +451,7 @@ private void performForwardNav(NavigationController.EventC2V.OnLocationForward e public void run() { //Release reference count to pair the retaining by NavigationControllerImpl // with Injector.getGraph().retainCachedObjectsBeforeNavigation(); - MvcGraphHelper.releaseCachedItemsAfterNavigation(); + __MvcGraphHelper.releaseCachedItemsAfterNavigation(Injector.getGraph()); if (finalLastFrag != null) { finalLastFrag.releaseDependencies(); @@ -472,8 +486,10 @@ public void run() { private void performBackNav(NavigationController.EventC2V.OnLocationBack event) { NavLocation currentLoc = event.getCurrentValue(); if (currentLoc == null) { + MvcActivity mvcActivity = ((MvcActivity) getActivity()); //Back to null which should finish the current activity - ((MvcActivity)getActivity()).performSuperBackKeyPressed(); + mvcActivity.performSuperBackKeyPressed(); + mvcActivity.toPrintAppExitMessage = true; } else { //FIXME: ChildFragmentManager hack - use getChildFragmentManager when bug is fixed FragmentManager fm = childFragmentManager(); diff --git a/samples/note/android/src/main/assets/logback.xml b/samples/note/android/src/main/assets/logback.xml index f5f8ed9..05a4614 100644 --- a/samples/note/android/src/main/assets/logback.xml +++ b/samples/note/android/src/main/assets/logback.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file From 7daed0e07fcab8420b8b3b2eae3ee4d8b4132eec Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sat, 21 Nov 2015 12:22:02 +1100 Subject: [PATCH 7/9] UI test for the bug fix that MvcGraph.use will retain the injected instance until the navigation is completely done --- .../nav/TestCaseNavigationFromController.java | 117 ++++++++++++++++++ .../lib/android/mvc/view/nav/ControllerE.java | 28 +++++ .../android/mvc/view/nav/DisposeCheckerE.java | 22 ++++ .../view/nav/MvcTestActivityNavigation.java | 3 + .../android/mvc/view/nav/NavFragmentE.java | 47 +++++++ .../view/nav/internal/ControllerEImpl.java | 54 ++++++++ .../nav/internal/DisposeCheckerEImpl.java | 28 +++++ .../res/layout/fragment_mvc_test_nav_e.xml | 34 +++++ 8 files changed, 333 insertions(+) create mode 100644 library/android-mvc-test/src/androidTest/java/com/shipdream/lib/android/mvc/view/nav/TestCaseNavigationFromController.java create mode 100644 library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/ControllerE.java create mode 100644 library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/DisposeCheckerE.java create mode 100644 library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/NavFragmentE.java create mode 100644 library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/ControllerEImpl.java create mode 100644 library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/DisposeCheckerEImpl.java create mode 100644 library/android-mvc-test/src/main/res/layout/fragment_mvc_test_nav_e.xml diff --git a/library/android-mvc-test/src/androidTest/java/com/shipdream/lib/android/mvc/view/nav/TestCaseNavigationFromController.java b/library/android-mvc-test/src/androidTest/java/com/shipdream/lib/android/mvc/view/nav/TestCaseNavigationFromController.java new file mode 100644 index 0000000..4d1dde0 --- /dev/null +++ b/library/android-mvc-test/src/androidTest/java/com/shipdream/lib/android/mvc/view/nav/TestCaseNavigationFromController.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav; + +import com.shipdream.lib.android.mvc.Injector; +import com.shipdream.lib.android.mvc.controller.NavigationController; +import com.shipdream.lib.android.mvc.view.AndroidMvc; +import com.shipdream.lib.android.mvc.view.BaseTestCase; +import com.shipdream.lib.poke.Component; +import com.shipdream.lib.poke.Consumer; +import com.shipdream.lib.poke.Provides; +import com.shipdream.lib.poke.ScopeCache; + +import org.junit.Test; + +import java.util.Random; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class TestCaseNavigationFromController extends BaseTestCase { + @Inject + private NavigationController navigationController; + + private Comp comp; + private DisposeCheckerE disposeCheckerEMock; + + public TestCaseNavigationFromController() { + super(MvcTestActivityNavigation.class); + } + + @Override + protected void waitTest() throws InterruptedException { + waitTest(200); + } + + public static class Comp extends Component{ + TestCaseNavigationFromController testCaseNavigation; + + Comp(ScopeCache scopeCache) { + super(scopeCache); + } + + @Singleton + @Provides + public DisposeCheckerE providesDisposeCheckerE() { + return testCaseNavigation.disposeCheckerEMock; + } + } + + @Override + protected void injectDependencies(ScopeCache mvcSingletonCache) { + super.injectDependencies(mvcSingletonCache); + + disposeCheckerEMock = mock(DisposeCheckerE.class); + comp = new Comp(mvcSingletonCache); + comp.testCaseNavigation = this; + AndroidMvc.graph().register(comp); + } + + @Override + protected void cleanDependencies() { + super.cleanDependencies(); + AndroidMvc.graph().unregister(comp); + } + + @Test + public void testShouldReleaseInjectionsAfterFragmentsArePoppedOut() throws Throwable { + onView(withText(NavFragmentA.class.getSimpleName())).check(matches(isDisplayed())); + + final String val = "Value = " + new Random().nextInt(); + + Injector.getGraph().use(ControllerE.class, new Consumer() { + @Override + public void consume(ControllerE instance) { + instance.setValue(val); + navigationController.navigateTo(this, MvcTestActivityNavigation.Loc.E); + } + }); + + //The value set to controller e in Injector.getGraph().use should be retained during the + //navigation + onView(withText(val)).check(matches(isDisplayed())); + + //The controller should not be disposed yet + verify(disposeCheckerEMock, times(0)).onDisposed(); + + navigationController.navigateBack(this); + + //Controller should be disposed after navigated away from fragment E + waitTest(); + verify(disposeCheckerEMock, times(1)).onDisposed(); + } + +} diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/ControllerE.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/ControllerE.java new file mode 100644 index 0000000..4df87b5 --- /dev/null +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/ControllerE.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav; + +import com.shipdream.lib.android.mvc.controller.BaseController; + +public interface ControllerE extends BaseController { + class Model { + public String value; + } + + void setValue(String value); + String getValue(); +} diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/DisposeCheckerE.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/DisposeCheckerE.java new file mode 100644 index 0000000..00a3701 --- /dev/null +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/DisposeCheckerE.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav; + +import com.shipdream.lib.android.mvc.Disposable; + +public interface DisposeCheckerE extends Disposable { +} diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/MvcTestActivityNavigation.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/MvcTestActivityNavigation.java index 3eead6b..8a605e1 100644 --- a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/MvcTestActivityNavigation.java +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/MvcTestActivityNavigation.java @@ -31,6 +31,7 @@ public static class Loc { public static final String B = "TestNavigationFragmentB"; public static final String C = "TestNavigationFragmentC"; public static final String D = "TestNavigationFragmentD"; + public static final String E = "TestNavigationFragmentE"; } @Override @@ -44,6 +45,8 @@ protected Class mapNavigationFragment(String locationId) return NavFragmentC.class; case Loc.D : return NavFragmentD.class; + case Loc.E : + return NavFragmentE.class; default: return null; } diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/NavFragmentE.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/NavFragmentE.java new file mode 100644 index 0000000..d64a476 --- /dev/null +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/NavFragmentE.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav; + +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import com.shipdream.lib.android.mvc.view.MvcFragment; +import com.shipdream.lib.android.mvc.view.test.R; + +import javax.inject.Inject; + +public class NavFragmentE extends MvcFragment { + @Inject + private ControllerE controllerE; + + private TextView textView; + + @Override + protected int getLayoutResId() { + return R.layout.fragment_mvc_test_nav_e; + } + + @Override + public void onViewReady(View view, Bundle savedInstanceState, Reason reason) { + super.onViewReady(view, savedInstanceState, reason); + textView = (TextView) view.findViewById(R.id.nav_frag_e_text); + + textView.setText(controllerE.getValue()); + } + +} diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/ControllerEImpl.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/ControllerEImpl.java new file mode 100644 index 0000000..597700a --- /dev/null +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/ControllerEImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav.internal; + +import android.util.Log; + +import com.shipdream.lib.android.mvc.controller.internal.BaseControllerImpl; +import com.shipdream.lib.android.mvc.view.nav.ControllerE; +import com.shipdream.lib.android.mvc.view.nav.DisposeCheckerE; + +import javax.inject.Inject; + +public class ControllerEImpl extends BaseControllerImpl implements ControllerE { + private int value = 0; + + @Inject + private DisposeCheckerE disposeCheckerE; + + @Override + public void onDisposed() { + Log.i("DisposeCheck", "Checker E disposed"); + disposeCheckerE.onDisposed(); + } + + @Override + protected Class getModelClassType() { + return Model.class; + } + + @Override + public void setValue(String value) { + getModel().value = value; + } + + @Override + public String getValue() { + return getModel().value; + } + +} diff --git a/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/DisposeCheckerEImpl.java b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/DisposeCheckerEImpl.java new file mode 100644 index 0000000..2416d4e --- /dev/null +++ b/library/android-mvc-test/src/main/java/com/shipdream/lib/android/mvc/view/nav/internal/DisposeCheckerEImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Kejun Xia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.shipdream.lib.android.mvc.view.nav.internal; + +import android.util.Log; + +import com.shipdream.lib.android.mvc.view.nav.DisposeCheckerE; + +public class DisposeCheckerEImpl implements DisposeCheckerE { + @Override + public void onDisposed() { + Log.i("DisposeCheck", "Checker E disposed"); + } +} diff --git a/library/android-mvc-test/src/main/res/layout/fragment_mvc_test_nav_e.xml b/library/android-mvc-test/src/main/res/layout/fragment_mvc_test_nav_e.xml new file mode 100644 index 0000000..488cea6 --- /dev/null +++ b/library/android-mvc-test/src/main/res/layout/fragment_mvc_test_nav_e.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + From b6162f128ea0c782bcf7df8f53d6de35dee0b4b2 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sat, 21 Nov 2015 12:32:25 +1100 Subject: [PATCH 8/9] Update ChangeLog --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 6b05c17..38f7f60 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,6 @@ Version: 1.4.1 -* Navigation will retain the state of all injected state managed objects including controllers until the next fragment is ready and shown. +* Able to initialize the controller state referenced by next navigating fragment and retain the state until the navigation is fully performed when the navigation is requested directly via NavigationController in MvcGraph.use method. +* Cached instances will be dereferenced and disposed when the instance is referenced by the fields with same variable name in both super and current class. Version: 1.4.0 * Refactor of class AndroidMvc so that controllers can access the MvcGraph From 082a19702c50b6f92fca8cbb907e5692e7d1f564 Mon Sep 17 00:00:00 2001 From: Kejun Xia Date: Sat, 21 Nov 2015 17:26:51 +1100 Subject: [PATCH 9/9] More unit tests --- .../shipdream/lib/android/mvc/Injector.java | 2 +- .../lib/android/mvc/MvcGraphException.java | 6 +- .../lib/android/mvc/TestInjector.java | 55 +++++++++++++++++++ .../lib/android/mvc/TestMvcGraphHelper.java | 50 +++++++++++++++++ 4 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestInjector.java create mode 100644 library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestMvcGraphHelper.java diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java index 4660fd8..9caa5cb 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/Injector.java @@ -3,7 +3,7 @@ import com.shipdream.lib.poke.exception.PokeException; public class Injector { - private static MvcGraph mvcGraph; + static MvcGraph mvcGraph; /** * Config the dependencies of MvcGraph. Be careful to use this method because it will dump the diff --git a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphException.java b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphException.java index 37e1289..32ccf82 100644 --- a/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphException.java +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/MvcGraphException.java @@ -1,11 +1,7 @@ package com.shipdream.lib.android.mvc; public class MvcGraphException extends RuntimeException { - public MvcGraphException(String message) { - super(message); - } - - public MvcGraphException(String message, Throwable cause) { + MvcGraphException(String message, Throwable cause) { super(message, cause); } } \ No newline at end of file diff --git a/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestInjector.java b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestInjector.java new file mode 100644 index 0000000..3477571 --- /dev/null +++ b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestInjector.java @@ -0,0 +1,55 @@ +package com.shipdream.lib.android.mvc; + +import com.shipdream.lib.android.mvc.event.bus.EventBus; +import com.shipdream.lib.android.mvc.event.bus.annotation.EventBusC2C; +import com.shipdream.lib.android.mvc.event.bus.internal.EventBusImpl; +import com.shipdream.lib.poke.Component; +import com.shipdream.lib.poke.Provides; + +import org.junit.After; +import org.junit.Test; + +import java.util.concurrent.ExecutorService; + +import javax.inject.Singleton; + +import static org.mockito.Mockito.mock; + +public class TestInjector { + @After + public void tearDown() throws Exception { + Injector.mvcGraph = null; + } + + @Test(expected = RuntimeException.class) + public void should_raise_exception_when_getting_mvc_graph_before_configuring_it() { + Injector.getGraph(); + } + + static class Comp extends Component { + @Provides + @EventBusC2C + @Singleton + public EventBus providesIEventBusC2C() { + return new EventBusImpl(); + } + } + + @Test(expected = RuntimeException.class) + public void should_raise_runtime_exception_when_exception_occurring_when_configuring_mvc_graph_by_injector() { + MvcGraph.BaseDependencies baseDependencies = new MvcGraph.BaseDependencies() { + @Override + protected ExecutorService createExecutorService() { + return mock(ExecutorService.class); + } + }; + + Injector.configGraph(baseDependencies); + + //Register component providing duplicate instances + Injector.getGraph().register(new Comp()); + + //Exception should be raised here + Injector.configGraph(baseDependencies); + } +} diff --git a/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestMvcGraphHelper.java b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestMvcGraphHelper.java new file mode 100644 index 0000000..36f9deb --- /dev/null +++ b/library/android-mvc-controller/src/test/java/com/shipdream/lib/android/mvc/TestMvcGraphHelper.java @@ -0,0 +1,50 @@ +package com.shipdream.lib.android.mvc; + +import com.shipdream.lib.android.mvc.controller.NavigationController; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.ExecutorService; + +import javax.inject.Inject; + +import static org.mockito.Mockito.mock; + +public class TestMvcGraphHelper { + @After + public void tearDown() throws Exception { + Injector.mvcGraph = null; + } + + class View { + @Inject + private NavigationController navigationController; + } + + @Test + public void should_retain_and_release_cached_instance_by_mvcgraph_helper() { + //Prepare graph + MvcGraph.BaseDependencies baseDependencies = new MvcGraph.BaseDependencies() { + @Override + protected ExecutorService createExecutorService() { + return mock(ExecutorService.class); + } + }; + + Injector.configGraph(baseDependencies); + + View view = new View(); + Injector.getGraph().inject(view); + + int initSize = __MvcGraphHelper.getAllCachedInstances(Injector.getGraph()).size(); + + __MvcGraphHelper.retainCachedObjectsBeforeNavigation(Injector.getGraph()); + Assert.assertNotEquals(initSize, __MvcGraphHelper.getAllCachedInstances(Injector.getGraph())); + + __MvcGraphHelper.releaseCachedItemsAfterNavigation(Injector.getGraph()); + Assert.assertEquals(initSize, __MvcGraphHelper.getAllCachedInstances(Injector.getGraph()).size()); + } + +}