diff --git a/app/build.gradle b/app/build.gradle index 3b36c4c749..8d0c42c47e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,9 +141,6 @@ dependencies { compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION" kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION" - // Model View Presenter - compile 'info.android15.nucleus:nucleus:2.0.5' - // Dependency injection compile "com.google.dagger:dagger:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java index ec5a12b5bd..ee30033003 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.java @@ -87,8 +87,9 @@ public abstract class BaseRxActivity

extends BaseActivity i } @Override - protected void onPause() { - super.onPause(); - presenterDelegate.onPause(isFinishing()); + protected void onDestroy() { + super.onDestroy(); + presenterDelegate.onDropView(); + presenterDelegate.onDestroy(!isChangingConfigurations()); } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java index 84044cdc8c..e03e6a1cae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseRxFragment.java @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.base.fragment; import android.os.Bundle; -import android.support.v4.app.Fragment; import eu.kanade.tachiyomi.App; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; @@ -85,13 +84,14 @@ public abstract class BaseRxFragment

extends BaseFragment i } @Override - public void onPause() { - super.onPause(); - presenterDelegate.onPause(getActivity().isFinishing() || isRemoving(this)); + public void onDestroyView() { + super.onDestroyView(); + presenterDelegate.onDropView(); } - private static boolean isRemoving(Fragment fragment) { - Fragment parent = fragment.getParentFragment(); - return fragment.isRemoving() || (parent != null && isRemoving(parent)); + @Override + public void onDestroy() { + super.onDestroy(); + presenterDelegate.onDestroy(!getActivity().isChangingConfigurations()); } } diff --git a/app/src/main/java/nucleus/factory/PresenterFactory.java b/app/src/main/java/nucleus/factory/PresenterFactory.java new file mode 100644 index 0000000000..22c0d1a3da --- /dev/null +++ b/app/src/main/java/nucleus/factory/PresenterFactory.java @@ -0,0 +1,7 @@ +package nucleus.factory; + +import nucleus.presenter.Presenter; + +public interface PresenterFactory

{ + P createPresenter(); +} diff --git a/app/src/main/java/nucleus/factory/PresenterStorage.java b/app/src/main/java/nucleus/factory/PresenterStorage.java new file mode 100644 index 0000000000..8c36926786 --- /dev/null +++ b/app/src/main/java/nucleus/factory/PresenterStorage.java @@ -0,0 +1,64 @@ +package nucleus.factory; + +import java.util.HashMap; + +import nucleus.presenter.Presenter; + +/** + * This is the singleton where all presenters are stored. + */ +public enum PresenterStorage { + + INSTANCE; + + private HashMap idToPresenter = new HashMap<>(); + private HashMap presenterToId = new HashMap<>(); + + /** + * Adds a presenter to the storage + * + * @param presenter a presenter to add + */ + public void add(final Presenter presenter) { + String id = presenter.getClass().getSimpleName() + "/" + System.nanoTime() + "/" + (int)(Math.random() * Integer.MAX_VALUE); + idToPresenter.put(id, presenter); + presenterToId.put(presenter, id); + presenter.addOnDestroyListener(new Presenter.OnDestroyListener() { + @Override + public void onDestroy() { + idToPresenter.remove(presenterToId.remove(presenter)); + } + }); + } + + /** + * Returns a presenter by id or null if such presenter does not exist. + * + * @param id id of a presenter that has been received by calling {@link #getId(Presenter)} + * @param

a type of presenter + * @return a presenter or null + */ + public

P getPresenter(String id) { + //noinspection unchecked + return (P)idToPresenter.get(id); + } + + /** + * Returns id of a given presenter. + * + * @param presenter a presenter to get id for. + * @return if of the presenter. + */ + public String getId(Presenter presenter) { + return presenterToId.get(presenter); + } + + /** + * Removes all presenters from the storage. + * Use this method for testing purposes only. + */ + public void clear() { + idToPresenter.clear(); + presenterToId.clear(); + } +} diff --git a/app/src/main/java/nucleus/factory/ReflectionPresenterFactory.java b/app/src/main/java/nucleus/factory/ReflectionPresenterFactory.java new file mode 100644 index 0000000000..b84d4fd0c2 --- /dev/null +++ b/app/src/main/java/nucleus/factory/ReflectionPresenterFactory.java @@ -0,0 +1,45 @@ +package nucleus.factory; + +import android.support.annotation.Nullable; + +import nucleus.presenter.Presenter; + +/** + * This class represents a {@link PresenterFactory} that creates a presenter using {@link Class#newInstance()} method. + * + * @param

the type of the presenter. + */ +public class ReflectionPresenterFactory

implements PresenterFactory

{ + + private Class

presenterClass; + + /** + * This method returns a {@link ReflectionPresenterFactory} instance if a given view class has + * a {@link RequiresPresenter} annotation, or null otherwise. + * + * @param viewClass a class of the view + * @param

a type of the presenter + * @return a {@link ReflectionPresenterFactory} instance that is supposed to create a presenter from {@link RequiresPresenter} annotation. + */ + @Nullable + public static

ReflectionPresenterFactory

fromViewClass(Class viewClass) { + RequiresPresenter annotation = viewClass.getAnnotation(RequiresPresenter.class); + //noinspection unchecked + Class

presenterClass = annotation == null ? null : (Class

)annotation.value(); + return presenterClass == null ? null : new ReflectionPresenterFactory<>(presenterClass); + } + + public ReflectionPresenterFactory(Class

presenterClass) { + this.presenterClass = presenterClass; + } + + @Override + public P createPresenter() { + try { + return presenterClass.newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/nucleus/factory/RequiresPresenter.java b/app/src/main/java/nucleus/factory/RequiresPresenter.java new file mode 100644 index 0000000000..081b477019 --- /dev/null +++ b/app/src/main/java/nucleus/factory/RequiresPresenter.java @@ -0,0 +1,13 @@ +package nucleus.factory; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import nucleus.presenter.Presenter; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface RequiresPresenter { + Class value(); +} diff --git a/app/src/main/java/nucleus/presenter/Presenter.java b/app/src/main/java/nucleus/presenter/Presenter.java new file mode 100644 index 0000000000..0c986dfb06 --- /dev/null +++ b/app/src/main/java/nucleus/presenter/Presenter.java @@ -0,0 +1,164 @@ +package nucleus.presenter; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This is a base class for all presenters. Subclasses can override + * {@link #onCreate}, {@link #onDestroy}, {@link #onSave}, + * {@link #onTakeView}, {@link #onDropView}. + *

+ * {@link Presenter.OnDestroyListener} can also be used by external classes + * to be notified about the need of freeing resources. + * + * @param a type of view to return with {@link #getView()}. + */ +public class Presenter { + + @Nullable private View view; + private CopyOnWriteArrayList onDestroyListeners = new CopyOnWriteArrayList<>(); + + /** + * This method is called after presenter construction. + * + * This method is intended for overriding. + * + * @param savedState If the presenter is being re-instantiated after a process restart then this Bundle + * contains the data it supplied in {@link #onSave}. + */ + protected void onCreate(@Nullable Bundle savedState) { + } + + /** + * This method is being called when a user leaves view. + * + * This method is intended for overriding. + */ + protected void onDestroy() { + } + + /** + * A returned state is the state that will be passed to {@link #onCreate} for a new presenter instance after a process restart. + * + * This method is intended for overriding. + * + * @param state a non-null bundle which should be used to put presenter's state into. + */ + protected void onSave(Bundle state) { + } + + /** + * This method is being called when a view gets attached to it. + * Normally this happens during {@link Activity#onResume()}, {@link android.app.Fragment#onResume()} + * and {@link android.view.View#onAttachedToWindow()}. + * + * This method is intended for overriding. + * + * @param view a view that should be taken + */ + protected void onTakeView(View view) { + } + + /** + * This method is being called when a view gets detached from the presenter. + * Normally this happens during {@link Activity#onPause()} ()}, {@link Fragment#onPause()} ()} + * and {@link android.view.View#onDetachedFromWindow()}. + * + * This method is intended for overriding. + */ + protected void onDropView() { + } + + /** + * A callback to be invoked when a presenter is about to be destroyed. + */ + public interface OnDestroyListener { + /** + * Called before {@link Presenter#onDestroy()}. + */ + void onDestroy(); + } + + /** + * Adds a listener observing {@link #onDestroy}. + * + * @param listener a listener to add. + */ + public void addOnDestroyListener(OnDestroyListener listener) { + onDestroyListeners.add(listener); + } + + /** + * Removed a listener observing {@link #onDestroy}. + * + * @param listener a listener to remove. + */ + public void removeOnDestroyListener(OnDestroyListener listener) { + onDestroyListeners.remove(listener); + } + + /** + * Returns a current view attached to the presenter or null. + * + * View is normally available between + * {@link Activity#onResume()} and {@link Activity#onPause()}, + * {@link Fragment#onResume()} and {@link Fragment#onPause()}, + * {@link android.view.View#onAttachedToWindow()} and {@link android.view.View#onDetachedFromWindow()}. + * + * Calls outside of these ranges will return null. + * Notice here that {@link Activity#onActivityResult(int, int, Intent)} is called *before* {@link Activity#onResume()} + * so you can't use this method as a callback. + * + * @return a current attached view. + */ + @Nullable + public View getView() { + return view; + } + + /** + * Initializes the presenter. + */ + public void create(Bundle bundle) { + onCreate(bundle); + } + + /** + * Destroys the presenter, calling all {@link Presenter.OnDestroyListener} callbacks. + */ + public void destroy() { + for (OnDestroyListener listener : onDestroyListeners) + listener.onDestroy(); + onDestroy(); + } + + /** + * Saves the presenter. + */ + public void save(Bundle state) { + onSave(state); + } + + /** + * Attaches a view to the presenter. + * + * @param view a view to attach. + */ + public void takeView(View view) { + this.view = view; + onTakeView(view); + } + + /** + * Detaches the presenter from a view. + */ + public void dropView() { + onDropView(); + this.view = null; + } +} diff --git a/app/src/main/java/nucleus/presenter/RxPresenter.java b/app/src/main/java/nucleus/presenter/RxPresenter.java new file mode 100644 index 0000000000..c614dec09b --- /dev/null +++ b/app/src/main/java/nucleus/presenter/RxPresenter.java @@ -0,0 +1,342 @@ +package nucleus.presenter; + +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import nucleus.presenter.delivery.DeliverFirst; +import nucleus.presenter.delivery.DeliverLatestCache; +import nucleus.presenter.delivery.DeliverReplay; +import nucleus.presenter.delivery.Delivery; +import rx.Observable; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func0; +import rx.internal.util.SubscriptionList; +import rx.subjects.BehaviorSubject; + +/** + * This is an extension of {@link Presenter} which provides RxJava functionality. + * + * @param a type of view. + */ +public class RxPresenter extends Presenter { + + private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested"; + + private final BehaviorSubject views = BehaviorSubject.create(); + private final SubscriptionList subscriptions = new SubscriptionList(); + + private final HashMap> restartables = new HashMap<>(); + private final HashMap restartableSubscriptions = new HashMap<>(); + private final ArrayList requested = new ArrayList<>(); + + /** + * Returns an {@link rx.Observable} that emits the current attached view or null. + * See {@link BehaviorSubject} for more information. + * + * @return an observable that emits the current attached view or null. + */ + public Observable view() { + return views; + } + + /** + * Registers a subscription to automatically unsubscribe it during onDestroy. + * See {@link SubscriptionList#add(Subscription) for details.} + * + * @param subscription a subscription to add. + */ + public void add(Subscription subscription) { + subscriptions.add(subscription); + } + + /** + * Removes and unsubscribes a subscription that has been registered with {@link #add} previously. + * See {@link SubscriptionList#remove(Subscription)} for details. + * + * @param subscription a subscription to remove. + */ + public void remove(Subscription subscription) { + subscriptions.remove(subscription); + } + + /** + * A restartable is any RxJava observable that can be started (subscribed) and + * should be automatically restarted (re-subscribed) after a process restart if + * it was still subscribed at the moment of saving presenter's state. + * + * Registers a factory. Re-subscribes the restartable after the process restart. + * + * @param restartableId id of the restartable + * @param factory factory of the restartable + */ + public void restartable(int restartableId, Func0 factory) { + restartables.put(restartableId, factory); + if (requested.contains(restartableId)) + start(restartableId); + } + + /** + * Starts the given restartable. + * + * @param restartableId id of the restartable + */ + public void start(int restartableId) { + stop(restartableId); + requested.add(restartableId); + restartableSubscriptions.put(restartableId, restartables.get(restartableId).call()); + } + + /** + * Unsubscribes a restartable + * + * @param restartableId id of a restartable. + */ + public void stop(int restartableId) { + requested.remove((Integer) restartableId); + Subscription subscription = restartableSubscriptions.get(restartableId); + if (subscription != null) + subscription.unsubscribe(); + } + + /** + * Checks if a restartable is unsubscribed. + * + * @param restartableId id of the restartable. + * @return true if the subscription is null or unsubscribed, false otherwise. + */ + public boolean isUnsubscribed(int restartableId) { + Subscription subscription = restartableSubscriptions.get(restartableId); + return subscription == null || subscription.isUnsubscribed(); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverFirst()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable should run. + * @param onNext a callback that will be called when received data should be delivered to view. + * @param onError a callback that will be called if the source observable emits onError. + * @param the type of the observable. + */ + public void restartableFirst(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverFirst()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableFirst(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableFirst(restartableId, observableFactory, onNext, null); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverLatestCache()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable should run. + * @param onNext a callback that will be called when received data should be delivered to view. + * @param onError a callback that will be called if the source observable emits onError. + * @param the type of the observable. + */ + public void restartableLatestCache(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverLatestCache()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableLatestCache(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableLatestCache(restartableId, observableFactory, onNext, null); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverReplay()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable should run. + * @param onNext a callback that will be called when received data should be delivered to view. + * @param onError a callback that will be called if the source observable emits onError. + * @param the type of the observable. + */ + public void restartableReplay(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverReplay()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableReplay(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableReplay(restartableId, observableFactory, onNext, null); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached. + * If a new onNext value appears while a view is attached, it will be delivered immediately. + * + * @param the type of source observable emissions + */ + public DeliverLatestCache deliverLatestCache() { + return new DeliverLatestCache<>(views); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable. + * + * @param the type of source observable emissions + */ + public DeliverFirst deliverFirst() { + return new DeliverFirst<>(views); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached. + * If a new onNext value appears while a view is attached, it will be delivered immediately. + * + * @param the type of source observable emissions + */ + public DeliverReplay deliverReplay() { + return new DeliverReplay<>(views); + } + + /** + * Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits + * a received {@link Delivery} into two {@link Action2} onNext and onError calls. + * + * @param onNext a method that will be called if the delivery contains an emitted onNext value. + * @param onError a method that will be called if the delivery contains an onError throwable. + * @param a type on onNext value. + * @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls. + */ + public Action1> split(final Action2 onNext, @Nullable final Action2 onError) { + return new Action1>() { + @Override + public void call(Delivery delivery) { + delivery.split(onNext, onError); + } + }; + } + + /** + * This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null. + */ + public Action1> split(Action2 onNext) { + return split(onNext, null); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onCreate(Bundle savedState) { + if (savedState != null) + requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY)); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onDestroy() { + views.onCompleted(); + subscriptions.unsubscribe(); + for (Map.Entry entry : restartableSubscriptions.entrySet()) + entry.getValue().unsubscribe(); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onSave(Bundle state) { + for (int i = requested.size() - 1; i >= 0; i--) { + int restartableId = requested.get(i); + Subscription subscription = restartableSubscriptions.get(restartableId); + if (subscription != null && subscription.isUnsubscribed()) + requested.remove(i); + } + state.putIntegerArrayList(REQUESTED_KEY, requested); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onTakeView(View view) { + views.onNext(view); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onDropView() { + views.onNext(null); + } + + /** + * Please, use restartableXX and deliverXX methods for pushing data from RxPresenter into View. + */ + @Deprecated + @Nullable + @Override + public View getView() { + return super.getView(); + } +} diff --git a/app/src/main/java/nucleus/presenter/delivery/DeliverFirst.java b/app/src/main/java/nucleus/presenter/delivery/DeliverFirst.java new file mode 100644 index 0000000000..bac7fc1dc5 --- /dev/null +++ b/app/src/main/java/nucleus/presenter/delivery/DeliverFirst.java @@ -0,0 +1,38 @@ +package nucleus.presenter.delivery; + +import rx.Notification; +import rx.Observable; +import rx.functions.Func1; + +public class DeliverFirst implements Observable.Transformer> { + + private final Observable view; + + public DeliverFirst(Observable view) { + this.view = view; + } + + @Override + public Observable> call(Observable observable) { + return observable.materialize() + .take(1) + .switchMap(new Func1, Observable>>() { + @Override + public Observable> call(final Notification notification) { + return view.map(new Func1>() { + @Override + public Delivery call(View view) { + return view == null ? null : new Delivery<>(view, notification); + } + }); + } + }) + .filter(new Func1, Boolean>() { + @Override + public Boolean call(Delivery delivery) { + return delivery != null; + } + }) + .take(1); + } +} diff --git a/app/src/main/java/nucleus/presenter/delivery/DeliverLatestCache.java b/app/src/main/java/nucleus/presenter/delivery/DeliverLatestCache.java new file mode 100644 index 0000000000..dfecd8db30 --- /dev/null +++ b/app/src/main/java/nucleus/presenter/delivery/DeliverLatestCache.java @@ -0,0 +1,42 @@ +package nucleus.presenter.delivery; + +import rx.Notification; +import rx.Observable; +import rx.functions.Func1; +import rx.functions.Func2; + +public class DeliverLatestCache implements Observable.Transformer> { + + private final Observable view; + + public DeliverLatestCache(Observable view) { + this.view = view; + } + + @Override + public Observable> call(Observable observable) { + return Observable + .combineLatest( + view, + observable + .materialize() + .filter(new Func1, Boolean>() { + @Override + public Boolean call(Notification notification) { + return !notification.isOnCompleted(); + } + }), + new Func2, Delivery>() { + @Override + public Delivery call(View view, Notification notification) { + return view == null ? null : new Delivery<>(view, notification); + } + }) + .filter(new Func1, Boolean>() { + @Override + public Boolean call(Delivery delivery) { + return delivery != null; + } + }); + } +} diff --git a/app/src/main/java/nucleus/presenter/delivery/DeliverReplay.java b/app/src/main/java/nucleus/presenter/delivery/DeliverReplay.java new file mode 100644 index 0000000000..18d2c0f383 --- /dev/null +++ b/app/src/main/java/nucleus/presenter/delivery/DeliverReplay.java @@ -0,0 +1,50 @@ +package nucleus.presenter.delivery; + +import rx.Notification; +import rx.Observable; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.subjects.ReplaySubject; + +public class DeliverReplay implements Observable.Transformer> { + + private final Observable view; + + public DeliverReplay(Observable view) { + this.view = view; + } + + @Override + public Observable> call(Observable observable) { + final ReplaySubject> subject = ReplaySubject.create(); + final Subscription subscription = observable + .materialize() + .filter(new Func1, Boolean>() { + @Override + public Boolean call(Notification notification) { + return !notification.isOnCompleted(); + } + }) + .subscribe(subject); + return view + .switchMap(new Func1>>() { + @Override + public Observable> call(final View view) { + return view == null ? Observable.>never() : subject + .map(new Func1, Delivery>() { + @Override + public Delivery call(Notification notification) { + return new Delivery<>(view, notification); + } + }); + } + }) + .doOnUnsubscribe(new Action0() { + @Override + public void call() { + subscription.unsubscribe(); + } + }); + } +} diff --git a/app/src/main/java/nucleus/presenter/delivery/Delivery.java b/app/src/main/java/nucleus/presenter/delivery/Delivery.java new file mode 100644 index 0000000000..270e1231c3 --- /dev/null +++ b/app/src/main/java/nucleus/presenter/delivery/Delivery.java @@ -0,0 +1,56 @@ +package nucleus.presenter.delivery; + +import android.support.annotation.Nullable; + +import rx.Notification; +import rx.functions.Action2; + +/** + * A class that represents a couple of View and Data. + * + * @param + * @param + */ +public final class Delivery { + + private final View view; + private final Notification notification; + + public Delivery(View view, Notification notification) { + this.view = view; + this.notification = notification; + } + + public void split(Action2 onNext, @Nullable Action2 onError) { + if (notification.getKind() == Notification.Kind.OnNext) + onNext.call(view, notification.getValue()); + else if (onError != null && notification.getKind() == Notification.Kind.OnError) + onError.call(view, notification.getThrowable()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Delivery delivery = (Delivery)o; + + if (view != null ? !view.equals(delivery.view) : delivery.view != null) return false; + return !(notification != null ? !notification.equals(delivery.notification) : delivery.notification != null); + } + + @Override + public int hashCode() { + int result = view != null ? view.hashCode() : 0; + result = 31 * result + (notification != null ? notification.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Delivery{" + + "view=" + view + + ", notification=" + notification + + '}'; + } +} diff --git a/app/src/main/java/nucleus/view/NucleusActivity.java b/app/src/main/java/nucleus/view/NucleusActivity.java new file mode 100644 index 0000000000..5f7d897427 --- /dev/null +++ b/app/src/main/java/nucleus/view/NucleusActivity.java @@ -0,0 +1,79 @@ +package nucleus.view; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import nucleus.factory.PresenterFactory; +import nucleus.factory.ReflectionPresenterFactory; +import nucleus.presenter.Presenter; + +/** + * This class is an example of how an activity could controls it's presenter. + * You can inherit from this class or copy/paste this class's code to + * create your own view implementation. + * + * @param

a type of presenter to return with {@link #getPresenter}. + */ +public abstract class NucleusActivity

extends Activity implements ViewWithPresenter

{ + + private static final String PRESENTER_STATE_KEY = "presenter_state"; + + private PresenterLifecycleDelegate

presenterDelegate = + new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.

fromViewClass(getClass())); + + /** + * Returns a current presenter factory. + */ + public PresenterFactory

getPresenterFactory() { + return presenterDelegate.getPresenterFactory(); + } + + /** + * Sets a presenter factory. + * Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory. + * Use this method for presenter dependency injection. + */ + @Override + public void setPresenterFactory(PresenterFactory

presenterFactory) { + presenterDelegate.setPresenterFactory(presenterFactory); + } + + /** + * Returns a current attached presenter. + * This method is guaranteed to return a non-null value between + * onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls + * if the presenter factory returns a non-null value. + * + * @return a currently attached presenter or null. + */ + public P getPresenter() { + return presenterDelegate.getPresenter(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) + presenterDelegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState()); + } + + @Override + protected void onResume() { + super.onResume(); + presenterDelegate.onResume(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + presenterDelegate.onDropView(); + presenterDelegate.onDestroy(!isChangingConfigurations()); + } +} diff --git a/app/src/main/java/nucleus/view/NucleusFragment.java b/app/src/main/java/nucleus/view/NucleusFragment.java new file mode 100644 index 0000000000..9979a2a8d1 --- /dev/null +++ b/app/src/main/java/nucleus/view/NucleusFragment.java @@ -0,0 +1,82 @@ +package nucleus.view; + +import android.app.Fragment; +import android.os.Bundle; + +import nucleus.factory.PresenterFactory; +import nucleus.factory.ReflectionPresenterFactory; +import nucleus.presenter.Presenter; + +/** + * This view is an example of how a view should control it's presenter. + * You can inherit from this class or copy/paste this class's code to + * create your own view implementation. + * + * @param

a type of presenter to return with {@link #getPresenter}. + */ +public abstract class NucleusFragment

extends Fragment implements ViewWithPresenter

{ + + private static final String PRESENTER_STATE_KEY = "presenter_state"; + private PresenterLifecycleDelegate

presenterDelegate = + new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.

fromViewClass(getClass())); + + /** + * Returns a current presenter factory. + */ + public PresenterFactory

getPresenterFactory() { + return presenterDelegate.getPresenterFactory(); + } + + /** + * Sets a presenter factory. + * Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory. + * Use this method for presenter dependency injection. + */ + @Override + public void setPresenterFactory(PresenterFactory

presenterFactory) { + presenterDelegate.setPresenterFactory(presenterFactory); + } + + /** + * Returns a current attached presenter. + * This method is guaranteed to return a non-null value between + * onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls + * if the presenter factory returns a non-null value. + * + * @return a currently attached presenter or null. + */ + public P getPresenter() { + return presenterDelegate.getPresenter(); + } + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + if (bundle != null) + presenterDelegate.onRestoreInstanceState(bundle.getBundle(PRESENTER_STATE_KEY)); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + bundle.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState()); + } + + @Override + public void onResume() { + super.onResume(); + presenterDelegate.onResume(this); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + presenterDelegate.onDropView(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + presenterDelegate.onDestroy(!getActivity().isChangingConfigurations()); + } +} diff --git a/app/src/main/java/nucleus/view/NucleusLayout.java b/app/src/main/java/nucleus/view/NucleusLayout.java new file mode 100644 index 0000000000..8e9c57f528 --- /dev/null +++ b/app/src/main/java/nucleus/view/NucleusLayout.java @@ -0,0 +1,113 @@ +package nucleus.view; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import nucleus.factory.PresenterFactory; +import nucleus.factory.ReflectionPresenterFactory; +import nucleus.presenter.Presenter; + +/** + * This view is an example of how a view should control it's presenter. + * You can inherit from this class or copy/paste this class's code to + * create your own view implementation. + * + * @param

a type of presenter to return with {@link #getPresenter}. + */ +public class NucleusLayout

extends FrameLayout implements ViewWithPresenter

{ + + private static final String PARENT_STATE_KEY = "parent_state"; + private static final String PRESENTER_STATE_KEY = "presenter_state"; + + private PresenterLifecycleDelegate

presenterDelegate = + new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.

fromViewClass(getClass())); + + public NucleusLayout(Context context) { + super(context); + } + + public NucleusLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public NucleusLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Returns a current presenter factory. + */ + public PresenterFactory

getPresenterFactory() { + return presenterDelegate.getPresenterFactory(); + } + + /** + * Sets a presenter factory. + * Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory. + * Use this method for presenter dependency injection. + */ + @Override + public void setPresenterFactory(PresenterFactory

presenterFactory) { + presenterDelegate.setPresenterFactory(presenterFactory); + } + + /** + * Returns a current attached presenter. + * This method is guaranteed to return a non-null value between + * onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls + * if the presenter factory returns a non-null value. + * + * @return a currently attached presenter or null. + */ + public P getPresenter() { + return presenterDelegate.getPresenter(); + } + + /** + * Returns the unwrapped activity of the view or throws an exception. + * + * @return an unwrapped activity + */ + public Activity getActivity() { + Context context = getContext(); + while (!(context instanceof Activity) && context instanceof ContextWrapper) + context = ((ContextWrapper) context).getBaseContext(); + if (!(context instanceof Activity)) + throw new IllegalStateException("Expected an activity context, got " + context.getClass().getSimpleName()); + return (Activity) context; + } + + @Override + protected Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + bundle.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState()); + bundle.putParcelable(PARENT_STATE_KEY, super.onSaveInstanceState()); + return bundle; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + Bundle bundle = (Bundle) state; + super.onRestoreInstanceState(bundle.getParcelable(PARENT_STATE_KEY)); + presenterDelegate.onRestoreInstanceState(bundle.getBundle(PRESENTER_STATE_KEY)); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isInEditMode()) + presenterDelegate.onResume(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + presenterDelegate.onDropView(); + presenterDelegate.onDestroy(!getActivity().isChangingConfigurations()); + } +} diff --git a/app/src/main/java/nucleus/view/ParcelFn.java b/app/src/main/java/nucleus/view/ParcelFn.java new file mode 100644 index 0000000000..5b4fedca78 --- /dev/null +++ b/app/src/main/java/nucleus/view/ParcelFn.java @@ -0,0 +1,25 @@ +package nucleus.view; + +import android.os.Parcel; + +class ParcelFn { + + private static final ClassLoader CLASS_LOADER = ParcelFn.class.getClassLoader(); + + static T unmarshall(byte[] array) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(array, 0, array.length); + parcel.setDataPosition(0); + Object value = parcel.readValue(CLASS_LOADER); + parcel.recycle(); + return (T)value; + } + + static byte[] marshall(Object o) { + Parcel parcel = Parcel.obtain(); + parcel.writeValue(o); + byte[] result = parcel.marshall(); + parcel.recycle(); + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/nucleus/view/PresenterLifecycleDelegate.java b/app/src/main/java/nucleus/view/PresenterLifecycleDelegate.java new file mode 100644 index 0000000000..655040a207 --- /dev/null +++ b/app/src/main/java/nucleus/view/PresenterLifecycleDelegate.java @@ -0,0 +1,127 @@ +package nucleus.view; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import nucleus.factory.PresenterFactory; +import nucleus.factory.PresenterStorage; +import nucleus.presenter.Presenter; + +/** + * This class adopts a View lifecycle to the Presenter`s lifecycle. + * + * @param

a type of the presenter. + */ +public final class PresenterLifecycleDelegate

{ + + private static final String PRESENTER_KEY = "presenter"; + private static final String PRESENTER_ID_KEY = "presenter_id"; + + @Nullable private PresenterFactory

presenterFactory; + @Nullable private P presenter; + @Nullable private Bundle bundle; + + private boolean presenterHasView; + + public PresenterLifecycleDelegate(@Nullable PresenterFactory

presenterFactory) { + this.presenterFactory = presenterFactory; + } + + /** + * {@link ViewWithPresenter#getPresenterFactory()} + */ + @Nullable + public PresenterFactory

getPresenterFactory() { + return presenterFactory; + } + + /** + * {@link ViewWithPresenter#setPresenterFactory(PresenterFactory)} + */ + public void setPresenterFactory(@Nullable PresenterFactory

presenterFactory) { + if (presenter != null) + throw new IllegalArgumentException("setPresenterFactory() should be called before onResume()"); + this.presenterFactory = presenterFactory; + } + + /** + * {@link ViewWithPresenter#getPresenter()} + */ + public P getPresenter() { + if (presenterFactory != null) { + if (presenter == null && bundle != null) + presenter = PresenterStorage.INSTANCE.getPresenter(bundle.getString(PRESENTER_ID_KEY)); + + if (presenter == null) { + presenter = presenterFactory.createPresenter(); + PresenterStorage.INSTANCE.add(presenter); + presenter.create(bundle == null ? null : bundle.getBundle(PRESENTER_KEY)); + } + bundle = null; + } + return presenter; + } + + /** + * {@link android.app.Activity#onSaveInstanceState(Bundle)}, {@link android.app.Fragment#onSaveInstanceState(Bundle)}, {@link android.view.View#onSaveInstanceState()}. + */ + public Bundle onSaveInstanceState() { + Bundle bundle = new Bundle(); + getPresenter(); + if (presenter != null) { + Bundle presenterBundle = new Bundle(); + presenter.save(presenterBundle); + bundle.putBundle(PRESENTER_KEY, presenterBundle); + bundle.putString(PRESENTER_ID_KEY, PresenterStorage.INSTANCE.getId(presenter)); + } + return bundle; + } + + /** + * {@link android.app.Activity#onCreate(Bundle)}, {@link android.app.Fragment#onCreate(Bundle)}, {@link android.view.View#onRestoreInstanceState(Parcelable)}. + */ + public void onRestoreInstanceState(Bundle presenterState) { + if (presenter != null) + throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()"); + this.bundle = ParcelFn.unmarshall(ParcelFn.marshall(presenterState)); + } + + /** + * {@link android.app.Activity#onResume()}, + * {@link android.app.Fragment#onResume()}, + * {@link android.view.View#onAttachedToWindow()} + */ + public void onResume(Object view) { + getPresenter(); + if (presenter != null && !presenterHasView) { + //noinspection unchecked + presenter.takeView(view); + presenterHasView = true; + } + } + + /** + * {@link android.app.Activity#onDestroy()}, + * {@link android.app.Fragment#onDestroyView()}, + * {@link android.view.View#onDetachedFromWindow()} + */ + public void onDropView() { + if (presenter != null && presenterHasView) { + presenter.dropView(); + presenterHasView = false; + } + } + + /** + * {@link android.app.Activity#onDestroy()}, + * {@link android.app.Fragment#onDestroy()}, + * {@link android.view.View#onDetachedFromWindow()} + */ + public void onDestroy(boolean isFinal) { + if (presenter != null && isFinal) { + presenter.destroy(); + presenter = null; + } + } +} diff --git a/app/src/main/java/nucleus/view/ViewWithPresenter.java b/app/src/main/java/nucleus/view/ViewWithPresenter.java new file mode 100644 index 0000000000..979065a5a4 --- /dev/null +++ b/app/src/main/java/nucleus/view/ViewWithPresenter.java @@ -0,0 +1,30 @@ +package nucleus.view; + +import nucleus.factory.PresenterFactory; +import nucleus.factory.ReflectionPresenterFactory; +import nucleus.presenter.Presenter; + +public interface ViewWithPresenter

{ + + /** + * Returns a current presenter factory. + */ + PresenterFactory

getPresenterFactory(); + + /** + * Sets a presenter factory. + * Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory. + * Use this method for presenter dependency injection. + */ + void setPresenterFactory(PresenterFactory

presenterFactory); + + /** + * Returns a current attached presenter. + * This method is guaranteed to return a non-null value between + * onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls + * if the presenter factory returns a non-null value. + * + * @return a currently attached presenter or null. + */ + P getPresenter(); +}