/* * Copyright 2016 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; import com.shipdream.lib.poke.Component; import com.shipdream.lib.poke.Consumer; import com.shipdream.lib.poke.Graph; import com.shipdream.lib.poke.Provider; import com.shipdream.lib.poke.Provides; import com.shipdream.lib.poke.exception.CircularDependenciesException; import com.shipdream.lib.poke.exception.PokeException; import com.shipdream.lib.poke.exception.ProvideException; import com.shipdream.lib.poke.exception.ProviderMissingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import javax.inject.Inject; public class MvcGraph { final Logger logger = LoggerFactory.getLogger(getClass()); UiThreadRunner uiThreadRunner; Graph graph; { uiThreadRunner = new UiThreadRunner() { @Override public boolean isOnUiThread() { return true; } @Override public void post(Runnable runnable) { runnable.run(); } @Override public void postDelayed(Runnable runnable, long delayMs) { runnable.run(); } }; MvcComponent rootComponent = new MvcComponent("MvcRootComponent"); graph = new Graph(); prepareInternalGraph(graph, rootComponent); } Graph prepareInternalGraph(Graph graph, MvcComponent rootComponent) { try { graph.setRootComponent(rootComponent); graph.getRootComponent().register(new Object() { @Provides public UiThreadRunner uiThreadRunner() { return uiThreadRunner; } }); } catch (Graph.IllegalRootComponentException e) { throw new MvcGraphException(e.getMessage(), e); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } graph.registerDisposeListener(new Provider.DisposeListener() { @Override public <T> void onDisposed(Provider<T> provider, T instance) { if (instance != null && instance instanceof Bean) { //When the cached instance is still there free and dispose it. Bean bean = (Bean) instance; bean.onDestroy(); logger.trace("---Bean destroyed - '{}'.", provider.type().getSimpleName()); } } }); return graph; } /** * Register {@link Graph.Monitor} which will be called the graph is about to inject or release an object * * @param monitor The monitor */ public void registerMonitor(final Graph.Monitor monitor) { if (uiThreadRunner.isOnUiThread()) { graph.registerMonitor(monitor); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.registerMonitor(monitor); } }); } } /** * Register {@link Graph.Monitor} which will be called the graph is about to inject or release an object * * @param monitor The monitor */ public void unregisterMonitor(final Graph.Monitor monitor) { if (uiThreadRunner.isOnUiThread()) { graph.unregisterMonitor(monitor); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.unregisterMonitor(monitor); } }); } } /** * Clear {@link Graph.Monitor} which will be called the graph is about to inject or release an object */ public void clearMonitors() { if (uiThreadRunner.isOnUiThread()) { graph.clearMonitors(); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.clearMonitors(); } }); } } /** * Reference an injectable object and retain it. Use * {@link #dereference(Object, Class, Annotation)} to dereference it when it's not used * any more. * @param type the type of the object * @param qualifier the qualifier * @return */ public <T> T reference(Class<T> type, Annotation qualifier) throws ProviderMissingException, ProvideException, CircularDependenciesException { if (!uiThreadRunner.isOnUiThread()) { throw new MvcGraphException("Cannot reference an instance from Non-UiThread"); } return graph.reference(type, qualifier, Inject.class); } /** * Dereference an injectable object. When it's not referenced by anything else after this * dereferencing, release its cached instance if possible. * @param instance the instance is to release * @param type the type of the object * @param qualifier the qualifier */ public <T> void dereference(T instance, Class<T> type, Annotation qualifier) throws ProviderMissingException { if (uiThreadRunner.isOnUiThread()) { } else { uiThreadRunner.post(new Runnable() { @Override public void run() { } }); } graph.dereference(instance, type, qualifier, Inject.class); } /** * Same as {@link #use(Class, Annotation, Consumer)} except using un-qualified injectable type. * @param type The type of the injectable instance * @param consumer Consume to use the instance */ public <T> void use(final Class<T> type, final Consumer<T> consumer) { if (uiThreadRunner.isOnUiThread()) { try { graph.use(type, Inject.class, consumer); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } else { uiThreadRunner.post(new Runnable() { @Override public void run() { try { graph.use(type, Inject.class, consumer); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } }); } } /** * Use an injectable instance in the scope of {@link Consumer#consume(Object)} without injecting * it as a field of an object. This method will automatically retain the instance before * {@link Consumer#consume(Object)} is called and released after it's returned. As a result, * it doesn't hold the instance like the field marked by {@link Inject} that will retain the * reference of the instance until {@link #release(Object)} is called. However, in the * scope of {@link Consumer#consume(Object)} the instance will be held. * * <p>For example,</p> * <pre> interface Os { } static class DeviceComponent extends Component { @Provides @Singleton public Os provide() { return new Os(){ }; } } class Device { @Inject private Os os; } mvcGraph.register(new DeviceComponent()); //OsReferenceCount = 0 mvcGraph.use(Os.class, null, new Consumer<Os>() { @Override public void consume(Os instance) { //First time to create the instance. //OsReferenceCount = 1 } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 final Device device = new Device(); mvcGraph.inject(device); //OsReferenceCount = 1 //New instance created and cached mvcGraph.use(Os.class, null, new Consumer<Os>() { @Override public void consume(Os instance) { //Since reference count is greater than 0, cached instance will be reused //OsReferenceCount = 2 Assert.assertTrue(device.os == instance); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 mvcGraph.release(device); //OsReferenceCount = 0 //Last instance released, so next time a new instance will be created mvcGraph.use(Os.class, null, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os != instance); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 mvcGraph.use(Os.class, null, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os != instance); } }); //Reference count decremented by use method automatically //OsReferenceCount = 0 //Cached instance cleared again mvcGraph.use(Os.class, null, new Consumer<Os>() { @Override public void consume(Os instance) { //OsReferenceCount = 1 mvcGraph.inject(device); //Injection will reuse the cached instance and increment the reference count //OsReferenceCount = 2 //Since the cached instance is cleared, the new instance is a newly created one. Assert.assertTrue(device.os == instance); } }); //Reference count decremented by use method automatically //OsReferenceCount = 1 mvcGraph.release(device); //OsReferenceCount = 0 * </pre> * * <p><b>Note that, if navigation is involved in {@link Consumer#consume(Object)}, though the * instance injected is still held until consume method returns, the injected instance may * loose its model when the next fragment is loaded. This is because Android doesn't load * fragment immediately by fragment manager, instead navigation will be done in the future main * loop. Therefore, if the model of an injected instance needs to be carried to the next fragment * navigated to, use {@link NavigationManager#navigate(Object)}.{@link Navigator#with(Class, Annotation, Preparer)}</b></p> * * @param type The type of the injectable instance * @param qualifier Qualifier for the injectable instance * @param consumer Consume to use the instance * @throws MvcGraphException throw when there are exceptions during the consumption of the instance */ public <T> void use(final Class<T> type, final Annotation qualifier, final Consumer<T> consumer) { if (uiThreadRunner.isOnUiThread()) { try { graph.use(type, qualifier, Inject.class, consumer); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } else { uiThreadRunner.post(new Runnable() { @Override public void run() { try { graph.use(type, qualifier, Inject.class, consumer); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } }); } } /** * Inject all fields annotated by {@link Inject}. References of controllers will be * incremented. * * @param target The target object whose fields annotated by {@link Inject} will be injected. */ public void inject(final Object target) { if (uiThreadRunner.isOnUiThread()) { try { graph.inject(target, Inject.class); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } else { uiThreadRunner.post(new Runnable() { @Override public void run() { try { graph.inject(target, Inject.class); } catch (PokeException e) { throw new MvcGraphException(e.getMessage(), e); } } }); } } /** * Release cached instances held by fields of target object. References of instances of the * instances will be decremented. Once the reference count of a contract type reaches 0, it will * be removed from the instances. * * @param target of which the object fields will be released. */ public void release(final Object target) { if (uiThreadRunner.isOnUiThread()) { try { graph.release(target, Inject.class); } catch (ProviderMissingException e) { throw new MvcGraphException(e.getMessage(), e); } } else { uiThreadRunner.post(new Runnable() { @Override public void run() { try { graph.release(target, Inject.class); } catch (ProviderMissingException e) { throw new MvcGraphException(e.getMessage(), e); } } }); } } /** * Add {@link Component} to the graph. * * @param component The root {@link Component} of this graph. */ public void setRootComponent(final MvcComponent component) { if (uiThreadRunner.isOnUiThread()) { try { graph.setRootComponent(component); } catch (Graph.IllegalRootComponentException e) { throw new MvcGraphException(e.getMessage(), e); } } else { uiThreadRunner.post(new Runnable() { @Override public void run() { try { graph.setRootComponent(component); } catch (Graph.IllegalRootComponentException e) { throw new MvcGraphException(e.getMessage(), e); } } }); } } public MvcComponent getRootComponent() { return (MvcComponent) graph.getRootComponent(); } /** * Register {@link Provider.DereferenceListener} which will be called when the provider * * @param onProviderFreedListener The listener */ public void registerDereferencedListener( final Provider.DereferenceListener onProviderFreedListener) { if (uiThreadRunner.isOnUiThread()) { graph.registerDereferencedListener(onProviderFreedListener); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.registerDereferencedListener(onProviderFreedListener); } }); } } /** * Unregister {@link Provider.DereferenceListener} which will be called when the last cached * instance of an injected contract is freed. * * @param onProviderFreedListener The listener */ public void unregisterDereferencedListener( final Provider.DereferenceListener onProviderFreedListener) { if (uiThreadRunner.isOnUiThread()) { graph.unregisterDereferencedListener(onProviderFreedListener); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.unregisterDereferencedListener(onProviderFreedListener); } }); } } /** * Clear {@link Provider.DereferenceListener}s which will be called when the last cached * instance of an injected contract is freed. */ public void clearDereferencedListeners() { if (uiThreadRunner.isOnUiThread()) { graph.clearDereferencedListeners(); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { graph.clearDereferencedListeners(); } }); } } }