Skip to content

Commit

Permalink
DB based paging (#19)
Browse files Browse the repository at this point in the history
- Uses RemoteMediator to page the movie list from the database while fetching new data from the network.
- Upgrades SQLDelight to 2.0.0-alpha05 to fix cashapp/sqldelight#2434
  • Loading branch information
julioromano committed Mar 17, 2023
1 parent 9a8eae6 commit d98a5fa
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package net.marcoromano.mooviez.database

import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver

public fun DatabaseFake(): Database = Database(
driver = JdbcSqliteDriver(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package net.marcoromano.mooviez.database

import android.content.Context
import androidx.sqlite.db.SupportSQLiteDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver

public fun DatabaseImpl(context: Context): Database = Database(
driver = AndroidSqliteDriver(
Expand Down
9 changes: 6 additions & 3 deletions database/public/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ plugins {
}

sqldelight {
database("Database") {
packageName = "net.marcoromano.mooviez.database"
schemaOutputDirectory = file("src/main/sqldelight/schema")
databases {
create("Database") {
packageName.set("net.marcoromano.mooviez.database")
dialect(libs.square.sqlite18dialect)
schemaOutputDirectory.set(file("src/main/sqldelight/schema"))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
CREATE TABLE Movie (
id INTEGER PRIMARY KEY NOT NULL,
position INTEGER PRIMARY KEY NOT NULL,
id INTEGER NOT NULL,
title TEXT NOT NULL,
poster_path TEXT NOT NULL,
overview TEXT NOT NULL,
vote_average REAL NOT NULL
vote_average REAL NOT NULL,
release_date TEXT NOT NULL
);

getMovies:
SELECT
id,
title,
poster_path,
overview,
vote_average
FROM Movie
;
CREATE TABLE MoviePage (
id INTEGER PRIMARY KEY NOT NULL,
next_page INTEGER
);

getMovie:
SELECT
id,
title,
poster_path,
overview,
vote_average
insertMovie:
INSERT OR REPLACE INTO Movie VALUES ?;

countMovies:
SELECT count(*) FROM Movie;

movies:
SELECT *
FROM Movie
WHERE id = ?
;
ORDER BY position ASC
LIMIT :limit OFFSET :offset;

insertMovie:
INSERT OR REPLACE INTO Movie (
id,
title,
poster_path,
overview,
vote_average
)
VALUES (?,?,?,?,?)
;
insertNextPage:
INSERT OR REPLACE INTO MoviePage(id, next_page) VALUES (0, ?);

deleteMovies:
DELETE FROM Movie;
nextPage:
SELECT next_page FROM MoviePage WHERE id == 0;
Binary file not shown.
15 changes: 8 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ plugin-playPublisher = "3.8.1"
square-moshi = "1.14.0"
square-okhttp = "4.10.0"
square-retrofit = "2.9.0"
square-sqlDelight = "1.5.5"
square-sqlDelight = "2.0.0-alpha05"

[plugins]
android-application = { id = "com.android.application", version.ref = "plugin-android" }
Expand All @@ -50,7 +50,7 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref =
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinter = { id = "org.jmailen.kotlinter", version.ref = "plugin-kotlinter" }
playPublisher = { id = "com.github.triplet.play", version.ref = "plugin-playPublisher" }
square-sqldelight = { id = "com.squareup.sqldelight", version.ref = "square-sqlDelight" }
square-sqldelight = { id = "app.cash.sqldelight", version.ref = "square-sqlDelight" }

[libraries]
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
Expand Down Expand Up @@ -164,7 +164,7 @@ plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
plugin-kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
plugin-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "plugin-kotlinter" }
plugin-playPublisher = { module = "com.github.triplet.gradle:play-publisher", version.ref = "plugin-playPublisher" }
plugin-square-sqldelight = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "square-sqlDelight" }
plugin-square-sqldelight = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "square-sqlDelight" }
robolectric = "org.robolectric:robolectric:4.9.2"
square-leakcanary = "com.squareup.leakcanary:leakcanary-android:2.10"
square-moshi = { module = "com.squareup.moshi:moshi", version.ref = "square-moshi" }
Expand All @@ -178,10 +178,11 @@ square-okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", ve
square-okio = "com.squareup.okio:okio:3.3.0"
square-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "square-retrofit" }
square-retrofitConverterMoshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "square-retrofit" }
square-sqlDelightAndroid = { module = "com.squareup.sqldelight:android-driver", version.ref = "square-sqlDelight" }
square-sqlDelightAndroidPaging3 = { module = "com.squareup.sqldelight:android-paging3-extensions", version.ref = "square-sqlDelight" }
square-sqlDelightCoroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "square-sqlDelight" }
square-sqlDelightJvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "square-sqlDelight" }
square-sqlDelightAndroid = { module = "app.cash.sqldelight:android-driver", version.ref = "square-sqlDelight" }
square-sqlDelightAndroidPaging3 = { module = "app.cash.sqldelight:androidx-paging3-extensions-jvm", version.ref = "square-sqlDelight" }
square-sqlDelightCoroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "square-sqlDelight" }
square-sqlDelightJvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "square-sqlDelight" }
square-sqlite18dialect = { module = "app.cash.sqldelight:sqlite-3-18-dialect", version.ref = "square-sqlDelight" }
square-turbine = "app.cash.turbine:turbine:0.12.1"

[bundles]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import net.marcoromano.mooviez.httpapi.TrendingMovies
import net.marcoromano.mooviez.database.Movie
import net.marcoromano.mooviez.widgets.UserScore

@Composable
Expand All @@ -59,7 +59,7 @@ internal fun TrendingLazyVerticalGrid(
private fun TrendingLazyVerticalGrid(
columns: GridCells,
modifier: Modifier,
pager: Flow<PagingData<TrendingMovies.Movie>>,
pager: Flow<PagingData<Movie>>,
onMovieClick: (id: Long) -> Unit,
) {
val trendingPagingData = pager.collectAsLazyPagingItems()
Expand All @@ -86,7 +86,7 @@ private fun TrendingLazyVerticalGrid(

@Composable
private fun Movie(
movie: TrendingMovies.Movie,
movie: Movie,
navToDetail: (id: Long) -> Unit,
) {
Column(
Expand Down Expand Up @@ -142,7 +142,8 @@ private fun Movie(
@Composable
private fun PreviewMovie() {
Movie(
movie = TrendingMovies.Movie(
movie = Movie(
position = 0,
id = 0,
title = "A very long title that must be wrapped in multiple lines",
poster_path = "https://dummyimage.com/500x750/000/fff.jpg",
Expand All @@ -163,15 +164,17 @@ private fun PreviewTrending() {
pager = flowOf(
PagingData.from(
listOf(
TrendingMovies.Movie(
Movie(
position = 0,
id = 0,
title = "A very long title that must be wrapped in multiple lines",
poster_path = "https://dummyimage.com/500x750/000/fff.jpg",
overview = "Once upon a time...",
vote_average = 1.2,
release_date = "2022-02-03",
),
TrendingMovies.Movie(
Movie(
position = 0,
id = 0,
title = "A very long title that must be wrapped in multiple lines",
poster_path = "https://dummyimage.com/500x750/000/fff.jpg",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,86 @@
package net.marcoromano.mooviez.trending.widgets.trending

import androidx.lifecycle.ViewModel
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import app.cash.sqldelight.paging3.QueryPagingSource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import net.marcoromano.mooviez.database.Database
import net.marcoromano.mooviez.database.Movie
import net.marcoromano.mooviez.httpapi.HttpApi
import net.marcoromano.mooviez.httpapi.TrendingMovies
import javax.inject.Inject

@HiltViewModel
internal class TrendingLazyVerticalGridViewModel @Inject constructor(
private val httpApi: HttpApi,
httpApi: HttpApi,
private val database: Database,
) : ViewModel() {
val pager = Pager(PagingConfig(pageSize = 20)) { // 20 comes from tmdb api docs
NetworkPagingSource(httpApi)
@OptIn(ExperimentalPagingApi::class)
val pager = Pager(
config = PagingConfig(pageSize = 20), // 20 comes from tmdb api docs
remoteMediator = Mediator(httpApi = httpApi, database = database),
) {
QueryPagingSource(
countQuery = database.movieQueries.countMovies(),
transacter = database.movieQueries,
context = Dispatchers.IO,
queryProvider = database.movieQueries::movies,
)
}.flow
}

private class NetworkPagingSource(
@OptIn(ExperimentalPagingApi::class)
private class Mediator(
private val httpApi: HttpApi,
) : PagingSource<Int, TrendingMovies.Movie>() {
override fun getRefreshKey(state: PagingState<Int, TrendingMovies.Movie>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
private val database: Database,
) : RemoteMediator<Int, Movie>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Movie>,
): MediatorResult {
return try {
val loadPage: Long? = when (loadType) {
LoadType.REFRESH -> null
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val remoteKey = database.movieQueries.nextPage().executeAsOne()
if (remoteKey.next_page == null) {
return MediatorResult.Success(endOfPaginationReached = true)
}
remoteKey.next_page
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TrendingMovies.Movie> {
return runCatching {
httpApi.trendingMovies(page = params.key ?: 1)
}.fold(
{
LoadResult.Page(
data = it.results,
prevKey = if (it.page > 1) it.page - 1 else null,
nextKey = if (it.page < it.total_pages) it.page + 1 else null,
)
},
{
LoadResult.Error(it)
},
)
val movies = httpApi.trendingMovies(page = loadPage?.toInt() ?: 1)
val nextPage = if (movies.page < movies.total_pages) movies.page + 1 else null

database.movieQueries.apply {
transaction {
movies.results.forEachIndexed { i, movie ->
insertMovie(
Movie(
position = (movies.page - 1) * 20L + i,
id = movie.id,
title = movie.title,
poster_path = movie.poster_path,
overview = movie.overview,
vote_average = movie.vote_average,
release_date = movie.release_date,
),
)
insertNextPage(nextPage?.toLong())
}
}
}

MediatorResult.Success(endOfPaginationReached = nextPage == null)
} catch (e: RuntimeException) {
return MediatorResult.Error(e)
}
}
}

0 comments on commit d98a5fa

Please sign in to comment.