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.
housekeeping/remove-warnings
Antoine POPINEAU 2020-07-08 22:11:50 +02:00
rodzic de0a494b43
commit b2e6ec43a8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A78AC64694F84063
10 zmienionych plików z 74 dodań i 24 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 = ""

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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))
}
}
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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>