├── .gitignore ├── .idea ├── assetWizardSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hari │ │ └── restaurantfinder │ │ └── restaurantfinder │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hari │ │ │ └── restaurantfinder │ │ │ ├── RestaurantApplication.kt │ │ │ ├── api │ │ │ ├── AuthInterceptor.kt │ │ │ ├── RestaurantApi.kt │ │ │ └── RestaurantApiEndpoint.kt │ │ │ ├── di │ │ │ └── DependencyInjection.kt │ │ │ ├── model │ │ │ ├── Restaurant.kt │ │ │ ├── RestaurantsObject.kt │ │ │ ├── StateObject.kt │ │ │ ├── UserRating.kt │ │ │ └── mvi │ │ │ │ ├── RestaurantActionState.kt │ │ │ │ └── RestaurantViewState.kt │ │ │ ├── presenter │ │ │ ├── GetRestaurantsUseCase.kt │ │ │ ├── MviRestaurantPresenter.kt │ │ │ └── MviToolbarPresenter.kt │ │ │ └── view │ │ │ ├── BaseViewHolder.kt │ │ │ ├── CustomToolbar.kt │ │ │ ├── MoreViewItemsHolder.kt │ │ │ ├── MviRestaurantView.kt │ │ │ ├── MviToolbarView.kt │ │ │ ├── RestaurantActivity.kt │ │ │ ├── RestaurantAdapter.kt │ │ │ └── RestaurantViewHolder.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── divider.xml │ │ ├── ic_arrow_back_white_24dp.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_warning.xml │ │ ├── layout │ │ ├── activity_restaurants.xml │ │ ├── activity_restaurants_grid.xml │ │ ├── item_more_available.xml │ │ └── item_restaurant_detail.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 │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hari │ └── restaurantfinder │ └── restaurantfinder │ └── 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/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /.idea/assetWizardSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Model View Intent 2 | 3 | This is a simple example to show how MVI pattern works. It is completely coded in kotlin. 4 | Understand the basics of MVI pattern before looking into the code. 5 | 6 | ## Thirdparty Libraries Used 7 | 8 | - Retrofit 2 and Gson Convertor 9 | - Mosby MVI 10 | - RxJava 11 | - Glide 12 | 13 | ## About Project 14 | 15 | Displays the restaurants available in your location. Used Zomato api to retrieve the restaurants details. 16 | 17 | Generate a api key from Zomato developer site (https://developers.zomato.com/api) and add it your `local.properties` like below 18 | 19 | `zomato_api_key = "api_key"` 20 | 21 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 28 7 | defaultConfig { 8 | applicationId "com.hari.restaurantfinder.restaurantfinder" 9 | minSdkVersion 21 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | buildTypes.each { 21 | Properties properties = new Properties() 22 | properties.load(project.rootProject.file("local.properties").newDataInputStream()) 23 | def zomatoApikey = properties.getProperty("zomato_api_key", "") 24 | it.buildConfigField('String', 'ZOMATO_API_KEY', zomatoApikey) 25 | } 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 32 | implementation 'com.android.support:appcompat-v7:' + rootProject.ext.supportLibVersion 33 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 36 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 37 | 38 | // Dagger 39 | implementation 'com.google.dagger:dagger:' + rootProject.ext.daggerVersion 40 | kapt 'com.google.dagger:dagger-compiler:' + rootProject.ext.daggerVersion 41 | 42 | // Retrofit 43 | implementation 'com.squareup.retrofit2:retrofit:' + rootProject.ext.retrofitVersion 44 | implementation 'com.google.code.gson:gson:' + rootProject.ext.gsonVersion 45 | implementation 'com.squareup.retrofit2:converter-gson:' + rootProject.ext.retrofitVersion 46 | implementation 'com.squareup.retrofit2:adapter-rxjava2:' + rootProject.ext.retrofitVersion 47 | implementation 'com.squareup.okhttp3:logging-interceptor:' + rootProject.ext.logInterceptorVersion 48 | 49 | // Recycler view 50 | implementation 'com.android.support:recyclerview-v7:' + rootProject.ext.supportLibVersion 51 | 52 | //Glide 53 | implementation 'com.github.bumptech.glide:glide:' + rootProject.ext.glideVersion 54 | 55 | //RxJava and RxAndroid 56 | implementation 'io.reactivex.rxjava2:rxjava:' + rootProject.ext.rxJava 57 | implementation 'io.reactivex.rxjava2:rxandroid:' + rootProject.ext.rxAndroid 58 | 59 | // Mosby for MVI 60 | implementation 'com.hannesdorfmann.mosby3:mvi:' + rootProject.ext.mosbyMvi 61 | 62 | // RxBinding 63 | implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:' + rootProject.ext.rxBindingVersion 64 | implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4-kotlin:' + rootProject.ext.rxBindingVersion 65 | implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:' + rootProject.ext.rxBindingVersion 66 | } 67 | 68 | -------------------------------------------------------------------------------- /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/hari/restaurantfinder/restaurantfinder/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.restaurantfinder 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.hari.restaurantfinder.restaurantfinder", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/RestaurantApplication.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.hari.restaurantfinder.di.DependencyInjection 6 | 7 | /** 8 | * @author Hari Hara Sudhan.N 9 | */ 10 | class RestaurantApplication : Application() { 11 | 12 | private val dependencyInjection: DependencyInjection = DependencyInjection() 13 | 14 | companion object { 15 | fun getDependencyInjection(context: Context): DependencyInjection { 16 | return (context.applicationContext as RestaurantApplication).dependencyInjection 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/api/AuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.api 2 | 3 | import com.hari.restaurantfinder.BuildConfig 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | /** 8 | * Adds api key to the header. 9 | * 10 | * @author Hari Hara Sudhan.N 11 | */ 12 | class AuthInterceptor: Interceptor { 13 | override fun intercept(chain: Interceptor.Chain): Response { 14 | var request = chain.request() 15 | request = request?.newBuilder() 16 | ?.addHeader("user-key", BuildConfig.ZOMATO_API_KEY) 17 | ?.build() 18 | return chain.proceed(request) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/api/RestaurantApi.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.api 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.logging.HttpLoggingInterceptor 5 | import retrofit2.Retrofit 6 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 7 | import retrofit2.converter.gson.GsonConverterFactory 8 | 9 | /** 10 | * Provides the retrofit instance. 11 | * 12 | * @author Hari Hara Sudhan.N 13 | */ 14 | class RestaurantApi { 15 | companion object { 16 | fun getClient(): Retrofit { 17 | val client = OkHttpClient.Builder() 18 | client.addInterceptor(AuthInterceptor()) 19 | 20 | // To display the request and response details in log 21 | val interceptor = HttpLoggingInterceptor() 22 | interceptor.level = HttpLoggingInterceptor.Level.HEADERS 23 | client.addInterceptor(interceptor) 24 | 25 | return Retrofit.Builder() 26 | .baseUrl("https://developers.zomato.com/") 27 | .client(client.build()) 28 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 29 | .addConverterFactory(GsonConverterFactory.create()) 30 | .build() 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/api/RestaurantApiEndpoint.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.api 2 | 3 | import com.hari.restaurantfinder.model.RestaurantsObject 4 | import io.reactivex.Observable 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | /** 9 | * @author Hari Hara Sudhan.N 10 | */ 11 | interface RestaurantApiEndpoint { 12 | 13 | @GET("api/v2.1/search") 14 | fun getRestaurantsAtLocation(@Query("lat") lat: Double, @Query("lon") lon: Double, 15 | @Query("start") start: Int, @Query("count") count: Int): Observable 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/di/DependencyInjection.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.di 2 | 3 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 4 | import com.hari.restaurantfinder.presenter.MviRestaurantPresenter 5 | import com.hari.restaurantfinder.presenter.MviToolbarPresenter 6 | import io.reactivex.Observable 7 | 8 | /** 9 | * @author Hari Hara Sudhan.N 10 | */ 11 | class DependencyInjection { 12 | 13 | private lateinit var mviRestaurantPresenter: MviRestaurantPresenter 14 | 15 | fun newRestaurantPresenter(latitude: Double, longitude: Double): MviRestaurantPresenter { 16 | mviRestaurantPresenter = MviRestaurantPresenter(latitude, longitude) 17 | return mviRestaurantPresenter 18 | } 19 | 20 | fun newRestaurantToolbarPresenter(): MviToolbarPresenter { 21 | val restaurantsCountObservable: Observable = mviRestaurantPresenter 22 | .viewStateObservable 23 | 24 | return MviToolbarPresenter(restaurantsCountObservable) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/Restaurant.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * @author Hari Hara Sudhan.N 7 | */ 8 | data class Restaurant(val id: String, 9 | val name: String, 10 | @SerializedName("featured_image") val thumbnailUrl: String, 11 | @SerializedName("user_rating") val userRating: UserRating) -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/RestaurantsObject.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * @author Hari Hara Sudhan.N 7 | */ 8 | data class RestaurantsObject(@SerializedName("restaurants") var restaurants: List) -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/StateObject.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model 2 | 3 | /** 4 | * @author Hari Hara Sudhan.N 5 | */ 6 | data class StateObject( 7 | val type: String, 8 | val restaurant: Restaurant? = null, 9 | val itemCount: Int? = null, 10 | val isLoading: Boolean? = null, 11 | val error: Throwable? = null) { 12 | companion object { 13 | const val TYPE_MORE_RESTAURANT = "MORE_RESTAURANT" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/UserRating.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * @author Hari Hara Sudhan.N 7 | */ 8 | data class UserRating(@SerializedName("aggregate_rating")val rating: String, 9 | @SerializedName("rating_text")val ratingText: String, 10 | @SerializedName("rating_color")val ratingColor: String) -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/mvi/RestaurantActionState.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model.mvi 2 | 3 | import com.hari.restaurantfinder.model.RestaurantsObject 4 | 5 | /** 6 | * @author Hari Hara Sudhan.N 7 | */ 8 | sealed class RestaurantActionState { 9 | object LoadingState: RestaurantActionState() 10 | object LoadMoreState: RestaurantActionState() 11 | object PullToRefreshState: RestaurantActionState() 12 | data class DataState(val restaurantsObject: RestaurantsObject) : RestaurantActionState() 13 | data class LoadMoreDataState(val restaurantsObject: RestaurantsObject) : RestaurantActionState() 14 | data class ErrorState(val error: Throwable) : RestaurantActionState() 15 | data class LoadMoreErrorState(val error: Throwable) : RestaurantActionState() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/model/mvi/RestaurantViewState.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.model.mvi 2 | 3 | import com.hari.restaurantfinder.model.RestaurantsObject 4 | 5 | /** 6 | * @author Hari Hara Sudhan.N 7 | */ 8 | data class RestaurantViewState( 9 | val isPageLoading: Boolean = false, 10 | val isPullToRefresh: Boolean = false, 11 | val isMoreRestaurantsLoading: Boolean = false, 12 | val restaurantsObject: RestaurantsObject? = null, 13 | val error: Throwable? = null) { 14 | 15 | fun copy(): Builder { 16 | return Builder(this) 17 | } 18 | 19 | class Builder(restaurantViewState: RestaurantViewState) { 20 | private var isPageLoading = restaurantViewState.isPageLoading 21 | private var isPullToRefresh = restaurantViewState.isPullToRefresh 22 | private var isMoreRestaurantsLoading = restaurantViewState.isMoreRestaurantsLoading 23 | private var restaurantsObject: RestaurantsObject? = restaurantViewState.restaurantsObject 24 | private var error: Throwable? = restaurantViewState.error 25 | 26 | fun isPageLoading(isPageLoading: Boolean): Builder { 27 | this.isPageLoading = isPageLoading 28 | return this 29 | } 30 | 31 | fun isPullToRefresh(isPullToRefresh: Boolean): Builder { 32 | this.isPullToRefresh = isPullToRefresh 33 | return this 34 | } 35 | 36 | fun isMoreRestaurantsLoading(isMoreRestaurantsLoading: Boolean): Builder { 37 | this.isMoreRestaurantsLoading = isMoreRestaurantsLoading 38 | return this 39 | } 40 | 41 | fun data(newRestaurantsObject: RestaurantsObject?): Builder { 42 | restaurantsObject = newRestaurantsObject 43 | return this 44 | } 45 | 46 | fun error(error: Throwable?): Builder { 47 | this.error = error 48 | return this 49 | } 50 | 51 | fun build(): RestaurantViewState { 52 | return RestaurantViewState(isPageLoading, isPullToRefresh, isMoreRestaurantsLoading, restaurantsObject, error) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/presenter/GetRestaurantsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.presenter 2 | 3 | import com.hari.restaurantfinder.api.RestaurantApi 4 | import com.hari.restaurantfinder.api.RestaurantApiEndpoint 5 | import com.hari.restaurantfinder.model.mvi.RestaurantActionState 6 | import io.reactivex.Observable 7 | 8 | /** 9 | * @author Hari Hara Sudhan.N 10 | */ 11 | object GetRestaurantsUseCase { 12 | 13 | /*Note: All are same api service calls, 14 | only the Loading, data, error states differs. */ 15 | 16 | // Default Loading 17 | fun getRestaurants(latitude: Double, longitude: Double): Observable { 18 | val endpoint = RestaurantApi.getClient().create(RestaurantApiEndpoint::class.java) 19 | return endpoint.getRestaurantsAtLocation(latitude, longitude, 0, 3) 20 | .map { RestaurantActionState.DataState(it) } 21 | .startWith(RestaurantActionState.LoadingState) 22 | .onErrorReturn { (RestaurantActionState.ErrorState(it)) } 23 | } 24 | 25 | // Load more restaurants 26 | fun getMoreRestaurants(latitude: Double, longitude: Double): Observable { 27 | val endpoint = RestaurantApi.getClient().create(RestaurantApiEndpoint::class.java) 28 | return endpoint.getRestaurantsAtLocation(latitude, longitude, 0, 3) 29 | .map { RestaurantActionState.LoadMoreDataState(it) } 30 | .startWith(RestaurantActionState.LoadMoreState) 31 | .onErrorReturn { (RestaurantActionState.LoadMoreErrorState(it)) } 32 | } 33 | 34 | // PullToRefresh 35 | fun getRestaurantsByPTR(latitude: Double, longitude: Double) : Observable { 36 | val endpoint = RestaurantApi.getClient().create(RestaurantApiEndpoint::class.java) 37 | return endpoint.getRestaurantsAtLocation(latitude, longitude, 0, 3) 38 | .map { RestaurantActionState.DataState(it) } 39 | .startWith(RestaurantActionState.PullToRefreshState) 40 | .onErrorReturn{(RestaurantActionState.ErrorState(it))} 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/presenter/MviRestaurantPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.presenter 2 | 3 | import android.util.Log 4 | import com.google.gson.Gson 5 | import com.hannesdorfmann.mosby3.mvi.MviBasePresenter 6 | import com.hari.restaurantfinder.model.StateObject 7 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 8 | import com.hari.restaurantfinder.model.mvi.RestaurantActionState 9 | import com.hari.restaurantfinder.view.MviRestaurantView 10 | import io.reactivex.Observable 11 | import io.reactivex.android.schedulers.AndroidSchedulers 12 | import io.reactivex.schedulers.Schedulers 13 | import java.util.concurrent.TimeUnit 14 | 15 | /** 16 | * @author Hari Hara Sudhan.N 17 | */ 18 | class MviRestaurantPresenter( 19 | private val latitude: Double, 20 | private val longitude: Double 21 | ) : MviBasePresenter() { 22 | 23 | override fun bindIntents() { 24 | 25 | // Default loading of page 26 | val restaurantsState: Observable = intent(MviRestaurantView::emitFirstTimeLoadEvent) 27 | .subscribeOn(Schedulers.io()) 28 | .debounce(200, TimeUnit.MILLISECONDS) 29 | .doOnNext { Log.d("Action", "First time page load event.") } 30 | .flatMap { GetRestaurantsUseCase.getRestaurants(latitude, longitude) } 31 | .observeOn(AndroidSchedulers.mainThread()) 32 | 33 | // Load more restaurants 34 | val loadMoreRestaurantsState: Observable = intent(MviRestaurantView::emitLoadMoreRestaurantsEvent) 35 | .subscribeOn(Schedulers.io()) 36 | .debounce(200, TimeUnit.MILLISECONDS) 37 | .doOnNext { Log.d("Action", "Load more restaurants event.") } 38 | .switchMap { GetRestaurantsUseCase.getMoreRestaurants(latitude, longitude) } 39 | .observeOn(AndroidSchedulers.mainThread()) 40 | 41 | // Loading page using pull to refresh 42 | val pullToRefreshState: Observable = intent(MviRestaurantView::emitPullToRefreshEvent) 43 | .subscribeOn(Schedulers.io()) 44 | .debounce(200, TimeUnit.MILLISECONDS) 45 | .doOnNext { Log.d("Action", "Pull to refresh event.") } 46 | .switchMap { GetRestaurantsUseCase.getRestaurantsByPTR(latitude, longitude) } 47 | .observeOn(AndroidSchedulers.mainThread()) 48 | 49 | val allViewState: Observable = Observable.merge( 50 | restaurantsState, 51 | loadMoreRestaurantsState, 52 | pullToRefreshState) 53 | 54 | val initializeState = RestaurantViewState(isPageLoading = true) 55 | val stateObservable = allViewState 56 | .scan(initializeState, this::viewStateReducer) 57 | .doOnNext { Log.d("State", Gson().toJson(it)) } 58 | 59 | subscribeViewState( 60 | stateObservable, 61 | MviRestaurantView::displayRestaurants 62 | ) 63 | } 64 | 65 | /** 66 | * Create a new immutable state by comparing current and previous states. 67 | */ 68 | private fun viewStateReducer(previousState: RestaurantViewState, 69 | currentState: RestaurantActionState): RestaurantViewState { 70 | return when (currentState) { 71 | RestaurantActionState.LoadingState -> { 72 | previousState 73 | .copy() 74 | .isPageLoading(true) 75 | .isPullToRefresh(false) 76 | .isMoreRestaurantsLoading(false) 77 | .data(null) 78 | .error(null) 79 | .build() 80 | } 81 | RestaurantActionState.PullToRefreshState -> { 82 | previousState 83 | .copy() 84 | .isPullToRefresh(true) 85 | .isPageLoading(false) 86 | .isMoreRestaurantsLoading(false) 87 | .data(null) 88 | .error(null) 89 | .build() 90 | } 91 | RestaurantActionState.LoadMoreState -> { 92 | val bridgeObject = StateObject(type = StateObject.TYPE_MORE_RESTAURANT, isLoading = true) 93 | val newRestaurantsArray = ArrayList() 94 | newRestaurantsArray.addAll(previousState.restaurantsObject!!.restaurants) 95 | newRestaurantsArray.removeAt(newRestaurantsArray.lastIndex) 96 | newRestaurantsArray.add(bridgeObject) 97 | previousState.restaurantsObject!!.restaurants = newRestaurantsArray 98 | previousState 99 | .copy() 100 | .isMoreRestaurantsLoading(true) 101 | .isPullToRefresh(false) 102 | .isPageLoading(false) 103 | .data(previousState.restaurantsObject) 104 | .build() 105 | } 106 | is RestaurantActionState.DataState -> { 107 | // Adds the item to load the remaining restaurantsObject while loading 108 | val bridgeObject = StateObject(type = StateObject.TYPE_MORE_RESTAURANT, itemCount = 3) 109 | val newRestaurantsArray = ArrayList() 110 | newRestaurantsArray.addAll(currentState.restaurantsObject.restaurants) 111 | newRestaurantsArray.add(bridgeObject) 112 | currentState.restaurantsObject.restaurants = newRestaurantsArray 113 | previousState 114 | .copy() 115 | .data(currentState.restaurantsObject) 116 | .build() 117 | } 118 | is RestaurantActionState.ErrorState -> { 119 | previousState 120 | .copy() 121 | .data(null) 122 | .error(currentState.error) 123 | .build() 124 | } 125 | is RestaurantActionState.LoadMoreDataState -> { 126 | val bridgeObject = StateObject(type = StateObject.TYPE_MORE_RESTAURANT, itemCount = 3) 127 | val newRestaurantsArray = ArrayList() 128 | newRestaurantsArray.addAll(previousState.restaurantsObject!!.restaurants) 129 | newRestaurantsArray.removeAt(newRestaurantsArray.lastIndex) 130 | newRestaurantsArray.addAll(currentState.restaurantsObject.restaurants) 131 | newRestaurantsArray.add(bridgeObject) 132 | currentState.restaurantsObject.restaurants = newRestaurantsArray 133 | previousState 134 | .copy() 135 | .isPageLoading(false) 136 | .isPullToRefresh(false) 137 | .data(currentState.restaurantsObject) 138 | .build() 139 | } 140 | is RestaurantActionState.LoadMoreErrorState -> { 141 | val bridgeObject = StateObject(type = StateObject.TYPE_MORE_RESTAURANT, error = currentState.error) 142 | val newRestaurantsArray = ArrayList() 143 | newRestaurantsArray.addAll(previousState.restaurantsObject!!.restaurants) 144 | newRestaurantsArray.removeAt(newRestaurantsArray.lastIndex) 145 | newRestaurantsArray.add(bridgeObject) 146 | previousState.restaurantsObject!!.restaurants = newRestaurantsArray 147 | previousState 148 | .copy() 149 | .isPageLoading(false) 150 | .isPullToRefresh(false) 151 | .data(previousState.restaurantsObject) 152 | .build() 153 | } 154 | } 155 | } 156 | 157 | public override fun getViewStateObservable(): Observable { 158 | return super.getViewStateObservable() 159 | } 160 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/presenter/MviToolbarPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.presenter 2 | 3 | import com.hannesdorfmann.mosby3.mvi.MviBasePresenter 4 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 5 | import com.hari.restaurantfinder.view.MviToolbarView 6 | import io.reactivex.Observable 7 | 8 | /** 9 | * @author Hari Hara Sudhan.N 10 | */ 11 | class MviToolbarPresenter(private val countObservable: Observable) 12 | : MviBasePresenter() { 13 | 14 | override fun bindIntents() { 15 | subscribeViewState(countObservable, MviToolbarView::displayRestaurantsCount) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import com.hari.restaurantfinder.model.StateObject 6 | 7 | /** 8 | * @author Hari Hara Sudhan.N 9 | */ 10 | abstract class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 11 | abstract fun setData(stateObject: StateObject) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/CustomToolbar.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Parcelable 6 | import android.support.v7.widget.Toolbar 7 | import android.util.AttributeSet 8 | import com.hannesdorfmann.mosby3.ViewGroupMviDelegate 9 | import com.hannesdorfmann.mosby3.ViewGroupMviDelegateCallback 10 | import com.hannesdorfmann.mosby3.ViewGroupMviDelegateImpl 11 | import com.hari.restaurantfinder.R 12 | import com.hari.restaurantfinder.RestaurantApplication 13 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 14 | import com.hari.restaurantfinder.presenter.MviToolbarPresenter 15 | 16 | /** 17 | * @author Hari Hara Sudhan.N 18 | */ 19 | class CustomToolbar constructor(context: Context, attributeSet: AttributeSet) : Toolbar(context, attributeSet), 20 | MviToolbarView, ViewGroupMviDelegateCallback { 21 | 22 | init { 23 | setNavigationIcon(R.drawable.ic_arrow_back_white_24dp) 24 | } 25 | 26 | private val delegate: ViewGroupMviDelegate = 27 | ViewGroupMviDelegateImpl(this, this, true) 28 | 29 | override fun superOnSaveInstanceState(): Parcelable? { 30 | return super.onSaveInstanceState() 31 | } 32 | 33 | override fun setRestoringViewState(restoringViewState: Boolean) { 34 | } 35 | 36 | override fun superOnRestoreInstanceState(state: Parcelable?) { 37 | super.onRestoreInstanceState(state) 38 | } 39 | 40 | override fun createPresenter(): MviToolbarPresenter { 41 | return RestaurantApplication.getDependencyInjection(context).newRestaurantToolbarPresenter() 42 | } 43 | 44 | override fun getMvpView(): MviToolbarView { 45 | return this 46 | } 47 | 48 | override fun displayRestaurantsCount(state: RestaurantViewState) { 49 | if((state.isPageLoading && state.error != null) 50 | || (state.isPullToRefresh && state.error != null)) { 51 | title = resources.getString(R.string.load_error) 52 | } else if (state.isPageLoading 53 | && state.restaurantsObject == null) { 54 | title = resources.getString(R.string.loading_page_message) 55 | } else if (state.restaurantsObject != null 56 | && (state.isPageLoading || state.isPullToRefresh || state.isMoreRestaurantsLoading)) { 57 | val count = state.restaurantsObject.restaurants.count() 58 | title = String.format(resources.getString(R.string.restaurants_count), if (count == 0) count else count - 1) 59 | } else if(state.isPullToRefresh 60 | && state.restaurantsObject == null) { 61 | title = resources.getString(R.string.pull_to_refresh_message) 62 | } 63 | } 64 | 65 | override fun onAttachedToWindow() { 66 | super.onAttachedToWindow() 67 | delegate.onAttachedToWindow() 68 | } 69 | 70 | override fun onDetachedFromWindow() { 71 | super.onDetachedFromWindow() 72 | delegate.onDetachedFromWindow() 73 | } 74 | 75 | @SuppressLint("MissingSuperCall") 76 | override fun onSaveInstanceState(): Parcelable? { 77 | return delegate.onSaveInstanceState() 78 | } 79 | 80 | @SuppressLint("MissingSuperCall") 81 | override fun onRestoreInstanceState(state: Parcelable?) { 82 | delegate.onRestoreInstanceState(state) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/MoreViewItemsHolder.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.view.View 4 | import com.hari.restaurantfinder.model.StateObject 5 | import kotlinx.android.synthetic.main.item_more_available.view.* 6 | 7 | /** 8 | * @author Hari Hara Sudhan.N 9 | */ 10 | class MoreViewItemsHolder(itemView: View, 11 | private val moreItemClickListener: MoreItemClickListener?) : 12 | BaseViewHolder(itemView), View.OnClickListener { 13 | 14 | interface MoreItemClickListener { 15 | fun loadMoreItems() 16 | } 17 | 18 | private val loadMoreButton = itemView.loadMoreButtton 19 | private val errorRetryButton = itemView.errorRetryButton 20 | 21 | init { 22 | moreItemClickListener?.let { 23 | itemView.setOnClickListener(this) 24 | loadMoreButton.setOnClickListener(this) 25 | errorRetryButton.setOnClickListener(this) 26 | } 27 | } 28 | 29 | override fun setData(stateObject: StateObject) { 30 | when { 31 | stateObject.isLoading == true -> { 32 | itemView.loadingView.visibility = View.VISIBLE 33 | itemView.moreItemsCount.visibility = View.GONE 34 | loadMoreButton.visibility = View.GONE 35 | errorRetryButton.visibility = View.GONE 36 | itemView.isClickable = false 37 | } 38 | stateObject.error != null -> { 39 | itemView.loadingView.visibility = View.GONE 40 | itemView.moreItemsCount.visibility = View.GONE 41 | loadMoreButton.visibility = View.GONE 42 | errorRetryButton.visibility = View.VISIBLE 43 | itemView.isClickable = true 44 | } 45 | else -> { 46 | itemView.moreItemsCount.visibility = View.VISIBLE 47 | itemView.moreItemsCount.text = "+" + stateObject.itemCount 48 | loadMoreButton.visibility = View.VISIBLE 49 | errorRetryButton.visibility = View.GONE 50 | itemView.loadingView.visibility = View.GONE 51 | itemView.isClickable = true 52 | } 53 | } 54 | } 55 | 56 | override fun onClick(v: View?) { 57 | moreItemClickListener?.loadMoreItems() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/MviRestaurantView.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import com.hannesdorfmann.mosby3.mvp.MvpView 4 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 5 | import io.reactivex.Observable 6 | 7 | /** 8 | * @author Hari Hara Sudhan.N 9 | */ 10 | interface MviRestaurantView : MvpView { 11 | fun emitFirstTimeLoadEvent(): Observable 12 | fun emitPullToRefreshEvent(): Observable 13 | fun emitLoadMoreRestaurantsEvent(): Observable 14 | fun displayRestaurants(restaurantViewState: RestaurantViewState) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/MviToolbarView.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import com.hannesdorfmann.mosby3.mvp.MvpView 4 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 5 | 6 | /** 7 | * @author Hari Hara Sudhan.N 8 | */ 9 | interface MviToolbarView : MvpView { 10 | fun displayRestaurantsCount(state: RestaurantViewState) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/RestaurantActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.location.LocationManager 7 | import android.os.Bundle 8 | import android.support.v4.content.ContextCompat 9 | import android.support.v7.widget.GridLayoutManager 10 | import android.view.View 11 | import com.hannesdorfmann.mosby3.mvi.MviActivity 12 | import com.hari.restaurantfinder.R 13 | import com.hari.restaurantfinder.RestaurantApplication 14 | import com.hari.restaurantfinder.model.mvi.RestaurantViewState 15 | import com.hari.restaurantfinder.presenter.MviRestaurantPresenter 16 | import com.jakewharton.rxbinding2.support.v4.widget.refreshes 17 | import com.jakewharton.rxbinding2.support.v7.widget.navigationClicks 18 | import io.reactivex.Observable 19 | import io.reactivex.schedulers.Schedulers 20 | import kotlinx.android.synthetic.main.activity_restaurants.* 21 | 22 | /** 23 | * @author Hari Hara Sudhan.N 24 | */ 25 | class RestaurantActivity : MviActivity(), MviRestaurantView { 26 | 27 | lateinit var adapter: RestaurantAdapter 28 | 29 | override fun emitPullToRefreshEvent() = refreshContainer.refreshes() 30 | 31 | override fun emitFirstTimeLoadEvent() = Observable.just(true).subscribeOn(Schedulers.io()) 32 | 33 | override fun emitLoadMoreRestaurantsEvent(): Observable { 34 | return adapter.loadMoreRestaurantsObservable() 35 | } 36 | 37 | override fun createPresenter(): MviRestaurantPresenter { 38 | var latitude = 0.0 39 | var longitude = 0.0 40 | val locationManager: LocationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager 41 | val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) 42 | if (isGpsEnabled 43 | && ContextCompat.checkSelfPermission( 44 | this, 45 | Manifest.permission.ACCESS_COARSE_LOCATION 46 | ) == PackageManager.PERMISSION_GRANTED 47 | ) { 48 | val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) 49 | location.let { 50 | latitude = location.latitude 51 | longitude = location.longitude 52 | } 53 | } 54 | 55 | return RestaurantApplication.getDependencyInjection(this).newRestaurantPresenter(latitude, longitude) 56 | } 57 | 58 | override fun displayRestaurants(restaurantViewState: RestaurantViewState) { 59 | if (restaurantViewState.isPageLoading 60 | && restaurantViewState.error != null) { 61 | renderErrorState(restaurantViewState) 62 | } else if (restaurantViewState.isPullToRefresh 63 | && restaurantViewState.error != null) { 64 | renderErrorStatePTR(restaurantViewState) 65 | } else if (restaurantViewState.isPageLoading 66 | && restaurantViewState.restaurantsObject == null) { 67 | renderPageLoading() 68 | } else if (restaurantViewState.isPageLoading 69 | && restaurantViewState.restaurantsObject != null) { 70 | renderLoadingDataState(restaurantViewState) 71 | } else if (restaurantViewState.isPullToRefresh 72 | && restaurantViewState.restaurantsObject == null) { 73 | renderPullToRefreshState() 74 | } else if (restaurantViewState.isPullToRefresh 75 | && restaurantViewState.restaurantsObject != null) { 76 | renderDataStatePTR(restaurantViewState) 77 | } else if (restaurantViewState.isMoreRestaurantsLoading 78 | && restaurantViewState.restaurantsObject != null) { 79 | renderDataState(restaurantViewState) 80 | } 81 | } 82 | 83 | private fun renderPageLoading() { 84 | loadingIndicator.visibility = View.VISIBLE 85 | recyclerView.visibility = View.GONE 86 | } 87 | 88 | private fun renderPullToRefreshState() { 89 | recyclerView.visibility = View.GONE 90 | networkError.visibility = View.GONE 91 | } 92 | 93 | private fun renderDataStatePTR(state: RestaurantViewState) { 94 | refreshContainer.isRefreshing = false 95 | recyclerView.visibility = View.VISIBLE 96 | adapter.setAdapterData(state.restaurantsObject!!.restaurants) 97 | adapter.notifyDataSetChanged() 98 | } 99 | 100 | private fun renderDataState(state: RestaurantViewState) { 101 | recyclerView.visibility = View.VISIBLE 102 | adapter.setAdapterData(state.restaurantsObject!!.restaurants) 103 | adapter.notifyDataSetChanged() 104 | } 105 | 106 | private fun renderLoadingDataState(state: RestaurantViewState) { 107 | loadingIndicator.visibility = View.GONE 108 | recyclerView.visibility = View.VISIBLE 109 | adapter.setAdapterData(state.restaurantsObject!!.restaurants) 110 | adapter.notifyDataSetChanged() 111 | } 112 | 113 | private fun renderErrorState(state: RestaurantViewState) { 114 | loadingIndicator.visibility = View.GONE 115 | networkError.visibility = View.VISIBLE 116 | } 117 | 118 | private fun renderErrorStatePTR(state: RestaurantViewState) { 119 | refreshContainer.isRefreshing = false 120 | networkError.visibility = View.VISIBLE 121 | } 122 | 123 | override fun onCreate(savedInstanceState: Bundle?) { 124 | super.onCreate(savedInstanceState) 125 | setContentView(R.layout.activity_restaurants) 126 | setSupportActionBar(toolBar) 127 | toolBar.setNavigationOnClickListener { finish() } 128 | val layoutManager = GridLayoutManager(this, 2) 129 | recyclerView.layoutManager = layoutManager 130 | adapter = RestaurantAdapter(this) 131 | recyclerView.adapter = adapter 132 | } 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/RestaurantAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.ViewGroup 7 | import com.hari.restaurantfinder.R 8 | import com.hari.restaurantfinder.model.StateObject 9 | import io.reactivex.Observable 10 | import io.reactivex.subjects.PublishSubject 11 | 12 | /** 13 | * @author Hari Hara Sudhan.N 14 | */ 15 | class RestaurantAdapter(private val context: Context) 16 | : RecyclerView.Adapter(), 17 | MoreViewItemsHolder.MoreItemClickListener { 18 | 19 | private val loadMoreRestaurants = PublishSubject.create() 20 | private var restaurants: List?=null 21 | 22 | companion object { 23 | const val RESTAURANT = 0 24 | const val MORE_ITEMS = 1 25 | } 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { 28 | return when(viewType) { 29 | MORE_ITEMS -> MoreViewItemsHolder(LayoutInflater.from(context).inflate(R.layout.item_more_available, 30 | parent, 31 | false), this) 32 | else -> { 33 | RestaurantViewHolder(LayoutInflater.from(context).inflate(R.layout.activity_restaurants_grid, 34 | parent, 35 | false)) 36 | } 37 | } 38 | } 39 | 40 | override fun getItemCount(): Int { 41 | return if (restaurants == null) { 42 | 0 43 | } else { 44 | restaurants!!.size 45 | } 46 | } 47 | 48 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 49 | holder.setData(restaurants!![position]) 50 | } 51 | 52 | override fun getItemViewType(position: Int): Int { 53 | return if(restaurants!![position].type == StateObject.TYPE_MORE_RESTAURANT) { 54 | MORE_ITEMS 55 | } else { 56 | RESTAURANT 57 | } 58 | } 59 | 60 | override fun loadMoreItems() { 61 | loadMoreRestaurants.onNext("") 62 | } 63 | 64 | fun setAdapterData(restaurants: List) { 65 | this.restaurants = restaurants 66 | } 67 | 68 | fun loadMoreRestaurantsObservable(): Observable { 69 | return loadMoreRestaurants 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hari/restaurantfinder/view/RestaurantViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.hari.restaurantfinder.view 2 | 3 | import android.view.View 4 | import com.bumptech.glide.Glide 5 | import com.hari.restaurantfinder.model.StateObject 6 | import com.hari.restaurantfinder.model.Restaurant 7 | import kotlinx.android.synthetic.main.activity_restaurants_grid.view.* 8 | 9 | /** 10 | * @author Hari Hara Sudhan.N 11 | */ 12 | class RestaurantViewHolder(itemView: View) : BaseViewHolder(itemView) { 13 | override fun setData(stateObject: StateObject) { 14 | val restaurant: Restaurant? = stateObject.restaurant 15 | itemView.restaurantName.text = restaurant?.name 16 | Glide.with(itemView.context) 17 | .load(restaurant?.thumbnailUrl) 18 | .into(itemView.restaurantImage) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warning.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_restaurants.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 23 | 28 | 29 | 30 | 36 | 37 | 44 | 45 | 46 | 47 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_restaurants_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 26 | 27 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_more_available.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 |