├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_cat.png
│ │ │ │ └── ic_favorite_black_24dp.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── ic_cat.png
│ │ │ │ └── ic_favorite_black_24dp.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── ic_cat.png
│ │ │ │ └── ic_favorite_black_24dp.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_cat.png
│ │ │ │ └── ic_favorite_black_24dp.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ └── ic_cat.png
│ │ │ ├── font
│ │ │ │ └── comicneue_regular.otf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── cat_list_item.xml
│ │ │ │ ├── fragment_favorites.xml
│ │ │ │ ├── fragment_cats.xml
│ │ │ │ ├── favorite_list_item.xml
│ │ │ │ └── activity_main.xml
│ │ │ ├── menu
│ │ │ │ └── main_tabs.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ ├── tumblr_kqagu1BAU71qzv5pwo1_400.jpg
│ │ │ │ ├── tumblr_l4jhgowvwP1qzadygo1_500.jpg
│ │ │ │ ├── tumblr_lhd86qgPZb1qgnva2o1_500.jpg
│ │ │ │ ├── tumblr_lhyv4kDLF21qcn249o1_250.gif
│ │ │ │ ├── tumblr_lkshxbyTsS1qd47y6o1_250.gif
│ │ │ │ ├── tumblr_lotdb6y79Y1qdvbl3o1_1280.jpg
│ │ │ │ ├── tumblr_lqvliwvOcZ1qcnzavo1_500.gif
│ │ │ │ ├── tumblr_lrgcywLdPE1qmwvx6o1_1280.jpg
│ │ │ │ ├── tumblr_lsqulhdKbm1qbe5pxo1_1280.jpg
│ │ │ │ ├── tumblr_lu8wzvo16d1qlyuwso1_400.jpg
│ │ │ │ ├── tumblr_luop03t84I1qg1f07o1_500.jpg
│ │ │ │ ├── tumblr_lx1a4ttlZj1qcxyrro1_500.jpg
│ │ │ │ ├── tumblr_lxi2pjxdzd1qfneslo1_500.jpg
│ │ │ │ ├── tumblr_ly804whs9T1qcmibao1_500.jpg
│ │ │ │ ├── tumblr_m00p9qEHHd1rq1wumo1_500.jpg
│ │ │ │ ├── tumblr_m0g9rhxZ7e1r0mbi6o1_1280.jpg
│ │ │ │ ├── tumblr_m12hhorbIS1qbe5pxo1_1280.jpg
│ │ │ │ ├── tumblr_m247xuJYKk1qbe5pxo1_1280.jpg
│ │ │ │ ├── tumblr_m4zpvvyIwY1qzex9io1_1280.jpg
│ │ │ │ └── tumblr_mbu9a1l1QP1qhwmnpo1_1280.jpg
│ │ │ └── preloaded_cats.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── catrates
│ │ │ │ ├── di
│ │ │ │ ├── modules
│ │ │ │ │ ├── MainActivityModule.kt
│ │ │ │ │ ├── FavoriteCatsPersistenceModule.kt
│ │ │ │ │ ├── RestModule.kt
│ │ │ │ │ ├── BuildersModule.kt
│ │ │ │ │ ├── AppModule.kt
│ │ │ │ │ └── CatStoreModule.kt
│ │ │ │ └── AppComponent.kt
│ │ │ │ ├── annotations
│ │ │ │ └── StoreCacheDir.java
│ │ │ │ ├── persistence
│ │ │ │ ├── AppDatabase.java
│ │ │ │ ├── FavoriteCatsRepository.kt
│ │ │ │ └── CatDao.java
│ │ │ │ ├── catapi
│ │ │ │ ├── CatData.java
│ │ │ │ ├── CatApi.kt
│ │ │ │ └── CatResponse.java
│ │ │ │ ├── data
│ │ │ │ └── CatParser.kt
│ │ │ │ ├── models
│ │ │ │ └── Cat.java
│ │ │ │ ├── favorites
│ │ │ │ ├── FavoriteCatsPresenter.kt
│ │ │ │ └── FavoriteCatsFragment.kt
│ │ │ │ ├── CatRatesApplication.kt
│ │ │ │ ├── catlist
│ │ │ │ ├── CatListPresenter.kt
│ │ │ │ └── CatsFragment.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── catrates
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── catrates
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
├── schemas
│ ├── com.example.catrates.AppDatabase
│ │ └── 1.json
│ └── com.example.catrates.persistence.AppDatabase
│ │ └── 1.json
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── .idea
├── vcs.xml
├── modules.xml
├── runConfigurations.xml
├── gradle.xml
└── misc.xml
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-hdpi/ic_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-mdpi/ic_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-xhdpi/ic_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-xxhdpi/ic_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-xxxhdpi/ic_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/font/comicneue_regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/font/comicneue_regular.otf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_kqagu1BAU71qzv5pwo1_400.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_kqagu1BAU71qzv5pwo1_400.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_l4jhgowvwP1qzadygo1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_l4jhgowvwP1qzadygo1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lhd86qgPZb1qgnva2o1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lhd86qgPZb1qgnva2o1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lhyv4kDLF21qcn249o1_250.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lhyv4kDLF21qcn249o1_250.gif
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lkshxbyTsS1qd47y6o1_250.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lkshxbyTsS1qd47y6o1_250.gif
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lotdb6y79Y1qdvbl3o1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lotdb6y79Y1qdvbl3o1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lqvliwvOcZ1qcnzavo1_500.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lqvliwvOcZ1qcnzavo1_500.gif
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lrgcywLdPE1qmwvx6o1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lrgcywLdPE1qmwvx6o1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lsqulhdKbm1qbe5pxo1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lsqulhdKbm1qbe5pxo1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lu8wzvo16d1qlyuwso1_400.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lu8wzvo16d1qlyuwso1_400.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_luop03t84I1qg1f07o1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_luop03t84I1qg1f07o1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lx1a4ttlZj1qcxyrro1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lx1a4ttlZj1qcxyrro1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_lxi2pjxdzd1qfneslo1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_lxi2pjxdzd1qfneslo1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_ly804whs9T1qcmibao1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_ly804whs9T1qcmibao1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_m00p9qEHHd1rq1wumo1_500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_m00p9qEHHd1rq1wumo1_500.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_m0g9rhxZ7e1r0mbi6o1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_m0g9rhxZ7e1r0mbi6o1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_m12hhorbIS1qbe5pxo1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_m12hhorbIS1qbe5pxo1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_m247xuJYKk1qbe5pxo1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_m247xuJYKk1qbe5pxo1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_m4zpvvyIwY1qzex9io1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_m4zpvvyIwY1qzex9io1_1280.jpg
--------------------------------------------------------------------------------
/app/src/main/assets/images/tumblr_mbu9a1l1QP1qhwmnpo1_1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benoberkfell/cat-rates/HEAD/app/src/main/assets/images/tumblr_mbu9a1l1QP1qhwmnpo1_1280.jpg
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/MainActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import dagger.Module
4 |
5 | @Module
6 | abstract class MainActivityModule {
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jul 08 12:08:20 PDT 2017
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.1-all.zip
7 |
--------------------------------------------------------------------------------
/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/java/com/example/catrates/annotations/StoreCacheDir.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.annotations;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | import javax.inject.Qualifier;
7 |
8 | @Qualifier
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface StoreCacheDir {
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/persistence/AppDatabase.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.persistence;
2 |
3 | import android.arch.persistence.room.Database;
4 | import android.arch.persistence.room.RoomDatabase;
5 |
6 | import com.example.catrates.models.Cat;
7 |
8 | @Database(entities = {Cat.class}, version = 1)
9 | public abstract class AppDatabase extends RoomDatabase {
10 |
11 | public abstract CatDao catDao();
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/catapi/CatData.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.catapi;
2 |
3 | import com.example.catrates.models.Cat;
4 |
5 | import org.simpleframework.xml.ElementList;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | public class CatData {
11 |
12 | @ElementList(name = "images")
13 | private ArrayList cats;
14 |
15 | List getCats() {
16 | return cats;
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/catapi/CatApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.catapi
2 |
3 | import io.reactivex.Single
4 | import okhttp3.ResponseBody
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | interface CatApi {
9 |
10 | @GET("/api/images/get")
11 | fun fetchCatPictures(@Query("format") format: String = "xml",
12 | @Query("results_per_page") count: Number = 20) : Single
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/cat_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_favorites.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/catrates/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cat Rates
3 |
4 |
5 | Hello blank fragment
6 | An error occurred loading cats! 😿
7 | Saved this cat. 😻
8 | Saved at %s ❤️
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/data/CatParser.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.data
2 |
3 | import com.example.catrates.catapi.CatResponse
4 | import com.nytimes.android.external.store3.base.Parser
5 | import okio.BufferedSource
6 | import org.simpleframework.xml.core.Persister
7 |
8 | class CatParser : Parser {
9 | override fun apply(raw: BufferedSource): CatResponse {
10 | return Persister().read(CatResponse::class.java, raw.inputStream())
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/catapi/CatResponse.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.catapi;
2 |
3 | import com.example.catrates.models.Cat;
4 |
5 | import org.simpleframework.xml.Element;
6 | import org.simpleframework.xml.Root;
7 |
8 | import java.util.List;
9 |
10 | @Root(name = "data", strict = false)
11 | public class CatResponse {
12 |
13 | @Element(name = "data")
14 | private CatData catData;
15 |
16 | public List getCats() {
17 | return catData.getCats();
18 | }
19 |
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/persistence/FavoriteCatsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.persistence
2 |
3 | import com.example.catrates.models.Cat
4 | import io.reactivex.Completable
5 | import io.reactivex.Observable
6 |
7 | class FavoriteCatsRepository constructor(val catDao: CatDao) {
8 |
9 | fun get() : Observable> {
10 | return catDao.all.toObservable()
11 | }
12 |
13 | fun put(cat: Cat) : Completable {
14 | return Completable.fromAction {
15 | cat.savedTime = System.currentTimeMillis()
16 | catDao.insertAll(cat)
17 | }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/main_tabs.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di
2 |
3 | import com.example.catrates.CatRatesApplication
4 | import com.example.catrates.di.modules.*
5 | import dagger.Component
6 | import dagger.android.support.AndroidSupportInjectionModule
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | @Component(modules = arrayOf(AndroidSupportInjectionModule::class,
11 | BuildersModule::class,
12 | AppModule::class,
13 | RestModule::class,
14 | CatStoreModule::class,
15 | FavoriteCatsPersistenceModule::class))
16 | interface AppComponent {
17 |
18 | fun inject(application: CatRatesApplication)
19 |
20 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/FavoriteCatsPersistenceModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import com.example.catrates.persistence.AppDatabase
4 | import com.example.catrates.persistence.CatDao
5 | import com.example.catrates.persistence.FavoriteCatsRepository
6 | import dagger.Module
7 | import dagger.Provides
8 | import javax.inject.Singleton
9 |
10 | @Module
11 | class FavoriteCatsPersistenceModule {
12 |
13 | @Provides
14 | @Singleton
15 | fun provideCatDao(appDb: AppDatabase) : CatDao {
16 | return appDb.catDao()
17 | }
18 |
19 | @Provides
20 | @Singleton
21 | fun provideFavoriteCatsRepository(dao: CatDao) : FavoriteCatsRepository {
22 | return FavoriteCatsRepository(dao)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/RestModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import com.example.catrates.BuildConfig
4 | import com.example.catrates.catapi.CatApi
5 | import dagger.Module
6 | import dagger.Provides
7 | import retrofit2.Retrofit
8 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
9 |
10 | @Module
11 | class RestModule {
12 |
13 | @Provides
14 | fun provideCatApi() : CatApi {
15 |
16 | return Retrofit.Builder()
17 | .baseUrl("http://thecatapi.com")
18 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
19 | .validateEagerly(BuildConfig.DEBUG)
20 | .build()
21 | .create(CatApi::class.java)
22 |
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/persistence/CatDao.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.persistence;
2 |
3 | import android.arch.persistence.room.Dao;
4 | import android.arch.persistence.room.Delete;
5 | import android.arch.persistence.room.Insert;
6 | import android.arch.persistence.room.OnConflictStrategy;
7 | import android.arch.persistence.room.Query;
8 |
9 | import com.example.catrates.models.Cat;
10 |
11 | import java.util.List;
12 |
13 | import io.reactivex.Single;
14 |
15 | @Dao
16 | public interface CatDao {
17 |
18 | @Query("SELECT * FROM cat")
19 | Single> getAll();
20 |
21 | @Insert(onConflict = OnConflictStrategy.REPLACE)
22 | void insertAll(Cat... cats);
23 |
24 | @Delete
25 | void delete(Cat cat);
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/catrates/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getTargetContext()
20 | assertEquals("com.example.catrates", appContext.packageName)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_cats.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/BuildersModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import com.example.catrates.catlist.CatsFragment
4 | import com.example.catrates.favorites.FavoriteCatsFragment
5 | import com.example.catrates.MainActivity
6 | import dagger.Module
7 | import dagger.android.ContributesAndroidInjector
8 |
9 | @Module
10 | abstract class BuildersModule {
11 |
12 | @ContributesAndroidInjector(modules = arrayOf(MainActivityModule::class))
13 | abstract fun contributeMainActivityInjector() : MainActivity
14 |
15 | @ContributesAndroidInjector(modules = arrayOf(MainActivityModule::class))
16 | abstract fun contributeCatFragmentInjector() : CatsFragment
17 |
18 | @ContributesAndroidInjector(modules = arrayOf(MainActivityModule::class))
19 | abstract fun contributeFavoritesFragmentInjector() : FavoriteCatsFragment
20 |
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/ben/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/models/Cat.java:
--------------------------------------------------------------------------------
1 | package com.example.catrates.models;
2 |
3 | import android.arch.persistence.room.Entity;
4 | import android.arch.persistence.room.PrimaryKey;
5 | import android.support.annotation.NonNull;
6 |
7 | import org.simpleframework.xml.Element;
8 | import org.simpleframework.xml.Root;
9 |
10 | @Entity
11 | @Root(name = "image")
12 | public class Cat {
13 |
14 | @PrimaryKey
15 | @NonNull
16 | @Element(name = "id")
17 | private String id;
18 |
19 | @Element(name = "url")
20 | private String url;
21 |
22 |
23 | @Element(name = "source_url")
24 | private String sourceUrl;
25 |
26 | private long savedTime;
27 |
28 | public String getId() {
29 | return id;
30 | }
31 |
32 | public void setId(String id) {
33 | this.id = id;
34 | }
35 |
36 | public String getUrl() {
37 | return url;
38 | }
39 |
40 | public void setUrl(String url) {
41 | this.url = url;
42 | }
43 |
44 | public String getSourceUrl() {
45 | return sourceUrl;
46 | }
47 |
48 | public void setSourceUrl(String sourceUrl) {
49 | this.sourceUrl = sourceUrl;
50 | }
51 |
52 | public long getSavedTime() {
53 | return savedTime;
54 | }
55 |
56 | public void setSavedTime(long savedTime) {
57 | this.savedTime = savedTime;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/favorite_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
19 |
20 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/schemas/com.example.catrates.AppDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "4086c17f719ae223bf28aaca674fcef6",
6 | "entities": [
7 | {
8 | "tableName": "Cat",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT, `url` TEXT, `sourceUrl` TEXT, `savedTime` INTEGER, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "TEXT"
15 | },
16 | {
17 | "fieldPath": "url",
18 | "columnName": "url",
19 | "affinity": "TEXT"
20 | },
21 | {
22 | "fieldPath": "sourceUrl",
23 | "columnName": "sourceUrl",
24 | "affinity": "TEXT"
25 | },
26 | {
27 | "fieldPath": "savedTime",
28 | "columnName": "savedTime",
29 | "affinity": "INTEGER"
30 | }
31 | ],
32 | "primaryKey": {
33 | "columnNames": [
34 | "id"
35 | ],
36 | "autoGenerate": false
37 | },
38 | "indices": [],
39 | "foreignKeys": []
40 | }
41 | ],
42 | "setupQueries": [
43 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
44 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"4086c17f719ae223bf28aaca674fcef6\")"
45 | ]
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import android.arch.persistence.room.Room
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import com.example.catrates.persistence.AppDatabase
7 | import com.example.catrates.persistence.CatDao
8 | import com.example.catrates.CatRatesApplication
9 | import com.example.catrates.annotations.StoreCacheDir
10 | import com.jakewharton.picasso.OkHttp3Downloader
11 | import com.squareup.picasso.Picasso
12 | import dagger.Module
13 | import dagger.Provides
14 | import java.io.File
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @Singleton
19 | class AppModule constructor(val application: CatRatesApplication) {
20 |
21 | @Provides
22 | @StoreCacheDir
23 | @Singleton
24 | fun provideCacheDir() : File {
25 | return application.cacheDir
26 | }
27 |
28 | @Provides
29 | @Singleton
30 | fun provideAppDatabase() : AppDatabase {
31 | return Room.databaseBuilder(application, AppDatabase::class.java, "database").build()
32 | }
33 |
34 |
35 |
36 | @Provides
37 | @Singleton
38 | fun provideSharedPrefs() : SharedPreferences {
39 | return application.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
40 | }
41 |
42 | @Provides
43 | @Singleton
44 | fun providePicasso() : Picasso {
45 | return Picasso.Builder(application)
46 | .downloader(OkHttp3Downloader(application.cacheDir, 100000000))
47 | .build()
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/schemas/com.example.catrates.persistence.AppDatabase/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 1,
5 | "identityHash": "02989b262e7658a1a61c75f067a70ae9",
6 | "entities": [
7 | {
8 | "tableName": "Cat",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT, `sourceUrl` TEXT, `savedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "TEXT",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "url",
19 | "columnName": "url",
20 | "affinity": "TEXT",
21 | "notNull": false
22 | },
23 | {
24 | "fieldPath": "sourceUrl",
25 | "columnName": "sourceUrl",
26 | "affinity": "TEXT",
27 | "notNull": false
28 | },
29 | {
30 | "fieldPath": "savedTime",
31 | "columnName": "savedTime",
32 | "affinity": "INTEGER",
33 | "notNull": true
34 | }
35 | ],
36 | "primaryKey": {
37 | "columnNames": [
38 | "id"
39 | ],
40 | "autoGenerate": false
41 | },
42 | "indices": [],
43 | "foreignKeys": []
44 | }
45 | ],
46 | "setupQueries": [
47 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
48 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"02989b262e7658a1a61c75f067a70ae9\")"
49 | ]
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
33 |
34 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/favorites/FavoriteCatsPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.favorites
2 |
3 | import com.example.catrates.models.Cat
4 | import com.example.catrates.persistence.FavoriteCatsRepository
5 | import io.reactivex.android.schedulers.AndroidSchedulers
6 | import io.reactivex.schedulers.Schedulers
7 | import org.joda.time.DateTime
8 | import org.joda.time.format.DateTimeFormat
9 | import org.joda.time.format.DateTimeFormatter
10 | import javax.inject.Inject
11 |
12 | class FavoriteCatsPresenter @Inject constructor(val repo: FavoriteCatsRepository) {
13 |
14 |
15 | lateinit private var view: FavoriteCatsView
16 |
17 | private val cats: MutableList = mutableListOf()
18 |
19 | private val dateFormat: DateTimeFormatter = DateTimeFormat.forPattern("MM/dd/yy HH:mm:ss")
20 |
21 | fun onAttach(view: FavoriteCatsView) {
22 | this.view = view
23 | }
24 |
25 | fun load() {
26 | repo.get().subscribeOn(Schedulers.io())
27 | .observeOn(AndroidSchedulers.mainThread())
28 | .subscribe {
29 | cats.clear()
30 | cats.addAll(it)
31 | view.didLoad()
32 | }
33 | }
34 |
35 | fun getItemCount() : Int {
36 | return cats.size
37 | }
38 |
39 | fun requestBinding(item: CatAdapterItem, position: Int) {
40 | val cat = cats[position]
41 |
42 | item.setImageUrl(cat.url)
43 | item.setSavedTime(DateTime(cat.savedTime).toString(dateFormat))
44 | }
45 |
46 | interface FavoriteCatsView {
47 | fun didLoad()
48 | }
49 |
50 | interface CatAdapterItem {
51 | fun setImageUrl(url: String)
52 | fun setSavedTime(string: String)
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/di/modules/CatStoreModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.di.modules
2 |
3 | import com.example.catrates.catapi.CatApi
4 | import com.example.catrates.annotations.StoreCacheDir
5 | import com.example.catrates.catapi.CatResponse
6 | import com.example.catrates.data.CatParser
7 | import com.nytimes.android.external.fs3.FileSystemPersisterFactory
8 | import com.nytimes.android.external.fs3.PathResolver
9 | import com.nytimes.android.external.fs3.SourcePersisterFactory
10 | import com.nytimes.android.external.fs3.filesystem.FileSystem
11 | import com.nytimes.android.external.fs3.filesystem.FileSystemFactory
12 | import com.nytimes.android.external.store3.base.impl.BarCode
13 | import com.nytimes.android.external.store3.base.impl.Store
14 | import com.nytimes.android.external.store3.base.impl.StoreBuilder
15 | import dagger.Module
16 | import dagger.Provides
17 | import okio.BufferedSource
18 | import java.io.File
19 | import javax.inject.Singleton
20 |
21 | @Module
22 | class CatStoreModule {
23 |
24 | @Provides
25 | @Singleton
26 | fun providePathResolver() : PathResolver {
27 | return PathResolver { key -> key.toString() }
28 | }
29 |
30 | @Provides
31 | @Singleton
32 | fun provideCatStore(api: CatApi,
33 | @StoreCacheDir cacheDir: File,
34 | pathResolver: PathResolver) : Store {
35 | return StoreBuilder.parsedWithKey()
36 | .fetcher {
37 | api.fetchCatPictures().map {
38 | it.source()
39 | }
40 | }
41 | .persister(FileSystemPersisterFactory.create(cacheDir, pathResolver))
42 | .parser(CatParser())
43 | .open()
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CatRates
2 |
3 | ## About
4 | This is a demo app for my conference talk, "Android Architecture For The Subway"
5 |
6 | It takes a feed of cat pictures from [thecatapi.com](thecatapi.com) and renders them. If one of these cats ranks highly
7 | in your book, you can also save it to keep your favorite cats close at hand. Just long-press on her/him.
8 |
9 | Want a new set of cats? Pull to refresh.
10 |
11 | ## Core Features
12 |
13 | This seeks to demo a few important concepts:
14 |
15 | **Caching.** Illustrating a use of a backing cache for persisting the cat picture stream. The stream is pulled down and
16 | rendered, and then the data is just thrown away on refresh, so there's not a lot of benefit to persist individual
17 | pictures just for the sake of caching them.
18 |
19 | So, we use the [Store](https://github.com/NYTimes/Store) library to handle fetching and persisting the cache for us.
20 |
21 | The Store fetches the response from the server, caches it locally, and pushes it through the parser to return data
22 | model objects we can use to render. If we want the feed again, Store gives it to us from the cache.
23 |
24 | This app is also using Picasso to hang onto images after rendering them.
25 |
26 | **Database Persistence.** The app uses Room to persist selected images off the stream you'd like to keep. While
27 | the Store is great for the raw feed, when we want to save a cat to keep, we're best off actually creating a local
28 | database record for it.
29 |
30 | **Preloading Data.** Suppose you downloaded this app, and then while bored on the subway, you remembered you
31 | had never opened this great cat app that had a million five-star reviews. Wouldn't you be disappointed to open it
32 | and find no cats, because you didn't have network access?
33 |
34 | Check out the `Application.onCreate()` where on your first launch we warm up the cache with your first set of cats,
35 | by copying a response into where the Store keeps its data. The initial response refers to some images in the
36 | assets directory, so you'll see all the cats in their full glory.
37 |
38 | You can even still adopt a cat by long-pressing.
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/CatRatesApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.SharedPreferences
6 | import com.example.catrates.di.DaggerAppComponent
7 | import com.example.catrates.di.modules.AppModule
8 | import com.nytimes.android.external.fs3.PathResolver
9 | import com.nytimes.android.external.store3.base.impl.BarCode
10 | import dagger.android.AndroidInjector
11 | import dagger.android.DispatchingAndroidInjector
12 | import dagger.android.HasActivityInjector
13 | import okio.Okio
14 | import timber.log.Timber
15 | import java.io.File
16 | import javax.inject.Inject
17 |
18 | class CatRatesApplication : Application(), HasActivityInjector {
19 |
20 | @Inject lateinit var dispatchingActivityInjector : DispatchingAndroidInjector
21 |
22 | @Inject lateinit var sharedPrefs: SharedPreferences
23 |
24 | @Inject lateinit var pathResolver: PathResolver
25 |
26 | val APPLIED_RES_PREF = "APPLIED_RES"
27 |
28 | override fun onCreate() {
29 | super.onCreate()
30 |
31 | DaggerAppComponent.builder()
32 | .appModule(AppModule(this))
33 | .build()
34 | .inject(this)
35 |
36 |
37 | Timber.plant(Timber.DebugTree())
38 |
39 | maybeApplyCannedResources()
40 | }
41 |
42 | override fun activityInjector(): AndroidInjector {
43 | return dispatchingActivityInjector;
44 | }
45 |
46 | /*
47 | * Insert data from our assets into the Store, so we have something on first use.
48 | */
49 | fun maybeApplyCannedResources() {
50 | val hasAppliedBefore = sharedPrefs.getBoolean(APPLIED_RES_PREF, false)
51 | if (!hasAppliedBefore) {
52 |
53 | val barcode = BarCode("Cats", "AllOfThem")
54 |
55 | val inputStream = assets.open("preloaded_cats.xml")
56 |
57 | val outputFile = File(cacheDir, pathResolver.resolve(barcode))
58 |
59 | val source = Okio.source(inputStream)
60 |
61 | val sink = Okio.buffer(Okio.sink(outputFile))
62 | sink.writeAll(source)
63 | sink.flush()
64 | sink.close()
65 |
66 | sharedPrefs.edit().putBoolean(APPLIED_RES_PREF, true).apply()
67 | }
68 | }
69 |
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/catlist/CatListPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.catlist
2 |
3 | import com.example.catrates.catapi.CatResponse
4 | import com.example.catrates.models.Cat
5 | import com.example.catrates.persistence.FavoriteCatsRepository
6 | import com.nytimes.android.external.store3.base.impl.BarCode
7 | import com.nytimes.android.external.store3.base.impl.Store
8 | import io.reactivex.Single
9 | import io.reactivex.android.schedulers.AndroidSchedulers
10 | import io.reactivex.schedulers.Schedulers
11 | import timber.log.Timber
12 | import javax.inject.Inject
13 |
14 | class CatListPresenter @Inject constructor(val catStore: Store,
15 | val favoritesRepo: FavoriteCatsRepository) {
16 |
17 | private val barCode = BarCode("Cats", "AllOfThem")
18 | private var cats : MutableList = mutableListOf()
19 |
20 | lateinit private var view: CatListView
21 |
22 | fun onAttach(view: CatListView) {
23 | this.view = view
24 | }
25 |
26 | fun refresh() {
27 | subscribeToCats(catStore.fetch(barCode))
28 | }
29 |
30 | fun load() {
31 | subscribeToCats(catStore.get(barCode))
32 | }
33 |
34 | private fun subscribeToCats(observable: Single) {
35 | observable.observeOn(AndroidSchedulers.mainThread())
36 | .subscribeOn(Schedulers.io())
37 | .subscribe({
38 | cats.clear()
39 | cats.addAll(it.cats)
40 | view.didLoad(cats.size)
41 | }, {
42 | Timber.e(it, "Oh no! Couldn't get any cats")
43 | view.failedToLoad()
44 | })
45 | }
46 |
47 | fun getItemCount() : Int {
48 | return cats.size
49 | }
50 |
51 | fun requestBinding(item: CatAdapterItem, position: Int) {
52 | item.setCat(cats[position])
53 | }
54 |
55 | fun saveCat(cat: Cat) {
56 | favoritesRepo.put(cat)
57 | .subscribeOn(Schedulers.io())
58 | .observeOn(AndroidSchedulers.mainThread())
59 | .subscribe {
60 | view.savedACat()
61 | }
62 | }
63 |
64 | interface CatListView {
65 | fun didLoad(itemCount: Int)
66 | fun failedToLoad()
67 | fun savedACat()
68 | }
69 |
70 | interface CatAdapterItem {
71 | fun setCat(cat: Cat)
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/favorites/FavoriteCatsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.favorites
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.support.v4.app.Fragment
6 | import android.support.v7.widget.LinearLayoutManager
7 | import android.support.v7.widget.RecyclerView
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.ImageView
12 | import android.widget.TextView
13 | import butterknife.BindView
14 | import butterknife.ButterKnife
15 | import com.example.catrates.R
16 | import com.squareup.picasso.Picasso
17 | import dagger.android.support.AndroidSupportInjection
18 | import javax.inject.Inject
19 |
20 |
21 | class FavoriteCatsFragment : Fragment(), FavoriteCatsPresenter.FavoriteCatsView {
22 |
23 | companion object {
24 | fun newInstance(): FavoriteCatsFragment {
25 | return FavoriteCatsFragment()
26 | }
27 | }
28 |
29 | @Inject
30 | lateinit var presenter: FavoriteCatsPresenter
31 |
32 | @Inject
33 | lateinit var picasso: Picasso
34 |
35 | @BindView(R.id.favorites_recycler_view)
36 | lateinit var recyclerView: RecyclerView
37 |
38 | private val adapter: FavoritesAdapter = FavoritesAdapter()
39 |
40 |
41 | //region Fragment Methods
42 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
43 | savedInstanceState: Bundle?): View? {
44 | val view = inflater.inflate(R.layout.fragment_favorites, container, false)
45 |
46 | ButterKnife.bind(this, view)
47 |
48 | recyclerView.layoutManager = LinearLayoutManager(context)
49 | recyclerView.adapter = adapter
50 |
51 | presenter.onAttach(this)
52 |
53 | return view
54 | }
55 |
56 | override fun onAttach(context: Context?) {
57 | AndroidSupportInjection.inject(this)
58 | super.onAttach(context)
59 | }
60 |
61 | override fun onResume() {
62 | super.onResume()
63 | presenter.load()
64 | }
65 | //endregion
66 |
67 |
68 | //region Presenter interface methods
69 | fun update() {
70 | presenter.load()
71 | }
72 |
73 | override fun didLoad() {
74 | adapter.notifyDataSetChanged()
75 | }
76 | //endregon
77 |
78 | //region RecyclerView
79 | inner class FavoritesAdapter : RecyclerView.Adapter() {
80 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
81 | presenter.requestBinding(holder, position)
82 | }
83 |
84 | override fun getItemCount(): Int {
85 | return presenter.getItemCount()
86 | }
87 |
88 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
89 | val view = LayoutInflater.from(parent.context)
90 | .inflate(R.layout.favorite_list_item, parent, false)
91 |
92 | return ViewHolder(view)
93 | }
94 |
95 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), FavoriteCatsPresenter.CatAdapterItem {
96 | val imageView : ImageView = itemView.findViewById(R.id.image_view)
97 | val textView: TextView = itemView.findViewById(R.id.saved_time_text_view)
98 |
99 | override fun setImageUrl(url: String) {
100 | picasso
101 | .load(url)
102 | .fit()
103 | .centerCrop()
104 | .into(imageView)
105 | }
106 |
107 | override fun setSavedTime(string: String) {
108 | textView.text = textView.context.getString(R.string.saved_at, string)
109 | }
110 |
111 | }
112 | }
113 | //endregion
114 | }
--------------------------------------------------------------------------------
/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 | buildToolsVersion "27.0.0"
12 | defaultConfig {
13 | applicationId "com.example.catrates"
14 | minSdkVersion 21
15 | targetSdkVersion 27
16 | versionCode 1
17 | versionName "1.0"
18 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
19 |
20 | javaCompileOptions {
21 | annotationProcessorOptions {
22 | arguments = ["room.schemaLocation":
23 | "$projectDir/schemas".toString()]
24 | }
25 | }
26 | }
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
31 | }
32 | }
33 |
34 | packagingOptions {
35 | exclude 'META-INF/rxjava.properties'
36 | }
37 | }
38 |
39 | final SUPPORT_VERSION = "27.0.0"
40 | final NYT_STORE_VERSION = "3.0.0-beta"
41 |
42 | dependencies {
43 | implementation fileTree(dir: 'libs', include: ['*.jar'])
44 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
45 | exclude group: 'com.android.support', module: 'support-annotations'
46 | })
47 | testImplementation 'junit:junit:4.12'
48 |
49 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
50 |
51 | // All the supporty things
52 |
53 | implementation "com.android.support:design:$SUPPORT_VERSION"
54 | implementation "com.android.support:appcompat-v7:$SUPPORT_VERSION"
55 | implementation "com.android.support:support-v4:$SUPPORT_VERSION"
56 | implementation "com.android.support:cardview-v7:$SUPPORT_VERSION"
57 |
58 | implementation 'com.android.support.constraint:constraint-layout:1.0.2'
59 |
60 | // NYT Store
61 |
62 | implementation "com.nytimes.android:cache3:$NYT_STORE_VERSION"
63 | implementation "com.nytimes.android:store3:$NYT_STORE_VERSION"
64 | implementation "com.nytimes.android:middleware3:$NYT_STORE_VERSION"
65 | implementation "com.nytimes.android:filesystem3:$NYT_STORE_VERSION"
66 |
67 | // Room persistence
68 |
69 | implementation "android.arch.persistence.room:runtime:1.0.0"
70 | kapt "android.arch.persistence.room:compiler:1.0.0"
71 | implementation "android.arch.persistence.room:rxjava2:1.0.0"
72 |
73 | // Dagger All The things
74 |
75 | implementation "com.google.dagger:dagger:2.12"
76 | kapt "com.google.dagger:dagger-compiler:2.12"
77 | compileOnly 'javax.annotation:jsr250-api:1.0'
78 | implementation 'com.google.dagger:dagger-android:2.12'
79 | implementation 'com.google.dagger:dagger-android-support:2.12'
80 | kapt "com.google.dagger:dagger-android-processor:2.12"
81 |
82 | // RxAndroid
83 | implementation 'io.reactivex.rxjava2:rxjava:2.1.6'
84 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
85 |
86 | // Retrofit
87 | implementation 'com.squareup.retrofit2:retrofit:2.3.0'
88 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
89 |
90 | // Simple XML parser
91 | implementation('org.simpleframework:simple-xml:2.7.1') {
92 | exclude group: 'xpp3', module: 'xpp3'
93 | exclude group: 'stax', module: 'stax-api'
94 | exclude group: 'stax', module: 'stax'
95 | }
96 |
97 |
98 | implementation 'com.jakewharton.timber:timber:4.5.1'
99 |
100 | implementation 'com.jakewharton:butterknife:8.8.1'
101 | kapt 'com.jakewharton:butterknife-compiler:8.8.1'
102 |
103 | implementation 'com.squareup.picasso:picasso:2.5.2'
104 | implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
105 |
106 | implementation 'net.danlew:android.joda:2.9.9'
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates
2 |
3 | import android.os.Bundle
4 | import android.support.design.widget.BottomNavigationView
5 | import android.support.v4.app.Fragment
6 | import android.support.v4.app.FragmentManager
7 | import android.support.v4.app.FragmentPagerAdapter
8 | import android.support.v4.view.ViewPager
9 | import android.support.v7.app.AppCompatActivity
10 | import android.view.MenuItem
11 | import butterknife.BindView
12 | import butterknife.ButterKnife
13 | import com.example.catrates.catapi.CatResponse
14 | import com.example.catrates.catlist.CatsFragment
15 | import com.example.catrates.favorites.FavoriteCatsFragment
16 | import com.nytimes.android.external.store3.base.impl.BarCode
17 | import com.nytimes.android.external.store3.base.impl.Store
18 | import dagger.android.AndroidInjection
19 | import dagger.android.AndroidInjector
20 | import dagger.android.DispatchingAndroidInjector
21 | import dagger.android.support.HasSupportFragmentInjector
22 | import javax.inject.Inject
23 |
24 | class MainActivity : AppCompatActivity(),
25 | HasSupportFragmentInjector,
26 | CatsFragment.CatFragmentInteractionListener {
27 |
28 | @Inject
29 | lateinit var fragmentInjector: DispatchingAndroidInjector
30 |
31 | @Inject
32 | lateinit var catStore: Store
33 |
34 | @BindView(R.id.view_pager)
35 | lateinit var viewPager: ViewPager
36 |
37 | @BindView(R.id.bottom_navigation_view)
38 | lateinit var tabs: BottomNavigationView
39 |
40 | val catFragment: CatsFragment = CatsFragment.newInstance()
41 | val favesFragment: FavoriteCatsFragment = FavoriteCatsFragment.newInstance()
42 |
43 | lateinit var viewPagerAdapter: CatsPagerAdapter
44 |
45 | var selectedMenuItem: MenuItem? = null
46 |
47 | override fun onCreate(savedInstanceState: Bundle?) {
48 | AndroidInjection.inject(this)
49 | super.onCreate(savedInstanceState)
50 | setContentView(R.layout.activity_main)
51 | ButterKnife.bind(this)
52 |
53 | viewPagerAdapter = CatsPagerAdapter(supportFragmentManager)
54 | viewPager.adapter = viewPagerAdapter
55 |
56 | tabs.setOnNavigationItemSelectedListener {
57 | when (it.itemId) {
58 | R.id.action_cats -> {
59 | viewPager.currentItem = 0
60 | true
61 | }
62 | R.id.action_favorites -> {
63 | viewPager.currentItem = 1
64 | true
65 | }
66 | else -> {
67 | false
68 | }
69 | }
70 | }
71 |
72 | selectedMenuItem = tabs.menu.getItem(0)
73 |
74 | viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
75 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
76 | //no-op
77 | }
78 |
79 | override fun onPageScrollStateChanged(state: Int) {
80 | //no-op
81 | }
82 |
83 | override fun onPageSelected(position: Int) {
84 | selectedMenuItem?.isChecked = false
85 |
86 | selectedMenuItem = tabs.menu.getItem(position)
87 | selectedMenuItem?.isChecked = true
88 | }
89 | })
90 |
91 |
92 | }
93 |
94 | override fun onResume() {
95 | super.onResume()
96 | }
97 |
98 | override fun supportFragmentInjector(): AndroidInjector {
99 | return fragmentInjector
100 | }
101 |
102 | override fun savedACat() {
103 | favesFragment.update()
104 | }
105 |
106 | inner class CatsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
107 | override fun getItem(position: Int): Fragment {
108 | return when (position) {
109 | 0 -> catFragment
110 | else -> favesFragment
111 | }
112 | }
113 |
114 | override fun getCount(): Int {
115 | return 2
116 | }
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
22 |
27 |
32 |
37 |
42 |
47 |
52 |
57 |
62 |
67 |
72 |
77 |
82 |
87 |
92 |
97 |
102 |
107 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/assets/preloaded_cats.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | file:///android_asset/images/tumblr_l4jhgowvwP1qzadygo1_500.jpg
7 | aig
8 | http://thecatapi.com/?id=aig
9 |
10 |
11 | file:///android_asset/images/tumblr_lotdb6y79Y1qdvbl3o1_1280.jpg
12 | 7f6
13 | http://thecatapi.com/?id=7f6
14 |
15 |
16 | file:///android_asset/images/tumblr_kqagu1BAU71qzv5pwo1_400.jpg
17 | 1hr
18 | http://thecatapi.com/?id=1hr
19 |
20 |
21 | file:///android_asset/images/tumblr_lxi2pjxdzd1qfneslo1_500.jpg
22 | 919
23 | http://thecatapi.com/?id=919
24 |
25 |
26 | file:///android_asset/images/tumblr_m247xuJYKk1qbe5pxo1_1280.jpg
27 | 9a3
28 | http://thecatapi.com/?id=9a3
29 |
30 |
31 | file:///android_asset/images/tumblr_lhyv4kDLF21qcn249o1_250.gif
32 | 7te
33 | http://thecatapi.com/?id=7te
34 |
35 |
36 | file:///android_asset/images/tumblr_m4zpvvyIwY1qzex9io1_1280.jpg
37 | MTU2Mjk2NA
38 | http://thecatapi.com/?id=MTU2Mjk2NA
39 |
40 |
41 | file:///android_asset/images/tumblr_ly804whs9T1qcmibao1_500.jpg
42 | 4qk
43 | http://thecatapi.com/?id=4qk
44 |
45 |
46 | file:///android_asset/images/tumblr_mbu9a1l1QP1qhwmnpo1_1280.jpg
47 | MTYwMDY3Nw
48 | http://thecatapi.com/?id=MTYwMDY3Nw
49 |
50 |
51 | file:///android_asset/images/tumblr_lx1a4ttlZj1qcxyrro1_500.jpg
52 | 4pq
53 | http://thecatapi.com/?id=4pq
54 |
55 |
56 | file:///android_asset/images/tumblr_m12hhorbIS1qbe5pxo1_1280.jpg
57 | 9b0
58 | http://thecatapi.com/?id=9b0
59 |
60 |
61 | file:///android_asset/images/tumblr_lsqulhdKbm1qbe5pxo1_1280.jpg
62 | 9el
63 | http://thecatapi.com/?id=9el
64 |
65 |
66 | file:///android_asset/images/tumblr_lu8wzvo16d1qlyuwso1_400.jpg
67 | 93k
68 | http://thecatapi.com/?id=93k
69 |
70 |
71 | file:///android_asset/images/tumblr_lhd86qgPZb1qgnva2o1_500.jpg
72 | bde
73 | http://thecatapi.com/?id=bde
74 |
75 |
76 | file:///android_asset/images/tumblr_lqvliwvOcZ1qcnzavo1_500.gif
77 | 4ai
78 | http://thecatapi.com/?id=4ai
79 |
80 |
81 | file:///android_asset/images/tumblr_lrgcywLdPE1qmwvx6o1_1280.jpg
82 | 95r
83 | http://thecatapi.com/?id=95r
84 |
85 |
86 | file:///android_asset/images/tumblr_m00p9qEHHd1rq1wumo1_500.jpg
87 | 3g5
88 | http://thecatapi.com/?id=3g5
89 |
90 |
91 | file:///android_asset/images/tumblr_luop03t84I1qg1f07o1_500.jpg
92 | ce5
93 | http://thecatapi.com/?id=ce5
94 |
95 |
96 | file:///android_asset/images/tumblr_m0g9rhxZ7e1r0mbi6o1_1280.jpg
97 | cn
98 | http://thecatapi.com/?id=cn
99 |
100 |
101 | file:///android_asset/images/tumblr_lkshxbyTsS1qd47y6o1_250.gif
102 | 4dh
103 | http://thecatapi.com/?id=4dh
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/catrates/catlist/CatsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.example.catrates.catlist
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.support.design.widget.Snackbar
6 | import android.support.v4.app.Fragment
7 | import android.support.v4.widget.SwipeRefreshLayout
8 | import android.support.v7.widget.GridLayoutManager
9 | import android.support.v7.widget.RecyclerView
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.widget.ImageView
14 | import butterknife.BindView
15 | import butterknife.ButterKnife
16 | import com.example.catrates.R
17 | import com.example.catrates.models.Cat
18 | import com.jakewharton.picasso.OkHttp3Downloader
19 | import com.squareup.picasso.Picasso
20 | import dagger.android.support.AndroidSupportInjection
21 | import javax.inject.Inject
22 |
23 |
24 | class CatsFragment : Fragment(), CatListPresenter.CatListView {
25 | companion object {
26 | fun newInstance(): CatsFragment {
27 | return CatsFragment()
28 | }
29 | }
30 |
31 | @Inject lateinit var presenter: CatListPresenter
32 |
33 | @Inject lateinit var picasso: Picasso
34 |
35 | @BindView(R.id.cats_recycler_view)
36 | lateinit var recyclerView: RecyclerView
37 |
38 | @BindView(R.id.swipe_layout)
39 | lateinit var swipeLayout: SwipeRefreshLayout
40 |
41 | private var mListener: CatFragmentInteractionListener? = null
42 |
43 | private val adapter: CatsAdapter = CatsAdapter()
44 |
45 | //region Fragment api methods
46 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
47 | savedInstanceState: Bundle?): View {
48 | val view = inflater.inflate(R.layout.fragment_cats, container, false)
49 |
50 | ButterKnife.bind(this, view)
51 | recyclerView.adapter = adapter
52 | recyclerView.layoutManager = GridLayoutManager(context, 2)
53 |
54 | presenter.onAttach(this)
55 |
56 | swipeLayout.setOnRefreshListener {
57 | presenter.refresh()
58 | }
59 |
60 |
61 | return view
62 | }
63 |
64 | override fun onResume() {
65 | super.onResume()
66 | presenter.load()
67 | }
68 |
69 | override fun onAttach(context: Context?) {
70 | AndroidSupportInjection.inject(this);
71 | super.onAttach(context)
72 | if (context is CatFragmentInteractionListener) {
73 | mListener = context
74 | } else {
75 | throw RuntimeException(context!!.toString() + " must implement CatFragmentInteractionListener")
76 | }
77 | }
78 |
79 | override fun onDetach() {
80 | super.onDetach()
81 | mListener = null
82 | }
83 | //endregion
84 |
85 | //region Presenter interface methods
86 | override fun didLoad(itemCount: Int) {
87 | swipeLayout.isRefreshing = false
88 | adapter.notifyItemRangeChanged(0, itemCount)
89 | }
90 |
91 | override fun failedToLoad() {
92 | swipeLayout.isRefreshing = false
93 | Snackbar.make(recyclerView, getString(R.string.error_loading_cats), Snackbar.LENGTH_LONG).show()
94 | }
95 |
96 | override fun savedACat() {
97 | Snackbar.make(recyclerView, getString(R.string.saved_this_cat), Snackbar.LENGTH_LONG).show()
98 | mListener?.savedACat()
99 | }
100 | //endregion
101 |
102 |
103 | //region RecyclerView adapter
104 | inner class CatsAdapter : RecyclerView.Adapter() {
105 |
106 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
107 | val view = LayoutInflater.from(parent.context)
108 | .inflate(R.layout.cat_list_item, parent, false)
109 |
110 | return ViewHolder(view)
111 | }
112 |
113 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
114 | presenter.requestBinding(holder, position)
115 | }
116 |
117 | override fun getItemCount(): Int {
118 | return presenter.getItemCount()
119 | }
120 |
121 |
122 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CatListPresenter.CatAdapterItem {
123 | private fun setImageUrl(url: String) {
124 | picasso
125 | .load(url)
126 | .fit()
127 | .centerCrop()
128 | .into(imageView)
129 | }
130 |
131 | override fun setCat(cat: Cat) {
132 | setImageUrl(cat.url)
133 |
134 | imageView.setOnLongClickListener {
135 | presenter.saveCat(cat)
136 | true
137 | }
138 | }
139 |
140 | val imageView: ImageView = itemView.findViewById(R.id.image_view)
141 | }
142 | }
143 | //endregion
144 |
145 | interface CatFragmentInteractionListener {
146 | fun savedACat()
147 | }
148 |
149 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------