/* * 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.android.mvc.event.bus.EventBus; import com.shipdream.lib.android.mvc.event.bus.annotation.EventBusC; import com.shipdream.lib.android.mvc.event.bus.annotation.EventBusV; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.inject.Inject; /** * <p> * A controller is a delegate of a Android view. It holds a {@link UiView} that needs to be implemented * by the corresponding Android view. Whenever the controller changes its model, it calls {@link UiView#update()} * to update the view. Be careful when you are calling {@link UiView#update()} on the non-UI thread, * because in this case, it needs to use {@link #uiThreadRunner} to post a runnable wrapping * {@link UiView#update()} to ensure the view is updated on UI thread. Use {@link #runTask(Task)} to * run heavy actions, especially network calls, on non-UI thread. * </p> * * <p> * In the above way the controller works as a presenter in MVP pattern. If you prefer MVVM pattern. * You can define events and {@link #postEvent(Object)} to the view. Since {@link #postEvent(Object)} * guarantees the event is posted onto UI thread, you don't need to worry about on which thread the * event is posted. * </p> * * <p> * When some code needs to run on non-UI thread, use {@link #runTask(Task)}, * {@link #runTask(Task, Task.Callback)} to run it on a different thread. Make sure if * {@link UiView#update()} needs to be called in scope of @link Task#execute(Task.Monitor)} use * {@link #uiThreadRunner} to post back to UI thread. * </p> * * <p> * The controller has 4 injected fields. They can be replaced by providing special or mocking objects * in unit tests. * <ul> * <li>a {@link EventBus} annotated by {@link EventBusC} to receive events from managers and other non ui components</li> * <li>a {@link EventBus} annotated by {@link EventBusV} to send event to Android views</li> * <li>a {@link ExecutorService} that runs {@link Task} via {@link #runTask(Task)} on non-UI thread. * by default, it has a fixed thread tool at size 10. You can inject a mocked {@link ExecutorService} * that runs everything on the main thread in your <b>Unit Tests</b> to avoid multi-thread complexity.</li> * <li>a protected {@link UiThreadRunner} to post a {@link Runnable} onto UI thread. Make sure * use it to {@link UiView#update()} view inside method block {@link Task#execute(Task.Monitor)} * which is run on non-UI thread. It's NOT necessary to use it in callback methods in * {@link Task.Callback} since the framework has already guaranteed it. * In non-Android environment, typically in unit tests, the framework will also provide a default * uiThreadRunner that runs everything on the same thread as the caller's.</li> * </ul> * </p> * @param <MODEL> The view model of the controller. */ public abstract class Controller<MODEL, VIEW extends UiView> extends Bean<MODEL> { protected VIEW view; @Inject @EventBusC private EventBus eventBusC; @Inject @EventBusV private EventBus eventBusV; @Inject private ExecutorService executorService; @Inject protected UiThreadRunner uiThreadRunner; protected Logger logger = LoggerFactory.getLogger(getClass()); /** * Called when the controller is created. Note that it could be called either when the * controller is instantiated for the first time or restored by views. * <p/> * <p>The model of the controller will be instantiated by model's default no-argument * constructor here whe {@link #modelType()} doesn't return null.</p> */ public void onCreated() { super.onCreated(); if (uiThreadRunner == null) { //Use mvc graph's default uiThreadRunner uiThreadRunner = Mvc.graph().uiThreadRunner; } eventBusC.register(this); } /** * Called when the controller is destroyed. This occurs when the controller is de-referenced and * not retained by any objects. */ @Override public void onDestroy() { super.onDestroy(); eventBusC.unregister(this); } /** * Get the view model the controller is holding. Don't write but only read the model from view. * Should only controller write the model. * * @return Null if the controller doesn't need to get its model saved and restored automatically * when {@link #modelType()} returns null. */ @Override public MODEL getModel() { return super.getModel(); } /** * Run a task on threads supplied by injected {@link ExecutorService} without a callback. By * default it runs tasks on separate threads by {@link ExecutorService} injected from AndroidMvc * framework. A simple {@link ExecutorService} that runs tasks on the same thread in test cases * to make the test easier. * * <p><b> * User the protected property {@link UiThreadRunner} to post action back to main UI thread * in the method block of {@link Task#execute(Task.Monitor)}. * </b></p> * <pre> * uiThreadRunner.post(new Runnable() { * @Override * public void run() { * view.update(); * } * }); * </pre> * * @param task The task * @return The monitor to track the state of the execution of the task. It also can cancel the * task. * */ protected <RESULT> Task.Monitor<RESULT> runTask(final Task<RESULT> task) { return runTask(executorService, task, null); } /** * Run a task on threads supplied by injected {@link ExecutorService}. By default it runs tasks * on separate threads by {@link ExecutorService} injected from AndroidMvc framework. A simple * {@link ExecutorService} that runs tasks on the same thread in test cases to make the test * easier. * * <p>The methods of callback will be guaranteed to be run Android's UI thread</p> * * <p><b> * User the protected property {@link UiThreadRunner} to post action back to main UI thread * in the method block of {@link Task#execute(Task.Monitor)}. * </b></p> * <pre> * uiThreadRunner.post(new Runnable() { * @Override * public void run() { * view.update(); * } * }); * </pre> * * @param task The task * @param callback The callback * @return The monitor to track the state of the execution of the task. It also can cancel the * task. */ protected <RESULT> Task.Monitor<RESULT> runTask(final Task<RESULT> task, final Task.Callback<RESULT> callback) { return runTask(executorService, task, callback); } /** * Run a task on the threads supplied by the given {@link ExecutorService}. The task could be * run either asynchronously or synchronously depending on the given executorService. * * <p>The methods of callback will be guaranteed to be run Android's UI thread</p> * * <p><b> * User the protected property {@link UiThreadRunner} to post action back to main UI thread * in the method block of {@link Task#execute(Task.Monitor)}. * </b></p> * <pre> * uiThreadRunner.post(new Runnable() { * @Override * public void run() { * view.update(); * } * }); * </pre> * * @param executorService The executor service managing how the task will be run * @param task The task * @param callback The callback * @return The monitor to track the state of the execution of the task. It also can cancel the * task. * */ protected <RESULT> Task.Monitor<RESULT> runTask(ExecutorService executorService, final Task<RESULT> task, final Task.Callback<RESULT> callback) { final Task.Monitor<RESULT> monitor = new Task.Monitor(task, uiThreadRunner, callback); Future<Void> future = executorService.submit(new Callable<Void>() { @Override public Void call() throws Exception { if (monitor.getState() == Task.Monitor.State.CANCELED) { return null; } monitor.setState(Task.Monitor.State.STARTED); if (callback != null) { uiThreadRunner.post(new Runnable() { @Override public void run() { callback.onStarted(monitor); } }); } try { final RESULT result = task.execute(monitor); if (monitor.getState() != Task.Monitor.State.CANCELED) { monitor.setState(Task.Monitor.State.DONE); if (callback != null) { uiThreadRunner.post(new Runnable() { @Override public void run() { callback.onSuccess(result); callback.onFinally(); } }); } } } catch (final Exception e) { if (e instanceof MvcGraphException) { //Injection exception will always be thrown out since it's a development //time error uiThreadRunner.post(new Runnable() { @Override public void run() { throw new RuntimeException(e); } }); } boolean interruptedByCancel = false; if (e instanceof InterruptedException) { if (monitor.getState() == Task.Monitor.State.INTERRUPTED) { interruptedByCancel = true; } } //If the exception is an interruption caused by cancelling, then ignore it if (!interruptedByCancel) { monitor.setState(Task.Monitor.State.ERRED); if (callback != null) { uiThreadRunner.post(new Runnable() { @Override public void run() { try { callback.onException(e); } catch (Exception e) { throw new RuntimeException(e); } finally { callback.onFinally(); } } }); } else { uiThreadRunner.post(new Runnable() { @Override public void run() { throw new RuntimeException(e); } }); } } } return null; } }); monitor.setFuture(future); return monitor; } /** * <p> * Post the event to views. It automatically guarantees the event will be received * and run on UI thread of Android. * </p> * * <p> * The event will be captured by views or any objects registered to {@link EventBus} annotated * by {@link EventBusV} and has corresponding method named onEvent() with single parameter with * the same type of the event. For example * </p> * <pre> * public class OnTextChangedEvent { * private String text; * * public OnTextChangedEvent(String text) { * this.text = text; * } * * public String getText() { * return text; * } * } * * public class SomeView { * @ Inject * @ EventBusV * private EventBus eventBusV; * * private TextView textView; * * public class SomeView() { * //This is just needed when you have a view not inheriting MvcFragment, MvcService or etc. * //In MvcFragment or MvcService will register to the event bus in onCreate automatically. * eventBusV.register(this); * } * * public void onEvent(OnTextChangedEvent onTextChangedEvent) { * textView.setText(onTextChangedEvent.getText()); * } * } * * public class SomeController{ * private void func() { * postEvent(new OnTextChangedEvent("Controller Wants to change text")); * } * } * </pre> * * @param event2V The event * @deprecated Use {@link #postEvent2V(Object)} instead */ protected void postEvent(final Object event2V) { postEvent2V(event2V); } /** * <p> * Post the event to other core components such as controllers and managers. Event will be * captured on the thread of the invoker. * </p> * * <p> * The event will be captured by {@link Controller}s, {@link Manager}s or others registered to * {@link EventBus} annotated by {@link EventBusC} and has corresponding method named onEvent() * with single parameter with the same type of the event. For example * </p> * * @param event2C The event to other {@link Controller}s, {@link Manager}s */ protected void postEvent2C(final Object event2C) { eventBusC.post(event2C); } /** * <p> * Post the event to views. It automatically guarantees the event will be received * and run on UI thread of Android. * </p> * * <p> * The event will be captured by views or any objects registered to {@link EventBus} annotated * by {@link EventBusV} and has corresponding method named onEvent() with single parameter with * the same type of the event. For example * </p> * <pre> * public class OnTextChangedEvent { * private String text; * * public OnTextChangedEvent(String text) { * this.text = text; * } * * public String getText() { * return text; * } * } * * public class SomeView { * @ Inject * @ EventBusV * private EventBus eventBusV; * * private TextView textView; * * public class SomeView() { * //This is just needed when you have a view not inheriting MvcFragment, MvcService or etc. * //In MvcFragment or MvcService will register to the event bus in onCreate automatically. * eventBusV.register(this); * } * * public void onEvent(OnTextChangedEvent onTextChangedEvent) { * textView.setText(onTextChangedEvent.getText()); * } * } * * public class SomeController{ * private void func() { * postEvent(new OnTextChangedEvent("Controller Wants to change text")); * } * } * </pre> * * @param eventV The event */ protected void postEvent2V(final Object eventV) { eventBusV.post(eventV); } }