├── .gitignore
├── .idea
├── gradle.xml
├── misc.xml
├── modules.xml
└── runConfigurations.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── io
│ │ └── spacenoodles
│ │ └── makingyourappreactive
│ │ ├── App.kt
│ │ ├── dagger
│ │ ├── NetInterceptor.kt
│ │ ├── component
│ │ │ └── AppComponent.kt
│ │ └── module
│ │ │ ├── AppModule.kt
│ │ │ ├── NetModule.kt
│ │ │ └── RepositoryModule.kt
│ │ ├── model
│ │ ├── Image.kt
│ │ ├── ImagePost.kt
│ │ ├── ImgurResponse.kt
│ │ └── Tag.kt
│ │ ├── sync
│ │ ├── NetService.kt
│ │ └── repository
│ │ │ └── ImagePostRepository.kt
│ │ ├── util
│ │ └── extension
│ │ │ ├── ContextExtensions.kt
│ │ │ ├── ProgressBarExtensions.kt
│ │ │ ├── RxExtensions.kt
│ │ │ ├── StringExtensions.kt
│ │ │ └── ViewGroupExtensions.kt
│ │ ├── view
│ │ ├── activity
│ │ │ ├── BaseActivity.kt
│ │ │ ├── ImageDetailActivity.kt
│ │ │ └── MainActivity.kt
│ │ └── adapter
│ │ │ ├── ImagePostAdapter.kt
│ │ │ └── listener
│ │ │ └── LazyLoadRecyclerViewListener.kt
│ │ └── viewModel
│ │ ├── MainActivityViewModel.kt
│ │ └── state
│ │ ├── MainActivityState.kt
│ │ └── Status.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_arrow_upward_black_24dp.xml
│ ├── ic_image_24dp.xml
│ ├── ic_launcher_background.xml
│ ├── ic_link_black_24dp.xml
│ └── ic_search_white_24dp.xml
│ ├── layout
│ ├── activity_image_detail.xml
│ ├── activity_main.xml
│ └── list_item_image_post.xml
│ ├── menu
│ └── search_menu.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
├── 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/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .idea/vcs.xml
11 | app/src/androidTest/
12 | app/src/test/
13 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Groovy
40 |
41 |
42 | Java
43 |
44 |
45 | Potentially confusing code constructsGroovy
46 |
47 |
48 | Threading issuesJava
49 |
50 |
51 |
52 |
53 | Android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MakingYourAppReactive
2 | A demo app that shows how to implement RxJava and MVVM.
3 |
4 | # Notice
5 | If you wish to use this code in your own project, please change the client id in NetInterceptor to use your own value. You must use your own Imgur client id in order to access Imgur's API.
6 |
--------------------------------------------------------------------------------
/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 "io.spacenoodles.makingyourappreactive"
13 | minSdkVersion 21
14 | targetSdkVersion 27
15 | versionCode 1
16 | versionName "1.0"
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 | multiDexEnabled true
19 | }
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | }
27 |
28 | dependencies {
29 | implementation fileTree(dir: 'libs', include: ['*.jar'])
30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
31 | implementation "com.android.support:appcompat-v7:$support_lib_version"
32 | implementation 'com.android.support.constraint:constraint-layout:1.0.2'
33 | implementation "com.android.support:cardview-v7:$support_lib_version"
34 | implementation "com.android.support:recyclerview-v7:$support_lib_version"
35 | implementation 'com.android.support:design:27.0.2'
36 | testImplementation 'junit:junit:4.12'
37 | androidTestImplementation 'com.android.support.test:runner:1.0.1'
38 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
39 |
40 | //Rx
41 | implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
42 | implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version"
43 | implementation "io.reactivex.rxjava2:rxjava:$rxjava2_version"
44 |
45 | //Lifecycle, LiveData and ViewModel
46 | implementation "android.arch.lifecycle:runtime:1.0.3"
47 | implementation "android.arch.lifecycle:extensions:1.0.0"
48 | kapt "android.arch.lifecycle:compiler:1.0.0"
49 |
50 | //Dependency Injection Framework
51 | implementation "com.google.dagger:dagger:$dagger_version"
52 | kapt "com.google.dagger:dagger-compiler:$dagger_version"
53 |
54 | //Networking and Communication
55 | implementation "com.google.code.gson:gson:$gson_version"
56 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
57 | implementation("com.squareup.retrofit2:retrofit:$retrofit_version") {
58 | //Exclude Retrofit’s OkHttp peer-dependency module - we define our own
59 | exclude module: 'okhttp'
60 | }
61 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
62 | implementation 'com.squareup.okio:okio:1.13.0'
63 | implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
64 | implementation 'com.squareup.picasso:picasso:2.5.2'
65 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
66 |
67 | implementation 'javax.annotation:javax.annotation-api:1.3.2'
68 | annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
69 | }
70 |
--------------------------------------------------------------------------------
/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/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/App.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive
2 |
3 | import android.app.Application
4 | import io.spacenoodles.makingyourappreactive.dagger.component.AppComponent
5 | import io.spacenoodles.makingyourappreactive.dagger.component.DaggerAppComponent
6 | import io.spacenoodles.makingyourappreactive.dagger.module.AppModule
7 |
8 | class App : Application() {
9 | companion object {
10 | lateinit var component: AppComponent
11 | }
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | component = DaggerAppComponent
16 | .builder()
17 | .appModule(AppModule(this))
18 | .build()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/dagger/NetInterceptor.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.dagger
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 |
6 | class NetInterceptor : Interceptor {
7 | override fun intercept(chain: Interceptor.Chain): Response {
8 | val ongoing = chain.request().newBuilder()
9 |
10 | ongoing.header("Authorization", "Client-ID 37398025a821c1b")
11 | ongoing.addHeader("Content-Type", "application/json")
12 |
13 | return chain.proceed(ongoing.build())
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/dagger/component/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.dagger.component
2 |
3 | import dagger.Component
4 | import io.spacenoodles.makingyourappreactive.dagger.module.AppModule
5 | import io.spacenoodles.makingyourappreactive.dagger.module.NetModule
6 | import io.spacenoodles.makingyourappreactive.dagger.module.RepositoryModule
7 | import io.spacenoodles.makingyourappreactive.viewModel.MainActivityViewModel
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | @Component(modules = arrayOf(
12 | AppModule::class,
13 | NetModule::class,
14 | RepositoryModule::class)
15 | ) interface AppComponent {
16 | fun inject(mainActivityViewModel: MainActivityViewModel)
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/dagger/module/AppModule.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.dagger.module
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import javax.inject.Singleton
7 |
8 | @Module
9 | class AppModule(internal val context: Context) {
10 | @Provides
11 | @Singleton
12 | internal fun provideContext(): Context {
13 | return context
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/dagger/module/NetModule.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.dagger.module
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import io.spacenoodles.makingyourappreactive.BuildConfig
6 | import io.spacenoodles.makingyourappreactive.dagger.NetInterceptor
7 | import io.spacenoodles.makingyourappreactive.sync.NetService
8 | import okhttp3.OkHttpClient
9 | import retrofit2.Retrofit
10 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
11 | import retrofit2.converter.gson.GsonConverterFactory
12 | import java.util.concurrent.TimeUnit
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | class NetModule {
17 | companion object {
18 | val BASE_URL = "https://api.imgur.com/3/"
19 | val OKHTTP_TIMEOUT = 15L //In Seconds
20 | }
21 | @Provides
22 | @Singleton
23 | internal fun provideNetInterceptor(): NetInterceptor {
24 | return NetInterceptor()
25 | }
26 |
27 | @Provides
28 | @Singleton
29 | internal fun provideHttpClient(interceptor: NetInterceptor): OkHttpClient {
30 | val builder = OkHttpClient.Builder()
31 | .connectTimeout(OKHTTP_TIMEOUT, TimeUnit.SECONDS)
32 | .writeTimeout(OKHTTP_TIMEOUT, TimeUnit.SECONDS)
33 | .readTimeout(OKHTTP_TIMEOUT, TimeUnit.SECONDS)
34 | .addInterceptor(interceptor)
35 |
36 | try {
37 | if(BuildConfig.DEBUG) {
38 | val loggingInterceptor = okhttp3.logging.HttpLoggingInterceptor()
39 | loggingInterceptor.level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY
40 | builder.addInterceptor(loggingInterceptor)
41 | }
42 | } catch(e: Exception) {
43 | e.printStackTrace()
44 | }
45 |
46 | return builder.build()
47 | }
48 |
49 | @Provides
50 | internal fun provideNetService(okHttpClient: OkHttpClient, interceptor: NetInterceptor): NetService {
51 |
52 | val tokenClient = okHttpClient.newBuilder()
53 | .addInterceptor(interceptor)
54 | .build()
55 |
56 | val retrofit = Retrofit.Builder()
57 | .baseUrl(BASE_URL)
58 | .addConverterFactory(GsonConverterFactory.create())
59 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
60 | .client(tokenClient)
61 | .build()
62 | return retrofit.create(NetService::class.java)
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/dagger/module/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.dagger.module
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import io.spacenoodles.makingyourappreactive.sync.NetService
6 | import io.spacenoodles.makingyourappreactive.sync.repository.ImagePostRepository
7 | import javax.inject.Singleton
8 |
9 | @Module
10 | class RepositoryModule {
11 | @Provides
12 | @Singleton
13 | internal fun provideImagePostRepository(netService: NetService): ImagePostRepository {
14 | return ImagePostRepository(netService)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/model/Image.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Image (
7 | @SerializedName("id")
8 | @Expose
9 | val id: String? = null,
10 | @SerializedName("title")
11 | @Expose
12 | val title: String? = null,
13 | @SerializedName("description")
14 | @Expose
15 | val description: String? = null,
16 | @SerializedName("datetime")
17 | @Expose
18 | val datetime: Long? = null,
19 | @SerializedName("type")
20 | @Expose
21 | val type: String? = null,
22 | @SerializedName("animated")
23 | @Expose
24 | val animated: Boolean? = null,
25 | @SerializedName("width")
26 | @Expose
27 | val width: Long? = null,
28 | @SerializedName("height")
29 | @Expose
30 | val height: Long? = null,
31 | @SerializedName("size")
32 | @Expose
33 | val size: Long? = null,
34 | @SerializedName("views")
35 | @Expose
36 | val views: Long? = null,
37 | @SerializedName("bandwidth")
38 | @Expose
39 | val bandwidth: Long? = null,
40 | @SerializedName("favorite")
41 | @Expose
42 | val favorite: Boolean? = null,
43 | @SerializedName("nsfw")
44 | @Expose
45 | val nsfw: Boolean? = null,
46 | @SerializedName("is_ad")
47 | @Expose
48 | val isAd: Boolean? = null,
49 | @SerializedName("in_most_viral")
50 | @Expose
51 | val inMostViral: Boolean? = null,
52 | @SerializedName("has_sound")
53 | @Expose
54 | val hasSound: Boolean? = null,
55 | @SerializedName("tags")
56 | @Expose
57 | val tags: List? = null,
58 | @SerializedName("ad_type")
59 | @Expose
60 | val adType: Long? = null,
61 | @SerializedName("ad_url")
62 | @Expose
63 | val adUrl: String? = null,
64 | @SerializedName("in_gallery")
65 | @Expose
66 | val inGallery: Boolean? = null,
67 | @SerializedName("link")
68 | @Expose
69 | val link: String? = null,
70 | @SerializedName("favorite_count")
71 | @Expose
72 | val favoriteCount: Long? = null,
73 | @SerializedName("ups")
74 | @Expose
75 | val ups: Long? = null,
76 | @SerializedName("downs")
77 | @Expose
78 | val downs: Long? = null,
79 | @SerializedName("poLongs")
80 | @Expose
81 | val poLongs: Long? = null,
82 | @SerializedName("score")
83 | @Expose
84 | val score: Long? = null
85 | )
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/model/ImagePost.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ImagePost (
7 | @SerializedName("id")
8 | @Expose
9 | val id: String? = null,
10 | @SerializedName("title")
11 | @Expose
12 | val title: String? = null,
13 | @SerializedName("description")
14 | @Expose
15 | val description: String? = null,
16 | @SerializedName("datetime")
17 | @Expose
18 | val datetime: Long? = null,
19 | @SerializedName("cover")
20 | @Expose
21 | val cover: String? = null,
22 | @SerializedName("cover_width")
23 | @Expose
24 | val coverWidth: Long? = null,
25 | @SerializedName("cover_height")
26 | @Expose
27 | val coverHeight: Long? = null,
28 | @SerializedName("account_url")
29 | @Expose
30 | val accountUrl: String? = null,
31 | @SerializedName("account_id")
32 | @Expose
33 | val accountId: Long? = null,
34 | @SerializedName("privacy")
35 | @Expose
36 | val privacy: String? = null,
37 | @SerializedName("layout")
38 | @Expose
39 | val layout: String? = null,
40 | @SerializedName("views")
41 | @Expose
42 | val views: Long? = null,
43 | @SerializedName("link")
44 | @Expose
45 | val link: String? = null,
46 | @SerializedName("ups")
47 | @Expose
48 | val ups: Long? = null,
49 | @SerializedName("downs")
50 | @Expose
51 | val downs: Long? = null,
52 | @SerializedName("poLongs")
53 | @Expose
54 | val poLongs: Long? = null,
55 | @SerializedName("score")
56 | @Expose
57 | val score: Long? = null,
58 | @SerializedName("is_album")
59 | @Expose
60 | val isAlbum: Boolean? = null,
61 | @SerializedName("favorite")
62 | @Expose
63 | val favorite: Boolean? = null,
64 | @SerializedName("nsfw")
65 | @Expose
66 | val nsfw: Boolean? = null,
67 | @SerializedName("section")
68 | @Expose
69 | val section: String? = null,
70 | @SerializedName("comment_count")
71 | @Expose
72 | val commentCount: Long? = null,
73 | @SerializedName("favorite_count")
74 | @Expose
75 | val favoriteCount: Long? = null,
76 | @SerializedName("topic")
77 | @Expose
78 | val topic: String? = null,
79 | @SerializedName("topic_id")
80 | @Expose
81 | val topicId: Long? = null,
82 | @SerializedName("images_count")
83 | @Expose
84 | val imagesCount: Long? = null,
85 | @SerializedName("in_gallery")
86 | @Expose
87 | val inGallery: Boolean? = null,
88 | @SerializedName("is_ad")
89 | @Expose
90 | val isAd: Boolean? = null,
91 | @SerializedName("tags")
92 | @Expose
93 | val tags: List? = null,
94 | @SerializedName("ad_type")
95 | @Expose
96 | val adType: Long? = null,
97 | @SerializedName("ad_url")
98 | @Expose
99 | val adUrl: String? = null,
100 | @SerializedName("in_most_viral")
101 | @Expose
102 | val inMostViral: Boolean? = null,
103 | @SerializedName("images")
104 | @Expose
105 | val images: List? = null
106 | )
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/model/ImgurResponse.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class ImgurResponse(
7 | @SerializedName("data")
8 | @Expose
9 | val data: List? = null
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/model/Tag.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | data class Tag (
7 | @SerializedName("name")
8 | @Expose
9 | val name: String? = null,
10 | @SerializedName("display_name")
11 | @Expose
12 | val displayName: String? = null,
13 | @SerializedName("followers")
14 | @Expose
15 | val followers: Long? = null,
16 | @SerializedName("total_items")
17 | @Expose
18 | val totalItems: Long? = null,
19 | @SerializedName("following")
20 | @Expose
21 | val following: Boolean? = null,
22 | @SerializedName("background_hash")
23 | @Expose
24 | val backgroundHash: String? = null,
25 | @SerializedName("accent")
26 | @Expose
27 | val accent: String? = null,
28 | @SerializedName("background_is_animated")
29 | @Expose
30 | val backgroundIsAnimated: Boolean? = null,
31 | @SerializedName("thumbnail_is_animated")
32 | @Expose
33 | val thumbnailIsAnimated: Boolean? = null,
34 | @SerializedName("is_promoted")
35 | @Expose
36 | val isPromoted: Boolean? = null,
37 | @SerializedName("description")
38 | @Expose
39 | val description: String? = null
40 | )
41 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/sync/NetService.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.sync
2 |
3 | import io.reactivex.Maybe
4 | import io.spacenoodles.makingyourappreactive.model.ImgurResponse
5 | import retrofit2.http.GET
6 | import retrofit2.http.Path
7 | import retrofit2.http.Query
8 |
9 | interface NetService {
10 |
11 | @GET("gallery/search/time/{page}")
12 | fun requestImagePosts(@Path("page") page: Int, @Query("q") searchQuery: String)
13 | : Maybe
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/sync/repository/ImagePostRepository.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.sync.repository
2 |
3 | import io.reactivex.Maybe
4 | import io.reactivex.schedulers.Schedulers
5 | import io.spacenoodles.makingyourappreactive.model.ImgurResponse
6 | import io.spacenoodles.makingyourappreactive.sync.NetService
7 | import io.spacenoodles.makingyourappreactive.util.extension.maybe
8 |
9 | class ImagePostRepository(private val netService: NetService) {
10 |
11 | private var cache = ImagePostCache()
12 |
13 | fun getImagePosts(page: Int, searchQuery: String): Maybe {
14 |
15 | return Maybe
16 | .concat(cache.getPageOfImagePosts(page, searchQuery).subscribeOn(Schedulers.io()),
17 | netService.requestImagePosts(page, searchQuery)
18 | .doOnSuccess {
19 | cache.update(page, searchQuery, it)
20 | }.subscribeOn(Schedulers.io()))
21 | .firstElement()
22 | }
23 |
24 | class ImagePostCache {
25 | private var cachedImagePosts = HashMap()
26 | var searchQuery = ""
27 |
28 | private fun clear() {
29 | cachedImagePosts.clear()
30 | }
31 |
32 | fun getPageOfImagePosts(page: Int, searchQuery: String): Maybe {
33 | if (this.searchQuery != searchQuery) {
34 | clear()
35 | }
36 | this.searchQuery = searchQuery
37 | if (cachedImagePosts.keys.contains(page)) {
38 | return maybe(cachedImagePosts[page])
39 | }
40 | return Maybe.empty()
41 | }
42 |
43 | fun update(page: Int, query: String, data: ImgurResponse) {
44 | if (this.searchQuery == query)
45 | cachedImagePosts[page] = data
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/util/extension/ContextExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.util.extension
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 |
6 | fun Context.toastLong(text: String) {
7 | Toast.makeText(this, text, Toast.LENGTH_LONG).show()
8 | }
9 |
10 | fun Context.toastLong(textRes: Int) = Toast
11 | .makeText(this, textRes, Toast.LENGTH_LONG)
12 | .show()
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/util/extension/ProgressBarExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.util.extension
2 |
3 | import android.graphics.PorterDuff
4 | import android.widget.ProgressBar
5 |
6 | fun ProgressBar?.setColor(color: Int) {
7 | if (this == null) return
8 | this.indeterminateDrawable?.let {
9 | it.setColorFilter(color, PorterDuff.Mode.SRC_IN)
10 | }
11 | this.progressDrawable?.let {
12 | it.setColorFilter(color, PorterDuff.Mode.SRC_IN)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/util/extension/RxExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.util.extension
2 |
3 | import io.reactivex.Maybe
4 |
5 | fun maybe(value: T?): Maybe {
6 | return if(value == null) Maybe.empty()
7 | else Maybe.just(value)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/util/extension/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.util.extension
2 |
3 | import android.webkit.URLUtil
4 |
5 | fun String?.isValidURL(): Boolean {
6 | if (this == null) return false
7 | return this.isNotEmpty() && URLUtil.isValidUrl(this)
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/util/extension/ViewGroupExtensions.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.util.extension
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 |
7 | fun ViewGroup.inflate(layoutRes: Int): View {
8 | return LayoutInflater.from(context).inflate(layoutRes, this, false)
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/view/activity/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.view.activity
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import io.reactivex.disposables.CompositeDisposable
6 | import io.reactivex.disposables.Disposable
7 |
8 | abstract class BaseActivity: AppCompatActivity() {
9 | private lateinit var disposables: CompositeDisposable
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | init()
14 | }
15 |
16 | private fun init() {
17 | initRx()
18 | }
19 |
20 | private fun initRx() {
21 | disposables = CompositeDisposable()
22 | }
23 |
24 | @Synchronized protected fun addSub(disposable: Disposable?) {
25 | if (disposable == null) return
26 | disposables.add(disposable)
27 | }
28 |
29 | override fun onDestroy() {
30 | super.onDestroy()
31 | if (!disposables.isDisposed) disposables.dispose()
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/view/activity/ImageDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.view.activity
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import android.util.Log
6 | import android.view.View
7 | import android.view.ViewTreeObserver
8 | import com.squareup.picasso.Callback
9 | import com.squareup.picasso.Picasso
10 | import io.spacenoodles.makingyourappreactive.R
11 | import kotlinx.android.synthetic.main.activity_image_detail.*
12 | import android.view.WindowManager
13 |
14 |
15 | class ImageDetailActivity : AppCompatActivity() {
16 |
17 | private var imageUrl = ""
18 | private var title = ""
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | setContentView(R.layout.activity_image_detail)
23 |
24 | val w = window
25 | w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
26 |
27 | val extras = intent.extras
28 | extras?.let {
29 | imageUrl = it.getString(MainActivity.EXTRA_IMAGE_URL, "")
30 | title = it.getString(MainActivity.EXTRA_IMAGE_TITLE, "")
31 | }
32 |
33 | init()
34 | postponeEnterTransition()
35 | //TODO: Handle situations where picasso fails better
36 | }
37 |
38 | private fun init() {
39 | loadImage()
40 | setTitle()
41 | }
42 |
43 | private fun loadImage() {
44 | Picasso.with(this)
45 | .load(imageUrl)
46 | .placeholder(R.drawable.ic_image_24dp)
47 | .into(image_view, object: Callback {
48 | override fun onSuccess() {
49 | scheduleTransition(image_view)
50 | }
51 |
52 | override fun onError() {
53 | Log.i("HI", "HI")
54 | }
55 | })
56 | }
57 |
58 | private fun setTitle() {
59 | title_text.text = title
60 | }
61 |
62 | private fun scheduleTransition(sharedElement: View) {
63 | sharedElement.viewTreeObserver.addOnPreDrawListener(
64 | object : ViewTreeObserver.OnPreDrawListener {
65 | override fun onPreDraw(): Boolean {
66 | sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
67 | startPostponedEnterTransition()
68 | return true
69 | }
70 | })
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/view/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.view.activity
2 |
3 | import android.app.ActivityOptions
4 | import android.arch.lifecycle.Observer
5 | import android.arch.lifecycle.ViewModelProviders
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Bundle
9 | import android.support.v4.content.ContextCompat
10 | import android.support.v4.view.ViewCompat
11 | import android.support.v7.widget.LinearLayoutManager
12 | import android.support.v7.widget.RecyclerView
13 | import android.util.Log
14 | import android.view.Menu
15 | import android.view.View
16 | import android.view.inputmethod.InputMethodManager
17 | import android.widget.SearchView
18 | import io.reactivex.android.schedulers.AndroidSchedulers
19 | import io.spacenoodles.makingyourappreactive.R
20 | import io.spacenoodles.makingyourappreactive.util.extension.setColor
21 | import io.spacenoodles.makingyourappreactive.util.extension.toastLong
22 | import io.spacenoodles.makingyourappreactive.view.adapter.listener.LazyLoadRecyclerViewListener
23 | import io.spacenoodles.makingyourappreactive.viewModel.MainActivityViewModel
24 | import io.spacenoodles.makingyourappreactive.viewModel.state.MainActivityState
25 | import io.spacenoodles.makingyourappreactive.viewModel.state.Status
26 | import kotlinx.android.synthetic.main.activity_main.*
27 |
28 |
29 | class MainActivity : BaseActivity() {
30 |
31 | private lateinit var viewModel: MainActivityViewModel
32 |
33 | private var searchView: SearchView? = null
34 | private var lazyListener: LazyLoadRecyclerViewListener? = null
35 |
36 | companion object {
37 | val EXTRA_IMAGE_URL = "imageUrl"
38 | val EXTRA_IMAGE_TITLE = "imageTitle"
39 | }
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | setContentView(R.layout.activity_main)
44 |
45 | init()
46 | }
47 |
48 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
49 | menuInflater.inflate(R.menu.search_menu, menu)
50 | searchView = menu.findItem(R.id.search).actionView as SearchView
51 | searchView?.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
52 | override fun onQueryTextSubmit(query: String): Boolean {
53 | viewModel.search(query)
54 | return true
55 | }
56 |
57 | override fun onQueryTextChange(newText: String): Boolean {
58 | viewModel.search(newText)
59 | return true
60 | }
61 | })
62 | searchView?.setQuery(viewModel.searchQuery, true)
63 | return true
64 | }
65 |
66 | private fun init() {
67 | initViewModel()
68 | initLayout()
69 | initSubscriptions()
70 | }
71 |
72 | private fun initViewModel() {
73 | viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
74 | }
75 |
76 | private fun initLayout() {
77 | progress_spinner.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
78 |
79 | val layoutManager = LinearLayoutManager(this)
80 | image_post_list.layoutManager = layoutManager
81 | image_post_list.adapter = viewModel.imagePostAdapter
82 | image_post_list.addOnScrollListener(object: RecyclerView.OnScrollListener() {
83 | override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
84 | super.onScrollStateChanged(recyclerView, newState)
85 | hideKeyboard()
86 | }
87 | })
88 |
89 | //Setup infinite scrolling
90 | lazyListener = object: LazyLoadRecyclerViewListener(layoutManager) {
91 | override fun onLoadMore(currentPage: Int) {
92 | viewModel.loadMore(searchView?.query.toString())
93 | }
94 | }
95 | image_post_list.addOnScrollListener(lazyListener)
96 | }
97 |
98 | private fun initSubscriptions() {
99 | viewModel.state.observe(this, Observer {
100 | it?.let {
101 | update(it)
102 | }
103 | })
104 |
105 | attachClickListenerToImageAdapter()
106 | }
107 |
108 | private fun update(state: MainActivityState) {
109 | when (state.status) {
110 | Status.LOADING -> {
111 | progress_spinner.visibility = View.VISIBLE
112 | search_result_header.visibility = View.GONE
113 | upward_arrow.visibility = View.GONE
114 | instructions.visibility = View.GONE
115 | }
116 |
117 | Status.TOO_SHORT -> {
118 | upward_arrow.visibility = View.VISIBLE
119 | instructions.visibility = View.VISIBLE
120 | search_result_header.visibility = View.GONE
121 | }
122 |
123 | Status.SUCCESS -> {
124 | if (image_post_list.adapter.itemCount == 0) {
125 | toastLong(R.string.no_images)
126 | upward_arrow.visibility = View.VISIBLE
127 | instructions.visibility = View.VISIBLE
128 | search_result_header.visibility = View.GONE
129 | } else {
130 | search_result_header.visibility = View.VISIBLE
131 | search_result_header.text = resources.getString(R.string.search_result_header, searchView?.query)
132 | }
133 | lazyListener?.setLoading(false)
134 | progress_spinner.visibility = View.GONE
135 | }
136 |
137 | Status.COMPLETE -> {
138 | progress_spinner.visibility = View.GONE
139 | }
140 |
141 | Status.ERROR -> {
142 | Log.e("MainActivity: ", state.error?.localizedMessage)
143 | }
144 | }
145 | }
146 |
147 | private fun attachClickListenerToImageAdapter() {
148 | addSub(viewModel.imagePostAdapter.clickStream.observeOn(AndroidSchedulers.mainThread())?.subscribe({
149 | searchView?.clearFocus()
150 | var intent = Intent(this@MainActivity, ImageDetailActivity::class.java)
151 | viewModel.imagePostAdapter.let { adapter ->
152 | intent.putExtra(MainActivity.EXTRA_IMAGE_URL, it.data.images?.first()?.link?:"")
153 | intent.putExtra(MainActivity.EXTRA_IMAGE_TITLE, it.data.title)
154 | }
155 | val options = ActivityOptions
156 | .makeSceneTransitionAnimation(this@MainActivity, it.imageView, ViewCompat.getTransitionName(it.imageView))
157 | startActivity(intent, options.toBundle())
158 | }))
159 | }
160 |
161 | private fun hideKeyboard() {
162 | val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
163 |
164 | // check if no view has focus:
165 | val v = this.currentFocus ?: return
166 |
167 | inputManager.hideSoftInputFromWindow(v.windowToken, 0)
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/view/adapter/ImagePostAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.view.adapter
2 |
3 | import android.app.Service
4 | import android.content.ClipData
5 | import android.content.ClipboardManager
6 | import android.support.v7.widget.RecyclerView
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import com.squareup.picasso.Picasso
10 | import io.reactivex.Observable
11 | import io.reactivex.subjects.PublishSubject
12 | import io.spacenoodles.makingyourappreactive.R
13 | import io.spacenoodles.makingyourappreactive.model.ImagePost
14 | import io.spacenoodles.makingyourappreactive.util.extension.inflate
15 | import io.spacenoodles.makingyourappreactive.util.extension.toastLong
16 | import kotlinx.android.synthetic.main.list_item_image_post.view.*
17 |
18 | class ImagePostAdapter(items: List) : RecyclerView.Adapter() {
19 | var items = items
20 |
21 | private val onClickSubject = PublishSubject.create()
22 | val clickStream: Observable get() = onClickSubject.hide()
23 |
24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
25 | return ViewHolder(parent.inflate(R.layout.list_item_image_post))
26 | }
27 |
28 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
29 | holder.bind(items[position], { onClickSubject.onNext(it) })
30 | }
31 |
32 | override fun getItemCount(): Int {
33 | return items.size
34 | }
35 |
36 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
37 | fun bind(item: ImagePost, click: (ImageItem) -> Unit = {}) {
38 | itemView.setOnClickListener(null)
39 | Picasso
40 | .with(itemView.context)
41 | .cancelRequest(itemView.image_view)
42 | itemView.image_view.setImageResource(R.drawable.ic_image_24dp)
43 | Picasso
44 | .with(itemView.context)
45 | .load(item.images?.first()?.link)
46 | .placeholder(R.drawable.ic_image_24dp)
47 | .fit()
48 | .centerCrop()
49 | .into(itemView.image_view)
50 |
51 | itemView.image_view.setOnClickListener { click.invoke(ImageItem(itemView.image_view, item)) }
52 | itemView.link_button.setOnClickListener {
53 | val clipboard = itemView.context.getSystemService(Service.CLIPBOARD_SERVICE) as ClipboardManager
54 | val clip = ClipData.newPlainText("Link", item.images?.first()?.link)
55 | clipboard.primaryClip = clip
56 | itemView.context.toastLong(itemView.context.getString(R.string.link_copied))
57 | }
58 |
59 | itemView.score_text.text = itemView.context.getString(R.string.score, item.score)
60 | itemView.title_text.text = item.title
61 | }
62 | }
63 |
64 | data class ImageItem(val imageView: View, val data: ImagePost)
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/view/adapter/listener/LazyLoadRecyclerViewListener.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.view.adapter.listener
2 |
3 | import android.support.v7.widget.LinearLayoutManager
4 | import android.support.v7.widget.RecyclerView
5 |
6 | abstract class LazyLoadRecyclerViewListener(private val layoutManager: LinearLayoutManager) : RecyclerView.OnScrollListener() {
7 | private var loading = true //True if we are still waiting for data to load
8 | private var currentPage = 1
9 |
10 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
11 | super.onScrolled(recyclerView, dx, dy)
12 |
13 | val visibleItemCount = recyclerView.childCount
14 | val totalItemCount = layoutManager.itemCount
15 | val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
16 |
17 | if (!loading && totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) {
18 | //End of list has been reached
19 | if (hasMore(totalItemCount)) {
20 | loading = true
21 | onLoadMore(++currentPage)
22 | }
23 | }
24 | }
25 |
26 | fun hasMore(totalItemCount: Int): Boolean {
27 | return true
28 | }
29 |
30 | fun reset() {
31 | loading = true
32 | currentPage = 1
33 | }
34 |
35 | fun setLoading(loading: Boolean) {
36 | this.loading = loading
37 | }
38 |
39 | abstract fun onLoadMore(currentPage: Int)
40 |
41 | companion object {
42 | private val VISIBLE_THRESHOLD = 5 //Number of items left in list before we start loading more
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/viewModel/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.viewModel
2 |
3 | import android.arch.lifecycle.MutableLiveData
4 | import android.arch.lifecycle.ViewModel
5 | import io.reactivex.Maybe
6 | import io.reactivex.android.schedulers.AndroidSchedulers
7 | import io.reactivex.disposables.CompositeDisposable
8 | import io.reactivex.disposables.Disposable
9 | import io.reactivex.schedulers.Schedulers
10 | import io.reactivex.subjects.PublishSubject
11 | import io.spacenoodles.makingyourappreactive.App
12 | import io.spacenoodles.makingyourappreactive.model.ImagePost
13 | import io.spacenoodles.makingyourappreactive.model.ImgurResponse
14 | import io.spacenoodles.makingyourappreactive.sync.repository.ImagePostRepository
15 | import io.spacenoodles.makingyourappreactive.util.extension.isValidURL
16 | import io.spacenoodles.makingyourappreactive.view.adapter.ImagePostAdapter
17 | import io.spacenoodles.makingyourappreactive.viewModel.state.MainActivityState
18 | import java.util.concurrent.TimeUnit
19 | import javax.inject.Inject
20 |
21 | class MainActivityViewModel : ViewModel() {
22 |
23 | val state: MutableLiveData
24 |
25 | init {
26 | App.component.inject(this)
27 | state = MutableLiveData()
28 | initRx()
29 | initSearch()
30 | }
31 |
32 | @Inject lateinit var imageRepo: ImagePostRepository
33 |
34 | private var currentPage = 0
35 | var imagePostAdapter = ImagePostAdapter(ArrayList())
36 | var searchQuery = ""
37 |
38 | private lateinit var searchEmitter: PublishSubject
39 |
40 | private lateinit var disposables: CompositeDisposable
41 |
42 | fun search(query: String) {
43 | if (searchQuery != query) {
44 | imagePostAdapter.items = ArrayList()
45 | imagePostAdapter.notifyDataSetChanged()
46 | }
47 | searchQuery = query
48 | searchEmitter.onNext(query)
49 | }
50 |
51 | private fun searchForImages(query: String): Maybe {
52 | return imageRepo.getImagePosts(currentPage, query)
53 | }
54 |
55 | fun loadMore(searchQuery: String) {
56 | currentPage++
57 | searchEmitter.onNext(searchQuery)
58 | }
59 |
60 | private fun initRx() {
61 | disposables = CompositeDisposable()
62 | }
63 |
64 | private fun initSearch() {
65 | searchEmitter = PublishSubject.create()
66 | addSub(
67 | searchEmitter
68 | .subscribeOn(Schedulers.io())
69 | .debounce(250, TimeUnit.MILLISECONDS)
70 | .doOnNext {
71 | if (it.length < 2) {
72 | state.postValue(MainActivityState.tooShort())
73 | }
74 | }
75 | .filter { it.length > 1 }
76 | .doOnNext { state.postValue(MainActivityState.loading()) }
77 | .doOnTerminate { state.postValue(MainActivityState.complete()) }
78 | .switchMap {
79 | searchForImages(it).toObservable()
80 | }
81 | .map {
82 | val newData = ImgurResponse(it.data?.filter { item ->
83 | item.images?.isNotEmpty() == true && item.images.first().link.isValidURL()
84 | && item.nsfw == false
85 | && item.images.first().size ?:Long.MAX_VALUE <= 1500000L
86 | })
87 | newData
88 | }
89 | .observeOn(AndroidSchedulers.mainThread())
90 | .doOnNext { response ->
91 | updateAdapter(response.data)
92 | state.postValue(MainActivityState.success())
93 | }
94 | .subscribe({},
95 | {
96 | state.postValue(MainActivityState.error(it))
97 | })
98 | )
99 | }
100 |
101 | private fun updateAdapter(data: List?) {
102 | //If the search is at least length 2, put the items in the adapter, otherwise clear the adapter
103 | if (searchQuery.length > 1) {
104 | imagePostAdapter.items = imagePostAdapter.items.plus(data ?: ArrayList())
105 | imagePostAdapter.notifyDataSetChanged()
106 | } else {
107 | imagePostAdapter.items = ArrayList()
108 | }
109 | }
110 |
111 | @Synchronized
112 | private fun addSub(disposable: Disposable?) {
113 | if (disposable == null) return
114 | disposables.add(disposable)
115 | }
116 |
117 | override fun onCleared() {
118 | super.onCleared()
119 | if (!disposables.isDisposed) disposables.dispose()
120 | }
121 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/viewModel/state/MainActivityState.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.viewModel.state
2 |
3 |
4 | data class MainActivityState private constructor(val status: Status,
5 | val error: Throwable? = null) {
6 | companion object {
7 |
8 | fun loading(): MainActivityState {
9 | return MainActivityState(Status.LOADING)
10 | }
11 |
12 | fun success(): MainActivityState {
13 | return MainActivityState(Status.SUCCESS)
14 | }
15 |
16 | fun complete(): MainActivityState {
17 | return MainActivityState(Status.COMPLETE)
18 | }
19 |
20 | fun tooShort(): MainActivityState {
21 | return MainActivityState(Status.TOO_SHORT)
22 | }
23 |
24 | fun error(error: Throwable): MainActivityState {
25 | return MainActivityState(Status.ERROR, error)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/spacenoodles/makingyourappreactive/viewModel/state/Status.kt:
--------------------------------------------------------------------------------
1 | package io.spacenoodles.makingyourappreactive.viewModel.state
2 |
3 | enum class Status {
4 | LOADING,
5 | SUCCESS,
6 | COMPLETE,
7 | TOO_SHORT,
8 | ERROR
9 | }
--------------------------------------------------------------------------------
/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/ic_arrow_upward_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_image_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_link_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_image_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
18 |
19 |
28 |
29 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
22 |
23 |
34 |
35 |
44 |
45 |
56 |
57 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_image_post.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
15 |
16 |
25 |
26 |
42 |
43 |
54 |
55 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/search_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00BCD4
4 | #0097A7
5 | #B2EBF2
6 | #607D8B
7 | #212121
8 | #757575
9 | @color/white
10 | #BDBDBD
11 | #FFFFFF
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4dp
5 | 8dp
6 | 10dp
7 | 12dp
8 | 16dp
9 | 24dp
10 | 36dp
11 | 48dp
12 | 56dp
13 |
14 |
15 | 22sp
16 | 16sp
17 | 56sp
18 |
19 |
20 | 250dp
21 | 16dp
22 |
23 |
24 | 100dp
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Make Your Apps Reactive
3 |
4 | Search for images...
5 | Results for %1$s
6 | Image link copied to clipboard!
7 | ImageDetailActivity
8 | Score: %1$d
9 | image_transition
10 | Search for images up there!
11 | No Images Found
12 | Search Query Too Short
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.10'
5 |
6 | //Dependency Versions
7 | ext.support_lib_version = '27.0.2'
8 | ext.rxandroid_version = '2.0.1'
9 | ext.rxjava2_version = '2.1.1'
10 | ext.okhttp_version = '3.7.0'
11 | ext.retrofit_version = '2.3.0'
12 | ext.dagger_version = '2.9'
13 | ext.gson_version = '2.8.2'
14 |
15 | repositories {
16 | google()
17 | jcenter()
18 | }
19 | dependencies {
20 | classpath 'com.android.tools.build:gradle:3.3.2'
21 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
22 |
23 | // NOTE: Do not place your application dependencies here; they belong
24 | // in the individual module build.gradle files
25 | }
26 | }
27 |
28 | allprojects {
29 | repositories {
30 | google()
31 | jcenter()
32 | }
33 | }
34 |
35 | task clean(type: Delete) {
36 | delete rootProject.buildDir
37 | }
38 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpaceWaffles/MakingYourAppReactive/0189e343c38446d22a47476e06975e684d8d3192/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 17 21:47:51 SGT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------