Improved loading of new and cached items.

Scrolling through large lists was a pain. The next page would only load
when the end of the list was reached, stopping the scroll action while the new
page was fetched.

This commits adds two items:

 * Artists, albums and playlists do not refresh data on resume, only
   using cached data until manually refreshed.
 * When manually refreshed, we initially fetch a few pages instead
   of only one. Also, on scroll, we try as best as we can to always keep
   10 pages (pages as in screen estate) worth of data loaded.
This commit is contained in:
Antoine POPINEAU 2020-07-08 22:11:50 +02:00
parent de0a494b43
commit b2e6ec43a8
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
10 changed files with 74 additions and 24 deletions

View File

@ -408,7 +408,7 @@ class MainActivity : AppCompatActivity() {
}
now_playing_details_favorite?.let { now_playing_details_favorite ->
favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _ ->
favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id)

View File

@ -67,21 +67,21 @@ class SearchActivity : AppCompatActivity() {
adapter.tracks.clear()
adapter.notifyDataSetChanged()
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _ ->
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
done++
adapter.artists.addAll(artists)
refresh()
}
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _ ->
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _ ,_ ->
done++
adapter.albums.addAll(albums)
refresh()
}
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _ ->
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
adapter.tracks.addAll(tracks)

View File

@ -33,8 +33,9 @@ import kotlinx.coroutines.withContext
class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
override val viewRes = R.layout.fragment_albums
override val recycler: RecyclerView get() = albums
override val alwaysRefresh = false
lateinit var artistTracksRepository: ArtistTracksRepository
private lateinit var artistTracksRepository: ArtistTracksRepository
var artistId = 0
var artistName = ""

View File

@ -21,6 +21,7 @@ import kotlinx.android.synthetic.main.fragment_artists.*
class ArtistsFragment : FunkwhaleFragment<Artist, ArtistsAdapter>() {
override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists
override val alwaysRefresh = false
companion object {
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {

View File

@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext
class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -28,14 +28,19 @@ abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.
}
abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment() {
val INITIAL_PAGES = 5
val OFFSCREEN_PAGES = 10
abstract val viewRes: Int
abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true
lateinit var repository: Repository<D, *>
lateinit var adapter: A
private var initialFetched = false
private var moreLoading = false
private var listener: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -51,7 +56,11 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
if (recycler.computeVerticalScrollOffset() > 0 && !recycler.canScrollVertically(1)) {
val offset = recycler.computeVerticalScrollOffset()
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
if (initialFetched && !moreLoading && offset > 0 && left < (recycler.height * OFFSCREEN_PAGES)) {
moreLoading = true
fetch(Repository.Origin.Network.origin, adapter.data.size)
}
}
@ -60,7 +69,7 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
fetch(Repository.Origin.Cache.origin)
if (adapter.data.isEmpty()) {
if (alwaysRefresh && adapter.data.isEmpty()) {
fetch(Repository.Origin.Network.origin)
}
}
@ -91,11 +100,11 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
var first = size == 0
if (upstreams == Repository.Origin.Network.origin) {
if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
swiper?.isRefreshing = true
}
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, hasMore ->
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, page, hasMore ->
lifecycleScope.launch(Main) {
if (isCache) {
adapter.data = data.toMutableList()
@ -112,10 +121,16 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
adapter.data.addAll(data)
if (!hasMore) {
swiper?.isRefreshing = false
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || page >= INITIAL_PAGES) swiper?.isRefreshing = false
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false
}
}
withContext(IO) {
when (hasMore) {
false -> withContext(IO) {
if (adapter.data.isNotEmpty()) {
try {
repository.cacheId?.let { cacheId ->
@ -129,6 +144,21 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
}
}
}
true -> {
moreLoading = false
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
if (page < INITIAL_PAGES) {
moreLoading = true
fetch(Repository.Origin.Network.origin, adapter.data.size)
} else {
initialFetched = true
}
}
}
}
}
when (first) {

View File

@ -41,18 +41,19 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
{ response ->
val data = response.getData()
if (behavior == Behavior.Progressive || response.next == null) {
emit(Repository.Response(Repository.Origin.Network, data, false))
} else {
emit(Repository.Response(Repository.Origin.Network, data, true))
when (behavior) {
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
else -> {
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
fetch(size + data.size).collect { emit(it) }
fetch(size + data.size).collect { emit(it) }
}
}
},
{ error ->
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), false))
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page,false))
}
}
)

View File

@ -1,6 +1,7 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.CacheItem
import kotlinx.coroutines.CoroutineScope
@ -8,6 +9,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import java.io.BufferedReader
import kotlin.math.ceil
interface Upstream<D> {
fun fetch(size: Int = 0): Flow<Repository.Response<D>>
@ -21,7 +23,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
Network(0b10)
}
data class Response<D>(val origin: Origin, val data: List<D>, val hasMore: Boolean)
data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)
abstract val context: Context?
abstract val cacheId: String?
@ -39,7 +41,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
cacheId?.let { cacheId ->
Cache.get(context, cacheId)?.let { reader ->
uncache(reader)?.let { cache ->
emit(Response(Origin.Cache, cache.data, false))
emit(Response(Origin.Cache, cache.data, ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(), false))
}
}
}
@ -48,8 +50,8 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
private fun fromNetwork(size: Int) = flow {
upstream
.fetch(size)
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.hasMore) }
.collect { response -> emit(Response(Origin.Network, response.data, response.hasMore)) }
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
}
protected open fun onDataFetched(data: List<D>) = data

View File

@ -17,10 +17,10 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, hasMore: Boolean) -> Unit) {
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
scope.launch(context) {
collect { data ->
callback(data.data, data.origin == Repository.Origin.Cache, data.hasMore)
callback(data.data, data.origin == Repository.Origin.Cache, data.page, data.hasMore)
}
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>