diff --git a/rx2/.gitignore b/rx2/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/rx2/.gitignore @@ -0,0 +1 @@ +/build diff --git a/rx2/build.gradle b/rx2/build.gradle new file mode 100644 index 00000000..efe6426a --- /dev/null +++ b/rx2/build.gradle @@ -0,0 +1,55 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "com.novoda:bintray-release:$bintrayVersion" + } +} + +apply plugin: 'com.android.library' +apply plugin: 'bintray-release' +apply plugin: 'jacoco-android' + +android { + compileSdkVersion COMPILE_SDK_VERSION + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + minSdkVersion MIN_SDK_VERSION + targetSdkVersion TARGET_SDK_VERSION + versionCode VERSION_CODE + versionName VERSION_NAME + } + buildTypes { + release { + minifyEnabled false + } + debug { + // output coverage with ./gradlew clean build createDebugCoverageReport + testCoverageEnabled true + } + } + lintOptions { + abortOnError false + } +} + +dependencies { + compile project(':thirtyinch') + compile 'io.reactivex.rxjava2:rxjava:2.0.3' + provided "com.android.support:support-annotations:$supportLibraryVersion" + + testCompile "junit:junit:$junitVersion" + testCompile "org.mockito:mockito-core:$mockitoVersion" +} + +publish { + userOrg = 'passsy' + groupId = 'net.grandcentrix.thirtyinch' + artifactId = 'thirtyinch-rx' + uploadName = 'ThirtyInch' + publishVersion = VERSION_NAME + //description = '' + website = 'https://github.com/grandcentrix/ThirtyInch' +} \ No newline at end of file diff --git a/rx2/src/main/AndroidManifest.xml b/rx2/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7f593b78 --- /dev/null +++ b/rx2/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandler.java b/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandler.java new file mode 100644 index 00000000..ec08524b --- /dev/null +++ b/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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 net.grandcentrix.thirtyinch.rx2; + +import net.grandcentrix.thirtyinch.TiLifecycleObserver; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; + +import android.support.annotation.NonNull; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + +public class RxTiPresenterDisposableHandler { + + private CompositeDisposable mPresenterDisposables = new CompositeDisposable(); + + private CompositeDisposable mUiDisposables; + + public RxTiPresenterDisposableHandler(final TiPresenter presenter) { + presenter.addLifecycleObserver(new TiLifecycleObserver() { + @Override + public void onChange(final TiPresenter.State state, + final boolean beforeLifecycleEvent) { + if (state == TiPresenter.State.VIEW_DETACHED && beforeLifecycleEvent) { + // dispose all UI disposable created in onAttachView(TiView) and added + // via manageViewDisposable(Disposable...) + if (mUiDisposables != null) { + mUiDisposables.dispose(); + mUiDisposables = null; + } + } + + if (state == TiPresenter.State.VIEW_ATTACHED && beforeLifecycleEvent) { + mUiDisposables = new CompositeDisposable(); + } + + if (state == TiPresenter.State.DESTROYED && beforeLifecycleEvent) { + mPresenterDisposables.dispose(); + mPresenterDisposables = null; + } + } + }); + + } + + /** + * Add your disposables here and they will automatically disposed when + * {@link TiPresenter#destroy()} gets called + * + * @throws IllegalStateException when the presenter has reached {@link net.grandcentrix.thirtyinch.TiPresenter.State#DESTROYED} + */ + public void manageDisposable(@NonNull final Disposable... disposables) { + if (mPresenterDisposables == null) { + throw new IllegalStateException("disposable handling doesn't work" + + " when the presenter has reached the DESTROYED state"); + } + + addDisposables(mPresenterDisposables, disposables); + } + + /** + * Add your disposables for View events to this method to get them automatically cleaned up + * in {@link TiPresenter#detachView()}. typically call this in {@link + * TiPresenter#attachView(TiView)} where you dispose to the UI events. + * + * @throws IllegalStateException when no view is attached + */ + public void manageViewDisposable(@NonNull final Disposable... disposables) { + if (mUiDisposables == null) { + throw new IllegalStateException("view disposable can't be handled" + + " when there is no view"); + } + + addDisposables(mUiDisposables, disposables); + } + + /** + * Adds all disposables to the given compositeDisposable if not already disposed + */ + private static void addDisposables(final CompositeDisposable compositeDisposable, + final Disposable... disposables) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < disposables.length; i++) { + final Disposable disposable = disposables[i]; + if (disposable.isDisposed()) { + continue; + } + + compositeDisposable.add(disposable); + } + } + +} diff --git a/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtils.java b/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtils.java new file mode 100644 index 00000000..92f76219 --- /dev/null +++ b/rx2/src/main/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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 net.grandcentrix.thirtyinch.rx2; + +import net.grandcentrix.thirtyinch.Removable; +import net.grandcentrix.thirtyinch.TiLifecycleObserver; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; + +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.disposables.Disposable; + +public class RxTiPresenterUtils { + + /** + * Observable of the view state. The View is ready to receive calls after calling {@link + * TiPresenter#attachView(TiView)} and before calling {@link TiPresenter#detachView()}. + */ + public static Observable isViewReady(final TiPresenter presenter) { + return Observable.create( + new ObservableOnSubscribe() { + @Override + public void subscribe(final ObservableEmitter emitter) + throws Exception { + if (!emitter.isDisposed()) { + emitter.onNext(presenter.getState() + == TiPresenter.State.VIEW_ATTACHED); + } + + final Removable removable = presenter + .addLifecycleObserver(new TiLifecycleObserver() { + @Override + public void onChange(final TiPresenter.State state, + final boolean beforeLifecycleEvent) { + if (!emitter.isDisposed()) { + emitter.onNext(state == + TiPresenter.State.VIEW_ATTACHED); + } + } + }); + + emitter.setDisposable(new Disposable() { + @Override + public void dispose() { + removable.remove(); + } + + @Override + public boolean isDisposed() { + return removable.isRemoved(); + } + }); + } + }) + .distinctUntilChanged(); + } + +} diff --git a/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandlerTest.java b/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandlerTest.java new file mode 100644 index 00000000..6a6be215 --- /dev/null +++ b/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterDisposableHandlerTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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 net.grandcentrix.thirtyinch.rx2; + +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import io.reactivex.observers.TestObserver; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +@RunWith(JUnit4.class) +public class RxTiPresenterDisposableHandlerTest { + + private RxTiPresenterDisposableHandler mDisposableHandler; + + private TiPresenter mPresenter; + + private TiView mView; + + @Before + public void setUp() throws Exception { + mPresenter = new TiPresenter() { + }; + mDisposableHandler = new RxTiPresenterDisposableHandler(mPresenter); + mView = mock(TiView.class); + } + + @Test + public void testManageDisposable_AfterDestroy_ShouldThrowIllegalState() throws Exception { + mPresenter.create(); + mPresenter.destroy(); + final TestObserver testObserver = new TestObserver<>(); + + try { + mDisposableHandler.manageDisposable(testObserver); + fail("no exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), containsString("DESTROYED")); + } + } + + @Test + public void testManageDisposable_WithAlreadyDisposedDisposable_ShouldDoNothing() + throws Exception { + final TestObserver testObserver = new TestObserver<>(); + testObserver.dispose(); + assertThat(testObserver.isDisposed(), is(true)); + + mDisposableHandler.manageDisposable(testObserver); + + assertThat(testObserver.isDisposed(), is(true)); + } + + @Test + public void testManageDisposable_WithDestroy_ShouldDispose() throws Exception { + mPresenter.create(); + final TestObserver testObserver = new TestObserver<>(); + + mDisposableHandler.manageDisposable(testObserver); + assertThat(testObserver.isDisposed(), is(false)); + + mPresenter.destroy(); + assertThat(testObserver.isDisposed(), is(true)); + } + + @Test + public void testManageVieDisposable_WithDetachSingleDispose_ShouldDispose() + throws Exception { + mPresenter.create(); + mPresenter.attachView(mView); + final TestObserver testObserver = new TestObserver<>(); + + mDisposableHandler.manageViewDisposable(testObserver); + assertThat(testObserver.isDisposed(), is(false)); + + mPresenter.detachView(); + assertThat(testObserver.isDisposed(), is(true)); + } + + @Test + public void testManageViewDisposable_manageAfterDetach_ShouldThrowIllegalStateException() + throws Exception { + mPresenter.create(); + mPresenter.attachView(mView); + mPresenter.detachView(); + + final TestObserver testObserver = new TestObserver<>(); + + try { + mDisposableHandler.manageViewDisposable(testObserver); + fail("no exception"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("when there is no view")); + } + } + + @Test + public void testManageViewDisposable_manageBeforeViewAttached_ShouldThrowIllegalStateException() + throws Exception { + mPresenter.create(); + final TestObserver testObserver = new TestObserver<>(); + + try { + mDisposableHandler.manageViewDisposable(testObserver); + fail("no exception"); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("when there is no view")); + } + } + + @Test + public void testManageViewDisposeable_WithOneAlreadyDisposed_ShouldNotAddToDisposable() + throws Exception { + mPresenter.create(); + mPresenter.attachView(mView); + final TestObserver firstTestObserver = new TestObserver<>(); + final TestObserver secondTestObserver = new TestObserver<>(); + secondTestObserver.dispose(); + + mDisposableHandler.manageViewDisposable(firstTestObserver, secondTestObserver); + + assertThat(firstTestObserver.isDisposed(), is(false)); + assertThat(secondTestObserver.isDisposed(), is(true)); + } + + @Test + public void testManagerViewDisposable_WithDetach_ShouldDispose() throws Exception { + mPresenter.create(); + mPresenter.attachView(mView); + final TestObserver firstTestObserver = new TestObserver<>(); + final TestObserver secondTestObserver = new TestObserver<>(); + final TestObserver thirdTestObserver = new TestObserver<>(); + + mDisposableHandler + .manageViewDisposable(firstTestObserver, secondTestObserver, thirdTestObserver); + assertThat(firstTestObserver.isDisposed(), equalTo(false)); + assertThat(secondTestObserver.isDisposed(), equalTo(false)); + assertThat(thirdTestObserver.isDisposed(), equalTo(false)); + + mPresenter.detachView(); + assertThat(firstTestObserver.isDisposed(), equalTo(true)); + assertThat(secondTestObserver.isDisposed(), equalTo(true)); + assertThat(thirdTestObserver.isDisposed(), equalTo(true)); + } +} \ No newline at end of file diff --git a/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtilsTest.java b/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtilsTest.java new file mode 100644 index 00000000..85b8b01f --- /dev/null +++ b/rx2/src/test/java/net/grandcentrix/thirtyinch/rx2/RxTiPresenterUtilsTest.java @@ -0,0 +1,64 @@ +package net.grandcentrix.thirtyinch.rx2; + +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import io.reactivex.observers.TestObserver; + +import static org.mockito.Mockito.mock; + +public class RxTiPresenterUtilsTest { + + private TiPresenter mPresenter; + + private TiView mView; + + @Before + public void setUp() throws Exception { + mView = mock(TiView.class); + mPresenter = new TiPresenter() { + }; + } + + @After + public void tearDown() throws Exception { + mPresenter = null; + mView = null; + } + + @Test + public void testIsViewReady_AttachView_ShouldCallValueFalseTrue() throws Exception { + mPresenter.create(); + + final TestObserver test = RxTiPresenterUtils.isViewReady(mPresenter).test(); + + mPresenter.attachView(mView); + test.assertValues(false, true); + } + + @Test + public void testIsViewReady_BeforeAttachView_ShouldCallValueFalse() throws Exception { + mPresenter.create(); + + final TestObserver test = RxTiPresenterUtils.isViewReady(mPresenter).test(); + + test.assertValue(false); + } + + @Test + public void testIsViewReady_DisposeBeforeAttachView_ShouldRemoveCallback() throws Exception { + mPresenter.create(); + + final TestObserver test = RxTiPresenterUtils.isViewReady(mPresenter).test(); + + test.assertValue(false); + test.dispose(); + test.isDisposed(); + mPresenter.attachView(mView); + test.assertValue(false); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 87e0b83e..8588ceb2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':thirtyinch', ':plugin', ':sample', ':rx', ':test', ':plugin-test' +include ':thirtyinch', ':plugin', ':sample', ':rx', ':test', ':plugin-test', ':rx2'