From d82d0adab60cdec04da41744ebc9e7a703d99cd9 Mon Sep 17 00:00:00 2001 From: Florina Muntenescu Date: Mon, 30 Mar 2020 16:44:34 +0100 Subject: [PATCH] Paging with network and database --- app/build.gradle | 1 + .../android/codelabs/paging/Injection.kt | 14 ++-- .../paging/data/GithubPagingSource.kt | 3 +- .../paging/data/GithubRemoteMediator.kt | 70 +++++++++++++++++++ .../codelabs/paging/data/GithubRepository.kt | 18 ++++- .../android/codelabs/paging/db/RepoDao.kt | 43 ++++++++++++ .../codelabs/paging/db/RepoDatabase.kt | 53 ++++++++++++++ .../paging/ui/SearchRepositoriesActivity.kt | 2 +- build.gradle | 2 +- 9 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt create mode 100644 app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt create mode 100644 app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt diff --git a/app/build.gradle b/app/build.gradle index 7ad34dd9..8ffd7b16 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.paging:paging-runtime:$pagingVersion" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion" kapt "androidx.room:room-compiler:$roomVersion" diff --git a/app/src/main/java/com/example/android/codelabs/paging/Injection.kt b/app/src/main/java/com/example/android/codelabs/paging/Injection.kt index 619d00c9..014d866c 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/Injection.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/Injection.kt @@ -20,7 +20,9 @@ import android.content.Context import androidx.lifecycle.ViewModelProvider import com.example.android.codelabs.paging.api.GithubService import com.example.android.codelabs.paging.data.GithubRepository +import com.example.android.codelabs.paging.db.RepoDatabase import com.example.android.codelabs.paging.ui.ViewModelFactory +import kotlinx.coroutines.Dispatchers /** * Class that handles object creation. @@ -33,15 +35,19 @@ object Injection { * Creates an instance of [GithubRepository] based on the [GithubService] and a * [GithubLocalCache] */ - private fun provideGithubRepository(): GithubRepository { - return GithubRepository(GithubService.create()) + private fun provideGithubRepository(context: Context): GithubRepository { + return GithubRepository(GithubService.create(), provideDatabase(context), Dispatchers.IO) + } + + private fun provideDatabase(context: Context): RepoDatabase { + return RepoDatabase.getInstance(context) } /** * Provides the [ViewModelProvider.Factory] that is then used to get a reference to * [ViewModel] objects. */ - fun provideViewModelFactory(): ViewModelProvider.Factory { - return ViewModelFactory(provideGithubRepository()) + fun provideViewModelFactory(context: Context): ViewModelProvider.Factory { + return ViewModelFactory(provideGithubRepository(context)) } } diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt b/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt index 5639b199..60debaf9 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt @@ -16,6 +16,7 @@ package com.example.android.codelabs.paging.data +import android.util.Log import androidx.paging.PagingSource import com.example.android.codelabs.paging.api.GithubService import com.example.android.codelabs.paging.api.IN_QUALIFIER @@ -25,7 +26,7 @@ import retrofit2.HttpException import java.io.IOException // GitHub page API is 1 based: https://developer.github.com/v3/#pagination -private const val GITHUB_STARTING_PAGE_INDEX = 1 +const val GITHUB_STARTING_PAGE_INDEX = 1 @ExperimentalCoroutinesApi class GithubPagingSource( diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt new file mode 100644 index 00000000..0c0cd747 --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRemoteMediator.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.codelabs.paging.data + +import android.util.Log +import androidx.paging.LoadType +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import com.example.android.codelabs.paging.data.GithubRepository.Companion.NETWORK_PAGE_SIZE +import com.example.android.codelabs.paging.db.RepoDatabase +import com.example.android.codelabs.paging.model.Repo +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +class GithubRemoteMediator( + private val pagingSource: GithubPagingSource, + private val repoDatabase: RepoDatabase, + private val ioDispatcher: CoroutineContext +) : RemoteMediator() { + + override suspend fun load(loadType: LoadType, state: PagingState?): MediatorResult { + Log.d("GithubRemoteMediator", "load type: $loadType, state: $state") + + val key = when (loadType) { + LoadType.REFRESH -> GITHUB_STARTING_PAGE_INDEX + LoadType.START -> state?.pages?.last()?.prevKey ?: GITHUB_STARTING_PAGE_INDEX + LoadType.END -> state?.pages?.last()?.nextKey ?: GITHUB_STARTING_PAGE_INDEX + } + + val result = pagingSource.load(PagingSource.LoadParams( + loadType = loadType, + key = key, + loadSize = NETWORK_PAGE_SIZE, + placeholdersEnabled = false, + pageSize = NETWORK_PAGE_SIZE + )) + + return when (result) { + is PagingSource.LoadResult.Page -> { + withContext(ioDispatcher) { + repoDatabase.withTransaction { + if (loadType == LoadType.REFRESH) { + repoDatabase.reposDao().clearRepos() + } + repoDatabase.reposDao().insert(result.data) + } + } + MediatorResult.Success(hasMoreData = result.nextKey != null) + } + is PagingSource.LoadResult.Error -> MediatorResult.Error(result.throwable) + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt index 2cf1b249..1b6eed01 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt @@ -21,15 +21,21 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.PagingDataFlow import com.example.android.codelabs.paging.api.GithubService +import com.example.android.codelabs.paging.db.RepoDatabase import com.example.android.codelabs.paging.model.Repo import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlin.coroutines.CoroutineContext /** * Repository class that works with local and remote data sources. */ @ExperimentalCoroutinesApi -class GithubRepository(private val service: GithubService) { +class GithubRepository( + private val service: GithubService, + private val database: RepoDatabase, + private val ioDispatcher: CoroutineContext +) { /** * Search repositories whose names match the query, exposed as a stream of data that will emit @@ -38,13 +44,19 @@ class GithubRepository(private val service: GithubService) { fun getSearchResultStream(query: String): Flow> { Log.d("GithubRepository", "New query: $query") + val pagingSourceFactory = database.reposDao().reposByName().asPagingSourceFactory() return PagingDataFlow( config = PagingConfig(pageSize = NETWORK_PAGE_SIZE), - pagingSourceFactory = { GithubPagingSource(service, query) } + remoteMediator = GithubRemoteMediator( + GithubPagingSource(service, query), + database, + ioDispatcher + ), + pagingSourceFactory = pagingSourceFactory ) } companion object { - private const val NETWORK_PAGE_SIZE = 50 + const val NETWORK_PAGE_SIZE = 50 } } diff --git a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt new file mode 100644 index 00000000..a71c6eef --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.codelabs.paging.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.example.android.codelabs.paging.model.Repo + +/** + * Room data access object for accessing the [Repo] table. + */ +@Dao +interface RepoDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(posts: List) + + // Do a similar query as the search API: + // Look for repos that contain the query string in the name or in the description + // and order those results descending, by the number of stars and then by name + @Query("SELECT * FROM repos ORDER BY stars DESC, name ASC") + fun reposByName(): DataSource.Factory + + @Query("DELETE FROM repos") + suspend fun clearRepos() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt new file mode 100644 index 00000000..b013bebc --- /dev/null +++ b/app/src/main/java/com/example/android/codelabs/paging/db/RepoDatabase.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.codelabs.paging.db + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import android.content.Context +import com.example.android.codelabs.paging.model.Repo + +/** + * Database schema that holds the list of repos. + */ +@Database( + entities = [Repo::class], + version = 1, + exportSchema = false +) +abstract class RepoDatabase : RoomDatabase() { + + abstract fun reposDao(): RepoDao + + companion object { + + @Volatile + private var INSTANCE: RepoDatabase? = null + + fun getInstance(context: Context): RepoDatabase = + INSTANCE ?: synchronized(this) { + INSTANCE + ?: buildDatabase(context).also { INSTANCE = it } + } + + private fun buildDatabase(context: Context) = + Room.databaseBuilder(context.applicationContext, + RepoDatabase::class.java, "Github.db") + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt index 8ab04500..247de382 100644 --- a/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt +++ b/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt @@ -50,7 +50,7 @@ class SearchRepositoriesActivity : AppCompatActivity() { setContentView(view) // get the view model - viewModel = ViewModelProvider(this, Injection.provideViewModelFactory()) + viewModel = ViewModelProvider(this, Injection.provideViewModelFactory(applicationContext)) .get(SearchRepositoriesViewModel::class.java) // add dividers between RecyclerView's row items diff --git a/build.gradle b/build.gradle index 130aa572..deea205a 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { repositories { google() jcenter() - maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6295087/artifacts/repository' } + maven { url 'https://androidx-dev-prod.appspot.com/snapshots/builds/6345264/artifacts/repository' } } }