[GSoC] Added Pagination to Leaderboard (#3881)

* Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen.

* Fixed Bug - missing data in landscape mode

* Added Unit Tests for Leaderboard

* Added JavaDocs

* Updated JavaDocs

* Added Pagination

* Added Merge Adapter

* Fixed Test Case

* Added Smooth Scroll

* Added Progress Bar for Paging

* Fixed Gradle
This commit is contained in:
Madhur Gupta 2020-07-29 05:59:33 +05:30 committed by GitHub
parent 5877d7a14a
commit 5f77f610f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 535 additions and 215 deletions

View file

@ -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"
@ -210,8 +211,8 @@ android {
configurations.all {
resolutionStrategy.force 'androidx.annotation:annotation:1.0.2'
exclude module: 'okhttp-ws'
}
flavorDimensions 'tier'
productFlavors {
prod {

View file

@ -3,14 +3,14 @@ package fr.free.nrw.commons.mwapi;
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;
@ -65,32 +65,32 @@ public class OkHttpJsonApiClient {
}
@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 {
@ -99,7 +99,7 @@ public class OkHttpJsonApiClient {
return new LeaderboardResponse();
}
}
return null;
return new LeaderboardResponse();
});
}
@ -188,7 +188,6 @@ public class OkHttpJsonApiClient {
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();

View file

@ -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))
.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);
}
));
}
}

View file

@ -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;
}
}

View file

@ -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";
}

View file

@ -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;
@ -21,25 +23,12 @@ import fr.free.nrw.commons.utils.ViewUtil;
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;
@ -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();
@ -67,12 +59,12 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
}
/**
* 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)
@ -81,8 +73,7 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
setLeaderboardUser(response);
setLeaderboardList(response.getLeaderboardList());
setViews(response);
}
},
t -> {
@ -101,20 +92,23 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
* 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();
}
});
}
/**
@ -123,20 +117,23 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
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);
}

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
@ -8,12 +11,15 @@ public class LeaderboardList {
@SerializedName("username")
@Expose
private String username;
@SerializedName("category_count")
@Expose
private Integer categoryCount;
@SerializedName("avatar")
@Expose
private String avatar;
@SerializedName("rank")
@Expose
private Integer rank;
@ -50,4 +56,29 @@ public class LeaderboardList {
this.rank = rank;
}
public static DiffUtil.ItemCallback<LeaderboardList> DIFF_CALLBACK =
new ItemCallback<LeaderboardList>() {
@Override
public boolean areItemsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem == oldItem;
}
@Override
public boolean areContentsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem.getRank().equals(oldItem.getRank());
}
};
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
LeaderboardList leaderboardList = (LeaderboardList) obj;
return leaderboardList.getRank().equals(this.getRank());
}
}

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.AVATAR_SOURCE_URL;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
@ -7,16 +9,16 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import java.util.List;
public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardListAdapter.ListViewHolder> {
public class LeaderboardListAdapter extends PagedListAdapter<LeaderboardList, LeaderboardListAdapter.ListViewHolder> {
private List<LeaderboardList> leaderboardList;
private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
protected LeaderboardListAdapter() {
super(LeaderboardList.DIFF_CALLBACK);
}
public class ListViewHolder extends RecyclerView.ViewHolder {
TextView rank;
@ -41,10 +43,6 @@ public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardList
}
}
public LeaderboardListAdapter(List<LeaderboardList> leaderboardList) {
this.leaderboardList = leaderboardList;
}
/**
* Overrides the onCreateViewHolder and inflates the recyclerview list item layout
* @param parent
@ -72,21 +70,12 @@ public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardList
TextView username = holder.username;
TextView count = holder.count;
rank.setText(leaderboardList.get(position).getRank().toString());
rank.setText(getItem(position).getRank().toString());
avatar.setImageURI(
Uri.parse(String.format(avatarSourceURL, leaderboardList.get(position).getAvatar(),
leaderboardList.get(position).getAvatar())));
username.setText(leaderboardList.get(position).getUsername());
count.setText(leaderboardList.get(position).getCategoryCount().toString());
}
/**
* Override the getItemCount method
* @return the size of the recycler view list
*/
@Override
public int getItemCount() {
return leaderboardList.size();
Uri.parse(String.format(AVATAR_SOURCE_URL, getItem(position).getAvatar(),
getItem(position).getAvatar())));
username.setText(getItem(position).getUsername());
count.setText(getItem(position).getCategoryCount().toString());
}
}

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
public class LeaderboardListViewModel extends ViewModel {
private DataSourceFactory dataSourceFactory;
private LiveData<PagedList<LeaderboardList>> listLiveData;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private LiveData<String> progressLoadStatus = new MutableLiveData<>();
public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) {
dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient, compositeDisposable, sessionManager);
initializePaging();
}
private void initializePaging() {
PagedList.Config pagedListConfig =
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE).build();
listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig)
.build();
progressLoadStatus = Transformations
.switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus);
}
public LiveData<String> getProgressLoadStatus() {
return progressLoadStatus;
}
public LiveData<PagedList<LeaderboardList>> getListLiveData() {
return listLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
}

