diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/Paging-3.iml b/.idea/Paging-3.iml
new file mode 100644
index 00000000..697945a8
--- /dev/null
+++ b/.idea/Paging-3.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..237afc07
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..8146eaef
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/advanced/end/app/build.gradle b/advanced/end/app/build.gradle
index 3aaf3f2d..eaeb6627 100644
--- a/advanced/end/app/build.gradle
+++ b/advanced/end/app/build.gradle
@@ -51,6 +51,7 @@ android {
}
dependencies {
+ implementation freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
implementation fileTree(dir: 'libs')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt
new file mode 100644
index 00000000..1f00db26
--- /dev/null
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubPagingSource.kt
@@ -0,0 +1,44 @@
+// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
+private const val GITHUB_STARTING_PAGE_INDEX = 1
+
+class GithubPagingSource(
+ private val service: GithubService,
+ private val query: String
+) : PagingSource() {
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
+ val apiQuery = query + IN_QUALIFIER
+ return try {
+ val response = service.searchRepos(apiQuery, position, params.loadSize)
+ val repos = response.items
+ val nextKey = if (repos.isEmpty()) {
+ null
+ } else {
+ // initial load size = 3 * NETWORK_PAGE_SIZE
+ // ensure we're not requesting duplicating items, at the 2nd request
+ position + (params.loadSize / NETWORK_PAGE_SIZE)
+ }
+ LoadResult.Page(
+ data = repos,
+ prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
+ nextKey = nextKey
+ )
+ } catch (exception: IOException) {
+ return LoadResult.Error(exception)
+ } catch (exception: HttpException) {
+ return LoadResult.Error(exception)
+ }
+ }
+ // The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load
+ override fun getRefreshKey(state: PagingState): Int? {
+ // We need to get the previous key (or next key if previous is null) of the page
+ // that was closest to the most recently accessed index.
+ // Anchor position is the most recently accessed index
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt
index 855e8e9f..382d6811 100644
--- a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt
@@ -26,38 +26,34 @@ import com.example.android.codelabs.paging.db.RepoDatabase
import com.example.android.codelabs.paging.model.Repo
import kotlinx.coroutines.flow.Flow
-/**
- * Repository class that works with local and remote data sources.
+/** Paging 3 now : )
+ * 1. Handles in-memory cache.
+ 2. Requests data when the user is close to the end of the list.
*/
-class GithubRepository(
- private val service: GithubService,
- private val database: RepoDatabase
-) {
- /**
- * Search repositories whose names match the query, exposed as a stream of data that will emit
- * every time we get more data from the network.
- */
- fun getSearchResultStream(query: String): Flow> {
- Log.d("GithubRepository", "New query: $query")
+class GithubRepository(private val service: GithubService,
+ private val database: RepoDatabase
+) {
- // appending '%' so we can allow other characters to be before and after the query string
- val dbQuery = "%${query.replace(' ', '%')}%"
- val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) }
+ // appending '%' so we can allow other characters to be before and after the query string
+ val dbQuery = "%${query.replace(' ', '%')}%"
+ val pagingSourceFactory = { database.reposDao().reposByName(dbQuery)}
+ fun getSearchResultStream(query: String): Flow> {
@OptIn(ExperimentalPagingApi::class)
return Pager(
- config = PagingConfig(pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false),
- remoteMediator = GithubRemoteMediator(
- query,
- service,
- database
+ config = PagingConfig(
+ pageSize = NETWORK_PAGE_SIZE,
+ maxSize = NETWORK_MAX_SIZE,
+ enablePlaceholders = false
),
+ remoteMediator = GithubRemoteMediator(service, query, database) }
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
- const val NETWORK_PAGE_SIZE = 30
+ const val NETWORK_PAGE_SIZE = 50
+ const val NETWORK_MAX_SIZE = 150
}
}
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt
index 0449ca49..5e3a3f72 100644
--- a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/ReposAdapter.kt
@@ -63,7 +63,7 @@ class ReposAdapter : PagingDataAdapter(UIMODEL_COMPARATOR)
}
override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean =
- oldItem == newItem
+ oldItem == newItem
}
}
}
\ No newline at end of file
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt
index 23316fa7..65fc8608 100644
--- a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt
@@ -48,12 +48,7 @@ class SearchRepositoriesActivity : AppCompatActivity() {
setContentView(view)
// get the view model
- val viewModel = ViewModelProvider(
- this, Injection.provideViewModelFactory(
- context = this,
- owner = this
- )
- )
+ val viewModel = ViewModelProvider(this, Injection.provideViewModelFactory(owner = this, context = this))
.get(SearchRepositoriesViewModel::class.java)
// add dividers between RecyclerView's row items
@@ -74,21 +69,20 @@ class SearchRepositoriesActivity : AppCompatActivity() {
*/
private fun ActivitySearchRepositoriesBinding.bindState(
uiState: StateFlow,
- pagingData: Flow>,
+ pagingData: Flow>,
uiActions: (UiAction) -> Unit
) {
val repoAdapter = ReposAdapter()
- val header = ReposLoadStateAdapter { repoAdapter.retry() }
list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
- header = header,
+ header = ReposLoadStateAdapter { repoAdapter.retry() },
footer = ReposLoadStateAdapter { repoAdapter.retry() }
)
+
bindSearch(
uiState = uiState,
onQueryChanged = uiActions
)
bindList(
- header = header,
repoAdapter = repoAdapter,
uiState = uiState,
pagingData = pagingData,
@@ -135,21 +129,21 @@ class SearchRepositoriesActivity : AppCompatActivity() {
}
private fun ActivitySearchRepositoriesBinding.bindList(
- header: ReposLoadStateAdapter,
repoAdapter: ReposAdapter,
uiState: StateFlow,
- pagingData: Flow>,
+ pagingData: Flow>,
onScrollChanged: (UiAction.Scroll) -> Unit
) {
- retryButton.setOnClickListener { repoAdapter.retry() }
list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) onScrollChanged(UiAction.Scroll(currentQuery = uiState.value.query))
}
})
val notLoading = repoAdapter.loadStateFlow
- .asRemotePresentationState()
- .map { it == RemotePresentationState.PRESENTED }
+ // Only emit when REFRESH LoadState for the paging source changes.
+ .distinctUntilChangedBy { it.source.refresh }
+ // Only react to cases where REFRESH completes i.e., NotLoading.
+ .map { it.source.refresh is LoadState.NotLoading }
val hasNotScrolledForCurrentSearch = uiState
.map { it.hasNotScrolledForCurrentSearch }
@@ -162,6 +156,19 @@ class SearchRepositoriesActivity : AppCompatActivity() {
)
.distinctUntilChanged()
+
+ // Collecting from loadStateFlow directly.
+ lifecycleScope.launch {
+ repoAdapter.loadStateFlow.collect { loadState ->
+ val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
+ // show empty list
+ emptyList.isVisible = isListEmpty
+ // Only show the list if refresh succeeds.
+ list.isVisible = !isListEmpty
+ }
+ }
+
+ /*
lifecycleScope.launch {
pagingData.collectLatest(repoAdapter::submitData)
}
@@ -171,38 +178,7 @@ class SearchRepositoriesActivity : AppCompatActivity() {
if (shouldScroll) list.scrollToPosition(0)
}
}
+ */
- lifecycleScope.launch {
- repoAdapter.loadStateFlow.collect { loadState ->
- // Show a retry header if there was an error refreshing, and items were previously
- // cached OR default to the default prepend state
- header.loadState = loadState.mediator
- ?.refresh
- ?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 }
- ?: loadState.prepend
-
- val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
- // show empty list
- emptyList.isVisible = isListEmpty
- // Only show the list if refresh succeeds, either from the the local db or the remote.
- list.isVisible = loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
- // Show loading spinner during initial load or refresh.
- progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
- // Show the retry state if initial load or refresh fails.
- retryButton.isVisible = loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
- // Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
- val errorState = loadState.source.append as? LoadState.Error
- ?: loadState.source.prepend as? LoadState.Error
- ?: loadState.append as? LoadState.Error
- ?: loadState.prepend as? LoadState.Error
- errorState?.let {
- Toast.makeText(
- this@SearchRepositoriesActivity,
- "\uD83D\uDE28 Wooops ${it.error}",
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
}
}
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt
index f4ccea8d..42319ea2 100644
--- a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesViewModel.kt
@@ -45,7 +45,7 @@ import kotlinx.coroutines.launch
*/
class SearchRepositoriesViewModel(
private val repository: GithubRepository,
- private val savedStateHandle: SavedStateHandle
+ private val savedStateHandle: SavedStateHandle // SavedStateRegistryOwner
) : ViewModel() {
/**
@@ -53,7 +53,7 @@ class SearchRepositoriesViewModel(
*/
val state: StateFlow
- val pagingDataFlow: Flow>
+ val pagingDataFlow: Flow>
/**
* Processor of side effects from the UI which in turn feedback into [state]
@@ -61,6 +61,8 @@ class SearchRepositoriesViewModel(
val accept: (UiAction) -> Unit
init {
+
+ // UiAction Stream
val initialQuery: String = savedStateHandle.get(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY
val lastQueryScrolled: String = savedStateHandle.get(LAST_QUERY_SCROLLED) ?: DEFAULT_QUERY
val actionStateFlow = MutableSharedFlow()
@@ -80,6 +82,7 @@ class SearchRepositoriesViewModel(
)
.onStart { emit(UiAction.Scroll(currentQuery = lastQueryScrolled)) }
+ // flows for both PagingData and UiState
pagingDataFlow = searches
.flatMapLatest { searchRepo(queryString = it.query) }
.cachedIn(viewModelScope)
@@ -143,14 +146,16 @@ class SearchRepositoriesViewModel(
}
sealed class UiAction {
- data class Search(val query: String) : UiAction()
- data class Scroll(val currentQuery: String) : UiAction()
+ data class Search(val query: String) : UiAction() // query
+ data class Scroll(//val currentQuery: String,
+ val visibleItemCount: Int,
+ val lastVisibleItemPosition: Int,
+ val totalItemCount: Int) : UiAction() // scrolling down the screen to load alot of data
}
data class UiState(
- val query: String = DEFAULT_QUERY,
- val lastQueryScrolled: String = DEFAULT_QUERY,
- val hasNotScrolledForCurrentSearch: Boolean = false
+ val query: String,
+ val searchResult: RepoSearchResult
)
sealed class UiModel {
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SeparatorViewHolder.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SeparatorViewHolder.kt
index 812961d5..996097c1 100644
--- a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SeparatorViewHolder.kt
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/ui/SeparatorViewHolder.kt
@@ -33,7 +33,7 @@ class SeparatorViewHolder(view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun create(parent: ViewGroup): SeparatorViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.separator_view_item, parent, false)
+ .inflate(R.layout.separator_view_item, parent, false)
return SeparatorViewHolder(view)
}
}
diff --git a/advanced/end/app/src/main/java/com/example/android/codelabs/paging/util/RemotePresentationState.kt b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/util/RemotePresentationState.kt
new file mode 100644
index 00000000..df1b55bb
--- /dev/null
+++ b/advanced/end/app/src/main/java/com/example/android/codelabs/paging/util/RemotePresentationState.kt
@@ -0,0 +1,3 @@
+enum class RemotePresentationState {
+ INITIAL, REMOTE_LOADING, SOURCE_LOADING, PRESENTED
+}
\ No newline at end of file
diff --git a/advanced/start/app/src/main/java/com/example/android/codelabs/paging/Injection.kt b/advanced/start/app/src/main/java/com/example/android/codelabs/paging/Injection.kt
index 2eba2eb3..f163e868 100644
--- a/advanced/start/app/src/main/java/com/example/android/codelabs/paging/Injection.kt
+++ b/advanced/start/app/src/main/java/com/example/android/codelabs/paging/Injection.kt
@@ -23,13 +23,13 @@ import com.example.android.codelabs.paging.data.GithubRepository
import com.example.android.codelabs.paging.ui.ViewModelFactory
/**
- * Class that handles object creation.
+ * Class that handles creation.
* Like this, objects can be passed as parameters in the constructors and then replaced for
* testing, where needed.
*/
object Injection {
- /**
+ /**object
* Creates an instance of [GithubRepository] based on the [GithubService] and a
* [GithubLocalCache]
*/
diff --git a/basic/end/settings.gradle b/basic/end/settings.gradle
index cf84bb15..cd5b7bc6 100644
--- a/basic/end/settings.gradle
+++ b/basic/end/settings.gradle
@@ -3,6 +3,14 @@
// https://docs.gradle.org/current/userguide/platforms.html
enableFeaturePreview("VERSION_CATALOGS")
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {