Use loading item instead of full screen loading.

This commit is contained in:
Onuray Sahin 2020-09-29 20:38:58 +03:00 committed by Benoit Marty
parent 0d16fe019e
commit 5d190a8137
7 changed files with 104 additions and 81 deletions

View file

@ -20,6 +20,6 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class SearchAction : VectorViewModelAction {
data class SearchWith(val searchTerm: String) : SearchAction()
object ScrolledToTop : SearchAction()
object LoadMore : SearchAction()
object Retry : SearchAction()
}

View file

@ -21,14 +21,15 @@ import android.os.Parcelable
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.trackItemsVisibilityChange
import im.vector.app.core.platform.StateView
@ -62,39 +63,19 @@ class SearchFragment @Inject constructor(
stateView.eventCallback = this
configureRecyclerView()
searchViewModel.observeViewEvents {
when (it) {
is SearchViewEvents.Failure -> {
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(it.throwable))
}
is SearchViewEvents.Loading -> {
stateView.state = StateView.State.Loading
}
}.exhaustive
}
}
private fun configureRecyclerView() {
searchResultRecycler.trackItemsVisibilityChange()
searchResultRecycler.configureWith(controller, showDivider = false)
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
controller.listener = this
controller.addModelBuildListener {
pendingScrollToPosition?.let {
searchResultRecycler.scrollToPosition(it)
searchResultRecycler.smoothScrollToPosition(it)
}
}
searchResultRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// Load next batch when scrolled to the top
if (newState == RecyclerView.SCROLL_STATE_IDLE
&& (searchResultRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() == 0) {
searchViewModel.handle(SearchAction.ScrolledToTop)
}
}
})
}
override fun onDestroy() {
@ -104,18 +85,26 @@ class SearchFragment @Inject constructor(
}
override fun invalidate() = withState(searchViewModel) { state ->
if (state.searchResult?.results?.isNotEmpty() == true) {
if (state.searchResult?.results.isNullOrEmpty()) {
when (state.asyncEventsRequest) {
is Loading -> {
stateView.state = StateView.State.Loading
}
is Fail -> {
stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error))
}
is Success -> {
stateView.state = StateView.State.Empty(
title = getString(R.string.search_no_results),
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results))
}
}
} else {
val lastBatchSize = state.lastBatch?.results?.size ?: 0
pendingScrollToPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0
stateView.state = StateView.State.Content
controller.setData(state)
val lastBatchSize = state.lastBatch?.results?.size ?: 0
val scrollPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0
pendingScrollToPosition = scrollPosition
} else {
stateView.state = StateView.State.Empty(
title = getString(R.string.search_no_results),
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_search_no_results)
)
}
}
@ -133,4 +122,8 @@ class SearchFragment @Inject constructor(
navigator.openRoom(requireContext(), event.roomId!!, event.eventId)
}
override fun loadMore() {
searchViewModel.handle(SearchAction.LoadMore)
}
}

View file

@ -17,8 +17,10 @@
package im.vector.app.features.home.room.detail.search
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
@ -34,8 +36,11 @@ class SearchResultController @Inject constructor(
var listener: Listener? = null
private var idx = 0
interface Listener {
fun onItemClicked(event: Event)
fun loadMore()
}
init {
@ -45,6 +50,18 @@ class SearchResultController @Inject constructor(
override fun buildModels(data: SearchViewState?) {
data?.searchResult?.results ?: return
if (!data.searchResult.nextBatch.isNullOrEmpty()) {
loadingItem {
// Always use a different id, because we can be notified several times of visibility state changed
id("loadMore${idx++}")
onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
listener?.loadMore()
}
}
}
}
buildSearchResultItems(data.searchResult.results!!)
}

View file

@ -20,5 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class SearchViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : SearchViewEvents()
data class Loading(val message: CharSequence? = null) : SearchViewEvents()
}

View file

@ -16,16 +16,21 @@
package im.vector.app.features.home.room.detail.search
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.MatrixCallback
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.internal.util.awaitCallback
class SearchViewModel @AssistedInject constructor(
@Assisted private val initialState: SearchViewState,
@ -48,26 +53,22 @@ class SearchViewModel @AssistedInject constructor(
override fun handle(action: SearchAction) {
when (action) {
is SearchAction.SearchWith -> handleSearchWith(action)
is SearchAction.ScrolledToTop -> handleScrolledToTop()
is SearchAction.Retry -> handleRetry()
is SearchAction.SearchWith -> handleSearchWith(action)
is SearchAction.LoadMore -> handleLoadMore()
is SearchAction.Retry -> handleRetry()
}.exhaustive
}
private fun handleSearchWith(action: SearchAction.SearchWith) {
if (action.searchTerm.length > 1) {
setState {
copy(searchTerm = action.searchTerm, isNextBatch = false)
copy(searchTerm = action.searchTerm)
}
startSearching()
}
}
private fun handleScrolledToTop() {
setState {
copy(isNextBatch = true)
}
private fun handleLoadMore() {
startSearching(true)
}
@ -75,44 +76,51 @@ class SearchViewModel @AssistedInject constructor(
startSearching()
}
private fun startSearching(scrolledToTop: Boolean = false) = withState { state ->
private fun startSearching(isNextBatch: Boolean = false) = withState { state ->
if (state.roomId == null || state.searchTerm == null) return@withState
// There is no batch to retrieve
if (scrolledToTop && state.searchResult?.nextBatch == null) return@withState
if (isNextBatch && state.searchResult?.nextBatch == null) return@withState
_viewEvents.post(SearchViewEvents.Loading())
session
.getRoom(state.roomId)
?.search(
searchTerm = state.searchTerm,
nextBatch = state.searchResult?.nextBatch,
orderByRecent = true,
beforeLimit = 0,
afterLimit = 0,
includeProfile = true,
limit = 20,
callback = object : MatrixCallback<SearchResult> {
override fun onFailure(failure: Throwable) {
onSearchFailure(failure)
}
override fun onSuccess(data: SearchResult) {
onSearchResultSuccess(data)
}
}
// Show full screen loading just for the clean search
if (!isNextBatch) {
setState {
copy(
asyncEventsRequest = Loading()
)
}
private fun onSearchFailure(failure: Throwable) {
setState {
copy(searchResult = null)
}
}
viewModelScope.launch {
try {
val result = awaitCallback<SearchResult> {
session
.getRoom(state.roomId)
?.search(
searchTerm = state.searchTerm,
nextBatch = state.searchResult?.nextBatch,
orderByRecent = true,
beforeLimit = 0,
afterLimit = 0,
includeProfile = true,
limit = 20,
callback = it
)
}
onSearchResultSuccess(result, isNextBatch)
} catch (failure: Throwable) {
_viewEvents.post(SearchViewEvents.Failure(failure))
setState {
copy(
asyncEventsRequest = Fail(failure),
searchResult = null
)
}
}
}
_viewEvents.post(SearchViewEvents.Failure(failure))
}
private fun onSearchResultSuccess(searchResult: SearchResult) = withState { state ->
private fun onSearchResultSuccess(searchResult: SearchResult, isNextBatch: Boolean) = withState { state ->
val accumulatedResult = SearchResult(
nextBatch = searchResult.nextBatch,
results = searchResult.results,
@ -120,7 +128,7 @@ class SearchViewModel @AssistedInject constructor(
)
// Accumulate results if it is the next batch
if (state.isNextBatch) {
if (isNextBatch) {
if (state.searchResult != null) {
accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!)
}
@ -130,7 +138,11 @@ class SearchViewModel @AssistedInject constructor(
}
setState {
copy(searchResult = accumulatedResult, lastBatch = searchResult)
copy(
searchResult = accumulatedResult,
lastBatch = searchResult,
asyncEventsRequest = Success(Unit)
)
}
}
}

View file

@ -16,7 +16,9 @@
package im.vector.app.features.home.room.detail.search
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.search.SearchResult
data class SearchViewState(
@ -26,7 +28,8 @@ data class SearchViewState(
val lastBatch: SearchResult? = null,
val searchTerm: String? = null,
val roomId: String? = null,
val isNextBatch: Boolean = false
// Current pagination request
val asyncEventsRequest: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: SearchArgs) : this(roomId = args.roomId)

View file

@ -2,8 +2,7 @@
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/stateView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultRecycler"