Browse Source

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 4 years ago
parent
commit
b2e6ec43a8
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
  1. 2
      app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt
  2. 6
      app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt
  3. 3
      app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt
  4. 1
      app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt
  5. 1
      app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt
  6. 44
      app/src/main/java/com/github/apognu/otter/fragments/FunkwhaleFragment.kt
  7. 13
      app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt
  8. 10
      app/src/main/java/com/github/apognu/otter/repositories/Repository.kt
  9. 4
      app/src/main/java/com/github/apognu/otter/utils/Extensions.kt
  10. 14
      app/src/main/res/xml/security.xml

2
app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt

@ -408,7 +408,7 @@ class MainActivity : AppCompatActivity() { @@ -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)

6
app/src/main/java/com/github/apognu/otter/activities/SearchActivity.kt

@ -67,21 +67,21 @@ class SearchActivity : AppCompatActivity() { @@ -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)

3
app/src/main/java/com/github/apognu/otter/fragments/AlbumsFragment.kt

@ -33,8 +33,9 @@ import kotlinx.coroutines.withContext @@ -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 = ""

1
app/src/main/java/com/github/apognu/otter/fragments/ArtistsFragment.kt

@ -21,6 +21,7 @@ import kotlinx.android.synthetic.main.fragment_artists.* @@ -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) {

1
app/src/main/java/com/github/apognu/otter/fragments/FavoritesFragment.kt

@ -19,6 +19,7 @@ import kotlinx.coroutines.withContext @@ -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)

44
app/src/main/java/com/github/apognu/otter/fragments/FunkwhaleFragment.kt

@ -28,14 +28,19 @@ abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) {

13
app/src/main/java/com/github/apognu/otter/repositories/HttpUpstream.kt

@ -41,18 +41,19 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr @@ -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))
}
}
)

10
app/src/main/java/com/github/apognu/otter/repositories/Repository.kt

@ -1,6 +1,7 @@ @@ -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 @@ -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>> { @@ -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>> { @@ -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>> { @@ -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

4
app/src/main/java/com/github/apognu/otter/utils/Extensions.kt

@ -17,10 +17,10 @@ import kotlinx.coroutines.flow.collect @@ -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)
}
}
}

14
app/src/main/res/xml/security.xml

@ -0,0 +1,14 @@ @@ -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…
Cancel
Save