Skip to content

Commit

Permalink
Pull to refresh on discovery feed (#443)
Browse files Browse the repository at this point in the history
* Added swipe to refresh WITH TESTS.
Created a swipe refresher for BaseFragments. A little copy pasta.

* Cleaning up some copy pasta.

* android x updates

* Added event tracking and updated tests.
  • Loading branch information
eoji committed Jan 24, 2019
1 parent 845082c commit 69a77e7
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 14 deletions.
8 changes: 8 additions & 0 deletions app/src/main/java/com/kickstarter/libs/Koala.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ public void trackDiscoveryFilterSelected(final @NonNull DiscoveryParams params)
this.client.track("Discover Modal Selected Filter", KoalaUtils.discoveryParamsProperties(params));
}

public void trackDiscoveryRefreshTriggered() {
this.client.track(KoalaEvent.TRIGGERED_REFRESH, new HashMap<String, Object>() {
{
put("type", "swipe");
}
});
}

/**
* Tracks a project show event.
*
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/kickstarter/libs/KoalaEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ private KoalaEvent() {}
public static final String SIGNUP_NEWSLETTER_TOGGLE = "Signup Newsletter Toggle";
public static final String STARRED_PROJECT = "Starred Project";
public static final String SWITCHED_PROJECTS = "Switched Projects";
public static final String TRIGGERED_REFRESH = "Triggered Refresh";
public static final String TWO_FACTOR_AUTH_CONFIRM_VIEW = "Two-factor Authentication Confirm View";
public static final String TWO_FACTOR_AUTH_RESEND_CODE = "Two-factor Authentication Resend Code";
public static final String UNSTARRED_PROJECT = "Unstarred Project";
Expand Down
34 changes: 33 additions & 1 deletion app/src/main/java/com/kickstarter/libs/SwipeRefresher.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public SwipeRefresher(final @NonNull BaseActivity<? extends ActivityViewModel> a
final @NonNull Func0<Observable<Boolean>> isRefreshing) {

// Iterate through colors in loading spinner while waiting for refresh
layout.setColorSchemeResources(R.color.ksr_green_500, R.color.ksr_green_700, R.color.ksr_green_800);
setColorSchemeResources(layout);

// Emits when user has signaled to refresh layout
RxSwipeRefreshLayout.refreshes(layout)
Expand All @@ -39,4 +39,36 @@ public SwipeRefresher(final @NonNull BaseActivity<? extends ActivityViewModel> a
.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<? extends FragmentViewModel> fragment,
final @NonNull SwipeRefreshLayout layout,
final @NonNull Action0 refreshAction,
final @NonNull Func0<Observable<Boolean>> 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.ksr_green_500, R.color.ksr_green_700, R.color.ksr_green_800);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.kickstarter.libs.BaseFragment;
import com.kickstarter.libs.RecyclerViewPaginator;
import com.kickstarter.libs.RefTag;
import com.kickstarter.libs.SwipeRefresher;
import com.kickstarter.libs.qualifiers.RequiresFragmentViewModel;
import com.kickstarter.libs.utils.AnimationUtils;
import com.kickstarter.libs.utils.ViewUtils;
Expand All @@ -37,6 +38,7 @@
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import butterknife.Bind;
import butterknife.ButterKnife;

Expand All @@ -54,6 +56,7 @@ public final class DiscoveryFragment extends BaseFragment<DiscoveryFragmentViewM
protected @Bind(R.id.discovery_empty_view) View emptyView;
protected @Bind(R.id.discovery_hearts_container) View heartsContainer;
protected @Bind(R.id.discovery_recycler_view) RecyclerView recyclerView;
protected @Bind(R.id.discovery_swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;

public DiscoveryFragment() {}

Expand All @@ -76,6 +79,9 @@ public DiscoveryFragment() {}
this.recyclerView.setAdapter(adapter);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this.recyclerView.getContext());
this.recyclerView.setLayoutManager(layoutManager);
new SwipeRefresher(
this, this.swipeRefreshLayout, this.viewModel.inputs::refresh, this.viewModel.outputs::isFetchingProjects
);
this.recyclerViewPaginator = new RecyclerViewPaginator(this.recyclerView, this.viewModel.inputs::nextPage);

this.viewModel.outputs.activity()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import com.kickstarter.ui.viewholders.ActivitySampleProjectViewHolder;
import com.kickstarter.ui.viewholders.DiscoveryOnboardingViewHolder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

Expand All @@ -44,6 +43,7 @@
import static com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair;
import static com.kickstarter.libs.rx.transformers.Transformers.neverError;
import static com.kickstarter.libs.rx.transformers.Transformers.takePairWhen;
import static com.kickstarter.libs.rx.transformers.Transformers.takeWhen;
import static com.kickstarter.libs.utils.BooleanUtils.isTrue;

public interface DiscoveryFragmentViewModel {
Expand All @@ -61,6 +61,9 @@ interface Inputs extends DiscoveryAdapter.Delegate {
/** Call when params from Discovery Activity change. */
void paramsFromActivity(final DiscoveryParams params);

/** Call when the projects should be refreshed. */
void refresh();

