From f9874afe8dea1792dbd0c856138debf0e761ab0c Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Sun, 1 Oct 2017 00:45:32 -0700 Subject: [PATCH] Sample app! (#97) * Stub a basic android sample scaffolding * Stub a basic android sample scaffolding * Add fleshed out mainactivity and kotlinactivity examples * Add recipes for AutoDisposeActivities * Fix missing symmetries in activity events * Put kotlin at end of file name for better consistency later * Add message to after-destroy exceptions for clarity * Add base class details * Add Fragment demo * Add View demo * Add AutoDisposeViewHolder demo * Better nullability annotations handling * Suppress a couple more * More idiomatic kotlin in AutoDisposeView + lazy * Cleaner When's * Fix dependencies * Fix capitalization * More idiomatic unbindernotifier handling * Document the corresponding event methods --- sample/build.gradle | 59 ++++++++ sample/src/main/AndroidManifest.xml | 55 +++++++ .../v7/widget/BindAwareViewHolder.java | 72 +++++++++ .../recipes/AutoDisposeActivity.java | 109 ++++++++++++++ .../recipes/AutoDisposeFragment.java | 139 ++++++++++++++++++ .../autodispose/recipes/AutoDisposeView.java | 119 +++++++++++++++ .../recipes/AutoDisposeViewHolder.java | 66 +++++++++ .../autodispose/recipes/package-info.java | 22 +++ .../uber/autodispose/sample/MainActivity.java | 119 +++++++++++++++ .../uber/autodispose/sample/package-info.java | 22 +++ .../recipes/AutoDisposeActivityKotlin.kt | 107 ++++++++++++++ .../recipes/AutoDisposeFragmentKotlin.kt | 137 +++++++++++++++++ .../recipes/AutoDisposeViewHolderKotlin.kt | 62 ++++++++ .../recipes/AutoDisposeViewKotlin.kt | 89 +++++++++++ .../uber/autodispose/sample/KotlinActivity.kt | 93 ++++++++++++ sample/src/main/res/layout/activity_main.xml | 45 ++++++ sample/src/main/res/layout/content_main.xml | 29 ++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2523 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1798 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3740 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6149 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8480 bytes sample/src/main/res/values/dimens.xml | 19 +++ sample/src/main/res/values/strings.xml | 21 +++ sample/src/main/res/values/styles.xml | 20 +++ settings.gradle | 1 + 26 files changed, 1405 insertions(+) create mode 100644 sample/build.gradle create mode 100755 sample/src/main/AndroidManifest.xml create mode 100644 sample/src/main/java/android/support/v7/widget/BindAwareViewHolder.java create mode 100644 sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeActivity.java create mode 100644 sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeFragment.java create mode 100644 sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeView.java create mode 100644 sample/src/main/java/com/uber/autodispose/recipes/AutoDisposeViewHolder.java create mode 100644 sample/src/main/java/com/uber/autodispose/recipes/package-info.java create mode 100644 sample/src/main/java/com/uber/autodispose/sample/MainActivity.java create mode 100644 sample/src/main/java/com/uber/autodispose/sample/package-info.java create mode 100644 sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeActivityKotlin.kt create mode 100644 sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeFragmentKotlin.kt create mode 100644 sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewHolderKotlin.kt create mode 100644 sample/src/main/kotlin/com/uber/autodispose/recipes/AutoDisposeViewKotlin.kt create mode 100644 sample/src/main/kotlin/com/uber/autodispose/sample/KotlinActivity.kt create mode 100644 sample/src/main/res/layout/activity_main.xml create mode 100644 sample/src/main/res/layout/content_main.xml create mode 100644 sample/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 sample/src/main/res/values/dimens.xml create mode 100644 sample/src/main/res/values/strings.xml create mode 100644 sample/src/main/res/values/styles.xml 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 0000000000000000000000000000000000000000..0536ae776291d4ab8245555cc06c7c20d3cb5c5b GIT binary patch literal 2523 zcmV<12_*K3P)Tww#qw`n|=;`nYP*<>2GZ$U#Jp z=oXm_BpXQ^8X8(F_HWIi$ieMvh!FwC7`%M>a?z0*^~dA!G!z#XuZpN7syWhQ?1YIr z^5Vq{H{yu~P?87xL|eBgsm^FLu8x=_DuA?|oSclOZ;Iq#XIL~u4?$9-2*SR-5G8#Z zF-fAJxUsRZRIw8tv2!dMV1gkgHGD0yl0-o(hlhvrNlXj~l1r)&yChN2>i+)zqcI`~ zk|B@_5}$`R-C{(LVgxBf(!xucMG(Y-ps=Rf zQivc0NlxjUZQhb%DT3s$Ni`@~BrS-fSc@PvO)hEP=WekeD8Ljy8#5%;*VnIEa)N?I zQILA%znH9*rQ50{CrB=d9%EF@CEaR!dwX9E`NH5r2yzXzu|`)Fb9>xjkwg2v*KOd( zAxY!o<7{MPgpH1lGCawa0BK8qmqE|nv_(OgA*b4W1<|)z*?b1_>~BHKV<)345(_DZ z_V>W$VAx=`o|?rPEN9(o*ssSX4rR5Utb>wPn$Rc0|V^o)2D#A@i{mgjzYR} zAzk8$$TUxaS{^pB0_$(#O#Lo+Ha4=Lf=0)N*~!}XA;-KETH2fV@#u9)l9Cir3>zFA zBmnW?Yhbh4NYJW<6r>6Y_nWonST079xwQz~Zug8Rm~v0Cv58UEKjLDK`kUB)`|jZO zK7UTHPfbnDh(6j+Rm&;JvL?ZenzQ^k^1n;z=5o2%c76 z!%@e(aPjs&u(n?S*I=7h)}y`$>`3(vIBDJuU0q$?=<$~{F)mARC`DeMUu&6T0BAYjZ^NIRA)jiMoF&yR~Q{T1VRv$ZA%t; z6S8bcaLJYqwBOl^KS?5qesZ`V{Je)EX;B4H_|vz3qULv8kg^>0g0eVCkZ*qnn!6o} z=@m$lU(sHCK~zldb{bfL<83i|<|;@oM-W6Dv2BIhkIGd^qDGP=LE<#e1)0s}qKFHk zPB(U$S-#`f^OO@f?gFKXK5f>!PG)?R%&RjlXWO5ct5JCI; zU*COIi&04u36j=9;Sdy5)U)vsR#?B=R}7Me1^RkZ78o8Bz|orD_{f<}P(G$ud3yM)w*Kr+&hle@sJ$Lzx zrbZ5ZmfkOY*4gpz8G;BCE=KYA5TKvnCkaqeSOqCEIMTcA30` z?CNX7wAneU93DX!Y4m>Svv)hIX2>D*)(OG_lAdsL_Sc|A1p$KYdXo!rU?*4X+o*h1g zHxR+)%sz7|$ssZ=8tCqS7=40himh+|hJ`9cogDRo^p*suYrn2$%8#rd%KP%B<3S~a zU5+T|W@A36s6&wzM3*8;8xAjsoM5RnTl5em^#_6a6IFufJm;KqFHXN(Re)#YJ|z2) z=_N_PqV=N38skeM=m>Lym%`eQA=qV*ND11&$m4Lj4K4 zP>gs4QHRbr_CT5;83(4Frc4X}ysCwR#Mc7xGxQ)RZ1j6T9rQukjjY{s1*3h?jtBVQj9uOSi6U(h|ZV{he!e!q2-W-&u@qIl8UU}gpyFo zp&Qe34wp-EP|7W>pB$7TqB9BT2@+#Y3_5EOIxi$Bnj=Uy6X{J!f*di0pIWZzg4oULL5o=89Ee)?RsWLf&AkDNOqmWiQF{RYX5d`guW%m%Ec5TIY^pUkOt*w1UaH0L6XLsUcBxo$k8jv$*^pRWAaATi2vGR&LdwBw`bTqvGJLH_4SO6bfQML7bATuz3# z#tBu9f}rS0F|~sDjAZsrt#l(v!i63#3Ny^p3&2q25T|OQR8Udvr>X@Bku+0|d}TaS z%8CgF5JXNp%RMnO)RX$w>|T%i6$ ztOgxddqvUZ>Skq@ldPcP&yZX3!R#1h+>#!@@B2D`F6R%B{)|d4s`Fid>FSN@)ywy?!5#>1Q;y4I4JRGOz#s zVg35`-^J|@kRKy&AU}z5-Vn#~{dMcsy(<4NF6P`&(i)7WwYYr^cly0phQ=U{B`m*) lj$0xux8=6nmRn4;{{x|3qgx~;xPkxx002ovPDHLkV1nFyx&Z(H literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ebc4adbb008613b8025b78262ae88e3c09e43b1f GIT binary patch literal 1798 zcmV+h2l@DkP)E6t8GG)zL?q=qfMK%wXQd=)>@Hh z6^fT8C|(dzQD85y7FYzUH3nF&a@kpA7ZxeV?tJ~evop)t*lFPnR)Y`w`aHkKniMM0+o^|;|oYh4AT6#n&Rl`VB}zZC;M0!6$JeuZSUW||26^)%K%_@bNFf0H5K75%5OXUh9)Qb$ zCxn3@LlGzdUIoB@(oh`4jyY!{3_zkv2zUVHm{Lx(Kj(6aAOq2w4}wr%82(NXf+fu#X){1OW9sz+sq_a^lA0m)Pz6 z{VHaF!I4gA?7mFbLH8@n%pINBvmPEJa0w;M)AMrg}4Ha12A)YSO+IGr0Y8jZ`y zV@*xX!k7Vi25fMoE(1CpccTL65bwYt^9JZ0w6RRk7!HR61_uXe%QQ4JMDjYFPUz|B zah*AH=0oymwOSX&3@|Y<0oSZWaJp$b{5LVKL>1iQPUvtnLrZTZG+>0Ad#{7daT`V- zJN?x&wWB4B5R?@(%Zv{X4=eIu@S(rI-<6-AA3p$+=&-|yx~))WI|~m6 zZ^4Cs^l;en8T?lF2`U{9)_v^Pp}O^O?#@oU&H|?Pzu>5OGl-%+V09u0d;By1TpM572IJf}{0cA;5aS`N#ltn5oa9&E6R9J^=(l#t8t+DEk8G<@whe%P`B zPBv{tm<=;VpNzcZ*GV^(6EmQfu+)(!u|ILif_4V~R<5EHc&Q2C_zTRyEEzDTw;JvWs zNpzmBYaeJ10HAXN!yO?3$O0`-xV$=2e z3hTdN{YL^2bq3sHZZ=WXW@9x^I$&N4V~^d51+WkIqQUei%ug?!ApuNjkYmzd{|^%j z@KChDFBf$1V~GGiofY6`(m4TkmZHuJcus&_7fD*!bqQ6b1^s1N(1g_OpmXIg_w zgvF0FHLZGp>a_S9G8+LtWOv?s5TtqbhW;_nH9vuIpf5r4BN0pl%9~ zStUSrwHC5#WEm7`Agd;g(;5V5OFRH83#`}uXmn470DG^JX@ULDydf~-SyiZN9U2!P z#~?s18qPIh$(Vvm$5%ci1aKq)z+ck@Q7K3Lnhy5`AAlV7EHqCM&oTIvJB#@w0_Zph zG=4+QK&93Pz|3p?Ws^fP`T%l6n)d-@n^KbkAo-U=|FVdsQ>8@^1h}bX`vj0xt(ndM z1JKN9o&%8L3E&ONfV>p)$}{_-zW=5M0W{3K9{|nM63GB&FrRH)O_NwkWCo;%jy8#3 z!_`tE?=!DqD@~4(ls2n*86fC2rpNcXVr~H8UheieI;A|-jYusro<-(yY2?l4s3xr1 ze3mg4`|k*Pm#P5ZLttdjLKchV-EbvDuRe-QJA4z;UlxI)av}3fhBO8+tu;*9##C8o z+^`xl4XJS4w2k_gW|{-1PMw!p6FfAzNz@<5N%_+Obruh+j7_Pr4IiRs1G*4C2A$w>&;67kt-rbe%D>g(&z zqqoJgy&t8MDJxd2c(0+M;ga2MA3|~${&Vd_xsog=jZGeEYirAvFJDgOlTuPrk`l>> zzQw3FSFKvLG9x2HvuV?&G({UXZqy`F{8%*hN^In1*66Dt($mvlz}+RNm!Gnhke68_ oAF9!0p&9=)6?VctZ_h~kA1N`SPDkIn6aWAK07*qoM6N<$f~WOF`~Uy| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e3e2f8df6c35e5911b98fc2ef0483f4e3844d68c GIT binary patch literal 3740 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 H{jmQ5jO9Z_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..128849a547491187175ebbf28e2c73f679a9d5ad GIT binary patch literal 8480 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 literal 0 HcmV?d00001 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 @@ + + + +