├── 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 | 4 | 10 | 16 | -------------------------------------------------------------------------------- /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 | 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 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 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 | --------------------------------------------------------------------------------