/** Call when we should load the root categories. */
void rootCategories(final List<Category> rootCategories);
}
Expand All @@ -69,6 +72,9 @@ interface Outputs {
/** Emits an activity for the activity sample view. */
Observable<Activity> activity();

/** Emits a boolean indicating whether projects are being fetched from the API. */
Observable<Boolean> isFetchingProjects();

/** Emits a list of projects to display.*/
Observable<List<Pair<Project, DiscoveryParams>>> projectList();

Expand Down Expand Up @@ -118,10 +124,15 @@ public ViewModel(final @NonNull Environment environment) {
(__, params) -> params
);

final Observable<DiscoveryParams> startOverWith = Observable.merge(
selectedParams,
selectedParams.compose(takeWhen(this.refresh))
);

final ApiPaginator<Project, DiscoverEnvelope, DiscoveryParams> paginator =
ApiPaginator.<Project, DiscoverEnvelope, DiscoveryParams>builder()
.nextPage(this.nextPage)
.startOverWith(selectedParams)
.startOverWith(startOverWith)
.envelopeToListOfData(DiscoverEnvelope::projects)
.envelopeToMoreUrl(env -> env.urls().api().moreProjects())
.loadWithParams(this.apiClient::fetchProjects)
Expand All @@ -130,6 +141,10 @@ public ViewModel(final @NonNull Environment environment) {
.concater(ListUtils::concatDistinct)
.build();

paginator.isFetching()
.compose(bindToLifecycle())
.subscribe(this.isFetchingProjects);

final Observable<Pair<Project, RefTag>> activitySampleProjectClick = this.activitySampleProjectClick
.map(p -> Pair.create(p, RefTag.activitySample()));

Expand Down Expand Up @@ -215,14 +230,10 @@ public ViewModel(final @NonNull Environment environment) {
.filter(ObjectUtils::isNotNull)
.compose(bindToLifecycle())
.subscribe(p -> this.koala.trackViewedUpdate(p, KoalaContext.Update.ACTIVITY_SAMPLE));
}

private List<Pair<Project, DiscoveryParams>> combineProjectsAndParams(final @NonNull List<Project> projects, final @NonNull DiscoveryParams params) {
final ArrayList<Pair<Project, DiscoveryParams>> projectAndParams = new ArrayList<>(projects.size());
for (int i = 0; i < projects.size(); i++) {
projectAndParams.add(Pair.create(projects.get(i), params));
}
return projectAndParams;
this.refresh
.compose(bindToLifecycle())
.subscribe(v -> this.koala.trackDiscoveryRefreshTriggered());
}

private boolean activityHasNotBeenSeen(final @Nullable Activity activity) {
Expand Down Expand Up @@ -261,10 +272,12 @@ private void saveLastSeenActivityId(final @Nullable Activity activity) {
private final PublishSubject<Void> nextPage = PublishSubject.create();
private final PublishSubject<DiscoveryParams> paramsFromActivity = PublishSubject.create();
private final PublishSubject<Project> projectCardClicked = PublishSubject.create();
private final PublishSubject<Void> refresh = PublishSubject.create();
private final PublishSubject<List<Category>> rootCategories = PublishSubject.create();

private final BehaviorSubject<Activity> activity = BehaviorSubject.create();
private final BehaviorSubject<Void> heartContainerClicked = BehaviorSubject.create();
private final BehaviorSubject<Boolean> isFetchingProjects = BehaviorSubject.create();
private final BehaviorSubject<List<Pair<Project, DiscoveryParams>>> projectList = BehaviorSubject.create();
private final Observable<Boolean> showActivityFeed;
private final Observable<Boolean> showLoginTout;
Expand Down Expand Up @@ -301,6 +314,9 @@ private void saveLastSeenActivityId(final @Nullable Activity activity) {
@Override public void projectCardViewHolderClicked(final @NonNull Project project) {
this.projectCardClicked.onNext(project);
}
@Override public void refresh() {
this.refresh.onNext(null);
}
@Override public void rootCategories(final @NonNull List<Category> rootCategories) {
this.rootCategories.onNext(rootCategories);
}
Expand All @@ -323,6 +339,9 @@ private void saveLastSeenActivityId(final @Nullable Activity activity) {
@Override public @NonNull Observable<Activity> activity() {
return this.activity;
}
@Override public @NonNull Observable<Boolean> isFetchingProjects() {
return this.isFetchingProjects;
}
@Override public @NonNull Observable<List<Pair<Project, DiscoveryParams>>> projectList() {
return this.projectList;
}
Expand Down
15 changes: 11 additions & 4 deletions app/src/main/res/layout/fragment_discovery.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/discovery_recycler_view"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/discovery_swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/project_card_view" />
android:layout_height="wrap_content">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/discovery_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/project_card_view" />

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

<LinearLayout
android:id="@+id/discovery_empty_view"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ private void setUpInitialHomeAllProjectsParams() {
this.vm.inputs.rootCategories(CategoryFactory.rootCategories());
}

@Test
public void testRefresh() {
setUpEnvironment(environment());

// Load initial params and root categories from activity.
setUpInitialHomeAllProjectsParams();

// Should emit current fragment's projects.
this.hasProjects.assertValues(true);
this.koalaTest.assertValues("Discover List View");

//Page is cleared and refreshed
this.vm.inputs.refresh();
this.hasProjects.assertValues(true, false, true);
this.koalaTest.assertValues("Discover List View", "Triggered Refresh");
}

@Test
public void testProjectsEmitWithNewCategoryParams() {
setUpEnvironment(environment());
Expand Down

0 comments on commit 69a77e7

Please sign in to comment.