├── .gitignore
├── .idea
├── .gitignore
├── .name
├── compiler.xml
├── copyright
│ ├── compose_paging.xml
│ └── profiles_settings.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── yusufarisoy
│ │ └── composepaging
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── yusufarisoy
│ │ │ └── composepagingapp
│ │ │ ├── App.kt
│ │ │ ├── data
│ │ │ ├── api
│ │ │ │ └── RickAndMortyServiceApi.kt
│ │ │ ├── entity
│ │ │ │ ├── BaseResponse.kt
│ │ │ │ ├── Character.kt
│ │ │ │ ├── CharacterUiModel.kt
│ │ │ │ ├── CharactersUiModel.kt
│ │ │ │ ├── HomeUiModel.kt
│ │ │ │ ├── Info.kt
│ │ │ │ ├── Location.kt
│ │ │ │ ├── Origin.kt
│ │ │ │ ├── SearchUiModel.kt
│ │ │ │ └── mapper
│ │ │ │ │ └── CharactersResponseToCharactersUiModelMapper.kt
│ │ │ └── repository
│ │ │ │ ├── CharactersRepository.kt
│ │ │ │ └── datasource
│ │ │ │ ├── CharacterDataSourceImpl.kt
│ │ │ │ └── CharactersDataSource.kt
│ │ │ ├── di
│ │ │ └── NetworkModule.kt
│ │ │ ├── domain
│ │ │ ├── FetchCharactersUseCase.kt
│ │ │ ├── HomePagingSource.kt
│ │ │ └── SearchPagingSource.kt
│ │ │ ├── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── home
│ │ │ │ ├── HomeFragment.kt
│ │ │ │ ├── HomeState.kt
│ │ │ │ └── HomeViewModel.kt
│ │ │ ├── search
│ │ │ │ ├── SearchFragment.kt
│ │ │ │ ├── SearchState.kt
│ │ │ │ └── SearchViewModel.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Typography.kt
│ │ │ ├── util
│ │ │ ├── BaseViewModel.kt
│ │ │ ├── BaseViewModelExt.kt
│ │ │ ├── FlowExt.kt
│ │ │ ├── Mapper.kt
│ │ │ ├── NetworkCall.kt
│ │ │ ├── NetworkResponse.kt
│ │ │ └── UseCase.kt
│ │ │ └── view
│ │ │ ├── common
│ │ │ ├── Character.kt
│ │ │ ├── Error.kt
│ │ │ └── Loading.kt
│ │ │ ├── home
│ │ │ └── Home.kt
│ │ │ └── search
│ │ │ └── Search.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_circle.xml
│ │ ├── ic_delete.xml
│ │ ├── ic_favorite_fill.xml
│ │ ├── ic_favorite_outline.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_search.xml
│ │ ├── ic_swipe_to_delete.xml
│ │ └── search_placeholder.png
│ │ ├── layout
│ │ └── activity_main.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-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── yusufarisoy
│ └── composepaging
│ └── ExampleUnitTest.kt
├── build.gradle
├── buildSrc
├── build.gradle.kts
├── build
│ ├── classes
│ │ └── kotlin
│ │ │ └── main
│ │ │ ├── Libs.class
│ │ │ ├── META-INF
│ │ │ └── buildSrc.kotlin_module
│ │ │ └── Versions.class
│ ├── kotlin
│ │ ├── buildSrcjar-classes.txt
│ │ └── compileKotlin
│ │ │ ├── build-history.bin
│ │ │ ├── caches-jvm
│ │ │ ├── inputs
│ │ │ │ ├── source-to-output.tab
│ │ │ │ ├── source-to-output.tab.keystream
│ │ │ │ ├── source-to-output.tab.keystream.len
│ │ │ │ ├── source-to-output.tab.len
│ │ │ │ ├── source-to-output.tab.values.at
│ │ │ │ ├── source-to-output.tab_i
│ │ │ │ └── source-to-output.tab_i.len
│ │ │ ├── jvm
│ │ │ │ └── kotlin
│ │ │ │ │ ├── class-attributes.tab
│ │ │ │ │ ├── class-attributes.tab.keystream
│ │ │ │ │ ├── class-attributes.tab.keystream.len
│ │ │ │ │ ├── class-attributes.tab.len
│ │ │ │ │ ├── class-attributes.tab.values.at
│ │ │ │ │ ├── class-attributes.tab_i
│ │ │ │ │ ├── class-attributes.tab_i.len
│ │ │ │ │ ├── class-fq-name-to-source.tab
│ │ │ │ │ ├── class-fq-name-to-source.tab.keystream
│ │ │ │ │ ├── class-fq-name-to-source.tab.keystream.len
│ │ │ │ │ ├── class-fq-name-to-source.tab.len
│ │ │ │ │ ├── class-fq-name-to-source.tab.values.at
│ │ │ │ │ ├── class-fq-name-to-source.tab_i
│ │ │ │ │ ├── class-fq-name-to-source.tab_i.len
│ │ │ │ │ ├── constants.tab
│ │ │ │ │ ├── constants.tab.keystream
│ │ │ │ │ ├── constants.tab.keystream.len
│ │ │ │ │ ├── constants.tab.len
│ │ │ │ │ ├── constants.tab.values
│ │ │ │ │ ├── constants.tab.values.at
│ │ │ │ │ ├── constants.tab.values.s
│ │ │ │ │ ├── constants.tab_i
│ │ │ │ │ ├── constants.tab_i.len
│ │ │ │ │ ├── internal-name-to-source.tab
│ │ │ │ │ ├── internal-name-to-source.tab.keystream
│ │ │ │ │ ├── internal-name-to-source.tab.keystream.len
│ │ │ │ │ ├── internal-name-to-source.tab.len
│ │ │ │ │ ├── internal-name-to-source.tab.values.at
│ │ │ │ │ ├── internal-name-to-source.tab_i
│ │ │ │ │ ├── internal-name-to-source.tab_i.len
│ │ │ │ │ ├── proto.tab
│ │ │ │ │ ├── proto.tab.keystream
│ │ │ │ │ ├── proto.tab.keystream.len
│ │ │ │ │ ├── proto.tab.len
│ │ │ │ │ ├── proto.tab.values.at
│ │ │ │ │ ├── proto.tab_i
│ │ │ │ │ ├── proto.tab_i.len
│ │ │ │ │ ├── source-to-classes.tab
│ │ │ │ │ ├── source-to-classes.tab.keystream
│ │ │ │ │ ├── source-to-classes.tab.keystream.len
│ │ │ │ │ ├── source-to-classes.tab.len
│ │ │ │ │ ├── source-to-classes.tab.values.at
│ │ │ │ │ ├── source-to-classes.tab_i
│ │ │ │ │ └── source-to-classes.tab_i.len
│ │ │ └── lookups
│ │ │ │ ├── counters.tab
│ │ │ │ ├── file-to-id.tab
│ │ │ │ ├── file-to-id.tab.keystream
│ │ │ │ ├── file-to-id.tab.keystream.len
│ │ │ │ ├── file-to-id.tab.len
│ │ │ │ ├── file-to-id.tab.values.at
│ │ │ │ ├── file-to-id.tab_i
│ │ │ │ ├── file-to-id.tab_i.len
│ │ │ │ ├── id-to-file.tab
│ │ │ │ ├── id-to-file.tab.keystream
│ │ │ │ ├── id-to-file.tab.keystream.len
│ │ │ │ ├── id-to-file.tab.len
│ │ │ │ ├── id-to-file.tab.values.at
│ │ │ │ ├── id-to-file.tab_i
│ │ │ │ ├── id-to-file.tab_i.len
│ │ │ │ ├── lookups.tab
│ │ │ │ ├── lookups.tab.keystream
│ │ │ │ ├── lookups.tab.keystream.len
│ │ │ │ ├── lookups.tab.len
│ │ │ │ ├── lookups.tab.values.at
│ │ │ │ ├── lookups.tab_i
│ │ │ │ └── lookups.tab_i.len
│ │ │ └── last-build.bin
│ ├── libs
│ │ └── buildSrc.jar
│ ├── pluginUnderTestMetadata
│ │ └── plugin-under-test-metadata.properties
│ ├── reports
│ │ └── plugin-development
│ │ │ └── validation-report.txt
│ └── tmp
│ │ └── jar
│ │ └── MANIFEST.MF
└── src
│ └── main
│ └── java
│ └── Dependencies.kt
├── composepaging
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── yusufarisoy
│ │ └── composepaging
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── yusufarisoy
│ │ └── composepaging
│ │ ├── BasePager.kt
│ │ ├── ComposePager.kt
│ │ ├── ComposePagerWithQuery.kt
│ │ ├── ComposePagingExt.kt
│ │ ├── pagingdata
│ │ └── ComposePagingData.kt
│ │ └── pagingsource
│ │ ├── ComposePagingSource.kt
│ │ └── ComposePagingSourceWithQuery.kt
│ └── test
│ └── java
│ └── com
│ └── yusufarisoy
│ └── composepaging
│ └── ExampleUnitTest.kt
├── 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 | ComposePagingApp
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/compose_paging.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ComposePaging
2 | ComposePaging library for [Jetpack Compose](https://developer.android.com/jetpack/compose).
3 |
4 | ## About
5 | ComposePaging is a paging library for Compose. ComposePaging makes it possible to use dynamic Composables as **`skippable`** for **better performance** like it should be.
6 |
7 | ## Metrics
8 | Metrics is the way of measuring the performance of composables. Ideally a composable method should be `restartable` and `skipabble` for the compiler to work with best performance.
9 |
10 |
11 | Besides being lazy, `ComposePaging` Feed items are easy to maintain and it's easy to make them `restartable` and `skipabble` while being updatable for better performance for your Android application.
12 |
13 | ## Attributes
14 | | Attribute | Description |
15 | |----- | --|
16 | | `prefetchDistance` | Defines the item distance to request the next page before reaching the bottom |
17 | | `initialPage` | Defines the initial page for the first request |
18 |
19 | ## Demo
20 | Composables items in the pages are updated without having any performance issues.
21 | | ComposePager | ComposePagerWithQuery |
22 | | ------ | ------ |
23 | | | |
24 |
25 | ## Usage
26 | #### PagingSource
27 | ComposePagingSource is used for requesting pages. [Example for more info](https://github.com/yusufarisoy/compose-paging/blob/main/app/src/main/java/com/yusufarisoy/composepaging/domain/HomePagingSource.kt).
28 | ```
29 | class PagingSource(
30 | private val useCase: UseCase,
31 | private val onPageLoaded: (itemCount: Int) -> Unit
32 | ) : ComposePagingSource() {
33 |
34 | override suspend fun load(params: Params): Result {
35 | val page = params.page
36 | val response = useCase.run(UseCase.Params(page = page))
37 |
38 | if (page == 0 && response.items.isEmpty()) {
39 | return Result.Error(NetworkError.EmptyFeedError("No results found"))
40 | }
41 |
42 | onPageLoaded(response.count)
43 |
44 | return Result.Page(data = response.items, hasNextPage = response.hasNextPage)
45 | }
46 | }
47 | ```
48 |
49 | #### PagingData and Pager in ViewModel
50 | ComposePagingData is used for storing the page data, it stores `SnapshotStateList` for better performance and maintainability. [Look for more info](https://github.com/yusufarisoy/compose-paging/blob/main/composepaging/src/main/java/com/yusufarisoy/composepaging/pagingdata/ComposePagingData.kt).
51 | ComposePager is used for managing the page data and states. Look for more info.
52 |
53 | ```
54 | val feed: ComposePagingData = ComposePagingData(prefetchDistance = 2)
55 | private val pager: ComposePager = ComposePager(
56 | pagingSource = PagingSource(
57 | useCase = useCase,
58 | onPageLoaded = ::onPageLoaded
59 | ),
60 | initialPage = 1
61 | )
62 | ```
63 |
64 | ## Setup
65 | ```
66 | allprojects {
67 | repositories {
68 | ...
69 | maven { url 'https://jitpack.io' }
70 | }
71 | }
72 | ```
73 | ```
74 | dependencies {
75 | implementation 'com.github.yusufarisoy:compose-paging:1.0.0'
76 | }
77 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | id 'kotlin-parcelize'
6 | id 'com.google.dagger.hilt.android'
7 | id 'dagger.hilt.android.plugin'
8 | }
9 |
10 | android {
11 | namespace 'com.yusufarisoy.composepagingapp'
12 | compileSdk 33
13 |
14 | defaultConfig {
15 | applicationId 'com.yusufarisoy.composepagingapp'
16 | minSdk 21
17 | targetSdk 33
18 | versionCode 1
19 | versionName "1.0"
20 |
21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 | buildFeatures {
38 | compose true
39 | }
40 | composeOptions {
41 | kotlinCompilerExtensionVersion '1.3.2'
42 | kotlinCompilerVersion '1.7.20'
43 | }
44 | android {
45 | defaultConfig {
46 | multiDexEnabled true
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 |
53 | implementation 'com.github.yusufarisoy:compose-paging:1.0.0'
54 |
55 | implementation Libs.coreKtx
56 | implementation Libs.appCompat
57 | //Compose
58 | implementation Libs.composeUi
59 | implementation Libs.composeCompiler
60 | implementation Libs.composeMaterial
61 | implementation Libs.composeTooling
62 | implementation Libs.activityCompose
63 | implementation Libs.hiltNavigationCompose
64 | //Retrofit and Gson
65 | implementation Libs.retrofit
66 | implementation Libs.retrofitConverter
67 | implementation Libs.gson
68 | //Lifecycle
69 | implementation Libs.lifecycleFragment
70 | implementation Libs.lifecycleCommon
71 | implementation Libs.lifecycleViewModel
72 | implementation Libs.lifecycleRunTime
73 | //Coroutines
74 | implementation Libs.coroutinesCore
75 | implementation Libs.coroutinesAndroid
76 | //Dagger Hilt
77 | kapt Libs.hiltCompiler
78 | implementation Libs.hiltAndroid
79 | //Coil
80 | implementation Libs.coilCompose
81 | //Test
82 | testImplementation Libs.testJunit
83 | androidTestImplementation Libs.testExtJunit
84 | androidTestImplementation Libs.espressoCore
85 | androidTestImplementation Libs.composeUiTest
86 | debugImplementation Libs.composeUiTooling
87 | }
--------------------------------------------------------------------------------
/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/androidTest/java/com/yusufarisoy/composepaging/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.yusufarisoy.composepaging", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/App.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class App : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/api/RickAndMortyServiceApi.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.api
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.BaseResponse
4 | import com.yusufarisoy.composepagingapp.data.entity.Character
5 | import retrofit2.Response
6 | import retrofit2.http.GET
7 | import retrofit2.http.Path
8 | import retrofit2.http.Query
9 |
10 | interface RickAndMortyServiceApi {
11 |
12 | @GET(GET_CHARACTERS)
13 | suspend fun getCharacters(
14 | @Query("name") name: String?,
15 | @Query("page") page: Int?
16 | ): Response>>
17 |
18 | @GET(GET_CHARACTER_BY_ID)
19 | suspend fun getCharacterById(@Path("id") id: Int): Response
20 |
21 | companion object {
22 | const val BASE_URL = "https://rickandmortyapi.com/api/"
23 | private const val GET_CHARACTERS = "character"
24 | private const val GET_CHARACTER_BY_ID = "character/{id}"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/BaseResponse.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class BaseResponse(
6 | @SerializedName("info") val info: Info,
7 | @SerializedName("results") val items: T
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/Character.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Character(
6 | @SerializedName("id") val id: Int,
7 | @SerializedName("name") val name: String,
8 | @SerializedName("status") val status: String,
9 | @SerializedName("species") val species: String,
10 | @SerializedName("type") val type: String,
11 | @SerializedName("gender") val gender: String,
12 | @SerializedName("origin") val origin: Origin,
13 | @SerializedName("location") val location: Location,
14 | @SerializedName("image") val image: String,
15 | @SerializedName("episode") val episode: List,
16 | @SerializedName("url") val url: String,
17 | @SerializedName("created") val created: String
18 | )
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/CharacterUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | data class CharacterUiModel(
4 | val id: Int,
5 | val name: String,
6 | val status: String,
7 | val species: String,
8 | val gender: String,
9 | val origin: String,
10 | val location: String,
11 | val image: String,
12 | val favorite: Boolean
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/CharactersUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | data class CharactersUiModel(
4 | val count: Int,
5 | val characters: List,
6 | val hasNextPage: Boolean
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/HomeUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import androidx.annotation.StringRes
4 |
5 | data class HomeUiModel(
6 | @StringRes val titleRes: Int,
7 | val itemCount: Int
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/Info.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Info(
6 | @SerializedName("count") val count: Int,
7 | @SerializedName("pages") val pages: Int,
8 | @SerializedName("next") val next: String?,
9 | @SerializedName("prev") val prev: String?
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/Location.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Location(
6 | @SerializedName("name") val name: String,
7 | @SerializedName("url") val url: String
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/Origin.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class Origin(
6 | @SerializedName("name") val name: String,
7 | @SerializedName("url") val url: String
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/SearchUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity
2 |
3 | data class SearchUiModel(
4 | val itemCount: Int? = null
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/entity/mapper/CharactersResponseToCharactersUiModelMapper.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.entity.mapper
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.BaseResponse
4 | import com.yusufarisoy.composepagingapp.data.entity.Character
5 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
6 | import com.yusufarisoy.composepagingapp.data.entity.CharactersUiModel
7 | import com.yusufarisoy.composepagingapp.util.Mapper
8 | import javax.inject.Inject
9 |
10 | class CharactersResponseToCharactersUiModelMapper @Inject constructor() :
11 | Mapper>, CharactersUiModel> {
12 |
13 | override fun map(input: BaseResponse>): CharactersUiModel {
14 | val characters = input.items.map { character ->
15 | CharacterUiModel(
16 | id = character.id,
17 | name = character.name,
18 | status = character.status,
19 | species = character.species,
20 | gender = character.gender,
21 | origin = character.origin.name,
22 | location = character.location.name,
23 | image = character.image,
24 | favorite = false
25 | )
26 | }
27 |
28 | return CharactersUiModel(
29 | hasNextPage = input.info.next != null,
30 | count = input.info.count,
31 | characters = characters
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/repository/CharactersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.repository
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.BaseResponse
4 | import com.yusufarisoy.composepagingapp.data.entity.Character
5 | import com.yusufarisoy.composepagingapp.data.repository.datasource.CharactersDataSource
6 | import com.yusufarisoy.composepagingapp.util.NetworkResponse
7 | import com.yusufarisoy.composepagingapp.util.getResult
8 | import javax.inject.Inject
9 |
10 | class CharactersRepository @Inject constructor(
11 | private val dataSource: CharactersDataSource
12 | ) {
13 |
14 | suspend fun fetchCharacters(
15 | characterName: String? = null,
16 | page: Int? = null
17 | ): NetworkResponse>> {
18 | val response = getResult {
19 | dataSource.getCharacters(characterName, page)
20 | }
21 |
22 | return response
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/repository/datasource/CharacterDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.repository.datasource
2 |
3 | import com.yusufarisoy.composepagingapp.data.api.RickAndMortyServiceApi
4 | import com.yusufarisoy.composepagingapp.data.entity.BaseResponse
5 | import com.yusufarisoy.composepagingapp.data.entity.Character
6 | import retrofit2.Response
7 | import javax.inject.Inject
8 |
9 | class CharactersDataSourceImpl @Inject constructor(
10 | private val service: RickAndMortyServiceApi
11 | ) : CharactersDataSource {
12 |
13 | override suspend fun getCharacters(
14 | characterName: String?,
15 | page: Int?
16 | ): Response>> {
17 | return service.getCharacters(characterName, page)
18 | }
19 |
20 | override suspend fun getCharacterById(id: Int): Response {
21 | return service.getCharacterById(id)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/data/repository/datasource/CharactersDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.data.repository.datasource
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.BaseResponse
4 | import com.yusufarisoy.composepagingapp.data.entity.Character
5 | import retrofit2.Response
6 |
7 | interface CharactersDataSource {
8 |
9 | suspend fun getCharacters(
10 | characterName: String?,
11 | page: Int?
12 | ): Response>>
13 |
14 | suspend fun getCharacterById(id: Int): Response
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.di
2 |
3 | import com.google.gson.Gson
4 | import com.yusufarisoy.composepagingapp.data.api.RickAndMortyServiceApi
5 | import com.yusufarisoy.composepagingapp.data.api.RickAndMortyServiceApi.Companion.BASE_URL
6 | import com.yusufarisoy.composepagingapp.data.repository.datasource.CharactersDataSource
7 | import com.yusufarisoy.composepagingapp.data.repository.datasource.CharactersDataSourceImpl
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.components.SingletonComponent
12 | import retrofit2.Retrofit
13 | import retrofit2.converter.gson.GsonConverterFactory
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | class NetworkModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideCharactersDataSource(service: RickAndMortyServiceApi): CharactersDataSource =
23 | CharactersDataSourceImpl(service)
24 |
25 | @Provides
26 | @Singleton
27 | fun provideRickAndMortyServiceApi(retrofit: Retrofit): RickAndMortyServiceApi =
28 | retrofit.create(RickAndMortyServiceApi::class.java)
29 |
30 | @Provides
31 | @Singleton
32 | fun provideRetrofit(gson: Gson): Retrofit = Retrofit.Builder()
33 | .baseUrl(BASE_URL)
34 | .addConverterFactory(GsonConverterFactory.create(gson))
35 | .build()
36 |
37 | @Provides
38 | @Singleton
39 | fun provideGson(): Gson = Gson()
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/domain/FetchCharactersUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.domain
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.CharactersUiModel
4 | import com.yusufarisoy.composepagingapp.data.entity.mapper.CharactersResponseToCharactersUiModelMapper
5 | import com.yusufarisoy.composepagingapp.data.repository.CharactersRepository
6 | import com.yusufarisoy.composepagingapp.util.NetworkError
7 | import com.yusufarisoy.composepagingapp.util.NetworkResponse
8 | import com.yusufarisoy.composepagingapp.util.UseCase
9 | import javax.inject.Inject
10 |
11 | class FetchCharactersUseCase @Inject constructor(
12 | private val charactersRepository: CharactersRepository,
13 | private val mapper: CharactersResponseToCharactersUiModelMapper
14 | ) : UseCase {
15 |
16 | override suspend fun run(params: Params): CharactersUiModel {
17 | return when (
18 | val response = charactersRepository.fetchCharacters(params.characterName, params.page)
19 | ) {
20 | is NetworkResponse.Success -> mapper.map(response.data)
21 | is NetworkResponse.Error -> throw NetworkError.NetworkCallError(
22 | message = "FetchCharactersUseCase NetworkCall Error"
23 | )
24 | }
25 | }
26 |
27 | data class Params(
28 | val page: Int = 0,
29 | val characterName: String? = null
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/domain/HomePagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.domain
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
4 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
5 | import com.yusufarisoy.composepagingapp.util.NetworkError
6 |
7 | class HomePagingSource(
8 | private val useCase: FetchCharactersUseCase,
9 | private val onPageLoaded: (itemCount: Int) -> Unit
10 | ) : ComposePagingSource() {
11 |
12 | override suspend fun load(params: Params): Result {
13 | val page = params.page
14 | val response = useCase.run(FetchCharactersUseCase.Params(page = page))
15 |
16 | if (page == 0 && response.characters.isEmpty()) {
17 | return Result.Error(NetworkError.EmptyFeedError("No results found"))
18 | }
19 |
20 | onPageLoaded(response.count)
21 |
22 | return Result.Page(data = response.characters, hasNextPage = response.hasNextPage)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/domain/SearchPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.domain
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
4 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
5 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSourceWithQuery
6 | import com.yusufarisoy.composepagingapp.util.NetworkError
7 |
8 | class SearchPagingSource(
9 | private val useCase: FetchCharactersUseCase
10 | ) : ComposePagingSourceWithQuery() {
11 |
12 | override suspend fun load(params: Params): ComposePagingSource.Result {
13 | val response = useCase.run(FetchCharactersUseCase.Params(params.page, params.query))
14 |
15 | if (params.page == 0 && response.characters.isEmpty()) {
16 | return ComposePagingSource.Result.Error(NetworkError.EmptyFeedError("No results found"))
17 | }
18 |
19 | return ComposePagingSource.Result.Page(
20 | data = response.characters,
21 | hasNextPage = response.hasNextPage
22 | )
23 | }
24 |
25 | data class Params(
26 | override val page: Int,
27 | override val query: String
28 | ) : ComposePagingSourceWithQuery.Params(page, query)
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.yusufarisoy.composepagingapp.R
6 | import com.yusufarisoy.composepagingapp.ui.home.HomeFragment
7 | import dagger.hilt.android.AndroidEntryPoint
8 |
9 | @AndroidEntryPoint
10 | class MainActivity : AppCompatActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_main)
15 | init()
16 | }
17 |
18 | private fun init() {
19 | supportFragmentManager
20 | .beginTransaction()
21 | .add(R.id.fragment_container, HomeFragment())
22 | .commit()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/home/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.home
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.compose.ui.platform.ComposeView
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import com.yusufarisoy.composepagingapp.R
11 | import com.yusufarisoy.composepagingapp.ui.search.SearchFragment
12 | import com.yusufarisoy.composepagingapp.ui.theme.ComposePagingAppTheme
13 | import com.yusufarisoy.composepagingapp.util.collectIn
14 | import com.yusufarisoy.composepagingapp.view.home.HomeScreen
15 | import dagger.hilt.android.AndroidEntryPoint
16 | import kotlinx.coroutines.flow.onEach
17 |
18 | @AndroidEntryPoint
19 | class HomeFragment : Fragment() {
20 |
21 | private val viewModel: HomeViewModel by viewModels()
22 |
23 | override fun onCreateView(
24 | inflater: LayoutInflater,
25 | container: ViewGroup?,
26 | savedInstanceState: Bundle?
27 | ): View = ComposeView(requireContext()).apply {
28 | setContent {
29 | ComposePagingAppTheme {
30 | HomeScreen(viewModel)
31 | }
32 | }
33 | }
34 |
35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36 | super.onViewCreated(view, savedInstanceState)
37 | observeEvents()
38 | }
39 |
40 | private fun observeEvents() {
41 | viewModel.eventFlow
42 | .onEach { event ->
43 | when (event) {
44 | is HomeEvent.OpenSearch -> openSearch()
45 | }
46 | }
47 | .collectIn(viewLifecycleOwner)
48 | }
49 |
50 | private fun openSearch() {
51 | parentFragmentManager
52 | .beginTransaction()
53 | .replace(R.id.fragment_container, SearchFragment())
54 | .addToBackStack(null)
55 | .commit()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/home/HomeState.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.home
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.HomeUiModel
4 | import com.yusufarisoy.composepagingapp.util.ViewEvent
5 | import com.yusufarisoy.composepagingapp.util.ViewState
6 |
7 | sealed class HomeState : ViewState {
8 |
9 | object Loading : HomeState()
10 |
11 | data class Content(val uiModel: HomeUiModel) : HomeState()
12 |
13 | data class Error(val errorMessage: String) : HomeState()
14 | }
15 |
16 | sealed class HomeEvent : ViewEvent {
17 |
18 | object OpenSearch : HomeEvent()
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.home
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.viewModelScope
5 | import com.yusufarisoy.composepaging.ComposePager
6 | import com.yusufarisoy.composepagingapp.R
7 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
8 | import com.yusufarisoy.composepagingapp.data.entity.HomeUiModel
9 | import com.yusufarisoy.composepagingapp.domain.FetchCharactersUseCase
10 | import com.yusufarisoy.composepagingapp.domain.HomePagingSource
11 | import com.yusufarisoy.composepaging.pagingdata.ComposePagingData
12 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
13 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Content
14 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Error
15 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Loading
16 | import com.yusufarisoy.composepagingapp.util.BaseViewModel
17 | import com.yusufarisoy.composepagingapp.util.secureLaunch
18 | import dagger.hilt.android.lifecycle.HiltViewModel
19 | import kotlinx.coroutines.CoroutineExceptionHandler
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.flow.StateFlow
22 | import kotlinx.coroutines.flow.launchIn
23 | import kotlinx.coroutines.flow.onEach
24 | import javax.inject.Inject
25 |
26 | @HiltViewModel
27 | class HomeViewModel @Inject constructor(
28 | private val useCase: FetchCharactersUseCase
29 | ) : BaseViewModel(Loading) {
30 |
31 | val feed: ComposePagingData = ComposePagingData(prefetchDistance = PREFETCH_DISTANCE)
32 | private val pager: ComposePager by lazy {
33 | ComposePager(
34 | pagingSource = HomePagingSource(
35 | useCase = useCase,
36 | onPageLoaded = ::onPageLoaded
37 | ),
38 | initialPage = INITIAL_PAGE
39 | )
40 | }
41 | private val pagerExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
42 | Log.e("HomeViewModel", "Pager Error: $throwable")
43 | onError(throwable)
44 | }
45 |
46 | val loadState: StateFlow
47 | get() = pager.loadState
48 |
49 | init {
50 | loadFirstPage()
51 | observePagerData()
52 | }
53 |
54 | private fun loadFirstPage() {
55 | secureLaunch(dispatcher = Dispatchers.IO, exceptionHandler = pagerExceptionHandler) {
56 | pager.loadFirstPage()
57 | }
58 | }
59 |
60 | private fun observePagerData() {
61 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
62 | pager.dataFlow
63 | .onEach { page ->
64 | feed.addPage(page)
65 | }
66 | .launchIn(viewModelScope)
67 | }
68 | }
69 |
70 | private fun onPageLoaded(itemCount: Int) {
71 | setState {
72 | Content(HomeUiModel(titleRes = R.string.home_title, itemCount = itemCount))
73 | }
74 | }
75 |
76 | fun requestNextPage() {
77 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
78 | pager.loadNextPage()
79 | }
80 | }
81 |
82 | fun resetPage() {
83 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
84 | pager.reset()
85 | }
86 | }
87 |
88 | fun onFavoriteClicked(id: Int, favorite: Boolean) {
89 | val index = feed.items.indexOfFirst { character -> character.id == id }
90 | if (index != -1) {
91 | feed.items[index] = feed.items[index].copy(favorite = !favorite)
92 | }
93 | }
94 |
95 | fun onCharacterRemoved(id: Int) {
96 | val index = feed.items.indexOfFirst { character -> character.id == id }
97 | if (index != -1) {
98 | feed.items.removeAt(index)
99 | }
100 | }
101 |
102 | fun onSearchClicked() {
103 | pushEvent(HomeEvent.OpenSearch)
104 | }
105 |
106 | override fun onError(throwable: Throwable) {
107 | super.onError(throwable)
108 | setState {
109 | Error(errorMessage = "Unexpected error occurred")
110 | }
111 | }
112 |
113 | companion object {
114 | private const val PREFETCH_DISTANCE = 2
115 | private const val INITIAL_PAGE = 1
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/search/SearchFragment.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.search
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.compose.ui.platform.ComposeView
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.viewModels
10 | import com.yusufarisoy.composepagingapp.ui.theme.ComposePagingAppTheme
11 | import com.yusufarisoy.composepagingapp.view.search.SearchScreen
12 | import dagger.hilt.android.AndroidEntryPoint
13 |
14 | @AndroidEntryPoint
15 | class SearchFragment : Fragment() {
16 |
17 | private val viewModel: SearchViewModel by viewModels()
18 |
19 | override fun onCreateView(
20 | inflater: LayoutInflater,
21 | container: ViewGroup?,
22 | savedInstanceState: Bundle?
23 | ): View = ComposeView(requireContext()).apply {
24 | setContent {
25 | ComposePagingAppTheme {
26 | SearchScreen(viewModel)
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/search/SearchState.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.search
2 |
3 | import com.yusufarisoy.composepagingapp.data.entity.SearchUiModel
4 | import com.yusufarisoy.composepagingapp.util.ViewEvent
5 | import com.yusufarisoy.composepagingapp.util.ViewState
6 |
7 | sealed class SearchState : ViewState {
8 |
9 | data class Content(val uiModel: SearchUiModel) : SearchState()
10 |
11 | data class Error(val errorMessage: String) : SearchState()
12 | }
13 |
14 | sealed class SearchEvent : ViewEvent
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.search
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.viewModelScope
5 | import com.yusufarisoy.composepaging.ComposePagerWithQuery
6 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
7 | import com.yusufarisoy.composepagingapp.data.entity.SearchUiModel
8 | import com.yusufarisoy.composepagingapp.domain.FetchCharactersUseCase
9 | import com.yusufarisoy.composepagingapp.domain.SearchPagingSource
10 | import com.yusufarisoy.composepaging.pagingdata.ComposePagingData
11 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
12 | import com.yusufarisoy.composepagingapp.util.BaseViewModel
13 | import com.yusufarisoy.composepagingapp.util.secureLaunch
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.CoroutineExceptionHandler
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.FlowPreview
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.flow.StateFlow
20 | import kotlinx.coroutines.flow.debounce
21 | import kotlinx.coroutines.flow.distinctUntilChanged
22 | import kotlinx.coroutines.flow.onEach
23 | import kotlinx.coroutines.flow.launchIn
24 | import javax.inject.Inject
25 |
26 | @HiltViewModel
27 | class SearchViewModel @Inject constructor(
28 | private val useCase: FetchCharactersUseCase
29 | ) : BaseViewModel(SearchState.Content(SearchUiModel())) {
30 |
31 | val feed: ComposePagingData = ComposePagingData(prefetchDistance = PREFETCH_DISTANCE)
32 | private val pager: ComposePagerWithQuery by lazy {
33 | ComposePagerWithQuery(
34 | pagingSource = SearchPagingSource(
35 | useCase = useCase
36 | )
37 | )
38 | }
39 | private val pagerExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
40 | Log.e("SearchViewModel", "Pager Error: $throwable")
41 | }
42 |
43 | private var page: Int = INITIAL_PAGE
44 | private val _queryFlow: MutableStateFlow = MutableStateFlow(EMPTY_STRING)
45 | val queryFlow: StateFlow = _queryFlow
46 |
47 | val loadState: StateFlow
48 | get() = pager.loadState
49 |
50 | init {
51 | observeQuery()
52 | observePagerData()
53 | }
54 |
55 | fun onQueryChanged(query: String) {
56 | secureLaunch {
57 | _queryFlow.emit(query)
58 | }
59 | }
60 |
61 | @OptIn(FlowPreview::class)
62 | private fun observeQuery() {
63 | secureLaunch(Dispatchers.IO) {
64 | _queryFlow
65 | .debounce(DEBOUNCE)
66 | .distinctUntilChanged()
67 | .onEach {
68 | if (it.length > 2) {
69 | search(it)
70 | } else {
71 | clearPage()
72 | }
73 | }
74 | .launchIn(viewModelScope)
75 | }
76 | }
77 |
78 | private fun observePagerData() {
79 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
80 | pager.dataFlow
81 | .onEach { page ->
82 | feed.addPage(page)
83 | }
84 | .launchIn(viewModelScope)
85 | }
86 | }
87 |
88 | private fun search(query: String) {
89 | page = INITIAL_PAGE
90 | feed.clear()
91 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
92 | pager.searchPage(SearchPagingSource.Params(page, query))
93 | }
94 | }
95 |
96 | fun requestNextPage() {
97 | page++
98 | secureLaunch(Dispatchers.IO, pagerExceptionHandler) {
99 | pager.loadNextPage(SearchPagingSource.Params(page, _queryFlow.value))
100 | }
101 | }
102 |
103 | private fun clearPage() {
104 | if (feed.items.isNotEmpty()) {
105 | page = 0
106 | feed.clear()
107 | }
108 | }
109 |
110 | fun onFavoriteClicked(id: Int, favorite: Boolean) {
111 | val index = feed.items.indexOfFirst { character -> character.id == id }
112 | if (index != -1) {
113 | feed.items[index] = feed.items[index].copy(favorite = !favorite)
114 | }
115 | }
116 |
117 | fun onCharacterRemoved(id: Int, name: String) {
118 | val index = feed.items.indexOfFirst { character -> character.id == id }
119 | if (index != -1) {
120 | feed.items.removeAt(index)
121 | }
122 | Log.i("SearchViewModel", "$name removed")
123 | }
124 |
125 | override fun onError(throwable: Throwable) {
126 | super.onError(throwable)
127 | setState {
128 | SearchState.Error(errorMessage = "Unexpected error occurred")
129 | }
130 | }
131 |
132 | companion object {
133 | private const val INITIAL_PAGE = 1
134 | private const val PREFETCH_DISTANCE = 0
135 | private const val DEBOUNCE = 300L
136 | private const val EMPTY_STRING = ""
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Teal200 = Color(0xFF03DAC5)
6 | val Black = Color(0XFF000000)
7 | val White = Color(0XFFFFFFFF)
8 | val Red900 = Color(0XFFB71C1C)
9 | val Red700 = Color(0XFFF44336)
10 | val Gray900 = Color(0XFF333333)
11 | val Gray700 = Color(0XFF999999)
12 | val Blue900 = Color(0XFF07444A)
13 | val Blue700 = Color(0XFF306364)
14 | val Green = Color(0XFF04CE71)
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.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(8.dp),
10 | large = RoundedCornerShape(16.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.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 = Blue900,
11 | primaryVariant = Blue700,
12 | secondary = Teal200,
13 | background = White
14 | )
15 |
16 | private val LightColorPalette = lightColors(
17 | primary = Blue900,
18 | primaryVariant = Blue700,
19 | secondary = Teal200
20 | )
21 |
22 | @Composable
23 | fun ComposePagingAppTheme(
24 | darkTheme: Boolean = isSystemInDarkTheme(),
25 | content: @Composable () -> Unit
26 | ) {
27 | val colors = if (darkTheme) {
28 | DarkColorPalette
29 | } else {
30 | LightColorPalette
31 | }
32 |
33 | MaterialTheme(
34 | colors = colors,
35 | typography = Typography,
36 | shapes = Shapes,
37 | content = content
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/ui/theme/Typography.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.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 | val Typography = Typography(
10 | body1 = TextStyle(
11 | fontFamily = FontFamily.Default,
12 | fontWeight = FontWeight.Normal,
13 | fontSize = 16.sp
14 | )
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.CoroutineExceptionHandler
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.receiveAsFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import kotlinx.coroutines.sync.Mutex
15 | import kotlinx.coroutines.sync.withLock
16 |
17 | abstract class BaseViewModel(initialState: S) : ViewModel() {
18 |
19 | private val stateMutex = Mutex()
20 | private val _stateFlow = MutableStateFlow(initialState)
21 | val stateFlow: StateFlow
22 | get() = _stateFlow.asStateFlow()
23 | val currentState: S
24 | get() = _stateFlow.value
25 |
26 | private val eventMutex = Mutex()
27 | private val _eventChannel = Channel()
28 | val eventFlow: Flow
29 | get() = _eventChannel.receiveAsFlow()
30 |
31 | val exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> onError(throwable) }
32 |
33 | protected fun setState(reducer: S.() -> S) {
34 | pushState {
35 | val newState = reducer(_stateFlow.value)
36 | _stateFlow.tryEmit(newState)
37 | }
38 | }
39 |
40 | private fun pushState(action: () -> Unit) {
41 | viewModelScope.launch {
42 | stateMutex.withLock {
43 | action.invoke()
44 | }
45 | }
46 | }
47 |
48 | fun pushEvent(event: E) {
49 | viewModelScope.launch {
50 | eventMutex.withLock {
51 | _eventChannel.send(event)
52 | }
53 | }
54 | }
55 |
56 | open fun onError(throwable: Throwable) {
57 | Log.e("BaseViewModel ExceptionHandler", throwable.toString())
58 | }
59 | }
60 |
61 | interface ViewState
62 |
63 | interface ViewEvent
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/BaseViewModelExt.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.CoroutineExceptionHandler
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.flow.onEach
10 | import kotlinx.coroutines.launch
11 |
12 | fun BaseViewModel<*, *>.secureLaunch(
13 | dispatcher: CoroutineDispatcher = Dispatchers.Main,
14 | exceptionHandler: CoroutineExceptionHandler = this.exceptionHandler,
15 | block: suspend () -> Unit
16 | ) {
17 | viewModelScope.launch(dispatcher + exceptionHandler) {
18 | block()
19 | }
20 | }
21 |
22 | fun , E> Fragment.collectEvent(
23 | viewModel: V,
24 | onEventPushed: suspend (E) -> Unit
25 | ): Job {
26 | return viewModel.eventFlow
27 | .onEach(onEventPushed)
28 | .collectIn(viewLifecycleOwner)
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/FlowExt.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.lifecycleScope
6 | import androidx.lifecycle.repeatOnLifecycle
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.launch
11 | import kotlin.coroutines.CoroutineContext
12 | import kotlin.coroutines.EmptyCoroutineContext
13 |
14 | fun Flow.collectIn(
15 | owner: LifecycleOwner,
16 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
17 | action: suspend CoroutineScope.(T) -> Unit = {}
18 | ) = owner.addRepeatingJob(minActiveState) {
19 | collect { action(it) }
20 | }
21 |
22 | fun LifecycleOwner.addRepeatingJob(
23 | state: Lifecycle.State,
24 | coroutineContext: CoroutineContext = EmptyCoroutineContext,
25 | block: suspend CoroutineScope.() -> Unit
26 | ): Job = lifecycleScope.launch(coroutineContext) {
27 | repeatOnLifecycle(state, block)
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | interface Mapper {
4 | fun map(input: Input): Output
5 | }
6 |
7 | fun , Input, Output> Input.mapWith(
8 | mapperClass: MapperClass
9 | ): Output = mapperClass.map(this)
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/NetworkCall.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | import retrofit2.Response
4 |
5 | suspend fun getResult(call: suspend () -> Response): NetworkResponse {
6 | val response = call()
7 | if (response.isSuccessful) {
8 | val body = response.body()
9 | if (body != null) {
10 | return NetworkResponse.Success(body)
11 | }
12 | }
13 |
14 | return NetworkResponse.Error("NetworkCall error")
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/NetworkResponse.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | sealed class NetworkResponse {
4 |
5 | data class Success(val data: T) : NetworkResponse()
6 |
7 | data class Error(val message: String) : NetworkResponse()
8 | }
9 |
10 | sealed class NetworkError(override val message: String) : Throwable(message) {
11 |
12 | data class NetworkCallError(override val message: String) : NetworkError(message)
13 |
14 | data class EmptyFeedError(override val message: String) : NetworkError(message)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/util/UseCase.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.util
2 |
3 | interface UseCase {
4 |
5 | suspend fun run(params: Params): Result
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/view/common/Character.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.view.common
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.offset
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.widthIn
17 | import androidx.compose.foundation.shape.RoundedCornerShape
18 | import androidx.compose.material.DismissDirection
19 | import androidx.compose.material.DismissValue
20 | import androidx.compose.material.ExperimentalMaterialApi
21 | import androidx.compose.material.FractionalThreshold
22 | import androidx.compose.material.Icon
23 | import androidx.compose.material.IconButton
24 | import androidx.compose.material.SwipeToDismiss
25 | import androidx.compose.material.Text
26 | import androidx.compose.material.rememberDismissState
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.getValue
29 | import androidx.compose.runtime.remember
30 | import androidx.compose.runtime.rememberCoroutineScope
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.draw.clip
34 | import androidx.compose.ui.draw.scale
35 | import androidx.compose.ui.draw.shadow
36 | import androidx.compose.ui.graphics.Color
37 | import androidx.compose.ui.layout.ContentScale
38 | import androidx.compose.ui.res.painterResource
39 | import androidx.compose.ui.res.stringResource
40 | import androidx.compose.ui.text.font.FontWeight
41 | import androidx.compose.ui.text.style.TextAlign
42 | import androidx.compose.ui.unit.dp
43 | import androidx.compose.ui.unit.sp
44 | import coil.compose.rememberImagePainter
45 | import com.yusufarisoy.composepagingapp.R
46 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
47 | import com.yusufarisoy.composepagingapp.ui.theme.Gray900
48 | import com.yusufarisoy.composepagingapp.ui.theme.Green
49 | import com.yusufarisoy.composepagingapp.ui.theme.Red700
50 | import com.yusufarisoy.composepagingapp.ui.theme.Red900
51 | import com.yusufarisoy.composepagingapp.ui.theme.White
52 | import kotlinx.coroutines.launch
53 |
54 | @OptIn(ExperimentalMaterialApi::class)
55 | @Composable
56 | fun Character(
57 | character: CharacterUiModel,
58 | modifier: Modifier = Modifier,
59 | onFavoriteClicked: (id: Int, favorite: Boolean) -> Unit,
60 | onCharacterRemoved: (id: Int, name: String) -> Unit
61 | ) {
62 | val dismissState = rememberDismissState(
63 | confirmStateChange = { state ->
64 | if (state == DismissValue.DismissedToStart) {
65 | onCharacterRemoved(character.id, character.name)
66 | }
67 | true
68 | }
69 | )
70 | val dismissScope = rememberCoroutineScope()
71 | val dismissColor by animateColorAsState(
72 | if (dismissState.targetValue == DismissValue.Default) Gray900 else Red700
73 | )
74 | val dismissScale by animateFloatAsState(
75 | if (dismissState.targetValue == DismissValue.Default) 1f else 1.2f
76 | )
77 | val (favoriteIcon, favoriteColor) = remember(character) {
78 | if (character.favorite) {
79 | R.drawable.ic_favorite_fill to Red900
80 | } else {
81 | R.drawable.ic_favorite_outline to Gray900
82 | }
83 | }
84 |
85 | SwipeToDismiss(
86 | state = dismissState,
87 | directions = setOf(DismissDirection.EndToStart),
88 | dismissThresholds = { FractionalThreshold(0.3f) },
89 | background = {
90 | Box(
91 | modifier = Modifier
92 | .fillMaxWidth()
93 | .height(120.dp)
94 | .background(dismissColor)
95 | .padding(horizontal = 20.dp),
96 | contentAlignment = Alignment.CenterEnd
97 | ) {
98 | Icon(
99 | painter = painterResource(R.drawable.ic_delete),
100 | contentDescription = stringResource(R.string.swipe_to_delete),
101 | modifier = Modifier.scale(dismissScale)
102 | )
103 | }
104 | }
105 | ) {
106 | Row(
107 | modifier = modifier
108 | .fillMaxWidth()
109 | .background(White)
110 | ) {
111 | CharacterImage(character)
112 |
113 | Column(
114 | modifier = Modifier
115 | .weight(weight = 1f, fill = false)
116 | .fillMaxWidth()
117 | .padding(vertical = 4.dp, horizontal = 12.dp)
118 | ) {
119 | Text(text = character.name, fontWeight = FontWeight.Bold)
120 | CharacterStatus(status = character.status)
121 | Text(text = character.gender)
122 | Text(text = character.location)
123 | }
124 |
125 | Column(
126 | modifier = Modifier.height(120.dp),
127 | verticalArrangement = Arrangement.SpaceBetween
128 | ) {
129 | IconButton(onClick = { onFavoriteClicked(character.id, character.favorite) }) {
130 | Icon(
131 | painter = painterResource(favoriteIcon),
132 | contentDescription = stringResource(R.string.favorite_status),
133 | tint = favoriteColor
134 | )
135 | }
136 |
137 | IconButton(
138 | onClick = {
139 | dismissScope.launch {
140 | dismissState.dismiss(DismissDirection.EndToStart)
141 | onCharacterRemoved(character.id, character.name)
142 | }
143 | }
144 | ) {
145 | Icon(
146 | painter = painterResource(R.drawable.ic_swipe_to_delete),
147 | contentDescription = stringResource(R.string.swipe_to_delete)
148 | )
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
155 | @Composable
156 | private fun CharacterImage(character: CharacterUiModel) {
157 | Box(
158 | modifier = Modifier
159 | .size(120.dp)
160 | .shadow(4.dp, shape = RoundedCornerShape(16.dp), clip = false)
161 | .background(color = Color.Transparent, shape = RoundedCornerShape(16.dp))
162 | .clip(RoundedCornerShape(16.dp))
163 | ) {
164 | Image(
165 | painter = rememberImagePainter(character.image),
166 | contentDescription = character.name,
167 | modifier = Modifier
168 | .matchParentSize()
169 | .align(Alignment.Center),
170 | contentScale = ContentScale.FillBounds
171 | )
172 |
173 | Text(
174 | text = character.species,
175 | modifier = Modifier
176 | .widthIn(min = 20.dp, max = 80.dp)
177 | .offset(x = (-4).dp, y = 4.dp)
178 | .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp), clip = false)
179 | .background(color = White, shape = RoundedCornerShape(16.dp))
180 | .clip(RoundedCornerShape(16.dp))
181 | .padding(vertical = 4.dp, horizontal = 8.dp)
182 | .align(Alignment.TopEnd),
183 | fontSize = 12.sp,
184 | textAlign = TextAlign.Center,
185 | maxLines = 1,
186 | )
187 | }
188 | }
189 |
190 | @Composable
191 | private fun CharacterStatus(status: String) {
192 | val color = remember {
193 | when(status) {
194 | "Alive" -> Green
195 | "Dead" -> Red900
196 | else -> Gray900
197 | }
198 | }
199 |
200 | Row(
201 | horizontalArrangement = Arrangement.spacedBy(2.dp),
202 | verticalAlignment = Alignment.CenterVertically
203 | ) {
204 | Icon(
205 | painter = painterResource(R.drawable.ic_circle),
206 | contentDescription = status,
207 | tint = color
208 | )
209 | Text(text = status, color = color)
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/view/common/Error.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.view.common
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material.Button
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.painterResource
13 | import androidx.compose.ui.res.stringResource
14 | import com.yusufarisoy.composepagingapp.R
15 |
16 | @Composable
17 | fun Error(
18 | message: String,
19 | modifier: Modifier = Modifier,
20 | showRetryButton: Boolean = false,
21 | retry: () -> Unit = {}
22 | ) {
23 | Column(
24 | modifier = modifier.fillMaxSize(),
25 | verticalArrangement = Arrangement.Center,
26 | horizontalAlignment = Alignment.CenterHorizontally
27 | ) {
28 | Image(
29 | painter = painterResource(R.drawable.ic_launcher_foreground),
30 | contentDescription = stringResource(R.string.error_state_message)
31 | )
32 | Text(text = message)
33 |
34 | if (showRetryButton) {
35 | Button(onClick = retry) {
36 | Text(text = stringResource(R.string.retry_button))
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/view/common/Loading.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.view.common
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material.CircularProgressIndicator
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import com.yusufarisoy.composepagingapp.ui.theme.Blue900
11 |
12 | @Composable
13 | fun Loading(modifier: Modifier = Modifier) {
14 | Column(
15 | modifier = modifier.fillMaxSize(),
16 | verticalArrangement = Arrangement.Center,
17 | horizontalAlignment = Alignment.CenterHorizontally
18 | ) {
19 | CircularProgressIndicator(color = Blue900)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/view/home/Home.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.view.home
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.material.CircularProgressIndicator
13 | import androidx.compose.material.Icon
14 | import androidx.compose.material.IconButton
15 | import androidx.compose.material.Scaffold
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.rememberScaffoldState
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.collectAsState
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.rememberCoroutineScope
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.res.painterResource
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.text.style.TextAlign
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.unit.sp
32 | import com.yusufarisoy.composepagingapp.R
33 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
34 | import com.yusufarisoy.composepagingapp.data.entity.HomeUiModel
35 | import com.yusufarisoy.composepaging.items
36 | import com.yusufarisoy.composepaging.pagingdata.ComposePagingData
37 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
38 | import com.yusufarisoy.composepagingapp.view.common.Error
39 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Loading
40 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Content
41 | import com.yusufarisoy.composepagingapp.ui.home.HomeState.Error
42 | import com.yusufarisoy.composepagingapp.ui.home.HomeViewModel
43 | import com.yusufarisoy.composepagingapp.ui.theme.Blue900
44 | import com.yusufarisoy.composepagingapp.ui.theme.White
45 | import com.yusufarisoy.composepagingapp.view.common.Character
46 | import com.yusufarisoy.composepagingapp.view.common.Loading
47 | import kotlinx.coroutines.launch
48 |
49 | @Composable
50 | fun HomeScreen(viewModel: HomeViewModel) {
51 | val state by viewModel.stateFlow.collectAsState()
52 | val loadState by viewModel.loadState.collectAsState()
53 | val scaffoldState = rememberScaffoldState()
54 | val scaffoldScope = rememberCoroutineScope()
55 | val context = LocalContext.current
56 | val requestNextPage = remember {
57 | {
58 | viewModel.requestNextPage()
59 | Toast.makeText(context, "New Page Requested", Toast.LENGTH_LONG).show()
60 | }
61 | }
62 | val onCharacterRemoved = remember<(Int, String) -> Unit> {
63 | { id, name ->
64 | viewModel.onCharacterRemoved(id)
65 | scaffoldScope.launch {
66 | scaffoldState.snackbarHostState.showSnackbar("$name removed")
67 | }
68 | }
69 | }
70 |
71 | Scaffold(
72 | modifier = Modifier.fillMaxSize(),
73 | scaffoldState = scaffoldState
74 | ) {
75 | Box(modifier = Modifier.fillMaxSize()) {
76 | when {
77 | state is Loading && loadState is ComposePagingSource.LoadState.Initial -> Loading()
78 | state is Error || loadState is ComposePagingSource.LoadState.Error -> Error(
79 | message = (state as Error).errorMessage,
80 | showRetryButton = true,
81 | retry = viewModel::resetPage
82 | )
83 | state is Content -> ContentState(
84 | uiModel = (state as Content).uiModel,
85 | feed = viewModel.feed,
86 | requestNextPage = requestNextPage,
87 | onFavoriteClicked = viewModel::onFavoriteClicked,
88 | onCharacterRemoved = onCharacterRemoved,
89 | onSearchClicked = viewModel::onSearchClicked
90 | )
91 | }
92 |
93 | // Shows ProgressBar at the bottom only when new page is loading
94 | if (loadState is ComposePagingSource.LoadState.Loading) {
95 | CircularProgressIndicator(
96 | modifier = Modifier.align(Alignment.BottomCenter),
97 | color = Blue900
98 | )
99 | }
100 | }
101 | }
102 | }
103 |
104 | @Composable
105 | private fun ContentState(
106 | uiModel: HomeUiModel,
107 | feed: ComposePagingData,
108 | requestNextPage: () -> Unit,
109 | onFavoriteClicked: (id: Int, favorite: Boolean) -> Unit,
110 | onCharacterRemoved: (id: Int, name: String) -> Unit,
111 | onSearchClicked: () -> Unit
112 | ) {
113 | LazyColumn(
114 | modifier = Modifier
115 | .fillMaxSize()
116 | .background(White),
117 | contentPadding = PaddingValues(8.dp),
118 | verticalArrangement = Arrangement.spacedBy(16.dp)
119 | ) {
120 | item(key = "home_header") {
121 | Header(onSearchClicked)
122 | }
123 |
124 | item(key = "home_characters_title") {
125 | CharactersTitle(uiModel)
126 | }
127 |
128 | items(
129 | feed = feed,
130 | requestNextPage = requestNextPage,
131 | key = { character -> character.id }
132 | ) { item ->
133 | Character(
134 | character = item,
135 | onFavoriteClicked = onFavoriteClicked,
136 | onCharacterRemoved = onCharacterRemoved
137 | )
138 | }
139 | }
140 | }
141 |
142 | @Composable
143 | private fun Header(onSearchClicked: () -> Unit) {
144 | Box(modifier = Modifier.fillMaxWidth()) {
145 | Text(
146 | text = stringResource(R.string.app_name),
147 | modifier = Modifier.align(Alignment.Center),
148 | fontSize = 20.sp,
149 | fontWeight = FontWeight.Bold,
150 | textAlign = TextAlign.Center
151 | )
152 | IconButton(
153 | onClick = onSearchClicked,
154 | modifier = Modifier.align(Alignment.CenterEnd)
155 | ) {
156 | Icon(
157 | painter = painterResource(R.drawable.ic_search),
158 | contentDescription = stringResource(R.string.search_button)
159 | )
160 | }
161 | }
162 | }
163 |
164 | @Composable
165 | private fun CharactersTitle(uiModel: HomeUiModel) {
166 | Row(
167 | modifier = Modifier.fillMaxWidth(),
168 | horizontalArrangement = Arrangement.SpaceBetween,
169 | verticalAlignment = Alignment.CenterVertically
170 | ) {
171 | Text(
172 | text = stringResource(id = uiModel.titleRes),
173 | fontWeight = FontWeight.Bold
174 | )
175 | Text(text = uiModel.itemCount.toString(), fontWeight = FontWeight.Light)
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yusufarisoy/composepagingapp/view/search/Search.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp.view.search
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.PaddingValues
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.lazy.LazyColumn
17 | import androidx.compose.foundation.shape.RoundedCornerShape
18 | import androidx.compose.material.CircularProgressIndicator
19 | import androidx.compose.material.Icon
20 | import androidx.compose.material.Scaffold
21 | import androidx.compose.material.Text
22 | import androidx.compose.material.TextField
23 | import androidx.compose.material.TextFieldDefaults
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.draw.clip
31 | import androidx.compose.ui.draw.shadow
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.platform.LocalContext
34 | import androidx.compose.ui.res.painterResource
35 | import androidx.compose.ui.res.stringResource
36 | import androidx.compose.ui.text.font.FontWeight
37 | import androidx.compose.ui.text.style.TextAlign
38 | import androidx.compose.ui.unit.dp
39 | import androidx.compose.ui.unit.sp
40 | import com.yusufarisoy.composepagingapp.R
41 | import com.yusufarisoy.composepagingapp.data.entity.CharacterUiModel
42 | import com.yusufarisoy.composepagingapp.data.entity.SearchUiModel
43 | import com.yusufarisoy.composepaging.items
44 | import com.yusufarisoy.composepaging.pagingdata.ComposePagingData
45 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
46 | import com.yusufarisoy.composepagingapp.ui.search.SearchState.Content
47 | import com.yusufarisoy.composepagingapp.ui.search.SearchState.Error
48 | import com.yusufarisoy.composepagingapp.ui.search.SearchViewModel
49 | import com.yusufarisoy.composepagingapp.ui.theme.Blue900
50 | import com.yusufarisoy.composepagingapp.ui.theme.Gray900
51 | import com.yusufarisoy.composepagingapp.ui.theme.White
52 | import com.yusufarisoy.composepagingapp.view.common.Character
53 | import com.yusufarisoy.composepagingapp.view.common.Error
54 |
55 | @Composable
56 | fun SearchScreen(viewModel: SearchViewModel) {
57 | val state by viewModel.stateFlow.collectAsState()
58 | val loadState by viewModel.loadState.collectAsState()
59 | val searchQuery by viewModel.queryFlow.collectAsState()
60 | val context = LocalContext.current
61 | val requestNextPage = remember {
62 | {
63 | viewModel.requestNextPage()
64 | Toast.makeText(context, "New Page Requested", Toast.LENGTH_LONG).show()
65 | }
66 | }
67 |
68 | Scaffold(modifier = Modifier.fillMaxSize()) {
69 | Box(modifier = Modifier.fillMaxSize()) {
70 | when {
71 | state is Error || loadState is ComposePagingSource.LoadState.Error -> Error(
72 | message = (state as Error).errorMessage
73 | )
74 | state is Content -> ContentState(
75 | uiModel = (state as Content).uiModel,
76 | feed = viewModel.feed,
77 | searchQuery = { searchQuery },
78 | onQueryChanged = viewModel::onQueryChanged,
79 | requestNextPage = requestNextPage,
80 | onFavoriteClicked = viewModel::onFavoriteClicked,
81 | onCharacterRemoved = viewModel::onCharacterRemoved
82 | )
83 | }
84 |
85 | if (loadState is ComposePagingSource.LoadState.Loading) {
86 | CircularProgressIndicator(
87 | modifier = Modifier.align(Alignment.BottomCenter),
88 | color = Blue900
89 | )
90 | }
91 | }
92 | }
93 | }
94 |
95 | @Composable
96 | private fun ContentState(
97 | uiModel: SearchUiModel,
98 | feed: ComposePagingData,
99 | modifier: Modifier = Modifier,
100 | searchQuery: () -> String,
101 | onQueryChanged: (String) -> Unit,
102 | requestNextPage: () -> Unit,
103 | onFavoriteClicked: (id: Int, favorite: Boolean) -> Unit,
104 | onCharacterRemoved: (id: Int, name: String) -> Unit
105 | ) {
106 | LazyColumn(
107 | modifier = modifier
108 | .fillMaxSize()
109 | .background(White),
110 | contentPadding = PaddingValues(8.dp),
111 | verticalArrangement = Arrangement.spacedBy(16.dp)
112 | ) {
113 | item(key = "home_header") {
114 | Text(
115 | text = stringResource(R.string.app_name),
116 | modifier = Modifier.fillMaxWidth(),
117 | fontSize = 20.sp,
118 | fontWeight = FontWeight.Bold,
119 | textAlign = TextAlign.Center
120 | )
121 | }
122 |
123 | item(key = "search_bar") {
124 | SearchBar(
125 | query = searchQuery,
126 | onQueryChanged = onQueryChanged
127 | )
128 | }
129 |
130 | if (feed.items.size == 0) {
131 | item(key = "search_empty_placeholder") {
132 | PlaceHolder()
133 | }
134 | }
135 |
136 | if (uiModel.itemCount != null) {
137 | item(key = "search_title") {
138 | Row(
139 | modifier = Modifier.fillMaxWidth(),
140 | horizontalArrangement = Arrangement.SpaceBetween,
141 | verticalAlignment = Alignment.CenterVertically
142 | ) {
143 | Text(
144 | text = stringResource(R.string.search_title, uiModel.itemCount),
145 | fontWeight = FontWeight.Bold
146 | )
147 | Text(text = uiModel.itemCount.toString(), fontWeight = FontWeight.Light)
148 | }
149 | }
150 | }
151 |
152 | items(
153 | feed = feed,
154 | requestNextPage = requestNextPage,
155 | key = { character -> character.id }
156 | ) { item ->
157 | Character(
158 | character = item,
159 | onFavoriteClicked = onFavoriteClicked,
160 | onCharacterRemoved = onCharacterRemoved
161 | )
162 | }
163 | }
164 | }
165 |
166 | @Composable
167 | private fun SearchBar(
168 | modifier: Modifier = Modifier,
169 | query: () -> String,
170 | onQueryChanged: (String) -> Unit
171 | ) {
172 | TextField(
173 | value = query(),
174 | onValueChange = onQueryChanged,
175 | modifier = modifier
176 | .fillMaxWidth()
177 | .border(width = 1.dp, color = Gray900, shape = RoundedCornerShape(16.dp)),
178 | leadingIcon = {
179 | Icon(
180 | painter = painterResource(R.drawable.ic_search),
181 | contentDescription = stringResource(R.string.search_button)
182 | )
183 | },
184 | singleLine = true,
185 | shape = RoundedCornerShape(16.dp),
186 | colors = TextFieldDefaults.textFieldColors(
187 | backgroundColor = White,
188 | leadingIconColor = Blue900,
189 | focusedIndicatorColor = Color.Transparent,
190 | unfocusedIndicatorColor = Color.Transparent,
191 | disabledIndicatorColor = Color.Transparent
192 | )
193 | )
194 | }
195 |
196 | @Composable
197 | private fun PlaceHolder() {
198 | Column(
199 | modifier = Modifier.fillMaxWidth(),
200 | verticalArrangement = Arrangement.Center,
201 | horizontalAlignment = Alignment.CenterHorizontally
202 | ) {
203 | Box(
204 | modifier = Modifier
205 | .shadow(4.dp, shape = RoundedCornerShape(16.dp), clip = false)
206 | .background(color = Color.Transparent, shape = RoundedCornerShape(16.dp))
207 | .clip(RoundedCornerShape(16.dp))
208 | ) {
209 | Image(
210 | painter = painterResource(R.drawable.search_placeholder),
211 | contentDescription = stringResource(R.string.search_button)
212 | )
213 | }
214 | Spacer(modifier = Modifier.height(8.dp))
215 |
216 | Text(text = stringResource(R.string.search_placeholder), color = Gray900)
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/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_circle.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_delete.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_favorite_fill.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_favorite_outline.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_swipe_to_delete.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/search_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/drawable/search_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/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 | ComposePaging
3 | Unknown error
4 | Retry
5 | Characters
6 | Swipe to delete
7 | Change favorite status
8 | Search
9 | %1d characters found
10 | Search your favorite characters
11 | Search your favorite Rick and Morty characters
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/app/src/test/java/com/yusufarisoy/composepaging/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.yusufarisoy.composepagingapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath Libs.toolsGradlePlugin
4 | classpath Libs.kotlinGradlePlugin
5 | classpath Libs.hiltGradlePlugin
6 | }
7 | }
8 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
9 | plugins {
10 | id 'com.android.application' version '7.3.1' apply false
11 | id 'com.android.library' version '7.3.1' apply false
12 | id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
13 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | }
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/Libs.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/classes/kotlin/main/Libs.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module:
--------------------------------------------------------------------------------
1 | " *
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/Versions.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/classes/kotlin/main/Versions.class
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/buildSrcjar-classes.txt:
--------------------------------------------------------------------------------
1 | /Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/classes/kotlin/main/Libs.class:/Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/classes/kotlin/main/Versions.class
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/build-history.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/build-history.bin
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.keystream:
--------------------------------------------------------------------------------
1 | +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.keystream.len:
--------------------------------------------------------------------------------
1 | ,
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.values.at:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab.values.at
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab.keystream:
--------------------------------------------------------------------------------
1 | VersionsLibs
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-attributes.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream:
--------------------------------------------------------------------------------
1 | VersionsLibs
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.keystream:
--------------------------------------------------------------------------------
1 | VersionsLibs
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values.at:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values.at
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values.s:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab.values.s
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/constants.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream:
--------------------------------------------------------------------------------
1 | VersionsLibs
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.keystream:
--------------------------------------------------------------------------------
1 | VersionsLibs.kotlin_module
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.values.at:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab.values.at
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream:
--------------------------------------------------------------------------------
1 | +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len:
--------------------------------------------------------------------------------
1 | ,
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module VersionsLibs.kotlin_module
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/counters.tab:
--------------------------------------------------------------------------------
1 | 9
2 | 0
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab.keystream:
--------------------------------------------------------------------------------
1 | +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab.keystream.len:
--------------------------------------------------------------------------------
1 | ,
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab.keystream:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab.keystream.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab.len:
--------------------------------------------------------------------------------
1 | �
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab.values.at:
--------------------------------------------------------------------------------
1 | / Header Record For PersistentHashMapValueStorage, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt, +$PROJECT_DIR$/src/main/java/Dependencies.kt
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.keystream:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.keystream
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.keystream.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.keystream.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.len:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.values.at:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab.values.at
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i.len:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i.len
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/last-build.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/kotlin/compileKotlin/last-build.bin
--------------------------------------------------------------------------------
/buildSrc/build/libs/buildSrc.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/libs/buildSrc.jar
--------------------------------------------------------------------------------
/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties:
--------------------------------------------------------------------------------
1 | implementation-classpath=/Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/classes/java/main\:/Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/classes/groovy/main\:/Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/classes/kotlin/main\:/Users/yusufarisoy/AndroidStudioProjects/compose-paging/buildSrc/build/resources/main
2 |
--------------------------------------------------------------------------------
/buildSrc/build/reports/plugin-development/validation-report.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/buildSrc/build/reports/plugin-development/validation-report.txt
--------------------------------------------------------------------------------
/buildSrc/build/tmp/jar/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 |
3 |
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Dependencies.kt:
--------------------------------------------------------------------------------
1 | object Versions {
2 | const val hilt = "2.42"
3 | const val compose = "1.3.3"
4 | const val retrofit = "2.9.0"
5 | const val lifecycle = "2.4.1"
6 | const val coroutines = "1.5.2"
7 | }
8 |
9 | object Libs {
10 | const val toolsGradlePlugin = "com.android.tools.build:gradle:7.1.1"
11 | const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
12 | const val hiltGradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:${Versions.hilt}"
13 | // Core
14 | const val coreKtx = "androidx.core:core-ktx:1.9.0"
15 | // AppCompat
16 | const val appCompat = "androidx.appcompat:appcompat:1.6.0"
17 | // Activity
18 | const val activityCompose = "androidx.activity:activity-compose:1.4.0"
19 | // Material
20 | const val material = "com.google.android.material:material:1.5.0"
21 | // Gson
22 | const val gson = "com.google.code.gson:gson:2.8.7"
23 | // Coil
24 | const val coilCompose = "io.coil-kt:coil-compose:1.4.0"
25 | // Lifecycle
26 | const val lifecycleFragment = "androidx.fragment:fragment-ktx:1.4.1"
27 | const val lifecycleRunTime = "androidx.lifecycle:lifecycle-runtime-ktx:2.5.0-alpha02"
28 | const val lifecycleCommon = "androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycle}"
29 | const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}"
30 | // Coroutines
31 | const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
32 | const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
33 | // Retrofit
34 | const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
35 | const val retrofitConverter = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}"
36 | // Hilt
37 | const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:1.0.0"
38 | const val hiltAndroid = "com.google.dagger:hilt-android:${Versions.hilt}"
39 | const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}"
40 | // Compose
41 | const val composeUi = "androidx.compose.ui:ui:${Versions.compose}"
42 | const val composeCompiler = "androidx.compose.compiler:compiler:1.3.2"
43 | const val composeUiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}"
44 | const val composeUiTest = "androidx.compose.ui:ui-test-junit4:${Versions.compose}"
45 | const val composeMaterial = "androidx.compose.material:material:1.3.1"
46 | const val composeTooling = "androidx.compose.ui:ui-tooling-preview:${Versions.compose}"
47 | // Test
48 | const val testJunit = "junit:junit:4.+"
49 | const val testExtJunit = "androidx.test.ext:junit:1.1.3"
50 | const val espressoCore = "androidx.test.espresso:espresso-core:3.4.0"
51 | }
--------------------------------------------------------------------------------
/composepaging/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/composepaging/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:09 PM
5 | */
6 |
7 | plugins {
8 | id 'com.android.library'
9 | id 'org.jetbrains.kotlin.android'
10 | id 'maven-publish'
11 | }
12 |
13 | android {
14 | namespace 'com.yusufarisoy.composepaging'
15 | compileSdk 33
16 |
17 | defaultConfig {
18 | minSdk 21
19 | targetSdk 33
20 |
21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22 | consumerProguardFiles "consumer-rules.pro"
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.2'
43 | kotlinCompilerVersion '1.7.20'
44 | }
45 | }
46 |
47 | dependencies {
48 |
49 | implementation 'androidx.core:core-ktx:1.9.0'
50 | implementation 'androidx.appcompat:appcompat:1.6.0'
51 | implementation 'com.google.android.material:material:1.7.0'
52 | // Compose
53 | implementation 'androidx.compose.ui:ui:1.3.3'
54 | implementation 'androidx.compose.compiler:compiler:1.3.2'
55 | implementation 'androidx.compose.ui:ui-tooling:1.3.3'
56 | implementation 'androidx.compose.material:material:1.3.1'
57 | implementation 'androidx.compose.ui:ui-tooling-preview:1.3.3'
58 | // Test
59 | testImplementation 'junit:junit:4.13.2'
60 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
61 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.3.3'
62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
63 | }
64 |
65 | afterEvaluate {
66 | publishing {
67 | publications {
68 | release(MavenPublication) {
69 | from components.release
70 |
71 | groupId = 'com.github.composepaging'
72 | artifactId = 'compose-paging'
73 | version = '1.0.1'
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/composepaging/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/composepaging/consumer-rules.pro
--------------------------------------------------------------------------------
/composepaging/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
--------------------------------------------------------------------------------
/composepaging/src/androidTest/java/com/yusufarisoy/composepaging/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:05 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import androidx.test.platform.app.InstrumentationRegistry
10 | import androidx.test.ext.junit.runners.AndroidJUnit4
11 |
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 |
15 | import org.junit.Assert.*
16 |
17 | /**
18 | * Instrumented test, which will execute on an Android device.
19 | *
20 | * See [testing documentation](http://d.android.com/tools/testing).
21 | */
22 | @RunWith(AndroidJUnit4::class)
23 | class ExampleInstrumentedTest {
24 | @Test
25 | fun useAppContext() {
26 | // Context of the app under test.
27 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
28 | assertEquals("com.yusufarisoy.composepaging.test", appContext.packageName)
29 | }
30 | }
--------------------------------------------------------------------------------
/composepaging/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/BasePager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:09 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource.LoadState
10 | import kotlinx.coroutines.channels.Channel
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.asStateFlow
14 | import kotlinx.coroutines.flow.receiveAsFlow
15 | import kotlinx.coroutines.sync.Mutex
16 | import kotlinx.coroutines.sync.withLock
17 |
18 | abstract class BasePager {
19 |
20 | protected var reachedLast: Boolean = false
21 |
22 | private val stateMutex = Mutex()
23 | private val _loadState: MutableStateFlow = MutableStateFlow(LoadState.Initial)
24 | val loadState = _loadState.asStateFlow()
25 |
26 | private val dataMutex = Mutex()
27 | private val _dataChannel: Channel> = Channel()
28 | val dataFlow: Flow>
29 | get() = _dataChannel.receiveAsFlow()
30 |
31 | protected suspend fun setState(reducer: LoadState.() -> LoadState) {
32 | pushState {
33 | val newState = reducer(_loadState.value)
34 | _loadState.tryEmit(newState)
35 | }
36 | }
37 |
38 | private suspend fun pushState(action: () -> Unit) {
39 | stateMutex.withLock {
40 | action.invoke()
41 | }
42 | }
43 |
44 | protected suspend fun pushData(data: List) {
45 | dataMutex.withLock {
46 | _dataChannel.send(data)
47 | }
48 | }
49 |
50 | fun canLoadMore() = !reachedLast && _loadState.value == LoadState.Idle
51 | }
52 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/ComposePager.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:15 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource
10 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource.LoadState
11 |
12 | class ComposePager(
13 | private val pagingSource: ComposePagingSource,
14 | private val initialPage: Int
15 | ) : BasePager() {
16 |
17 | private var page: Int = initialPage
18 |
19 | suspend fun loadFirstPage() {
20 | loadPage()
21 | }
22 |
23 | suspend fun loadNextPage() {
24 | if (!canLoadMore()) return
25 |
26 | page++
27 | setState {
28 | LoadState.Loading
29 | }
30 | loadPage()
31 | }
32 |
33 | private suspend fun loadPage() {
34 | when (val result = pagingSource.load(params = ComposePagingSource.Params(page))) {
35 | is ComposePagingSource.Result.Page -> {
36 | pushData(result.data)
37 | reachedLast = !result.hasNextPage
38 | setState {
39 | LoadState.Idle
40 | }
41 | }
42 | is ComposePagingSource.Result.Error ->
43 | setState {
44 | LoadState.Error(result.throwable)
45 | }
46 | }
47 | }
48 |
49 | suspend fun reset() {
50 | page = initialPage
51 | setState {
52 | LoadState.Initial
53 | }
54 | loadFirstPage()
55 | }
56 |
57 | fun currentPage() = page
58 | }
59 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/ComposePagerWithQuery.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:15 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource.LoadState
10 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSource.Result
11 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSourceWithQuery
12 | import com.yusufarisoy.composepaging.pagingsource.ComposePagingSourceWithQuery.Params
13 |
14 | class ComposePagerWithQuery(
15 | private val pagingSource: ComposePagingSourceWithQuery
16 | ) : BasePager() {
17 |
18 | suspend fun searchPage(params: Input) {
19 | setState {
20 | LoadState.Loading
21 | }
22 | loadPage(params)
23 | }
24 |
25 | suspend fun loadNextPage(params: Input) {
26 | if (!canLoadMore()) return
27 |
28 | setState {
29 | LoadState.Loading
30 | }
31 | loadPage(params)
32 | }
33 |
34 | private suspend fun loadPage(params: Input) {
35 | when (val result = pagingSource.load(params)) {
36 | is Result.Page -> {
37 | pushData(result.data)
38 | reachedLast = !result.hasNextPage
39 | setState {
40 | LoadState.Idle
41 | }
42 | }
43 | is Result.Error ->
44 | setState {
45 | LoadState.Error(result.throwable)
46 | }
47 | }
48 | }
49 |
50 | suspend fun reset(params: Input) {
51 | setState {
52 | LoadState.Initial
53 | }
54 | searchPage(params)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/ComposePagingExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:15 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import androidx.compose.foundation.lazy.LazyItemScope
10 | import androidx.compose.foundation.lazy.LazyListScope
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.remember
14 | import com.yusufarisoy.composepaging.pagingdata.ComposePagingData
15 |
16 | inline fun LazyListScope.items(
17 | feed: ComposePagingData,
18 | noinline requestNextPage: () -> Unit,
19 | noinline key: ((item: Value) -> Any)? = null,
20 | crossinline content: @Composable LazyItemScope.(item: Value) -> Unit
21 | ) {
22 | items(
23 | count = feed.items.size,
24 | key = if (key != null) { index -> key(feed.items[index]) } else null
25 | ) { index ->
26 | val fetchDistanceReached = remember(index, feed.items.size) {
27 | index == feed.items.size - feed.prefetchDistance - 1
28 | }
29 | LaunchedEffect(fetchDistanceReached) {
30 | if (fetchDistanceReached) requestNextPage()
31 | }
32 | content(feed.items[index])
33 | }
34 | }
35 |
36 | inline fun LazyListScope.itemsIndexed(
37 | feed: ComposePagingData,
38 | noinline requestNextPage: () -> Unit,
39 | noinline key: ((index: Int, item: Value) -> Any)? = null,
40 | crossinline content: @Composable LazyItemScope.(index: Int, item: Value) -> Unit
41 | ) {
42 | items(
43 | count = feed.items.size,
44 | key = if (key != null) { index -> key(index, feed.items[index]) } else null
45 | ) { index ->
46 | val fetchDistanceReached = remember(index, feed.items.size) {
47 | index == feed.items.size - feed.prefetchDistance - 1
48 | }
49 | LaunchedEffect(fetchDistanceReached) {
50 | if (fetchDistanceReached) requestNextPage()
51 | }
52 | content(index, feed.items[index])
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/pagingdata/ComposePagingData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:14 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging.pagingdata
8 |
9 | import androidx.compose.runtime.mutableStateListOf
10 | import androidx.compose.runtime.snapshots.SnapshotStateList
11 |
12 | class ComposePagingData(val prefetchDistance: Int = PREFETCH_DISTANCE) {
13 |
14 | val items: SnapshotStateList = mutableStateListOf()
15 |
16 | fun addPage(data: List) {
17 | items.addAll(data)
18 | }
19 |
20 | fun clear() {
21 | items.clear()
22 | }
23 |
24 | companion object {
25 | private const val PREFETCH_DISTANCE: Int = 0
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/pagingsource/ComposePagingSource.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:15 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging.pagingsource
8 |
9 | abstract class ComposePagingSource {
10 |
11 | data class Params(val page: Int)
12 |
13 | sealed class LoadState {
14 |
15 | object Initial : LoadState()
16 |
17 | object Loading : LoadState()
18 |
19 | object Idle : LoadState()
20 |
21 | data class Error(val throwable: Throwable) : LoadState()
22 | }
23 |
24 | sealed class Result {
25 |
26 | data class Error(
27 | val throwable: Throwable
28 | ) : Result()
29 |
30 | data class Page(
31 | val data: List,
32 | val hasNextPage: Boolean
33 | ) : Result()
34 | }
35 |
36 | abstract suspend fun load(params: Params): Result
37 | }
38 |
--------------------------------------------------------------------------------
/composepaging/src/main/java/com/yusufarisoy/composepaging/pagingsource/ComposePagingSourceWithQuery.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:15 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging.pagingsource
8 |
9 | abstract class ComposePagingSourceWithQuery {
10 |
11 | abstract class Params(open val page: Int, open val query: String)
12 |
13 | abstract suspend fun load(params: Input): ComposePagingSource.Result
14 | }
15 |
--------------------------------------------------------------------------------
/composepaging/src/test/java/com/yusufarisoy/composepaging/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Created by Yusuf Arisoy on Jan 23, 2023
3 | * Copyright (C) 2023 Yusuf Arisoy. All rights reserved.
4 | * Last modified 1/8/24, 12:05 PM
5 | */
6 |
7 | package com.yusufarisoy.composepaging
8 |
9 | import org.junit.Test
10 |
11 | import org.junit.Assert.*
12 |
13 | /**
14 | * Example local unit test, which will execute on the development machine (host).
15 | *
16 | * See [testing documentation](http://d.android.com/tools/testing).
17 | */
18 | class ExampleUnitTest {
19 | @Test
20 | fun addition_isCorrect() {
21 | assertEquals(4, 2 + 2)
22 | }
23 | }
--------------------------------------------------------------------------------
/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/yusufarisoy/compose-paging/b1e07775867087e9e4ad57ceb2ce2ed80dc76670/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jan 16 11:25:24 TRT 2023
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 | maven { url 'https://www.jitpack.io' }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url 'https://www.jitpack.io' }
15 | }
16 | }
17 | rootProject.name = "ComposePagingApp"
18 | include ':app'
19 | include ':composepaging'
20 |
--------------------------------------------------------------------------------