#3760 Convert SearchCategoriesFragment to use Pagination (#3770)

This commit is contained in:
Seán Mac Gillicuddy 2020-06-10 13:57:13 +01:00 committed by GitHub
parent 63ab4a25aa
commit f4d81eb4ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 956 additions and 685 deletions

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.explore
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.MergeAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.utils.ViewUtil
import kotlinx.android.synthetic.main.fragment_search_paginated.*
abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
SearchFragmentContract.View<T> {
abstract val pagedListAdapter: PagedListAdapter<T,*>
abstract val injectedPresenter: SearchFragmentContract.Presenter<T>
abstract val emptyTemplateTextId: Int
private val loadingAdapter by lazy { FooterAdapter { injectedPresenter.retryFailedRequest() } }
private val mergeAdapter by lazy { MergeAdapter(pagedListAdapter, loadingAdapter) }
private var searchResults: LiveData<PagedList<T>>? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_search_paginated, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
paginatedSearchResultsList.apply {
layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2)
adapter = mergeAdapter
}
injectedPresenter.listFooterData.observe(
viewLifecycleOwner,
Observer(loadingAdapter::submitList)
)
}
override fun observeSearchResults(searchResults: LiveData<PagedList<T>>) {
this.searchResults?.removeObservers(viewLifecycleOwner)
this.searchResults = searchResults
searchResults.observe(viewLifecycleOwner, Observer {
pagedListAdapter.submitList(it)
contentNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE
})
}
override fun onAttach(context: Context) {
super.onAttach(context)
injectedPresenter.onAttachView(this)
}
override fun onDetach() {
super.onDetach()
injectedPresenter.onDetachView()
}
override fun setEmptyViewText(query: String) {
contentNotFound.text = getString(emptyTemplateTextId, query)
}
override fun hideInitialLoadProgress() {
paginatedSearchInitialLoadProgress.visibility = View.GONE
}
override fun showInitialLoadInProgress() {
paginatedSearchInitialLoadProgress.visibility = View.VISIBLE
}
override fun showSnackbar() {
ViewUtil.showShortSnackbar(paginatedSearchResultsList, R.string.error_loading_depictions)
}
fun onQueryUpdated(query: String) {
injectedPresenter.onQueryUpdated(query)
}
}
private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT
private val Fragment.orientation get() = activity!!.resources.configuration.orientation

View file

@ -0,0 +1,69 @@
package fr.free.nrw.commons.explore
import androidx.lifecycle.MutableLiveData
import fr.free.nrw.commons.upload.depicts.proxy
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
abstract class BaseSearchPresenter<T>(
val mainThreadScheduler: Scheduler,
val pageableDataSource: PageableDataSource<T>
) : SearchFragmentContract.Presenter<T> {
private val DUMMY: SearchFragmentContract.View<T> = proxy()
private var view: SearchFragmentContract.View<T> = DUMMY
private var currentQuery: String? = null
private val compositeDisposable = CompositeDisposable()
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
override fun onAttachView(view: SearchFragmentContract.View<T>) {
this.view = view
compositeDisposable.addAll(
pageableDataSource.searchResults.subscribe(view::observeSearchResults),
pageableDataSource.loadingStates
.observeOn(mainThreadScheduler)
.subscribe(::onLoadingState, Timber::e),
pageableDataSource.noItemsLoadedEvent.subscribe {
setEmptyViewText()
}
)
}
private fun onLoadingState(it: LoadingState) = when (it) {
LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem))
LoadingState.Complete -> {
listFooterData.postValue(emptyList())
view.hideInitialLoadProgress()
}
LoadingState.InitialLoad -> view.showInitialLoadInProgress()
LoadingState.Error -> {
setEmptyViewText()
view.showSnackbar()
view.hideInitialLoadProgress()
listFooterData.postValue(listOf(FooterItem.RefreshItem))
}
}
private fun setEmptyViewText() {
currentQuery?.let(view::setEmptyViewText)
}
override fun retryFailedRequest() {
pageableDataSource.retryFailedRequest()
}
override fun onDetachView() {
view = DUMMY
compositeDisposable.clear()
}
override fun onQueryUpdated(query: String) {
currentQuery = query
pageableDataSource.onQueryUpdated(query)
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.explore
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder<T>(override val containerView: View) :
RecyclerView.ViewHolder(containerView), LayoutContainer {
abstract fun bind(item: T)
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.explore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.list_item_load_more.*
class FooterAdapter(private val onRefreshClicked: () -> Unit) :
ListAdapter<FooterItem, FooterViewHolder>(object :
DiffUtil.ItemCallback<FooterItem>() {
override fun areItemsTheSame(oldItem: FooterItem, newItem: FooterItem) = oldItem == newItem
override fun areContentsTheSame(oldItem: FooterItem, newItem: FooterItem) =
oldItem == newItem
}) {
override fun getItemViewType(position: Int): Int {
return getItem(position).ordinal
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (FooterItem.values()[viewType]) {
FooterItem.LoadingItem -> LoadingViewHolder(parent.inflate(R.layout.list_item_progress))
FooterItem.RefreshItem -> RefreshViewHolder(
parent.inflate(R.layout.list_item_load_more),
onRefreshClicked
)
}
override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {}
}
open class FooterViewHolder(override val containerView: View) :
RecyclerView.ViewHolder(containerView),
LayoutContainer
class LoadingViewHolder(containerView: View) : FooterViewHolder(containerView)
class RefreshViewHolder(containerView: View, onRefreshClicked: () -> Unit) :
FooterViewHolder(containerView) {
init {
listItemLoadMoreButton.setOnClickListener { onRefreshClicked() }
}
}
enum class FooterItem { LoadingItem, RefreshItem }
fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutId, this, attachToRoot)

View file

@ -125,7 +125,7 @@ public class SearchActivity extends NavigationBaseActivity
}
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment.updateCategoryList(query.toString());
searchCategoryFragment.onQueryUpdated(query.toString());
}
} else {

View file

@ -0,0 +1,14 @@
package fr.free.nrw.commons.explore
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract
import fr.free.nrw.commons.explore.categories.PageableCategoriesDataSource
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
class SearchCategoriesFragmentPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesDataSource
) : BaseSearchPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentContract.Presenter

View file

@ -0,0 +1,67 @@
package fr.free.nrw.commons.explore
import androidx.paging.PositionalDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import io.reactivex.Completable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
abstract class SearchDataSource<T>(
private val loadingStates: PublishProcessor<LoadingState>
) : PositionalDataSource<T>() {
private var lastExecutedRequest: (() -> Boolean)? = null
private fun storeAndExecute(function: () -> Boolean) {
function.also { lastExecutedRequest = it }.invoke()
}
private fun performWithTryCatch(function: () -> Unit) = try {
function.invoke()
loadingStates.offer(LoadingState.Complete)
} catch (e: Exception) {
Timber.e(e)
loadingStates.offer(LoadingState.Error)
}
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<T>
) {
storeAndExecute {
loadingStates.offer(LoadingState.InitialLoad)
performWithTryCatch {
callback.onResult(
getItems(params.requestedLoadSize, params.requestedStartPosition),
params.requestedStartPosition
)
}
}
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
storeAndExecute {
loadingStates.offer(LoadingState.Loading)
performWithTryCatch {
callback.onResult(getItems(params.loadSize, params.startPosition))
}
}
}
protected abstract fun getItems(loadSize: Int, startPosition: Int): List<T>
fun retryFailedRequest() {
Completable.fromAction { lastExecutedRequest?.invoke() }
.subscribeOn(Schedulers.io())
.subscribe()
}
}
fun <T> dataSource(
loadingStates: PublishProcessor<LoadingState>,
loadFunction: LoadFunction<T>
) = object : SearchDataSource<T>(loadingStates) {
override fun getItems(loadSize: Int, startPosition: Int): List<T> {
return loadFunction(loadSize, startPosition)
}
}

View file

@ -0,0 +1,92 @@
package fr.free.nrw.commons.explore
import androidx.lifecycle.LiveData
import androidx.paging.Config
import androidx.paging.DataSource
import androidx.paging.PagedList
import androidx.paging.toLiveData
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.explore.depictions.LoadingStates
import io.reactivex.Flowable
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject
private const val PAGE_SIZE = 50
private const val INITIAL_LOAD_SIZE = 50
abstract class PageableDataSource<T>(private val liveDataConverter: LiveDataConverter) {
lateinit var query: String
private val dataSourceFactoryFactory: () -> SearchDataSourceFactory<T> = {
dataSourceFactory(_loadingStates, loadFunction)
}
private val _loadingStates = PublishProcessor.create<LoadingState>()
val loadingStates: Flowable<LoadingState> = _loadingStates
private val _searchResults = PublishProcessor.create<LiveData<PagedList<T>>>()
val searchResults: Flowable<LiveData<PagedList<T>>> = _searchResults
private val _noItemsLoadedEvent = PublishProcessor.create<Unit>()
val noItemsLoadedEvent: Flowable<Unit> = _noItemsLoadedEvent
private var currentFactory: SearchDataSourceFactory<T>? = null
abstract val loadFunction: LoadFunction<T>
fun onQueryUpdated(query: String) {
this.query = query
_searchResults.offer(
liveDataConverter.convert(dataSourceFactoryFactory().also { currentFactory = it }) {
_noItemsLoadedEvent.offer(Unit)
}
)
}
fun retryFailedRequest() {
currentFactory?.retryFailedRequest()
}
}
class LiveDataConverter @Inject constructor() {
fun <T> convert(
dataSourceFactory: SearchDataSourceFactory<T>,
zeroItemsLoadedFunction: () -> Unit
): LiveData<PagedList<T>> {
return dataSourceFactory.toLiveData(
Config(
pageSize = PAGE_SIZE,
initialLoadSizeHint = INITIAL_LOAD_SIZE,
enablePlaceholders = false
),
boundaryCallback = object : PagedList.BoundaryCallback<T>() {
override fun onZeroItemsLoaded() {
zeroItemsLoadedFunction()
}
}
)
}
}
abstract class SearchDataSourceFactory<T>(val loadingStates: LoadingStates) :
DataSource.Factory<Int, T>() {
private var currentDataSource: SearchDataSource<T>? = null
abstract val loadFunction: LoadFunction<T>
override fun create() =
dataSource(loadingStates, loadFunction).also { currentDataSource = it }
fun retryFailedRequest() {
currentDataSource?.retryFailedRequest()
}
}
fun <T> dataSourceFactory(loadingStates: LoadingStates, loadFunction: LoadFunction<T>) =
object : SearchDataSourceFactory<T>(loadingStates) {
override val loadFunction: LoadFunction<T> = loadFunction
}
sealed class LoadingState {
object InitialLoad : LoadingState()
object Loading : LoadingState()
object Complete : LoadingState()
object Error : LoadingState()
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.explore
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import fr.free.nrw.commons.BasePresenter
interface SearchFragmentContract {
interface View<T> {
fun showSnackbar()
fun observeSearchResults(searchResults: LiveData<PagedList<T>>)
fun setEmptyViewText(query: String)
fun showInitialLoadInProgress()
fun hideInitialLoadProgress()
}
interface Presenter<T> : BasePresenter<View<T>> {
val listFooterData: LiveData<List<FooterItem>>
fun onQueryUpdated(query: String)
fun retryFailedRequest()
}
}

View file

@ -1,29 +0,0 @@
package fr.free.nrw.commons.explore;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactory;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactoryFactory;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
public abstract class SearchModule {
@Binds
public abstract SearchDepictionsFragmentContract.UserActionListener bindsSearchDepictionsFragmentPresenter(
SearchDepictionsFragmentPresenter
presenter
);
@Provides
static public SearchDepictionsDataSourceFactoryFactory providesSearchDepictionsFactoryFactory(
DepictsClient depictsClient){
return (query, loadingStates) -> new SearchDepictionsDataSourceFactory(depictsClient, query, loadingStates);
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.explore
import dagger.Binds
import dagger.Module
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
abstract class SearchModule {
@Binds
abstract fun bindsSearchDepictionsFragmentPresenter(
presenter: SearchDepictionsFragmentPresenter
): SearchDepictionsFragmentContract.Presenter
@Binds
abstract fun bindsSearchCategoriesFragmentPresenter(
presenter: SearchCategoriesFragmentPresenter?
): SearchCategoriesFragmentContract.Presenter?
}

View file

@ -0,0 +1,16 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.category.CategoryClient
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableDataSource
import javax.inject.Inject
class PageableCategoriesDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient
) : PageableDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int ->
categoryClient.searchCategories(query, loadSize, startPosition).blockingFirst()
}
}

