Advancement with Couchbase Lite.

This commit is contained in:
Antoine POPINEAU 2020-09-26 17:26:42 +02:00
parent 1126d47a1a
commit 4e3da9160e
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
26 changed files with 415 additions and 171 deletions

View File

@ -7,13 +7,12 @@ plugins {
id("kotlin-android") id("kotlin-android")
id("kotlin-android-extensions") id("kotlin-android-extensions")
id("kotlin-kapt") id("kotlin-kapt")
id("realm-android")
id("org.jlleitschuh.gradle.ktlint") version "8.1.0" id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
id("com.gladed.androidgitversion") version "0.4.10" id("com.gladed.androidgitversion") version "0.4.10"
id("com.github.triplet.play") version "2.4.2" id("com.github.triplet.play") version "2.4.2"
kotlin("plugin.serialization") version "1.3.70" kotlin("plugin.serialization") version "1.3.72"
} }
val props = Properties().apply { val props = Properties().apply {
@ -149,13 +148,15 @@ dependencies {
implementation("com.aliassadi:power-preference-lib:1.4.1") implementation("com.aliassadi:power-preference-lib:1.4.1")
implementation("com.github.kittinunf.fuel:fuel:2.2.3") implementation("com.github.kittinunf.fuel:fuel:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.1.0") implementation("com.github.kittinunf.fuel:fuel-coroutines:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0") implementation("com.github.kittinunf.fuel:fuel-android:2.2.3")
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3") implementation("com.github.kittinunf.fuel:fuel-kotlinx-serialization:2.2.3")
implementation("com.squareup.picasso:picasso:2.71828") implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.2.1") implementation("jp.wasabeef:picasso-transformations:2.2.1")
implementation("com.couchbase.lite:couchbase-lite-android:2.7.1")
implementation("com.github.MOLO17:couchbase-lite-kotlin:1.0.0")
debugImplementation("com.amitshekhar.android:debug-db:1.0.6") debugImplementation("com.amitshekhar.android:debug-db:1.0.6")
kapt("androidx.room:room-compiler:2.2.5") kapt("androidx.room:room-compiler:2.2.5")

View File

@ -17,7 +17,8 @@
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
tools:replace="android:label">
<activity <activity
android:name=".activities.SplashActivity" android:name=".activities.SplashActivity"

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.widget.SearchView import android.widget.SearchView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room import androidx.room.Room
import com.couchbase.lite.*
import com.github.apognu.otter.activities.MainActivity import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.activities.SearchActivity import com.github.apognu.otter.activities.SearchActivity
import com.github.apognu.otter.adapters.* import com.github.apognu.otter.adapters.*
@ -14,10 +15,7 @@ import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.playback.MediaSession import com.github.apognu.otter.playback.MediaSession
import com.github.apognu.otter.playback.QueueManager.Companion.factory import com.github.apognu.otter.playback.QueueManager.Companion.factory
import com.github.apognu.otter.repositories.* import com.github.apognu.otter.repositories.*
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.*
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.viewmodels.* import com.github.apognu.otter.viewmodels.*
import com.google.android.exoplayer2.database.ExoDatabaseProvider import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DefaultDownloadIndex import com.google.android.exoplayer2.offline.DefaultDownloadIndex
@ -28,7 +26,6 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.preference.PowerPreference import com.preference.PowerPreference
import io.realm.Realm
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.BroadcastChannel
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -38,6 +35,7 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.dsl.module import org.koin.dsl.module
import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -86,7 +84,7 @@ class Otter : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Realm.init(this) CouchbaseLite.init(this)
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
@ -107,6 +105,13 @@ class Otter : Application() {
} }
} }
single {
Database("otter", DatabaseConfiguration()).apply {
createIndex("type", IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("type"))))
createIndex("order", IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("order"))))
}
}
fragment { BrowseFragment() } fragment { BrowseFragment() }
fragment { LandscapeQueueFragment() } fragment { LandscapeQueueFragment() }
@ -114,7 +119,7 @@ class Otter : Application() {
single { ArtistsRepository(get(), get()) } single { ArtistsRepository(get(), get()) }
factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) } factory { (id: Int) -> ArtistTracksRepository(get(), get(), id) }
viewModel { ArtistsViewModel(get(), get()) } viewModel { ArtistsViewModel(get()) }
factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) } factory { (context: Context?, listener: ArtistsFragment.OnArtistClickListener) -> ArtistsAdapter(context, listener) }
factory { (id: Int?) -> AlbumsRepository(get(), get(), id) } factory { (id: Int?) -> AlbumsRepository(get(), get(), id) }
@ -122,7 +127,7 @@ class Otter : Application() {
factory { (context: Context?, adapter: AlbumsAdapter.OnAlbumClickListener) -> AlbumsAdapter(context, adapter) } factory { (context: Context?, adapter: AlbumsAdapter.OnAlbumClickListener) -> AlbumsAdapter(context, adapter) }
factory { (context: Context?, adapter: AlbumsGridAdapter.OnAlbumClickListener) -> AlbumsGridAdapter(context, adapter) } factory { (context: Context?, adapter: AlbumsGridAdapter.OnAlbumClickListener) -> AlbumsGridAdapter(context, adapter) }
factory { (id: Int?) -> TracksRepository(get(), get(), id) } factory { (id: Int?) -> TracksRepository(get(), get(), get(), id) }
viewModel { (id: Int) -> TracksViewModel(get { parametersOf(id) }, get(), id) } viewModel { (id: Int) -> TracksViewModel(get { parametersOf(id) }, get(), id) }
factory { (context: Context?, favoriteListener: TracksAdapter.OnFavoriteListener?) -> TracksAdapter(context, favoriteListener) } factory { (context: Context?, favoriteListener: TracksAdapter.OnFavoriteListener?) -> TracksAdapter(context, favoriteListener) }
@ -149,8 +154,6 @@ class Otter : Application() {
single { ArtistsSearchRepository(get(), get()) } single { ArtistsSearchRepository(get(), get()) }
single { AlbumsSearchRepository(get(), get { parametersOf(null) }) } single { AlbumsSearchRepository(get(), get { parametersOf(null) }) }
single { TracksSearchRepository(get(), get { parametersOf(null) }) } single { TracksSearchRepository(get(), get { parametersOf(null) }) }
single { Mediator(get(), get(), get()) }
}) })
} }
@ -164,6 +167,8 @@ class Otter : Application() {
fun deleteAllData() { fun deleteAllData() {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear() PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
filesDir.deleteRecursively()
cacheDir.listFiles()?.forEach { cacheDir.listFiles()?.forEach {
it.delete() it.delete()
} }

View File

@ -25,10 +25,6 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
fun onClick(holder: View?, artist: Artist) fun onClick(holder: View?, artist: Artist)
} }
// override fun getItemCount() = data.size
// override fun getItemId(position: Int) = data[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false) val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)

View File

@ -9,7 +9,7 @@ import com.github.apognu.otter.fragments.*
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var tabs = mutableListOf<Fragment>() var tabs = mutableListOf<Fragment>()
override fun getCount() = 5 override fun getCount() = 1
override fun getItem(position: Int): Fragment { override fun getItem(position: Int): Fragment {
tabs.getOrNull(position)?.let { tabs.getOrNull(position)?.let {

View File

@ -50,7 +50,7 @@ class AlbumsFragment : OtterFragment<FunkwhaleAlbum, Album, AlbumsAdapter>() {
companion object { companion object {
fun new(artist: Artist, _art: String? = null): AlbumsFragment { fun new(artist: Artist, _art: String? = null): AlbumsFragment {
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.cover() else "" val art = _art ?: (artist.cover() ?: "")
return AlbumsFragment().apply { return AlbumsFragment().apply {
arguments = bundleOf( arguments = bundleOf(

View File

@ -8,10 +8,7 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.transition.Fade import androidx.transition.Fade
import androidx.transition.Slide import androidx.transition.Slide
import com.github.apognu.otter.R import com.github.apognu.otter.R
@ -33,7 +30,7 @@ class ArtistsFragment : PagedOtterFragment<FunkwhaleArtist, Artist, ArtistsAdapt
override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) } override val adapter by inject<ArtistsAdapter> { parametersOf(context, OnArtistClickListener()) }
override val viewModel by viewModel<ArtistsViewModel>() override val viewModel by viewModel<ArtistsViewModel>()
override val liveData by lazy { viewModel.artistsPaged } override val liveData by lazy { viewModel.artists }
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

View File

@ -5,12 +5,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.paging.map
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
@ -21,11 +19,14 @@ import com.github.apognu.otter.utils.EventBus
import com.github.apognu.otter.utils.log import com.github.apognu.otter.utils.log
import com.github.apognu.otter.utils.untilNetwork import com.github.apognu.otter.utils.untilNetwork
import kotlinx.android.synthetic.main.fragment_artists.* import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.ext.scope import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var data: MutableList<D> = mutableListOf() var data: MutableList<D> = mutableListOf()
@ -154,8 +155,6 @@ abstract class OtterFragment<DAO : Any, D : Any, A : OtterAdapter<D, *>> : Fragm
} }
abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *>> : Fragment() { abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *>> : Fragment() {
open val OFFSCREEN_PAGES = 10
abstract val repository: Repository<DAO> abstract val repository: Repository<DAO>
abstract val adapter: A abstract val adapter: A
open val viewModel: ViewModel? = null open val viewModel: ViewModel? = null
@ -219,15 +218,4 @@ abstract class PagedOtterFragment<DAO : Any, D : Any, A : PagingDataAdapter<D, *
} }
} }
} }
private fun needsMoreOffscreenPages(): Boolean {
view?.let {
val offset = recycler.computeVerticalScrollOffset()
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
return left < (recycler.height * OFFSCREEN_PAGES)
}
return false
}
} }

View File

@ -5,24 +5,29 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType import androidx.paging.LoadType
import androidx.paging.PagingState import androidx.paging.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.room.withTransaction import com.couchbase.lite.Database
import com.github.apognu.otter.models.dao.DecoratedArtistEntity import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext 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.log import com.github.apognu.otter.utils.log
import kotlinx.coroutines.flow.collect import com.molo17.couchbase.lite.doInBatch
import com.molo17.couchbase.lite.from
import com.molo17.couchbase.lite.select
import com.molo17.couchbase.lite.where
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
class Mediator(private val context: Context, private val database: OtterDatabase, private val repository: ArtistsRepository) : RemoteMediator<Int, DecoratedArtistEntity>(), KoinComponent { class Mediator(private val context: Context, private val database: Database, private val repository: ArtistsRepository) : RemoteMediator<Int, Artist>(), KoinComponent {
override suspend fun load(loadType: LoadType, state: PagingState<Int, DecoratedArtistEntity>): MediatorResult { override suspend fun load(loadType: LoadType, state: PagingState<Int, Artist>): MediatorResult {
loadType.log()
return try { return try {
val key = when (loadType) { val key = when (loadType) {
LoadType.REFRESH -> 1 LoadType.REFRESH -> 1
@ -33,20 +38,28 @@ class Mediator(private val context: Context, private val database: OtterDatabase
} }
} }
key.log("fetching page") val response = withContext(IO) {
repository.fetch((key - 1) * AppContext.PAGE_SIZE).take(1).first()
}
val response = repository.fetch((key - 1) * AppContext.PAGE_SIZE).take(1).first() database.doInBatch {
database.withTransaction {
if (loadType == LoadType.REFRESH) { if (loadType == LoadType.REFRESH) {
Cache.delete(context, "key") Cache.delete(context, "key")
database.artists().deleteAll()
select("_id")
.from(database)
.where { "type" equalTo "artist" }
.execute()
.forEach { delete(getDocument(it.getString(0))) }
} }
Cache.set(context, "key", (key + 1).toString().toByteArray()) Cache.set(context, "key", (key + 1).toString().toByteArray())
response.data.forEach { FunkwhaleArtist.persist(database, response.data, (key + 1) * 100)
database.artists().insert(it.toDao())
listeners.forEach {
it()
listeners.remove(it)
} }
} }
@ -55,4 +68,10 @@ class Mediator(private val context: Context, private val database: OtterDatabase
MediatorResult.Error(e) MediatorResult.Error(e)
} }
} }
private var listeners: MutableList<() -> Unit> = mutableListOf()
fun addListener(listener: () -> Unit) {
listeners.add(listener)
}
} }

View File

@ -1,5 +1,8 @@
package com.github.apognu.otter.models.api package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.utils.toCouchbaseArray
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -15,6 +18,34 @@ data class FunkwhaleAlbum(
data class Artist(val id: Int, val name: String) data class Artist(val id: Int, val name: String)
fun cover() = cover?.urls?.original fun cover() = cover?.urls?.original
companion object {
fun persist(database: Database, albums: List<FunkwhaleAlbum>, base: Int? = null) {
albums.forEachIndexed { index, album ->
val doc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
doc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", album.artist.id)
setString("artist_name", album.artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
base?.let {
setInt("order", base + index)
}
database.save(this)
}
}
}
}
} }
@Serializable @Serializable
@ -22,4 +53,3 @@ data class Covers(val urls: CoverUrls?)
@Serializable @Serializable
data class CoverUrls(val original: String?) data class CoverUrls(val original: String?)

View File

@ -1,5 +1,8 @@
package com.github.apognu.otter.models.api package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.utils.toCouchbaseArray
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -16,4 +19,52 @@ data class FunkwhaleArtist(
val cover: Covers?, val cover: Covers?,
val release_date: String? val release_date: String?
) )
companion object {
fun persist(database: Database, artists: List<FunkwhaleArtist>, base: Int? = null) {
artists
// .filter { it.albums?.isNotEmpty() ?: false }
.forEachIndexed { index, artist ->
artist.albums?.forEach { album ->
val albumDoc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
albumDoc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", artist.id)
setString("artist_name", artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
database.save(this)
}
}
val artistDoc = database.getDocument("artist:${artist.id}")?.toMutable() ?: MutableDocument("artist:${artist.id}")
artistDoc.run {
setString("type", "artist")
setInt("id", artist.id)
setString("name", artist.name)
setArray("albums", artist.albums?.map { it.id }.toCouchbaseArray())
artist.albums?.getOrNull(0)?.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
base?.let {
setInt("order", base + index)
}
database.save(this)
}
}
}
}
} }

View File

@ -1,6 +1,9 @@
package com.github.apognu.otter.models.api package com.github.apognu.otter.models.api
import com.couchbase.lite.Database
import com.couchbase.lite.MutableDocument
import com.github.apognu.otter.models.domain.SearchResult import com.github.apognu.otter.models.domain.SearchResult
import com.github.apognu.otter.utils.toCouchbaseArray
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import kotlinx.serialization.ContextualSerialization import kotlinx.serialization.ContextualSerialization
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -30,6 +33,63 @@ data class FunkwhaleTrack(
album = FunkwhaleAlbum(0, FunkwhaleAlbum.Artist(0, ""), "", Covers(CoverUrls("")), ""), album = FunkwhaleAlbum(0, FunkwhaleAlbum.Artist(0, ""), "", Covers(CoverUrls("")), ""),
uploads = listOf(FunkwhaleUpload(download.contentId, 0, 0)) uploads = listOf(FunkwhaleUpload(download.contentId, 0, 0))
) )
fun persist(database: Database, tracks: List<FunkwhaleTrack>, base: Int? = null) {
tracks.forEachIndexed { index, track ->
val artistDoc = database.getDocument("artist:${track.artist.id}")?.toMutable() ?: MutableDocument("artist:${track.artist.id}")
artistDoc.run {
setString("type", "artist")
setInt("id", track.artist.id)
setString("name", track.artist.name)
database.save(this)
}
track.album?.let { album ->
val albumDoc = database.getDocument("album:${album.id}")?.toMutable() ?: MutableDocument("album:${album.id}")
albumDoc.run {
setString("type", "album")
setInt("id", album.id)
setInt("artist_id", album.artist.id)
setString("artist_name", album.artist.name)
setString("title", album.title)
setString("release_date", album.release_date)
album.cover?.urls?.original?.let { cover ->
setString("cover", cover)
}
database.save(this)
}
}
val doc = database.getDocument("track:${track.id}")?.toMutable() ?: MutableDocument("track:${track.id}")
doc.run {
setString("type", "track")
setInt("id", track.id)
track.album?.let { album ->
setInt("albumId", album.id)
}
setInt("artistId", track.artist.id)
setString("title", track.title)
setInt("position", track.position)
setInt("disc_number", track.disc_number ?: 0)
setString("copyright", track.copyright)
setString("license", track.license)
setString("uploads", track.uploads.getOrNull(0)?.listen_url ?: "")
database.save(this)
}
}
}
} }
@Serializable @Serializable

