diff --git a/ChangeLog.md b/ChangeLog.md index 909deb6..38f7f60 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,7 @@ +Version: 1.4.1 +* 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 * 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/extension/service-core/build.gradle b/extension/service-core/build.gradle index 2ea8079..afda7c8 100644 --- a/extension/service-core/build.gradle +++ b/extension/service-core/build.gradle @@ -21,6 +21,11 @@ plugins { apply plugin: 'java' apply plugin: 'maven' +configurations { + provided + compile.extendsFrom provided +} + task sourceJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' @@ -50,7 +55,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-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 d656af8..69033e0 100644 --- a/extension/service-mediastore/build.gradle +++ b/extension/service-mediastore/build.gradle @@ -16,6 +16,11 @@ apply plugin: 'java' +configurations { + provided + compile.extendsFrom provided +} + task sourceJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' @@ -45,5 +50,5 @@ sourceSets { } dependencies { - compile rootProject.ext.lib.androidMinSdk + provided rootProject.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..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 @@ -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 7e6557d..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 @@ -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,8 @@ *

*/ public class MvcGraph { + List cachedInstancesBeforeNavigation = new ArrayList<>(); + private Logger logger = LoggerFactory.getLogger(getClass()); ScopeCache singletonScopeCache; DefaultProviderFinder defaultProviderFinder; List stateManagedObjects = new ArrayList<>(); @@ -105,6 +110,11 @@ public void onFreed(Provider provider) { if (obj instanceof Disposable) { ((Disposable) obj).onDisposed(); } + + if (obj instanceof BaseControllerImpl) { + logger.trace("--Controller freed - '{}'.", + obj.getClass().getSimpleName()); + } } } @@ -481,6 +491,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 +511,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/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/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..30016be --- /dev/null +++ b/library/android-mvc-controller/src/main/java/com/shipdream/lib/android/mvc/__MvcGraphHelper.java @@ -0,0 +1,49 @@ +package com.shipdream.lib.android.mvc; + +import com.shipdream.lib.poke.Provider; +import com.shipdream.lib.poke.ScopeCache; + +import java.util.Collection; + +/** + * 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) { + 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) { + mvcGraph.cachedInstancesBeforeNavigation.add(provider); + provider.retain(); + } + } + } + + /** + * Internal use. Don't call it in app directly. + */ + public static void releaseCachedItemsAfterNavigation(MvcGraph mvcGraph) { + //Release all cached items after the fragment navigated to is ready to show. + for (Provider provider : mvcGraph.cachedInstancesBeforeNavigation) { + if (provider != null) { + provider.release(); + } + } + mvcGraph.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/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..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 @@ -16,6 +16,8 @@ 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; @@ -94,10 +96,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); + } + }); + */ + __MvcGraphHelper.retainCachedObjectsBeforeNavigation(Injector.getGraph()); + 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 +132,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 +175,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-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()); + } + +} 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/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 @@ + + + + + + + + + + + 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..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,6 +24,8 @@ 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; @@ -39,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(); @@ -69,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(); } @@ -396,7 +411,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(); @@ -404,15 +419,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(); @@ -431,6 +445,21 @@ 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() { + //Release reference count to pair the retaining by NavigationControllerImpl + // with Injector.getGraph().retainCachedObjectsBeforeNavigation(); + __MvcGraphHelper.releaseCachedItemsAfterNavigation(Injector.getGraph()); + + if (finalLastFrag != null) { + finalLastFrag.releaseDependencies(); + } + + fragment.unregisterOnViewReadyListener(this); + } + }); transaction.replace(getContentLayoutResId(), fragment, fragmentTag); transaction.addToBackStack(fragmentTag); transaction.commit(); @@ -457,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/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 cb4cb33..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 @@ -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; @@ -31,6 +32,23 @@ public static class CachedItem { Class type; 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<>(); @@ -43,6 +61,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 +91,11 @@ public void removeCache(Class type, Annotation qualifier) { cache.remove(PokeHelper.makeProviderKey(type, qualifier)); } + /** + * Gets all cached items this cache still manages + * @return The collection of cached times + */ + public Collection getCachedItems() { + return cache.values(); + } } 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); 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 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) { 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);