├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mrntlu │ │ └── jetpackcompose_paginationcaching │ │ ├── HiltApplication.kt │ │ ├── MainActivity.kt │ │ ├── SingletonModule.kt │ │ ├── models │ │ ├── Movie.kt │ │ ├── MovieResponse.kt │ │ └── RemoteKeys.kt │ │ ├── repository │ │ └── MoviesRemoteMediator.kt │ │ ├── service │ │ ├── MoviesApiService.kt │ │ ├── MoviesDao.kt │ │ ├── MoviesDatabase.kt │ │ └── RemoteKeysDao.kt │ │ ├── ui │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── viewmodels │ │ └── MoviesViewModel.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | JetpackCompose-PaginationCaching -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetpackCompose-PaginationCaching 2 | 3 | https://proandroiddev.com/caching-and-pagination-with-paging-3-in-android-jetpack-compose-b636aaf116ce 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/main/java/com/mrntlu/jetpackcompose_paginationcaching/utils/Constants.kt -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'com.google.dagger.hilt.android' 6 | } 7 | 8 | android { 9 | namespace 'com.mrntlu.jetpackcompose_paginationcaching' 10 | compileSdk 33 11 | 12 | defaultConfig { 13 | applicationId "com.mrntlu.jetpackcompose_paginationcaching" 14 | minSdk 29 15 | targetSdk 33 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | buildFeatures { 39 | compose true 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion '1.3.1' 43 | } 44 | packagingOptions { 45 | resources { 46 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | 53 | implementation 'androidx.core:core-ktx:1.9.0' 54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' 55 | implementation 'androidx.activity:activity-compose:1.6.1' 56 | implementation "androidx.compose.ui:ui:$compose_ui_version" 57 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" 58 | implementation 'androidx.compose.material:material:1.3.1' 59 | 60 | //Paging 3 61 | def paging_version = "3.1.1" 62 | implementation "androidx.paging:paging-runtime:$paging_version" 63 | implementation "androidx.paging:paging-compose:1.0.0-alpha17" 64 | 65 | //Retrofit 66 | def retrofit_version = "2.9.0" 67 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 68 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 69 | 70 | //Hilt 71 | def hilt_version = "2.44" 72 | implementation "com.google.dagger:hilt-android:$hilt_version" 73 | kapt "com.google.dagger:hilt-compiler:$hilt_version" 74 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 75 | 76 | def nav_version = "2.5.3" 77 | implementation "androidx.navigation:navigation-compose:$nav_version" 78 | 79 | //Room 80 | def room_version = "2.4.3" 81 | implementation "androidx.room:room-runtime:$room_version" 82 | kapt "androidx.room:room-compiler:$room_version" 83 | implementation "androidx.room:room-ktx:$room_version" 84 | implementation "androidx.room:room-paging:$room_version" 85 | 86 | //Coil 87 | implementation "io.coil-kt:coil-compose:2.2.2" 88 | 89 | //Leak Canary 90 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' 91 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/HiltApplication.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching 2 | 3 | import android.app.Application 4 | import coil.ImageLoader 5 | import coil.ImageLoaderFactory 6 | import coil.disk.DiskCache 7 | import coil.memory.MemoryCache 8 | import dagger.hilt.android.HiltAndroidApp 9 | 10 | @HiltAndroidApp 11 | class HiltApplication: Application(), ImageLoaderFactory { 12 | override fun newImageLoader(): ImageLoader { 13 | return ImageLoader.Builder(this) 14 | .crossfade(true) 15 | .memoryCache { 16 | MemoryCache.Builder(this) 17 | .maxSizePercent(0.25) 18 | .build() 19 | } 20 | .diskCache { 21 | DiskCache.Builder() 22 | .directory(cacheDir.resolve("coil_cache")) 23 | .maxSizePercent(0.02) 24 | .build() 25 | } 26 | .build() 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.* 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.rounded.Warning 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.layout.ContentScale 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.unit.dp 21 | import androidx.hilt.navigation.compose.hiltViewModel 22 | import androidx.paging.LoadState 23 | import androidx.paging.compose.collectAsLazyPagingItems 24 | import androidx.paging.compose.items 25 | import coil.compose.AsyncImagePainter 26 | import coil.compose.rememberAsyncImagePainter 27 | import com.mrntlu.jetpackcompose_paginationcaching.ui.theme.JetpackComposePaginationCachingTheme 28 | import com.mrntlu.jetpackcompose_paginationcaching.viewmodels.MoviesViewModel 29 | import dagger.hilt.android.AndroidEntryPoint 30 | 31 | @AndroidEntryPoint 32 | class MainActivity : ComponentActivity() { 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContent { 37 | JetpackComposePaginationCachingTheme { 38 | Surface( 39 | modifier = Modifier.fillMaxSize(), 40 | color = MaterialTheme.colors.background 41 | ) { 42 | MainScreen() 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | //TODO Implement Placeholders 50 | @Composable 51 | fun MainScreen() { 52 | val moviesViewModel = hiltViewModel() 53 | 54 | val movies = moviesViewModel.getPopularMovies().collectAsLazyPagingItems() 55 | 56 | LazyColumn { 57 | items( 58 | items = movies 59 | ) { movie -> 60 | movie?.let { 61 | Row( 62 | horizontalArrangement = Arrangement.Center, 63 | verticalAlignment = Alignment.CenterVertically, 64 | ) { 65 | if (movie.posterPath != null) { 66 | var isImageLoading by remember { mutableStateOf(false) } 67 | 68 | val painter = rememberAsyncImagePainter( 69 | model = "https://image.tmdb.org/t/p/w154" + movie.posterPath, 70 | ) 71 | 72 | isImageLoading = when(painter.state) { 73 | is AsyncImagePainter.State.Loading -> true 74 | else -> false 75 | } 76 | 77 | Box ( 78 | contentAlignment = Alignment.Center 79 | ) { 80 | Image( 81 | modifier = Modifier 82 | .padding(horizontal = 6.dp, vertical = 3.dp) 83 | .height(115.dp) 84 | .width(77.dp) 85 | .clip(RoundedCornerShape(8.dp)), 86 | painter = painter, 87 | contentDescription = "Poster Image", 88 | contentScale = ContentScale.FillBounds, 89 | ) 90 | 91 | if (isImageLoading) { 92 | CircularProgressIndicator( 93 | modifier = Modifier 94 | .padding(horizontal = 6.dp, vertical = 3.dp), 95 | color = MaterialTheme.colors.primary, 96 | ) 97 | } 98 | } 99 | } 100 | Text( 101 | modifier = Modifier 102 | .padding(vertical = 18.dp, horizontal = 8.dp), 103 | text = it.title 104 | ) 105 | } 106 | Divider() 107 | } 108 | } 109 | 110 | val loadState = movies.loadState.mediator 111 | item { 112 | if (loadState?.refresh == LoadState.Loading) { 113 | Column( 114 | modifier = Modifier 115 | .fillParentMaxSize(), 116 | horizontalAlignment = Alignment.CenterHorizontally, 117 | verticalArrangement = Arrangement.Center, 118 | ) { 119 | Text( 120 | modifier = Modifier 121 | .padding(8.dp), 122 | text = "Refresh Loading" 123 | ) 124 | 125 | CircularProgressIndicator(color = MaterialTheme.colors.primary) 126 | } 127 | } 128 | 129 | if (loadState?.append == LoadState.Loading) { 130 | Box( 131 | modifier = Modifier 132 | .fillMaxWidth() 133 | .padding(16.dp), 134 | contentAlignment = Alignment.Center, 135 | ) { 136 | CircularProgressIndicator(color = MaterialTheme.colors.primary) 137 | } 138 | } 139 | 140 | if (loadState?.refresh is LoadState.Error || loadState?.append is LoadState.Error) { 141 | val isPaginatingError = (loadState.append is LoadState.Error) || movies.itemCount > 1 142 | val error = if (loadState.append is LoadState.Error) 143 | (loadState.append as LoadState.Error).error 144 | else 145 | (loadState.refresh as LoadState.Error).error 146 | 147 | val modifier = if (isPaginatingError) { 148 | Modifier.padding(8.dp) 149 | } else { 150 | Modifier.fillParentMaxSize() 151 | } 152 | Column( 153 | modifier = modifier, 154 | verticalArrangement = Arrangement.Center, 155 | horizontalAlignment = Alignment.CenterHorizontally, 156 | ) { 157 | if (!isPaginatingError) { 158 | Icon( 159 | modifier = Modifier 160 | .size(64.dp), 161 | imageVector = Icons.Rounded.Warning, contentDescription = null 162 | ) 163 | } 164 | 165 | Text( 166 | modifier = Modifier 167 | .padding(8.dp), 168 | text = error.message ?: error.toString(), 169 | textAlign = TextAlign.Center, 170 | ) 171 | 172 | Button( 173 | onClick = { 174 | movies.refresh() 175 | }, 176 | content = { 177 | Text(text = "Refresh") 178 | }, 179 | colors = ButtonDefaults.buttonColors( 180 | backgroundColor = MaterialTheme.colors.primary, 181 | contentColor = Color.White, 182 | ) 183 | ) 184 | } 185 | } 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/SingletonModule.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesApiService 6 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesDao 7 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesDatabase 8 | import com.mrntlu.jetpackcompose_paginationcaching.service.RemoteKeysDao 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import retrofit2.Retrofit 15 | import retrofit2.converter.gson.GsonConverterFactory 16 | import javax.inject.Singleton 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | class SingletonModule { 21 | 22 | @Singleton 23 | @Provides 24 | fun provideRetrofitInstance(): MoviesApiService = 25 | Retrofit.Builder() 26 | .baseUrl("https://api.themoviedb.org/3/") 27 | .addConverterFactory(GsonConverterFactory.create()) 28 | .build() 29 | .create(MoviesApiService::class.java) 30 | 31 | @Singleton 32 | @Provides 33 | fun provideMovieDatabase(@ApplicationContext context: Context): MoviesDatabase = 34 | Room 35 | .databaseBuilder(context, MoviesDatabase::class.java, "movies_database") 36 | .build() 37 | 38 | @Singleton 39 | @Provides 40 | fun provideMoviesDao(moviesDatabase: MoviesDatabase): MoviesDao = moviesDatabase.getMoviesDao() 41 | 42 | @Singleton 43 | @Provides 44 | fun provideRemoteKeysDao(moviesDatabase: MoviesDatabase): RemoteKeysDao = moviesDatabase.getRemoteKeysDao() 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/models/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.models 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.google.gson.annotations.SerializedName 7 | 8 | @Entity(tableName = "movies") 9 | data class Movie( 10 | @PrimaryKey(autoGenerate = false) 11 | val id: Int, 12 | @ColumnInfo(name = "original_title") 13 | @SerializedName("original_title") 14 | val ogTitle: String, 15 | @ColumnInfo(name = "overview") 16 | val overview: String, 17 | @ColumnInfo(name = "popularity") 18 | val popularity: Double, 19 | @ColumnInfo(name = "poster_path") 20 | @SerializedName("poster_path") 21 | val posterPath: String?, 22 | @ColumnInfo(name = "release_date") 23 | @SerializedName("release_date") 24 | val releaseDate: String, 25 | @ColumnInfo(name = "title") 26 | val title: String, 27 | @ColumnInfo(name = "page") 28 | var page: Int, 29 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/models/MovieResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.models 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class MovieResponse( 6 | val page: Int, 7 | @SerializedName(value = "results") 8 | val movies: List, 9 | @SerializedName("total_pages") 10 | val totalPages: Int, 11 | @SerializedName("total_results") 12 | val totalResults: Int 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/models/RemoteKeys.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.models 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | /*** 8 | * When we get the last item loaded from the PagingState, there's no way to know the index of the page it belonged to. 9 | * To solve this problem, we can add another table that stores the next and previous page keys for each Movie. 10 | */ 11 | @Entity(tableName = "remote_key") 12 | data class RemoteKeys( 13 | @PrimaryKey(autoGenerate = false) 14 | @ColumnInfo(name = "movie_id") 15 | val movieID: Int, 16 | val prevKey: Int?, 17 | val currentPage: Int, 18 | val nextKey: Int?, 19 | @ColumnInfo(name = "created_at") 20 | val createdAt: Long = System.currentTimeMillis() 21 | ) 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/repository/MoviesRemoteMediator.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.repository 2 | 3 | import androidx.paging.ExperimentalPagingApi 4 | import androidx.paging.LoadType 5 | import androidx.paging.PagingState 6 | import androidx.paging.RemoteMediator 7 | import androidx.room.withTransaction 8 | import com.mrntlu.jetpackcompose_paginationcaching.models.Movie 9 | import com.mrntlu.jetpackcompose_paginationcaching.models.RemoteKeys 10 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesApiService 11 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesDatabase 12 | import kotlinx.coroutines.delay 13 | import retrofit2.HttpException 14 | import java.io.IOException 15 | import java.util.concurrent.TimeUnit 16 | 17 | /** 18 | * RemoteMediator acts as a signal from the Paging library when the app has run out of cached data. 19 | * You can use this signal to load additional data from the network and store it in the local database, 20 | * where a PagingSource can load it and provide it to the UI to display. 21 | */ 22 | @OptIn(ExperimentalPagingApi::class) 23 | class MoviesRemoteMediator ( 24 | private val moviesApiService: MoviesApiService, 25 | private val moviesDatabase: MoviesDatabase, 26 | ): RemoteMediator() { 27 | /** 28 | * When additional data is needed, the Paging library calls the load() method from the RemoteMediator implementation. 29 | * This function typically fetches the new data from a network source and saves it to local storage. 30 | * Over time the data stored in the database requires invalidation, such as when the user manually triggers a refresh. 31 | * This is represented by the LoadType property passed to the load() method. 32 | * The LoadType informs the RemoteMediator whether it needs to refresh the existing data or fetch additional data that needs to be appended or prepended to the existing list. 33 | */ 34 | 35 | /** 36 | * In cases where the local data needs to be fully refreshed, initialize() should return InitializeAction.LAUNCH_INITIAL_REFRESH. 37 | * This causes the RemoteMediator to perform a remote refresh to fully reload the data. 38 | * 39 | * In cases where the local data doesn't need to be refreshed, initialize() should return InitializeAction.SKIP_INITIAL_REFRESH. 40 | * This causes the RemoteMediator to skip the remote refresh and load the cached data. 41 | */ 42 | override suspend fun initialize(): InitializeAction { 43 | val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) 44 | 45 | return if (System.currentTimeMillis() - (moviesDatabase.getRemoteKeysDao().getCreationTime() ?: 0) < cacheTimeout) { 46 | // Cached data is up-to-date, so there is no need to re-fetch 47 | // from the network. 48 | InitializeAction.SKIP_INITIAL_REFRESH 49 | } else { 50 | // Need to refresh cached data from network; returning 51 | // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's 52 | // APPEND and PREPEND from running until REFRESH succeeds. 53 | InitializeAction.LAUNCH_INITIAL_REFRESH 54 | } 55 | } 56 | 57 | /** LoadType.Append 58 | * When we need to load data at the end of the currently loaded data set, the load parameter is LoadType.APPEND 59 | */ 60 | private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? { 61 | // Get the last page that was retrieved, that contained items. 62 | // From that last page, get the last item 63 | return state.pages.lastOrNull { 64 | it.data.isNotEmpty() 65 | }?.data?.lastOrNull()?.let { movie -> 66 | moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(movie.id) 67 | } 68 | } 69 | 70 | /** LoadType.Prepend 71 | * When we need to load data at the beginning of the currently loaded data set, the load parameter is LoadType.PREPEND 72 | */ 73 | private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { 74 | // Get the first page that was retrieved, that contained items. 75 | // From that first page, get the first item 76 | return state.pages.firstOrNull { 77 | it.data.isNotEmpty() 78 | }?.data?.firstOrNull()?.let { movie -> 79 | moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(movie.id) 80 | } 81 | } 82 | 83 | /** LoadType.REFRESH 84 | * Gets called when it's the first time we're loading data, or when PagingDataAdapter.refresh() is called; 85 | * so now the point of reference for loading our data is the state.anchorPosition. 86 | * If this is the first load, then the anchorPosition is null. 87 | * When PagingDataAdapter.refresh() is called, the anchorPosition is the first visible position in the displayed list, so we will need to load the page that contains that specific item. 88 | */ 89 | private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState): RemoteKeys? { 90 | // The paging library is trying to load data after the anchor position 91 | // Get the item closest to the anchor position 92 | return state.anchorPosition?.let { position -> 93 | state.closestItemToPosition(position)?.id?.let { id -> 94 | moviesDatabase.getRemoteKeysDao().getRemoteKeyByMovieID(id) 95 | } 96 | } 97 | } 98 | 99 | /**. 100 | * 101 | * @param state This gives us information about the pages that were loaded before, 102 | * the most recently accessed index in the list, and the PagingConfig we defined when initializing the paging stream. 103 | * @param loadType this tells us whether we need to load data at the end (LoadType.APPEND) 104 | * or at the beginning of the data (LoadType.PREPEND) that we previously loaded, 105 | * or if this the first time we're loading data (LoadType.REFRESH). 106 | */ 107 | override suspend fun load( 108 | loadType: LoadType, 109 | state: PagingState 110 | ): MediatorResult { 111 | val page: Int = when (loadType) { 112 | LoadType.REFRESH -> { 113 | //New Query so clear the DB 114 | val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) 115 | remoteKeys?.nextKey?.minus(1) ?: 1 116 | } 117 | LoadType.PREPEND -> { 118 | val remoteKeys = getRemoteKeyForFirstItem(state) 119 | // If remoteKeys is null, that means the refresh result is not in the database yet. 120 | val prevKey = remoteKeys?.prevKey 121 | prevKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) 122 | } 123 | LoadType.APPEND -> { 124 | val remoteKeys = getRemoteKeyForLastItem(state) 125 | 126 | // If remoteKeys is null, that means the refresh result is not in the database yet. 127 | // We can return Success with endOfPaginationReached = false because Paging 128 | // will call this method again if RemoteKeys becomes non-null. 129 | // If remoteKeys is NOT NULL but its nextKey is null, that means we've reached 130 | // the end of pagination for append. 131 | val nextKey = remoteKeys?.nextKey 132 | nextKey ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) 133 | } 134 | } 135 | 136 | try { 137 | val apiResponse = moviesApiService.getPopularMovies(page = page) 138 | 139 | delay(1000L) //TODO For testing only! 140 | 141 | val movies = apiResponse.movies 142 | val endOfPaginationReached = movies.isEmpty() 143 | 144 | moviesDatabase.withTransaction { 145 | if (loadType == LoadType.REFRESH) { 146 | moviesDatabase.getRemoteKeysDao().clearRemoteKeys() 147 | moviesDatabase.getMoviesDao().clearAllMovies() 148 | } 149 | val prevKey = if (page > 1) page - 1 else null 150 | val nextKey = if (endOfPaginationReached) null else page + 1 151 | val remoteKeys = movies.map { 152 | RemoteKeys(movieID = it.id, prevKey = prevKey, currentPage = page, nextKey = nextKey) 153 | } 154 | 155 | moviesDatabase.getRemoteKeysDao().insertAll(remoteKeys) 156 | moviesDatabase.getMoviesDao().insertAll(movies.onEachIndexed { _, movie -> movie.page = page }) 157 | } 158 | return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) 159 | } catch (error: IOException) { 160 | return MediatorResult.Error(error) 161 | } catch (error: HttpException) { 162 | return MediatorResult.Error(error) 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/service/MoviesApiService.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.service 2 | 3 | import com.mrntlu.jetpackcompose_paginationcaching.models.MovieResponse 4 | import com.mrntlu.jetpackcompose_paginationcaching.utils.Constants.MOVIE_API_KEY 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface MoviesApiService { 9 | @GET("movie/popular?api_key=${MOVIE_API_KEY}&language=en-US") 10 | suspend fun getPopularMovies( 11 | @Query("page") page: Int 12 | ): MovieResponse 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/service/MoviesDao.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.service 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.room.Dao 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.mrntlu.jetpackcompose_paginationcaching.models.Movie 9 | 10 | @Dao 11 | interface MoviesDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.REPLACE) 14 | suspend fun insertAll(movies: List) 15 | 16 | @Query("Select * From movies Order By page") 17 | fun getMovies(): PagingSource 18 | 19 | @Query("Delete From movies") 20 | suspend fun clearAllMovies() 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/service/MoviesDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.service 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.mrntlu.jetpackcompose_paginationcaching.models.Movie 6 | import com.mrntlu.jetpackcompose_paginationcaching.models.RemoteKeys 7 | 8 | @Database( 9 | entities = [Movie::class, RemoteKeys::class], 10 | version = 1, 11 | ) 12 | abstract class MoviesDatabase: RoomDatabase() { 13 | abstract fun getMoviesDao(): MoviesDao 14 | abstract fun getRemoteKeysDao(): RemoteKeysDao 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/service/RemoteKeysDao.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.service 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.mrntlu.jetpackcompose_paginationcaching.models.RemoteKeys 8 | 9 | @Dao 10 | interface RemoteKeysDao { 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | suspend fun insertAll(remoteKey: List) 13 | 14 | @Query("Select * From remote_key Where movie_id = :id") 15 | suspend fun getRemoteKeyByMovieID(id: Int): RemoteKeys? 16 | 17 | @Query("Delete From remote_key") 18 | suspend fun clearRemoteKeys() 19 | 20 | @Query("Select created_at From remote_key Order By created_at DESC LIMIT 1") 21 | suspend fun getCreationTime(): Long? 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun JetpackComposePaginationCachingTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mrntlu/jetpackcompose_paginationcaching/viewmodels/MoviesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mrntlu.jetpackcompose_paginationcaching.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.paging.* 5 | import com.mrntlu.jetpackcompose_paginationcaching.models.Movie 6 | import com.mrntlu.jetpackcompose_paginationcaching.repository.MoviesRemoteMediator 7 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesApiService 8 | import com.mrntlu.jetpackcompose_paginationcaching.service.MoviesDatabase 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.Flow 11 | import javax.inject.Inject 12 | 13 | const val PAGE_SIZE = 20 14 | 15 | @HiltViewModel 16 | class MoviesViewModel @Inject constructor( 17 | private val moviesApiService: MoviesApiService, 18 | private val moviesDatabase: MoviesDatabase, 19 | ): ViewModel() { 20 | /** 21 | * A PagingSource still loads the data; but when the paged data is exhausted, the Paging library triggers the RemoteMediator to load new data from the network source. 22 | * The RemoteMediator stores the new data in the local database, so an in-memory cache in the ViewModel is unnecessary. 23 | * Finally, the PagingSource invalidates itself, and the Pager creates a new instance to load the fresh data from the database. 24 | */ 25 | 26 | @OptIn(ExperimentalPagingApi::class) 27 | fun getPopularMovies(): Flow> = 28 | Pager( 29 | config = PagingConfig( 30 | pageSize = PAGE_SIZE, 31 | prefetchDistance = 10, 32 | initialLoadSize = PAGE_SIZE, // How many items you want to load initially 33 | ), 34 | pagingSourceFactory = { 35 | // The pagingSourceFactory lambda should always return a brand new PagingSource 36 | // when invoked as PagingSource instances are not reusable. 37 | moviesDatabase.getMoviesDao().getMovies() 38 | }, 39 | remoteMediator = MoviesRemoteMediator( 40 | moviesApiService, 41 | moviesDatabase, 42 | ) 43 | ).flow 44 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JetpackCompose-PaginationCaching 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_ui_version = '1.3.2' 4 | } 5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 6 | plugins { 7 | id 'com.android.application' version '7.3.1' apply false 8 | id 'com.android.library' version '7.3.1' apply false 9 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false 10 | id 'com.google.dagger.hilt.android' version '2.44' apply false 11 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrNtlu/JetpackCompose-PaginationCaching/bf5e4672533ca5b460c9342fd82c71f53b820dea/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Dec 18 15:37:50 TRT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "JetpackCompose-PaginationCaching" 16 | include ':app' 17 | --------------------------------------------------------------------------------