View File

@ -4,9 +4,6 @@ import androidx.lifecycle.LiveData
import androidx.paging.DataSource import androidx.paging.DataSource
import androidx.room.* import androidx.room.*
import com.github.apognu.otter.models.api.FunkwhaleArtist import com.github.apognu.otter.models.api.FunkwhaleArtist
import io.realm.RealmObject
import io.realm.annotations.Required
@Entity(tableName = "artists") @Entity(tableName = "artists")
data class ArtistEntity( data class ArtistEntity(
@PrimaryKey @PrimaryKey
@ -59,14 +56,3 @@ data class DecoratedArtistEntity(
val album_count: Int, val album_count: Int,
val album_cover: String? val album_cover: String?
) )
open class RealmArtist(
@io.realm.annotations.PrimaryKey
var id: Int = 0,
@Required
var name: String = ""
) : RealmObject()
fun FunkwhaleArtist.toRealmDao() = run {
RealmArtist(id, name)
}

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedAlbumEntity import com.github.apognu.otter.models.dao.DecoratedAlbumEntity
data class Album( data class Album(
@ -12,6 +13,17 @@ data class Album(
): SearchResult { ): SearchResult {
companion object { companion object {
fun from(album: Result) = album.getDictionary(0).run {
Album(
getInt("id"),
getString("title") ?: "N/A",
0,
getString("cover"),
getString("release_date"),
getString("artist_name") ?: "N/A"
)
}
fun fromDecoratedEntity(entity: DecoratedAlbumEntity): Album = entity.run { fun fromDecoratedEntity(entity: DecoratedAlbumEntity): Album = entity.run {
Album( Album(
id, id,

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedArtistEntity import com.github.apognu.otter.models.dao.DecoratedArtistEntity
data class Artist( data class Artist(
@ -11,6 +12,15 @@ data class Artist(
) : SearchResult { ) : SearchResult {
companion object { companion object {
fun from(artist: Result) = artist.getDictionary(0).run {
Artist(
getInt("id"),
getString("name") ?: "N/A",
getArray("albums")?.count() ?: 0,
getString("cover")
)
}
fun fromDecoratedEntity(entity: DecoratedArtistEntity): Artist = entity.run { fun fromDecoratedEntity(entity: DecoratedArtistEntity): Artist = entity.run {
Artist( Artist(
id, id,

View File

@ -1,5 +1,6 @@
package com.github.apognu.otter.models.domain package com.github.apognu.otter.models.domain
import com.couchbase.lite.Result
import com.github.apognu.otter.models.dao.DecoratedTrackEntity import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.preference.PowerPreference import com.preference.PowerPreference
@ -23,6 +24,17 @@ data class Track(
) : SearchResult { ) : SearchResult {
companion object { companion object {
fun from(track: Result) = track.getDictionary(0).run {
Track(
getInt("id"),
getString("title") ?: "N/A",
getInt("position"),
getString("copyright"),
getString("license"),
false
)
}
fun fromDecoratedEntity(entity: DecoratedTrackEntity) = entity.run { fun fromDecoratedEntity(entity: DecoratedTrackEntity) = entity.run {
Track( Track(
id, id,

View File

@ -2,15 +2,16 @@ package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import com.couchbase.lite.*
import com.github.apognu.otter.models.api.FunkwhaleAlbum import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.dao.DecoratedAlbumEntity
import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.dao.toDao
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.molo17.couchbase.lite.*
import kotlinx.coroutines.GlobalScope
class AlbumsRepository(override val context: Context, private val database: OtterDatabase, artistId: Int?) : Repository<FunkwhaleAlbum>() { class AlbumsRepository(override val context: Context, private val couch: Database, artistId: Int?) : Repository<FunkwhaleAlbum>() {
override val upstream: Upstream<FunkwhaleAlbum> by lazy { override val upstream: Upstream<FunkwhaleAlbum> by lazy {
val url = val url =
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title" if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
@ -24,22 +25,36 @@ class AlbumsRepository(override val context: Context, private val database: Otte
} }
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> { override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach { FunkwhaleAlbum.persist(couch, data)
insert(it)
}
return super.onDataFetched(data) return super.onDataFetched(data)
} }
fun insert(album: FunkwhaleAlbum) = database.albums().insert(album.toDao()) fun insert(albums: List<FunkwhaleAlbum>) = FunkwhaleAlbum.persist(couch, albums)
fun all() = database.albums().allDecorated()
fun find(ids: List<Int>) = database.albums().findAllDecorated(ids)
fun ofArtist(id: Int): LiveData<List<DecoratedAlbumEntity>> { fun all() =
select(SelectResult.all())
.from(couch)
.where { "type" equalTo "album"}
.asFlow()
.asLiveData()
fun find(ids: List<Int>) =
select(SelectResult.all())
.from(couch)
.where { ("type" equalTo "album") and (Meta.id.`in`(*ids.map { Expression.string("album:$it") }.toTypedArray())) }
.asFlow()
.asLiveData(GlobalScope.coroutineContext)
fun ofArtist(id: Int): LiveData<ResultSet> {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
fetch().collect() fetch().collect()
} }
return database.albums().forArtistDecorated(id) return select(SelectResult.all())
.from(couch)
.where { ("type" equalTo "album") and ("artist_id" equalTo id) }
.asFlow()
.asLiveData(GlobalScope.coroutineContext)
} }
} }

View File

@ -1,51 +1,64 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData
import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.couchbase.lite.Database
import com.couchbase.lite.Expression
import com.couchbase.lite.Meta
import com.github.apognu.otter.models.Mediator
import com.github.apognu.otter.models.api.FunkwhaleArtist import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.dao.DecoratedArtistEntity import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.models.dao.OtterDatabase import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.models.dao.toDao import com.github.apognu.otter.viewmodels.CouchbasePagingSource
import com.github.apognu.otter.models.dao.toRealmDao import com.molo17.couchbase.lite.*
import io.realm.Realm import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collect class ArtistsRepository(override val context: Context, private val database: Database) : Repository<FunkwhaleArtist>() {
import kotlinx.coroutines.launch private val mediator = Mediator(context, database, this)
class ArtistsRepository(override val context: Context, private val database: OtterDatabase) : Repository<FunkwhaleArtist>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=id", FunkwhaleArtist.serializer()) HttpUpstream(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", FunkwhaleArtist.serializer())
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> { val pager = Pager(
scope.launch(IO) { // config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = AppContext.PAGE_SIZE * 5, prefetchDistance = 10 * AppContext.PAGE_SIZE, maxSize = 25 * AppContext.PAGE_SIZE, enablePlaceholders = false),
data.forEach { artist -> config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = 10, prefetchDistance = 2),
database.artists().insert(artist.toDao()) pagingSourceFactory = {
CouchbasePagingSource(this).apply {
Realm.getDefaultInstance().executeTransaction { realm -> mediator.addListener {
realm.insertOrUpdate(artist.toRealmDao()) invalidate()
}
artist.albums?.forEach { album ->
database.albums().insert(album.toDao(artist.id))
} }
} }
} },
remoteMediator = mediator
)
return super.onDataFetched(data) fun insert(artists: List<FunkwhaleArtist>) = FunkwhaleArtist.persist(database, artists)
}
fun insert(artist: FunkwhaleArtist) = database.artists().insert(artist.toDao()) fun all(page: Int) =
select(all())
.from(database)
.where { "type" equalTo "artist" }
.orderBy { "order".ascending() }
.limit(AppContext.PAGE_SIZE, AppContext.PAGE_SIZE * page)
.execute()
fun allPaged() = database.artists().allPaged() fun get(id: Int) =
select(all())
.from(database)
.where {
("type" equalTo "artist") and ("_id" equalTo "artist:$id")
}
.limit(1)
.execute()
.next()
.run { Artist.from(this) }
fun all(): LiveData<List<DecoratedArtistEntity>> { fun find(ids: List<Int>) =
scope.launch(IO) { select(all())
fetch().collect() .from(database)
} .where { ("type" equalTo "artist") and (Meta.id.`in`(*ids.map { Expression.string("artist:$it") }.toTypedArray())) }
.asFlow()
return database.artists().allDecorated() .asLiveData(GlobalScope.coroutineContext)
}
fun get(id: Int) = database.artists().getDecorated(id)
fun find(ids: List<Int>) = database.artists().findDecorated(ids)
} }

View File

@ -8,6 +8,7 @@ import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.FuelError import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult import com.github.kittinunf.fuel.coroutines.awaitObjectResult
import com.github.kittinunf.fuel.coroutines.awaitStringResponseResult
import com.github.kittinunf.result.Result import com.github.kittinunf.result.Result
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -49,8 +50,6 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
} }
}, },
{ error -> { error ->
error.log()
when (error.exception) { when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut) is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(listOf(), page, false)) else -> emit(Repository.Response(listOf(), page, false))
@ -67,13 +66,18 @@ class HttpUpstream<D : Any>(val behavior: Behavior, private val url: String, pri
} }
} }
val (_, response, result) = request.awaitObjectResponseResult(AppContext.deserializer(OtterResponseSerializer(serializer))) val (_, response, result) = request.awaitStringResponseResult()
val items = AppContext.deserializer(OtterResponseSerializer(serializer)).deserialize(result.get())
if (response.statusCode == 401) { if (response.statusCode == 401) {
return retryGet(url) return retryGet(url)
} }
result items?.let {
return Result.success(items)
}
Result.error(FuelError.wrap(Exception("")))
} catch (e: Exception) { } catch (e: Exception) {
Result.error(FuelError.wrap(e)) Result.error(FuelError.wrap(e))
} }

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.map import androidx.lifecycle.map
import com.couchbase.lite.Database
import com.github.apognu.otter.models.api.FunkwhaleAlbum import com.github.apognu.otter.models.api.FunkwhaleAlbum
import com.github.apognu.otter.models.api.FunkwhaleArtist import com.github.apognu.otter.models.api.FunkwhaleArtist
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
@ -24,13 +25,13 @@ class ArtistsSearchRepository(override val context: Context?, private val reposi
private val _ids: MutableLiveData<List<Int>> = MutableLiveData() private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Artist>> = Transformations.switchMap(_ids) { val results: LiveData<List<Artist>> = Transformations.switchMap(_ids) {
repository.find(it).map { artists -> artists.map { artist -> Artist.fromDecoratedEntity(artist) } } repository.find(it).map { result ->
result.map { artist -> Artist.from(artist) }
}
} }
override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> { override fun onDataFetched(data: List<FunkwhaleArtist>): List<FunkwhaleArtist> {
data.forEach { repository.insert(data)
repository.insert(it)
}
ids.addAll(data.map { it.id }) ids.addAll(data.map { it.id })
_ids.postValue(ids) _ids.postValue(ids)
@ -57,13 +58,13 @@ class AlbumsSearchRepository(override val context: Context?, private val reposit
private val _ids: MutableLiveData<List<Int>> = MutableLiveData() private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Album>> = Transformations.switchMap(_ids) { val results: LiveData<List<Album>> = Transformations.switchMap(_ids) {
repository.find(it).map { albums -> albums.map { album -> Album.fromDecoratedEntity(album) } } repository.find(it).map { result ->
result.map { album -> Album.from(album) }
}
} }
override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> { override fun onDataFetched(data: List<FunkwhaleAlbum>): List<FunkwhaleAlbum> {
data.forEach { repository.insert(data)
repository.insert(it)
}
ids.addAll(data.map { it.id }) ids.addAll(data.map { it.id })
_ids.postValue(ids) _ids.postValue(ids)
@ -91,13 +92,13 @@ class TracksSearchRepository(override val context: Context?, private val reposit
private val _ids: MutableLiveData<List<Int>> = MutableLiveData() private val _ids: MutableLiveData<List<Int>> = MutableLiveData()
val results: LiveData<List<Track>> = Transformations.switchMap(_ids) { val results: LiveData<List<Track>> = Transformations.switchMap(_ids) {
repository.find(it).map { tracks -> tracks.map { track -> Track.fromDecoratedEntity(track) } } repository.find(it).map { result ->
result.map { track -> Track.from(track) }
}
} }
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> { override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> {
data.forEach { repository.insert(data)
repository.insert(it)
}
ids.addAll(data.map { it.id }) ids.addAll(data.map { it.id })
_ids.postValue(ids) _ids.postValue(ids)

View File

@ -2,11 +2,13 @@ package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.couchbase.lite.*
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.models.api.FunkwhaleTrack import com.github.apognu.otter.models.api.FunkwhaleTrack
import com.github.apognu.otter.models.dao.DecoratedTrackEntity import com.github.apognu.otter.models.dao.DecoratedTrackEntity
import com.github.apognu.otter.models.dao.OtterDatabase import com.github.apognu.otter.models.dao.OtterDatabase
import com.github.apognu.otter.models.domain.Track import com.github.apognu.otter.models.domain.Track
import com.github.apognu.otter.utils.asLiveData
import com.github.apognu.otter.utils.getMetadata import com.github.apognu.otter.utils.getMetadata
import com.github.apognu.otter.utils.maybeNormalizeUrl import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
@ -15,7 +17,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class TracksRepository(override val context: Context, private val database: OtterDatabase, albumId: Int?) : Repository<FunkwhaleTrack>() { class TracksRepository(override val context: Context, private val database: OtterDatabase, private val couch: Database, albumId: Int?) : Repository<FunkwhaleTrack>() {
override val upstream = override val upstream =
HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer()) HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", FunkwhaleTrack.serializer())
@ -39,18 +41,21 @@ class TracksRepository(override val context: Context, private val database: Otte
} }
override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking { override fun onDataFetched(data: List<FunkwhaleTrack>): List<FunkwhaleTrack> = runBlocking {
data.forEach { track -> FunkwhaleTrack.persist(couch, data)
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
}
data.sortedWith(compareBy({ it.disc_number }, { it.position })) data.sortedWith(compareBy({ it.disc_number }, { it.position }))
} }
fun insert(track: FunkwhaleTrack) { fun insert(tracks: List<FunkwhaleTrack>) = FunkwhaleTrack.persist(couch, tracks)
database.tracks().insertWithAssocs(database.artists(), database.albums(), database.uploads(), track)
}
fun find(ids: List<Int>) = database.tracks().findAllDecorated(ids) fun find(ids: List<Int>) = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(couch))
.where(
Expression.property("type").equalTo(Expression.string("track"))
.and(Meta.id.`in`(*ids.map { Expression.string("track:$it") }.toTypedArray()))
)
.asLiveData()
suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id) suspend fun ofArtistBlocking(id: Int) = database.tracks().ofArtistBlocking(id)

View File

@ -2,6 +2,9 @@ package com.github.apognu.otter.utils
import android.os.Build import android.os.Build
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import com.couchbase.lite.*
import com.couchbase.lite.Array
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.BrowseFragment import com.github.apognu.otter.fragments.BrowseFragment
import com.github.apognu.otter.models.api.DownloadInfo import com.github.apognu.otter.models.api.DownloadInfo
@ -77,3 +80,32 @@ fun Request.authorize(): Request {
} }
fun Download.getMetadata(): DownloadInfo? = AppContext.json.parse(DownloadInfo.serializer(), String(this.request.data)) fun Download.getMetadata(): DownloadInfo? = AppContext.json.parse(DownloadInfo.serializer(), String(this.request.data))
class DocumentSetLiveData(val query: Query) : LiveData<List<Result>>() {
private var token: ListenerToken? = null
override fun onActive() {
token = query.addChangeListener {
postValue(query.execute().allResults())
}
query.execute()
}
override fun onInactive() {
token?.let {
query.removeChangeListener(it)
token = null
}
}
}
fun Query.asLiveData() = DocumentSetLiveData(this)
fun <T : Any> List<T>?.toCouchbaseArray(): Array {
this?.let {
return MutableArray(this)
}
return MutableArray(listOf())
}

View File

@ -12,11 +12,11 @@ class AlbumsViewModel(private val repository: AlbumsRepository, private val trac
val albums: LiveData<List<Album>> by lazy { val albums: LiveData<List<Album>> by lazy {
if (artistId == null) { if (artistId == null) {
Transformations.map(repository.all()) { Transformations.map(repository.all()) {
it.map { album -> Album.fromDecoratedEntity(album) } it.map { result -> Album.from(result) }
} }
} else { } else {
Transformations.map(repository.ofArtist(artistId)) { Transformations.map(repository.ofArtist(artistId)) {
it.map { album -> Album.fromDecoratedEntity(album) } it.map { result -> Album.from(result) }
} }
} }
} }

View File

@ -1,30 +1,37 @@
package com.github.apognu.otter.viewmodels package com.github.apognu.otter.viewmodels
import androidx.lifecycle.* import androidx.lifecycle.ViewModel
import androidx.paging.Pager import androidx.lifecycle.asLiveData
import androidx.paging.PagingConfig import androidx.lifecycle.viewModelScope
import androidx.paging.PagingSource
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.map
import com.github.apognu.otter.models.Mediator
import com.github.apognu.otter.models.domain.Artist import com.github.apognu.otter.models.domain.Artist
import com.github.apognu.otter.repositories.ArtistsRepository import com.github.apognu.otter.repositories.ArtistsRepository
import com.github.apognu.otter.utils.AppContext import com.github.apognu.otter.utils.AppContext
import kotlinx.coroutines.flow.map
class ArtistsViewModel(repository: ArtistsRepository, mediator: Mediator) : ViewModel() { class CouchbasePagingSource(val repository: ArtistsRepository) : PagingSource<Int, Artist>() {
private val pager = Pager( override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Artist> {
config = PagingConfig(pageSize = AppContext.PAGE_SIZE, initialLoadSize = AppContext.PAGE_SIZE * 5, prefetchDistance = 10 * AppContext.PAGE_SIZE, maxSize = 25 * AppContext.PAGE_SIZE, enablePlaceholders = false), return try {
pagingSourceFactory = repository.allPaged().asPagingSourceFactory(), val page = params.key ?: 0
remoteMediator = mediator
)
val artistsPaged = pager val artists = repository.all(page).map { Artist.from(it) }
.flow val prevKey = if (page > 0) page - 1 else null
.map { artists -> artists.map { Artist.fromDecoratedEntity(it) } } val nextKey = if (artists.isNotEmpty() && artists.size == AppContext.PAGE_SIZE) page + 1 else null
.cachedIn(viewModelScope)
.asLiveData()
val artists: LiveData<List<Artist>> = repository.all().map { artists -> LoadResult.Page(
artists.map { Artist.fromDecoratedEntity(it) } data = artists,
prevKey = prevKey,
nextKey = nextKey
)
} catch (e: Exception) {
LoadResult.Error(e)
}
} }
} }
class ArtistsViewModel(repository: ArtistsRepository) : ViewModel() {
val artists = repository.pager
.flow
.cachedIn(viewModelScope)
.asLiveData(viewModelScope.coroutineContext)
}

View File

@ -30,7 +30,7 @@ class FavoritesViewModel(private val repository: FavoritesRepository, private va
val ids = it.map { favorite -> favorite.track_id } val ids = it.map { favorite -> favorite.track_id }
Transformations.map(tracksRepository.find(ids)) { tracks -> Transformations.map(tracksRepository.find(ids)) { tracks ->
tracks.map { track -> Track.fromDecoratedEntity(track) }.sortedBy { it.title } tracks.map { track -> Track.from(track) }.sortedBy { it.title }
} }
} }
} }

View File

@ -5,9 +5,8 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:4.0.0") classpath("com.android.tools.build:gradle:4.0.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
classpath("io.realm:realm-gradle-plugin:10.0.0-BETA.6")
} }
} }