Skip to content

[GSoC] Added Pagination to Leaderboard #3881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 29, 2020
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
Expand Down Expand Up @@ -210,8 +211,8 @@ android {

configurations.all {
resolutionStrategy.force 'androidx.annotation:annotation:1.0.2'
exclude module: 'okhttp-ws'
}

flavorDimensions 'tier'
productFlavors {
prod {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import fr.free.nrw.commons.profile.achievements.FeaturedImages;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.profile.achievements.FeaturedImages;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
Expand Down Expand Up @@ -65,32 +65,32 @@ public OkHttpJsonApiClient(OkHttpClient okHttpClient,
}

@NonNull
public Single<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ "/leaderboard.py";
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
Expand All @@ -99,7 +99,7 @@ public Single<LeaderboardResponse> getLeaderboard(String userName, String durati
return new LeaderboardResponse();
}
}
return null;
return new LeaderboardResponse();
});
}

Expand Down Expand Up @@ -188,7 +188,6 @@ public Single<FeedbackResponse> getAchievements(String userName) {
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package fr.free.nrw.commons.profile.leaderboard;

import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET;

import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.PageKeyedDataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Objects;
import timber.log.Timber;

public class DataSourceClass extends PageKeyedDataSource<Integer, LeaderboardList> {

private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
private MutableLiveData<String> progressLiveStatus;
private CompositeDisposable compositeDisposable = new CompositeDisposable();

public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
progressLiveStatus = new MutableLiveData<>();
}


public MutableLiveData<String> getProgressLiveStatus() {
return progressLiveStatus;
}

@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
@NonNull LoadInitialCallback<Integer, LeaderboardList> callback) {

compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
"all_time", "upload", String.valueOf(PAGE_SIZE), String.valueOf(START_OFFSET))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define these as constants.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to move these as constants when I implement the filters, will it be okay if we leave them as hardcoded for now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK for now if you don't forget to fix them soon :-)

.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), null, response.getLimit());
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));

}

@Override
public void loadBefore(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {

}

@Override
public void loadAfter(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
"all_time", "upload", String.valueOf(PAGE_SIZE), String.valueOf(params.key))
.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), params.key + PAGE_SIZE);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.free.nrw.commons.profile.leaderboard;

import androidx.lifecycle.MutableLiveData;
import androidx.paging.DataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;

public class DataSourceFactory extends DataSource.Factory<Integer, LeaderboardList> {

private MutableLiveData<DataSourceClass> liveData;
private OkHttpJsonApiClient okHttpJsonApiClient;
private CompositeDisposable compositeDisposable;
private SessionManager sessionManager;

public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable,
SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.compositeDisposable = compositeDisposable;
this.sessionManager = sessionManager;
liveData = new MutableLiveData<>();
}

public MutableLiveData<DataSourceClass> getMutableLiveData() {
return liveData;
}

@Override
public DataSource<Integer, LeaderboardList> create() {
DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager);
liveData.postValue(dataSourceClass);
return dataSourceClass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package fr.free.nrw.commons.profile.leaderboard;

public class LeaderboardConstants {

public static final int PAGE_SIZE = 10;

public static final int START_OFFSET = 0;

public static final String AVATAR_SOURCE_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";

public final static String LOADING = "Loading";

public final static String LOADED = "Loaded";

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package fr.free.nrw.commons.profile.leaderboard;

import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;

import android.accounts.Account;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.MergeAdapter;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
Expand All @@ -21,25 +23,12 @@
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
import timber.log.Timber;

public class LeaderboardFragment extends CommonsDaggerSupportFragment {

@BindView(R.id.avatar)
SimpleDraweeView avatar;

@BindView(R.id.username)
TextView username;

@BindView(R.id.rank)
TextView rank;

@BindView(R.id.count)
TextView count;

@BindView(R.id.leaderboard_list)
RecyclerView leaderboardListRecyclerView;

Expand All @@ -52,7 +41,10 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;

private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
@Inject
ViewModelFactory viewModelFactory;

LeaderboardListViewModel viewModel;

private CompositeDisposable compositeDisposable = new CompositeDisposable();

Expand All @@ -67,12 +59,12 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
}

/**
* To call the API to get results in form Single<JSONObject>
* which then calls parseJson when results are fetched
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private void setLeaderboard() {
if (checkAccount()) {
try{
try {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
"all_time", "upload", null, null)
Expand All @@ -81,8 +73,7 @@ private void setLeaderboard() {
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
setLeaderboardUser(response);
setLeaderboardList(response.getLeaderboardList());
setViews(response);
}
},
t -> {
Expand All @@ -101,20 +92,23 @@ private void setLeaderboard() {
* Set the views
* @param response Leaderboard Response Object
*/
private void setLeaderboardUser(LeaderboardResponse response) {
hideProgressBar();
avatar.setImageURI(
Uri.parse(String.format(avatarSourceURL, response.getAvatar(), response.getAvatar())));
username.setText(response.getUsername());
rank.setText(String.format("%s %d", getString(R.string.rank_prefix), response.getRank()));
count.setText(String.format("%s %d", getString(R.string.count_prefix), response.getCategoryCount()));
}

private void setLeaderboardList(List<LeaderboardList> leaderboardList) {
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(leaderboardList);
private void setViews(LeaderboardResponse response) {
viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class);
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter();
UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response);
MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
leaderboardListRecyclerView.setLayoutManager(linearLayoutManager);
leaderboardListRecyclerView.setAdapter(leaderboardListAdapter);
leaderboardListRecyclerView.setAdapter(mergeAdapter);

viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList);
viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> {
if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) {
showProgressBar();
} else if (status.equalsIgnoreCase(LOADED)) {
hideProgressBar();
}
});
}

/**
Expand All @@ -123,20 +117,23 @@ private void setLeaderboardList(List<LeaderboardList> leaderboardList) {
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
avatar.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
rank.setVisibility(View.VISIBLE);
leaderboardListRecyclerView.setVisibility(View.VISIBLE);
}
}

/**
* to show progressbar
*/
private void showProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
}
}

/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
avatar.setVisibility(View.INVISIBLE);
username.setVisibility(View.INVISIBLE);
rank.setVisibility(View.INVISIBLE);
leaderboardListRecyclerView.setVisibility(View.INVISIBLE);
}

Expand Down
Loading