View file

@ -9,30 +9,39 @@ public class LeaderboardResponse {
@SerializedName("status")
@Expose
private Integer status;
@SerializedName("username")
@Expose
private String username;
@SerializedName("category_count")
@Expose
private Integer categoryCount;
@SerializedName("limit")
@Expose
private Object limit;
private int limit;
@SerializedName("avatar")
@Expose
private String avatar;
@SerializedName("offset")
@Expose
private Object offset;
private int offset;
@SerializedName("duration")
@Expose
private String duration;
@SerializedName("leaderboard_list")
@Expose
private List<LeaderboardList> leaderboardList = null;
@SerializedName("category")
@Expose
private String category;
@SerializedName("rank")
@Expose
private Integer rank;
@ -61,11 +70,11 @@ public class LeaderboardResponse {
this.categoryCount = categoryCount;
}
public Object getLimit() {
public int getLimit() {
return limit;
}
public void setLimit(Object limit) {
public void setLimit(int limit) {
this.limit = limit;
}
@ -77,11 +86,11 @@ public class LeaderboardResponse {
this.avatar = avatar;
}
public Object getOffset() {
public int getOffset() {
return offset;
}
public void setOffset(Object offset) {
public void setOffset(int offset) {
this.offset = offset;
}

View file

@ -0,0 +1,78 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.AVATAR_SOURCE_URL;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
public class UserDetailAdapter extends RecyclerView.Adapter<UserDetailAdapter.DataViewHolder> {
LeaderboardResponse leaderboardResponse;
public UserDetailAdapter(LeaderboardResponse leaderboardResponse) {
this.leaderboardResponse = leaderboardResponse;
}
public class DataViewHolder extends RecyclerView.ViewHolder {
TextView rank;
SimpleDraweeView avatar;
TextView username;
TextView count;
public DataViewHolder(@NonNull View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.rank);
this.avatar = itemView.findViewById(R.id.avatar);
this.username = itemView.findViewById(R.id.username);
this.count = itemView.findViewById(R.id.count);
}
public Context getContext() {
return itemView.getContext();
}
}
@NonNull
@Override
public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_user_element, parent, false);
return new DataViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.rank_prefix),
leaderboardResponse.getRank()));
avatar.setImageURI(
Uri.parse(String.format(AVATAR_SOURCE_URL, leaderboardResponse.getAvatar(),
leaderboardResponse.getAvatar())));
username.setText(leaderboardResponse.getUsername());
count.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.count_prefix),
leaderboardResponse.getCategoryCount()));
}
@Override
public int getItemCount() {
return 1;
}
}

View file

@ -0,0 +1,30 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import javax.inject.Inject;
public class ViewModelFactory implements ViewModelProvider.Factory {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
@Inject
public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) {
return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager);
}
throw new IllegalArgumentException("Unknown class name");
}
}

View file

@ -1,116 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/leaderboard_list"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/leaderboard_list" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar" />
<TextView
android:id="@+id/rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rank" />
<LinearLayout
android:id="@+id/column_names"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="15dp"
android:weightSum="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/count">
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_rank"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:text="@string/leaderboard_column_user"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_count"
android:layout_height="match_parent"/>
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/column_names">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/leaderboard_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</androidx.core.widget.NestedScrollView>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,18 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:weightSum="1">
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:weightSum="1">
<TextView
android:id="@+id/user_rank"
@ -51,5 +47,4 @@
android:layout_weight="0.2"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar" />
<TextView
android:id="@+id/rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rank" />
<LinearLayout
android:id="@+id/column_names"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="15dp"
android:weightSum="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/count">
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_rank"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:text="@string/leaderboard_column_user"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_count"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -8,6 +8,7 @@ import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import media
import org.junit.Before
@ -56,7 +57,7 @@ class ReasonBuilderTest {
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
`when`(okHttpJsonApiClient!!.getLeaderboard(anyString(), anyString(), anyString(), anyString(), anyString()))
.thenReturn(Single.just(mock(LeaderboardResponse::class.java)))
.thenReturn(Observable.just(mock(LeaderboardResponse::class.java)))
val media = media(filename="test_file", dateUploaded = Date())