├── .gitignore
├── README.md
├── README_images
└── paging_with_network_screenshot.gif
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── chetdeva
│ │ └── githubit
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── chetdeva
│ │ │ └── githubit
│ │ │ ├── GithubItApplication.kt
│ │ │ ├── Injection.kt
│ │ │ ├── api
│ │ │ ├── ApiRequestHelper.kt
│ │ │ ├── GithubApi.kt
│ │ │ ├── GithubApiService.kt
│ │ │ ├── Item.kt
│ │ │ └── UsersSearchResponse.kt
│ │ │ ├── data
│ │ │ ├── GithubDataSourceFactory.kt
│ │ │ ├── GithubPageKeyedDataSource.kt
│ │ │ ├── GithubRepository.kt
│ │ │ ├── InMemoryByPageKeyRepository.kt
│ │ │ ├── Listing.kt
│ │ │ └── NetworkState.kt
│ │ │ ├── ui
│ │ │ ├── NetworkStateItemViewHolder.kt
│ │ │ ├── SearchUsersActivity.kt
│ │ │ ├── SearchUsersViewModel.kt
│ │ │ ├── UserItemViewHolder.kt
│ │ │ ├── UsersAdapter.kt
│ │ │ └── ViewModelFactory.kt
│ │ │ └── util
│ │ │ └── GithubAppGlideModule.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_placeholder.png
│ │ └── ic_search_white_24dp.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_search_repositories.xml
│ │ ├── network_state_item.xml
│ │ ├── repo_view_item.xml
│ │ └── user_item.xml
│ │ ├── menu
│ │ └── menu_search.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── chetdeva
│ └── githubit
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 | .idea
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GithubIt
2 |
3 | A simple app that uses `PageKeyedDataSource` from Android Paging Library. It uses the [Github API](https://developer.github.com/v3/search/#search-users) for searching users.
4 |
5 |
6 |
7 | ### PageKeyedDataSource
8 |
9 | Use `GithubPageKeyedDataSource` to `loadInitial` and `loadAfter`.
10 |
11 | ```kotlin
12 | /**
13 | * A data source that uses the before/after keys returned in page requests.
14 | */
15 | class GithubPageKeyedDataSource : PageKeyedDataSource() {
16 |
17 | override fun loadInitial(params: LoadInitialParams,
18 | callback: LoadInitialCallback) {
19 |
20 | val currentPage = 1
21 | val nextPage = currentPage + 1
22 |
23 | val request = githubApi.searchUsers(
24 | query = searchQuery,
25 | page = currentPage,
26 | perPage = params.requestedLoadSize)
27 |
28 | // Retrofit Call onResponse omitted
29 |
30 | callback.onResult(items, null, nextPage)
31 | }
32 |
33 | override fun loadAfter(params: LoadParams,
34 | callback: LoadCallback) {
35 |
36 | val currentPage = params.key
37 | val nextPage = currentPage + 1
38 |
39 | val request = githubApi.searchUsersAsync(
40 | query = searchQuery,
41 | page = currentPage,
42 | perPage = params.requestedLoadSize)
43 |
44 | // Retrofit Call onResponse omitted
45 |
46 | callback.onResult(items, nextPage)
47 | }
48 | }
49 | ```
50 |
51 | ### DataSource.Factory
52 |
53 | Create a `GithubDataSourceFactory`.
54 |
55 | ```kotlin
56 | class GithubDataSourceFactory(
57 | private val searchQuery: String,
58 | private val githubApi: GithubApiService,
59 | private val retryExecutor: Executor
60 | ) : DataSource.Factory() {
61 |
62 | val source = MutableLiveData()
63 |
64 | override fun create(): DataSource {
65 | val source = GithubPageKeyedDataSource(searchQuery, githubApi, retryExecutor)
66 | this.source.postValue(source)
67 | return source
68 | }
69 | }
70 | ```
71 |
72 | ### Repository
73 |
74 | Hook it up with your `InMemoryByPageKeyRepository`.
75 |
76 | ```kotlin
77 | class InMemoryByPageKeyRepository(
78 | private val githubApi: GithubApiService,
79 | private val networkExecutor: Executor
80 | ) : GithubRepository {
81 |
82 | @MainThread
83 | override fun searchUsers(searchQuery: String, pageSize: Int): Listing- {
84 |
85 | val factory = githubDataSourceFactory(searchQuery)
86 |
87 | val config = pagedListConfig(pageSize)
88 |
89 | val livePagedList = LivePagedListBuilder(factory, config)
90 | .setFetchExecutor(networkExecutor)
91 | .build()
92 |
93 | return Listing(
94 | pagedList = livePagedList,
95 | networkState = switchMap(factory.source) { it.network },
96 | retry = { factory.source.value?.retryAllFailed() },
97 | refresh = { factory.source.value?.invalidate() },
98 | refreshState = switchMap(factory.source) { it.initial })
99 | }
100 |
101 | private fun githubDataSourceFactory(searchQuery: String): GithubDataSourceFactory {
102 | return GithubDataSourceFactory(searchQuery, githubApi, networkExecutor)
103 | }
104 |
105 | private fun pagedListConfig(pageSize: Int): PagedList.Config {
106 | return PagedList.Config.Builder()
107 | .setEnablePlaceholders(false)
108 | .setInitialLoadSizeHint(pageSize * 2)
109 | .setPageSize(pageSize)
110 | .build()
111 | }
112 | }
113 | ```
114 |
115 | ### ViewModel
116 |
117 | Call `searchUsers` from the `SearchUsersViewModel`:
118 |
119 | ```kotlin
120 | class SearchUsersViewModel(
121 | private val repository: GithubRepository
122 | ) : ViewModel() {
123 |
124 | private val searchQuery = MutableLiveData()
125 | private val itemResult = map(searchQuery) {
126 | repository.searchUsers(it, PAGE_SIZE)
127 | }
128 | val items = switchMap(itemResult) { it.pagedList }!!
129 | val networkState = switchMap(itemResult) { it.networkState }!!
130 | val refreshState = switchMap(itemResult) { it.refreshState }!!
131 |
132 | ...
133 | }
134 | ```
135 |
136 | Thanks for stopping by! :)
137 |
138 | ### References:
139 |
140 | - [PagingWithNetworkSample](https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample)
141 |
--------------------------------------------------------------------------------
/README_images/paging_with_network_screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chetdeva/GithubIt/ec1216e2775459f546084c38c0b5a522073bb11c/README_images/paging_with_network_screenshot.gif
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: 'kotlin-kapt'
8 |
9 | android {
10 | compileSdkVersion 27
11 | defaultConfig {
12 | applicationId "com.chetdeva.githubit"
13 | minSdkVersion 17
14 | targetSdkVersion 27
15 | versionCode 1
16 | versionName "1.0"
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 | }
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
30 | implementation "com.android.support:appcompat-v7:$supportLibVersion"
31 | implementation "com.android.support:recyclerview-v7:$supportLibVersion"
32 | implementation "com.android.support:design:$supportLibVersion"
33 | implementation "com.android.support.constraint:constraint-layout:$constraintLayoutVersion"
34 |
35 | // architecture components
36 | implementation "android.arch.lifecycle:extensions:$archComponentsVersion"
37 | implementation "android.arch.lifecycle:runtime:$archComponentsVersion"
38 | implementation "android.arch.paging:runtime:$pagingVersion"
39 | kapt "android.arch.lifecycle:compiler:$archComponentsVersion"
40 |
41 | // retrofit
42 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
43 | implementation"com.squareup.retrofit2:converter-gson:$retrofitVersion"
44 | implementation "com.squareup.retrofit2:retrofit-mock:$retrofitVersion"
45 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpLoggingInterceptorVersion"
46 |
47 | implementation 'com.github.bumptech.glide:glide:4.7.1'
48 | kapt 'com.github.bumptech.glide:compiler:4.7.1'
49 |
50 | testImplementation 'junit:junit:4.12'
51 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
52 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
53 | }
54 |
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chetdeva/githubit/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.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.getTargetContext()
22 | assertEquals("com.chetdeva.githubit", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/GithubItApplication.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit
2 |
3 | import android.app.Application
4 |
5 | /**
6 | * @author chetansachdeva
7 | */
8 |
9 | class GithubItApplication: Application() {
10 |
11 | override fun onCreate() {
12 | super.onCreate()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/Injection.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit
2 |
3 | import android.arch.lifecycle.ViewModelProvider
4 | import android.content.Context
5 | import com.chetdeva.githubit.api.GithubApi
6 | import com.chetdeva.githubit.api.GithubApiService
7 | import com.chetdeva.githubit.data.GithubRepository
8 | import com.chetdeva.githubit.data.InMemoryByPageKeyRepository
9 | import com.chetdeva.githubit.ui.ViewModelFactory
10 | import java.util.concurrent.Executors
11 |
12 | /**
13 | * Class that handles object creation.
14 | * Like this, objects can be passed as parameters in the constructors and then replaced for
15 | * testing, where needed.
16 | */
17 | object Injection {
18 |
19 | // thread pool used for network requests
20 | @Suppress("PrivatePropertyName")
21 | private val NETWORK_IO = Executors.newFixedThreadPool(5)
22 |
23 | /**
24 | * Creates an instance of [GithubRepository] based on the [GithubApiService]
25 | */
26 | private fun provideGithubRepository(): GithubRepository {
27 | return InMemoryByPageKeyRepository(provideGithubApiService(), NETWORK_IO)
28 | }
29 |
30 | /**
31 | * Creates an instance of [GithubApiService] based on the [GithubApi]
32 | */
33 | private fun provideGithubApiService(): GithubApiService {
34 | return GithubApiService(GithubApi.create())
35 | }
36 |
37 | /**
38 | * Provides the [ViewModelProvider.Factory] that is then used to get a reference to
39 | * [ViewModel] objects.
40 | */
41 | fun provideViewModelFactory(): ViewModelProvider.Factory {
42 | return ViewModelFactory(provideGithubRepository())
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/api/ApiRequestHelper.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.api
2 |
3 | import retrofit2.Call
4 | import retrofit2.Callback
5 | import retrofit2.Response
6 | import java.io.IOException
7 |
8 | /**
9 | * @author chetansachdeva
10 | */
11 |
12 | object ApiRequestHelper {
13 |
14 | inline fun syncRequest(request: Call,
15 | onSuccess: (T?) -> Unit,
16 | onError: (String) -> Unit) {
17 | try {
18 | val response = request.execute()
19 | onSuccess(response.body())
20 | } catch (exception: IOException) {
21 | onError(exception.message ?: "unknown error")
22 | }
23 | }
24 |
25 | inline fun asyncRequest(request: Call,
26 | crossinline onSuccess: (T?) -> Unit,
27 | crossinline onError: (String) -> Unit) {
28 |
29 | request.enqueue(object : Callback {
30 | override fun onFailure(call: Call, t: Throwable) {
31 | onError(t.message ?: "unknown err")
32 | }
33 |
34 | override fun onResponse(call: Call, response: Response) {
35 | if (response.isSuccessful) {
36 | onSuccess(response.body())
37 | } else {
38 | onError("error code: ${response.code()}")
39 | }
40 | }
41 | })
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/api/GithubApi.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.api
2 |
3 | import okhttp3.OkHttpClient
4 | import okhttp3.logging.HttpLoggingInterceptor
5 | import okhttp3.logging.HttpLoggingInterceptor.Level
6 | import retrofit2.Call
7 | import retrofit2.Retrofit
8 | import retrofit2.converter.gson.GsonConverterFactory
9 | import retrofit2.http.GET
10 | import retrofit2.http.Query
11 |
12 | /**
13 | * Github API communication setup via Retrofit.
14 | */
15 | interface GithubApi {
16 | /**
17 | * Get repos ordered by stars.
18 | */
19 | @GET("search/users?sort=followers")
20 | fun searchUsers(@Query("q") query: String,
21 | @Query("page") page: Int,
22 | @Query("per_page") perPage: Int): Call
23 |
24 |
25 | companion object {
26 | private const val BASE_URL = "https://api.github.com/"
27 |
28 | fun create(): GithubApi {
29 | val logger = HttpLoggingInterceptor()
30 | logger.level = Level.BODY
31 |
32 | val client = OkHttpClient.Builder()
33 | .addInterceptor(logger)
34 | .build()
35 | return Retrofit.Builder()
36 | .baseUrl(BASE_URL)
37 | .client(client)
38 | .addConverterFactory(GsonConverterFactory.create())
39 | .build()
40 | .create(GithubApi::class.java)
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/api/GithubApiService.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.api
2 |
3 | /**
4 | * @author chetansachdeva
5 | */
6 |
7 | class GithubApiService(
8 | private val githubApi: GithubApi
9 | ) {
10 |
11 | fun searchUsersSync(query: String, page: Int, perPage: Int,
12 | onPrepared: () -> Unit,
13 | onSuccess: (UsersSearchResponse?) -> Unit,
14 | onError: (String) -> Unit) {
15 |
16 | val request = githubApi.searchUsers(query, page, perPage)
17 | onPrepared()
18 | ApiRequestHelper.syncRequest(request, onSuccess, onError)
19 | }
20 |
21 | fun searchUsersAsync(query: String, page: Int, perPage: Int,
22 | onPrepared: () -> Unit,
23 | onSuccess: (UsersSearchResponse?) -> Unit,
24 | onError: (String) -> Unit) {
25 |
26 | val request = githubApi.searchUsers(query, page, perPage)
27 | onPrepared()
28 | ApiRequestHelper.asyncRequest(request, onSuccess, onError)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/api/Item.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.api
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | class Item {
6 | @SerializedName("login")
7 | var login: String? = null
8 | @SerializedName("id")
9 | var id: Int? = null
10 | @SerializedName("node_id")
11 | var nodeId: String? = null
12 | @SerializedName("avatar_url")
13 | var avatarUrl: String? = null
14 | @SerializedName("gravatar_id")
15 | var gravatarId: String? = null
16 | var url: String? = null
17 | @SerializedName("html_url")
18 | var htmlUrl: String? = null
19 | @SerializedName("followers_url")
20 | var followersUrl: String? = null
21 | @SerializedName("subscriptions_url")
22 | var subscriptionsUrl: String? = null
23 | @SerializedName("organizations_url")
24 | var organizationsUrl: String? = null
25 | @SerializedName("repos_url")
26 | var reposUrl: String? = null
27 | @SerializedName("received_events_url")
28 | var receivedEventsUrl: String? = null
29 | @SerializedName("type")
30 | var type: String? = null
31 | @SerializedName("score")
32 | var score: Float? = null
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/api/UsersSearchResponse.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.api
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | /**
6 | * @author chetansachdeva
7 | */
8 |
9 | class UsersSearchResponse {
10 | @SerializedName("total_count")
11 | var totalCount: Int? = null
12 | @SerializedName("incomplete_results")
13 | var incompleteResults: Boolean? = null
14 | @SerializedName("items")
15 | var items: List
- ? = null
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/GithubDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.DataSource
5 | import com.chetdeva.githubit.api.GithubApiService
6 | import com.chetdeva.githubit.api.Item
7 | import java.util.concurrent.Executor
8 |
9 | /**
10 | * A simple data source factory which also provides a way to observe the last created data source.
11 | * This allows us to channel its network request status etc back to the UI. See the Listing creation
12 | * in the Repository class.
13 | */
14 | class GithubDataSourceFactory(
15 | private val searchQuery: String,
16 | private val githubApi: GithubApiService,
17 | private val retryExecutor: Executor
18 | ) : DataSource.Factory() {
19 |
20 | val source = MutableLiveData()
21 |
22 | override fun create(): DataSource {
23 | val source = GithubPageKeyedDataSource(searchQuery, githubApi, retryExecutor)
24 | this.source.postValue(source)
25 | return source
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/GithubPageKeyedDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.paging.PageKeyedDataSource
5 | import com.chetdeva.githubit.api.GithubApiService
6 | import com.chetdeva.githubit.api.Item
7 | import java.util.concurrent.Executor
8 |
9 | /**
10 | *
11 | */
12 | class GithubPageKeyedDataSource(
13 | private val searchQuery: String,
14 | private val apiService: GithubApiService,
15 | private val retryExecutor: Executor
16 | ) : PageKeyedDataSource() {
17 |
18 | var retry: (() -> Any)? = null
19 | val network = MutableLiveData()
20 | val initial = MutableLiveData()
21 |
22 | override fun loadBefore(params: LoadParams,
23 | callback: LoadCallback) {
24 | // ignored, since we only ever append to our initial load
25 | }
26 |
27 | /**
28 | * load initial
29 | */
30 | override fun loadInitial(params: LoadInitialParams,
31 | callback: LoadInitialCallback) {
32 |
33 | val currentPage = 1
34 | val nextPage = currentPage + 1
35 |
36 | makeLoadInitialRequest(params, callback, currentPage, nextPage)
37 | }
38 |
39 | private fun makeLoadInitialRequest(params: LoadInitialParams,
40 | callback: LoadInitialCallback,
41 | currentPage: Int,
42 | nextPage: Int) {
43 |
44 | // triggered by a refresh, we better execute sync
45 | apiService.searchUsersSync(
46 | query = searchQuery,
47 | page = currentPage,
48 | perPage = params.requestedLoadSize,
49 | onPrepared = {
50 | postInitialState(NetworkState.LOADING)
51 | },
52 | onSuccess = { responseBody ->
53 | val items = responseBody?.items ?: emptyList()
54 | retry = null
55 | postInitialState(NetworkState.LOADED)
56 | callback.onResult(items, null, nextPage)
57 | },
58 | onError = { errorMessage ->
59 | retry = { loadInitial(params, callback) }
60 | postInitialState(NetworkState.error(errorMessage))
61 | })
62 | }
63 |
64 | /**
65 | * load after
66 | */
67 | override fun loadAfter(params: LoadParams,
68 | callback: LoadCallback) {
69 |
70 | val currentPage = params.key
71 | val nextPage = currentPage + 1
72 |
73 | makeLoadAfterRequest(params, callback, currentPage, nextPage)
74 | }
75 |
76 | private fun makeLoadAfterRequest(params: LoadParams,
77 | callback: LoadCallback,
78 | currentPage: Int,
79 | nextPage: Int) {
80 |
81 | apiService.searchUsersAsync(
82 | query = searchQuery,
83 | page = currentPage,
84 | perPage = params.requestedLoadSize,
85 | onPrepared = {
86 | postAfterState(NetworkState.LOADING)
87 | },
88 | onSuccess = { responseBody ->
89 | val items = responseBody?.items ?: emptyList()
90 | retry = null
91 | callback.onResult(items, nextPage)
92 | postAfterState(NetworkState.LOADED)
93 | },
94 | onError = { errorMessage ->
95 | retry = { loadAfter(params, callback) }
96 | postAfterState(NetworkState.error(errorMessage))
97 | })
98 | }
99 |
100 | fun retryAllFailed() {
101 | val prevRetry = retry
102 | retry = null
103 | prevRetry?.let { retry ->
104 | retryExecutor.execute { retry() }
105 | }
106 | }
107 |
108 | private fun postInitialState(state: NetworkState) {
109 | network.postValue(state)
110 | initial.postValue(state)
111 | }
112 |
113 | private fun postAfterState(state: NetworkState) {
114 | network.postValue(state)
115 | }
116 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/GithubRepository.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | import com.chetdeva.githubit.api.Item
4 |
5 | /**
6 | * @author chetansachdeva
7 | */
8 |
9 | interface GithubRepository {
10 | fun searchUsers(searchQuery: String, pageSize: Int): Listing
-
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/InMemoryByPageKeyRepository.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | import android.arch.lifecycle.Transformations.switchMap
4 | import android.arch.paging.LivePagedListBuilder
5 | import android.arch.paging.PagedList
6 | import android.support.annotation.MainThread
7 | import com.chetdeva.githubit.api.GithubApiService
8 | import com.chetdeva.githubit.api.Item
9 | import java.util.concurrent.Executor
10 |
11 | /**
12 | * Repository implementation that returns a [Listing] that loads data directly from network
13 | */
14 | class InMemoryByPageKeyRepository(
15 | private val githubApi: GithubApiService,
16 | private val networkExecutor: Executor
17 | ) : GithubRepository {
18 |
19 | @MainThread
20 | override fun searchUsers(searchQuery: String, pageSize: Int): Listing
- {
21 |
22 | val factory = githubDataSourceFactory(searchQuery)
23 |
24 | val config = pagedListConfig(pageSize)
25 |
26 | val livePagedList = LivePagedListBuilder(factory, config)
27 | .setFetchExecutor(networkExecutor)
28 | .build()
29 |
30 | return Listing(
31 | pagedList = livePagedList,
32 | networkState = switchMap(factory.source) { it.network },
33 | retry = { factory.source.value?.retryAllFailed() },
34 | refresh = { factory.source.value?.invalidate() },
35 | refreshState = switchMap(factory.source) { it.initial })
36 | }
37 |
38 | private fun githubDataSourceFactory(searchQuery: String): GithubDataSourceFactory {
39 | return GithubDataSourceFactory(searchQuery, githubApi, networkExecutor)
40 | }
41 |
42 | private fun pagedListConfig(pageSize: Int): PagedList.Config {
43 | return PagedList.Config.Builder()
44 | .setEnablePlaceholders(false)
45 | .setInitialLoadSizeHint(pageSize * 2)
46 | .setPageSize(pageSize)
47 | .build()
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/Listing.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | import android.arch.lifecycle.LiveData
4 | import android.arch.paging.PagedList
5 |
6 | /**
7 | * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
8 | */
9 | data class Listing(
10 | // the LiveData of paged lists for the UI to observe
11 | val pagedList: LiveData>,
12 | // represents the network request status to show to the user
13 | val networkState: LiveData,
14 | // represents the refresh status to show to the user. Separate from network, this
15 | // value is importantly only when refresh is requested.
16 | val refreshState: LiveData,
17 | // refreshes the whole data and fetches it from scratch.
18 | val refresh: () -> Unit,
19 | // retries any failed requests.
20 | val retry: () -> Unit)
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/data/NetworkState.kt:
--------------------------------------------------------------------------------
1 | package com.chetdeva.githubit.data
2 |
3 | enum class Status {
4 | RUNNING,
5 | SUCCESS,
6 | FAILED
7 | }
8 |
9 | @Suppress("DataClassPrivateConstructor")
10 | data class NetworkState private constructor(
11 | val status: Status,
12 | val msg: String? = null) {
13 | companion object {
14 | val LOADED = NetworkState(Status.SUCCESS)
15 | val LOADING = NetworkState(Status.RUNNING)
16 | fun error(msg: String?) = NetworkState(Status.FAILED, msg)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chetdeva/githubit/ui/NetworkStateItemViewHolder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.chetdeva.githubit.ui
18 |
19 | import android.support.v7.widget.RecyclerView
20 | import android.view.LayoutInflater
21 | import android.view.View
22 | import android.view.ViewGroup
23 | import android.widget.Button
24 | import android.widget.ProgressBar
25 | import android.widget.TextView
26 | import com.chetdeva.githubit.R
27 | import com.chetdeva.githubit.data.NetworkState
28 | import com.chetdeva.githubit.data.Status
29 |
30 | /**
31 | * A View Holder that can display a loading or have click action.
32 | * It is used to show the network state of paging.
33 | */
34 | class NetworkStateItemViewHolder(view: View,
35 | private val retryCallback: () -> Unit) : RecyclerView.ViewHolder(view) {
36 |
37 | private val progressBar = view.findViewById(R.id.progress_bar)
38 | private val retry = view.findViewById