diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 000000000..168974fd4 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + repositories { + jcenter() + google() + } + + dependencies { + classpath deps.build.gradlePlugins.android + classpath deps.build.gradlePlugins.kotlin + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion deps.build.compileSdkVersion + buildToolsVersion deps.build.buildToolsVersion + + defaultConfig { + minSdkVersion deps.build.minSdkVersion + targetSdkVersion deps.build.targetSdkVersion + } + compileOptions { + // TODO Go to 1.8 + D8 on AGP 3.x + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + provided deps.misc.javaxExtras + compile project(':autodispose') + compile project(':autodispose-android') + compile project(':autodispose-android-archcomponents') + compile project(':autodispose-kotlin') + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support.constraint:constraint-layout:1.1.0-beta1' + compile 'com.android.support:design:26.1.0' +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100755 index 000000000..afbe05b0d --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/java/android/support/v7/widget/BindAwareViewHolder.java b/sample/src/main/java/android/support/v7/widget/BindAwareViewHolder.java new file mode 100644 index 000000000..94b61471c --- /dev/null +++ b/sample/src/main/java/android/support/v7/widget/BindAwareViewHolder.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.view.View; + +/** + * A bind-aware {@link RecyclerView.ViewHolder} implementation that knows when it's bound or + * unbound. + *

+ * Disclaimer: This is in no way supported and THIS COULD BREAK AT ANY TIME. Left for research. + */ +public abstract class BindAwareViewHolder extends RecyclerView.ViewHolder { + + public BindAwareViewHolder(View itemView) { + super(itemView); + } + + protected void onBind() { + + } + + protected void onUnbind() { + + } + + @Override void setFlags(int flags, int mask) { + boolean wasBound = isBound(); + super.setFlags(flags, mask); + notifyBinding(wasBound, isBound()); + } + + @Override void addFlags(int flags) { + boolean wasBound = isBound(); + super.addFlags(flags); + notifyBinding(wasBound, isBound()); + } + + @Override void clearPayload() { + boolean wasBound = isBound(); + super.clearPayload(); + notifyBinding(wasBound, isBound()); + } + + @Override void resetInternal() { + boolean wasBound = isBound(); + super.resetInternal(); + notifyBinding(wasBound, isBound()); + } + + private void notifyBinding(boolean previousBound, boolean currentBound) { + if (previousBound && !currentBound) { + onUnbind(); + } else if (!previousBound && currentBound) { + onBind(); + } + } +} diff --git a/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeActivity.java b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeActivity.java new file mode 100644 index 000000000..aa200892a --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeActivity.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.LifecycleScopeProvider; +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; + +/** + * An {@link Activity} example implementation for making one implement {@link + * LifecycleScopeProvider}. One would normally use this as a base activity class to extend others + * from. + */ +public abstract class AutoDisposeActivity extends Activity + implements LifecycleScopeProvider { + + public enum ActivityEvent { + CREATE, START, RESUME, PAUSE, STOP, DESTROY + } + + /** + * This is a function of current event -> target disposal event. That is to say that if event A + * returns B, then any stream subscribed to during A will autodispose on B. In Android, we make + * symmetric boundary conditions. Create -> Destroy, Start -> Stop, etc. For anything after Resume + * we dispose on the next immediate destruction event. Subscribing after Destroy is an error. + */ + private static Function CORRESPONDING_EVENTS = + new Function() { + @Override public ActivityEvent apply(ActivityEvent activityEvent) throws Exception { + switch (activityEvent) { + case CREATE: + return ActivityEvent.DESTROY; + case START: + return ActivityEvent.STOP; + case RESUME: + return ActivityEvent.PAUSE; + case PAUSE: + return ActivityEvent.STOP; + case STOP: + return ActivityEvent.DESTROY; + default: + throw new LifecycleEndedException("Cannot bind to Activity lifecycle after destroy."); + } + } + }; + + private final BehaviorSubject lifecycleEvents = BehaviorSubject.create(); + + @Override public Observable lifecycle() { + return lifecycleEvents.hide(); + } + + @Override public Function correspondingEvents() { + return CORRESPONDING_EVENTS; + } + + @Nullable @Override public ActivityEvent peekLifecycle() { + return lifecycleEvents.getValue(); + } + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lifecycleEvents.onNext(ActivityEvent.CREATE); + } + + @Override protected void onStart() { + super.onStart(); + lifecycleEvents.onNext(ActivityEvent.START); + } + + @Override protected void onResume() { + super.onResume(); + lifecycleEvents.onNext(ActivityEvent.RESUME); + } + + @Override protected void onPause() { + lifecycleEvents.onNext(ActivityEvent.PAUSE); + super.onPause(); + } + + @Override protected void onStop() { + lifecycleEvents.onNext(ActivityEvent.STOP); + super.onStop(); + } + + @Override protected void onDestroy() { + lifecycleEvents.onNext(ActivityEvent.DESTROY); + super.onDestroy(); + } +} diff --git a/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeFragment.java b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeFragment.java new file mode 100644 index 000000000..f1885bc56 --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeFragment.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.View; +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.LifecycleScopeProvider; +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; + +/** + * A {@link Fragment} example implementation for making one implement {@link + * LifecycleScopeProvider}. One would normally use this as a base fragment class to extend others + * from. + */ +public abstract class AutoDisposeFragment extends Fragment + implements LifecycleScopeProvider { + + public enum FragmentEvent { + ATTACH, CREATE, CREATE_VIEW, START, RESUME, PAUSE, STOP, DESTROY_VIEW, DESTROY, DETACH + } + + /** + * This is a function of current event -> target disposal event. That is to say that if event A + * returns B, then any stream subscribed to during A will autodispose on B. In Android, we make + * symmetric boundary conditions. Create -> Destroy, Start -> Stop, etc. For anything after Resume + * we dispose on the next immediate destruction event. Subscribing after Detach is an error. + */ + private static Function CORRESPONDING_EVENTS = + new Function() { + @Override public FragmentEvent apply(FragmentEvent event) throws Exception { + switch (event) { + case ATTACH: + return FragmentEvent.DETACH; + case CREATE: + return FragmentEvent.DESTROY; + case CREATE_VIEW: + return FragmentEvent.DESTROY_VIEW; + case START: + return FragmentEvent.STOP; + case RESUME: + return FragmentEvent.PAUSE; + case PAUSE: + return FragmentEvent.STOP; + case STOP: + return FragmentEvent.DESTROY_VIEW; + case DESTROY_VIEW: + return FragmentEvent.DESTROY; + case DESTROY: + return FragmentEvent.DETACH; + default: + throw new LifecycleEndedException("Cannot bind to Fragment lifecycle after detach."); + } + } + }; + + private final BehaviorSubject lifecycleEvents = BehaviorSubject.create(); + + @Override public Observable lifecycle() { + return lifecycleEvents.hide(); + } + + @Override public Function correspondingEvents() { + return CORRESPONDING_EVENTS; + } + + @Nullable @Override public FragmentEvent peekLifecycle() { + return lifecycleEvents.getValue(); + } + + @Override public void onAttach(Context context) { + super.onAttach(context); + lifecycleEvents.onNext(FragmentEvent.ATTACH); + } + + @Override public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + lifecycleEvents.onNext(FragmentEvent.CREATE); + } + + @Override public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + lifecycleEvents.onNext(FragmentEvent.CREATE_VIEW); + } + + @Override public void onStart() { + super.onStart(); + lifecycleEvents.onNext(FragmentEvent.START); + } + + @Override public void onResume() { + super.onResume(); + lifecycleEvents.onNext(FragmentEvent.RESUME); + } + + @Override public void onPause() { + lifecycleEvents.onNext(FragmentEvent.PAUSE); + super.onPause(); + } + + @Override public void onStop() { + lifecycleEvents.onNext(FragmentEvent.STOP); + super.onStop(); + } + + @Override public void onDestroyView() { + lifecycleEvents.onNext(FragmentEvent.DESTROY_VIEW); + super.onDestroyView(); + } + + @Override public void onDestroy() { + lifecycleEvents.onNext(FragmentEvent.DESTROY); + super.onDestroy(); + } + + @Override public void onDetach() { + lifecycleEvents.onNext(FragmentEvent.DETACH); + super.onDetach(); + } +} diff --git a/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeView.java b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeView.java new file mode 100644 index 000000000..10926e062 --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeView.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.view.View; +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.LifecycleScopeProvider; +import com.uber.autodispose.android.ViewScopeProvider; +import io.reactivex.Observable; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; + +/** + * An example implementation of an AutoDispose View with lifecycle handling and precondition checks + * using {@link LifecycleScopeProvider}. The precondition checks here are only different from what + * {@link ViewScopeProvider} provides in that it will check against subscription in the constructor. + */ +public abstract class AutoDisposeView extends View + implements LifecycleScopeProvider { + + /** + * This is a function of current event -> target disposal event. That is to say that if event + * "Attach" returns "Detach", then any stream subscribed to during Attach will autodispose on + * Detach. + */ + private static Function CORRESPONDING_EVENTS = + new Function() { + @Override public ViewEvent apply(ViewEvent viewEvent) throws Exception { + switch (viewEvent) { + case ATTACH: + return ViewEvent.DETACH; + default: + throw new LifecycleEndedException("Cannot bind to View lifecycle after detach."); + } + } + }; + + @Nullable private BehaviorSubject lifecycleEvents = null; + + public AutoDisposeView(Context context) { + this(context, null); + } + + public AutoDisposeView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, View.NO_ID); + } + + public AutoDisposeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public AutoDisposeView(Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + if (!isInEditMode()) { + // This is important to gate so you don't break the IDE preview! + lifecycleEvents = BehaviorSubject.create(); + } + } + + public enum ViewEvent { + ATTACH, DETACH + } + + @Override protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (lifecycleEvents != null) { + lifecycleEvents.onNext(ViewEvent.ATTACH); + } + } + + @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (lifecycleEvents != null) { + lifecycleEvents.onNext(ViewEvent.DETACH); + } + } + + @Override public Observable lifecycle() { + //noinspection ConstantConditions only in layoutlib + return lifecycleEvents.hide(); + } + + @Override public Function correspondingEvents() { + return CORRESPONDING_EVENTS; + } + + @Nullable @Override public ViewEvent peekLifecycle() { + //noinspection ConstantConditions only in layoutlib + return lifecycleEvents.getValue(); + } +} diff --git a/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeViewHolder.java b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeViewHolder.java new file mode 100644 index 000000000..23eaaa648 --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeViewHolder.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes; + +import android.support.annotation.Nullable; +import android.support.v7.widget.BindAwareViewHolder; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import com.uber.autodispose.ScopeProvider; +import io.reactivex.Maybe; +import io.reactivex.subjects.MaybeSubject; + +/** + * Example implementation of a {@link RecyclerView.ViewHolder} implementation that implements + * {@link ScopeProvider}. This could be useful for cases where you have subscriptions that should be + * disposed upon unbinding or otherwise aren't overwritten in future binds. + */ +public abstract class AutoDisposeViewHolder extends BindAwareViewHolder implements ScopeProvider { + + private static Object NOTIFICATION = new Object(); + + @Nullable private MaybeSubject unbindNotifier = null; + + public AutoDisposeViewHolder(View itemView) { + super(itemView); + } + + private synchronized MaybeSubject notifier() { + MaybeSubject n = unbindNotifier; + if (n == null) { + n = MaybeSubject.create(); + unbindNotifier = n; + } + return n; + } + + @Override protected void onUnbind() { + emitUnbindIfPresent(); + unbindNotifier = null; + } + + private void emitUnbindIfPresent() { + MaybeSubject notifier = unbindNotifier; + if (notifier != null && !notifier.hasComplete()) { + notifier.onSuccess(NOTIFICATION); + } + } + + @Override public final Maybe requestScope() { + return notifier(); + } +} diff --git a/sample/src/main/java/com/uber/autodispose/recipes/package-info.java b/sample/src/main/java/com/uber/autodispose/recipes/package-info.java new file mode 100644 index 000000000..682eef0ab --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/recipes/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Android recipes for AutoDispose. + */ +@com.uber.javaxextras.FieldsMethodsAndParametersAreNonNullByDefault +package com.uber.autodispose.recipes; + diff --git a/sample/src/main/java/com/uber/autodispose/sample/MainActivity.java b/sample/src/main/java/com/uber/autodispose/sample/MainActivity.java new file mode 100644 index 000000000..889ee410d --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/sample/MainActivity.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.sample; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import com.uber.autodispose.AutoDispose; +import com.uber.autodispose.android.lifecycle.AndroidLifecycle; +import io.reactivex.Observable; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import java.util.concurrent.TimeUnit; + +/** + * Demo activity, shamelessly borrowed from the RxLifecycle sample + *

+ * This leverages the Architecture Components support for the demo + */ +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "AutoDispose"; + + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Log.d(TAG, "onCreate()"); + + setContentView(R.layout.activity_main); + + // Using automatic disposal, this should determine that the correct time to + // dispose is onCreate (the opposite of onStop). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose(new Action() { + @Override public void run() throws Exception { + Log.i(TAG, "Disposing subscription from onCreate()"); + } + }) + .to(AutoDispose.with(AndroidLifecycle.from(this)).forObservable()) + .subscribe(new Consumer() { + @Override public void accept(Long num) throws Exception { + Log.i(TAG, "Started in onCreate(), running until onDestroy(): " + num); + } + }); + } + + @Override protected void onStart() { + super.onStart(); + + Log.d(TAG, "onStart()"); + + // Using automatic disposal, this should determine that the correct time to + // dispose is onStop (the opposite of onStart). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose(new Action() { + @Override public void run() throws Exception { + Log.i(TAG, "Disposing subscription from onStart()"); + } + }) + .to(AutoDispose.with(AndroidLifecycle.from(this)).forObservable()) + .subscribe(new Consumer() { + @Override public void accept(Long num) throws Exception { + Log.i(TAG, "Started in onStart(), running until in onStop(): " + num); + } + }); + } + + @Override protected void onResume() { + super.onResume(); + + Log.d(TAG, "onResume()"); + + // Using automatic disposal, this should determine that the correct time to + // dispose is onPause (the opposite of onResume). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose(new Action() { + @Override public void run() throws Exception { + Log.i(TAG, "Disposing subscription from onResume()"); + } + }) + // `.forObservable` is necessary if you're compiling on JDK7 or below. + // If you're using JDK8+, then you can safely remove it. + .to(AutoDispose.with(AndroidLifecycle.from(this)).forObservable()) + .subscribe(new Consumer() { + @Override public void accept(Long num) throws Exception { + Log.i(TAG, "Started in onResume(), running until in onDestroy(): " + num); + } + }); + } + + @Override protected void onPause() { + Log.d(TAG, "onPause()"); + super.onPause(); + } + + @Override protected void onStop() { + Log.d(TAG, "onStop()"); + super.onStop(); + } + + @Override protected void onDestroy() { + Log.d(TAG, "onDestroy()"); + super.onDestroy(); + } +} diff --git a/sample/src/main/java/com/uber/autodispose/sample/package-info.java b/sample/src/main/java/com/uber/autodispose/sample/package-info.java new file mode 100644 index 000000000..332b7ff5e --- /dev/null +++ b/sample/src/main/java/com/uber/autodispose/sample/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Android recipes for AutoDispose. + */ +@com.uber.javaxextras.FieldsMethodsAndParametersAreNonNullByDefault +package com.uber.autodispose.sample; + diff --git a/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeActivityKotlin.kt b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeActivityKotlin.kt new file mode 100644 index 000000000..fad319713 --- /dev/null +++ b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeActivityKotlin.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes + +import android.app.Activity +import android.os.Bundle +import com.uber.autodispose.LifecycleEndedException +import com.uber.autodispose.LifecycleScopeProvider +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.CREATE +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.DESTROY +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.PAUSE +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.RESUME +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.START +import com.uber.autodispose.recipes.AutoDisposeActivityKotlin.ActivityEvent.STOP +import io.reactivex.Observable +import io.reactivex.functions.Function +import io.reactivex.subjects.BehaviorSubject + +/** + * An [Activity] example implementation for making one implement [LifecycleScopeProvider]. One + * would normally use this as a base activity class to extend others from. + */ +abstract class AutoDisposeActivityKotlin : Activity(), LifecycleScopeProvider { + + private val lifecycleEvents = BehaviorSubject.create() + + enum class ActivityEvent { + CREATE, START, RESUME, PAUSE, STOP, DESTROY + } + + override fun lifecycle(): Observable { + return lifecycleEvents.hide() + } + + override fun correspondingEvents(): Function { + return CORRESPONDING_EVENTS + } + + override fun peekLifecycle(): ActivityEvent? { + return lifecycleEvents.value + } + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + lifecycleEvents.onNext(ActivityEvent.CREATE) + } + + override fun onStart() { + super.onStart() + lifecycleEvents.onNext(ActivityEvent.START) + } + + override fun onResume() { + super.onResume() + lifecycleEvents.onNext(ActivityEvent.RESUME) + } + + override fun onPause() { + lifecycleEvents.onNext(ActivityEvent.PAUSE) + super.onPause() + } + + override fun onStop() { + lifecycleEvents.onNext(ActivityEvent.STOP) + super.onStop() + } + + override fun onDestroy() { + lifecycleEvents.onNext(ActivityEvent.DESTROY) + super.onDestroy() + } + + companion object { + + /** + * This is a function of current event -> target disposal event. That is to say that if event A + * returns B, then any stream subscribed to during A will autodispose on B. In Android, we make + * symmetric boundary conditions. Create -> Destroy, Start -> Stop, etc. For anything after + * Resume we dispose on the next immediate destruction event. Subscribing after Destroy is an + * error. + */ + private val CORRESPONDING_EVENTS = Function { activityEvent -> + when (activityEvent) { + CREATE -> DESTROY + START -> STOP + RESUME -> PAUSE + PAUSE -> STOP + STOP -> DESTROY + else -> throw LifecycleEndedException("Cannot bind to Activity lifecycle after destroy.") + } + } + } +} diff --git a/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeFragmentKotlin.kt b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeFragmentKotlin.kt new file mode 100644 index 000000000..022d3c63b --- /dev/null +++ b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeFragmentKotlin.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes + +import android.app.Fragment +import android.content.Context +import android.os.Bundle +import android.view.View +import com.uber.autodispose.LifecycleEndedException +import com.uber.autodispose.LifecycleScopeProvider +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.ATTACH +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.CREATE +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.CREATE_VIEW +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.DESTROY +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.DESTROY_VIEW +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.DETACH +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.PAUSE +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.RESUME +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.START +import com.uber.autodispose.recipes.AutoDisposeFragmentKotlin.FragmentEvent.STOP +import io.reactivex.Observable +import io.reactivex.functions.Function +import io.reactivex.subjects.BehaviorSubject + +/** + * A [Fragment] example implementation for making one implement [LifecycleScopeProvider]. One would + * normally use this as a base fragment class to extend others from. + */ +abstract class AutoDisposeFragmentKotlin : Fragment(), LifecycleScopeProvider { + + private val lifecycleEvents = BehaviorSubject.create() + + enum class FragmentEvent { + ATTACH, CREATE, CREATE_VIEW, START, RESUME, PAUSE, STOP, DESTROY_VIEW, DESTROY, DETACH + } + + override fun lifecycle(): Observable { + return lifecycleEvents.hide() + } + + override fun correspondingEvents(): Function { + return CORRESPONDING_EVENTS + } + + override fun peekLifecycle(): FragmentEvent? { + return lifecycleEvents.value + } + + override fun onAttach(context: Context) { + super.onAttach(context) + lifecycleEvents.onNext(FragmentEvent.ATTACH) + } + + override fun onCreate(savedInstanceState: Bundle) { + super.onCreate(savedInstanceState) + lifecycleEvents.onNext(FragmentEvent.CREATE) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle) { + super.onViewCreated(view, savedInstanceState) + lifecycleEvents.onNext(FragmentEvent.CREATE_VIEW) + } + + override fun onStart() { + super.onStart() + lifecycleEvents.onNext(FragmentEvent.START) + } + + override fun onResume() { + super.onResume() + lifecycleEvents.onNext(FragmentEvent.RESUME) + } + + override fun onPause() { + lifecycleEvents.onNext(FragmentEvent.PAUSE) + super.onPause() + } + + override fun onStop() { + lifecycleEvents.onNext(FragmentEvent.STOP) + super.onStop() + } + + override fun onDestroyView() { + lifecycleEvents.onNext(FragmentEvent.DESTROY_VIEW) + super.onDestroyView() + } + + override fun onDestroy() { + lifecycleEvents.onNext(FragmentEvent.DESTROY) + super.onDestroy() + } + + override fun onDetach() { + lifecycleEvents.onNext(FragmentEvent.DETACH) + super.onDetach() + } + + companion object { + + /** + * This is a function of current event -> target disposal event. That is to say that if event A + * returns B, then any stream subscribed to during A will autodispose on B. In Android, we make + * symmetric boundary conditions. Create -> Destroy, Start -> Stop, etc. For anything after + * Resume we dispose on the next immediate destruction event. Subscribing after Detach is an + * error. + */ + private val CORRESPONDING_EVENTS = Function { event -> + when (event) { + ATTACH -> DETACH + CREATE -> DESTROY + CREATE_VIEW -> DESTROY_VIEW + START -> STOP + RESUME -> PAUSE + PAUSE -> STOP + STOP -> DESTROY_VIEW + DESTROY_VIEW -> DESTROY + DESTROY -> DETACH + else -> throw LifecycleEndedException("Cannot bind to Fragment lifecycle after detach.") + } + } + } +} diff --git a/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewHolderKotlin.kt b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewHolderKotlin.kt new file mode 100644 index 000000000..904d75d2f --- /dev/null +++ b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewHolderKotlin.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes + +import android.support.v7.widget.BindAwareViewHolder +import android.support.v7.widget.RecyclerView.ViewHolder +import android.view.View +import com.uber.autodispose.ScopeProvider +import io.reactivex.Maybe +import io.reactivex.subjects.MaybeSubject + +private object NOTIFICATION + +/** + * Example implementation of a [ViewHolder] implementation that implements + * [ScopeProvider]. This could be useful for cases where you have subscriptions that should be + * disposed upon unbinding or otherwise aren't overwritten in future binds. + */ +abstract class AutoDisposeViewHolderKotlin(itemView: View) + : BindAwareViewHolder(itemView), ScopeProvider { + + private var unbindNotifier: MaybeSubject? = null + + private val notifier: MaybeSubject + get() { + synchronized(this) { + return unbindNotifier ?: MaybeSubject.create().also { unbindNotifier = it } + } + } + + override fun onUnbind() { + emitUnbindIfPresent() + unbindNotifier = null + } + + private fun emitUnbindIfPresent() { + unbindNotifier?.let { + if (!it.hasComplete()) { + it.onSuccess(NOTIFICATION) + } + } + } + + override fun requestScope(): Maybe<*> { + return notifier + } + +} diff --git a/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewKotlin.kt b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewKotlin.kt new file mode 100644 index 000000000..588c68460 --- /dev/null +++ b/sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewKotlin.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.recipes + +import android.content.Context +import android.os.Build +import android.support.annotation.RequiresApi +import android.util.AttributeSet +import android.view.View +import com.uber.autodispose.LifecycleEndedException +import com.uber.autodispose.LifecycleScopeProvider +import com.uber.autodispose.android.ViewScopeProvider +import com.uber.autodispose.recipes.AutoDisposeViewKotlin.ViewEvent +import io.reactivex.Observable +import io.reactivex.functions.Function +import io.reactivex.subjects.BehaviorSubject + +/** + * An example implementation of an AutoDispose View with lifecycle handling and precondition checks + * using [LifecycleScopeProvider]. The precondition checks here are only different from what + * [ViewScopeProvider] provides in that it will check against subscription in the constructor. + */ +abstract class AutoDisposeViewKotlin : View, LifecycleScopeProvider { + + enum class ViewEvent { + ATTACH, DETACH + } + + private val lifecycleEvents by lazy { BehaviorSubject.create() } + + @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = View.NO_ID) + : super(context, attrs, defStyleAttr) + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) + : super(context, attrs, defStyleAttr, defStyleRes) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + lifecycleEvents.onNext(ViewEvent.ATTACH) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + lifecycleEvents.onNext(ViewEvent.DETACH) + } + + override fun lifecycle(): Observable = lifecycleEvents.hide() + + override fun correspondingEvents(): Function { + return CORRESPONDING_EVENTS + } + + override fun peekLifecycle(): ViewEvent? { + return lifecycleEvents.value + } + + companion object { + + /** + * This is a function of current event -> target disposal event. That is to say that if event + * "Attach" returns "Detach", then any stream subscribed to during Attach will autodispose on + * Detach. + */ + private val CORRESPONDING_EVENTS = Function { viewEvent -> + when (viewEvent) { + ViewEvent.ATTACH -> ViewEvent.DETACH + else -> throw LifecycleEndedException("Cannot bind to View lifecycle after detach.") + } + } + } +} diff --git a/sample/src/main/kotlin/com/uber/autodispose/sample/KotlinActivity.kt b/sample/src/main/kotlin/com/uber/autodispose/sample/KotlinActivity.kt new file mode 100644 index 000000000..4e1ea119d --- /dev/null +++ b/sample/src/main/kotlin/com/uber/autodispose/sample/KotlinActivity.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017. Uber Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.uber.autodispose.sample + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.util.Log +import com.uber.autodispose.android.lifecycle.AndroidLifecycle +import com.uber.autodispose.kotlin.autoDisposeWith +import io.reactivex.Observable +import java.util.concurrent.TimeUnit + +/** + * Demo activity, shamelessly borrowed from RxLifecycle's demo. + * + * This leverages the Architecture Components support for the demo + */ +class KotlinActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate()") + setContentView(R.layout.activity_main) + + // Using automatic disposal, this should determine that the correct time to + // dispose is onDestroy (the opposite of onCreate). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose { Log.i(TAG, "Disposing subscription from onCreate()") } + .autoDisposeWith(AndroidLifecycle.from(this)) + .subscribe { num -> Log.i(TAG, "Started in onCreate(), running until onDestroy(): " + num) } + } + + override fun onStart() { + super.onStart() + + Log.d(TAG, "onStart()") + + // Using automatic disposal, this should determine that the correct time to + // dispose is onStop (the opposite of onStart). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose { Log.i(TAG, "Disposing subscription from onStart()") } + .autoDisposeWith(AndroidLifecycle.from(this)) + .subscribe { num -> Log.i(TAG, "Started in onStart(), running until in onStop(): " + num) } + } + + override fun onResume() { + super.onResume() + + Log.d(TAG, "onResume()") + + // Using automatic disposal, this should determine that the correct time to + // dispose is onPause (the opposite of onResume). + Observable.interval(1, TimeUnit.SECONDS) + .doOnDispose { Log.i(TAG, "Disposing subscription from onResume()") } + .autoDisposeWith(AndroidLifecycle.from(this)) + .subscribe { num -> + Log.i(TAG, "Started in onResume(), running until in onPause(): " + num) + } + } + + override fun onPause() { + Log.d(TAG, "onPause()") + super.onPause() + } + + override fun onStop() { + Log.d(TAG, "onStop()") + super.onStop() + } + + override fun onDestroy() { + Log.d(TAG, "onDestroy()") + super.onDestroy() + } + + companion object { + private val TAG = "AutoDispose-Kotlin" + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..5ceab5283 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/content_main.xml b/sample/src/main/res/layout/content_main.xml new file mode 100644 index 000000000..f59a5c90f --- /dev/null +++ b/sample/src/main/res/layout/content_main.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0536ae776 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ebc4adbb0 Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e3e2f8df6 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b88d9c17d Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..128849a54 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml new file mode 100644 index 000000000..11ae6a708 --- /dev/null +++ b/sample/src/main/res/values/dimens.xml @@ -0,0 +1,19 @@ + + + + 16dp + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 000000000..f840a12e2 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + AutoDispose Demo + AutoDispose Kotlin Demo + AutoDispose Demo + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 000000000..1cce474b1 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + +