All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.shipdream.lib.android.mvc.MvcGraph Maven / Gradle / Ivy

/*
 * 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;

import com.shipdream.lib.android.mvc.controller.NavigationController;
import com.shipdream.lib.android.mvc.controller.internal.AsyncTask;
import com.shipdream.lib.android.mvc.controller.internal.BaseControllerImpl;
import com.shipdream.lib.android.mvc.controller.internal.Navigator;
import com.shipdream.lib.android.mvc.controller.internal.Preparer;
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.annotation.EventBusC2V;
import com.shipdream.lib.android.mvc.event.bus.internal.EventBusImpl;
import com.shipdream.lib.poke.Component;
import com.shipdream.lib.poke.Consumer;
import com.shipdream.lib.poke.Graph;
import com.shipdream.lib.poke.ImplClassLocator;
import com.shipdream.lib.poke.ImplClassLocatorByPattern;
import com.shipdream.lib.poke.ImplClassNotFoundException;
import com.shipdream.lib.poke.Provider;
import com.shipdream.lib.poke.Provider.OnFreedListener;
import com.shipdream.lib.poke.ProviderByClassType;
import com.shipdream.lib.poke.ProviderFinderByRegistry;
import com.shipdream.lib.poke.Provides;
import com.shipdream.lib.poke.ScopeCache;
import com.shipdream.lib.poke.SimpleGraph;
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.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;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * {@link MvcGraph} injects instances and all its nested dependencies to target object
 * recursively. By default, all injected instances and their dependencies that are located by
 * naming convention will be SINGLETON. It can also register custom injections by
 * {@link #register(Component)}.
 * 

* Priority of finding implementation of contract is by
*

    *
  1. Registered implementation by {@link #register(Component)}
  2. *
  3. General class name mapping strategy. Implementation for interface a.b.c.SomeContract * should be named as a.b.c.internal.SomeContractImpl. Interface: a.b.c.SomeContract --> * a.b.c.internal.SomeContractImpl
  4. *
*

* As described above, explicit implementation can be registered by {@link #register(Component)}. * Once an implementation is registered, it will override the default auto implementation locating * described above. This would be handy in unit testing where if partial of real implementations * are wanted to be used and the other are mocks.
*

*

Note that, qualifier will be ignore for dependencies injected by naming convention * strategy, though qualifier of provide methods of registered {@link Component} will still * be taken into account. *

*/ public class MvcGraph { private Logger logger = LoggerFactory.getLogger(getClass()); ScopeCache singletonScopeCache; DefaultProviderFinder defaultProviderFinder; List stateManagedObjects = new ArrayList<>(); //Composite graph to hide methods Graph graph; public MvcGraph(BaseDependencies baseDependencies) throws ProvideException, ProviderConflictException { singletonScopeCache = new ScopeCache(); defaultProviderFinder = new DefaultProviderFinder(MvcGraph.this); defaultProviderFinder.register(new __Component(singletonScopeCache, baseDependencies)); graph = new SimpleGraph(defaultProviderFinder); graph.registerProviderFreedListener(new OnFreedListener() { @Override public void onFreed(Provider provider) { Object obj = provider.findCachedInstance(); if (obj != null) { //When the cached instance is still there free and dispose it. if (obj instanceof StateManaged) { stateManagedObjects.remove(obj); } if (obj instanceof Disposable) { ((Disposable) obj).onDisposed(); } if (obj instanceof BaseControllerImpl) { logger.trace("--Controller freed - '{}'.", obj.getClass().getSimpleName()); } } } }); } /** * For testing to hijack the cache * @param singletonScopeCache the cache to hijack */ void hijack(ScopeCache singletonScopeCache) { this.singletonScopeCache = singletonScopeCache; } /** * 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(Graph.Monitor monitor) { 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(Graph.Monitor monitor) { graph.unregisterMonitor(monitor); } /** * Clear {@link Graph.Monitor} which will be called the graph is about to inject or release an object */ public void clearMonitors() { graph.clearMonitors(); } /** * Register {@link OnFreedListener} which will be called when the last cached * instance of an injected contract is freed. * * @param onProviderFreedListener The listener */ public void registerProviderFreedListener(OnFreedListener onProviderFreedListener) { graph.registerProviderFreedListener(onProviderFreedListener); } /** * Unregister {@link OnFreedListener} which will be called when the last cached * instance of an injected contract is freed. * * @param onProviderFreedListener The listener */ public void unregisterProviderFreedListener(OnFreedListener onProviderFreedListener) { graph.unregisterProviderFreedListener(onProviderFreedListener); } /** * Clear {@link OnFreedListener}s which will be called when the last cached * instance of an injected contract is freed. */ public void clearOnProviderFreedListeners() { graph.clearOnProviderFreedListeners(); } /** * 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 reference(Class type, Annotation qualifier) throws ProviderMissingException, ProvideException, CircularDependenciesException { 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 type the type of the object * @param qualifier the qualifier */ public void dereference(T instance, Class type, Annotation qualifier) throws ProviderMissingException { 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 void use(final Class type, final Consumer consumer) { 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. * *

For example,

*
        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() {
            @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() {
            @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() {
            @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() {
            @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() {
            @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
     * 
* *

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 state 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 state of an injected instance needs to be carried to the next fragment * navigated to, use {@link NavigationController#navigate(Object)}.{@link Navigator#with(Class, Annotation, Preparer)}

* * @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 void use(final Class type, final Annotation qualifier, final Consumer consumer) { 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(Object target) { 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 cache of the * instances will be decremented. Once the reference count of a contract type reaches 0, it will * be removed from the cache. * * @param target of which the object fields will be released. */ public void release(Object target) { try { graph.release(target, Inject.class); } catch (ProviderMissingException e) { throw new MvcGraphException(e.getMessage(), e); } } /** * Register all providers listed by the {@link Component} * * @param component The component */ public void register(Component component) { try { defaultProviderFinder.register(component); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } /** * Unregister all providers listed by the {@link Component} * * @param component The component */ public void unregister(Component component) { defaultProviderFinder.unregister(component); } /** * Save state of all injected objects * @param stateKeeper The state keeper to manage the state */ public void saveAllStates(StateKeeper stateKeeper) { int size = stateManagedObjects.size(); for (int i = 0; i < size; i++) { StateManaged obj = stateManagedObjects.get(i); stateKeeper.saveState(obj.getState(), obj.getStateType()); } } /** * Restore state of all injected objects * @param stateKeeper The state keeper to manage the state */ @SuppressWarnings("unchecked") public void restoreAllStates(StateKeeper stateKeeper) { int size = stateManagedObjects.size(); for (int i = 0; i < size; i++) { StateManaged obj = stateManagedObjects.get(i); Object state = stateKeeper.getState(obj.getStateType()); if(state != null) { stateManagedObjects.get(i).restoreState(state); } } } /** * Dependencies for all controllers */ public abstract static class BaseDependencies { /** * Create a new instance of EventBus for events among controllers. This event bus will be * injected into fields annotated by {@link EventBusC2C}. * * @return The event bus */ protected EventBus createEventBusC2C() { return new EventBusImpl(); } /** * Create a new instance of EventBus for events from controllers to views. This event bus * will be injected into fields annotated by {@link EventBusC2V}. * * @return The event bus */ protected EventBus createEventBusC2V() { return new EventBusImpl(); } /** * Create a new instance of ExecutorService to support * {@link BaseControllerImpl#runAsyncTask(Object, AsyncTask)}. To run tasks really * asynchronously by calling {@link BaseControllerImpl#runAsyncTask(Object, AsyncTask)}, an * {@link ExecutorService} runs tasks on threads different from the caller of * {@link BaseControllerImpl#runAsyncTask(Object, AsyncTask)} is needed. However, to provide * a {@link ExecutorService} runs tasks on the same thread would be handy for testing. For * example, network responses can be mocked to return immediately. * * @return The {@link ExecutorService} controls on which threads tasks sent to * {@link BaseControllerImpl#runAsyncTask(Object, AsyncTask)} will be running on. */ protected abstract ExecutorService createExecutorService(); } /** * Internal use. Do use this in your code. */ public static class __Component extends Component { private final BaseDependencies baseDependencies; public __Component(ScopeCache scopeCache, BaseDependencies baseDependencies) { super(scopeCache); this.baseDependencies = baseDependencies; } @Provides @EventBusC2C @Singleton public EventBus providesIEventBusC2C() { return baseDependencies.createEventBusC2C(); } @Provides @EventBusC2V @Singleton public EventBus providesIEventBusC2V() { return baseDependencies.createEventBusC2V(); } @Provides @Singleton public ExecutorService providesExecutorService() { return baseDependencies.createExecutorService(); } } static class DefaultProviderFinder extends ProviderFinderByRegistry { private final MvcGraph mvcGraph; private final ImplClassLocator defaultImplClassLocator; private Map providers = new HashMap<>(); private DefaultProviderFinder(MvcGraph mvcGraph) { this.mvcGraph = mvcGraph; defaultImplClassLocator = new ImplClassLocatorByPattern(mvcGraph.singletonScopeCache); } @SuppressWarnings("unchecked") @Override public Provider findProvider(Class type, Annotation qualifier) throws ProviderMissingException { Provider provider = super.findProvider(type, qualifier); if (provider == null) { provider = providers.get(type); if (provider == null) { try { Class impClass = defaultImplClassLocator.locateImpl(type); provider = new MvcProvider<>(mvcGraph.stateManagedObjects, type, impClass); provider.setScopeCache(defaultImplClassLocator.getScopeCache()); providers.put(type, provider); } catch (ImplClassNotFoundException e) { throw new ProviderMissingException(type, qualifier, e); } } } return provider; } } 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) { super(type, implementationClass); this.stateManagedObjects = stateManagedObjects; } @SuppressWarnings("unchecked") @Override public T createInstance() throws ProvideException { final T newInstance = (T) super.createInstance(); if (newInstance instanceof BaseControllerImpl) { registerOnInjectedListener(new OnInjectedListener() { @Override public void onInjected(Object object) { BaseControllerImpl controller = (BaseControllerImpl) object; controller.onConstruct(); unregisterOnInjectedListener(this); logger.trace("++Controller injected - '{}'.", object.getClass().getSimpleName()); } }); } if (newInstance instanceof StateManaged) { stateManagedObjects.add((StateManaged) newInstance); } return newInstance; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy