From 497016f5d42f76580e88afa4f6c73599aa92af36 Mon Sep 17 00:00:00 2001 From: mtgriego Date: Thu, 17 Oct 2024 15:28:23 -0700 Subject: [PATCH] [NoTicket] Delete BaseActivity and ActivityViewModel (#2153) * initial work to get RX2 updated for discoveryviewmodel (WIP) * WIP remove more RX2 implementation, remove RXView stuff, make discovery work * WIP adjust tests for discoveryViewmodel * update user to always have a value * fix a bunch of tests, one to go * revert changes that broke a couple tests * fix the last test! * remove unused variables, fix logged out container logic for being shown * try adding import to fix circle CI * fix some more RX1 stuff, remove BaseActivity and ActivityViewModel * remove unused import * remove some more unused classes ;) * remove some more RX1 * remove more unneeded RX1 * fix internal tools issue * re-delete internal tools after merge * re-remove some internal tools from merge * lint * more lint * more lint * STOP IT LINT * lint, my mortal enemy * update segment tests for new default properties * added a couple disposable clearing for facebook activity * clear more disposables --- .../ui/activities/InternalToolsActivity.kt | 10 +- .../viewmodels/InternalToolsViewModel.java | 13 - .../kickstarter/libs/ActivityViewModel.java | 117 ------- .../libs/ActivityViewModelManager.java | 106 ------- .../com/kickstarter/libs/ApiPaginator.java | 267 ---------------- .../com/kickstarter/libs/BaseActivity.java | 291 ------------------ .../com/kickstarter/libs/CurrentConfig.java | 88 ------ .../com/kickstarter/libs/CurrentConfig.kt | 96 ++++++ .../kickstarter/libs/CurrentConfigType.java | 16 - .../com/kickstarter/libs/CurrentConfigType.kt | 15 + .../com/kickstarter/libs/SwipeRefresher.java | 73 ----- .../libs/loadmore/ApolloPaginate.kt | 259 ---------------- .../qualifiers/RequiresActivityViewModel.java | 13 - .../libs/rx/transformers/Transformers.java | 53 +--- .../libs/utils/ApplicationUtils.java | 74 ----- .../libs/utils/ApplicationUtils.kt | 70 +++++ .../kickstarter/mock/MockCurrentConfig.java | 5 +- .../mock/factories/ApiExceptionFactory.java | 3 +- .../services/ConnectivityReceiver.kt | 2 +- .../FacebookConfirmationActivity.kt | 36 ++- .../com/kickstarter/ui/toolbars/KSToolbar.kt | 19 +- .../kickstarter/ui/toolbars/ProjectToolbar.kt | 9 +- .../viewholders/AddCardViewHolderViewModel.kt | 20 +- .../ui/viewholders/AddOnViewHolder.kt | 65 ++-- .../ui/viewholders/RewardAddCardViewHolder.kt | 17 +- .../ThanksCategoryHolderViewModel.kt | 15 +- .../viewholders/ThanksCategoryViewHolder.kt | 16 +- .../viewmodels/AddOnViewHolderViewModel.kt | 70 +++-- .../FacebookConfirmationViewModel.java | 143 --------- .../FacebookConfirmationViewModel.kt | 167 ++++++++++ .../viewmodels/MessageHolderViewModel.kt | 3 - .../transformers/TakeWhenTransformerTest.java | 74 ----- .../transformers/TakeWhenTransformerTest.kt | 79 +++++ .../AddCardViewHolderViewModelTest.kt | 24 +- .../AddOnViewHolderViewModelTest.kt | 29 +- .../FacebookConfimationViewModelTest.java | 100 ------ .../FacebookConfimationViewModelTest.kt | 106 +++++++ .../ThanksCategoryHolderViewModelTest.java | 43 --- .../ThanksCategoryHolderViewModelTest.kt | 39 +++ 39 files changed, 765 insertions(+), 1880 deletions(-) delete mode 100644 app/src/internal/java/com/kickstarter/viewmodels/InternalToolsViewModel.java delete mode 100644 app/src/main/java/com/kickstarter/libs/ActivityViewModel.java delete mode 100644 app/src/main/java/com/kickstarter/libs/ActivityViewModelManager.java delete mode 100644 app/src/main/java/com/kickstarter/libs/ApiPaginator.java delete mode 100644 app/src/main/java/com/kickstarter/libs/BaseActivity.java delete mode 100644 app/src/main/java/com/kickstarter/libs/CurrentConfig.java create mode 100644 app/src/main/java/com/kickstarter/libs/CurrentConfig.kt delete mode 100644 app/src/main/java/com/kickstarter/libs/CurrentConfigType.java create mode 100644 app/src/main/java/com/kickstarter/libs/CurrentConfigType.kt delete mode 100644 app/src/main/java/com/kickstarter/libs/SwipeRefresher.java delete mode 100644 app/src/main/java/com/kickstarter/libs/loadmore/ApolloPaginate.kt delete mode 100644 app/src/main/java/com/kickstarter/libs/qualifiers/RequiresActivityViewModel.java delete mode 100644 app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.java create mode 100644 app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.kt delete mode 100644 app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java create mode 100644 app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.kt delete mode 100644 app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.java create mode 100644 app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.kt delete mode 100644 app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.java create mode 100644 app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.kt delete mode 100644 app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.java create mode 100644 app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.kt diff --git a/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt b/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt index 7ddf26dd93..12a6d78fe1 100644 --- a/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt +++ b/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.view.View import android.webkit.URLUtil import android.widget.EditText +import androidx.activity.ComponentActivity import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.work.BackoffPolicy @@ -22,25 +23,20 @@ import com.kickstarter.KSApplication import com.kickstarter.R import com.kickstarter.databinding.InternalToolsLayoutBinding import com.kickstarter.libs.ApiEndpoint -import com.kickstarter.libs.BaseActivity import com.kickstarter.libs.Build import com.kickstarter.libs.FirebaseHelper import com.kickstarter.libs.Logout import com.kickstarter.libs.preferences.StringPreferenceType import com.kickstarter.libs.qualifiers.ApiEndpointPreference -import com.kickstarter.libs.qualifiers.RequiresActivityViewModel import com.kickstarter.libs.utils.Secrets -import com.kickstarter.libs.utils.TransitionUtils import com.kickstarter.libs.utils.ViewUtils import com.kickstarter.libs.utils.WorkUtils import com.kickstarter.services.firebase.ResetDeviceIdWorker -import com.kickstarter.viewmodels.InternalToolsViewModel import org.joda.time.format.DateTimeFormat import java.util.concurrent.TimeUnit import javax.inject.Inject -@RequiresActivityViewModel(InternalToolsViewModel::class) -class InternalToolsActivity : BaseActivity() { +class InternalToolsActivity : ComponentActivity() { @JvmField @Inject @ApiEndpointPreference @@ -239,6 +235,4 @@ class InternalToolsActivity : BaseActivity() { } ProcessPhoenix.triggerRebirth(this) } - - override fun exitTransition() = TransitionUtils.slideInFromLeft() } diff --git a/app/src/internal/java/com/kickstarter/viewmodels/InternalToolsViewModel.java b/app/src/internal/java/com/kickstarter/viewmodels/InternalToolsViewModel.java deleted file mode 100644 index c37ae9beb9..0000000000 --- a/app/src/internal/java/com/kickstarter/viewmodels/InternalToolsViewModel.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kickstarter.viewmodels; - -import com.kickstarter.libs.ActivityViewModel; -import com.kickstarter.libs.Environment; -import com.kickstarter.ui.activities.InternalToolsActivity; - -import androidx.annotation.NonNull; - -public class InternalToolsViewModel extends ActivityViewModel { - public InternalToolsViewModel(final @NonNull Environment environment) { - super(environment); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/ActivityViewModel.java b/app/src/main/java/com/kickstarter/libs/ActivityViewModel.java deleted file mode 100644 index b7ff644580..0000000000 --- a/app/src/main/java/com/kickstarter/libs/ActivityViewModel.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.kickstarter.libs; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Pair; - -import com.kickstarter.ui.data.ActivityResult; -import com.trello.rxlifecycle.ActivityEvent; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import rx.Observable; -import rx.subjects.PublishSubject; -import rx.subscriptions.CompositeSubscription; -import timber.log.Timber; - -public class ActivityViewModel { - - private final PublishSubject viewChange = PublishSubject.create(); - private final Observable view = this.viewChange.filter(v -> v != null); - private final CompositeSubscription subscriptions = new CompositeSubscription(); - - private final PublishSubject activityResult = PublishSubject.create(); - - private final PublishSubject intent = PublishSubject.create(); - protected final AnalyticEvents analyticEvents; - - public ActivityViewModel(final @NonNull Environment environment) { - this.analyticEvents = environment.analytics(); - } - - /** - * Takes activity result data from the activity. - */ - public void activityResult(final @NonNull ActivityResult activityResult) { - this.activityResult.onNext(activityResult); - } - - /** - * Takes intent data from the view. - */ - public void intent(final @NonNull Intent intent) { - this.intent.onNext(intent); - } - - @CallSuper - protected void onCreate(final @NonNull Context context, final @Nullable Bundle savedInstanceState) { - Timber.d("onCreate %s", this.toString()); - dropView(); - } - - @CallSuper - protected void onResume(final @NonNull ViewType view) { - Timber.d("onResume %s", this.toString()); - onTakeView(view); - } - - @CallSuper - protected void onPause() { - Timber.d("onPause %s", this.toString()); - dropView(); - } - - @CallSuper - protected void onDestroy() { - Timber.d("onDestroy %s", this.toString()); - - this.subscriptions.clear(); - this.viewChange.onCompleted(); - } - - private void onTakeView(final @NonNull ViewType view) { - Timber.d("onTakeView %s %s", this.toString(), view.toString()); - this.viewChange.onNext(view); - } - - private void dropView() { - Timber.d("dropView %s", this.toString()); - this.viewChange.onNext(null); - } - - protected @NonNull Observable activityResult() { - return this.activityResult; - } - - protected @NonNull Observable intent() { - return this.intent; - } - - /** - * By composing this transformer with an observable you guarantee that every observable in your view model - * will be properly completed when the view model completes. - * - * It is required that *every* observable in a view model do `.compose(bindToLifecycle())` before calling - * `subscribe`. - */ - public @NonNull Observable.Transformer bindToLifecycle() { - return source -> source.takeUntil( - this.view.switchMap(v -> v.lifecycle().map(e -> Pair.create(v, e))) - .filter(ve -> isFinished(ve.first, ve.second)) - ); - } - - /** - * Determines from a view and lifecycle event if the view's life is over. - */ - private boolean isFinished(final @NonNull ViewType view, final @NonNull ActivityEvent event) { - - if (view instanceof BaseActivity) { - return event == ActivityEvent.DESTROY && ((BaseActivity) view).isFinishing(); - } - - return event == ActivityEvent.DESTROY; - } -} diff --git a/app/src/main/java/com/kickstarter/libs/ActivityViewModelManager.java b/app/src/main/java/com/kickstarter/libs/ActivityViewModelManager.java deleted file mode 100644 index f156684ae5..0000000000 --- a/app/src/main/java/com/kickstarter/libs/ActivityViewModelManager.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.kickstarter.libs; - -import android.content.Context; -import android.os.Bundle; - -import com.kickstarter.KSApplication; -import com.kickstarter.libs.utils.extensions.BundleExtKt; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.UUID; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class ActivityViewModelManager { - private static final String VIEW_MODEL_ID_KEY = "view_model_id"; - private static final String VIEW_MODEL_STATE_KEY = "view_model_state"; - - private static final ActivityViewModelManager instance = new ActivityViewModelManager(); - private final Map viewModels = new HashMap<>(); - - public static @NonNull ActivityViewModelManager getInstance() { - return instance; - } - - @SuppressWarnings("unchecked") - public T fetch(final @NonNull Context context, final @NonNull Class viewModelClass, - final @Nullable Bundle savedInstanceState) { - final String id = fetchId(savedInstanceState); - ActivityViewModel activityViewModel = this.viewModels.get(id); - - if (activityViewModel == null) { - activityViewModel = create(context, viewModelClass, savedInstanceState, id); - } - - return (T) activityViewModel; - } - - public void destroy(final @NonNull ActivityViewModel activityViewModel) { - activityViewModel.onDestroy(); - - final Iterator> iterator = this.viewModels.entrySet().iterator(); - while (iterator.hasNext()) { - final Map.Entry entry = iterator.next(); - if (activityViewModel.equals(entry.getValue())) { - iterator.remove(); - } - } - } - - public void save(final @NonNull ActivityViewModel activityViewModel, final @NonNull Bundle envelope) { - envelope.putString(VIEW_MODEL_ID_KEY, findIdForViewModel(activityViewModel)); - - final Bundle state = new Bundle(); - envelope.putBundle(VIEW_MODEL_STATE_KEY, state); - } - - private ActivityViewModel create(final @NonNull Context context, final @NonNull Class viewModelClass, - final @Nullable Bundle savedInstanceState, final @NonNull String id) { - - final KSApplication application = (KSApplication) context.getApplicationContext(); - final Environment environment = application.component().environment(); - final ActivityViewModel activityViewModel; - - try { - final Constructor constructor = viewModelClass.getConstructor(Environment.class); - activityViewModel = (ActivityViewModel) constructor.newInstance(environment); - - // Need to catch these exceptions separately, otherwise the compiler turns them into `ReflectiveOperationException`. - // That exception is only available in API19+ - } catch (IllegalAccessException exception) { - throw new RuntimeException(exception); - } catch (InvocationTargetException exception) { - throw new RuntimeException(exception); - } catch (InstantiationException exception) { - throw new RuntimeException(exception); - } catch (NoSuchMethodException exception) { - throw new RuntimeException(exception); - } - - this.viewModels.put(id, activityViewModel); - activityViewModel.onCreate(context, BundleExtKt.maybeGetBundle(savedInstanceState, VIEW_MODEL_STATE_KEY)); - - return activityViewModel; - } - - private String fetchId(final @Nullable Bundle savedInstanceState) { - return savedInstanceState != null ? - savedInstanceState.getString(VIEW_MODEL_ID_KEY) : - UUID.randomUUID().toString(); - } - - private String findIdForViewModel(final @NonNull ActivityViewModel activityViewModel) { - for (final Map.Entry entry : this.viewModels.entrySet()) { - if (activityViewModel.equals(entry.getValue())) { - return entry.getKey(); - } - } - - throw new RuntimeException("Cannot find view model in map!"); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/ApiPaginator.java b/app/src/main/java/com/kickstarter/libs/ApiPaginator.java deleted file mode 100644 index 0c55ad9a5f..0000000000 --- a/app/src/main/java/com/kickstarter/libs/ApiPaginator.java +++ /dev/null @@ -1,267 +0,0 @@ -package com.kickstarter.libs; - -import android.util.Pair; - -import com.kickstarter.libs.rx.transformers.Transformers; -import com.kickstarter.libs.utils.ListUtils; -import com.kickstarter.services.ApiClientType; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import rx.Observable; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.subjects.PublishSubject; - -/** - * An object to facilitate loading pages of data from the API. - * - * @param The type of data returned from the array, e.g. `Project`, `Activity`, etc. - * @param The type of envelope the API returns for a list of data, e.g. `DiscoverEnvelope`. - * @param The type of params that {@link ApiClientType} can use to make a request. Many times this can just be `Void`. - */ -public final class ApiPaginator { - private final @NonNull Observable nextPage; - private final @NonNull Observable startOverWith; - private final @NonNull Func1> envelopeToListOfData; - private final @NonNull Func1> loadWithParams; - private final @NonNull Func1> loadWithPaginationPath; - private final @NonNull Func1 envelopeToMoreUrl; - private final @NonNull Func1, List> pageTransformation; - private final boolean clearWhenStartingOver; - private final @NonNull Func2, List, List> concater; - private final boolean distinctUntilChanged; - - private final @NonNull PublishSubject _morePath = PublishSubject.create(); - private final @NonNull PublishSubject _isFetching = PublishSubject.create(); - - // Outputs - public @NonNull Observable> paginatedData() { - return this.paginatedData; - } - private final @NonNull Observable> paginatedData; - public @NonNull Observable isFetching() { - return this.isFetching; - } - private final @NonNull Observable isFetching = this._isFetching; - public @NonNull Observable loadingPage() { - return this.loadingPage; - } - private final @NonNull Observable loadingPage; - - private ApiPaginator( - final @NonNull Observable nextPage, - final @NonNull Observable startOverWith, - final @NonNull Func1> envelopeToListOfData, - final @NonNull Func1> loadWithParams, - final @NonNull Func1> loadWithPaginationPath, - final @NonNull Func1 envelopeToMoreUrl, - final @NonNull Func1, List> pageTransformation, - final boolean clearWhenStartingOver, - final @NonNull Func2, List, List> concater, - final boolean distinctUntilChanged - ) { - this.nextPage = nextPage; - this.startOverWith = startOverWith; - this.envelopeToListOfData = envelopeToListOfData; - this.loadWithParams = loadWithParams; - this.envelopeToMoreUrl = envelopeToMoreUrl; - this.pageTransformation = pageTransformation; - this.loadWithPaginationPath = loadWithPaginationPath; - this.clearWhenStartingOver = clearWhenStartingOver; - this.concater = concater; - this.distinctUntilChanged = distinctUntilChanged; - - this.paginatedData = this.startOverWith.switchMap(this::dataWithPagination); - this.loadingPage = this.startOverWith.switchMap(__ -> nextPage.scan(1, (accum, ___) -> accum + 1)); - } - - public final static class Builder { - private Observable nextPage; - private Observable startOverWith; - private Func1> envelopeToListOfData; - private Func1> loadWithParams; - private Func1> loadWithPaginationPath; - private Func1 envelopeToMoreUrl; - private Func1, List> pageTransformation; - private boolean clearWhenStartingOver; - private Func2, List, List> concater = ListUtils::concat; - private boolean distinctUntilChanged; - - /** - * [Required] An observable that emits whenever a new page of data should be loaded. - */ - public @NonNull Builder nextPage(final @NonNull Observable nextPage) { - this.nextPage = nextPage; - return this; - } - - /** - * [Optional] An observable that emits when a fresh first page should be loaded. - */ - public @NonNull Builder startOverWith(final @NonNull Observable startOverWith) { - this.startOverWith = startOverWith; - return this; - } - - /** - * [Required] A function that takes an `Envelope` instance and returns the list of data embedded in it. - */ - public @NonNull Builder envelopeToListOfData(final @NonNull Func1> envelopeToListOfData) { - this.envelopeToListOfData = envelopeToListOfData; - return this; - } - - /** - * [Required] A function to extract the more URL from an API response envelope. - */ - public @NonNull Builder envelopeToMoreUrl(final @NonNull Func1 envelopeToMoreUrl) { - this.envelopeToMoreUrl = envelopeToMoreUrl; - return this; - } - - /** - * [Required] A function that makes an API request with a pagination URL. - */ - public @NonNull Builder loadWithPaginationPath(final @NonNull Func1> loadWithPaginationPath) { - this.loadWithPaginationPath = loadWithPaginationPath; - return this; - } - - /** - * [Required] A function that takes a `Params` and performs the associated network request - * and returns an `Observable` - */ - public @NonNull Builder loadWithParams(final @NonNull Func1> loadWithParams) { - this.loadWithParams = loadWithParams; - return this; - } - - /** - * [Optional] Function to transform every page of data that is loaded. - */ - public @NonNull Builder pageTransformation(final @NonNull Func1, List> pageTransformation) { - this.pageTransformation = pageTransformation; - return this; - } - - /** - * [Optional] Determines if the list of loaded data is cleared when starting over from the first page. - */ - public @NonNull Builder clearWhenStartingOver(final boolean clearWhenStartingOver) { - this.clearWhenStartingOver = clearWhenStartingOver; - return this; - } - - /** - * [Optional] Determines how two lists are concatenated together while paginating. A regular `ListUtils::concat` is probably - * sufficient, but sometimes you may want `ListUtils::concatDistinct` - */ - public @NonNull Builder concater(final @NonNull Func2, List, List> concater) { - this.concater = concater; - return this; - } - - /** - * [Optional] Determines if the list of loaded data is should be distinct until changed. - */ - public @NonNull Builder distinctUntilChanged(final boolean distinctUntilChanged) { - this.distinctUntilChanged = distinctUntilChanged; - return this; - } - - public @NonNull ApiPaginator build() throws RuntimeException { - // Early error when required field is not set - if (this.nextPage == null) { - throw new RuntimeException("`nextPage` is required"); - } - if (this.envelopeToListOfData == null) { - throw new RuntimeException("`envelopeToListOfData` is required"); - } - if (this.loadWithParams == null) { - throw new RuntimeException("`loadWithParams` is required"); - } - if (this.loadWithPaginationPath == null) { - throw new RuntimeException("`loadWithPaginationPath` is required"); - } - if (this.envelopeToMoreUrl == null) { - throw new RuntimeException("`envelopeToMoreUrl` is required"); - } - - // Default params for optional fields - if (this.startOverWith == null) { - this.startOverWith = Observable.just(null); - } - if (this.pageTransformation == null) { - this.pageTransformation = x -> x; - } - if (this.concater == null) { - this.concater = ListUtils::concat; - } - - return new ApiPaginator<>(this.nextPage, this.startOverWith, this.envelopeToListOfData, this.loadWithParams, - this.loadWithPaginationPath, this.envelopeToMoreUrl, this.pageTransformation, this.clearWhenStartingOver, this.concater, - this.distinctUntilChanged); - } - } - - public @NonNull static Builder builder() { - return new Builder<>(); - } - - /** - * Returns an observable that emits the accumulated list of paginated data each time a new page is loaded. - */ - private @NonNull Observable> dataWithPagination(final @NonNull Params firstPageParams) { - final Observable> data = paramsAndMoreUrlWithPagination(firstPageParams) - .concatMap(this::fetchData) - .takeUntil(List::isEmpty); - - final Observable> paginatedData = this.clearWhenStartingOver - ? data.scan(new ArrayList<>(), this.concater) - : data.scan(this.concater); - - return this.distinctUntilChanged ? paginatedData.distinctUntilChanged() : paginatedData; - } - - /** - * Returns an observable that emits the params for the next page of data *or* the more URL for the next page. - */ - private @NonNull Observable> paramsAndMoreUrlWithPagination(final @NonNull Params firstPageParams) { - - return this._morePath - .map(path -> new Pair(null, path)) - .compose(Transformers.takeWhen(this.nextPage)) - .startWith(new Pair<>(firstPageParams, null)); - } - - private @NonNull Observable> fetchData(final @NonNull Pair paginatingData) { - - return (paginatingData.second != null - ? this.loadWithPaginationPath.call(paginatingData.second) - : this.loadWithParams.call(paginatingData.first)) - .retry(2) - .compose(Transformers.neverError()) - .doOnNext(this::keepMorePath) - .map(this.envelopeToListOfData) - .map(this.pageTransformation) - .takeUntil(data -> data.isEmpty()) - .doOnSubscribe(() -> this._isFetching.onNext(true)) - .doAfterTerminate(() -> this._isFetching.onNext(false)); - } - - private void keepMorePath(final @NonNull Envelope envelope) { - try { - final URL url = new URL(this.envelopeToMoreUrl.call(envelope)); - this._morePath.onNext(pathAndQueryFromURL(url)); - } catch (MalformedURLException ignored) {} - } - - private @NonNull String pathAndQueryFromURL(final @NonNull URL url) { - return url.getPath() + "?" + url.getQuery(); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/BaseActivity.java b/app/src/main/java/com/kickstarter/libs/BaseActivity.java deleted file mode 100644 index f69c8095bf..0000000000 --- a/app/src/main/java/com/kickstarter/libs/BaseActivity.java +++ /dev/null @@ -1,291 +0,0 @@ -package com.kickstarter.libs; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Pair; - -import com.google.firebase.crashlytics.FirebaseCrashlytics; -import com.kickstarter.ApplicationComponent; -import com.kickstarter.KSApplication; -import com.kickstarter.R; -import com.kickstarter.libs.qualifiers.RequiresActivityViewModel; -import com.kickstarter.libs.utils.extensions.BundleExtKt; -import com.kickstarter.services.ConnectivityReceiver; -import com.kickstarter.ui.data.ActivityResult; -import com.kickstarter.ui.extensions.ActivityExtKt; -import com.trello.rxlifecycle.ActivityEvent; -import com.trello.rxlifecycle.RxLifecycle; -import com.trello.rxlifecycle.components.ActivityLifecycleProvider; - -import androidx.annotation.AnimRes; -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AppCompatActivity; -import rx.Observable; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.subjects.BehaviorSubject; -import rx.subjects.PublishSubject; -import rx.subscriptions.CompositeSubscription; -import timber.log.Timber; - -public abstract class BaseActivity extends AppCompatActivity implements ActivityLifecycleProvider, - ActivityLifecycleType, ConnectivityReceiver.ConnectivityReceiverListener { - - private final PublishSubject back = PublishSubject.create(); - private final BehaviorSubject lifecycle = BehaviorSubject.create(); - private static final String VIEW_MODEL_KEY = "viewModel"; - private final CompositeSubscription subscriptions = new CompositeSubscription(); - private final ConnectivityReceiver connectivityReceiver = new ConnectivityReceiver(this, this); - protected ViewModelType viewModel; - - /** - * Get viewModel. - */ - public ViewModelType viewModel() { - return this.viewModel; - } - - /** - * Returns an observable of the activity's lifecycle events. - */ - public final Observable lifecycle() { - return this.lifecycle.asObservable(); - } - - /** - * Completes an observable when an {@link ActivityEvent} occurs in the activity's lifecycle. - */ - public final Observable.Transformer bindUntilEvent(final ActivityEvent event) { - return RxLifecycle.bindUntilActivityEvent(this.lifecycle, event); - } - - /** - * Completes an observable when the lifecycle event opposing the current lifecyle event is emitted. - * For example, if a subscription is made during {@link ActivityEvent#CREATE}, the observable will be completed - * in {@link ActivityEvent#DESTROY}. - */ - public final Observable.Transformer bindToLifecycle() { - return RxLifecycle.bindActivity(this.lifecycle); - } - - /** - * Sends activity result data to the view model. - */ - @CallSuper - @Override - protected void onActivityResult(final int requestCode, final int resultCode, final @Nullable Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - this.viewModel.activityResult(ActivityResult.create(requestCode, resultCode, intent)); - } - - @CallSuper - @Override - protected void onCreate(final @Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Timber.d("onCreate %s", this.toString()); - - this.lifecycle.onNext(ActivityEvent.CREATE); - - assignViewModel(savedInstanceState); - - this.viewModel.intent(getIntent()); - - super.getLifecycle().addObserver(this.connectivityReceiver); - } - - /** - * Called when an activity is set to `singleTop` and it is relaunched while at the top of the activity stack. - */ - @CallSuper - @Override - protected void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - this.viewModel.intent(intent); - } - - @CallSuper - @Override - protected void onStart() { - super.onStart(); - Timber.d("onStart %s", this.toString()); - this.lifecycle.onNext(ActivityEvent.START); - - this.back - .compose(bindUntilEvent(ActivityEvent.STOP)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(__ -> goBack(), FirebaseCrashlytics.getInstance()::recordException); - } - - @CallSuper - @Override - protected void onResume() { - super.onResume(); - Timber.d("onResume %s", this.toString()); - this.lifecycle.onNext(ActivityEvent.RESUME); - - assignViewModel(null); - if (this.viewModel != null) { - this.viewModel.onResume(this); - } - } - - @CallSuper - @Override - protected void onPause() { - this.lifecycle.onNext(ActivityEvent.PAUSE); - super.onPause(); - Timber.d("onPause %s", this.toString()); - - if (this.viewModel != null) { - this.viewModel.onPause(); - } - } - - @CallSuper - @Override - protected void onStop() { - this.lifecycle.onNext(ActivityEvent.STOP); - super.onStop(); - Timber.d("onStop %s", this.toString()); - } - - @CallSuper - @Override - protected void onDestroy() { - this.lifecycle.onNext(ActivityEvent.DESTROY); - super.onDestroy(); - Timber.d("onDestroy %s", this.toString()); - - this.subscriptions.clear(); - - if (isFinishing()) { - if (this.viewModel != null) { - ActivityViewModelManager.getInstance().destroy(this.viewModel); - this.viewModel = null; - } - } - } - - /** - * @deprecated Use {@link #back()} instead. - * - * In rare situations, onBackPressed can be triggered after {@link #onSaveInstanceState(Bundle)} has been called. - * This causes an {@link IllegalStateException} in the fragment manager's `checkStateLoss` method, because the - * UI state has changed after being saved. The sequence of events might look like this: - * - * onSaveInstanceState -> onStop -> onBackPressed - * - * To avoid that situation, we need to ignore calls to `onBackPressed` after the activity has been saved. Since - * the activity is stopped after `onSaveInstanceState` is called, we can create an observable of back events, - * and a subscription that calls super.onBackPressed() only when the activity has not been stopped. - */ - @CallSuper - @Override - @Deprecated - public void onBackPressed() { - back(); - } - - /** This is called when a user loses data or Wi-Fi connection. - * When the user loses connection we will show our network error Snackbar. - * We're also using (findViewById(android.R.id.content) to get the root view of our activity - * so whatever Activity the user navigates to while disconnected the error will display. - */ - @Override - public void onNetworkConnectionChanged(final boolean isConnected) { - if (!isConnected) { - ActivityExtKt.showSnackbar(findViewById(android.R.id.content), getString(R.string.Youre_offline)); - } - } - /** - * Call when the user wants triggers a back event, e.g. clicking back in a toolbar or pressing the device back button. - */ - public void back() { - this.back.onNext(null); - } - - /** - * Override in subclasses for custom exit transitions. First item in pair is the enter animation, - * second item in pair is the exit animation. - */ - protected @Nullable Pair exitTransition() { - return null; - } - - @CallSuper - @Override - protected void onSaveInstanceState(final @NonNull Bundle outState) { - super.onSaveInstanceState(outState); - Timber.d("onSaveInstanceState %s", this.toString()); - - final Bundle viewModelEnvelope = new Bundle(); - if (this.viewModel != null) { - ActivityViewModelManager.getInstance().save(this.viewModel, viewModelEnvelope); - } - - outState.putBundle(VIEW_MODEL_KEY, viewModelEnvelope); - } - - protected final void startActivityWithTransition(final @NonNull Intent intent, final @AnimRes int enterAnim, - final @AnimRes int exitAnim) { - startActivity(intent); - overridePendingTransition(enterAnim, exitAnim); - } - - /** - * Returns the {@link KSApplication} instance. - */ - protected @NonNull KSApplication application() { - return (KSApplication) getApplication(); - } - - /** - * Convenience method to return a Dagger component. - */ - protected @NonNull ApplicationComponent component() { - return application().component(); - } - - /** - * Returns the application's {@link Environment}. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public @NonNull Environment environment() { - return component().environment(); - } - - /** - * @deprecated Use {@link #bindToLifecycle()} or {@link #bindUntilEvent(ActivityEvent)} instead. - */ - @Deprecated - protected final void addSubscription(final @NonNull Subscription subscription) { - this.subscriptions.add(subscription); - } - - /** - * Triggers a back press with an optional transition. - */ - private void goBack() { - super.onBackPressed(); - - final Pair exitTransitions = exitTransition(); - if (exitTransitions != null) { - overridePendingTransition(exitTransitions.first, exitTransitions.second); - } - } - - private void assignViewModel(final @Nullable Bundle viewModelEnvelope) { - if (this.viewModel == null) { - final RequiresActivityViewModel annotation = getClass().getAnnotation(RequiresActivityViewModel.class); - final Class viewModelClass = annotation == null ? null : (Class) annotation.value(); - if (viewModelClass != null) { - this.viewModel = ActivityViewModelManager.getInstance().fetch(this, - viewModelClass, - BundleExtKt.maybeGetBundle(viewModelEnvelope, VIEW_MODEL_KEY)); - } - } - } -} diff --git a/app/src/main/java/com/kickstarter/libs/CurrentConfig.java b/app/src/main/java/com/kickstarter/libs/CurrentConfig.java deleted file mode 100644 index ba30b5f920..0000000000 --- a/app/src/main/java/com/kickstarter/libs/CurrentConfig.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.kickstarter.libs; - -import android.content.res.AssetManager; - -import com.google.gson.Gson; -import com.kickstarter.libs.preferences.StringPreferenceType; -import com.kickstarter.libs.rx.transformers.Transformers; -import com.kickstarter.libs.utils.extensions.AnyExtKt; - -import java.io.IOException; -import java.io.InputStream; - -import androidx.annotation.NonNull; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.subjects.BehaviorSubject; -import timber.log.Timber; - -public final class CurrentConfig implements CurrentConfigType { - private final static String ASSET_PATH = "json/server-config.json"; - - private final BehaviorSubject config = BehaviorSubject.create(); - - public CurrentConfig(final @NonNull AssetManager assetManager, - final @NonNull Gson gson, - final @NonNull StringPreferenceType configPreference) { - - // Loads config from disk - final Observable diskConfig = Observable.just(ASSET_PATH) - .map(path -> configJSONString(path, assetManager)) - .map(json -> gson.fromJson(json, Config.class)) - .filter(AnyExtKt::isNotNull) - .compose(Transformers.neverError()) - .subscribeOn(AndroidSchedulers.mainThread()); - - // Loads config from string preference - final Observable prefConfig = Observable.just(configPreference) - .map(StringPreferenceType::get) - .map(json -> gson.fromJson(json, Config.class)) - .filter(AnyExtKt::isNotNull) - .compose(Transformers.neverError()) - .subscribeOn(AndroidSchedulers.mainThread()); - - // Seed config observable with what's cached - Observable.concat(prefConfig, diskConfig) - .take(1) - .subscribe(this.config::onNext); - - // Cache any new values to preferences - this.config.skip(1) - .filter(AnyExtKt::isNotNull) - .subscribe(c -> configPreference.set(gson.toJson(c, Config.class))); - } - - /** - * Get an observable representation of the current config. Emits immediately with the freshes copy of the config - * and then emits again for any fresher values. - */ - public @NonNull Observable observable() { - return this.config; - } - - public void config(final @NonNull Config config) { - this.config.onNext(config); - } - - /** - * @param assetPath Path where `server-config.json` lives. - * @param assetManager Asset manager to use to load `server-config.json`. - * @return A string representation of the config JSON. - */ - private @NonNull String configJSONString(final @NonNull String assetPath, final @NonNull AssetManager assetManager) { - try { - final InputStream input; - input = assetManager.open(assetPath); - final byte[] buffer = new byte[input.available()]; - input.read(buffer); - input.close(); - - return new String(buffer); - } catch (final IOException e) { - Timber.e(e); - // TODO: This should probably be fatal? - } - - return "{}"; - } -} diff --git a/app/src/main/java/com/kickstarter/libs/CurrentConfig.kt b/app/src/main/java/com/kickstarter/libs/CurrentConfig.kt new file mode 100644 index 0000000000..689e314302 --- /dev/null +++ b/app/src/main/java/com/kickstarter/libs/CurrentConfig.kt @@ -0,0 +1,96 @@ +package com.kickstarter.libs + +import android.content.res.AssetManager +import com.google.gson.Gson +import com.kickstarter.libs.preferences.StringPreferenceType +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.libs.utils.extensions.isNotNull +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.BehaviorSubject +import timber.log.Timber +import java.io.IOException + +class CurrentConfig( + assetManager: AssetManager, + gson: Gson, + configPreference: StringPreferenceType +) : CurrentConfigType { + private val config = BehaviorSubject.create() + private val disposables = CompositeDisposable() + + init { + // Loads config from disk + + val diskConfig = Observable.just(ASSET_PATH) + .map { configJSONString(it, assetManager) } + .filter { gson.fromJson(it, Config::class.java).isNotNull() } + .map { gson.fromJson(it, Config::class.java) } + .map { it } + .compose(Transformers.neverErrorV2()) + .subscribeOn(AndroidSchedulers.mainThread()) + + // Loads config from string preference + val prefConfig = Observable.just(configPreference) + .filter { it.get().isNotNull() } + .map { it.get() } + .map { it } + .filter { gson.fromJson(it, Config::class.java).isNotNull() } + .map { gson.fromJson(it, Config::class.java) } + .map { it } + .compose(Transformers.neverErrorV2()) + .subscribeOn(AndroidSchedulers.mainThread()) + + // Seed config observable with what's cached + Observable.concat(prefConfig, diskConfig) + .take(1) + .subscribe { t: Config -> config.onNext(t) } + .addToDisposable(disposables) + + // Cache any new values to preferences + config.skip(1) + .filter { it.isNotNull() } + .map { it } + .subscribe { configPreference.set(gson.toJson(it, Config::class.java)) } + .addToDisposable(disposables) + } + + /** + * Get an observable representation of the current config. Emits immediately with the freshes copy of the config + * and then emits again for any fresher values. + */ + override fun observable(): Observable { + return this.config + } + + override fun config(config: Config) { + this.config.onNext(config) + } + + /** + * @param assetPath Path where `server-config.json` lives. + * @param assetManager Asset manager to use to load `server-config.json`. + * @return A string representation of the config JSON. + */ + private fun configJSONString(assetPath: String, assetManager: AssetManager): String { + try { + val input = assetManager.open(assetPath) + val buffer = ByteArray(input.available()) + input.read(buffer) + input.close() + + return String(buffer) + } catch (e: IOException) { + Timber.e(e) + // TODO: This should probably be fatal? + } + + return "{}" + } + + companion object { + private const val ASSET_PATH = "json/server-config.json" + } +} diff --git a/app/src/main/java/com/kickstarter/libs/CurrentConfigType.java b/app/src/main/java/com/kickstarter/libs/CurrentConfigType.java deleted file mode 100644 index 02093ea91d..0000000000 --- a/app/src/main/java/com/kickstarter/libs/CurrentConfigType.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.kickstarter.libs; - -import rx.Observable; - -public interface CurrentConfigType { - - /** - * Returns the config as an observable. - */ - Observable observable(); - - /** - * Set a new config. - */ - void config(Config config); -} diff --git a/app/src/main/java/com/kickstarter/libs/CurrentConfigType.kt b/app/src/main/java/com/kickstarter/libs/CurrentConfigType.kt new file mode 100644 index 0000000000..ad23aabf44 --- /dev/null +++ b/app/src/main/java/com/kickstarter/libs/CurrentConfigType.kt @@ -0,0 +1,15 @@ +package com.kickstarter.libs + +import io.reactivex.Observable + +interface CurrentConfigType { + /** + * Returns the config as an observable. + */ + fun observable(): Observable + + /** + * Set a new config. + */ + fun config(config: Config) +} diff --git a/app/src/main/java/com/kickstarter/libs/SwipeRefresher.java b/app/src/main/java/com/kickstarter/libs/SwipeRefresher.java deleted file mode 100644 index f45e7c25fc..0000000000 --- a/app/src/main/java/com/kickstarter/libs/SwipeRefresher.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.kickstarter.libs; - -import com.jakewharton.rxbinding.support.v4.widget.RxSwipeRefreshLayout; -import com.kickstarter.R; - -import androidx.annotation.NonNull; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Action0; -import rx.functions.Func0; - -public final class SwipeRefresher { - /** - * - * @param activity Activity to bind lifecycle events for. - * @param layout Layout to subscribe to for refresh events, send signals when no longer refreshing. - * @param refreshAction Action to call when a refresh event is emitted, likely a viewModel input. - * @param isRefreshing Observable that emits events when the refreshing status changes. - */ - public SwipeRefresher(final @NonNull BaseActivity activity, - final @NonNull SwipeRefreshLayout layout, - final @NonNull Action0 refreshAction, - final @NonNull Func0> isRefreshing) { - - // Iterate through colors in loading spinner while waiting for refresh - setColorSchemeResources(layout); - - // Emits when user has signaled to refresh layout - RxSwipeRefreshLayout.refreshes(layout) - .compose(activity.bindToLifecycle()) - .subscribe(__ -> refreshAction.call()); - - // Emits when the refreshing status changes. Hides loading spinner when feed is no longer refreshing. - isRefreshing.call() - .filter(refreshing -> !refreshing) - .compose(activity.bindToLifecycle()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(layout::setRefreshing); - } - - /** - * - * @param fragment Fragment to bind lifecycle events for. - * @param layout Layout to subscribe to for refresh events, send signals when no longer refreshing. - * @param refreshAction Action to call when a refresh event is emitted, likely a viewModel input. - * @param isRefreshing Observable that emits events when the refreshing status changes. - */ - public SwipeRefresher(final @NonNull BaseFragment fragment, - final @NonNull SwipeRefreshLayout layout, - final @NonNull Action0 refreshAction, - final @NonNull Func0> isRefreshing) { - setColorSchemeResources(layout); - - - // Emits when user has signaled to refresh layout - RxSwipeRefreshLayout.refreshes(layout) - .compose(fragment.bindToLifecycle()) - .subscribe(__ -> refreshAction.call()); - - // Emits when the refreshing status changes. Hides loading spinner when feed is no longer refreshing. - isRefreshing.call() - .filter(refreshing -> !refreshing) - .compose(fragment.bindToLifecycle()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(layout::setRefreshing); - } - - private void setColorSchemeResources(final @NonNull SwipeRefreshLayout layout) { - // Iterate through colors in loading spinner while waiting for refresh - layout.setColorSchemeResources(R.color.kds_create_700, R.color.kds_create_500, R.color.kds_create_300); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/loadmore/ApolloPaginate.kt b/app/src/main/java/com/kickstarter/libs/loadmore/ApolloPaginate.kt deleted file mode 100644 index 3a317a1224..0000000000 --- a/app/src/main/java/com/kickstarter/libs/loadmore/ApolloPaginate.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.kickstarter.libs.loadmore - -import android.util.Pair -import com.kickstarter.libs.rx.transformers.Transformers -import com.kickstarter.models.ApolloEnvelope -import rx.Observable -import rx.functions.Func1 -import rx.functions.Func2 -import rx.subjects.PublishSubject -import java.net.MalformedURLException -import java.util.ArrayList - -class ApolloPaginate( - val nextPage: Observable, - val startOverWith: Observable, - val envelopeToListOfData: Func1>, - val loadWithParams: Func1, Observable>, - val pageTransformation: Func1, List>, - val clearWhenStartingOver: Boolean = true, - val concater: Func2?, List?, List?>, - val distinctUntilChanged: Boolean, - val isReversed: Boolean -) { - private val _morePath = PublishSubject.create() - private val _isFetching = PublishSubject.create() - private var isFetching: Observable = this._isFetching - private var loadingPage: Observable? = null - private var paginatedData: Observable>? = null - - init { - paginatedData = - startOverWith.switchMap { firstPageParams: Params -> - this.dataWithPagination( - firstPageParams - ) - } - loadingPage = - startOverWith.switchMap { - nextPage.scan(1, { accum: Int, _ -> accum + 1 }) - } - } - - class Builder { - private var nextPage: Observable? = null - private var startOverWith: Observable? = null - private var envelopeToListOfData: Func1>? = null - private var loadWithParams: Func1, Observable>? = null - private var pageTransformation: Func1, List> = Func1, List> { - x: List -> - x - } - private var clearWhenStartingOver = false - - private var concater: Func2?, List?, List?> = - Func2 { xs: List?, ys: List? -> - mutableListOf().apply { - if (isReversed) { - ys?.toMutableList()?.let { this.addAll(it) } - xs?.toMutableList()?.let { this.addAll(it) } - } else { - xs?.toMutableList()?.let { this.addAll(it) } - ys?.toMutableList()?.let { this.addAll(it) } - } - }.toList() - } - private var distinctUntilChanged = false - private var isReversed = false - - /** - * [Required] An observable that emits whenever a new page of data should be loaded. - */ - fun nextPage(nextPage: Observable): Builder { - this.nextPage = nextPage - return this - } - - /** - * [Optional] An observable that emits when a fresh first page should be loaded. - */ - fun startOverWith(startOverWith: Observable): Builder { - this.startOverWith = startOverWith - return this - } - - /** - * [Required] A function that takes an `Envelope` instance and returns the list of data embedded in it. - */ - fun envelopeToListOfData(envelopeToListOfData: Func1>): Builder { - this.envelopeToListOfData = envelopeToListOfData - return this - } - - /** - * [Required] A function that takes a `Params` and performs the associated network request - * and returns an `Observable` - */ - fun loadWithParams(loadWithParams: Func1, Observable>): Builder { - this.loadWithParams = loadWithParams - return this - } - - /** - * [Optional] Function to transform every page of data that is loaded. - */ - fun pageTransformation(pageTransformation: Func1, List>): Builder { - this.pageTransformation = pageTransformation - return this - } - - /** - * [Optional] Determines if the list of loaded data is cleared when starting over from the first page. - */ - fun clearWhenStartingOver(clearWhenStartingOver: Boolean): Builder { - this.clearWhenStartingOver = clearWhenStartingOver - return this - } - - /** - * [Optional] Determines how two lists are concatenated together while paginating. A regular `ListUtils::concat` is probably - * sufficient, but sometimes you may want `ListUtils::concatDistinct` - */ - fun concater(concater: Func2?, List?, List?>): Builder { - this.concater = concater - return this - } - - /** - * [Optional] Determines if the list of loaded data is should be distinct until changed. - */ - fun distinctUntilChanged(distinctUntilChanged: Boolean): Builder { - this.distinctUntilChanged = distinctUntilChanged - return this - } - - /** - * [Optional] Determines if the list of loaded data is should be distinct until changed. - */ - fun isReversed(isReversed: Boolean): Builder { - this.isReversed = isReversed - return this - } - - @Throws(RuntimeException::class) - fun build(): ApolloPaginate { - // Early error when required field is not set - if (nextPage == null) { - throw RuntimeException("`nextPage` is required") - } - if (envelopeToListOfData == null) { - throw RuntimeException("`envelopeToListOfData` is required") - } - if (loadWithParams == null) { - throw RuntimeException("`loadWithParams` is required") - } - - // Default params for optional fields - if (startOverWith == null) { - startOverWith = Observable.just(null) - } - - return ApolloPaginate( - requireNotNull(nextPage), - requireNotNull(startOverWith), - requireNotNull(envelopeToListOfData), - requireNotNull(loadWithParams), - pageTransformation, - clearWhenStartingOver, - concater, - distinctUntilChanged, - isReversed - ) - } - } - - companion object { - @JvmStatic - fun builder(): Builder = Builder() - } - - /** - * Returns an observable that emits the accumulated list of paginated data each time a new page is loaded. - */ - private fun dataWithPagination(firstPageParams: Params): Observable?>? { - val data = paramsAndMoreUrlWithPagination(firstPageParams)?.concatMap { - fetchData(it) - }?.takeUntil { obj -> - obj?.isEmpty() - } - - val paginatedData = - if (clearWhenStartingOver) - data?.scan(ArrayList(), concater) - else - data?.scan(concater) - - return if (distinctUntilChanged) - paginatedData?.distinctUntilChanged() - else - paginatedData - } - - /** - * Returns an observable that emits the params for the next page of data *or* the more URL for the next page. - */ - private fun paramsAndMoreUrlWithPagination(firstPageParams: Params): Observable>? { - return _morePath - .map { path: String? -> - Pair( - firstPageParams, - path - ) - } - .compose(Transformers.takeWhen(nextPage)) - .startWith(Pair(firstPageParams, null)) - } - - private fun fetchData(paginatingData: Pair): Observable?> { - - return loadWithParams.call(paginatingData) - .retry(2) - .compose(Transformers.neverError()) - .doOnNext { envelope: Envelope -> - keepMorePath(envelope) - } - .map(envelopeToListOfData) - .map(this.pageTransformation) - .takeUntil { data: List -> data.isEmpty() } - .doOnSubscribe { _isFetching.onNext(true) } - .doAfterTerminate { - _isFetching.onNext(false) - } - } - - private fun keepMorePath(envelope: Envelope) { - try { - _morePath.onNext( - if (isReversed) - envelope.pageInfoEnvelope()?.startCursor - else - envelope.pageInfoEnvelope()?.endCursor - ) - } catch (ignored: MalformedURLException) { - ignored.printStackTrace() - } - } - - // Outputs - fun paginatedData(): Observable>? { - return paginatedData - } - - fun isFetching(): Observable { - return isFetching - } - - fun loadingPage(): Observable? { - return loadingPage - } -} diff --git a/app/src/main/java/com/kickstarter/libs/qualifiers/RequiresActivityViewModel.java b/app/src/main/java/com/kickstarter/libs/qualifiers/RequiresActivityViewModel.java deleted file mode 100644 index a750e9497d..0000000000 --- a/app/src/main/java/com/kickstarter/libs/qualifiers/RequiresActivityViewModel.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kickstarter.libs.qualifiers; - -import com.kickstarter.libs.ActivityViewModel; - -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Inherited -@Retention(RetentionPolicy.RUNTIME) -public @interface RequiresActivityViewModel { - Class value(); -} diff --git a/app/src/main/java/com/kickstarter/libs/rx/transformers/Transformers.java b/app/src/main/java/com/kickstarter/libs/rx/transformers/Transformers.java index 87ae12ee36..aa87b3c2b1 100644 --- a/app/src/main/java/com/kickstarter/libs/rx/transformers/Transformers.java +++ b/app/src/main/java/com/kickstarter/libs/rx/transformers/Transformers.java @@ -1,12 +1,11 @@ package com.kickstarter.libs.rx.transformers; +import androidx.annotation.NonNull; + import com.kickstarter.services.ApiException; import com.kickstarter.services.apiresponses.ErrorEnvelope; -import androidx.annotation.NonNull; import rx.Observable; -import rx.functions.Action1; -import rx.subjects.PublishSubject; public final class Transformers { private Transformers() {} @@ -62,19 +61,6 @@ public static NeverApiErrorTransformer neverApiError() { return new NeverApiErrorTransformer<>(); } - /** - * Prevents an observable from erroring on any {@link ApiException} exceptions, - * and any errors that do occur will be piped into the supplied - * errors publish subject. `null` values will never be sent to - * the publish subject. - * - * @deprecated Use {@link Observable#materialize()} instead. - */ - @Deprecated - public static NeverApiErrorTransformer pipeApiErrorsTo(final @NonNull PublishSubject errorSubject) { - return new NeverApiErrorTransformer<>(errorSubject::onNext); - } - /** * Prevents an observable from erroring on any {@link ApiException} exceptions, * and any errors that do occur will be piped into the supplied @@ -87,26 +73,6 @@ public static NeverApiErrorTransformerV2 pipeApiErrorsToV2(final @NonNull return new NeverApiErrorTransformerV2<>(errorSubject::onNext); } - /** - * Prevents an observable from erroring on any {@link ApiException} exceptions, - * and any errors that do occur will be piped into the supplied - * errors actions. `null` values will never be sent to the action. - * - * @deprecated Use {@link Observable#materialize()} instead. - */ - @Deprecated - public static NeverApiErrorTransformer pipeApiErrorsTo(final @NonNull Action1 errorAction) { - return new NeverApiErrorTransformer<>(errorAction); - } - - /** - * Emits the latest value of the source observable whenever the `when` - * observable emits. - */ - public static TakeWhenTransformer takeWhen(final @NonNull Observable when) { - return new TakeWhenTransformer<>(when); - } - /** * Emits the latest value of the source observable whenever the `when` * observable emits. @@ -121,6 +87,7 @@ public static TakeWhenTransformerV2 takeWhenV2(final @NonNull io.re * Emits the latest value of the source `when` observable whenever the * `when` observable emits. */ + // TODO: Delete this when last RX1 is removed public static TakePairWhenTransformer takePairWhen(final @NonNull Observable when) { return new TakePairWhenTransformer<>(when); } @@ -142,6 +109,7 @@ public static ZipPairTransformerV2 zipPairV2(final @NonNull io.reac /** * Emits the latest values from two observables whenever either emits. */ + // TODO: Delete this when last RX1 is removed public static CombineLatestPairTransformer combineLatestPair(final @NonNull Observable second) { return new CombineLatestPairTransformer<>(second); } @@ -152,19 +120,6 @@ public static CombineLatestPairTransformer combineLatestPair(final public static CombineLatestPairTransformerV2 combineLatestPair(final @NonNull io.reactivex.Observable second) { return new CombineLatestPairTransformerV2<>(second); } - /** - * Waits until `until` emits one single item and then switches context to the source. This - * can be useful to delay work until a user logs in: - * - * ``` - * somethingThatRequiresAuth - * .compose(waitUntil(currentUser.loggedInUser())) - * .subscribe(show) - * ``` - */ - public static @NonNull WaitUntilTransformer waitUntil(final @NonNull Observable until) { - return new WaitUntilTransformer<>(until); - } /** * Converts an observable of any type into an observable of `null`s. This is useful for forcing diff --git a/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.java b/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.java deleted file mode 100644 index acf6b591fb..0000000000 --- a/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.kickstarter.libs.utils; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Parcelable; - -import com.kickstarter.R; -import com.kickstarter.ui.activities.DiscoveryActivity; - -import java.util.List; - -import androidx.annotation.NonNull; -import rx.Observable; - -public final class ApplicationUtils { - private ApplicationUtils() {} - - public static void openUrlExternally(final @NonNull Context context, final @NonNull String url) { - final Uri uri = Uri.parse(url); - final List targetIntents = targetIntents(context, uri); - - if (!targetIntents.isEmpty()) { - /* We need to remove the first intent so it's not duplicated when we add the - EXTRA_INITIAL_INTENTS intents. */ - final Intent chooserIntent = Intent.createChooser(targetIntents.remove(0), context.getString(R.string.View_project)); - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, - targetIntents.toArray(new Parcelable[targetIntents.size()])); - context.startActivity(chooserIntent); - } - } - - /** - * - * Starts the main activity at the top of a task stack, clearing all previous activities. - * - * `ACTION_MAIN` does not expect to receive any data in the intent, it should be the same intent as if a user had - * just launched the app. - */ - public static void startNewDiscoveryActivity(final @NonNull Context context) { - final Intent intent = new Intent(context, DiscoveryActivity.class) - .setAction(Intent.ACTION_MAIN) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - - context.startActivity(intent); - } - - /** - * Clears all activities from the task stack except discovery. - */ - public static void resumeDiscoveryActivity(final @NonNull Context context) { - final Intent intent = new Intent(context, DiscoveryActivity.class) - .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - context.startActivity(intent); - } - - private static List targetIntents(final @NonNull Context context, final @NonNull Uri uri) { - final Uri fakeUri = Uri.parse("http://www.kickstarter.com"); - final Intent browserIntent = new Intent(Intent.ACTION_VIEW, fakeUri); - - return Observable.from(context.getPackageManager().queryIntentActivities(browserIntent, 0)) - .filter(resolveInfo -> !resolveInfo.activityInfo.packageName.contains("com.kickstarter")) - .map(resolveInfo -> { - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setPackage(resolveInfo.activityInfo.packageName); - intent.setData(uri); - return intent; - }) - .toList() - .toBlocking() - .single(); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.kt b/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.kt new file mode 100644 index 0000000000..9a8f8b9632 --- /dev/null +++ b/app/src/main/java/com/kickstarter/libs/utils/ApplicationUtils.kt @@ -0,0 +1,70 @@ +package com.kickstarter.libs.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.net.Uri +import android.os.Parcelable +import com.kickstarter.R +import com.kickstarter.ui.activities.DiscoveryActivity + +object ApplicationUtils { + fun openUrlExternally(context: Context, url: String) { + val uri = Uri.parse(url) + val targetIntents = targetIntents(context, uri) + + if (targetIntents.isNotEmpty()) { + /* We need to remove the first intent so it's not duplicated when we add the + EXTRA_INITIAL_INTENTS intents. */ + val chooserIntent = Intent.createChooser( + targetIntents.toMutableList().removeAt(0), + context.getString(R.string.View_project) + ) + chooserIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + targetIntents.toTypedArray() + ) + context.startActivity(chooserIntent) + } + } + + /** + * + * Starts the main activity at the top of a task stack, clearing all previous activities. + * + * `ACTION_MAIN` does not expect to receive any data in the intent, it should be the same intent as if a user had + * just launched the app. + */ + fun startNewDiscoveryActivity(context: Context) { + val intent = Intent(context, DiscoveryActivity::class.java) + .setAction(Intent.ACTION_MAIN) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + + context.startActivity(intent) + } + + /** + * Clears all activities from the task stack except discovery. + */ + fun resumeDiscoveryActivity(context: Context) { + val intent = Intent(context, DiscoveryActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + + context.startActivity(intent) + } + + private fun targetIntents(context: Context, uri: Uri): List { + val fakeUri = Uri.parse("http://www.kickstarter.com") + val browserIntent = Intent(Intent.ACTION_VIEW, fakeUri) + + return context.packageManager.queryIntentActivities(browserIntent, 0) + .filter { resolveInfo: ResolveInfo -> !resolveInfo.activityInfo.packageName.contains("com.kickstarter") } + .map { resolveInfo: ResolveInfo -> + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.setPackage(resolveInfo.activityInfo.packageName) + intent.setData(uri) + intent + } + .toList() + } +} diff --git a/app/src/main/java/com/kickstarter/mock/MockCurrentConfig.java b/app/src/main/java/com/kickstarter/mock/MockCurrentConfig.java index 5afa6556b1..977ccaae6d 100644 --- a/app/src/main/java/com/kickstarter/mock/MockCurrentConfig.java +++ b/app/src/main/java/com/kickstarter/mock/MockCurrentConfig.java @@ -4,8 +4,9 @@ import com.kickstarter.libs.CurrentConfigType; import androidx.annotation.NonNull; -import rx.Observable; -import rx.subjects.BehaviorSubject; + +import io.reactivex.Observable; +import io.reactivex.subjects.BehaviorSubject; public final class MockCurrentConfig implements CurrentConfigType { diff --git a/app/src/main/java/com/kickstarter/mock/factories/ApiExceptionFactory.java b/app/src/main/java/com/kickstarter/mock/factories/ApiExceptionFactory.java index 130e700684..743c59959c 100644 --- a/app/src/main/java/com/kickstarter/mock/factories/ApiExceptionFactory.java +++ b/app/src/main/java/com/kickstarter/mock/factories/ApiExceptionFactory.java @@ -8,8 +8,9 @@ import java.util.Collections; import androidx.annotation.NonNull; + +import io.reactivex.Observable; import okhttp3.ResponseBody; -import rx.Observable; public final class ApiExceptionFactory { private ApiExceptionFactory() {} diff --git a/app/src/main/java/com/kickstarter/services/ConnectivityReceiver.kt b/app/src/main/java/com/kickstarter/services/ConnectivityReceiver.kt index b4cdc1169e..1f992583d5 100644 --- a/app/src/main/java/com/kickstarter/services/ConnectivityReceiver.kt +++ b/app/src/main/java/com/kickstarter/services/ConnectivityReceiver.kt @@ -36,7 +36,7 @@ class ConnectivityReceiver( } override fun onReceive(context: Context, intent: Intent) { - // TODO: once all migration to RXJava is completed and BaseActivity removed, get rid of the BroadcastReceiver receiver + // TODO: once all migration to RXJava is completed, get rid of the BroadcastReceiver receiver // and simple register the lifecycle observer on the target activity try { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/app/src/main/java/com/kickstarter/ui/activities/FacebookConfirmationActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/FacebookConfirmationActivity.kt index 961bbdc046..6306361a44 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/FacebookConfirmationActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/FacebookConfirmationActivity.kt @@ -2,22 +2,27 @@ package com.kickstarter.ui.activities import android.content.Intent import android.os.Bundle -import android.util.Pair +import androidx.activity.ComponentActivity +import androidx.activity.viewModels import com.jakewharton.rxbinding.view.RxView import com.kickstarter.R import com.kickstarter.databinding.FacebookConfirmationLayoutBinding import com.kickstarter.libs.ActivityRequestCodes -import com.kickstarter.libs.BaseActivity -import com.kickstarter.libs.qualifiers.RequiresActivityViewModel import com.kickstarter.libs.utils.SwitchCompatUtils import com.kickstarter.libs.utils.TransitionUtils import com.kickstarter.libs.utils.ViewUtils +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.viewmodels.FacebookConfirmationViewModel -import rx.android.schedulers.AndroidSchedulers +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable -@RequiresActivityViewModel(FacebookConfirmationViewModel.ViewModel::class) -class FacebookConfirmationActivity : BaseActivity() { +class FacebookConfirmationActivity : ComponentActivity() { private lateinit var binding: FacebookConfirmationLayoutBinding + private lateinit var viewModelFactory: FacebookConfirmationViewModel.Factory + private val viewModel: FacebookConfirmationViewModel.FacebookConfirmationViewModel by viewModels { + viewModelFactory + } + private val disposables = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,26 +33,26 @@ class FacebookConfirmationActivity : BaseActivity? { - return TransitionUtils.slideInFromLeft() - } - private fun prefillEmail(email: String) { binding.email.text = email } diff --git a/app/src/main/java/com/kickstarter/ui/toolbars/KSToolbar.kt b/app/src/main/java/com/kickstarter/ui/toolbars/KSToolbar.kt index b08d885d47..be07c5986e 100644 --- a/app/src/main/java/com/kickstarter/ui/toolbars/KSToolbar.kt +++ b/app/src/main/java/com/kickstarter/ui/toolbars/KSToolbar.kt @@ -13,12 +13,9 @@ import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import com.kickstarter.KSApplication import com.kickstarter.R -import com.kickstarter.libs.BaseActivity import com.kickstarter.libs.Environment import com.kickstarter.libs.qualifiers.WebEndpoint import com.kickstarter.libs.utils.Secrets -import rx.Subscription -import rx.subscriptions.CompositeSubscription open class KSToolbar @JvmOverloads constructor( context: Context, @@ -31,8 +28,6 @@ open class KSToolbar @JvmOverloads constructor( @WebEndpoint private var webEndpoint: String? = null - private val subscriptions = CompositeSubscription() - init { init(context) } @@ -77,20 +72,8 @@ open class KSToolbar @JvmOverloads constructor( super.onAttachedToWindow() } - @CallSuper - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - subscriptions.clear() - } - - protected fun addSubscription(subscription: Subscription) { - subscriptions.add(subscription) - } - private fun backButtonClick() { - if (context is BaseActivity<*>) { - (context as BaseActivity<*>).back() - } else if (context is ComponentActivity) { + if (context is ComponentActivity) { (context as ComponentActivity).onBackPressedDispatcher.onBackPressed() } else { (context as AppCompatActivity).onBackPressed() diff --git a/app/src/main/java/com/kickstarter/ui/toolbars/ProjectToolbar.kt b/app/src/main/java/com/kickstarter/ui/toolbars/ProjectToolbar.kt index 2c3fe594d5..bf5a4f5c7f 100644 --- a/app/src/main/java/com/kickstarter/ui/toolbars/ProjectToolbar.kt +++ b/app/src/main/java/com/kickstarter/ui/toolbars/ProjectToolbar.kt @@ -2,8 +2,9 @@ package com.kickstarter.ui.toolbars import android.content.Context import android.util.AttributeSet +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity import com.kickstarter.R -import com.kickstarter.libs.BaseActivity import com.kickstarter.ui.views.IconButton class ProjectToolbar @JvmOverloads constructor( @@ -20,6 +21,10 @@ class ProjectToolbar @JvmOverloads constructor( } private fun backIconClick() { - (context as BaseActivity<*>).back() + if (context is ComponentActivity) { + (context as ComponentActivity).onBackPressedDispatcher.onBackPressed() + } else { + (context as AppCompatActivity).onBackPressed() + } } } diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/AddCardViewHolderViewModel.kt b/app/src/main/java/com/kickstarter/ui/viewholders/AddCardViewHolderViewModel.kt index 95703615f2..4f55f2ce40 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/AddCardViewHolderViewModel.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/AddCardViewHolderViewModel.kt @@ -1,10 +1,9 @@ package com.kickstarter.ui.viewholders -import androidx.annotation.NonNull -import com.kickstarter.libs.ActivityViewModel -import com.kickstarter.libs.Environment -import rx.Observable -import rx.subjects.PublishSubject +import com.kickstarter.libs.utils.extensions.addToDisposable +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject interface AddCardViewHolderViewModel { @@ -17,8 +16,7 @@ interface AddCardViewHolderViewModel { fun setDefaultState(): Observable } - class ViewModel(@NonNull environment: Environment) : - ActivityViewModel(environment), + class ViewModel : Inputs, Outputs { val inputs: Inputs = this @@ -28,21 +26,23 @@ interface AddCardViewHolderViewModel { val loading = PublishSubject.create() val default = PublishSubject.create() + private val disposables = CompositeDisposable() + init { state .filter { it == State.DEFAULT } - .compose(bindToLifecycle()) .subscribe { this.default.onNext(it) } + .addToDisposable(disposables) state .filter { it == State.LOADING } - .compose(bindToLifecycle()) .subscribe { this.loading.onNext(it) } + .addToDisposable(disposables) } // inputs @@ -51,5 +51,7 @@ interface AddCardViewHolderViewModel { // output override fun setDefaultState() = this.default override fun setLoadingState() = this.loading + + fun clear() = disposables.clear() } } diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/AddOnViewHolder.kt b/app/src/main/java/com/kickstarter/ui/viewholders/AddOnViewHolder.kt index c888abbddd..009895fe9d 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/AddOnViewHolder.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/AddOnViewHolder.kt @@ -6,72 +6,74 @@ import androidx.core.view.isGone import androidx.recyclerview.widget.LinearLayoutManager import com.kickstarter.R import com.kickstarter.databinding.ItemAddOnBinding -import com.kickstarter.libs.rx.transformers.Transformers.observeForUI +import com.kickstarter.libs.rx.transformers.Transformers.observeForUIV2 import com.kickstarter.libs.utils.RewardViewUtils import com.kickstarter.libs.utils.ViewUtils +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.models.Reward import com.kickstarter.ui.adapters.RewardItemsAdapter import com.kickstarter.ui.data.ProjectData import com.kickstarter.viewmodels.AddOnViewHolderViewModel -import rx.android.schedulers.AndroidSchedulers +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable class AddOnViewHolder(private val binding: ItemAddOnBinding) : KSViewHolder(binding.root) { private var viewModel = AddOnViewHolderViewModel.ViewModel(environment()) private val currencyConversionString = context().getString(R.string.About_reward_amount) private val ksString = requireNotNull(environment().ksString()) + private val disposables = CompositeDisposable() init { val rewardItemAdapter = setUpItemAdapter() this.viewModel.outputs.conversionIsGone() - .compose(bindToLifecycle()) - .compose(observeForUI()) - .subscribe(ViewUtils.setGone(binding.addOnConversionTextView)) + .compose(observeForUIV2()) + .subscribe { ViewUtils.setGone(binding.addOnConversionTextView) } + .addToDisposable(disposables) this.viewModel.outputs.conversion() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.addOnConversionTextView.text = this.ksString.format( this.currencyConversionString, "reward_amount", it ) } + .addToDisposable(disposables) this.viewModel.outputs.descriptionForNoReward() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.addOnDescriptionTextView.setText(it) } + .addToDisposable(disposables) this.viewModel.outputs.titleForNoReward() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.titleContainer.addOnTitleNoSpannable.setText(it) } + .addToDisposable(disposables) this.viewModel.outputs.descriptionForReward() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.addOnDescriptionTextView.text = it } + .addToDisposable(disposables) this.viewModel.outputs.minimumAmountTitle() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.addOnMinimumTextView.text = it } + .addToDisposable(disposables) this.viewModel.outputs.rewardItems() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { rewardItemAdapter.rewardsItems(it) } + .addToDisposable(disposables) this.viewModel.outputs.rewardItemsAreGone() - .compose(bindToLifecycle()) - .compose(observeForUI()) - .subscribe(ViewUtils.setGone(binding.addOnItemsContainer.addOnItemLayout)) + .compose(observeForUIV2()) + .subscribe { ViewUtils.setGone(binding.addOnItemsContainer.addOnItemLayout) } + .addToDisposable(disposables) this.viewModel.outputs.isAddonTitleGone() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { shouldHideAddonAmount -> if (shouldHideAddonAmount) { binding.titleContainer.addOnTitleTextView.visibility = View.GONE @@ -81,30 +83,31 @@ class AddOnViewHolder(private val binding: ItemAddOnBinding) : KSViewHolder(bind binding.titleContainer.addOnTitleTextView.visibility = View.VISIBLE } } + .addToDisposable(disposables) this.viewModel.outputs.titleForReward() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.titleContainer.addOnTitleNoSpannable.text = it } + .addToDisposable(disposables) this.viewModel.outputs.titleForAddOn() - .compose(bindToLifecycle()) - .compose(observeForUI()) + .compose(observeForUIV2()) .subscribe { binding.titleContainer.addOnTitleTextView.text = RewardViewUtils.styleTitleForAddOns(context(), it.first, it.second) } + .addToDisposable(disposables) this.viewModel.outputs.localPickUpIsGone() - .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { binding.rewardItemLocalPickupContainer.localPickupGroup.isGone = it } + .addToDisposable(disposables) this.viewModel.outputs.localPickUpName() - .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { binding.rewardItemLocalPickupContainer.localPickupLocation.text = it } + .addToDisposable(disposables) } override fun bindData(data: Any?) { @@ -115,6 +118,12 @@ class AddOnViewHolder(private val binding: ItemAddOnBinding) : KSViewHolder(bind } } + override fun destroy() { + viewModel.clear() + disposables.clear() + super.destroy() + } + private fun bindReward(projectAndReward: Pair) { this.viewModel.inputs.configureWith(projectAndReward.first, projectAndReward.second) } diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/RewardAddCardViewHolder.kt b/app/src/main/java/com/kickstarter/ui/viewholders/RewardAddCardViewHolder.kt index c0500fd40a..24662b00b7 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/RewardAddCardViewHolder.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/RewardAddCardViewHolder.kt @@ -2,7 +2,9 @@ package com.kickstarter.ui.viewholders import androidx.core.view.isGone import com.kickstarter.databinding.ItemAddCardBinding -import rx.android.schedulers.AndroidSchedulers +import com.kickstarter.libs.utils.extensions.addToDisposable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable enum class State { DEFAULT, LOADING @@ -10,7 +12,8 @@ enum class State { class RewardAddCardViewHolder(val binding: ItemAddCardBinding, val delegate: Delegate) : KSViewHolder(binding.root) { - private val viewModel: AddCardViewHolderViewModel.ViewModel = AddCardViewHolderViewModel.ViewModel(environment()) + private val viewModel: AddCardViewHolderViewModel.ViewModel = AddCardViewHolderViewModel.ViewModel() + private val disposables = CompositeDisposable() init { this.binding.addCardButton.setOnClickListener { @@ -19,21 +22,21 @@ class RewardAddCardViewHolder(val binding: ItemAddCardBinding, val delegate: Del this.viewModel.outputs .setDefaultState() - .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { this.binding.newPaymentPlusIcon.isGone = false this.binding.newPaymentProgress.isGone = true } + .addToDisposable(disposables) this.viewModel.outputs .setLoadingState() - .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { this.binding.newPaymentProgress.isGone = false this.binding.newPaymentPlusIcon.isGone = true } + .addToDisposable(disposables) } override fun bindData(data: Any?) { @@ -45,4 +48,10 @@ class RewardAddCardViewHolder(val binding: ItemAddCardBinding, val delegate: Del interface Delegate { fun addNewCardButtonClicked() } + + override fun destroy() { + disposables.clear() + viewModel.clear() + super.destroy() + } } diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryHolderViewModel.kt b/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryHolderViewModel.kt index f76f33d06e..8e9e892604 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryHolderViewModel.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryHolderViewModel.kt @@ -1,11 +1,10 @@ package com.kickstarter.ui.viewholders -import com.kickstarter.libs.ActivityViewModel import com.kickstarter.libs.Environment -import com.kickstarter.libs.rx.transformers.Transformers.takeWhen +import com.kickstarter.libs.rx.transformers.Transformers.takeWhenV2 import com.kickstarter.models.Category -import rx.Observable -import rx.subjects.PublishSubject +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject interface ThanksCategoryHolderViewModel { @@ -25,9 +24,9 @@ interface ThanksCategoryHolderViewModel { fun notifyDelegateOfCategoryClick(): Observable } - class ViewModel(val environment: Environment) : ActivityViewModel(environment), Inputs, Outputs { + class ViewModel(val environment: Environment) : Inputs, Outputs { private val category = PublishSubject.create() - private val categoryViewClicked = PublishSubject.create() + private val categoryViewClicked = PublishSubject.create() private val categoryName: Observable private val notifyDelegateOfCategoryClick: Observable @@ -37,14 +36,14 @@ interface ThanksCategoryHolderViewModel { init { this.categoryName = this.category.map { it.name() } - this.notifyDelegateOfCategoryClick = this.category.compose(takeWhen(this.categoryViewClicked)) + this.notifyDelegateOfCategoryClick = this.category.compose(takeWhenV2(this.categoryViewClicked)) } override fun configureWith(category: Category) { this.category.onNext(category) } override fun categoryViewClicked() { - this.categoryViewClicked.onNext(null) + this.categoryViewClicked.onNext(Unit) } override fun categoryName(): Observable { diff --git a/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryViewHolder.kt b/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryViewHolder.kt index de9081ec43..a4b7894157 100644 --- a/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryViewHolder.kt +++ b/app/src/main/java/com/kickstarter/ui/viewholders/ThanksCategoryViewHolder.kt @@ -4,8 +4,10 @@ import android.view.View import com.kickstarter.R import com.kickstarter.databinding.ThanksCategoryViewBinding import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.libs.utils.extensions.isNotNull import com.kickstarter.models.Category +import io.reactivex.disposables.CompositeDisposable class ThanksCategoryViewHolder( private val binding: ThanksCategoryViewBinding, @@ -14,6 +16,7 @@ class ThanksCategoryViewHolder( private val viewModel = ThanksCategoryHolderViewModel.ViewModel(environment()) private val delegate: Delegate = delegate private val ksString = requireNotNull(environment().ksString()) + private val disposables = CompositeDisposable() interface Delegate { fun categoryViewHolderClicked(category: Category) @@ -21,14 +24,14 @@ class ThanksCategoryViewHolder( init { viewModel.outputs.categoryName() - .compose(bindToLifecycle()) - .compose(Transformers.observeForUI()) + .compose(Transformers.observeForUIV2()) .subscribe { categoryName: String -> setCategoryButtonText(categoryName) } + .addToDisposable(disposables) viewModel.outputs.notifyDelegateOfCategoryClick() .filter { it.isNotNull() } - .compose(bindToLifecycle()) - .compose(Transformers.observeForUI()) + .compose(Transformers.observeForUIV2()) .subscribe { category: Category -> this.delegate.categoryViewHolderClicked(category) } + .addToDisposable(disposables) } @Throws(Exception::class) override fun bindData(data: Any?) { @@ -43,4 +46,9 @@ class ThanksCategoryViewHolder( override fun onClick(view: View) { viewModel.inputs.categoryViewClicked() } + + override fun destroy() { + disposables.clear() + super.destroy() + } } diff --git a/app/src/main/java/com/kickstarter/viewmodels/AddOnViewHolderViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/AddOnViewHolderViewModel.kt index 5d5b7cb35b..efd30faa17 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/AddOnViewHolderViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/AddOnViewHolderViewModel.kt @@ -1,22 +1,21 @@ package com.kickstarter.viewmodels import android.util.Pair -import androidx.annotation.NonNull import com.kickstarter.R -import com.kickstarter.libs.ActivityViewModel import com.kickstarter.libs.Environment import com.kickstarter.libs.models.Country import com.kickstarter.libs.utils.RewardUtils +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.libs.utils.extensions.isNotNull import com.kickstarter.libs.utils.extensions.negate import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.models.RewardsItem import com.kickstarter.ui.data.ProjectData -import com.kickstarter.ui.viewholders.RewardViewHolder -import rx.Observable -import rx.subjects.BehaviorSubject -import rx.subjects.PublishSubject +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject import java.math.RoundingMode interface AddOnViewHolderViewModel { @@ -76,7 +75,7 @@ interface AddOnViewHolderViewModel { * - No interaction with the user just displaying information * - Loading in [AddOnViewHolder] -> [RewardAndAddOnsAdapter] -> [BackingFragment] */ - class ViewModel(@NonNull environment: Environment) : ActivityViewModel(environment), Inputs, Outputs { + class ViewModel(environment: Environment) : Inputs, Outputs { private val ksCurrency = requireNotNull(environment.ksCurrency()) private val isAddonTitleGone = BehaviorSubject.create() @@ -98,6 +97,8 @@ interface AddOnViewHolderViewModel { val inputs: Inputs = this val outputs: Outputs = this + private val disposables = CompositeDisposable() + init { val reward = this.projectDataAndReward .map { it.second } @@ -107,81 +108,88 @@ interface AddOnViewHolderViewModel { projectAndReward .map { buildCurrency(it.first, it.second) } - .compose(bindToLifecycle()) - .subscribe(this.minimumAmountTitle) + .subscribe { this.minimumAmountTitle.onNext(it) } + .addToDisposable(disposables) projectAndReward .map { it.first } .map { it.currency() == it.currentCurrency() } - .compose(bindToLifecycle()) - .subscribe(this.conversionIsGone) + .subscribe { this.conversionIsGone.onNext(it) } + .addToDisposable(disposables) projectAndReward .map { getCurrency(it) } - .compose(bindToLifecycle()) - .subscribe(this.conversion) + .subscribe { this.conversion.onNext(it) } + .addToDisposable(disposables) reward .filter { RewardUtils.isReward(it) } + .filter { it.description().isNotNull() } .map { it.description() } - .compose(bindToLifecycle()) - .subscribe(this.descriptionForReward) + .map { it } + .subscribe { this.descriptionForReward.onNext(it) } + .addToDisposable(disposables) reward .filter { !it.isAddOn() && RewardUtils.isNoReward(it) } - .compose(bindToLifecycle()) .subscribe { this.descriptionForNoReward.onNext(R.string.Thanks_for_bringing_this_project_one_step_closer_to_becoming_a_reality) this.titleForNoReward.onNext(R.string.You_pledged_without_a_reward) } + .addToDisposable(disposables) reward .filter { RewardUtils.isItemized(it) } + .filter { if (it.isAddOn()) it.addOnsItems().isNotNull() else it.rewardsItems().isNotNull() } .map { if (it.isAddOn()) it.addOnsItems() else it.rewardsItems() } - .compose(bindToLifecycle()) - .subscribe(this.rewardItems) + .map { it } + .subscribe { this.rewardItems.onNext(it) } + .addToDisposable(disposables) reward .map { RewardUtils.isItemized(it) } .map { it.negate() } .distinctUntilChanged() - .compose(bindToLifecycle()) - .subscribe(this.rewardItemsAreGone) + .subscribe { this.rewardItemsAreGone.onNext(it) } + .addToDisposable(disposables) reward .filter { !it.isAddOn() && RewardUtils.isReward(it) } + .filter { it.title().isNotNull() } .map { it.title() } - .compose(bindToLifecycle()) - .subscribe(this.titleForReward) + .map { it } + .subscribe { this.titleForReward.onNext(it) } + .addToDisposable(disposables) reward .map { !it.isAddOn() } - .compose(bindToLifecycle()) - .subscribe(this.titleIsGone) + .subscribe { this.titleIsGone.onNext(it) } + .addToDisposable(disposables) reward .filter { it.isAddOn() && it.quantity()?.let { q -> q > 0 } ?: false } .map { reward -> parametersForTitle(reward) } - .compose(bindToLifecycle()) - .subscribe(this.titleForAddOn) + .subscribe { this.titleForAddOn.onNext(it) } + .addToDisposable(disposables) reward .filter { !RewardUtils.isShippable(it) } .map { RewardUtils.isLocalPickup(it) } - .compose(bindToLifecycle()) .subscribe { this.localPickUpIsGone.onNext(!it) } + .addToDisposable(disposables) reward .filter { !RewardUtils.isShippable(it) } .filter { RewardUtils.isLocalPickup(it) } + .filter { it.localReceiptLocation()?.displayableName().isNotNull() } .map { it.localReceiptLocation()?.displayableName() } - .filter { it.isNotNull() } - .compose(bindToLifecycle()) - .subscribe(this.localPickUpName) + .map { it } + .subscribe { this.localPickUpName.onNext(it) } + .addToDisposable(disposables) } private fun getCurrency(it: Pair) = @@ -241,5 +249,7 @@ interface AddOnViewHolderViewModel { override fun localPickUpIsGone(): Observable = localPickUpIsGone override fun localPickUpName(): Observable = localPickUpName + + fun clear() = disposables.clear() } } diff --git a/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java b/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java deleted file mode 100644 index 124c96d2a5..0000000000 --- a/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.kickstarter.viewmodels; - -import android.util.Pair; - -import com.kickstarter.libs.ActivityViewModel; -import com.kickstarter.libs.CurrentConfigType; -import com.kickstarter.libs.Environment; -import com.kickstarter.services.ApiClientType; -import com.kickstarter.services.apiresponses.AccessTokenEnvelope; -import com.kickstarter.services.apiresponses.ErrorEnvelope; -import com.kickstarter.ui.IntentKey; -import com.kickstarter.ui.activities.FacebookConfirmationActivity; -import com.kickstarter.viewmodels.usecases.LoginUseCase; - -import androidx.annotation.NonNull; -import rx.Notification; -import rx.Observable; -import rx.subjects.BehaviorSubject; -import rx.subjects.PublishSubject; - -import static com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair; -import static com.kickstarter.libs.rx.transformers.Transformers.errors; -import static com.kickstarter.libs.rx.transformers.Transformers.takeWhen; -import static com.kickstarter.libs.rx.transformers.Transformers.values; - -public interface FacebookConfirmationViewModel { - - interface Inputs { - /** Call when the create new account button has been clicked. */ - void createNewAccountClick(); - - /** Call when the send newsletter switch has been toggled. */ - void sendNewslettersClick(boolean __); - } - - interface Outputs { - /** Fill the view's email address. */ - Observable prefillEmail(); - - /** Emits a string to display when sign up fails. */ - Observable signupError(); - - /** Finish Facebook confirmation activity with OK result. */ - Observable signupSuccess(); - - /** Emits a boolean to check send newsletter switch. */ - Observable sendNewslettersIsChecked(); - } - - final class ViewModel extends ActivityViewModel implements Inputs, Outputs { - private final ApiClientType client; - private final LoginUseCase loginUserCase; - private final CurrentConfigType currentConfig; - - public ViewModel(final @NonNull Environment environment) { - super(environment); - - this.client = environment.apiClient(); - this.currentConfig = environment.currentConfig(); - this.loginUserCase = new LoginUseCase(environment); - - final Observable facebookAccessToken = intent() - .map(i -> i.getStringExtra(IntentKey.FACEBOOK_TOKEN)) - .ofType(String.class); - - final Observable> tokenAndNewsletter = facebookAccessToken - .compose(combineLatestPair(this.sendNewslettersIsChecked)); - - intent() - .map(i -> i.getParcelableExtra(IntentKey.FACEBOOK_USER)) - .ofType(ErrorEnvelope.FacebookUser.class) - .map(ErrorEnvelope.FacebookUser::email) - .compose(bindToLifecycle()) - .subscribe(this.prefillEmail::onNext); - - final Observable> createNewAccountNotification = tokenAndNewsletter - .compose(takeWhen(this.createNewAccountClick)) - .flatMap(tn -> this.client.registerWithFacebook(tn.first, tn.second)) - .share() - .materialize(); - - createNewAccountNotification - .compose(errors()) - .map(ErrorEnvelope::fromThrowable) - .map(ErrorEnvelope::errorMessage) - .takeUntil(this.signupSuccess) - .compose(bindToLifecycle()) - .subscribe(this.signupError); - - createNewAccountNotification - .compose(values()) - .ofType(AccessTokenEnvelope.class) - .compose(bindToLifecycle()) - .subscribe(this::registerWithFacebookSuccess); - - this.sendNewslettersClick - .compose(bindToLifecycle()) - .subscribe(this.sendNewslettersIsChecked::onNext); - - this.currentConfig.observable() - .take(1) - .map(config -> false) - .subscribe(this.sendNewslettersIsChecked::onNext); - } - - private void registerWithFacebookSuccess(final @NonNull AccessTokenEnvelope envelope) { - this.loginUserCase.setToken(envelope.accessToken()); - this.loginUserCase.setUser(envelope.user()); - this.signupSuccess.onNext(null); - } - - private final PublishSubject createNewAccountClick = PublishSubject.create(); - private final PublishSubject sendNewslettersClick = PublishSubject.create(); - - private final BehaviorSubject prefillEmail = BehaviorSubject.create(); - private final PublishSubject signupError = PublishSubject.create(); - private final PublishSubject signupSuccess = PublishSubject.create(); - private final BehaviorSubject sendNewslettersIsChecked = BehaviorSubject.create(); - - public final Inputs inputs = this; - public final Outputs outputs = this; - - @Override public void createNewAccountClick() { - this.createNewAccountClick.onNext(null); - } - @Override public void sendNewslettersClick(final boolean b) { - this.sendNewslettersClick.onNext(b); - } - - @Override public @NonNull Observable prefillEmail() { - return this.prefillEmail; - } - @Override public @NonNull Observable signupError() { - return this.signupError; - } - @Override public @NonNull Observable signupSuccess() { - return this.signupSuccess; - } - @Override public @NonNull Observable sendNewslettersIsChecked() { - return this.sendNewslettersIsChecked; - } - } -} diff --git a/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.kt new file mode 100644 index 0000000000..102266c6e5 --- /dev/null +++ b/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.kt @@ -0,0 +1,167 @@ +package com.kickstarter.viewmodels + +import android.content.Intent +import android.os.Parcelable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.kickstarter.libs.Environment +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.libs.utils.extensions.isNotNull +import com.kickstarter.services.apiresponses.AccessTokenEnvelope +import com.kickstarter.services.apiresponses.ErrorEnvelope +import com.kickstarter.services.apiresponses.ErrorEnvelope.FacebookUser +import com.kickstarter.ui.IntentKey +import com.kickstarter.viewmodels.usecases.LoginUseCase +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject + +interface FacebookConfirmationViewModel { + interface Inputs { + /** Call when the create new account button has been clicked. */ + fun createNewAccountClick() + + /** Call when the send newsletter switch has been toggled. */ + fun sendNewslettersClick(b: Boolean) + } + + interface Outputs { + /** Fill the view's email address. */ + fun prefillEmail(): Observable + + /** Emits a string to display when sign up fails. */ + fun signupError(): Observable + + /** Finish Facebook confirmation activity with OK result. */ + fun signupSuccess(): Observable + + /** Emits a boolean to check send newsletter switch. */ + fun sendNewslettersIsChecked(): Observable + } + + class FacebookConfirmationViewModel(environment: Environment) : + ViewModel(), Inputs, Outputs { + private val client = requireNotNull(environment.apiClientV2()) + private val loginUserCase = LoginUseCase(environment) + private val currentConfig = requireNotNull(environment.currentConfig()) + + private fun registerWithFacebookSuccess(envelope: AccessTokenEnvelope) { + loginUserCase.setToken(envelope.accessToken()) + loginUserCase.setUser(envelope.user()) + signupSuccess.onNext(Unit) + } + + private val createNewAccountClick: PublishSubject = PublishSubject.create() + private val sendNewslettersClick: PublishSubject = PublishSubject.create() + + private val prefillEmail: BehaviorSubject = BehaviorSubject.create() + private val signupError: PublishSubject = PublishSubject.create() + private val signupSuccess: PublishSubject = PublishSubject.create() + private val sendNewslettersIsChecked: BehaviorSubject = BehaviorSubject.create() + private val intent = PublishSubject.create() + private val disposables = CompositeDisposable() + + @JvmField + val inputs: Inputs = this + @JvmField + val outputs: Outputs = this + + init { + val intentObservable = intent.share() + + val facebookAccessToken = intentObservable + .map { i: Intent -> i.getStringExtra(IntentKey.FACEBOOK_TOKEN) } + .ofType(String::class.java) + + val tokenAndNewsletter = facebookAccessToken + .compose(Transformers.combineLatestPair(this.sendNewslettersIsChecked)) + + intentObservable + .filter { it.getParcelableExtra(IntentKey.FACEBOOK_USER).isNotNull() } + .map { it.getParcelableExtra(IntentKey.FACEBOOK_USER) } + .filter { it is FacebookUser } + .map { it as FacebookUser } + .map { it.email() } + .subscribe { prefillEmail.onNext(it) } + .addToDisposable(disposables) + + val createNewAccountNotification = tokenAndNewsletter + .compose(Transformers.takeWhenV2(this.createNewAccountClick)) + .flatMap { + client.registerWithFacebook( + it.first, it.second + ) + } + .materialize() + + createNewAccountNotification + .compose(Transformers.errorsV2()) + .map { ErrorEnvelope.fromThrowable(it) } + .map { it.errorMessage() } + .takeUntil(this.signupSuccess) + .subscribe { this.signupError.onNext(it) } + .addToDisposable(disposables) + + createNewAccountNotification + .compose(Transformers.valuesV2()) + .ofType(AccessTokenEnvelope::class.java) + .subscribe { envelope: AccessTokenEnvelope -> + this.registerWithFacebookSuccess( + envelope + ) + } + .addToDisposable(disposables) + + sendNewslettersClick + .subscribe { v: Boolean -> sendNewslettersIsChecked.onNext(v) } + .addToDisposable(disposables) + + currentConfig.observable() + .take(1) + .map { false } + .subscribe { v: Boolean -> sendNewslettersIsChecked.onNext(v) } + .addToDisposable(disposables) + } + + override fun createNewAccountClick() { + createNewAccountClick.onNext(Unit) + } + + override fun sendNewslettersClick(b: Boolean) { + sendNewslettersClick.onNext(b) + } + + override fun prefillEmail(): Observable { + return this.prefillEmail + } + + override fun signupError(): Observable { + return this.signupError + } + + override fun signupSuccess(): Observable { + return this.signupSuccess + } + + override fun sendNewslettersIsChecked(): Observable { + return this.sendNewslettersIsChecked + } + + fun provideIntent(intent: Intent) { + this.intent.onNext(intent) + } + + override fun onCleared() { + disposables.clear() + super.onCleared() + } + } + + class Factory(private val environment: Environment) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return FacebookConfirmationViewModel(environment) as T + } + } +} diff --git a/app/src/main/java/com/kickstarter/viewmodels/MessageHolderViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/MessageHolderViewModel.kt index 8f685347cf..9057f2ce3d 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/MessageHolderViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/MessageHolderViewModel.kt @@ -1,14 +1,12 @@ package com.kickstarter.viewmodels import android.util.Pair -import com.kickstarter.libs.ActivityViewModel import com.kickstarter.libs.CurrentUserTypeV2 import com.kickstarter.libs.Environment import com.kickstarter.libs.utils.PairUtils import com.kickstarter.libs.utils.extensions.negate import com.kickstarter.models.Message import com.kickstarter.models.User -import com.kickstarter.ui.viewholders.MessageViewHolder import io.reactivex.Observable import io.reactivex.subjects.PublishSubject @@ -45,7 +43,6 @@ interface MessageHolderViewModel { } class ViewModel(environment: Environment) : - ActivityViewModel(environment), Inputs, Outputs { private val currentUser: CurrentUserTypeV2? diff --git a/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.java b/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.java deleted file mode 100644 index b863773914..0000000000 --- a/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.kickstarter.libs.rx.transformers; - -import org.junit.Test; - -import rx.Observable; -import rx.observers.TestSubscriber; -import rx.subjects.PublishSubject; - -public class TakeWhenTransformerTest { - - @Test - public void testTakeWhen_sourceEmitsFirst() { - - final PublishSubject source = PublishSubject.create(); - final PublishSubject sample = PublishSubject.create(); - final Observable result = source.compose(Transformers.takeWhen(sample)); - - final TestSubscriber resultTest = new TestSubscriber<>(); - result.subscribe(resultTest); - - source.onNext(1); - resultTest.assertNoValues(); - - source.onNext(2); - resultTest.assertNoValues(); - - sample.onNext(null); - resultTest.assertValues(2); - - sample.onNext(null); - resultTest.assertValues(2, 2); - - source.onNext(3); - resultTest.assertValues(2, 2); - - sample.onNext(null); - resultTest.assertValues(2, 2, 3); - } - - @Test - public void testTakeWhen_sourceEmitsSecond() { - - final PublishSubject source = PublishSubject.create(); - final PublishSubject sample = PublishSubject.create(); - final Observable result = source.compose(Transformers.takeWhen(sample)); - - final TestSubscriber resultTest = new TestSubscriber<>(); - result.subscribe(resultTest); - - sample.onNext(null); - resultTest.assertNoValues(); - - sample.onNext(null); - resultTest.assertNoValues(); - - source.onNext(1); - resultTest.assertNoValues(); - - sample.onNext(null); - resultTest.assertValues(1); - - source.onNext(2); - resultTest.assertValues(1); - - sample.onNext(null); - resultTest.assertValues(1, 2); - - source.onNext(3); - resultTest.assertValues(1, 2); - - sample.onNext(null); - resultTest.assertValues(1, 2, 3); - } -} diff --git a/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.kt b/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.kt new file mode 100644 index 0000000000..01ccb112a8 --- /dev/null +++ b/app/src/test/java/com/kickstarter/libs/rx/transformers/TakeWhenTransformerTest.kt @@ -0,0 +1,79 @@ +package com.kickstarter.libs.rx.transformers + +import com.kickstarter.libs.utils.extensions.addToDisposable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subscribers.TestSubscriber +import org.junit.After +import org.junit.Test + +class TakeWhenTransformerTest { + private val disposables = CompositeDisposable() + + @Test + fun testTakeWhen_sourceEmitsFirst() { + val source = PublishSubject.create() + val sample = PublishSubject.create() + val result = source.compose(Transformers.takeWhenV2(sample)) + + val resultTest = TestSubscriber() + result.subscribe { resultTest.onNext(it) }.addToDisposable(disposables) + + source.onNext(1) + resultTest.assertNoValues() + + source.onNext(2) + resultTest.assertNoValues() + + sample.onNext(Unit) + resultTest.assertValues(2) + + sample.onNext(Unit) + resultTest.assertValues(2, 2) + + source.onNext(3) + resultTest.assertValues(2, 2) + + sample.onNext(Unit) + resultTest.assertValues(2, 2, 3) + } + + @Test + fun testTakeWhen_sourceEmitsSecond() { + val source = PublishSubject.create() + val sample = PublishSubject.create() + val result = source.compose(Transformers.takeWhenV2(sample)) + + val resultTest = TestSubscriber() + result.subscribe { resultTest.onNext(it) }.addToDisposable(disposables) + + sample.onNext(Unit) + resultTest.assertNoValues() + + sample.onNext(Unit) + resultTest.assertNoValues() + + source.onNext(1) + resultTest.assertNoValues() + + sample.onNext(Unit) + resultTest.assertValues(1) + + source.onNext(2) + resultTest.assertValues(1) + + sample.onNext(Unit) + resultTest.assertValues(1, 2) + + source.onNext(3) + resultTest.assertValues(1, 2) + + sample.onNext(Unit) + resultTest.assertValues(1, 2, 3) + } + + @After + fun clear() { + disposables.clear() + } +} diff --git a/app/src/test/java/com/kickstarter/viewmodels/AddCardViewHolderViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/AddCardViewHolderViewModelTest.kt index 6fa621c1b9..45fef8988c 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/AddCardViewHolderViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/AddCardViewHolderViewModelTest.kt @@ -1,27 +1,30 @@ package com.kickstarter.viewmodels import com.kickstarter.KSRobolectricTestCase -import com.kickstarter.libs.Environment +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.ui.viewholders.AddCardViewHolderViewModel import com.kickstarter.ui.viewholders.State +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subscribers.TestSubscriber +import org.junit.After import org.junit.Test -import rx.observers.TestSubscriber class AddCardViewHolderViewModelTest : KSRobolectricTestCase() { private lateinit var vm: AddCardViewHolderViewModel.ViewModel private val loading = TestSubscriber() private val default = TestSubscriber() + private val disposables = CompositeDisposable() - private fun setUpEnvironment(environment: Environment) { - vm = AddCardViewHolderViewModel.ViewModel(environment) - this.vm.outputs.setLoadingState().subscribe(loading) - this.vm.outputs.setDefaultState().subscribe(default) + private fun setUpEnvironment() { + vm = AddCardViewHolderViewModel.ViewModel() + this.vm.outputs.setLoadingState().subscribe { loading.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.setDefaultState().subscribe { default.onNext(it) }.addToDisposable(disposables) } @Test fun testLoadingState() { - setUpEnvironment(environment()) + setUpEnvironment() this.vm.inputs.configureWith(State.LOADING) loading.assertValue(State.LOADING) @@ -29,10 +32,15 @@ class AddCardViewHolderViewModelTest : KSRobolectricTestCase() { } fun testDefaultState() { - setUpEnvironment(environment()) + setUpEnvironment() this.vm.inputs.configureWith(State.DEFAULT) default.assertValue(State.DEFAULT) loading.assertNoValues() } + + @After + fun clear() { + disposables.clear() + } } diff --git a/app/src/test/java/com/kickstarter/viewmodels/AddOnViewHolderViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/AddOnViewHolderViewModelTest.kt index 7262c8b160..c2c039283b 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/AddOnViewHolderViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/AddOnViewHolderViewModelTest.kt @@ -5,12 +5,14 @@ import androidx.annotation.NonNull import com.kickstarter.KSRobolectricTestCase import com.kickstarter.R import com.kickstarter.libs.Environment +import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.mock.factories.ProjectDataFactory import com.kickstarter.mock.factories.ProjectFactory import com.kickstarter.mock.factories.RewardFactory import com.kickstarter.models.RewardsItem +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subscribers.TestSubscriber import org.junit.Test -import rx.observers.TestSubscriber class AddOnViewHolderViewModelTest : KSRobolectricTestCase() { private lateinit var vm: AddOnViewHolderViewModel.ViewModel @@ -27,21 +29,22 @@ class AddOnViewHolderViewModelTest : KSRobolectricTestCase() { private val titleForAddOn = TestSubscriber.create>() private val localPickUpIsGone = TestSubscriber.create() private val localPickUpName = TestSubscriber.create() + private val disposables = CompositeDisposable() private fun setUpEnvironment(@NonNull environment: Environment) { this.vm = AddOnViewHolderViewModel.ViewModel(environment) - this.vm.outputs.isAddonTitleGone().subscribe(this.quantityIsGone) - this.vm.outputs.conversion().subscribe(this.conversion) - this.vm.outputs.conversionIsGone().subscribe(this.conversionIsGone) - this.vm.outputs.descriptionForNoReward().subscribe(this.descriptionForNoReward) - this.vm.outputs.descriptionForReward().subscribe(this.descriptionForReward) - this.vm.outputs.rewardItems().subscribe(this.rewardItems) - this.vm.outputs.rewardItemsAreGone().subscribe(this.rewardItemsAreGone) - this.vm.outputs.titleForNoReward().subscribe(this.titleForNoReward) - this.vm.outputs.titleForReward().subscribe(this.titleForReward) - this.vm.outputs.titleForAddOn().subscribe(this.titleForAddOn) - this.vm.outputs.localPickUpIsGone().subscribe(this.localPickUpIsGone) - this.vm.outputs.localPickUpName().subscribe(this.localPickUpName) + this.vm.outputs.isAddonTitleGone().subscribe { this.quantityIsGone.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.conversion().subscribe { this.conversion.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.conversionIsGone().subscribe { this.conversionIsGone.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.descriptionForNoReward().subscribe { this.descriptionForNoReward.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.descriptionForReward().subscribe { this.descriptionForReward.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.rewardItems().subscribe { this.rewardItems.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.rewardItemsAreGone().subscribe { this.rewardItemsAreGone.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.titleForNoReward().subscribe { this.titleForNoReward.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.titleForReward().subscribe { this.titleForReward.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.titleForAddOn().subscribe { this.titleForAddOn.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.localPickUpIsGone().subscribe { this.localPickUpIsGone.onNext(it) }.addToDisposable(disposables) + this.vm.outputs.localPickUpName().subscribe { this.localPickUpName.onNext(it) }.addToDisposable(disposables) } @Test diff --git a/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.java b/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.java deleted file mode 100644 index 06c6fd795f..0000000000 --- a/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.kickstarter.viewmodels; - -import android.content.Intent; - -import com.kickstarter.KSRobolectricTestCase; -import com.kickstarter.libs.CurrentConfigType; -import com.kickstarter.libs.Environment; -import com.kickstarter.mock.MockCurrentConfig; -import com.kickstarter.mock.factories.ApiExceptionFactory; -import com.kickstarter.mock.factories.ConfigFactory; -import com.kickstarter.mock.services.MockApiClient; -import com.kickstarter.services.ApiClientType; -import com.kickstarter.services.apiresponses.AccessTokenEnvelope; -import com.kickstarter.services.apiresponses.ErrorEnvelope; -import com.kickstarter.ui.IntentKey; - -import org.junit.Test; - -import java.util.Collections; - -import androidx.annotation.NonNull; -import rx.Observable; -import rx.observers.TestSubscriber; - -public class FacebookConfimationViewModelTest extends KSRobolectricTestCase { - private FacebookConfirmationViewModel.ViewModel vm; - private final TestSubscriber prefillEmail = new TestSubscriber<>(); - private final TestSubscriber signupError = new TestSubscriber<>(); - private final TestSubscriber signupSuccess = new TestSubscriber<>(); - private final TestSubscriber sendNewslettersIsChecked = new TestSubscriber<>(); - - @Test - public void testPrefillEmail() { - final ErrorEnvelope.FacebookUser facebookUser = ErrorEnvelope.FacebookUser.builder() - .id(1L).name("Test").email("test@kickstarter.com") - .build(); - - this.vm = new FacebookConfirmationViewModel.ViewModel(environment()); - this.vm.intent(new Intent().putExtra(IntentKey.FACEBOOK_USER, facebookUser)); - - this.vm.outputs.prefillEmail().subscribe(this.prefillEmail); - - this.prefillEmail.assertValue("test@kickstarter.com"); - } - - @Test - public void testSignupErrorDisplay() { - final ApiClientType apiClient = new MockApiClient() { - @Override - public @NonNull Observable registerWithFacebook(final @NonNull String fbAccessToken, final boolean sendNewsletters) { - return Observable.error(ApiExceptionFactory.apiError( - ErrorEnvelope.builder().httpCode(404).errorMessages(Collections.singletonList("oh no")).build()) - ); - } - }; - - final Environment environment = environment().toBuilder().apiClient(apiClient).build(); - this.vm = new FacebookConfirmationViewModel.ViewModel(environment); - - this.vm.intent(new Intent().putExtra(IntentKey.FACEBOOK_TOKEN, "token")); - this.vm.outputs.signupError().subscribe(this.signupError); - - this.vm.inputs.sendNewslettersClick(true); - this.vm.inputs.createNewAccountClick(); - - this.signupError.assertValue("oh no"); - } - - @Test - public void testSuccessfulUserCreation() { - final ApiClientType apiClient = new MockApiClient(); - - final Environment environment = environment().toBuilder().apiClient(apiClient).build(); - this.vm = new FacebookConfirmationViewModel.ViewModel(environment); - - this.vm.intent(new Intent().putExtra(IntentKey.FACEBOOK_TOKEN, "token")); - this.vm.outputs.signupSuccess().subscribe(this.signupSuccess); - - this.vm.inputs.sendNewslettersClick(true); - this.vm.inputs.createNewAccountClick(); - - this.signupSuccess.assertValueCount(1); - } - - @Test - public void testToggleSendNewsLetter_isNotChecked() { - final CurrentConfigType currentConfig = new MockCurrentConfig(); - currentConfig.config(ConfigFactory.config().toBuilder().countryCode("US").build()); - final Environment environment = environment().toBuilder().currentConfig(currentConfig).build(); - this.vm = new FacebookConfirmationViewModel.ViewModel(environment); - - this.vm.outputs.sendNewslettersIsChecked().subscribe(this.sendNewslettersIsChecked); - this.sendNewslettersIsChecked.assertValue(false); - - this.vm.inputs.sendNewslettersClick(true); - this.vm.inputs.sendNewslettersClick(false); - - this.sendNewslettersIsChecked.assertValues(false, true, false); - } -} diff --git a/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.kt new file mode 100644 index 0000000000..c39bd9a9f3 --- /dev/null +++ b/app/src/test/java/com/kickstarter/viewmodels/FacebookConfimationViewModelTest.kt @@ -0,0 +1,106 @@ +package com.kickstarter.viewmodels + +import android.content.Intent +import com.kickstarter.KSRobolectricTestCase +import com.kickstarter.libs.CurrentConfigType +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.mock.MockCurrentConfig +import com.kickstarter.mock.factories.ApiExceptionFactory +import com.kickstarter.mock.factories.ConfigFactory.config +import com.kickstarter.mock.services.MockApiClientV2 +import com.kickstarter.services.ApiClientTypeV2 +import com.kickstarter.services.apiresponses.AccessTokenEnvelope +import com.kickstarter.services.apiresponses.ErrorEnvelope +import com.kickstarter.services.apiresponses.ErrorEnvelope.FacebookUser +import com.kickstarter.ui.IntentKey +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subscribers.TestSubscriber +import org.junit.After +import org.junit.Test + +class FacebookConfimationViewModelTest : KSRobolectricTestCase() { + private val prefillEmail = TestSubscriber() + private val signupError = TestSubscriber() + private val signupSuccess = TestSubscriber() + private val sendNewslettersIsChecked = TestSubscriber() + private val disposables = CompositeDisposable() + + @Test + fun testPrefillEmail() { + val facebookUser = FacebookUser.builder() + .id(1L).name("Test").email("test@kickstarter.com") + .build() + + val vm = FacebookConfirmationViewModel.FacebookConfirmationViewModel(environment()) + vm.provideIntent(Intent().putExtra(IntentKey.FACEBOOK_USER, facebookUser)) + + vm.outputs.prefillEmail().subscribe { this.prefillEmail.onNext(it) }.addToDisposable(disposables) + + prefillEmail.assertValue("test@kickstarter.com") + } + + @Test + fun testSignupErrorDisplay() { + val apiClient: ApiClientTypeV2 = object : MockApiClientV2() { + override fun registerWithFacebook( + fbAccessToken: String, + sendNewsletters: Boolean + ): Observable { + return Observable.error( + ApiExceptionFactory.apiError( + ErrorEnvelope.builder().httpCode(404).errorMessages(listOf("oh no")).build() + ) + ) + } + } + + val environment = environment().toBuilder().apiClientV2(apiClient).build() + val vm = FacebookConfirmationViewModel.FacebookConfirmationViewModel(environment) + + vm.outputs.signupError().subscribe { this.signupError.onNext(it) }.addToDisposable(disposables) + + vm.provideIntent(Intent().putExtra(IntentKey.FACEBOOK_TOKEN, "token")) + vm.inputs.sendNewslettersClick(true) + vm.inputs.createNewAccountClick() + + signupError.assertValue("oh no") + } + + @Test + fun testSuccessfulUserCreation() { + val apiClient: ApiClientTypeV2 = MockApiClientV2() + + val environment = environment().toBuilder().apiClientV2(apiClient).build() + val vm = FacebookConfirmationViewModel.FacebookConfirmationViewModel(environment) + + vm.outputs.signupSuccess().subscribe { this.signupSuccess.onNext(it) }.addToDisposable(disposables) + + vm.provideIntent(Intent().putExtra(IntentKey.FACEBOOK_TOKEN, "token")) + vm.inputs.sendNewslettersClick(true) + vm.inputs.createNewAccountClick() + + signupSuccess.assertValueCount(1) + } + + @Test + fun testToggleSendNewsLetter_isNotChecked() { + val currentConfig: CurrentConfigType = MockCurrentConfig() + currentConfig.config(config().toBuilder().countryCode("US").build()) + val environment = environment().toBuilder().currentConfig(currentConfig).build() + val vm = FacebookConfirmationViewModel.FacebookConfirmationViewModel(environment) + + vm.outputs.sendNewslettersIsChecked().subscribe { this.sendNewslettersIsChecked.onNext(it) }.addToDisposable(disposables) + sendNewslettersIsChecked.assertValue(false) + + vm.inputs.sendNewslettersClick(true) + vm.inputs.sendNewslettersClick(false) + + sendNewslettersIsChecked.assertValues(false, true, false) + } + + @After + fun clear() { + disposables.clear() + } +} diff --git a/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.java b/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.java deleted file mode 100644 index 5721e69033..0000000000 --- a/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.kickstarter.viewmodels; - -import com.kickstarter.KSRobolectricTestCase; -import com.kickstarter.libs.Environment; -import com.kickstarter.mock.factories.CategoryFactory; -import com.kickstarter.models.Category; -import com.kickstarter.ui.viewholders.ThanksCategoryHolderViewModel; - -import org.junit.Test; - -import androidx.annotation.NonNull; -import rx.observers.TestSubscriber; - -public final class ThanksCategoryHolderViewModelTest extends KSRobolectricTestCase { - private ThanksCategoryHolderViewModel.ViewModel vm; - private final TestSubscriber categoryName = new TestSubscriber<>(); - private final TestSubscriber notifyDelegateOfCategoryClick = new TestSubscriber<>(); - - protected void setUpEnvironment(final @NonNull Environment environment) { - this.vm = new ThanksCategoryHolderViewModel.ViewModel(environment); - this.vm.getOutputs().categoryName().subscribe(this.categoryName); - this.vm.getOutputs().notifyDelegateOfCategoryClick().subscribe(this.notifyDelegateOfCategoryClick); - } - - @Test - public void testCategoryName() { - final Category category = CategoryFactory.musicCategory(); - setUpEnvironment(environment()); - - this.vm.getInputs().configureWith(category); - this.categoryName.assertValues(category.name()); - } - -// @Test -// public void testCategoryViewClicked() { -// final Category category = CategoryFactory.bluesCategory(); -// setUpEnvironment(environment()); -// -// this.vm.getInputs().configureWith(category); -// this.vm.getInputs().categoryViewClicked(); -// this.notifyDelegateOfCategoryClick.assertValues(category); -// } -} diff --git a/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.kt new file mode 100644 index 0000000000..4fbf91352c --- /dev/null +++ b/app/src/test/java/com/kickstarter/viewmodels/ThanksCategoryHolderViewModelTest.kt @@ -0,0 +1,39 @@ +package com.kickstarter.viewmodels + +import com.kickstarter.KSRobolectricTestCase +import com.kickstarter.libs.Environment +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.mock.factories.CategoryFactory.musicCategory +import com.kickstarter.models.Category +import com.kickstarter.ui.viewholders.ThanksCategoryHolderViewModel +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subscribers.TestSubscriber +import org.junit.After +import org.junit.Test + +class ThanksCategoryHolderViewModelTest : KSRobolectricTestCase() { + private var vm: ThanksCategoryHolderViewModel.ViewModel? = null + private val categoryName = TestSubscriber() + private val notifyDelegateOfCategoryClick = TestSubscriber() + private val disposables = CompositeDisposable() + + private fun setUpEnvironment(environment: Environment) { + this.vm = ThanksCategoryHolderViewModel.ViewModel(environment) + vm!!.outputs.categoryName().subscribe { this.categoryName.onNext(it) }.addToDisposable(disposables) + vm!!.outputs.notifyDelegateOfCategoryClick().subscribe { this.notifyDelegateOfCategoryClick.onNext(it) }.addToDisposable(disposables) + } + + @Test + fun testCategoryName() { + val category = musicCategory() + setUpEnvironment(environment()) + + vm!!.inputs.configureWith(category) + categoryName.assertValues(category.name()) + } + + @After + fun clear() { + disposables.clear() + } +}