View file

@ -0,0 +1,45 @@
package fr.free.nrw.commons.explore.categories
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.BaseViewHolder
import fr.free.nrw.commons.explore.inflate
import kotlinx.android.synthetic.main.item_recent_searches.*
class PagedSearchCategoriesAdapter(val onCategoryClicked: (String) -> Unit) :
PagedListAdapter<String, CategoryItemViewHolder>(
object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryItemViewHolder {
return CategoryItemViewHolder(
parent.inflate(R.layout.item_recent_searches),
onCategoryClicked
)
}
override fun onBindViewHolder(holder: CategoryItemViewHolder, position: Int) {
holder.bind(getItem(position)!!)
}
}
class CategoryItemViewHolder(containerView: View, val onCategoryClicked: (String) -> Unit) :
BaseViewHolder<String>(containerView) {
override fun bind(item: String) {
containerView.setOnClickListener { onCategoryClicked(item) }
textView1.text = item.substringAfter(CATEGORY_PREFIX)
}
}

View file

@ -0,0 +1,8 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.explore.SearchFragmentContract
interface SearchCategoriesFragmentContract {
interface View : SearchFragmentContract.View<String>
interface Presenter : SearchFragmentContract.Presenter<String>
}

View file

@ -1,214 +0,0 @@
package fr.free.nrw.commons.explore.categories;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.res.Configuration;
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.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import kotlin.Unit;
import timber.log.Timber;
/**
* Displays the category search screen.
*/
public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.imagesListBox)
RecyclerView categoriesRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView categoriesNotFoundView;
String query;
@BindView(R.id.bottomProgressBar)
ProgressBar bottomProgressBar;
boolean isLoadingCategories;
@Inject RecentSearchesDao recentSearchesDao;
@Inject CategoryClient categoryClient;
@Inject
@Named("default_preferences")
JsonKvStore basicKvStore;
private SearchCategoriesAdapter categoriesAdapter;
private List<String> queryList = new ArrayList<>();
/**
* This method saves Search Query in the Recent Searches Database.
* @param query
*/
private void saveQuery(String query) {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
}
else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
}
categoriesAdapter = new SearchCategoriesAdapter(item -> {
CategoryDetailsActivity.startYourself(getContext(), item);
saveQuery(query);
return Unit.INSTANCE;
});
categoriesRecyclerView.setAdapter(categoriesAdapter);
categoriesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// check if end of recycler view is reached, if yes then add more results to existing results
if (!recyclerView.canScrollVertically(1)) {
addCategoriesToList(query);
}
}
});
return rootView;
}
/**
* Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void updateCategoryList(String query) {
this.query = query;
categoriesNotFoundView.setVisibility(GONE);
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
queryList.clear();
categoriesAdapter.clear();
compositeDisposable.add(categoryClient.searchCategories(query,25)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> saveQuery(query))
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Adds 25 more results to existing search results
*/
public void addCategoriesToList(String query) {
if(isLoadingCategories) {
return;
}
isLoadingCategories=true;
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handlePaginationSuccess, this::handleError));
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
private void handlePaginationSuccess(List<String> mediaList) {
queryList.addAll(mediaList);
progressBar.setVisibility(View.GONE);
bottomProgressBar.setVisibility(GONE);
categoriesAdapter.addAll(mediaList);
isLoadingCategories=false;
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
private void handleSuccess(List<String> mediaList) {
queryList = mediaList;
if (mediaList == null || mediaList.isEmpty()) {
initErrorView();
}
else {
bottomProgressBar.setVisibility(View.GONE);
progressBar.setVisibility(GONE);
categoriesAdapter.addAll(mediaList);
}
}
/**
* Logs and handles API error scenario
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried categories");
try {
initErrorView();
ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.error_loading_categories);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
categoriesNotFoundView.setText(getString(R.string.categories_not_found,query));
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.no_internet);
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity
import fr.free.nrw.commons.explore.BaseSearchFragment
import fr.free.nrw.commons.explore.SearchFragmentContract
import javax.inject.Inject
/**
* Displays the category search screen.
*/
class SearchCategoryFragment : BaseSearchFragment<String>() {
@Inject
lateinit var presenter: SearchCategoriesFragmentContract.Presenter
override val emptyTemplateTextId: Int = R.string.categories_not_found
override val injectedPresenter: SearchFragmentContract.Presenter<String>
get() = presenter
override val pagedListAdapter by lazy {
PagedSearchCategoriesAdapter { CategoryDetailsActivity.startYourself(context, it) }
}
}

View file

@ -6,6 +6,7 @@ import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.inflate
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_depictions.*
@ -19,9 +20,7 @@ class DepictionAdapter(val onDepictionClicked: (DepictedItem) -> Unit) :
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder {
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.LoadingState
import fr.free.nrw.commons.explore.PageableDataSource
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject
typealias LoadFunction<T> = (Int, Int) -> List<T>
typealias LoadingStates = PublishProcessor<LoadingState>
class PageableDepictionsDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val depictsClient: DepictsClient
) : PageableDataSource<DepictedItem>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int ->
depictsClient.searchForDepictions(query, loadSize, startPosition).blockingGet()
}
}

View file

@ -1,99 +1,26 @@
package fr.free.nrw.commons.explore.depictions
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.PagedList
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.MergeAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.explore.BaseSearchFragment
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.ViewUtil
import kotlinx.android.synthetic.main.fragment_search_depictions.*
import javax.inject.Inject
/**
* Display depictions in search fragment
*/
class SearchDepictionsFragment : CommonsDaggerSupportFragment(),
class SearchDepictionsFragment : BaseSearchFragment<DepictedItem>(),
SearchDepictionsFragmentContract.View {
@Inject
lateinit var presenter: SearchDepictionsFragmentContract.UserActionListener
lateinit var presenter: SearchDepictionsFragmentContract.Presenter
private val depictionsAdapter by lazy {
override val emptyTemplateTextId: Int = R.string.depictions_not_found
override val injectedPresenter: SearchFragmentContract.Presenter<DepictedItem>
get() = presenter
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
}
private val loadingAdapter by lazy { FooterAdapter { presenter.retryFailedRequest() } }
private val mergeAdapter by lazy { MergeAdapter(depictionsAdapter, loadingAdapter) }
var searchResults: LiveData<PagedList<DepictedItem>>? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_search_depictions, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
depictionsSearchResultsList.apply {
layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2)
adapter = mergeAdapter
}
presenter.listFooterData.observe(viewLifecycleOwner, Observer(loadingAdapter::submitList))
}
override fun onAttach(context: Context) {
super.onAttach(context)
presenter.onAttachView(this)
}
override fun onDetach() {
super.onDetach()
presenter.onDetachView()
}
override fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>) {
this.searchResults?.removeObservers(viewLifecycleOwner)
this.searchResults = searchResults
searchResults.observe(viewLifecycleOwner, Observer {
depictionsAdapter.submitList(it)
depictionNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE
})
}
override fun setEmptyViewText(query: String) {
depictionNotFound.text = getString(R.string.depictions_not_found, query)
}
override fun hideInitialLoadProgress() {
depictionSearchInitialLoadProgress.visibility = GONE
}
override fun showInitialLoadInProgress() {
depictionSearchInitialLoadProgress.visibility = VISIBLE
}
override fun showSnackbar() {
ViewUtil.showShortSnackbar(depictionsSearchResultsList, R.string.error_loading_depictions)
}
fun onQueryUpdated(query: String) {
presenter.onQueryUpdated(query)
}
}
private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT
private val Fragment.orientation get() = activity!!.resources.configuration.orientation

View file

@ -1,25 +1,12 @@
package fr.free.nrw.commons.explore.depictions
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import fr.free.nrw.commons.BasePresenter
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
/**
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
*/
interface SearchDepictionsFragmentContract {
interface View {
fun showSnackbar()
fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>)
fun setEmptyViewText(query: String)
fun showInitialLoadInProgress()
fun hideInitialLoadProgress()
}
interface UserActionListener : BasePresenter<View?> {
val listFooterData: LiveData<List<FooterItem>>
fun onQueryUpdated(query: String)
fun retryFailedRequest()
}
interface View : SearchFragmentContract.View<DepictedItem>
interface Presenter : SearchFragmentContract.Presenter<DepictedItem>
}

View file

@ -1,11 +1,9 @@
package fr.free.nrw.commons.explore.depictions
import androidx.lifecycle.MutableLiveData
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.upload.depicts.proxy
import fr.free.nrw.commons.explore.BaseSearchPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
@ -13,61 +11,7 @@ import javax.inject.Named
* The presenter class for SearchDepictionsFragment
*/
class SearchDepictionsFragmentPresenter @Inject constructor(
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
private val searchableDataSourceFactory: SearchableDepictionsDataSourceFactory
) : SearchDepictionsFragmentContract.UserActionListener {
private val compositeDisposable = CompositeDisposable()
private var view = DUMMY
private var currentQuery: String? = null
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
override fun onAttachView(view: SearchDepictionsFragmentContract.View) {
this.view = view
compositeDisposable.addAll(
searchableDataSourceFactory.searchResults.subscribe(view::observeSearchResults),
searchableDataSourceFactory.loadingStates
.observeOn(mainThreadScheduler)
.subscribe(::onLoadingState, Timber::e),
searchableDataSourceFactory.noItemsLoadedEvent.subscribe {
setEmptyViewText()
}
)
}
private fun onLoadingState(it: LoadingState) = when (it) {
LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem))
LoadingState.Complete -> {
listFooterData.postValue(emptyList())
view.hideInitialLoadProgress()
}
LoadingState.InitialLoad -> view.showInitialLoadInProgress()
LoadingState.Error -> {
setEmptyViewText()
view.showSnackbar()
view.hideInitialLoadProgress()
listFooterData.postValue(listOf(FooterItem.RefreshItem))
}
}
private fun setEmptyViewText() {
currentQuery?.let(view::setEmptyViewText)
}
override fun retryFailedRequest() {
searchableDataSourceFactory.retryFailedRequest()
}
override fun onDetachView() {
view = DUMMY
compositeDisposable.clear()
}
override fun onQueryUpdated(query: String) {
currentQuery = query
searchableDataSourceFactory.onQueryUpdated(query)
}
companion object {
private val DUMMY: SearchDepictionsFragmentContract.View = proxy()
}
}
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableDepictionsDataSource
) : BaseSearchPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
SearchDepictionsFragmentContract.Presenter

View file

@ -43,6 +43,7 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;

View file

@ -34,7 +34,6 @@ import android.widget.RelativeLayout;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="@dimen/tiny_gap">
<TextView
android:id="@+id/contentNotFound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/paginatedSearchResultsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarSize="@dimen/standard_gap"
android:scrollbars="vertical" />
<ProgressBar
android:id="@+id/paginatedSearchInitialLoadProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>