Compare commits

..

23 Commits

Author SHA1 Message Date
EricKuck 924e4bebfa Version bump 2022-02-18 14:28:27 -06:00
EricKuck 4ea4aa5c56 Fix issue with detach callbacks happening while not yet attached 2022-02-18 14:15:05 -06:00
Eric Kuck 3b275d31c2 Add PopRootControllerMode to address Android 12 back button behavior (#663) 2022-01-27 16:41:20 -06:00
EricKuck 0e21c8c9c1 Version bump 2021-11-30 11:39:30 -06:00
EricKuck 8297e0273d RouterStateAdapter now properly restores routers after process death 2021-11-28 11:27:47 -06:00
EricKuck 46519c2c2c Ensure preContextUnavailable can't use a null context in the parameter
Fixes #377 and #516
2021-11-23 15:50:52 -06:00
EricKuck 211da8b2ea Version bump for 3.1.1 release 2021-07-19 17:42:43 -05:00
EricKuck 26db962168 Revert maven-publish plugin migration 2021-07-19 17:41:17 -05:00
EricKuck f4c1c6ccf5 Provide a bit more safety around pushing controllers outside of normal lifecycle events 2021-07-19 17:20:32 -05:00
Mario c89caa87e0 Apply the kotlin plugin to lint module (#653)
Fixes missing IssueRegistry from lint.jar
2021-07-19 13:08:34 -05:00
Paul Woitaschek 2748566437 Make the StringSparseArrayParceler internal. 2021-07-03 22:09:39 +02:00
Paul Woitaschek 506c99ed41 Convert the StringSparseArrayParceler to kotlin. 2021-07-03 22:08:18 +02:00
Paul Woitaschek 4fe0ec5f51 Rename .java to .kt 2021-07-03 22:08:18 +02:00
Paul Woitaschek afa93f2cc1 Unit-test the StringSparseArrayParceler. 2021-07-03 22:02:08 +02:00
Paul Woitaschek 836f92b615 Update the publish task name. 2021-07-03 21:41:21 +02:00
Paul Woitaschek 94c817bbd9 Remove support for rxLifecycle2. 2021-07-03 21:40:43 +02:00
Paul Woitaschek fc1fee3e17 Kotlin backstack (#652)
Migrate the Backstack to kotlin
2021-07-03 21:32:11 +02:00
Paul Woitaschek 76b7572a01 Migrate to the maven-publish plugin. (#651) 2021-07-03 21:31:24 +02:00
Paul Woitaschek 3fc63b7f5f Remove the deprecated kotlin-android-extensions plugin and use the kotlin-parcelize plugin replacement instead. 2021-07-03 14:37:40 +02:00
Paul Woitaschek 0ef52211a2 Update LeakCanary to 2.7. (#650)
Also simplify the usage by extracting the leak watching logic into a lifecycle listener.
2021-07-03 14:31:04 +02:00
Paul Woitaschek 7574131940 Remove the unused rxJavaVersion declaration. 2021-07-02 22:40:01 +02:00
Paul Woitaschek 1ab9a4c4f6 Remove the unused rxLifecycleVersion declaration. 2021-07-02 22:35:52 +02:00
Paul Woitaschek 3bc23bd5cd Update to agp 7.0.0-beta05 2021-07-02 22:31:53 +02:00
45 changed files with 491 additions and 812 deletions
+9 -11
View File
@@ -20,30 +20,27 @@ Conductor is architecture-agnostic and does not try to force any design decision
## Installation
```gradle
implementation 'com.bluelinelabs:conductor:3.1.0'
implementation 'com.bluelinelabs:conductor:3.1.4'
// AndroidX Transition change handlers:
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.0'
implementation 'com.bluelinelabs:conductor-androidx-transition:3.1.4'
// ViewPager PagerAdapter:
implementation 'com.bluelinelabs:conductor-viewpager:3.1.0'
implementation 'com.bluelinelabs:conductor-viewpager:3.1.4'
// ViewPager2 Adapter:
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.0'
// RxJava2 lifecycle support:
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.1.0'
implementation 'com.bluelinelabs:conductor-viewpager2:3.1.4'
// RxJava2 Autodispose support:
implementation 'com.bluelinelabs:conductor-autodispose:3.1.0'
implementation 'com.bluelinelabs:conductor-autodispose:3.1.4'
// Lifecycle-aware Controllers (architecture components):
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.0'
implementation 'com.bluelinelabs:conductor-archlifecycle:3.1.4'
```
**SNAPSHOT**
Just use `3.1.1-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
Just use `3.1.5-SNAPSHOT` as your version number in any of the dependencies above and add the url to the snapshot repository:
```gradle
allprojects {
@@ -79,7 +76,8 @@ public class MainActivity extends Activity {
ViewGroup container = (ViewGroup) findViewById(R.id.controller_container);
router = Conductor.attachRouter(this, container, savedInstanceState);
router = Conductor.attachRouter(this, container, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER);
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(new HomeController()));
}
+2 -1
View File
@@ -1,4 +1,4 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
configurations {
lintChecks
@@ -7,6 +7,7 @@ configurations {
dependencies {
compileOnly rootProject.ext.lintapi
compileOnly rootProject.ext.lintchecks
compileOnly rootProject.ext.kotlinStd
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.lint
@@ -1,25 +0,0 @@
apply from: rootProject.file('dependencies.gradle')
apply plugin: 'com.android.library'
apply plugin: "com.vanniktech.maven.publish"
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION_NAME
}
}
dependencies {
api rootProject.ext.rxJava2
api rootProject.ext.rxLifecycle2
api rootProject.ext.rxLifecycleAndroid2
implementation project(':conductor')
}
ext.artifactId = 'conductor-rxlifecycle2'
@@ -1,3 +0,0 @@
POM_NAME=Conductor RxLifecycle2 Extensions
POM_ARTIFACT_ID=conductor-rxlifecycle2
POM_PACKAGING=aar
@@ -1,3 +0,0 @@
<manifest package="com.bluelinelabs.conductor.rxlifecycle2">
<application />
</manifest>
@@ -1,14 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
public enum ControllerEvent {
CREATE,
CONTEXT_AVAILABLE,
CREATE_VIEW,
ATTACH,
DETACH,
DESTROY_VIEW,
CONTEXT_UNAVAILABLE,
DESTROY
}
@@ -1,71 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.View;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import io.reactivex.subjects.BehaviorSubject;
public class ControllerLifecycleSubjectHelper {
private ControllerLifecycleSubjectHelper() {
}
public static BehaviorSubject<ControllerEvent> create(Controller controller) {
ControllerEvent initialState;
if (controller.isBeingDestroyed() || controller.isDestroyed()) {
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
} else if (controller.isAttached()) {
initialState = ControllerEvent.ATTACH;
} else if (controller.getView() != null) {
initialState = ControllerEvent.CREATE_VIEW;
} else if (controller.getActivity() != null) {
initialState = ControllerEvent.CONTEXT_AVAILABLE;
} else {
initialState = ControllerEvent.CREATE;
}
final BehaviorSubject<ControllerEvent> subject = BehaviorSubject.createDefault(initialState);
controller.addLifecycleListener(new Controller.LifecycleListener() {
@Override
public void preContextAvailable(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CONTEXT_AVAILABLE);
}
@Override
public void preCreateView(@NonNull Controller controller) {
subject.onNext(ControllerEvent.CREATE_VIEW);
}
@Override
public void preAttach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.ATTACH);
}
@Override
public void preDetach(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DETACH);
}
@Override
public void preDestroyView(@NonNull Controller controller, @NonNull View view) {
subject.onNext(ControllerEvent.DESTROY_VIEW);
}
@Override
public void preContextUnavailable(@NonNull Controller controller, @NonNull Context context) {
subject.onNext(ControllerEvent.CONTEXT_UNAVAILABLE);
}
@Override
public void preDestroy(@NonNull Controller controller) {
subject.onNext(ControllerEvent.DESTROY);
}
});
return subject;
}
}
@@ -1,49 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bluelinelabs.conductor.Controller;
import com.trello.rxlifecycle2.LifecycleProvider;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* A base {@link Controller} that can be used to expose lifecycle events using RxJava
*/
public abstract class RxController extends Controller implements LifecycleProvider<ControllerEvent> {
private final BehaviorSubject<ControllerEvent> lifecycleSubject;
public RxController(){
this(null);
}
public RxController(@Nullable Bundle args) {
super(args);
lifecycleSubject = ControllerLifecycleSubjectHelper.create(this);
}
@Override
@NonNull
@CheckResult
public final Observable<ControllerEvent> lifecycle() {
return lifecycleSubject.hide();
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindUntilEvent(@NonNull ControllerEvent event) {
return RxLifecycle.bindUntilEvent(lifecycleSubject, event);
}
@Override
@NonNull
@CheckResult
public final <T> LifecycleTransformer<T> bindToLifecycle() {
return RxControllerLifecycle.bindController(lifecycleSubject);
}
}
@@ -1,43 +0,0 @@
package com.bluelinelabs.conductor.rxlifecycle2;
import androidx.annotation.NonNull;
import com.trello.rxlifecycle2.LifecycleTransformer;
import com.trello.rxlifecycle2.OutsideLifecycleException;
import com.trello.rxlifecycle2.RxLifecycle;
import io.reactivex.Observable;
import io.reactivex.functions.Function;
public class RxControllerLifecycle {
/**
* Binds the given source to a Controller lifecycle. This is the Controller version of
* {@link com.trello.rxlifecycle2.android.RxLifecycleAndroid#bindFragment(Observable)}.
*
* @param lifecycle the lifecycle sequence of a Controller
* @return a reusable {@link io.reactivex.ObservableTransformer} that unsubscribes the source during the Controller lifecycle
*/
public static <T> LifecycleTransformer<T> bindController(@NonNull final Observable<ControllerEvent> lifecycle) {
return RxLifecycle.bind(lifecycle, CONTROLLER_LIFECYCLE);
}
private static final Function<ControllerEvent, ControllerEvent> CONTROLLER_LIFECYCLE =
new Function<ControllerEvent, ControllerEvent>() {
@Override
public ControllerEvent apply(ControllerEvent lastEvent) {
switch (lastEvent) {
case CREATE:
return ControllerEvent.DESTROY;
case CONTEXT_AVAILABLE:
return ControllerEvent.CONTEXT_UNAVAILABLE;
case ATTACH:
return ControllerEvent.DETACH;
case CREATE_VIEW:
return ControllerEvent.DESTROY_VIEW;
case DETACH:
return ControllerEvent.DESTROY;
default:
throw new OutsideLifecycleException("Cannot bind to Controller lifecycle when outside of it.");
}
}
};
}
+2 -2
View File
@@ -20,7 +20,7 @@ dependencies {
implementation project(':conductor')
}
ext.artifactId = 'conductor-viewpager'
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager'
apply plugin: "com.vanniktech.maven.publish"
+3 -3
View File
@@ -1,6 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -28,7 +28,7 @@ dependencies {
implementation project(':conductor')
}
ext.artifactId = 'conductor-viewpager2'
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor-viewpager2'
apply plugin: "com.vanniktech.maven.publish"
@@ -10,7 +10,7 @@ import androidx.viewpager2.adapter.StatefulAdapter
import androidx.viewpager2.widget.ViewPager2
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
/**
* An ViewPager2 adapter that uses Routers as pages
@@ -48,7 +48,8 @@ abstract class RouterStateAdapter(private val host: Controller) :
}
private fun inferViewPager(recyclerView: RecyclerView): ViewPager2 {
return recyclerView.parent as? ViewPager2 ?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
return recyclerView.parent as? ViewPager2
?: error("Expected ViewPager2 instance. Got: ${recyclerView.parent}")
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
@@ -119,7 +120,8 @@ abstract class RouterStateAdapter(private val host: Controller) :
override fun saveState(): Parcelable {
// Ensure all visible pages are saved, starting at the outermost pages and working our way in
val visiblePositions = (0 until visibleRouters.size()).map { visibleRouters.keyAt(it) }.toMutableList()
val visiblePositions = (0 until visibleRouters.size())
.map { visibleRouters.keyAt(it) }.toMutableList()
while (visiblePositions.isNotEmpty()) {
val lastPosition = visiblePositions.removeAt(visiblePositions.lastIndex)
savePage(getItemId(lastPosition), visibleRouters[lastPosition])
@@ -152,7 +154,7 @@ abstract class RouterStateAdapter(private val host: Controller) :
private fun attachRouter(holder: RouterViewHolder, position: Int) {
val itemId = getItemId(position)
val router = host.getChildRouter(holder.container, "$itemId")
val router = host.getChildRouter(holder.container, "$itemId", true, false)!!
// This should have already been handled by onViewRecycled, but it seems like this wasn't
// always reliably called
+3 -2
View File
@@ -17,6 +17,7 @@ dependencies {
implementation savedState
testImplementation rootProject.ext.junit
testImplementation rootProject.ext.robolectric
testImplementation kotestAssertions
implementation archComponentsLifecycle
@@ -26,7 +27,7 @@ dependencies {
lintPublish project(':conductor-lint')
}
ext.artifactId = 'conductor'
apply from: rootProject.file('dependencies.gradle')
ext.artifactId = 'conductor'
apply plugin: "com.vanniktech.maven.publish"
@@ -1,118 +0,0 @@
package com.bluelinelabs.conductor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
class Backstack implements Iterable<RouterTransaction> {
private static final String KEY_ENTRIES = "Backstack.entries";
private final Deque<RouterTransaction> backstack = new ArrayDeque<>();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isEmpty() {
return backstack.isEmpty();
}
int size() {
return backstack.size();
}
@Nullable
RouterTransaction root() {
return backstack.size() > 0 ? backstack.getLast() : null;
}
@Override @NonNull
public Iterator<RouterTransaction> iterator() {
return backstack.iterator();
}
@NonNull
Iterator<RouterTransaction> reverseIterator() {
return backstack.descendingIterator();
}
@NonNull
List<RouterTransaction> popTo(@NonNull RouterTransaction transaction) {
List<RouterTransaction> popped = new ArrayList<>();
if (backstack.contains(transaction)) {
while (backstack.peek() != transaction) {
RouterTransaction poppedTransaction = pop();
popped.add(poppedTransaction);
}
} else {
throw new RuntimeException("Tried to pop to a transaction that was not on the back stack");
}
return popped;
}
@NonNull
RouterTransaction pop() {
RouterTransaction popped = backstack.pop();
popped.controller().destroy();
return popped;
}
@Nullable
RouterTransaction peek() {
return backstack.peek();
}
void push(@NonNull RouterTransaction transaction) {
backstack.push(transaction);
}
@NonNull
List<RouterTransaction> popAll() {
List<RouterTransaction> list = new ArrayList<>();
while (!isEmpty()) {
list.add(pop());
}
return list;
}
void setBackstack(@NonNull List<RouterTransaction> backstack) {
this.backstack.clear();
for (RouterTransaction transaction : backstack) {
this.backstack.push(transaction);
}
}
boolean contains(@NonNull Controller controller) {
for (RouterTransaction transaction : backstack) {
if (controller == transaction.controller()) {
return true;
}
}
return false;
}
void saveInstanceState(@NonNull Bundle outState) {
ArrayList<Bundle> entryBundles = new ArrayList<>(backstack.size());
for (RouterTransaction entry : backstack) {
entryBundles.add(entry.saveInstanceState());
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles);
}
void restoreInstanceState(@NonNull Bundle savedInstanceState) {
ArrayList<Bundle> entryBundles = savedInstanceState.getParcelableArrayList(KEY_ENTRIES);
if (entryBundles != null) {
Collections.reverse(entryBundles);
for (Bundle transactionBundle : entryBundles) {
backstack.push(new RouterTransaction(transactionBundle));
}
}
}
}
@@ -0,0 +1,89 @@
package com.bluelinelabs.conductor
import android.os.Bundle
import java.util.*
internal class Backstack : Iterable<RouterTransaction> {
private val backstack: Deque<RouterTransaction> = ArrayDeque()
val isEmpty: Boolean get() = backstack.isEmpty()
val size: Int get() = backstack.size
fun root(): RouterTransaction? = backstack.lastOrNull()
override fun iterator(): MutableIterator<RouterTransaction> {
return backstack.iterator()
}
fun reverseIterator(): Iterator<RouterTransaction> = backstack.descendingIterator()
fun popTo(transaction: RouterTransaction): List<RouterTransaction> {
if (transaction in backstack) {
val popped: MutableList<RouterTransaction> = ArrayList()
while (backstack.peek() != transaction) {
val poppedTransaction = pop()
popped.add(poppedTransaction)
}
return popped
} else {
throw RuntimeException("Tried to pop to a transaction that was not on the back stack")
}
}
fun pop(): RouterTransaction {
return backstack.pop().also {
it.controller.destroy()
}
}
fun peek(): RouterTransaction? = backstack.peek()
fun push(transaction: RouterTransaction) {
backstack.push(transaction)
}
fun popAll(): List<RouterTransaction> {
val list: MutableList<RouterTransaction> = ArrayList()
while (!isEmpty) {
list.add(pop())
}
return list
}
fun setBackstack(backstack: List<RouterTransaction>) {
this.backstack.clear()
backstack.forEach { transaction ->
this.backstack.push(transaction)
}
}
operator fun contains(controller: Controller): Boolean {
return backstack.any {
it.controller == controller
}
}
fun saveInstanceState(outState: Bundle) {
val entryBundles = ArrayList<Bundle>(backstack.size)
backstack.mapTo(entryBundles) {
it.saveInstanceState()
}
outState.putParcelableArrayList(KEY_ENTRIES, entryBundles)
}
fun restoreInstanceState(savedInstanceState: Bundle) {
val entryBundles = savedInstanceState.getParcelableArrayList<Bundle?>(KEY_ENTRIES)
if (entryBundles != null) {
entryBundles.reverse()
for (transactionBundle in entryBundles) {
backstack.push(RouterTransaction(transactionBundle!!))
}
}
}
companion object {
private const val KEY_ENTRIES = "Backstack.entries"
}
}
@@ -9,7 +9,6 @@ import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -215,6 +214,23 @@ public abstract class Controller {
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded) {
return getChildRouter(container, tag, createIfNeeded, true);
}
/**
* Retrieves the child {@link Router} for the given container/tag combination. Note that multiple
* routers should not exist in the same container unless a lot of care is taken to maintain order
* between them. Avoid using the same container unless you have a great reason to do so (ex: ViewPagers).
* The only time this method will return {@code null} is when the child router does not exist prior
* to calling this method and the createIfNeeded parameter is set to false.
*
* @param container The ViewGroup that hosts the child Router
* @param tag The router's tag or {@code null} if none is needed
* @param createIfNeeded If true, a router will be created if one does not yet exist. Else {@code null} will be returned in this case.
* @param boundToHostContainerId If true, a router will only ever rebind with a container with the same view id on state restoration. Note that this must be set to true if the tag is null.
*/
@Nullable
public final Router getChildRouter(@NonNull ViewGroup container, @Nullable String tag, boolean createIfNeeded, boolean boundToHostContainerId) {
@IdRes final int containerId = container.getId();
if (containerId == View.NO_ID) {
throw new IllegalStateException("You must set an id on your container.");
@@ -222,7 +238,7 @@ public abstract class Controller {
ControllerHostedRouter childRouter = null;
for (ControllerHostedRouter router : childRouters) {
if (router.getHostId() == containerId && TextUtils.equals(tag, router.getTag())) {
if (router.matches(containerId, tag)) {
childRouter = router;
break;
}
@@ -230,7 +246,7 @@ public abstract class Controller {
if (childRouter == null) {
if (createIfNeeded) {
childRouter = new ControllerHostedRouter(container.getId(), tag);
childRouter = new ControllerHostedRouter(container.getId(), tag, boundToHostContainerId);
childRouter.setHostContainer(this, container);
childRouters.add(childRouter);
@@ -856,6 +872,27 @@ public abstract class Controller {
}
}
final void onContextUnavailable(@NonNull Context context) {
if (isContextAvailable) {
for (Router childRouter : childRouters) {
childRouter.onContextUnavailable(context);
}
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, context);
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
}
final void executeWithRouter(@NonNull RouterRequiringFunc listener) {
if (router != null) {
listener.execute();
@@ -908,20 +945,7 @@ public abstract class Controller {
destroy(true);
}
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, activity);
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
}
onContextUnavailable(activity);
}
void attach(@NonNull View view) {
@@ -981,24 +1005,26 @@ public abstract class Controller {
final boolean removeViewRef = !blockViewRefRemoval && (forceViewRefRemoval || retainViewMode == RetainViewMode.RELEASE_DETACH || isBeingDestroyed);
if (attached) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDetach(this, view);
}
attached = false;
if (!awaitingParentAttach) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preDetach(this, view);
}
attached = false;
onDetach(view);
}
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
if (hasOptionsMenu && !optionsMenuHidden) {
router.invalidateOptionsMenu();
}
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDetach(this, view);
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postDetach(this, view);
}
} else {
attached = false;
awaitingParentAttach = false;
}
}
@@ -1123,18 +1149,7 @@ public abstract class Controller {
private void performDestroy() {
if (isContextAvailable) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.preContextUnavailable(this, getActivity());
}
isContextAvailable = false;
onContextUnavailable();
listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
lifecycleListener.postContextUnavailable(this);
}
onContextUnavailable(getActivity());
}
if (!destroyed) {
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
@@ -22,18 +23,24 @@ class ControllerHostedRouter extends Router {
private final String KEY_HOST_ID = "ControllerHostedRouter.hostId";
private final String KEY_TAG = "ControllerHostedRouter.tag";
private final String KEY_BOUND_TO_CONTAINER = "ControllerHostedRouter.boundToContainer";
private Controller hostController;
@IdRes private int hostId;
private String tag;
private boolean isDetachFrozen;
private boolean boundToContainer;
ControllerHostedRouter() { }
ControllerHostedRouter(int hostId, @Nullable String tag) {
ControllerHostedRouter(int hostId, @Nullable String tag, boolean boundToContainer) {
if (!boundToContainer && tag == null) {
throw new IllegalStateException("ControllerHostedRouter can't be created without a tag if not bounded to its container");
}
this.hostId = hostId;
this.tag = tag;
this.boundToContainer = boundToContainer;
}
final void setHostController(@NonNull Controller controller) {
@@ -213,6 +220,7 @@ class ControllerHostedRouter extends Router {
super.saveInstanceState(outState);
outState.putInt(KEY_HOST_ID, hostId);
outState.putBoolean(KEY_BOUND_TO_CONTAINER, boundToContainer);
outState.putString(KEY_TAG, tag);
}
@@ -221,6 +229,7 @@ class ControllerHostedRouter extends Router {
super.restoreInstanceState(savedInstanceState);
hostId = savedInstanceState.getInt(KEY_HOST_ID);
boundToContainer = savedInstanceState.getBoolean(KEY_BOUND_TO_CONTAINER);
tag = savedInstanceState.getString(KEY_TAG);
}
@@ -234,9 +243,18 @@ class ControllerHostedRouter extends Router {
return hostId;
}
@Nullable
String getTag() {
return tag;
boolean matches(int hostId, @Nullable String tag) {
if (!boundToContainer && container == null) {
if (this.tag == null) {
throw new IllegalStateException("Host ID can't be variable with a null tag");
}
if (this.tag.equals(tag)) {
this.hostId = hostId;
return true;
}
}
return this.hostId == hostId && TextUtils.equals(tag, this.tag);
}
@Override @NonNull
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
@@ -34,14 +35,14 @@ import java.util.List;
public abstract class Router {
private static final String KEY_BACKSTACK = "Router.backstack";
private static final String KEY_POPS_LAST_VIEW = "Router.popsLastView";
private static final String KEY_POP_ROOT_CONTROLLER_MODE = "Router.popRootControllerMode";
final Backstack backstack = new Backstack();
private final List<ControllerChangeListener> changeListeners = new ArrayList<>();
private final List<ChangeTransaction> pendingControllerChanges = new ArrayList<>();
final List<Controller> destroyingControllers = new ArrayList<>();
private boolean popsLastView = false;
private PopRootControllerMode popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
boolean containerFullyAttached = false;
boolean isActivityStopped = false;
@@ -94,7 +95,7 @@ public abstract class Router {
//noinspection ConstantConditions
if (backstack.peek().controller().handleBack()) {
return true;
} else if (popCurrentController()) {
} else if ((backstack.getSize() > 1 || popRootControllerMode != PopRootControllerMode.NEVER) && popCurrentController()) {
return true;
}
}
@@ -161,7 +162,7 @@ public abstract class Router {
}
}
if (popsLastView) {
if (popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW) {
return topTransaction != null;
} else {
return !backstack.isEmpty();
@@ -220,7 +221,7 @@ public abstract class Router {
}
void destroy(boolean popViews) {
popsLastView = true;
popRootControllerMode = PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW;
final List<RouterTransaction> poppedControllers = backstack.popAll();
trackDestroyingControllers(poppedControllers);
@@ -250,10 +251,24 @@ public abstract class Router {
* If set to true, this router will handle back presses by performing a change handler on the last controller and view
* in the stack. This defaults to false so that the developer can either finish its containing Activity or otherwise
* hide its parent view without any strange artifacting.
*
* Note: This method has been deprecated and should be replaced with setPopRootControllerMode.
*/
@NonNull
@Deprecated
public Router setPopsLastView(boolean popsLastView) {
this.popsLastView = popsLastView;
this.popRootControllerMode = popsLastView ? PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW : PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW;
return this;
}
/**
* Sets the method this router will use to handle back presses when there is only one controller left in the backstack.
* Defaults to POP_ROOT_CONTROLLER_LEAVING_VIEW so that the developer can either finish its containing Activity or
* otherwise hide its parent view without any strange artifacting.
*/
@NonNull
public Router setPopRootControllerMode(@NonNull PopRootControllerMode popRootControllerMode) {
this.popRootControllerMode = popRootControllerMode;
return this;
}
@@ -280,7 +295,7 @@ public abstract class Router {
public boolean popToRoot(@Nullable ControllerChangeHandler changeHandler) {
ThreadUtils.ensureMainThread();
if (backstack.size() > 1) {
if (backstack.getSize() > 1) {
//noinspection ConstantConditions
popToTransaction(backstack.root(), changeHandler);
return true;
@@ -375,7 +390,7 @@ public abstract class Router {
*/
@SuppressWarnings("WeakerAccess")
public int getBackstackSize() {
return backstack.size();
return backstack.getSize();
}
/**
@@ -383,7 +398,7 @@ public abstract class Router {
*/
@NonNull
public List<RouterTransaction> getBackstack() {
List<RouterTransaction> list = new ArrayList<>(backstack.size());
List<RouterTransaction> list = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
list.add(backstackIterator.next());
@@ -649,14 +664,14 @@ public abstract class Router {
backstack.saveInstanceState(backstackState);
outState.putParcelable(KEY_BACKSTACK, backstackState);
outState.putBoolean(KEY_POPS_LAST_VIEW, popsLastView);
outState.putInt(KEY_POP_ROOT_CONTROLLER_MODE, popRootControllerMode.ordinal());
}
public void restoreInstanceState(@NonNull Bundle savedInstanceState) {
Bundle backstackBundle = savedInstanceState.getParcelable(KEY_BACKSTACK);
//noinspection ConstantConditions
backstack.restoreInstanceState(backstackBundle);
popsLastView = savedInstanceState.getBoolean(KEY_POPS_LAST_VIEW);
popRootControllerMode = PopRootControllerMode.values()[savedInstanceState.getInt(KEY_POP_ROOT_CONTROLLER_MODE)];
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
@@ -700,7 +715,7 @@ public abstract class Router {
}
private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
if (backstack.size() > 0) {
if (backstack.getSize() > 0) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> updatedBackstack = new ArrayList<>();
@@ -745,9 +760,18 @@ public abstract class Router {
}
}
void onContextUnavailable(@NonNull Context context) {
for (RouterTransaction transaction : backstack) {
transaction.controller().onContextUnavailable(context);
}
for (Controller controller : destroyingControllers) {
controller.onContextUnavailable(context);
}
}
@NonNull
final List<Controller> getControllers() {
List<Controller> controllers = new ArrayList<>(backstack.size());
List<Controller> controllers = new ArrayList<>(backstack.getSize());
Iterator<RouterTransaction> backstackIterator = backstack.reverseIterator();
while (backstackIterator.hasNext()) {
@@ -793,7 +817,7 @@ public abstract class Router {
if (to != null) {
to.ensureValidIndex(getTransactionIndexer());
setRouterOnController(toController);
} else if (backstack.size() == 0 && !popsLastView) {
} else if (backstack.getSize() == 0 && popRootControllerMode == PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW) {
// We're emptying out the backstack. Views get weird if you transition them out, so just no-op it. The host
// Activity or controller should be handling this by finishing or at least hiding this view.
changeHandler = new NoOpControllerChangeHandler();
@@ -1001,4 +1025,25 @@ public abstract class Router {
@NonNull abstract Router getRootRouter();
@NonNull abstract TransactionIndexer getTransactionIndexer();
/**
* Defines the way a Router will handle back button or pop events when there is only one controller
* left in the backstack.
*/
public enum PopRootControllerMode {
/**
* The Router will not pop the final controller left on the backstack when the back button is pressed
* or when pop events are called. This mode should generally be used for Activity-hosted routers.
*/
NEVER,
/**
* The Router will pop the final controller, but will leave its view in the hierarchy. This is useful
* when the developer wishes to allow its containing Activity to finish or otherwise hide its parent
* view without any strange artifacting.
*/
POP_ROOT_CONTROLLER_BUT_NOT_VIEW,
/**
* The Router will pop both the final controller as well as its view.
*/
POP_ROOT_CONTROLLER_AND_VIEW
}
}
@@ -74,7 +74,7 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
) {
// Should only happen if pushing another controller over this one was aborted
if (
controller == changeController &&
controller === changeController &&
changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeController.view?.windowToken != null &&
@@ -94,9 +94,10 @@ internal class OwnViewTreeLifecycleAndRegistry private constructor(
changeType: ControllerChangeType
) {
if (
controller == changeController &&
controller === changeController &&
!changeType.isEnter &&
changeHandler.removesFromViewOnPush() &&
changeController.view != null &&
lifecycleRegistry.currentState == Lifecycle.State.RESUMED
) {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@@ -1,62 +0,0 @@
package com.bluelinelabs.conductor.internal;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import android.util.SparseArray;
public class StringSparseArrayParceler implements Parcelable {
private final SparseArray<String> stringSparseArray;
public StringSparseArrayParceler(@NonNull SparseArray<String> stringSparseArray) {
this.stringSparseArray = stringSparseArray;
}
StringSparseArrayParceler(@NonNull Parcel in) {
stringSparseArray = new SparseArray<>();
final int size = in.readInt();
for (int i = 0; i < size; i++) {
stringSparseArray.put(in.readInt(), in.readString());
}
}
@NonNull
public SparseArray<String> getStringSparseArray() {
return stringSparseArray;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
final int size = stringSparseArray.size();
out.writeInt(size);
for (int i = 0; i < size; i++) {
int key = stringSparseArray.keyAt(i);
out.writeInt(key);
out.writeString(stringSparseArray.get(key));
}
}
public static final Parcelable.Creator<StringSparseArrayParceler> CREATOR = new Parcelable.Creator<StringSparseArrayParceler>() {
@Override
public StringSparseArrayParceler createFromParcel(Parcel in) {
return new StringSparseArrayParceler(in);
}
@Override
public StringSparseArrayParceler[] newArray(int size) {
return new StringSparseArrayParceler[size];
}
};
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.internal
import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
internal class StringSparseArrayParceler(val stringSparseArray: SparseArray<String>) : Parcelable {
override fun describeContents(): Int = 0
override fun writeToParcel(out: Parcel, flags: Int) {
val size = stringSparseArray.size()
out.writeInt(size)
for (i in 0 until size) {
val key = stringSparseArray.keyAt(i)
out.writeInt(key)
out.writeString(stringSparseArray[key])
}
}
companion object {
@Suppress("unused")
@JvmField
val CREATOR: Parcelable.Creator<StringSparseArrayParceler> =
object : Parcelable.Creator<StringSparseArrayParceler> {
override fun createFromParcel(parcel: Parcel): StringSparseArrayParceler {
val stringSparseArray = SparseArray<String>()
val size = parcel.readInt()
for (i in 0 until size) {
stringSparseArray.put(parcel.readInt(), parcel.readString())
}
return StringSparseArrayParceler(stringSparseArray)
}
override fun newArray(size: Int): Array<StringSparseArrayParceler?> = arrayOfNulls(size)
}
}
}
@@ -1,67 +0,0 @@
package com.bluelinelabs.conductor;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class BackstackTests {
private Backstack backstack;
@Before
public void setup() {
backstack = new Backstack();
}
@Test
public void testPush() {
assertEquals(0, backstack.size());
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(1, backstack.size());
}
@Test
public void testPop() {
backstack.push(RouterTransaction.with(new TestController()));
backstack.push(RouterTransaction.with(new TestController()));
assertEquals(2, backstack.size());
backstack.pop();
assertEquals(1, backstack.size());
backstack.pop();
assertEquals(0, backstack.size());
}
@Test
public void testPeek() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
backstack.push(transaction1);
assertEquals(transaction1, backstack.peek());
backstack.push(transaction2);
assertEquals(transaction2, backstack.peek());
backstack.pop();
assertEquals(transaction1, backstack.peek());
}
@Test
public void testPopTo() {
RouterTransaction transaction1 = RouterTransaction.with(new TestController());
RouterTransaction transaction2 = RouterTransaction.with(new TestController());
RouterTransaction transaction3 = RouterTransaction.with(new TestController());
backstack.push(transaction1);
backstack.push(transaction2);
backstack.push(transaction3);
assertEquals(3, backstack.size());
backstack.popTo(transaction1);
assertEquals(1, backstack.size());
assertEquals(transaction1, backstack.peek());
}
}
@@ -0,0 +1,60 @@
package com.bluelinelabs.conductor
import org.junit.Assert.assertEquals
import org.junit.Test
class BackstackTests {
private val backstack = Backstack()
@Test
fun testPush() {
assertEquals(0, backstack.size.toLong())
backstack.push(TestController().asTransaction())
assertEquals(1, backstack.size.toLong())
}
@Test
fun testPop() {
backstack.push(TestController().asTransaction())
backstack.push(TestController().asTransaction())
assertEquals(2, backstack.size.toLong())
backstack.pop()
assertEquals(1, backstack.size.toLong())
backstack.pop()
assertEquals(0, backstack.size.toLong())
}
@Test
fun testPeek() {
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
backstack.push(transaction1)
assertEquals(transaction1, backstack.peek())
backstack.push(transaction2)
assertEquals(transaction2, backstack.peek())
backstack.pop()
assertEquals(transaction1, backstack.peek())
}
@Test
fun testPopTo() {
val transaction1 = TestController().asTransaction()
val transaction2 = TestController().asTransaction()
val transaction3 = TestController().asTransaction()
backstack.push(transaction1)
backstack.push(transaction2)
backstack.push(transaction3)
assertEquals(3, backstack.size.toLong())
backstack.popTo(transaction1)
assertEquals(1, backstack.size.toLong())
assertEquals(transaction1, backstack.peek())
}
}
@@ -112,7 +112,7 @@ class ControllerLifecycleActivityReferenceTests {
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -146,7 +146,7 @@ class ControllerLifecycleActivityReferenceTests {
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -199,7 +199,7 @@ class ControllerLifecycleActivityReferenceTests {
val listener = ActivityReferencingLifecycleListener()
child.addLifecycleListener(listener)
val childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
child.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -276,7 +276,7 @@ class ControllerTests {
Assert.assertNull(child2.parentController)
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.VIEW_ID))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.setRoot(child1.asTransaction())
Assert.assertEquals(1, parent.childRouters.size)
Assert.assertEquals(childRouter, parent.childRouters[0])
@@ -362,7 +362,7 @@ class ControllerTests {
val childTransaction1 = TestController().asTransaction()
val childTransaction2 = TestController().asTransaction()
var childRouter = parent.getChildRouter(parent.view!!.findViewById(TestController.CHILD_VIEW_ID_1))
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.setRoot(childTransaction1)
childRouter.pushController(childTransaction2)
val savedState = Bundle()
@@ -124,7 +124,7 @@ class ReattachCaseTests {
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -179,7 +179,7 @@ class ReattachCaseTests {
val childRouter = controllerB.getChildRouter(
controllerB.view!!.findViewById(TestController.VIEW_ID)
)
childRouter.setPopsLastView(true)
childRouter.setPopRootControllerMode(Router.PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
childRouter.pushController(
childController.asTransaction(
pushChangeHandler = MockChangeHandler.defaultHandler(),
@@ -0,0 +1,50 @@
package com.bluelinelabs.conductor.internal
import android.os.Parcel
import android.os.Parcelable
import android.util.SparseArray
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.shouldBe
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class StringSparseArrayParcelerTest {
@Test
fun emptyArray() {
SparseArray<String>().parcelAndUnParcel().size() shouldBeExactly 0
}
@Test
fun arrayWithContents() {
val array = SparseArray<String>()
array.put(1, "one")
array.put(7, "seven")
val unParceled = array.parcelAndUnParcel()
unParceled.size() shouldBeExactly 2
unParceled[1] shouldBe "one"
unParceled[7] shouldBe "seven"
}
private fun SparseArray<String>.parcelAndUnParcel(): SparseArray<String> {
val parceler = StringSparseArrayParceler(this)
val parcel = Parcel.obtain()
parceler.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
@Suppress("UNCHECKED_CAST")
val creator = StringSparseArrayParceler::class.java.getField("CREATOR").get(null)
as Parcelable.Creator<StringSparseArrayParceler>
val unParceled = creator.createFromParcel(parcel)
return unParceled.stringSparseArray.also {
check(it !== this)
}
}
}
+1 -4
View File
@@ -58,7 +58,6 @@ dependencies {
implementation project(':conductor')
implementation project(':conductor-modules:viewpager')
implementation project(':conductor-modules:viewpager2')
implementation project(':conductor-modules:rxlifecycle2')
implementation project(':conductor-modules:autodispose')
implementation project(':conductor-modules:arch-components-lifecycle')
implementation project(':conductor-modules:androidx-transition')
@@ -72,7 +71,5 @@ dependencies {
implementation "androidx.activity:activity-compose:1.3.0-beta02"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
debugImplementation rootProject.ext.leakCanary
releaseImplementation rootProject.ext.leakCanaryNoOp
testImplementation rootProject.ext.leakCanaryNoOp
implementation rootProject.ext.leakCanary
}
+8 -9
View File
@@ -1,29 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.bluelinelabs.conductor.demo"
xmlns:android="http://schemas.android.com/apk/res/android">
package="com.bluelinelabs.conductor.demo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name="com.bluelinelabs.conductor.demo.DemoApplication"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:fullBackupContent="true"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="com.bluelinelabs.conductor.demo.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@@ -1,16 +0,0 @@
package com.bluelinelabs.conductor.demo
import android.app.Application
import com.squareup.leakcanary.LeakCanary
import com.squareup.leakcanary.RefWatcher
class DemoApplication : Application() {
override fun onCreate() {
super.onCreate()
refWatcher = LeakCanary.install(this)
}
companion object {
lateinit var refWatcher: RefWatcher
}
}
@@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import com.bluelinelabs.conductor.Conductor.attachRouter
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.Router.PopRootControllerMode
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.demo.controllers.HomeController
import com.bluelinelabs.conductor.demo.databinding.ActivityMainBinding
@@ -23,6 +24,8 @@ class MainActivity : AppCompatActivity(), ToolbarProvider {
setContentView(binding.root)
router = attachRouter(this, binding.controllerContainer, savedInstanceState)
.setPopRootControllerMode(PopRootControllerMode.NEVER)
if (!router.hasRootController()) {
router.setRoot(with(HomeController()))
}
@@ -11,22 +11,20 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.archlifecycle.LifecycleController
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
class ArchLifecycleController : LifecycleController() {
private var hasExited = false
init {
Log.i(TAG, "Conductor: Constructor called")
watchForLeaks()
lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
@@ -104,21 +102,6 @@ class ArchLifecycleController : LifecycleController() {
override fun onDestroy() {
Log.i(TAG, "Conductor: onDestroy() called")
super.onDestroy()
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
@@ -6,14 +6,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.controllers.base.watchForLeaks
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
import com.uber.autodispose.autoDisposable
import io.reactivex.Observable
@@ -23,10 +21,10 @@ import java.util.concurrent.TimeUnit
// instead of Activities or Fragments.
class AutodisposeController : Controller() {
private var hasExited = false
private val scopeProvider = ControllerScopeProvider.from(this)
init {
watchForLeaks()
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
.autoDisposable(scopeProvider)
@@ -98,20 +96,6 @@ class AutodisposeController : Controller() {
public override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy() called")
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
@@ -2,10 +2,10 @@ package com.bluelinelabs.conductor.demo.controllers
import android.graphics.PorterDuff
import android.os.Bundle
import android.support.annotation.DrawableRes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
@@ -17,7 +17,9 @@ import com.bluelinelabs.conductor.demo.databinding.RowHomeBinding
import com.bluelinelabs.conductor.demo.util.viewBinding
class ExternalModulesController : BaseController(R.layout.controller_additional_modules) {
private val binding: ControllerAdditionalModulesBinding by viewBinding(ControllerAdditionalModulesBinding::bind)
private val binding: ControllerAdditionalModulesBinding by viewBinding(
ControllerAdditionalModulesBinding::bind
)
override val title = "External Module Demos"
@@ -40,11 +42,6 @@ class ExternalModulesController : BaseController(R.layout.controller_additional_
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
ModuleModel.RX_LIFECYCLE_2 -> router.pushController(
with(RxLifecycle2Controller())
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
)
ModuleModel.ARCH_LIFECYCLE -> router.pushController(
with(ArchLifecycleController())
.pushChangeHandler(FadeChangeHandler())
@@ -56,7 +53,6 @@ class ExternalModulesController : BaseController(R.layout.controller_additional_
private enum class ModuleModel(val title: String, @ColorRes val color: Int) {
AUTODISPOSE("Autodispose", R.color.purple_300),
RX_LIFECYCLE_2("Rx Lifecycle 2", R.color.blue_grey_300),
ARCH_LIFECYCLE("Arch Components Lifecycle", R.color.orange_300);
}
@@ -83,7 +79,10 @@ private class AdditionalModulesAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ModuleModel) {
binding.title.text = item.title
binding.dot.drawable.setColorFilter(ContextCompat.getColor(itemView.context, item.color), PorterDuff.Mode.SRC_ATOP)
binding.dot.drawable.setColorFilter(
ContextCompat.getColor(itemView.context, item.color),
PorterDuff.Mode.SRC_ATOP
)
itemView.setOnClickListener { modelClickListener(item) }
}
}
@@ -1,6 +1,7 @@
package com.bluelinelabs.conductor.demo.controllers
import android.view.View
import com.bluelinelabs.conductor.Router.PopRootControllerMode
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.controllers.base.BaseController
@@ -18,7 +19,7 @@ class MultipleChildRouterController : BaseController(R.layout.controller_multipl
val childContainers = listOf(binding.container0, binding.container1, binding.container2)
childContainers.forEach { container ->
val childRouter = getChildRouter(container).setPopsLastView(false)
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_BUT_NOT_VIEW)
if (!childRouter.hasRootController()) {
childRouter.setRoot(RouterTransaction.with(NavigationDemoController(0, NavigationDemoController.DisplayUpMode.HIDE)))
}
@@ -3,6 +3,7 @@ package com.bluelinelabs.conductor.demo.controllers
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.Router.PopRootControllerMode
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.bluelinelabs.conductor.demo.R
@@ -11,7 +12,6 @@ import com.bluelinelabs.conductor.demo.databinding.ControllerParentBinding
import com.bluelinelabs.conductor.demo.util.getMaterialColor
import com.bluelinelabs.conductor.demo.util.viewBinding
class ParentController : BaseController(R.layout.controller_parent) {
private val binding: ControllerParentBinding by viewBinding(ControllerParentBinding::bind)
@@ -50,7 +50,7 @@ class ParentController : BaseController(R.layout.controller_parent) {
else -> throw IllegalStateException("Invalid child index $index")
}
val childRouter = getChildRouter(container).setPopsLastView(true)
val childRouter = getChildRouter(container).setPopRootControllerMode(PopRootControllerMode.POP_ROOT_CONTROLLER_AND_VIEW)
if (!childRouter.hasRootController()) {
val child = ChildController(
title = "Child Controller #$index",
@@ -1,119 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction.Companion.with
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.demo.DemoApplication
import com.bluelinelabs.conductor.demo.R
import com.bluelinelabs.conductor.demo.ToolbarProvider
import com.bluelinelabs.conductor.demo.databinding.ControllerLifecycleBinding
import com.bluelinelabs.conductor.rxlifecycle2.ControllerEvent
import com.bluelinelabs.conductor.rxlifecycle2.RxController
import io.reactivex.Observable
import java.util.concurrent.TimeUnit
// Shamelessly borrowed from the official RxLifecycle demo by Trello and adapted for Conductor Controllers
// instead of Activities or Fragments.
class RxLifecycle2Controller : RxController() {
private var hasExited = false
init {
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from constructor") }
.compose(bindUntilEvent(ControllerEvent.DESTROY))
.subscribe { num: Long ->
Log.i(TAG, "Started in constructor, running until onDestroy(): $num")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedViewState: Bundle?
): View {
Log.i(TAG, "onCreateView() called")
val binding = ControllerLifecycleBinding.inflate(inflater, container, false)
binding.title.text = binding.root.resources.getString(R.string.rxlifecycle_title, TAG)
binding.nextReleaseView.setOnClickListener {
retainViewMode = RetainViewMode.RELEASE_DETACH
router.pushController(
with(TextController("Logcat should now report that the observables from onAttach() and onViewBound() have been disposed of, while the constructor observable is still running."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
binding.nextRetainView.setOnClickListener {
retainViewMode = RetainViewMode.RETAIN_DETACH
router.pushController(
with(TextController("Logcat should now report that the observables from onAttach() has been disposed of, while the constructor and onViewBound() observables are still running."))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from onCreateView()") }
.compose(bindUntilEvent(ControllerEvent.DESTROY_VIEW))
.subscribe { num: Long ->
Log.i(TAG, "Started in onCreateView(), running until onDestroyView(): $num")
}
return binding.root
}
override fun onAttach(view: View) {
super.onAttach(view)
Log.i(TAG, "onAttach() called")
(activity as ToolbarProvider).toolbar.title = "RxLifecycle2 Demo"
Observable.interval(1, TimeUnit.SECONDS)
.doOnDispose { Log.i(TAG, "Disposing from onAttach()") }
.compose(bindUntilEvent(ControllerEvent.DETACH))
.subscribe { num: Long ->
Log.i(TAG, "Started in onAttach(), running until onDetach(): $num")
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
Log.i(TAG, "onDestroyView() called")
}
override fun onDetach(view: View) {
super.onDetach(view)
Log.i(TAG, "onDetach() called")
}
public override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy() called")
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
companion object {
private const val TAG = "RxLifecycleController"
}
}
@@ -14,7 +14,11 @@ import com.bluelinelabs.conductor.demo.ToolbarProvider
abstract class BaseController(
@LayoutRes private val layoutRes: Int,
args: Bundle? = null
) : RefWatchingController(args) {
) : Controller(args) {
init {
watchForLeaks()
}
// Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should*
// be accessed. In a production app, this would use Dagger instead.
@@ -1,32 +0,0 @@
package com.bluelinelabs.conductor.demo.controllers.base
import android.os.Bundle
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.demo.DemoApplication
abstract class RefWatchingController(args: Bundle?) : Controller(args) {
private var hasExited = false
public override fun onDestroy() {
super.onDestroy()
if (hasExited) {
DemoApplication.refWatcher.watch(this)
}
}
override fun onChangeEnded(
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
super.onChangeEnded(changeHandler, changeType)
hasExited = !changeType.isEnter
if (isDestroyed) {
DemoApplication.refWatcher.watch(this)
}
}
}
@@ -0,0 +1,39 @@
package com.bluelinelabs.conductor.demo.controllers.base
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import leakcanary.AppWatcher
private class RefWatchingControllerLifecycleListener : Controller.LifecycleListener() {
private var hasExited = false
override fun postDestroy(controller: Controller) {
if (hasExited) {
controller.expectWeaklyReachable()
}
}
override fun onChangeEnd(
controller: Controller,
changeHandler: ControllerChangeHandler,
changeType: ControllerChangeType
) {
hasExited = !changeType.isEnter
if (controller.isDestroyed) {
controller.expectWeaklyReachable()
}
}
private fun Controller.expectWeaklyReachable() {
AppWatcher.objectWatcher.expectWeaklyReachable(
this,
"A destroyed controller should have only weak references."
)
}
}
fun Controller.watchForLeaks() {
addLifecycleListener(RefWatchingControllerLifecycleListener())
}
-1
View File
@@ -30,7 +30,6 @@
<!-- Drag Dismiss Demo -->
<string name="drag_to_dismiss">Elastic scrolling view - scroll past bounds to dismiss.</string>
<!-- RxLifecycle Demo -->
<string name="rxlifecycle_title">Watch tag %1$s on logcat for a demo.</string>
<string name="next_controller_retain">Push Controller, retaining this View</string>
<string name="next_controller_release">Push Controller, releasing this View</string>
+4 -12
View File
@@ -4,19 +4,15 @@ ext {
targetSdkVersion = 28
picassoVersion = '2.5.2'
leakCanaryVersion = '1.5.4'
rxJavaVersion = '1.3.8'
rxJava2Version = '2.1.14'
rxLifecycleVersion = '1.0'
rxLifecycle2Version = '2.2.1'
autodisposeVersion = '1.0.0'
archComponentsVersion = '2.3.1'
junitVersion = '4.12'
mvnPublishVersion = '0.13.0'
mvnPublishVersion = '0.13.0'
dokkaVersion = '1.4.32'
composeVersion = "1.0.0-beta09"
agpVersion = "7.0.0-beta04"
agpVersion = '7.0.3'
lintVersion = agpVersion.replaceFirst(~/\d*/) { version ->
// the major version of lint is always 23 version higher than the major version of agp
version.toInteger() + 23
@@ -35,15 +31,10 @@ ext {
picasso = "com.squareup.picasso:picasso:$picassoVersion"
leakCanary = "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
leakCanaryNoOp = "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"
leakCanary = "com.squareup.leakcanary:leakcanary-android:2.7"
rxJava = "io.reactivex:rxjava:$rxJavaVersion"
rxJava2 = "io.reactivex.rxjava2:rxjava:$rxJava2Version"
rxLifecycle2 = "com.trello.rxlifecycle2:rxlifecycle:$rxLifecycle2Version"
rxLifecycleAndroid2 = "com.trello.rxlifecycle2:rxlifecycle-android:$rxLifecycle2Version"
autodispose = "com.uber.autodispose:autodispose:$autodisposeVersion"
autodisposeLifecycle = "com.uber.autodispose:autodispose-lifecycle:$autodisposeVersion"
autodisposeKtx = "com.uber.autodispose:autodispose-ktx:$autodisposeVersion"
@@ -55,6 +46,7 @@ ext {
junit = "junit:junit:$junitVersion"
robolectric = "org.robolectric:robolectric:4.5.1"
kotestAssertions = "io.kotest:kotest-assertions-core:4.6.0"
lintapi = "com.android.tools.lint:lint-api:$lintVersion"
lintchecks = "com.android.tools.lint:lint-checks:$lintVersion"
+3 -20
View File
@@ -1,25 +1,9 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
VERSION_CODE=3
VERSION_NAME=3.1.5-SNAPSHOT
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=3.1.1-SNAPSHOT
VERSION_CODE=3
GROUP=com.bluelinelabs
POM_DESCRIPTION=A small, yet full-featured framework that allows building View-based Android applications
POM_URL=https://github.com/bluelinelabs/Conductor
POM_SCM_URL=https://github.com/bluelinelabs/Conductor
@@ -30,4 +14,3 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=erickuck
POM_DEVELOPER_NAME=Eric Kuck
android.useAndroidX=true
-1
View File
@@ -2,7 +2,6 @@ include ':conductor'
include ':conductor-lint'
include ':conductor-modules:viewpager'
include ':conductor-modules:viewpager2'
include ':conductor-modules:rxlifecycle2'
include ':conductor-modules:autodispose'
include ':conductor-modules:arch-components-lifecycle'
include ':conductor-modules:androidx-transition'