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:
parent
de0a494b43
commit
b2e6ec43a8
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue