Skip to content

Commit

Permalink
Feat/paging (#13)
Browse files Browse the repository at this point in the history
* rewrited paging with jetpack paging 3;
* db schema changes.
  • Loading branch information
kid1412621 authored May 31, 2024
1 parent d6489b4 commit ee4d14b
Show file tree
Hide file tree
Showing 29 changed files with 908 additions and 364 deletions.
10 changes: 6 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.android.build.api.dsl.ApkSigningConfig
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
import java.io.FileInputStream
import java.util.Properties

Expand All @@ -25,7 +26,7 @@ android {
targetSdk = 34
versionCode = 9
versionName = "0.2.2"
setProperty("archivesBaseName", "subspace-v${versionName}-${versionCode}")
archivesName = "subspace-v${versionName}-${versionCode}"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down Expand Up @@ -130,8 +131,8 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-tooling")

// paging
implementation("androidx.paging:paging-compose:3.3.0-rc01")
implementation("androidx.paging:paging-runtime-ktx:3.3.0-rc01")
implementation("androidx.paging:paging-compose:3.3.0")
implementation("androidx.paging:paging-runtime-ktx:3.3.0")

// nav
implementation("androidx.navigation:navigation-compose")
Expand All @@ -141,7 +142,7 @@ dependencies {
// implementation("androidx.compose.material3:material3")
// wait for https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#DismissibleDrawerSheet(androidx.compose.material3.DrawerState,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.WindowInsets,kotlin.Function1)
// see: https://developer.android.com/jetpack/androidx/releases/compose-material3#1.3.0-alpha04
implementation("androidx.compose.material3:material3:1.3.0-alpha06")
implementation("androidx.compose.material3:material3:1.3.0-beta01")
// wait for MD3 implementation
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.animation:animation:1.6.7")
Expand All @@ -161,6 +162,7 @@ dependencies {
// room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-paging:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")

// retrofit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package me.nanova.subspace.data

import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import me.nanova.subspace.data.api.QTApiService
import me.nanova.subspace.data.db.AppDatabase
import me.nanova.subspace.domain.model.QTListParams
import me.nanova.subspace.domain.model.RemoteKeys
import me.nanova.subspace.domain.model.Torrent
import me.nanova.subspace.domain.model.toEntity

@OptIn(ExperimentalPagingApi::class)
class TorrentRemoteMediator(
private val currentAccountId: Long,
private val query: QTListParams,
private val database: AppDatabase,
private val networkService: QTApiService
) : RemoteMediator<Int, Torrent>() {

private val torrentDao = database.torrentDao()
private val remoteKeyDao = database.remoteKeyDao()

override suspend fun initialize(): InitializeAction {
// return InitializeAction.SKIP_INITIAL_REFRESH
return InitializeAction.LAUNCH_INITIAL_REFRESH
}

override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Torrent>
): MediatorResult {
return try {
val offset = when (loadType) {
LoadType.REFRESH -> 0
LoadType.APPEND -> {
val remoteKeys = state.pages
.lastOrNull() { it.data.isNotEmpty() } // Find the first page with items
?.data?.lastOrNull() // Get the first item in that page
?.let { remoteKeyDao.remoteKeysItemId(it.id) }
remoteKeys?.nextOffset
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
LoadType.PREPEND -> {
val remoteKeys = state.pages
.firstOrNull { it.data.isNotEmpty() }
?.data?.firstOrNull()
?.let { remoteKeyDao.remoteKeysItemId(it.id) }
remoteKeys?.prevOffset
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
}

// fetch api
val response = networkService.list(
query.copy(offset = offset, limit = state.config.pageSize).toMap()
)
val endOfPaginationReached = response.size < state.config.pageSize

// update db
database.withTransaction {
if (loadType == LoadType.REFRESH) {
remoteKeyDao.clearRemoteKeys(currentAccountId)
torrentDao.clearAll(currentAccountId)
}

val entities = response.map { it.toEntity(currentAccountId) }
torrentDao.insertAll(entities)
val prevOffset = if (offset == 0) null else offset - state.config.pageSize
val nextOffset =
if (endOfPaginationReached) null else offset + state.config.pageSize
val keys = entities.map {
RemoteKeys(
torrentId = it.id,
prevOffset = prevOffset,
nextOffset = nextOffset,
accountId = currentAccountId
)
}
remoteKeyDao.insertAll(keys)
}

MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (e: Exception) {
// might display db data when network is unavailable, but not sure the user case, let decide in future
MediatorResult.Error(e)
}
}
}
60 changes: 34 additions & 26 deletions app/src/main/kotlin/me/nanova/subspace/data/TorrentRepoImpl.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,55 @@
package me.nanova.subspace.data

import kotlinx.coroutines.flow.map
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import me.nanova.subspace.data.api.QTApiService
import me.nanova.subspace.data.db.AppDatabase
import me.nanova.subspace.data.db.TorrentDao
import me.nanova.subspace.data.db.TorrentDao.Companion.buildQuery
import me.nanova.subspace.domain.model.Account
import me.nanova.subspace.domain.model.QTFilterState
import me.nanova.subspace.domain.model.QTListParams
import me.nanova.subspace.domain.model.Torrent
import me.nanova.subspace.domain.model.toEntity
import me.nanova.subspace.domain.repo.TorrentRepo
import javax.inject.Inject
import javax.inject.Provider

class TorrentRepoImpl @Inject constructor(
private val database: AppDatabase,
private val torrentDao: TorrentDao,
private val storage: Storage,
private val apiService: Provider<QTApiService>
) : TorrentRepo {
override suspend fun apiVersion() = apiService.get().version()

// override fun torrents() =
// torrentDao.getAll().map { model -> model.map { it.toModel() } }


override suspend fun refresh(params: Map<String, String?>) {
val list = apiService.get().getTorrents(params)
storage.currentAccountId.collect { id ->
val copy = list.map {
it.toEntity(id ?: throw RuntimeException("no current account"))
// override suspend fun apiVersion() = apiService.get().version()

@OptIn(ExperimentalPagingApi::class)
override fun torrents(account: Account, filter: QTListParams): Flow<PagingData<Torrent>> {
return Pager(
config = PagingConfig(
pageSize = PAGE_SIZE,
prefetchDistance = 1,
enablePlaceholders = true,
),
remoteMediator = TorrentRemoteMediator(
account.id,
filter,
database,
apiService.get()
),
pagingSourceFactory = {
torrentDao.pagingSource(buildQuery(account.id, filter))
}
torrentDao.insertAll(copy)
}
).flow
}

override suspend fun fetch(params: QTListParams): List<Torrent> {
val list = apiService.get().list(params.toMap())

// storage.currentAccountId.collect { id ->
// val copy = list.map {
// it.toEntity(id ?: throw RuntimeException("no current account"))
// }
// torrentDao.insertAll(copy)
// }
return list
companion object {
const val PAGE_SIZE = 20
}

}


Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.nanova.subspace.data.api

import kotlinx.coroutines.flow.Flow
import me.nanova.subspace.domain.model.Torrent
import retrofit2.http.GET
import retrofit2.http.QueryMap
Expand All @@ -10,9 +11,9 @@ interface QTApiService {
suspend fun version(): String

@GET("api/v2/torrents/info")
suspend fun getTorrents(@QueryMap params: Map<String, String?>): List<Torrent>
suspend fun list(@QueryMap params: Map<String, String?>): List<Torrent>

@GET("api/v2/torrents/info")
suspend fun list(@QueryMap params: Map<String, String?>): List<Torrent>
fun flow(@QueryMap params: Map<String, String?>): Flow<List<Torrent>>
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import me.nanova.subspace.data.AccountType
import me.nanova.subspace.domain.model.AccountType
import me.nanova.subspace.domain.model.Account

@Dao
Expand Down
75 changes: 72 additions & 3 deletions app/src/main/kotlin/me/nanova/subspace/data/db/AppDatabase.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,86 @@
package me.nanova.subspace.data.db

import android.util.Log
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import me.nanova.subspace.domain.model.Account
import me.nanova.subspace.domain.model.TorrentDB
import me.nanova.subspace.domain.model.RemoteKeys
import me.nanova.subspace.domain.model.TorrentEntity

@Database(
entities = [
Account::class,
TorrentDB::class
], version = 1
TorrentEntity::class,
RemoteKeys::class
], version = 2
)
abstract class AppDatabase : RoomDatabase() {
abstract fun torrentDao(): TorrentDao
abstract fun remoteKeyDao(): RemoteKeyDao
abstract fun accountDao(): AccountDao

companion object {
const val DATABASE_NAME = "subspace.db"

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
try {
db.execSQL("ALTER TABLE Account ADD COLUMN use_lan_switch INTEGER NOT NULL DEFAULT 0;")
db.execSQL("ALTER TABLE Account ADD COLUMN lan_url TEXT NOT NULL DEFAULT '';")
db.execSQL("ALTER TABLE Account ADD COLUMN lan_ssid TEXT NOT NULL DEFAULT '';")
// sqlite table name is case-insensitive
db.execSQL("ALTER TABLE Account RENAME TO tmp_account;")
db.execSQL("ALTER TABLE tmp_account RENAME TO account;")

// previous version didn't use this table
db.execSQL("DROP TABLE torrent;")
db.execSQL(
"""
CREATE TABLE torrent (
id TEXT PRIMARY KEY NOT NULL,
hash TEXT NOT NULL,
account_id INTEGER NOT NULL,
name TEXT NOT NULL,
added_on INTEGER NOT NULL,
size INTEGER NOT NULL,
downloaded INTEGER NOT NULL,
uploaded INTEGER NOT NULL,
progress REAL NOT NULL,
eta INTEGER NOT NULL,
state TEXT NOT NULL,
category TEXT,
tags TEXT,
dlspeed INTEGER NOT NULL,
upspeed INTEGER NOT NULL,
ratio REAL NOT NULL,
leechs INTEGER NOT NULL,
seeds INTEGER NOT NULL,
priority INTEGER NOT NULL,
last_updated INTEGER NOT NULL
);
"""
)
db.execSQL("CREATE INDEX index_torrent_hash ON torrent (hash);")
db.execSQL("CREATE INDEX index_torrent_account_id ON torrent (account_id);")

db.execSQL(
"""
CREATE TABLE remote_keys (
torrent_id TEXT PRIMARY KEY NOT NULL,
account_id INTEGER NOT NULL,
prev_offset INTEGER,
next_offset INTEGER
);
"""
)
db.execSQL("CREATE INDEX index_remote_keys_account_id ON remote_keys (account_id);")
} catch (e: Exception) {
Log.e("AppDatabase", "DB migration error: $e")
}
}
}
}

}
19 changes: 19 additions & 0 deletions app/src/main/kotlin/me/nanova/subspace/data/db/RemoteKeyDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.nanova.subspace.data.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import me.nanova.subspace.domain.model.RemoteKeys

@Dao
interface RemoteKeyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<RemoteKeys>)

@Query("SELECT * FROM remote_keys WHERE torrent_id = :id")
suspend fun remoteKeysItemId(id: String): RemoteKeys?

@Query("DELETE FROM remote_keys WHERE account_id = :accountId")
suspend fun clearRemoteKeys(accountId: Long)
}
Loading

0 comments on commit ee4d14b

Please sign in to comment.