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 ->
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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