├── .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