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 -> now_playing_details_favorite?.let { now_playing_details_favorite ->
favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _ -> favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id) track.favorite = favorites.contains(track.id)

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import kotlinx.android.synthetic.main.fragment_artists.*
class ArtistsFragment : FunkwhaleFragment<Artist, ArtistsAdapter>() { class ArtistsFragment : FunkwhaleFragment<Artist, ArtistsAdapter>() {
override val viewRes = R.layout.fragment_artists override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists override val recycler: RecyclerView get() = artists
override val alwaysRefresh = false
companion object { companion object {
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) { 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>() { class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites override val recycler: RecyclerView get() = favorites
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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() { abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment() {
val INITIAL_PAGES = 5
val OFFSCREEN_PAGES = 10
abstract val viewRes: Int abstract val viewRes: Int
abstract val recycler: RecyclerView abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context) open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true
lateinit var repository: Repository<D, *> lateinit var repository: Repository<D, *>
lateinit var adapter: A lateinit var adapter: A
private var initialFetched = false private var initialFetched = false
private var moreLoading = false
private var listener: Job? = null private var listener: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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 -> (repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (upstream.behavior == HttpUpstream.Behavior.Progressive) { if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
recycler.setOnScrollChangeListener { _, _, _, _, _ -> 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) 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) fetch(Repository.Origin.Cache.origin)
if (adapter.data.isEmpty()) { if (alwaysRefresh && adapter.data.isEmpty()) {
fetch(Repository.Origin.Network.origin) 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) { private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
var first = size == 0 var first = size == 0
if (upstreams == Repository.Origin.Network.origin) { if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
swiper?.isRefreshing = true 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) { lifecycleScope.launch(Main) {
if (isCache) { if (isCache) {
adapter.data = data.toMutableList() adapter.data = data.toMutableList()
@ -112,10 +121,16 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
adapter.data.addAll(data) adapter.data.addAll(data)
if (!hasMore) { (repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
swiper?.isRefreshing = false 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()) { if (adapter.data.isNotEmpty()) {
try { try {
repository.cacheId?.let { cacheId -> 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) { when (first) {

View File

@ -41,18 +41,19 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
{ response -> { response ->
val data = response.getData() val data = response.getData()
if (behavior == Behavior.Progressive || response.next == null) { when (behavior) {
emit(Repository.Response(Repository.Origin.Network, data, false)) Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
} else { else -> {
emit(Repository.Response(Repository.Origin.Network, data, true)) 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 -> { error ->
when (error.exception) { when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut) 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 package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.CacheItem import com.github.apognu.otter.utils.CacheItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -8,6 +9,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import java.io.BufferedReader import java.io.BufferedReader
import kotlin.math.ceil
interface Upstream<D> { interface Upstream<D> {
fun fetch(size: Int = 0): Flow<Repository.Response<D>> fun fetch(size: Int = 0): Flow<Repository.Response<D>>
@ -21,7 +23,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
Network(0b10) 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 context: Context?
abstract val cacheId: String? abstract val cacheId: String?
@ -39,7 +41,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
cacheId?.let { cacheId -> cacheId?.let { cacheId ->
Cache.get(context, cacheId)?.let { reader -> Cache.get(context, cacheId)?.let { reader ->
uncache(reader)?.let { cache -> 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 { private fun fromNetwork(size: Int) = flow {
upstream upstream
.fetch(size) .fetch(size)
.map { response -> Response(Origin.Network, onDataFetched(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.hasMore)) } .collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
} }
protected open fun onDataFetched(data: List<D>) = data protected open fun onDataFetched(data: List<D>) = data

View File

@ -17,10 +17,10 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext 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) { scope.launch(context) {
collect { data -> 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>