Convert the LeaderboardFragment to kotlin

This commit is contained in:
Paul Hawke 2024-11-30 11:46:18 -06:00
parent 755d2c62c2
commit 440eaec5e7
5 changed files with 338 additions and 380 deletions

View file

@ -5,8 +5,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.paging.PageKeyedDataSource
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
@ -23,7 +23,7 @@ class DataSourceClass(
private val limit: Int,
private val offset: Int
) : PageKeyedDataSource<Int, LeaderboardList>() {
val progressLiveStatus: MutableLiveData<String> = MutableLiveData()
val progressLiveStatus: MutableLiveData<LeaderboardConstants.LoadingStatus> = MutableLiveData()
private val compositeDisposable = CompositeDisposable()

View file

@ -19,17 +19,18 @@ object LeaderboardConstants {
*/
const val USER_LINK_PREFIX: String = "https://commons.wikimedia.org/wiki/User:"
/**
* This is the a constant string for the state loading, when the pages are getting loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
const val LOADING: String = "Loading"
/**
* This is the a constant string for the state loaded, when the pages are loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
const val LOADED: String = "Loaded"
sealed class LoadingStatus {
/**
* This is the state loading, when the pages are getting loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
data object LOADING: LoadingStatus()
/**
* This is the state loaded, when the pages are loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
data object LOADED: LoadingStatus()
}
/**
* This API endpoint is to update the leaderboard avatar

View file

@ -1,363 +0,0 @@
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 android.accounts.Account;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.MergeAdapter;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
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.Objects;
import javax.inject.Inject;
import timber.log.Timber;
/**
* This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment
*/
public class LeaderboardFragment extends CommonsDaggerSupportFragment {
@Inject
SessionManager sessionManager;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
ViewModelFactory viewModelFactory;
/**
* View model for the paged leaderboard list
*/
private LeaderboardListViewModel viewModel;
/**
* Composite disposable for API call
*/
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* Duration of the leaderboard API
*/
private String duration;
/**
* Category of the Leaderboard API
*/
private String category;
/**
* Page size of the leaderboard API
*/
private int limit = PAGE_SIZE;
/**
* offset for the leaderboard API
*/
private int offset = START_OFFSET;
/**
* Set initial User Rank to 0
*/
private int userRank;
/**
* This variable represents if user wants to scroll to his rank or not
*/
private boolean scrollToRank;
private String userName;
private FragmentLeaderboardBinding binding;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
userName = getArguments().getString(ProfileActivity.KEY_USERNAME);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentLeaderboardBinding.inflate(inflater, container, false);
hideLayouts();
// Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu
if(ConfigUtils.isBetaFlavour()) {
binding.progressBar.setVisibility(View.GONE);
binding.scroll.setVisibility(View.GONE);
return binding.getRoot();
}
binding.progressBar.setVisibility(View.VISIBLE);
setSpinners();
/**
* This array is for the duration filter, we have three filters weekly, yearly and all-time
* each filter have a key and value pair, the value represents the param of the API
*/
String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values);
/**
* This array is for the category filter, we have three filters upload, used and nearby
* each filter have a key and value pair, the value represents the param of the API
*/
String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values);
duration = durationValues[0];
category = categoryValues[0];
setLeaderboard(duration, category, limit, offset);
binding.durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
duration = durationValues[binding.durationSpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
binding.categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
category = categoryValues[binding.categorySpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
binding.scroll.setOnClickListener(view -> scrollToUserRank());
return binding.getRoot();
}
@Override
public void setMenuVisibility(boolean visible) {
super.setMenuVisibility(visible);
// Whenever this fragment is revealed in a menu,
// notify Beta users the page data is unavailable
if(ConfigUtils.isBetaFlavour() && visible) {
Context ctx = null;
if(getContext() != null) {
ctx = getContext();
} else if(getView() != null && getView().getContext() != null) {
ctx = getView().getContext();
}
if(ctx != null) {
Toast.makeText(ctx,
R.string.leaderboard_unavailable_beta,
Toast.LENGTH_LONG).show();
}
}
}
/**
* Refreshes the leaderboard list
*/
private void refreshLeaderboard() {
scrollToRank = false;
if (viewModel != null) {
viewModel.refresh(duration, category, limit, offset);
setLeaderboard(duration, category, limit, offset);
}
}
/**
* Performs Auto Scroll to the User's Rank
* We use userRank+1 to load one extra user and prevent overlapping of my rank button
* If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top
*/
private void scrollToUserRank() {
if(userRank==0){
Toast.makeText(getContext(),R.string.no_achievements_yet,Toast.LENGTH_SHORT).show();
}else {
if (binding == null) {
return;
}
if (Objects.requireNonNull(binding.leaderboardList.getAdapter()).getItemCount()
> userRank + 1) {
binding.leaderboardList.smoothScrollToPosition(userRank + 1);
} else {
if (viewModel != null) {
viewModel.refresh(duration, category, userRank + 1, 0);
setLeaderboard(duration, category, userRank + 1, 0);
scrollToRank = true;
}
}
}
}
/**
* Set the spinners for the leaderboard filters
*/
private void setSpinners() {
ArrayAdapter<CharSequence> categoryAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_categories, android.R.layout.simple_spinner_item);
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.categorySpinner.setAdapter(categoryAdapter);
ArrayAdapter<CharSequence> durationAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_durations, android.R.layout.simple_spinner_item);
durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.durationSpinner.setAdapter(durationAdapter);
}
/**
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private void setLeaderboard(String duration, String category, int limit, int offset) {
if (checkAccount()) {
try {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(userName),
duration, category, null, null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
userRank = response.getRank();
setViews(response, duration, category, limit, offset);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
onError();
}
));
}
catch (Exception e){
Timber.d(e+"success");
}
}
}
/**
* Set the views
* @param response Leaderboard Response Object
*/
private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) {
viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class);
viewModel.setParams(duration, category, limit, offset);
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter();
UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response);
MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
binding.leaderboardList.setLayoutManager(linearLayoutManager);
binding.leaderboardList.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();
if (scrollToRank) {
binding.leaderboardList.smoothScrollToPosition(userRank + 1);
}
}
});
}
/**
* to hide progressbar
*/
private void hideProgressBar() {
if (binding != null) {
binding.progressBar.setVisibility(View.GONE);
binding.categorySpinner.setVisibility(View.VISIBLE);
binding.durationSpinner.setVisibility(View.VISIBLE);
binding.scroll.setVisibility(View.VISIBLE);
binding.leaderboardList.setVisibility(View.VISIBLE);
}
}
/**
* to show progressbar
*/
private void showProgressBar() {
if (binding != null) {
binding.progressBar.setVisibility(View.VISIBLE);
binding.scroll.setVisibility(View.INVISIBLE);
}
}
/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
binding.categorySpinner.setVisibility(View.INVISIBLE);
binding.durationSpinner.setVisibility(View.INVISIBLE);
binding.leaderboardList.setVisibility(View.INVISIBLE);
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(getActivity());
return false;
}
return true;
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private void onError() {
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
if (binding!=null) {
binding.progressBar.setVisibility(View.GONE);
}
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
binding = null;
}
}

View file

@ -0,0 +1,319 @@
package fr.free.nrw.commons.profile.leaderboard
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.MergeAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.Objects
import javax.inject.Inject
/**
* This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment
*/
class LeaderboardFragment : CommonsDaggerSupportFragment() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
lateinit var okHttpJsonApiClient: OkHttpJsonApiClient
@Inject
lateinit var viewModelFactory: ViewModelFactory
private var viewModel: LeaderboardListViewModel? = null
private var duration: String? = null
private var category: String? = null
private val limit: Int = PAGE_SIZE
private val offset: Int = START_OFFSET
private var userRank = 0
private var scrollToRank = false
private var userName: String? = null
private var binding: FragmentLeaderboardBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { userName = it.getString(ProfileActivity.KEY_USERNAME) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentLeaderboardBinding.inflate(inflater, container, false)
hideLayouts()
// Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu
if (isBetaFlavour) {
binding!!.progressBar.visibility = View.GONE
binding!!.scroll.visibility = View.GONE
return binding!!.root
}
binding!!.progressBar.visibility = View.VISIBLE
setSpinners()
/*
* This array is for the duration filter, we have three filters weekly, yearly and all-time
* each filter have a key and value pair, the value represents the param of the API
*/
val durationValues = requireContext().resources
.getStringArray(R.array.leaderboard_duration_values)
duration = durationValues[0]
/*
* This array is for the category filter, we have three filters upload, used and nearby
* each filter have a key and value pair, the value represents the param of the API
*/
val categoryValues = requireContext().resources
.getStringArray(R.array.leaderboard_category_values)
category = categoryValues[0]
setLeaderboard(duration, category, limit, offset)
with(binding!!) {
durationSpinner.onItemSelectedListener = SelectionListener {
duration = durationValues[durationSpinner.selectedItemPosition]
refreshLeaderboard()
}
categorySpinner.onItemSelectedListener = SelectionListener {
category = categoryValues[categorySpinner.selectedItemPosition]
refreshLeaderboard()
}
scroll.setOnClickListener { scrollToUserRank() }
return root
}
}
override fun setMenuVisibility(visible: Boolean) {
super.setMenuVisibility(visible)
// Whenever this fragment is revealed in a menu,
// notify Beta users the page data is unavailable
if (isBetaFlavour && visible) {
val ctx: Context? = if (context != null) {
context
} else if (view != null && requireView().context != null) {
requireView().context
} else {
null
}
ctx?.let {
Toast.makeText(it, R.string.leaderboard_unavailable_beta, Toast.LENGTH_LONG).show()
}
}
}
/**
* Refreshes the leaderboard list
*/
private fun refreshLeaderboard() {
scrollToRank = false
viewModel?.let {
it.refresh(duration, category, limit, offset)
setLeaderboard(duration, category, limit, offset)
}
}
/**
* Performs Auto Scroll to the User's Rank
* We use userRank+1 to load one extra user and prevent overlapping of my rank button
* If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top
*/
private fun scrollToUserRank() {
if (userRank == 0) {
Toast.makeText(context, R.string.no_achievements_yet, Toast.LENGTH_SHORT).show()
} else {
if (binding == null) {
return
}
val itemCount = binding?.leaderboardList?.adapter?.itemCount ?: 0
if (itemCount > userRank + 1) {
binding!!.leaderboardList.smoothScrollToPosition(userRank + 1)
} else {
viewModel?.let {
it.refresh(duration, category, userRank + 1, 0)
setLeaderboard(duration, category, userRank + 1, 0)
scrollToRank = true
}
}
}
}
/**
* Set the spinners for the leaderboard filters
*/
private fun setSpinners() {
val categoryAdapter = ArrayAdapter.createFromResource(
requireContext(),
R.array.leaderboard_categories, android.R.layout.simple_spinner_item
)
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding!!.categorySpinner.adapter = categoryAdapter
val durationAdapter = ArrayAdapter.createFromResource(
requireContext(),
R.array.leaderboard_durations, android.R.layout.simple_spinner_item
)
durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding!!.durationSpinner.adapter = durationAdapter
}
/**
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private fun setLeaderboard(duration: String?, category: String?, limit: Int, offset: Int) {
if (checkAccount()) {
try {
compositeDisposable.add(
okHttpJsonApiClient.getLeaderboard(
Objects.requireNonNull(userName),
duration, category, null, null
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ response: LeaderboardResponse? ->
if (response != null && response.status == 200) {
userRank = response.rank!!
setViews(response, duration, category, limit, offset)
}
},
{ t: Throwable? ->
Timber.e(t, "Fetching leaderboard statistics failed")
onError()
}
))
} catch (e: Exception) {
Timber.d(e, "success")
}
}
}
/**
* Set the views
* @param response Leaderboard Response Object
*/
private fun setViews(
response: LeaderboardResponse,
duration: String?,
category: String?,
limit: Int,
offset: Int
) {
viewModel = ViewModelProvider(this, viewModelFactory).get(
LeaderboardListViewModel::class.java
)
viewModel!!.setParams(duration, category, limit, offset)
val leaderboardListAdapter = LeaderboardListAdapter()
val userDetailAdapter = UserDetailAdapter(response)
val mergeAdapter = MergeAdapter(userDetailAdapter, leaderboardListAdapter)
val linearLayoutManager = LinearLayoutManager(context)
binding!!.leaderboardList.layoutManager = linearLayoutManager
binding!!.leaderboardList.adapter = mergeAdapter
viewModel!!.listLiveData.observe(viewLifecycleOwner, leaderboardListAdapter::submitList)
viewModel!!.progressLoadStatus.observe(viewLifecycleOwner) { status ->
when (status) {
LOADING -> {
showProgressBar()
}
LOADED -> {
hideProgressBar()
if (scrollToRank) {
binding!!.leaderboardList.smoothScrollToPosition(userRank + 1)
}
}
}
}
}
/**
* to hide progressbar
*/
private fun hideProgressBar() = binding?.let {
it.progressBar.visibility = View.GONE
it.categorySpinner.visibility = View.VISIBLE
it.durationSpinner.visibility = View.VISIBLE
it.scroll.visibility = View.VISIBLE
it.leaderboardList.visibility = View.VISIBLE
}
/**
* to show progressbar
*/
private fun showProgressBar() = binding?.let {
it.progressBar.visibility = View.VISIBLE
it.scroll.visibility = View.INVISIBLE
}
/**
* used to hide the layouts while fetching results from api
*/
private fun hideLayouts() = binding?.let {
it.categorySpinner.visibility = View.INVISIBLE
it.durationSpinner.visibility = View.INVISIBLE
it.leaderboardList.visibility = View.INVISIBLE
}
/**
* check to ensure that user is logged in
*/
private fun checkAccount() = if (sessionManager.currentAccount == null) {
Timber.d("Current account is null")
showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in))
sessionManager.forceLogin(requireActivity())
false
} else {
true
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private fun onError() {
showLongToast(requireActivity(), resources.getString(R.string.error_occurred))
binding?.let { it.progressBar.visibility = View.GONE }
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
binding = null
}
private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) =
handler()
override fun onNothingSelected(p0: AdapterView<*>?) = Unit
}
}

View file

@ -2,11 +2,13 @@ package fr.free.nrw.commons.profile.leaderboard
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus
import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE
/**
@ -26,9 +28,8 @@ class LeaderboardListViewModel(
.setPageSize(PAGE_SIZE).build()
).build()
val progressLoadStatus: LiveData<String> = dataSourceFactory.mutableLiveData.switchMap {
it.progressLiveStatus
}
val progressLoadStatus: LiveData<LoadingStatus> =
dataSourceFactory.mutableLiveData.switchMap { it.progressLiveStatus }
/**
* Refreshes the paged list with the new params and starts the loading of new data