├── .circleci └── config.yml ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── thomaskioko │ │ └── livedatademo │ │ ├── TestApp.java │ │ ├── db │ │ ├── DbTest.java │ │ └── MovieDaoTest.java │ │ ├── util │ │ ├── EspressoTestUtil.java │ │ ├── MatcherUtil.java │ │ ├── RecyclerViewMatcher.java │ │ ├── TaskExecutorWithIdlingResourceRule.java │ │ ├── TestUtil.java │ │ ├── TmdbTestRunner.java │ │ └── ViewModelUtil.java │ │ └── view │ │ ├── MovieDetailFragmentTest.java │ │ └── MovieListFragmentTest.java │ ├── debug │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── thomaskioko │ │ └── livedatademo │ │ └── testing │ │ └── SingleFragmentActivity.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── thomaskioko │ │ │ └── livedatademo │ │ │ ├── TmdbApp.java │ │ │ ├── db │ │ │ ├── TmdbDb.java │ │ │ ├── TmdbTypeConverters.java │ │ │ ├── dao │ │ │ │ ├── GenreDao.java │ │ │ │ ├── MovieDao.java │ │ │ │ └── VideoDao.java │ │ │ └── entity │ │ │ │ ├── Genre.java │ │ │ │ ├── Movie.java │ │ │ │ └── TmdbVideo.java │ │ │ ├── di │ │ │ ├── AppInjector.java │ │ │ ├── Injectable.java │ │ │ ├── component │ │ │ │ └── AppComponent.java │ │ │ ├── module │ │ │ │ ├── AppModule.java │ │ │ │ ├── FragmentBuildersModule.java │ │ │ │ ├── MainActivityModule.java │ │ │ │ ├── NetworkModule.java │ │ │ │ ├── RoomModule.java │ │ │ │ └── ViewModelModule.java │ │ │ └── qualifires │ │ │ │ └── ViewModelKey.java │ │ │ ├── repository │ │ │ ├── TmdbRepository.java │ │ │ ├── api │ │ │ │ ├── AuthInterceptor.java │ │ │ │ ├── GenreResponse.java │ │ │ │ ├── MovieResult.java │ │ │ │ ├── TmdbService.java │ │ │ │ └── VideoResult.java │ │ │ ├── model │ │ │ │ └── ApiResponse.java │ │ │ └── util │ │ │ │ ├── AppExecutors.java │ │ │ │ ├── LiveDataCallAdapter.java │ │ │ │ ├── LiveDataCallAdapterFactory.java │ │ │ │ └── NetworkBoundResource.java │ │ │ ├── utils │ │ │ ├── AbsentLiveData.java │ │ │ ├── AppConstants.java │ │ │ ├── DeviceUtils.java │ │ │ ├── Objects.java │ │ │ └── TagView.java │ │ │ ├── view │ │ │ ├── adapter │ │ │ │ ├── MovieListAdapter.java │ │ │ │ ├── SearchItemAdapter.java │ │ │ │ └── VideoListAdapter.java │ │ │ ├── callback │ │ │ │ ├── MovieCallback.java │ │ │ │ └── VideoCallback.java │ │ │ └── ui │ │ │ │ ├── MainActivity.java │ │ │ │ ├── common │ │ │ │ └── NavigationController.java │ │ │ │ ├── fragment │ │ │ │ ├── DetailsTransition.java │ │ │ │ ├── MovieDetailFragment.java │ │ │ │ └── MovieListFragment.java │ │ │ │ └── utils │ │ │ │ └── ScrollingFabBehavior.java │ │ │ ├── viewmodel │ │ │ ├── MovieDetailViewModel.java │ │ │ ├── MovieListViewModel.java │ │ │ ├── ProjectViewModelFactory.java │ │ │ └── SearchViewModel.java │ │ │ └── vo │ │ │ ├── Resource.java │ │ │ └── Status.java │ └── res │ │ ├── drawable-nodpi │ │ ├── badge_1.png │ │ ├── badge_10.png │ │ ├── badge_2.png │ │ ├── badge_3.png │ │ ├── badge_4.png │ │ ├── badge_5.png │ │ ├── badge_6.png │ │ ├── badge_7.png │ │ ├── badge_8.png │ │ ├── badge_9.png │ │ ├── bg_collection.png │ │ ├── bg_watched.png │ │ ├── bg_watchlist.png │ │ ├── fanart_dark.jpg │ │ ├── ic_audience_rotten.png │ │ ├── ic_audience_unknown.png │ │ ├── ic_generic_man_dark.png │ │ ├── ic_score_audience.png │ │ ├── ic_score_fresh.png │ │ ├── ic_score_imdb.png │ │ ├── ic_score_rotten.png │ │ ├── ic_score_superfresh.png │ │ ├── poster_placeholder.png │ │ ├── tmdb.png │ │ ├── trakttv.png │ │ └── user_trakt.png │ │ ├── drawable-v21 │ │ └── selectable_item_background.xml │ │ ├── drawable │ │ ├── black_translucent_gradient.xml │ │ ├── custom_cursor.xml │ │ ├── gradient_headerbar.xml │ │ ├── gradient_headerbar_bottom.xml │ │ ├── gradient_headerbar_top.xml │ │ ├── gradient_light_bottom.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_play_arrow_.xml │ │ ├── ic_play_circle_outline_24dp.xml │ │ ├── ic_search_24dp.xml │ │ ├── movie_bage.xml │ │ └── selectable_item_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── include_movie_details_layout.xml │ │ ├── include_toolbar.xml │ │ ├── item_movie_layout.xml │ │ ├── item_search_movie_layout.xml │ │ ├── item_tagview.xml │ │ ├── item_video_trailer.xml │ │ ├── movie_detail_fragment.xml │ │ ├── movie_list_fragment.xml │ │ ├── movie_poster.xml │ │ ├── movie_profile.xml │ │ └── movie_profile_header_wrapper.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── ic_play_arrow_white.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── ic_play_arrow_white.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── ic_play_arrow_white.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ ├── ic_launcher_round.png │ │ └── ic_play_arrow_white.png │ │ ├── transition-v21 │ │ ├── details_window_enter_transition.xml │ │ └── details_window_return_transition.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── searchable.xml │ ├── test-common │ └── java │ │ └── com │ │ └── thomaskioko │ │ └── livedatademo │ │ └── util │ │ └── LiveDataTestUtil.java │ └── test │ ├── java │ └── com │ │ └── thomaskioko │ │ └── livedatademo │ │ ├── ExampleUnitTest.java │ │ ├── api │ │ ├── ApiResponseTest.java │ │ └── TmdbServiceTest.java │ │ ├── repository │ │ └── NetworkBoundResourceTest.java │ │ └── util │ │ ├── ApiUtil.java │ │ ├── CountingAppExecutors.java │ │ └── InstantAppExecutors.java │ └── resources │ └── api-response │ ├── popular-movies.json │ ├── search-movie-id.json │ ├── search-movie.json │ └── videos-by-movie-id.json ├── art ├── HomeScreen.png ├── MovieDetails.png └── archtiture.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Gradle CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | working_directory: ~/code 9 | docker: 10 | - image: circleci/android:api-27-node8-alpha 11 | environment: 12 | JVM_OPTS: -Xmx3200m 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 17 | - run: 18 | name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. 19 | command: sudo chmod +x ./gradlew 20 | - run: 21 | name: Approve license for build tools 22 | command: (echo y; echo y; echo y; echo y; echo y; echo y) | $ANDROID_HOME/tools/bin/sdkmanager --licenses 23 | 24 | - run: 25 | name: Download Dependencies 26 | command: ./gradlew androidDependencies 27 | - run: 28 | name: Download Dependencies 29 | command: ./gradlew androidDependencies 30 | - save_cache: 31 | paths: 32 | - ~/.gradle 33 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 34 | - run: 35 | name: Run Tests 36 | command: ./gradlew lint test 37 | - store_artifacts: 38 | path: app/build/reports 39 | destination: reports 40 | - store_test_results: 41 | path: app/build/test-results 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Intellij 38 | *.iml 39 | .idea/ 40 | 41 | # Keystore files 42 | *.jks 43 | 44 | # External native build folder generated in Android Studio 2.2 and later 45 | .externalNativeBuild 46 | 47 | # Google Services (e.g. APIs or Firebase) 48 | google-services.json 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android LiveData & ViewModel Demo 2 | --------------------------------------- 3 | [![CircleCI](https://circleci.com/gh/kioko/android-liveData-viewModel.svg)](https://circleci.com/gh/kioko/android-liveData-viewModel) 4 | 5 | A simple android project that demonstrates how to implement Android Architecture Components. 6 | 7 | 8 | 13 | 18 | 19 |
9 |

10 | Home Page 11 |

12 |
14 |

15 | Movie Details 16 |

17 |
20 | 21 | ### Architecture 22 | The app uses ViewModel to abstract the data from UI and TmdbRepository as single source of truth for data. TmdbRepository first fetch the data from database if exist than display data to the user and at the same time it also fetches data from the webservice and update the result in database and reflect the changes to UI from database. 23 | 24 | ![](https://github.com/kioko/android-liveData-viewModel/blob/master/art/archtiture.png) 25 | 26 | ### Requirements 27 | 28 | * JDK Version 1.7 & above 29 | * Android Studio Preview Version 3.0 30 | 31 | ### Prerequisites 32 | For the app to make requests you require a [TMDB API key](https://developers.themoviedb.org/3/getting-started ). 33 | 34 | If you don’t already have an account, you will need to [create one](https://www.themoviedb.org/account/signup) 35 | in order to request an API Key. 36 | 37 | Once you have it, open `gradle.properties` file and paste your API key in `TMDB_API_KEY` variable. 38 | 39 | ### Libraries 40 | 41 | 42 | * [Android Support Library][support-lib] 43 | * [Android Architecture Components][arch] 44 | * [Dagger 2][dagger2] for dependency injection 45 | * [Retrofit][retrofit] for REST api communication 46 | * [OkHttp][OkHttp] for adding interceptors to Retrofit 47 | * [Glide][glide] for image loading 48 | * [Timber][timber] for logging 49 | * [espresso][espresso] for UI tests 50 | * [mockito][mockito] for mocking in tests 51 | 52 | 53 | [mockwebserver]: https://github.com/square/okhttp/tree/master/mockwebserver 54 | [support-lib]: https://developer.android.com/topic/libraries/support-library/index.html 55 | [arch]: https://developer.android.com/arch 56 | [OkHttp]: http://square.github.io/okhttp/ 57 | [espresso]: https://google.github.io/android-testing-support-library/docs/espresso/ 58 | [dagger2]: https://google.github.io/dagger 59 | [retrofit]: http://square.github.io/retrofit 60 | [glide]: https://github.com/bumptech/glide 61 | [timber]: https://github.com/JakeWharton/timber 62 | [mockito]: http://site.mockito.org 63 | 64 | 65 | ### License 66 | 67 | Copyright 2017 Thomas Kioko 68 | 69 | 70 | Licensed under the Apache License, Version 2.0 (the "License"); 71 | you may not use this file except in compliance with the License. 72 | You may obtain a copy of the License at 73 | 74 | http://www.apache.org/licenses/LICENSE-2.0 75 | 76 | Unless required by applicable law or agreed to in writing, software 77 | distributed under the License is distributed on an "AS IS" BASIS, 78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 79 | See the License for the specific language governing permissions and 80 | limitations under the License. 81 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion '27.0.3' 6 | defaultConfig { 7 | applicationId "com.thomaskioko.livedatademo" 8 | minSdkVersion 15 9 | targetSdkVersion 27 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "com.thomaskioko.livedatademo.util.TmdbTestRunner" 13 | 14 | compileOptions.incremental = false 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_8 17 | targetCompatibility JavaVersion.VERSION_1_8 18 | } 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | sourceSets { 28 | androidTest.java.srcDirs += "src/test-common/java" 29 | test.java.srcDirs += "src/test-common/java" 30 | } 31 | 32 | buildTypes.each { 33 | it.buildConfigField 'String', 'TMDB_API_KEY', TMDB_API_KEY 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation "com.android.support:appcompat-v7:$supportLibVersion" 40 | implementation "com.android.support.constraint:constraint-layout:$constraintLayoutVersion" 41 | 42 | implementation "com.android.support:recyclerview-v7:$supportLibVersion" 43 | implementation "com.android.support:cardview-v7:$supportLibVersion" 44 | implementation "com.android.support:design:$supportLibVersion" 45 | implementation "com.android.support:palette-v7:$supportLibVersion" 46 | 47 | implementation "com.github.bumptech.glide:glide:$glideVersion" 48 | 49 | //for lifecycle and LiveData and ViewModel 50 | implementation "android.arch.lifecycle:runtime:$archRuntimeVersion" 51 | implementation "android.arch.lifecycle:extensions:$archExtensionVersion" 52 | annotationProcessor "android.arch.lifecycle:compiler:$archVersion" 53 | 54 | implementation "android.arch.persistence.room:runtime:$archVersion" 55 | annotationProcessor "android.arch.persistence.room:compiler:$archVersion" 56 | 57 | //Retrofit 58 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 59 | implementation "com.squareup.retrofit2:converter-jackson:$retrofitVersion" 60 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 61 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" 62 | implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" 63 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" 64 | 65 | //Dagger 66 | implementation "com.google.dagger:dagger:$daggerVersion" 67 | implementation "com.google.dagger:dagger-android:$daggerVersion" 68 | implementation "com.google.dagger:dagger-android-support:$daggerVersion" 69 | annotationProcessor "com.google.dagger:dagger-android-processor:$daggerVersion" 70 | annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion" 71 | 72 | //Other Libs 73 | implementation "com.miguelcatalan:materialsearchview:$materialSearchViewVersion" 74 | implementation "com.mikhaellopez:circularprogressbar:$circularProgressbarVersion" 75 | implementation "joda-time:joda-time:$jodaTimeVersion" 76 | implementation "com.jakewharton.timber:timber:$timberVersion" 77 | implementation "com.jakewharton:butterknife:$butterKnifeVersion" 78 | annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion" 79 | 80 | // Testing 81 | testImplementation "org.mockito:mockito-core:$mockitoVersion" 82 | androidTestCompile "org.mockito:mockito-core:$mockitoVersion" 83 | androidTestCompile "org.mockito:mockito-android:$mockitoVersion" 84 | 85 | testImplementation "junit:junit:$junitVersion" 86 | testImplementation "android.arch.core:core-testing:$archVersion" 87 | 88 | testImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion" 89 | 90 | // Test helpers for Room 91 | testImplementation "android.arch.persistence.room:testing:$archVersion" 92 | androidTestImplementation "com.android.support.test:runner:$supportTestVersion" 93 | androidTestImplementation "com.android.support.test.espresso:espresso-core:$espressoCoreVersion" 94 | 95 | androidTestImplementation("android.arch.core:core-testing:$archVersion", { 96 | }) 97 | 98 | androidTestImplementation "org.mockito:mockito-core:$mockitoVersion", { 99 | exclude group: 'net.bytebuddy' 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /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/kioko/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/androidTest/java/com/thomaskioko/livedatademo/TestApp.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo; 2 | 3 | import android.app.Application; 4 | 5 | /** 6 | * We use a separate App for tests to prevent initializing dependency injection. 7 | * 8 | * See {@link com.thomaskioko.livedatademo.util.TmdbTestRunner}. 9 | */ 10 | public class TestApp extends Application { 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/db/DbTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db; 2 | 3 | import android.arch.persistence.room.Room; 4 | import android.support.test.InstrumentationRegistry; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | 9 | /** 10 | * 11 | */ 12 | 13 | abstract public class DbTest { 14 | 15 | protected TmdbDb tmdbDb; 16 | 17 | @Before 18 | public void initDb() { 19 | tmdbDb = Room.inMemoryDatabaseBuilder( 20 | InstrumentationRegistry.getContext(), 21 | TmdbDb.class 22 | ).build(); 23 | } 24 | 25 | @After 26 | public void close(){ 27 | tmdbDb.close(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/db/MovieDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db; 2 | 3 | import com.thomaskioko.livedatademo.db.entity.Movie; 4 | import com.thomaskioko.livedatademo.util.TestUtil; 5 | 6 | import org.junit.Test; 7 | 8 | import java.util.List; 9 | 10 | import static com.thomaskioko.livedatademo.util.LiveDataTestUtil.getValue; 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.core.IsNull.notNullValue; 14 | 15 | /** 16 | * 17 | */ 18 | 19 | public class MovieDaoTest extends DbTest { 20 | 21 | @Test 22 | public void testInsertAndRead() throws InterruptedException { 23 | 24 | Movie movie = TestUtil.createMovie("Star Wars: The Last Jedi", "\\/9E2y5Q7WlCVNEhP5GiVTjhEhx1o.jpg"); 25 | tmdbDb.movieDao().insert(movie); 26 | 27 | List loadedMovie = getValue(tmdbDb.movieDao().searchMovieByTitle("Star Wars: The Last Jedi")); 28 | assertThat(loadedMovie, notNullValue()); 29 | assertThat(loadedMovie.get(0).title, is("Star Wars: The Last Jedi")); 30 | } 31 | 32 | @Test 33 | public void testCreateIfNotExists_exists() throws InterruptedException { 34 | Movie movie = TestUtil.createMovie("Star Wars: The Last Jedi", "\\/9E2y5Q7WlCVNEhP5GiVTjhEhx1o.jpg"); 35 | tmdbDb.movieDao().insert(movie); 36 | assertThat(tmdbDb.movieDao().createMovieIfNotExists(movie), is( -1L)); 37 | } 38 | 39 | @Test 40 | public void testCreateIfNotExists_doesNotExists() throws InterruptedException { 41 | Movie movie = TestUtil.createMovie("Star Wars: The Last Jedi", "\\/9E2y5Q7WlCVNEhP5GiVTjhEhx1o.jpg"); 42 | assertThat(tmdbDb.movieDao().createMovieIfNotExists(movie), is( 1L)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/EspressoTestUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.graphics.Color; 4 | import android.graphics.drawable.ColorDrawable; 5 | import android.os.Bundle; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentActivity; 9 | import android.support.v4.app.FragmentManager; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.ProgressBar; 13 | 14 | /** 15 | * Utility methods for espresso tests. 16 | */ 17 | public class EspressoTestUtil { 18 | /** 19 | * Disables progress bar animations for the views of the given activity rule 20 | * 21 | * @param activityTestRule The activity rule whose views will be checked 22 | */ 23 | public static void disableProgressBarAnimations( 24 | ActivityTestRule activityTestRule) { 25 | activityTestRule.getActivity().getSupportFragmentManager() 26 | .registerFragmentLifecycleCallbacks( 27 | new FragmentManager.FragmentLifecycleCallbacks() { 28 | @Override 29 | public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v, 30 | Bundle savedInstanceState) { 31 | // traverse all views, if any is a progress bar, replace its animation 32 | traverseViews(v); 33 | } 34 | }, true); 35 | } 36 | 37 | private static void traverseViews(View view) { 38 | if (view instanceof ViewGroup) { 39 | traverseViewGroup((ViewGroup) view); 40 | } else { 41 | if (view instanceof ProgressBar) { 42 | disableProgressBarAnimation((ProgressBar) view); 43 | } 44 | } 45 | } 46 | 47 | private static void traverseViewGroup(ViewGroup view) { 48 | final int count = view.getChildCount(); 49 | for (int i = 0; i < count; i++) { 50 | traverseViews(view.getChildAt(i)); 51 | } 52 | } 53 | 54 | /** 55 | * necessary to run tests on older API levels where progress bar uses handler loop to animate. 56 | * 57 | * @param progressBar The progress bar whose animation will be swapped with a drawable 58 | */ 59 | private static void disableProgressBarAnimation(ProgressBar progressBar) { 60 | progressBar.setIndeterminateDrawable(new ColorDrawable(Color.BLUE)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/MatcherUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.content.res.Resources; 4 | import android.support.annotation.NonNull; 5 | import android.support.test.espresso.matcher.BoundedMatcher; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | import org.hamcrest.Description; 11 | import org.hamcrest.Matcher; 12 | 13 | /** 14 | * This class contains custom matchers 15 | */ 16 | 17 | public class MatcherUtil { 18 | 19 | private static Matcher withToolbarTitle(final Matcher textMatcher) { 20 | return new BoundedMatcher(Toolbar.class) { 21 | @Override 22 | public boolean matchesSafely(Toolbar toolbar) { 23 | return textMatcher.matches(toolbar.getTitle()); 24 | } 25 | 26 | @Override 27 | public void describeTo(Description description) { 28 | description.appendText("with toolbar title: "); 29 | textMatcher.describeTo(description); 30 | } 31 | }; 32 | } 33 | 34 | 35 | /** 36 | * Original source from Espresso library, modified to handle spanned fields 37 | * 38 | * Returns a matcher that matches a descendant of {@link TextView} that is 39 | * displaying the string associated with the given resource id. 40 | * 41 | * @param text 42 | * the string resource the text view is expected to hold. 43 | */ 44 | public static Matcher withText(final String text) { 45 | 46 | return new BoundedMatcher(TextView.class) { 47 | private String resourceName = null; 48 | private String expectedText = null; 49 | 50 | @Override 51 | public void describeTo(Description description) { 52 | description.appendText("with string from resource id: "); 53 | description.appendValue(text); 54 | if (null != this.resourceName) { 55 | description.appendText("["); 56 | description.appendText(this.resourceName); 57 | description.appendText("]"); 58 | } 59 | if (null != this.expectedText) { 60 | description.appendText(" value: "); 61 | description.appendText(this.expectedText); 62 | } 63 | } 64 | 65 | @Override 66 | public boolean matchesSafely(TextView textView) { 67 | if (null == this.expectedText) { 68 | try { 69 | this.expectedText = textView.getText().toString(); 70 | } catch (Resources.NotFoundException ignored) { 71 | /* 72 | * view could be from a context unaware of the resource 73 | * id. 74 | */ 75 | } 76 | } 77 | if (null != this.expectedText) { 78 | return this.expectedText.equals(textView.getText() 79 | .toString()); 80 | } else { 81 | return false; 82 | } 83 | } 84 | }; 85 | } 86 | 87 | @NonNull 88 | public static RecyclerViewMatcher listMatcher(int recyclerId) { 89 | return new RecyclerViewMatcher(recyclerId); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/RecyclerViewMatcher.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.content.res.Resources; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | import org.hamcrest.Description; 8 | import org.hamcrest.Matcher; 9 | import org.hamcrest.TypeSafeMatcher; 10 | 11 | /** 12 | * taken from https://gist.github.com/baconpat/8405a88d04bd1942eb5e430d33e4faa2 13 | * license MIT 14 | */ 15 | public class RecyclerViewMatcher { 16 | 17 | private final int recyclerViewId; 18 | 19 | public RecyclerViewMatcher(int recyclerViewId) { 20 | this.recyclerViewId = recyclerViewId; 21 | } 22 | 23 | public Matcher atPosition(final int position) { 24 | return atPositionOnView(position, -1); 25 | } 26 | 27 | public Matcher atPositionOnView(final int position, final int targetViewId) { 28 | 29 | return new TypeSafeMatcher() { 30 | Resources resources = null; 31 | View childView; 32 | 33 | public void describeTo(Description description) { 34 | String idDescription = Integer.toString(recyclerViewId); 35 | if (this.resources != null) { 36 | try { 37 | idDescription = this.resources.getResourceName(recyclerViewId); 38 | } catch (Resources.NotFoundException var4) { 39 | idDescription = String.format("%s (resource name not found)", recyclerViewId); 40 | } 41 | } 42 | 43 | description.appendText("RecyclerView with id: " + idDescription + " at position: " + position); 44 | } 45 | 46 | public boolean matchesSafely(View view) { 47 | 48 | this.resources = view.getResources(); 49 | 50 | if (childView == null) { 51 | RecyclerView recyclerView = view.getRootView().findViewById(recyclerViewId); 52 | if (recyclerView != null && recyclerView.getId() == recyclerViewId) { 53 | RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position); 54 | if (viewHolder != null) { 55 | childView = viewHolder.itemView; 56 | } 57 | } 58 | else { 59 | return false; 60 | } 61 | } 62 | 63 | if (targetViewId == -1) { 64 | return view == childView; 65 | } else { 66 | View targetView = childView.findViewById(targetViewId); 67 | return view == targetView; 68 | } 69 | } 70 | }; 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/TaskExecutorWithIdlingResourceRule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.arch.core.executor.testing.CountingTaskExecutorRule; 4 | import android.support.test.espresso.Espresso; 5 | import android.support.test.espresso.IdlingResource; 6 | 7 | import org.junit.runner.Description; 8 | 9 | import java.util.concurrent.CopyOnWriteArrayList; 10 | 11 | /** 12 | * A Junit rule that registers Architecture Components' background threads as an Espresso idling 13 | * resource. 14 | */ 15 | public class TaskExecutorWithIdlingResourceRule extends CountingTaskExecutorRule { 16 | private CopyOnWriteArrayList callbacks = 17 | new CopyOnWriteArrayList<>(); 18 | @Override 19 | protected void starting(Description description) { 20 | Espresso.registerIdlingResources(new IdlingResource() { 21 | @Override 22 | public String getName() { 23 | return "architecture components idling resource"; 24 | } 25 | 26 | @Override 27 | public boolean isIdleNow() { 28 | return TaskExecutorWithIdlingResourceRule.this.isIdle(); 29 | } 30 | 31 | @Override 32 | public void registerIdleTransitionCallback(IdlingResource.ResourceCallback callback) { 33 | callbacks.add(callback); 34 | } 35 | }); 36 | super.starting(description); 37 | } 38 | 39 | @Override 40 | protected void onIdle() { 41 | super.onIdle(); 42 | for (IdlingResource.ResourceCallback callback : callbacks) { 43 | callback.onTransitionToIdle(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import com.thomaskioko.livedatademo.db.entity.Movie; 4 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | 10 | public class TestUtil { 11 | 12 | public static Movie createMovie(String title, String posterPath) { 13 | String overView = "Set in a post-apocalyptic world, young Thomas is deposited in a community of boys after his memory is erased, soon learning they're all trapped in a maze that will require him to join forces with fellow “runners” for a shot at escape."; 14 | String backDropPath = "/lkOZcsXcOLZYeJ2YxJd3vSldvU4.jpg.jpg"; 15 | List genreIds = new ArrayList<>(); 16 | genreIds.add(23); 17 | return new Movie(198663, posterPath, 7.8, "2014-09-10", title, 18 | false, overView, "The Maze Runner", "en", backDropPath, 19 | 732.263205, 6559, false, 7.3, genreIds); 20 | } 21 | 22 | 23 | public static List getMovieList() { 24 | List movieList = new ArrayList<>(); 25 | movieList.add(createMovie("The Maze Runner", "\\/coss7RgL0NH6g4fC2s5atvf3dFO.jpg")); 26 | movieList.add(createMovie("Jumanji", "\\/47pLZ1gr63WaciDfHCpmoiXJlVr.jpg")); 27 | 28 | return movieList; 29 | } 30 | 31 | 32 | public static TmdbVideo createTmdbVideo() { 33 | return new TmdbVideo("571bf094c3a368525f006b86", "Official Trailer", "Trailer", 34 | "XRVD32rnzOw", 1080, "YouTube", "en", "US", 198663 35 | ); 36 | } 37 | 38 | 39 | public static List getTmdbVideoList() { 40 | List tmdbVideos = new ArrayList<>(); 41 | tmdbVideos.add(createTmdbVideo()); 42 | tmdbVideos.add(createTmdbVideo()); 43 | tmdbVideos.add(createTmdbVideo()); 44 | 45 | return tmdbVideos; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/TmdbTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.test.runner.AndroidJUnitRunner; 6 | 7 | import com.thomaskioko.livedatademo.TestApp; 8 | 9 | 10 | /** 11 | * Custom runner to disable dependency injection. 12 | */ 13 | public class TmdbTestRunner extends AndroidJUnitRunner { 14 | @Override 15 | public Application newApplication(ClassLoader cl, String className, Context context) 16 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 17 | return super.newApplication(cl, TestApp.class.getName(), context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/util/ViewModelUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.arch.lifecycle.ViewModel; 4 | import android.arch.lifecycle.ViewModelProvider; 5 | 6 | /** 7 | * Creates a one off view model factory for the given view model instance. 8 | */ 9 | public class ViewModelUtil { 10 | private ViewModelUtil() {} 11 | public static ViewModelProvider.Factory createFor(T model) { 12 | return new ViewModelProvider.Factory() { 13 | @Override 14 | public T create(Class modelClass) { 15 | if (modelClass.isAssignableFrom(model.getClass())) { 16 | return (T) model; 17 | } 18 | throw new IllegalArgumentException("unexpected model class " + modelClass); 19 | } 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/view/MovieDetailFragmentTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view; 2 | 3 | import android.arch.lifecycle.MutableLiveData; 4 | import android.support.test.espresso.matcher.ViewMatchers; 5 | import android.support.test.rule.ActivityTestRule; 6 | import android.support.test.runner.AndroidJUnit4; 7 | 8 | import com.thomaskioko.livedatademo.R; 9 | import com.thomaskioko.livedatademo.db.entity.Movie; 10 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 11 | import com.thomaskioko.livedatademo.testing.SingleFragmentActivity; 12 | import com.thomaskioko.livedatademo.util.EspressoTestUtil; 13 | import com.thomaskioko.livedatademo.util.MatcherUtil; 14 | import com.thomaskioko.livedatademo.util.TestUtil; 15 | import com.thomaskioko.livedatademo.util.ViewModelUtil; 16 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieDetailFragment; 17 | import com.thomaskioko.livedatademo.viewmodel.MovieDetailViewModel; 18 | import com.thomaskioko.livedatademo.vo.Resource; 19 | 20 | import org.junit.Before; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | 25 | import java.text.NumberFormat; 26 | import java.util.List; 27 | import java.util.Locale; 28 | 29 | import static android.support.test.espresso.Espresso.onView; 30 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 31 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 32 | import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 33 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 34 | import static org.hamcrest.Matchers.not; 35 | import static org.mockito.ArgumentMatchers.anyInt; 36 | import static org.mockito.Mockito.doNothing; 37 | import static org.mockito.Mockito.mock; 38 | import static org.mockito.Mockito.when; 39 | 40 | @RunWith(AndroidJUnit4.class) 41 | public class MovieDetailFragmentTest { 42 | @Rule 43 | public ActivityTestRule activityRule = 44 | new ActivityTestRule<>(SingleFragmentActivity.class, true, true); 45 | 46 | private MovieDetailViewModel viewModel; 47 | private MutableLiveData> data = new MutableLiveData<>(); 48 | private MutableLiveData>> videoData = new MutableLiveData<>(); 49 | 50 | @Before 51 | public void init() { 52 | EspressoTestUtil.disableProgressBarAnimations(activityRule); 53 | MovieDetailFragment fragment = MovieDetailFragment.create(anyInt(), anyInt()); 54 | viewModel = mock(MovieDetailViewModel.class); 55 | when(viewModel.getMovie()).thenReturn(data); 56 | when(viewModel.getVideoMovies()).thenReturn(videoData); 57 | doNothing().when(viewModel).setMovieId(anyInt()); 58 | 59 | fragment.viewModelFactory = ViewModelUtil.createFor(viewModel); 60 | activityRule.getActivity().setFragment(fragment); 61 | } 62 | 63 | @Test 64 | public void loading() { 65 | data.postValue(Resource.loading(null)); 66 | 67 | //Verify that progressbar is set 68 | onView(withId(R.id.progress_bar)).check(matches(isDisplayed())); 69 | } 70 | 71 | @Test 72 | public void testMovieDetailsShown() { 73 | Movie movie = TestUtil.getMovieList().get(0); 74 | 75 | data.postValue(Resource.success(movie)); 76 | 77 | //Verify that the progressbar is not shown 78 | onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); 79 | 80 | //Verify that the error message is not shown 81 | onView(withId(R.id.error_msg)).check(matches(not(isDisplayed()))); 82 | 83 | //Verify that the movie title is set 84 | onView(withId(R.id.title)).check(matches(MatcherUtil.withText(movie.title))); 85 | 86 | String movieRating = NumberFormat.getInstance(Locale.getDefault()).format(movie.rating); 87 | 88 | //Verify that text rating is set 89 | onView(withId(R.id.rating_text)).check(matches(MatcherUtil.withText(movieRating))); 90 | 91 | //Verify that movie overview is set 92 | onView(withId(R.id.movie_detail_plot)).check(matches(MatcherUtil.withText(movie.overview))); 93 | 94 | //Verify that the movie release year is set 95 | onView(withId(R.id.year_title)).check(matches(MatcherUtil.withText(movie.releaseYear))); 96 | 97 | } 98 | 99 | @Test 100 | public void testDisplayMovieVideosOnSuccess(){ 101 | List tmdbVideoList = TestUtil.getTmdbVideoList(); 102 | videoData.postValue(Resource.success(tmdbVideoList)); 103 | 104 | //Verify that the progressbar is not shown 105 | onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed()))); 106 | 107 | //Verify that the error message is not shown 108 | onView(withId(R.id.error_msg)).check(matches(not(isDisplayed()))); 109 | 110 | //verify that the recycler view has at least an item displayed 111 | onView(MatcherUtil.listMatcher(R.id.recyclerview_trailer).atPosition(0)).check(matches(isDisplayed())); 112 | 113 | } 114 | 115 | @Test 116 | public void testShowVideoError() { 117 | videoData.postValue(Resource.error("Failed to load data", null)); 118 | 119 | //Verify that Error message is shown 120 | onView(withId(R.id.error_msg)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); 121 | } 122 | 123 | @Test 124 | public void testMovieDetailShowError() { 125 | data.postValue(Resource.error("Failed to load data", null)); 126 | 127 | //Verify that Error message is shown 128 | onView(withId(R.id.error_msg)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/thomaskioko/livedatademo/view/MovieListFragmentTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view; 2 | 3 | import android.arch.lifecycle.MediatorLiveData; 4 | import android.support.annotation.NonNull; 5 | import android.support.test.espresso.matcher.ViewMatchers; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | import android.widget.EditText; 9 | 10 | import com.thomaskioko.livedatademo.R; 11 | import com.thomaskioko.livedatademo.db.entity.Movie; 12 | import com.thomaskioko.livedatademo.testing.SingleFragmentActivity; 13 | import com.thomaskioko.livedatademo.util.EspressoTestUtil; 14 | import com.thomaskioko.livedatademo.util.RecyclerViewMatcher; 15 | import com.thomaskioko.livedatademo.util.TestUtil; 16 | import com.thomaskioko.livedatademo.util.ViewModelUtil; 17 | import com.thomaskioko.livedatademo.view.ui.common.NavigationController; 18 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieListFragment; 19 | import com.thomaskioko.livedatademo.viewmodel.MovieListViewModel; 20 | import com.thomaskioko.livedatademo.vo.Resource; 21 | 22 | import org.junit.Before; 23 | import org.junit.Rule; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | 27 | import java.util.List; 28 | 29 | import static android.support.test.espresso.Espresso.onView; 30 | import static android.support.test.espresso.action.ViewActions.click; 31 | import static android.support.test.espresso.action.ViewActions.pressImeActionButton; 32 | import static android.support.test.espresso.action.ViewActions.typeText; 33 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 34 | import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 35 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 36 | import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 37 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 38 | import static org.hamcrest.CoreMatchers.not; 39 | import static org.mockito.ArgumentMatchers.anyString; 40 | import static org.mockito.Mockito.doNothing; 41 | import static org.mockito.Mockito.mock; 42 | import static org.mockito.Mockito.verify; 43 | import static org.mockito.Mockito.when; 44 | 45 | /** 46 | * @author Thomas Kioko 47 | */ 48 | 49 | @RunWith(AndroidJUnit4.class) 50 | public class MovieListFragmentTest { 51 | 52 | @Rule 53 | public ActivityTestRule activityTestRule = 54 | new ActivityTestRule<>(SingleFragmentActivity.class, true, true); 55 | 56 | private MovieListViewModel viewModel; 57 | private MediatorLiveData>> result = new MediatorLiveData<>(); 58 | private NavigationController navigationController; 59 | 60 | @Before 61 | public void init() { 62 | EspressoTestUtil.disableProgressBarAnimations(activityTestRule); 63 | MovieListFragment movieListFragment = new MovieListFragment(); 64 | 65 | viewModel = mock(MovieListViewModel.class); 66 | navigationController = mock(NavigationController.class); 67 | doNothing().when(viewModel).setSearchQuery(anyString()); 68 | when(viewModel.getPopularMovies()).thenReturn(result); 69 | when(viewModel.getSearchResults()).thenReturn(result); 70 | 71 | movieListFragment.viewModelFactory = ViewModelUtil.createFor(viewModel); 72 | movieListFragment.navigationController = navigationController; 73 | 74 | activityTestRule.getActivity().setFragment(movieListFragment); 75 | } 76 | 77 | @Test 78 | public void testSearchMovie() { 79 | //click on the search icon 80 | onView(withId(R.id.action_search)).perform(click()); 81 | 82 | //Type the test in the search field and submit the query 83 | onView(isAssignableFrom(EditText.class)).perform(typeText("Spider man"), 84 | pressImeActionButton()); 85 | 86 | //verify(viewModel).setSearchQuery("Spider man"); 87 | 88 | result.postValue(Resource.loading(null)); 89 | 90 | //Check the progressBar is displayed 91 | onView(withId(R.id.progressbar)).check(matches(isDisplayed())); 92 | 93 | } 94 | 95 | 96 | @Test 97 | public void testLoadResults() { 98 | result.postValue(Resource.success(TestUtil.getMovieList())); 99 | 100 | onView(listMatcher().atPosition(0)).check(matches(isDisplayed())); 101 | onView(withId(R.id.progressbar)).check(matches(not(isDisplayed()))); 102 | } 103 | 104 | @Test 105 | public void testMovieItemClick() { 106 | //Load items 107 | result.postValue(Resource.success(TestUtil.getMovieList())); 108 | 109 | //Verify that items are displayed 110 | onView(listMatcher().atPosition(0)).check(matches(isDisplayed())); 111 | 112 | //Verify that the progress bar is shown 113 | onView(withId(R.id.progressbar)).check(matches(not(isDisplayed()))); 114 | 115 | // Click on the first item on the recyclerview 116 | onView(listMatcher().atPosition(0)).perform(click()); 117 | 118 | //Verify that we can navigate to MovieDetailFragment after clicking on an Item 119 | verify(navigationController).navigateToMovieDetailFragment(346364); 120 | } 121 | 122 | @Test 123 | public void testShowError() { 124 | result.postValue(Resource.error("Failed to load data", null)); 125 | onView(withId(R.id.tvError)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))); 126 | } 127 | 128 | @NonNull 129 | private RecyclerViewMatcher listMatcher() { 130 | return new RecyclerViewMatcher(R.id.recyclerView); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/debug/java/com/thomaskioko/livedatademo/testing/SingleFragmentActivity.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.testing; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.ViewGroup; 8 | import android.widget.FrameLayout; 9 | 10 | import com.thomaskioko.livedatademo.R; 11 | 12 | /** 13 | * Used for testing fragments inside a fake activity. 14 | * 15 | * @author Thomas Kioko 16 | */ 17 | 18 | public class SingleFragmentActivity extends AppCompatActivity { 19 | 20 | @Override 21 | protected void onCreate(@Nullable Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | FrameLayout content = new FrameLayout(this); 24 | content.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 25 | ViewGroup.LayoutParams.MATCH_PARENT)); 26 | content.setId(R.id.container); 27 | setContentView(content); 28 | } 29 | 30 | public void setFragment(Fragment fragment) { 31 | getSupportFragmentManager().beginTransaction() 32 | .add(R.id.container, fragment, "TEST") 33 | .commit(); 34 | } 35 | 36 | public void replaceFragment(Fragment fragment) { 37 | getSupportFragmentManager().beginTransaction() 38 | .replace(R.id.container, fragment).commit(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/TmdbApp.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | 6 | import com.thomaskioko.livedatademo.di.AppInjector; 7 | 8 | import javax.inject.Inject; 9 | 10 | import dagger.android.AndroidInjector; 11 | import dagger.android.DispatchingAndroidInjector; 12 | import dagger.android.HasActivityInjector; 13 | import timber.log.Timber; 14 | 15 | /** 16 | * 17 | */ 18 | 19 | public class TmdbApp extends Application implements HasActivityInjector { 20 | 21 | @Inject 22 | DispatchingAndroidInjector dispatchingAndroidInjector; 23 | 24 | 25 | @Override 26 | public void onCreate() { 27 | super.onCreate(); 28 | 29 | if (BuildConfig.DEBUG) { 30 | Timber.plant(new Timber.DebugTree()); 31 | } 32 | 33 | AppInjector.init(this); 34 | } 35 | 36 | @Override 37 | public AndroidInjector activityInjector() { 38 | return dispatchingAndroidInjector; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/TmdbDb.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db; 2 | 3 | import android.arch.persistence.room.Database; 4 | import android.arch.persistence.room.RoomDatabase; 5 | 6 | import com.thomaskioko.livedatademo.db.dao.GenreDao; 7 | import com.thomaskioko.livedatademo.db.dao.MovieDao; 8 | import com.thomaskioko.livedatademo.db.dao.VideoDao; 9 | import com.thomaskioko.livedatademo.db.entity.Genre; 10 | import com.thomaskioko.livedatademo.db.entity.Movie; 11 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 12 | 13 | /** 14 | * Main database description. 15 | */ 16 | @Database(entities = {Movie.class, Genre.class, TmdbVideo.class}, version = 1) 17 | public abstract class TmdbDb extends RoomDatabase { 18 | 19 | abstract public MovieDao movieDao(); 20 | 21 | abstract public GenreDao genreDao(); 22 | 23 | abstract public VideoDao videoDao(); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/TmdbTypeConverters.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.arch.persistence.room.TypeConverter; 5 | import android.arch.persistence.room.util.StringUtil; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | @SuppressLint("RestrictedApi") 11 | public class TmdbTypeConverters { 12 | @TypeConverter 13 | public static List stringToIntList(String data) { 14 | if (data == null) { 15 | return Collections.emptyList(); 16 | } 17 | return StringUtil.splitToIntList(data); 18 | } 19 | 20 | @TypeConverter 21 | public static String intListToString(List ints) { 22 | return StringUtil.joinIntoString(ints); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/dao/GenreDao.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.dao; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.persistence.room.Dao; 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.thomaskioko.livedatademo.db.entity.Genre; 10 | 11 | import java.util.List; 12 | 13 | 14 | /** 15 | * Interface for database access on Genre related operations. 16 | */ 17 | @Dao 18 | public abstract class GenreDao { 19 | 20 | @Query("SELECT * FROM Genre") 21 | public abstract LiveData> findAll(); 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | public abstract void insert(Genre... genres); 25 | 26 | @Insert(onConflict = OnConflictStrategy.REPLACE) 27 | public abstract void insertGenres(List genreList); 28 | 29 | @Query("DELETE FROM Genre") 30 | public abstract void deleteAll(); 31 | 32 | @Query("SELECT * FROM Genre where id = :id") 33 | public abstract LiveData searchGenresById(int id); 34 | 35 | @Insert(onConflict = OnConflictStrategy.IGNORE) 36 | public abstract long createGenreIfNotExists(Genre genre); 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/dao/MovieDao.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.dao; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.persistence.room.Dao; 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.thomaskioko.livedatademo.db.entity.Movie; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Interface for database access on Movie related operations. 15 | */ 16 | @Dao 17 | public abstract class MovieDao { 18 | 19 | @Query("SELECT * FROM Movie") 20 | public abstract LiveData> findAll(); 21 | 22 | @Insert(onConflict = OnConflictStrategy.REPLACE) 23 | public abstract void insert(Movie... movie); 24 | 25 | @Insert(onConflict = OnConflictStrategy.REPLACE) 26 | public abstract void insertMovies(List movieList); 27 | 28 | @Query("DELETE FROM Movie") 29 | public abstract void deleteAll(); 30 | 31 | @Query("SELECT * FROM Movie where title = :title") 32 | public abstract LiveData> searchMovieByTitle(String title); 33 | 34 | @Query("SELECT * FROM Movie where id = :id") 35 | public abstract LiveData searchMovieById(int id); 36 | 37 | @Insert(onConflict = OnConflictStrategy.IGNORE) 38 | public abstract long createMovieIfNotExists(Movie movie); 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/dao/VideoDao.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.dao; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.persistence.room.Dao; 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.thomaskioko.livedatademo.db.entity.TmdbVideo; 10 | 11 | import java.util.List; 12 | 13 | 14 | /** 15 | * Interface for database access on Video related operations. 16 | */ 17 | @Dao 18 | public abstract class VideoDao { 19 | 20 | @Query("SELECT * FROM TmdbVideo") 21 | public abstract LiveData> findAll(); 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | public abstract void insert(TmdbVideo... tmdbVideos); 25 | 26 | @Insert(onConflict = OnConflictStrategy.REPLACE) 27 | public abstract void insertVideo(List userList); 28 | 29 | @Query("SELECT * FROM TmdbVideo where movieId = :movieId") 30 | public abstract LiveData> searchVideodByMovieId(int movieId); 31 | 32 | @Query("DELETE FROM TmdbVideo") 33 | public abstract void deleteAll(); 34 | 35 | 36 | @Insert(onConflict = OnConflictStrategy.IGNORE) 37 | public abstract long createVideoIfNotExists(TmdbVideo tmdbVideo); 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/entity/Genre.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.entity; 2 | 3 | 4 | import android.arch.persistence.room.Entity; 5 | import android.arch.persistence.room.Index; 6 | import android.support.annotation.NonNull; 7 | 8 | @Entity(indices = {@Index("id")}, 9 | primaryKeys = {"id"}) 10 | public class Genre { 11 | 12 | @NonNull 13 | public int id; 14 | public String name; 15 | 16 | public Genre(int id, String name) { 17 | this.id = id; 18 | this.name = name; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "ClassPojo [id = " + id + ", name = " + name + "]"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/entity/Movie.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.entity; 2 | 3 | import android.arch.persistence.room.Entity; 4 | import android.arch.persistence.room.Index; 5 | import android.arch.persistence.room.TypeConverters; 6 | import android.support.annotation.NonNull; 7 | 8 | import com.google.gson.annotations.Expose; 9 | import com.google.gson.annotations.SerializedName; 10 | import com.thomaskioko.livedatademo.db.TmdbTypeConverters; 11 | 12 | import java.util.List; 13 | 14 | 15 | @Entity(indices = {@Index("id")}, 16 | primaryKeys = {"id"}) 17 | @TypeConverters(TmdbTypeConverters.class) 18 | public class Movie { 19 | 20 | @SerializedName("id") 21 | @Expose 22 | @NonNull 23 | public final int id; 24 | @SerializedName(value = "poster_path") 25 | public String posterUrl; 26 | public Double rating; 27 | @SerializedName(value = "release_date") 28 | public String releaseYear; 29 | public String title; 30 | public Boolean adult; 31 | public String overview; 32 | @SerializedName(value = "original_title") 33 | public String originalTitle; 34 | @SerializedName(value = "original_language") 35 | public String originalLanguage; 36 | @SerializedName(value = "backdrop_path") 37 | public String backdropPath; 38 | public Double popularity; 39 | @SerializedName(value = "vote_count") 40 | public Integer voteCount; 41 | public Boolean video; 42 | @SerializedName(value = "vote_average") 43 | public Double voteAverage; 44 | @SerializedName(value = "genre_ids") 45 | public List genreIds; 46 | 47 | public Movie(int id, String posterUrl, Double rating, String releaseYear, String title, Boolean adult, 48 | String overview, String originalTitle, String originalLanguage, String backdropPath, 49 | Double popularity, Integer voteCount, Boolean video, Double voteAverage, List genreIds){ 50 | this.id = id; 51 | this.posterUrl = posterUrl; 52 | this.rating = rating; 53 | this.releaseYear = releaseYear; 54 | this.title = title; 55 | this.adult = adult; 56 | this.overview = overview; 57 | this.originalLanguage = originalLanguage; 58 | this.originalTitle = originalTitle; 59 | this.backdropPath = backdropPath; 60 | this.popularity = popularity; 61 | this.voteCount = voteCount; 62 | this.video = video; 63 | this.voteAverage = voteAverage; 64 | this.genreIds = genreIds; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/db/entity/TmdbVideo.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.db.entity; 2 | 3 | import android.arch.persistence.room.Entity; 4 | import android.arch.persistence.room.Index; 5 | import android.support.annotation.NonNull; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.google.gson.annotations.Expose; 9 | import com.google.gson.annotations.SerializedName; 10 | 11 | /** 12 | * 13 | */ 14 | 15 | @Entity(indices = {@Index("id")}, 16 | primaryKeys = {"id"}) 17 | public class TmdbVideo { 18 | 19 | @SerializedName("id") 20 | @Expose 21 | @NonNull 22 | public String id; 23 | public String name; 24 | public String type; 25 | public String key; 26 | public int size; 27 | public String site; 28 | public String iso_639_1; 29 | public String iso_3166_1; 30 | @JsonIgnore 31 | public int movieId; 32 | 33 | public TmdbVideo(String id, String name, String type, String key, int size, String site, String iso_639_1, 34 | String iso_3166_1, int movieId) { 35 | this.id = id; 36 | this.name = name; 37 | this.type = type; 38 | this.key = key; 39 | this.size = size; 40 | this.site = site; 41 | this.iso_639_1 = iso_639_1; 42 | this.iso_3166_1 = iso_3166_1; 43 | this.movieId = movieId; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "ClassPojo [site = " + site + ", id = " + id + ", iso_639_1 = " + iso_639_1 + ", name = " + name + ", type = " + type + ", key = " + key + ", iso_3166_1 = " + iso_3166_1 + ", size = " + size + "]"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/AppInjector.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v4.app.FragmentActivity; 8 | import android.support.v4.app.FragmentManager; 9 | 10 | import com.thomaskioko.livedatademo.TmdbApp; 11 | import com.thomaskioko.livedatademo.di.component.DaggerAppComponent; 12 | 13 | import dagger.android.AndroidInjection; 14 | import dagger.android.support.AndroidSupportInjection; 15 | import dagger.android.support.HasSupportFragmentInjector; 16 | 17 | 18 | public class AppInjector { 19 | private AppInjector() {} 20 | 21 | public static void init(TmdbApp tmdbApp) { 22 | DaggerAppComponent.builder().application(tmdbApp) 23 | .build().inject(tmdbApp); 24 | 25 | tmdbApp.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { 26 | @Override 27 | public void onActivityCreated(Activity activity, Bundle bundle) { 28 | handleActivity(activity); 29 | } 30 | 31 | @Override 32 | public void onActivityStarted(Activity activity) { 33 | 34 | } 35 | 36 | @Override 37 | public void onActivityResumed(Activity activity) { 38 | 39 | } 40 | 41 | @Override 42 | public void onActivityPaused(Activity activity) { 43 | 44 | } 45 | 46 | @Override 47 | public void onActivityStopped(Activity activity) { 48 | 49 | } 50 | 51 | @Override 52 | public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { 53 | 54 | } 55 | 56 | @Override 57 | public void onActivityDestroyed(Activity activity) { 58 | 59 | } 60 | }); 61 | } 62 | 63 | private static void handleActivity(Activity activity) { 64 | if (activity instanceof HasSupportFragmentInjector) { 65 | AndroidInjection.inject(activity); 66 | } 67 | 68 | if (activity instanceof FragmentActivity) { 69 | ((FragmentActivity) activity).getSupportFragmentManager() 70 | .registerFragmentLifecycleCallbacks( 71 | new FragmentManager.FragmentLifecycleCallbacks() { 72 | @Override 73 | public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) { 74 | if(f instanceof Injectable){ 75 | AndroidSupportInjection.inject(f); 76 | } 77 | } 78 | } 79 | , true 80 | ); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/Injectable.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di; 2 | 3 | /** 4 | * Marks an activity / fragment injectable. 5 | * 6 | */ 7 | 8 | public interface Injectable { 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/component/AppComponent.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.component; 2 | 3 | import android.app.Application; 4 | 5 | import com.thomaskioko.livedatademo.TmdbApp; 6 | import com.thomaskioko.livedatademo.di.module.AppModule; 7 | import com.thomaskioko.livedatademo.di.module.MainActivityModule; 8 | import com.thomaskioko.livedatademo.di.module.RoomModule; 9 | 10 | import javax.inject.Singleton; 11 | 12 | import dagger.BindsInstance; 13 | import dagger.Component; 14 | import dagger.android.AndroidInjectionModule; 15 | 16 | 17 | @Singleton 18 | @Component(modules = { 19 | AndroidInjectionModule.class, 20 | AppModule.class, 21 | MainActivityModule.class, 22 | RoomModule.class, 23 | }) 24 | public interface AppComponent { 25 | @Component.Builder 26 | interface Builder { 27 | @BindsInstance 28 | Builder application(Application application); 29 | 30 | AppComponent build(); 31 | } 32 | 33 | void inject(TmdbApp tmdbApp); 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/AppModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import com.thomaskioko.livedatademo.repository.api.TmdbService; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | import retrofit2.Retrofit; 10 | 11 | 12 | @Module(includes = { 13 | ViewModelModule.class, 14 | NetworkModule.class 15 | }) 16 | public class AppModule { 17 | 18 | @Provides 19 | @Singleton 20 | TmdbService provideTmdbService(Retrofit retrofit) { 21 | return retrofit.create(TmdbService.class); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/FragmentBuildersModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieDetailFragment; 4 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieListFragment; 5 | 6 | import dagger.Module; 7 | import dagger.android.ContributesAndroidInjector; 8 | 9 | 10 | @Module 11 | public abstract class FragmentBuildersModule { 12 | 13 | @ContributesAndroidInjector 14 | abstract MovieListFragment contributeMovieListFragment(); 15 | 16 | @ContributesAndroidInjector 17 | abstract MovieDetailFragment contributeMovieDetailFragment(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/MainActivityModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import com.thomaskioko.livedatademo.view.ui.MainActivity; 4 | 5 | import dagger.Module; 6 | import dagger.android.ContributesAndroidInjector; 7 | 8 | 9 | @Module 10 | public abstract class MainActivityModule { 11 | @ContributesAndroidInjector(modules = FragmentBuildersModule.class) 12 | abstract MainActivity contributeMainActivity(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/NetworkModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.thomaskioko.livedatademo.BuildConfig; 6 | import com.thomaskioko.livedatademo.repository.api.AuthInterceptor; 7 | import com.thomaskioko.livedatademo.repository.util.LiveDataCallAdapterFactory; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | import okhttp3.OkHttpClient; 16 | import okhttp3.logging.HttpLoggingInterceptor; 17 | import retrofit2.Retrofit; 18 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 19 | import retrofit2.converter.gson.GsonConverterFactory; 20 | 21 | import static com.thomaskioko.livedatademo.utils.AppConstants.BASE_URL; 22 | import static com.thomaskioko.livedatademo.utils.AppConstants.CONNECT_TIMEOUT; 23 | import static com.thomaskioko.livedatademo.utils.AppConstants.READ_TIMEOUT; 24 | import static com.thomaskioko.livedatademo.utils.AppConstants.WRITE_TIMEOUT; 25 | 26 | /** 27 | * This class is responsible for setting up Retrofit and anything related to network calls. 28 | * 29 | */ 30 | 31 | @Module 32 | public class NetworkModule { 33 | 34 | 35 | @Provides 36 | @Singleton 37 | HttpLoggingInterceptor provideOkHttpInterceptors() { 38 | return new HttpLoggingInterceptor(). 39 | setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE); 40 | } 41 | 42 | /** 43 | * Configure OkHttpClient. This helps us override some of the default configuration. Like the 44 | * connection timeout. 45 | * 46 | * @return OkHttpClient 47 | */ 48 | @Provides 49 | @Singleton 50 | OkHttpClient okHttpClient(HttpLoggingInterceptor httpLoggingInterceptor) { 51 | 52 | return new OkHttpClient.Builder() 53 | .addInterceptor(new AuthInterceptor()) 54 | .addInterceptor(httpLoggingInterceptor) 55 | .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) 56 | .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) 57 | .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) 58 | .build(); 59 | } 60 | 61 | 62 | @Provides 63 | @Singleton 64 | Retrofit provideRetrofitClient(@NonNull OkHttpClient okHttpClient) { 65 | return new Retrofit.Builder() 66 | .baseUrl(BASE_URL) 67 | .client(okHttpClient) 68 | .addConverterFactory(GsonConverterFactory.create()) // Serialize Objects 69 | .addCallAdapterFactory(new LiveDataCallAdapterFactory()) 70 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //Set call to return {@link Observable} 71 | .build(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/RoomModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import android.app.Application; 4 | import android.arch.persistence.room.Room; 5 | 6 | import com.thomaskioko.livedatademo.db.TmdbDb; 7 | import com.thomaskioko.livedatademo.db.dao.GenreDao; 8 | import com.thomaskioko.livedatademo.db.dao.MovieDao; 9 | import com.thomaskioko.livedatademo.db.dao.VideoDao; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import dagger.Module; 14 | import dagger.Provides; 15 | 16 | 17 | @Module 18 | public class RoomModule { 19 | 20 | @Singleton 21 | @Provides 22 | TmdbDb providesRoomDatabase(Application app) { 23 | return Room.databaseBuilder(app, TmdbDb.class, "tmdb_db").build(); 24 | } 25 | 26 | @Singleton 27 | @Provides 28 | MovieDao provideMovieDao(TmdbDb db){ 29 | return db.movieDao(); 30 | } 31 | 32 | @Singleton 33 | @Provides 34 | GenreDao provideGenreDao(TmdbDb db){ 35 | return db.genreDao(); 36 | } 37 | 38 | 39 | @Singleton 40 | @Provides 41 | VideoDao VideoDao(TmdbDb db){ 42 | return db.videoDao(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/module/ViewModelModule.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.module; 2 | 3 | import android.arch.lifecycle.ViewModel; 4 | import android.arch.lifecycle.ViewModelProvider; 5 | 6 | import com.thomaskioko.livedatademo.di.qualifires.ViewModelKey; 7 | import com.thomaskioko.livedatademo.viewmodel.MovieDetailViewModel; 8 | import com.thomaskioko.livedatademo.viewmodel.MovieListViewModel; 9 | import com.thomaskioko.livedatademo.viewmodel.ProjectViewModelFactory; 10 | import com.thomaskioko.livedatademo.viewmodel.SearchViewModel; 11 | 12 | import dagger.Binds; 13 | import dagger.Module; 14 | import dagger.multibindings.IntoMap; 15 | 16 | @Module 17 | public abstract class ViewModelModule { 18 | 19 | @Binds 20 | @IntoMap 21 | @ViewModelKey(SearchViewModel.class) 22 | abstract ViewModel bindSearchViewModel(SearchViewModel searchViewModel); 23 | 24 | @Binds 25 | @IntoMap 26 | @ViewModelKey(MovieListViewModel.class) 27 | abstract ViewModel bindMovieListViewModel(MovieListViewModel movieListViewModel); 28 | 29 | @Binds 30 | @IntoMap 31 | @ViewModelKey(MovieDetailViewModel.class) 32 | abstract ViewModel bindMovieDetailViewModel(MovieDetailViewModel movieDetailViewModel); 33 | 34 | @Binds 35 | abstract ViewModelProvider.Factory bindViewModelFactory(ProjectViewModelFactory projectViewModelFactory); 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/di/qualifires/ViewModelKey.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.di.qualifires; 2 | 3 | import android.arch.lifecycle.ViewModel; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | import dagger.MapKey; 12 | 13 | @Documented 14 | @Target({ElementType.METHOD}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @MapKey 17 | public @interface ViewModelKey { 18 | Class value(); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/api/AuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.api; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.thomaskioko.livedatademo.BuildConfig; 6 | 7 | import java.io.IOException; 8 | 9 | import okhttp3.HttpUrl; 10 | import okhttp3.Interceptor; 11 | import okhttp3.Request; 12 | import okhttp3.Response; 13 | import okhttp3.logging.HttpLoggingInterceptor; 14 | 15 | /** 16 | * This class add information (API Key) to {@link okhttp3.OkHttpClient} which is passed in 17 | * {@link com.thomaskioko.livedatademo.di.module.NetworkModule#okHttpClient(HttpLoggingInterceptor)} 18 | * which is required when making a request. This will ensure that all requests are made with the API key 19 | * 20 | */ 21 | public class AuthInterceptor implements Interceptor { 22 | 23 | /** 24 | * Default constructor. 25 | */ 26 | public AuthInterceptor() { 27 | } 28 | 29 | @Override 30 | public Response intercept(@NonNull Chain chain) throws IOException { 31 | Request request = chain.request(); 32 | HttpUrl url = request.url().newBuilder() 33 | .addQueryParameter("api_key", BuildConfig.TMDB_API_KEY) 34 | .build(); 35 | request = request.newBuilder().url(url).build(); 36 | return chain.proceed(request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/api/GenreResponse.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.api; 2 | 3 | import com.thomaskioko.livedatademo.db.entity.Genre; 4 | 5 | import java.util.List; 6 | 7 | 8 | public class GenreResponse { 9 | 10 | private List genres; 11 | 12 | public List getGenres() { 13 | return genres; 14 | } 15 | 16 | public void setGenres(List genres) { 17 | this.genres = genres; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "ClassPojo [genres = " + genres + "]"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/api/MovieResult.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.api; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.thomaskioko.livedatademo.db.entity.Movie; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | 10 | public class MovieResult { 11 | private Integer page; 12 | private List results = new ArrayList<>(); 13 | @SerializedName(value = "total_results") 14 | private Integer totalResults; 15 | @SerializedName(value = "total_pages") 16 | private Integer totalPages; 17 | 18 | /** 19 | * @return The page 20 | */ 21 | public Integer getPage() { 22 | return page; 23 | } 24 | 25 | /** 26 | * @param page The page 27 | */ 28 | public void setPage(Integer page) { 29 | this.page = page; 30 | } 31 | 32 | /** 33 | * @return The results 34 | */ 35 | public List getResults() { 36 | return results; 37 | } 38 | 39 | /** 40 | * @param results The results 41 | */ 42 | public void setResults(List results) { 43 | this.results = results; 44 | } 45 | 46 | /** 47 | * @return The totalResults 48 | */ 49 | public Integer getTotalResults() { 50 | return totalResults; 51 | } 52 | 53 | /** 54 | * @param totalResults The total_results 55 | */ 56 | public void setTotalResults(Integer totalResults) { 57 | this.totalResults = totalResults; 58 | } 59 | 60 | /** 61 | * @return The totalPages 62 | */ 63 | public Integer getTotalPages() { 64 | return totalPages; 65 | } 66 | 67 | /** 68 | * @param totalPages The total_pages 69 | */ 70 | public void setTotalPages(Integer totalPages) { 71 | this.totalPages = totalPages; 72 | } 73 | 74 | } 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/api/TmdbService.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.api; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | 5 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 6 | import com.thomaskioko.livedatademo.db.entity.Movie; 7 | 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Query; 11 | 12 | 13 | public interface TmdbService { 14 | 15 | @GET("movie/top_rated?") 16 | LiveData> getTopRatedMovies(); 17 | 18 | @GET("movie/popular?") 19 | LiveData> getPopularMovies(); 20 | 21 | @GET("movie/latest") 22 | LiveData> getLatestMovies(); 23 | 24 | @GET("discover/movie?sort_by=popularity.desc") 25 | LiveData> discoverPopularMovies(); 26 | 27 | @GET("search/movie?") 28 | LiveData> searchMovies(@Query("query") String query); 29 | 30 | @GET("movie/{movie_id}") 31 | LiveData> getMovieById(@Path("movie_id") int movieId); 32 | 33 | @GET("movie/{movie_id}/similar") 34 | LiveData> getSimilarMovies(@Path("movie_id") int movieId); 35 | 36 | @GET("genre/movie/list") 37 | LiveData> getGenres(); 38 | 39 | @GET("movie/{movie_id}/videos") 40 | LiveData> getMovieVideos(@Path("movie_id") int movieId); 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/api/VideoResult.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.api; 2 | 3 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 4 | 5 | import java.util.List; 6 | 7 | 8 | /** 9 | * 10 | */ 11 | 12 | public class VideoResult { 13 | private String id; 14 | private List results; 15 | 16 | public String getId() { 17 | return id; 18 | } 19 | 20 | public void setId(String id) { 21 | this.id = id; 22 | } 23 | 24 | public List getResults() { 25 | return results; 26 | } 27 | 28 | public void setResults(List results) { 29 | this.results = results; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "ClassPojo [id = " + id + ", results = " + results + "]"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/model/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.model; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.util.ArrayMap; 6 | 7 | import java.io.IOException; 8 | import java.util.Collections; 9 | import java.util.Map; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | import retrofit2.Response; 14 | import timber.log.Timber; 15 | 16 | /** 17 | * Common class used by API responses. 18 | */ 19 | 20 | public class ApiResponse { 21 | 22 | private static final Pattern LINK_PATTERN = Pattern 23 | .compile("<([^>]*)>[\\s]*;[\\s]*rel=\"([a-zA-Z0-9]+)\""); 24 | private static final Pattern PAGE_PATTERN = Pattern.compile("\\bpage=(\\d+)"); 25 | private static final String NEXT_LINK = "next"; 26 | public final int code; 27 | @Nullable 28 | public final T body; 29 | @Nullable 30 | public final String errorMessage; 31 | @NonNull 32 | public final Map links; 33 | 34 | public ApiResponse(Throwable error) { 35 | code = 500; 36 | body = null; 37 | errorMessage = error.getMessage(); 38 | links = Collections.emptyMap(); 39 | } 40 | 41 | public ApiResponse(Response response) { 42 | code = response.code(); 43 | if(response.isSuccessful()) { 44 | body = response.body(); 45 | errorMessage = null; 46 | } else { 47 | String message = null; 48 | if (response.errorBody() != null) { 49 | try { 50 | message = response.errorBody().string(); 51 | } catch (IOException ignored) { 52 | Timber.e(ignored, "error while parsing response"); 53 | } 54 | } 55 | if (message == null || message.trim().length() == 0) { 56 | message = response.message(); 57 | } 58 | errorMessage = message; 59 | body = null; 60 | } 61 | String linkHeader = response.headers().get("link"); 62 | if (linkHeader == null) { 63 | links = Collections.emptyMap(); 64 | } else { 65 | links = new ArrayMap<>(); 66 | Matcher matcher = LINK_PATTERN.matcher(linkHeader); 67 | 68 | while (matcher.find()) { 69 | int count = matcher.groupCount(); 70 | if (count == 2) { 71 | links.put(matcher.group(2), matcher.group(1)); 72 | } 73 | } 74 | } 75 | } 76 | 77 | public boolean isSuccessful() { 78 | return code >= 200 && code < 300; 79 | } 80 | 81 | public Integer getNextPage() { 82 | String next = links.get(NEXT_LINK); 83 | if (next == null) { 84 | return null; 85 | } 86 | Matcher matcher = PAGE_PATTERN.matcher(next); 87 | if (!matcher.find() || matcher.groupCount() != 1) { 88 | return null; 89 | } 90 | try { 91 | return Integer.parseInt(matcher.group(1)); 92 | } catch (NumberFormatException ex) { 93 | Timber.w("cannot parse next page from %s", next); 94 | return null; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/util/AppExecutors.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.util; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.support.annotation.NonNull; 6 | 7 | import java.util.concurrent.Executor; 8 | import java.util.concurrent.Executors; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Singleton; 12 | 13 | /** 14 | * Global executor pools for the whole application. 15 | *

16 | * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind 17 | * webservice requests). 18 | */ 19 | @Singleton 20 | public class AppExecutors { 21 | 22 | private final Executor diskIO; 23 | 24 | private final Executor networkIO; 25 | 26 | private final Executor mainThread; 27 | 28 | public AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) { 29 | this.diskIO = diskIO; 30 | this.networkIO = networkIO; 31 | this.mainThread = mainThread; 32 | } 33 | 34 | @Inject 35 | public AppExecutors() { 36 | this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3), 37 | new MainThreadExecutor()); 38 | } 39 | 40 | public Executor diskIO() { 41 | return diskIO; 42 | } 43 | 44 | public Executor networkIO() { 45 | return networkIO; 46 | } 47 | 48 | public Executor mainThread() { 49 | return mainThread; 50 | } 51 | 52 | private static class MainThreadExecutor implements Executor { 53 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 54 | @Override 55 | public void execute(@NonNull Runnable command) { 56 | mainThreadHandler.post(command); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/util/LiveDataCallAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.util; 2 | 3 | 4 | import android.arch.lifecycle.LiveData; 5 | 6 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 7 | 8 | import java.lang.reflect.Type; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | import retrofit2.Call; 12 | import retrofit2.CallAdapter; 13 | import retrofit2.Callback; 14 | import retrofit2.Response; 15 | 16 | /** 17 | * A Retrofit adapterthat converts the Call into a LiveData of ApiResponse. 18 | * @param 19 | */ 20 | public class LiveDataCallAdapter implements CallAdapter>> { 21 | private final Type responseType; 22 | public LiveDataCallAdapter(Type responseType) { 23 | this.responseType = responseType; 24 | } 25 | 26 | @Override 27 | public Type responseType() { 28 | return responseType; 29 | } 30 | 31 | @Override 32 | public LiveData> adapt(Call call) { 33 | return new LiveData>() { 34 | AtomicBoolean started = new AtomicBoolean(false); 35 | @Override 36 | protected void onActive() { 37 | super.onActive(); 38 | if (started.compareAndSet(false, true)) { 39 | call.enqueue(new Callback() { 40 | @Override 41 | public void onResponse(Call call, Response response) { 42 | postValue(new ApiResponse<>(response)); 43 | } 44 | 45 | @Override 46 | public void onFailure(Call call, Throwable throwable) { 47 | postValue(new ApiResponse(throwable)); 48 | } 49 | }); 50 | } 51 | } 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/util/LiveDataCallAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.util; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | 5 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | 11 | import retrofit2.CallAdapter; 12 | import retrofit2.Retrofit; 13 | 14 | public class LiveDataCallAdapterFactory extends CallAdapter.Factory { 15 | 16 | @Override 17 | public CallAdapter get(Type returnType, Annotation[] annotations, Retrofit retrofit) { 18 | if (getRawType(returnType) != LiveData.class) { 19 | return null; 20 | } 21 | Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType); 22 | Class rawObservableType = getRawType(observableType); 23 | if (rawObservableType != ApiResponse.class) { 24 | throw new IllegalArgumentException("type must be a resource"); 25 | } 26 | if (! (observableType instanceof ParameterizedType)) { 27 | throw new IllegalArgumentException("resource must be parameterized"); 28 | } 29 | Type bodyType = getParameterUpperBound(0, (ParameterizedType) observableType); 30 | return new LiveDataCallAdapter<>(bodyType); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/repository/util/NetworkBoundResource.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.repository.util; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.lifecycle.MediatorLiveData; 5 | import android.support.annotation.MainThread; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.annotation.WorkerThread; 9 | 10 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 11 | import com.thomaskioko.livedatademo.utils.Objects; 12 | import com.thomaskioko.livedatademo.vo.Resource; 13 | 14 | 15 | /** 16 | * A generic class that can provide a resource backed by both the sqlite database and the network. 17 | *

18 | * You can read more about it in the Architecture 19 | * Guide. 20 | * 21 | * @param 22 | * @param 23 | */ 24 | public abstract class NetworkBoundResource { 25 | private final AppExecutors appExecutors; 26 | 27 | private final MediatorLiveData> result = new MediatorLiveData<>(); 28 | 29 | @MainThread 30 | public NetworkBoundResource(AppExecutors appExecutors) { 31 | this.appExecutors = appExecutors; 32 | result.setValue(Resource.loading(null)); 33 | //TODO:: Add method to check if data should be saved. This should apply for search data. 34 | LiveData dbSource = loadFromDb(); 35 | result.addSource(dbSource, data -> { 36 | result.removeSource(dbSource); 37 | if (shouldFetch(data)) { 38 | fetchFromNetwork(dbSource); 39 | } else { 40 | result.addSource(dbSource, newData -> setValue(Resource.success(newData))); 41 | } 42 | }); 43 | } 44 | 45 | @MainThread 46 | private void setValue(Resource newValue) { 47 | if (!Objects.equals(result.getValue(), newValue)) { 48 | result.setValue(newValue); 49 | } 50 | } 51 | 52 | private void fetchFromNetwork(final LiveData dbSource) { 53 | LiveData> apiResponse = createCall(); 54 | // we re-attach dbSource as a new source, it will dispatch its latest value quickly 55 | result.addSource(dbSource, newData -> setValue(Resource.loading(newData))); 56 | result.addSource(apiResponse, response -> { 57 | result.removeSource(apiResponse); 58 | result.removeSource(dbSource); 59 | //noinspection ConstantConditions 60 | if (response.isSuccessful()) { 61 | appExecutors.diskIO().execute(() -> { 62 | saveCallResult(processResponse(response)); 63 | appExecutors.mainThread().execute(() -> 64 | // we specially request a new live data, 65 | // otherwise we will get immediately last cached value, 66 | // which may not be updated with latest results received from network. 67 | result.addSource(loadFromDb(), 68 | newData -> setValue(Resource.success(newData))) 69 | ); 70 | }); 71 | } else { 72 | onFetchFailed(); 73 | result.addSource(dbSource, 74 | newData -> setValue(Resource.error(response.errorMessage, newData))); 75 | } 76 | }); 77 | } 78 | 79 | protected void onFetchFailed() { 80 | } 81 | 82 | public LiveData> asLiveData() { 83 | return result; 84 | } 85 | 86 | @WorkerThread 87 | protected RequestType processResponse(ApiResponse response) { 88 | return response.body; 89 | } 90 | 91 | @WorkerThread 92 | protected abstract void saveCallResult(@NonNull RequestType item); 93 | 94 | @MainThread 95 | protected abstract boolean shouldFetch(@Nullable ResultType data); 96 | 97 | @NonNull 98 | @MainThread 99 | protected abstract LiveData loadFromDb(); 100 | 101 | @NonNull 102 | @MainThread 103 | protected abstract LiveData> createCall(); 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/utils/AbsentLiveData.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.utils; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | 5 | /** 6 | * A LiveData class that has {@code null} value. 7 | */ 8 | public class AbsentLiveData extends LiveData { 9 | private AbsentLiveData() { 10 | postValue(null); 11 | } 12 | public static LiveData create() { 13 | //noinspection unchecked 14 | return new AbsentLiveData(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/utils/AppConstants.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.utils; 2 | 3 | 4 | import android.graphics.Color; 5 | 6 | public class AppConstants { 7 | /** 8 | * Connection timeout duration 9 | */ 10 | public static final int CONNECT_TIMEOUT = 60 * 1000; 11 | /** 12 | * Connection Read timeout duration 13 | */ 14 | public static final int READ_TIMEOUT = 60 * 1000; 15 | /** 16 | * Connection write timeout duration 17 | */ 18 | public static final int WRITE_TIMEOUT = 60 * 1000; 19 | /** 20 | * Endpoint 21 | */ 22 | public static final String BASE_URL = "http://api.themoviedb.org/3/"; 23 | 24 | 25 | public static boolean DEBUG = true; 26 | 27 | // the dimens unit is dp or sp, not px 28 | public static final float DEFAULT_LINE_MARGIN = 5; 29 | public static final float DEFAULT_TAG_MARGIN = 5; 30 | public static final float DEFAULT_TAG_TEXT_PADDING_LEFT = 8; 31 | public static final float DEFAULT_TAG_TEXT_PADDING_TOP = 5; 32 | public static final float DEFAULT_TAG_TEXT_PADDING_RIGHT = 8; 33 | public static final float DEFAULT_TAG_TEXT_PADDING_BOTTOM = 5; 34 | public static final float LAYOUT_WIDTH_OFFSET = 2; 35 | 36 | public static final float DEFAULT_TAG_TEXT_SIZE = 14f; 37 | public static final float DEFAULT_TAG_DELETE_INDICATOR_SIZE = 14f; 38 | public static final float DEFAULT_TAG_LAYOUT_BORDER_SIZE = 0f; 39 | public static final float DEFAULT_TAG_RADIUS = 100; 40 | public static final int DEFAULT_TAG_LAYOUT_COLOR = Color.parseColor("#00BFFF"); 41 | public static final int DEFAULT_TAG_LAYOUT_COLOR_PRESS = Color.parseColor("#88363636"); 42 | public static final int DEFAULT_TAG_TEXT_COLOR = Color.parseColor("#ffffff"); 43 | public static final int DEFAULT_TAG_DELETE_INDICATOR_COLOR = Color.parseColor("#ffffff"); 44 | public static final int DEFAULT_TAG_LAYOUT_BORDER_COLOR = Color.parseColor("#ffffff"); 45 | public static final String DEFAULT_TAG_DELETE_ICON = "×"; 46 | public static final boolean DEFAULT_TAG_IS_DELETABLE = false; 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/utils/DeviceUtils.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.os.Build; 7 | import android.view.Window; 8 | import android.view.WindowManager; 9 | 10 | import com.thomaskioko.livedatademo.R; 11 | 12 | /** 13 | * 14 | */ 15 | 16 | public class DeviceUtils { 17 | 18 | public static void setTranslucentStatusBar(Window window, int color) { 19 | if (window == null) return; 20 | int sdkInt = Build.VERSION.SDK_INT; 21 | if (sdkInt >= Build.VERSION_CODES.LOLLIPOP) { 22 | setTranslucentStatusBarLollipop(window, color); 23 | } else if (sdkInt >= Build.VERSION_CODES.KITKAT) { 24 | setTranslucentStatusBarKiKat(window); 25 | } 26 | } 27 | 28 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 29 | private static void setTranslucentStatusBarLollipop(Window window, int color) { 30 | window.setStatusBarColor( 31 | window.getContext() 32 | .getResources() 33 | .getColor(color)); 34 | } 35 | 36 | @TargetApi(Build.VERSION_CODES.KITKAT) 37 | private static void setTranslucentStatusBarKiKat(Window window) { 38 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 39 | } 40 | 41 | public static int getToolbarHeight(Context context) { 42 | final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes( 43 | new int[]{R.attr.actionBarSize}); 44 | int toolbarHeight = (int) styledAttributes.getDimension(0, 0); 45 | styledAttributes.recycle(); 46 | 47 | return toolbarHeight; 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/utils/Objects.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.utils; 2 | 3 | public class Objects { 4 | public static boolean equals(Object o1, Object o2) { 5 | if (o1 == null) { 6 | return o2 == null; 7 | } 8 | if (o2 == null) { 9 | return false; 10 | } 11 | return o1.equals(o2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/adapter/MovieListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | 9 | import com.bumptech.glide.Glide; 10 | import com.thomaskioko.livedatademo.R; 11 | import com.thomaskioko.livedatademo.db.entity.Movie; 12 | import com.thomaskioko.livedatademo.view.callback.MovieCallback; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import butterknife.BindView; 18 | import butterknife.ButterKnife; 19 | 20 | 21 | public class MovieListAdapter extends RecyclerView.Adapter { 22 | 23 | private List mMovieList = new ArrayList<>(); 24 | private MovieCallback mMovieCallback; 25 | 26 | public MovieListAdapter(MovieCallback movieCallback) { 27 | mMovieCallback = movieCallback; 28 | } 29 | 30 | @Override 31 | public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 32 | View view = LayoutInflater.from(parent.getContext()) 33 | .inflate(R.layout.item_movie_layout, parent, false); 34 | return new MovieViewHolder(view); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(MovieViewHolder holder, int position) { 39 | 40 | Movie movie = mMovieList.get(position); 41 | String posterUrl = holder.ivPoster.getContext().getString(R.string.tmdb_image_url) + 42 | holder.ivPoster.getContext().getString(R.string.image_size_780) + movie.posterUrl; 43 | 44 | Glide.with(holder.ivPoster.getContext()) 45 | .load(posterUrl) 46 | .into(holder.ivPoster); 47 | 48 | holder.itemView.setOnClickListener(view -> mMovieCallback.onClick(holder.ivPoster, movie)); 49 | 50 | } 51 | 52 | @Override 53 | public int getItemCount() { 54 | return mMovieList.size(); 55 | } 56 | 57 | public void setData(List movieList) { 58 | mMovieList = movieList; 59 | notifyDataSetChanged(); 60 | } 61 | 62 | public void clearAdapter() { 63 | mMovieList.clear(); 64 | notifyDataSetChanged(); 65 | } 66 | 67 | /** 68 | * ViewHolder class 69 | */ 70 | public class MovieViewHolder extends RecyclerView.ViewHolder { 71 | 72 | @BindView(R.id.ivPoster) 73 | public ImageView ivPoster; 74 | public View itemView; 75 | 76 | public MovieViewHolder(View itemView) { 77 | super(itemView); 78 | ButterKnife.bind(this, itemView); 79 | 80 | this.itemView = itemView; 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/adapter/SearchItemAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.bumptech.glide.Glide; 11 | import com.thomaskioko.livedatademo.R; 12 | import com.thomaskioko.livedatademo.db.entity.Movie; 13 | import com.thomaskioko.livedatademo.view.callback.MovieCallback; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import butterknife.BindView; 19 | import butterknife.ButterKnife; 20 | 21 | 22 | public class SearchItemAdapter extends RecyclerView.Adapter { 23 | 24 | private List mMovieList = new ArrayList<>(); 25 | private MovieCallback mMovieClickCallback; 26 | 27 | 28 | public SearchItemAdapter(MovieCallback movieClickCallback) { 29 | mMovieClickCallback = movieClickCallback; 30 | } 31 | 32 | @Override 33 | public SearchItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 34 | View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_movie_layout, parent, false); 35 | return new ViewHolder(itemView); 36 | } 37 | 38 | @Override 39 | public void onBindViewHolder(SearchItemAdapter.ViewHolder holder, int position) { 40 | final Movie movie = mMovieList.get(position); 41 | 42 | String posterUrl = holder.imageView.getContext().getString(R.string.tmdb_image_url) + 43 | holder.imageView.getContext().getString(R.string.image_size_780) + movie.posterUrl; 44 | 45 | holder.tvName.setText(movie.title); 46 | holder.releaseYear.setText(movie.releaseYear); 47 | holder.itemView.setOnClickListener(view -> mMovieClickCallback.onClick(holder.imageView, movie)); 48 | Glide.with(holder.imageView.getContext()) 49 | .load(posterUrl) 50 | .into(holder.imageView); 51 | } 52 | 53 | @Override 54 | public int getItemCount() { 55 | return mMovieList.size(); 56 | } 57 | 58 | public class ViewHolder extends RecyclerView.ViewHolder { 59 | @BindView(R.id.movie_title) 60 | TextView tvName; 61 | @BindView(R.id.movie_thumb) 62 | ImageView imageView; 63 | @BindView(R.id.movie_release_year) 64 | TextView releaseYear; 65 | View itemView; 66 | 67 | 68 | public ViewHolder(View view) { 69 | super(view); 70 | ButterKnife.bind(this, view); 71 | this.itemView = view; 72 | } 73 | } 74 | 75 | 76 | public void setItems(List movieList) { 77 | mMovieList = movieList; 78 | notifyDataSetChanged(); 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/adapter/VideoListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.adapter; 2 | 3 | import android.os.Handler; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.bumptech.glide.Glide; 12 | import com.thomaskioko.livedatademo.R; 13 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 14 | import com.thomaskioko.livedatademo.view.callback.VideoCallback; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | 22 | /** 23 | * @author Thomas Kioko 24 | */ 25 | 26 | public class VideoListAdapter extends RecyclerView.Adapter { 27 | 28 | private List mVideoList = new ArrayList<>(); 29 | private VideoCallback mVideoClickCallback; 30 | 31 | public VideoListAdapter(VideoCallback videoClickCallback) { 32 | mVideoClickCallback = videoClickCallback; 33 | } 34 | 35 | @Override 36 | public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 37 | View view = LayoutInflater.from(parent.getContext()) 38 | .inflate(R.layout.item_video_trailer, parent, false); 39 | return new MovieViewHolder(view); 40 | } 41 | 42 | @Override 43 | public void onBindViewHolder(MovieViewHolder holder, int position) { 44 | 45 | TmdbVideo video = mVideoList.get(position); 46 | 47 | String videoUrl = holder.ivPoster.getContext() 48 | .getResources().getString(R.string.thumbnail_max_resolution, video.key); 49 | 50 | holder.tvVideoTitle.setText(video.name); 51 | Glide.with(holder.ivPoster.getContext()) 52 | .load(videoUrl) 53 | .into(holder.ivPoster); 54 | 55 | holder.itemView.setOnClickListener(view -> { 56 | // <--- Giving time to the ripple effect finish before opening a new activity 57 | new Handler().postDelayed(() -> mVideoClickCallback.onClick(holder.ivPoster, video), 200); 58 | }); 59 | 60 | } 61 | 62 | @Override 63 | public int getItemCount() { 64 | return mVideoList.size(); 65 | } 66 | 67 | public void setData(List movieList) { 68 | mVideoList = movieList; 69 | notifyDataSetChanged(); 70 | } 71 | 72 | public void clearAdapter() { 73 | mVideoList.clear(); 74 | notifyDataSetChanged(); 75 | } 76 | 77 | /** 78 | * ViewHolder class 79 | */ 80 | public class MovieViewHolder extends RecyclerView.ViewHolder { 81 | 82 | @BindView(R.id.iv_video_background) 83 | ImageView ivPoster; 84 | @BindView(R.id.text_view_video_title) 85 | TextView tvVideoTitle; 86 | View itemView; 87 | 88 | public MovieViewHolder(View itemView) { 89 | super(itemView); 90 | ButterKnife.bind(this, itemView); 91 | 92 | this.itemView = itemView; 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/callback/MovieCallback.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.callback; 2 | 3 | import android.widget.ImageView; 4 | 5 | import com.thomaskioko.livedatademo.db.entity.Movie; 6 | 7 | /** 8 | * 9 | */ 10 | 11 | public interface MovieCallback { 12 | void onClick(ImageView sharedImageView, Movie movie); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/callback/VideoCallback.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.callback; 2 | 3 | import android.widget.ImageView; 4 | 5 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 6 | 7 | /** 8 | * 9 | */ 10 | 11 | public interface VideoCallback { 12 | void onClick(ImageView sharedImageView, TmdbVideo tmdbVideo); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.ui; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.thomaskioko.livedatademo.R; 8 | import com.thomaskioko.livedatademo.view.ui.common.NavigationController; 9 | 10 | import javax.inject.Inject; 11 | 12 | import dagger.android.AndroidInjection; 13 | import dagger.android.AndroidInjector; 14 | import dagger.android.DispatchingAndroidInjector; 15 | import dagger.android.support.HasSupportFragmentInjector; 16 | 17 | public class MainActivity extends AppCompatActivity implements HasSupportFragmentInjector { 18 | 19 | @Inject 20 | DispatchingAndroidInjector dispatchingAndroidInjector; 21 | 22 | @Inject 23 | NavigationController navigationController; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_main); 29 | 30 | AndroidInjection.inject(this); 31 | 32 | // Add project list fragment if this is first creation 33 | if (savedInstanceState == null) { 34 | navigationController.navigateToMovieListFragment(); 35 | } 36 | 37 | // Being here means we are in animation mode 38 | supportPostponeEnterTransition(); 39 | 40 | } 41 | 42 | @Override 43 | public AndroidInjector supportFragmentInjector() { 44 | return dispatchingAndroidInjector; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/ui/common/NavigationController.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.ui.common; 2 | 3 | import android.os.Build; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.view.ViewCompat; 6 | import android.transition.Fade; 7 | import android.view.View; 8 | 9 | import com.thomaskioko.livedatademo.R; 10 | import com.thomaskioko.livedatademo.view.ui.MainActivity; 11 | import com.thomaskioko.livedatademo.view.ui.fragment.DetailsTransition; 12 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieDetailFragment; 13 | import com.thomaskioko.livedatademo.view.ui.fragment.MovieListFragment; 14 | 15 | import javax.inject.Inject; 16 | 17 | /** 18 | * A utility class that handles navigation in {@link MainActivity}. 19 | */ 20 | 21 | public class NavigationController { 22 | private final int containerId; 23 | private final FragmentManager fragmentManager; 24 | 25 | @Inject 26 | public NavigationController(MainActivity mainActivity) { 27 | containerId = R.id.container; 28 | fragmentManager = mainActivity.getSupportFragmentManager(); 29 | } 30 | 31 | public void navigateToMovieListFragment() { 32 | MovieListFragment fragment = new MovieListFragment(); 33 | fragmentManager.beginTransaction() 34 | .replace(containerId, fragment) 35 | .commitAllowingStateLoss(); 36 | } 37 | 38 | public void navigateToMovieDetailFragment(View sharedImageView, int movieId) { 39 | MovieDetailFragment fragment = MovieDetailFragment.create(movieId); 40 | 41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 42 | fragment.setSharedElementEnterTransition(new DetailsTransition()); 43 | fragment.setEnterTransition(new Fade()); 44 | fragment.setExitTransition(new Fade()); 45 | fragment.setSharedElementReturnTransition(new DetailsTransition()); 46 | } 47 | fragmentManager.beginTransaction() 48 | .setReorderingAllowed(true) 49 | .addSharedElement(sharedImageView, ViewCompat.getTransitionName(sharedImageView)) 50 | .replace(containerId, fragment) 51 | .addToBackStack(null) 52 | .commitAllowingStateLoss(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/ui/fragment/DetailsTransition.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.ui.fragment; 2 | 3 | import android.os.Build; 4 | import android.support.annotation.RequiresApi; 5 | import android.transition.ChangeBounds; 6 | import android.transition.ChangeImageTransform; 7 | import android.transition.ChangeTransform; 8 | import android.transition.TransitionSet; 9 | 10 | /** 11 | * 12 | */ 13 | 14 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 15 | public class DetailsTransition extends TransitionSet { 16 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 17 | public DetailsTransition() { 18 | setOrdering(ORDERING_TOGETHER); 19 | addTransition(new ChangeBounds()) 20 | .addTransition(new ChangeTransform()) 21 | .setStartDelay(25) 22 | .setDuration(350) 23 | .addTransition(new ChangeImageTransform()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/view/ui/utils/ScrollingFabBehavior.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.view.ui.utils; 2 | 3 | import android.content.Context; 4 | import android.support.design.widget.AppBarLayout; 5 | import android.support.design.widget.CoordinatorLayout; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | 10 | import com.thomaskioko.livedatademo.utils.DeviceUtils; 11 | 12 | /** 13 | * class to handle displaying of the FloatingActionButton 14 | */ 15 | 16 | public class ScrollingFabBehavior extends CoordinatorLayout.Behavior { 17 | private float toolbarHeight; 18 | 19 | public ScrollingFabBehavior(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | this.toolbarHeight = DeviceUtils.getToolbarHeight(context); 22 | } 23 | 24 | @Override 25 | public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton fab, View dependency) { 26 | return dependency instanceof AppBarLayout; 27 | } 28 | 29 | @Override 30 | public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton fab, View dependency) { 31 | if (dependency instanceof AppBarLayout) { 32 | CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); 33 | int fabBottomMargin = lp.bottomMargin; 34 | int distanceToScroll = fab.getHeight() + fabBottomMargin; 35 | float ratio = dependency.getY() / toolbarHeight; 36 | fab.setTranslationY(distanceToScroll * ratio); 37 | } 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/viewmodel/MovieDetailViewModel.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.viewmodel; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.lifecycle.MutableLiveData; 5 | import android.arch.lifecycle.Transformations; 6 | import android.arch.lifecycle.ViewModel; 7 | import android.support.annotation.VisibleForTesting; 8 | import android.support.v7.graphics.Palette; 9 | 10 | import com.thomaskioko.livedatademo.db.entity.Genre; 11 | import com.thomaskioko.livedatademo.db.entity.Movie; 12 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 13 | import com.thomaskioko.livedatademo.repository.TmdbRepository; 14 | import com.thomaskioko.livedatademo.utils.AbsentLiveData; 15 | import com.thomaskioko.livedatademo.utils.Objects; 16 | import com.thomaskioko.livedatademo.vo.Resource; 17 | 18 | import java.util.List; 19 | 20 | import javax.inject.Inject; 21 | 22 | 23 | public class MovieDetailViewModel extends ViewModel { 24 | 25 | @VisibleForTesting 26 | final MutableLiveData movieId = new MutableLiveData<>(); 27 | private final LiveData> movie; 28 | private final LiveData>> videos; 29 | private final MutableLiveData mPalette = new MutableLiveData<>(); 30 | private TmdbRepository tmdbRepository; 31 | 32 | @Inject 33 | MovieDetailViewModel(TmdbRepository tmdbRepository) { 34 | this.tmdbRepository = tmdbRepository; 35 | movie = Transformations.switchMap(movieId, movieId -> { 36 | if (movieId == null) { 37 | return AbsentLiveData.create(); 38 | } else { 39 | return tmdbRepository.getMovieById(movieId); 40 | } 41 | }); 42 | 43 | videos = Transformations.switchMap(movieId, movieId -> { 44 | if (movieId == null) { 45 | return AbsentLiveData.create(); 46 | } else { 47 | return tmdbRepository.getMovieVideo(movieId); 48 | } 49 | }); 50 | } 51 | 52 | @VisibleForTesting 53 | public LiveData> getMovie() { 54 | return movie; 55 | } 56 | 57 | @VisibleForTesting 58 | public void setMovieId(int movieId) { 59 | if (Objects.equals(movieId, this.movieId.getValue())) { 60 | return; 61 | } 62 | 63 | this.movieId.setValue(movieId); 64 | } 65 | 66 | @VisibleForTesting 67 | public LiveData>> getVideoMovies() { 68 | return videos; 69 | } 70 | 71 | public LiveData> getMovieGenresById(int genreId) { 72 | return tmdbRepository.getGenresById(genreId); 73 | } 74 | 75 | @VisibleForTesting 76 | public void setPalette(Palette palette) { 77 | if (Objects.equals(palette, mPalette.getValue())) { 78 | return; 79 | } 80 | 81 | mPalette.setValue(palette); 82 | } 83 | 84 | @VisibleForTesting 85 | public LiveData getPalette() { 86 | return mPalette; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/viewmodel/MovieListViewModel.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.viewmodel; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.lifecycle.MutableLiveData; 5 | import android.arch.lifecycle.Observer; 6 | import android.arch.lifecycle.Transformations; 7 | import android.arch.lifecycle.ViewModel; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.support.annotation.VisibleForTesting; 11 | 12 | import com.thomaskioko.livedatademo.repository.TmdbRepository; 13 | import com.thomaskioko.livedatademo.db.entity.Movie; 14 | import com.thomaskioko.livedatademo.utils.AbsentLiveData; 15 | import com.thomaskioko.livedatademo.utils.Objects; 16 | import com.thomaskioko.livedatademo.vo.Resource; 17 | 18 | import java.util.List; 19 | import java.util.Locale; 20 | 21 | import javax.inject.Inject; 22 | 23 | import timber.log.Timber; 24 | 25 | 26 | public class MovieListViewModel extends ViewModel { 27 | 28 | private final LiveData>> moviesLiveData; 29 | private final MutableLiveData query = new MutableLiveData<>(); 30 | private final LiveData>> searchResults; 31 | 32 | 33 | @Inject 34 | MovieListViewModel(@NonNull TmdbRepository tmdbRepository) { 35 | moviesLiveData = tmdbRepository.getPopularMovies(); 36 | searchResults = Transformations.switchMap(query, search -> { 37 | 38 | if (search == null || search.trim().length() == 0) { 39 | return AbsentLiveData.create(); 40 | } else { 41 | return tmdbRepository.searchMovie(search); 42 | } 43 | }); 44 | } 45 | 46 | @VisibleForTesting 47 | public LiveData>> getSearchResults() { 48 | return searchResults; 49 | } 50 | 51 | public void setSearchQuery(@NonNull String originalInput) { 52 | String input = originalInput.toLowerCase(Locale.getDefault()).trim(); 53 | if (Objects.equals(input, query.getValue())) { 54 | return; 55 | } 56 | query.setValue(input); 57 | } 58 | 59 | 60 | @VisibleForTesting 61 | public LiveData>> getPopularMovies() { 62 | return moviesLiveData; 63 | } 64 | 65 | @VisibleForTesting 66 | static class NextPageHandler implements Observer> { 67 | @Nullable 68 | private LiveData> nextPageLiveData; 69 | private final MutableLiveData loadMoreState = new MutableLiveData<>(); 70 | private String query; 71 | private final TmdbRepository repository; 72 | @VisibleForTesting 73 | boolean hasMore; 74 | 75 | @VisibleForTesting 76 | NextPageHandler(TmdbRepository repository) { 77 | this.repository = repository; 78 | reset(); 79 | } 80 | 81 | void queryNextPage(String query) { 82 | if (Objects.equals(this.query, query)) { 83 | return; 84 | } 85 | unregister(); 86 | this.query = query; 87 | nextPageLiveData = repository.searchNextPage(query); 88 | loadMoreState.setValue(new LoadMoreState(true, null)); 89 | //noinspection ConstantConditions 90 | nextPageLiveData.observeForever(this); 91 | } 92 | 93 | @Override 94 | public void onChanged(@Nullable Resource result) { 95 | if (result == null) { 96 | reset(); 97 | } else { 98 | switch (result.status) { 99 | case SUCCESS: 100 | hasMore = Boolean.TRUE.equals(result.data); 101 | unregister(); 102 | loadMoreState.setValue(new LoadMoreState(false, null)); 103 | break; 104 | case ERROR: 105 | hasMore = true; 106 | unregister(); 107 | loadMoreState.setValue(new LoadMoreState(false, 108 | result.message)); 109 | break; 110 | } 111 | } 112 | } 113 | 114 | private void unregister() { 115 | if (nextPageLiveData != null) { 116 | nextPageLiveData.removeObserver(this); 117 | nextPageLiveData = null; 118 | if (hasMore) { 119 | query = null; 120 | } 121 | } 122 | } 123 | 124 | private void reset() { 125 | unregister(); 126 | hasMore = true; 127 | loadMoreState.setValue(new LoadMoreState(false, null)); 128 | } 129 | 130 | MutableLiveData getLoadMoreState() { 131 | return loadMoreState; 132 | } 133 | } 134 | 135 | static class LoadMoreState { 136 | private final boolean running; 137 | private final String errorMessage; 138 | private boolean handledError = false; 139 | 140 | LoadMoreState(boolean running, String errorMessage) { 141 | this.running = running; 142 | this.errorMessage = errorMessage; 143 | } 144 | 145 | boolean isRunning() { 146 | return running; 147 | } 148 | 149 | String getErrorMessage() { 150 | return errorMessage; 151 | } 152 | 153 | String getErrorMessageIfNotHandled() { 154 | if (handledError) { 155 | return null; 156 | } 157 | handledError = true; 158 | return errorMessage; 159 | } 160 | } 161 | 162 | 163 | @Override 164 | protected void onCleared() { 165 | super.onCleared(); 166 | Timber.d("@onCleared called"); 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/viewmodel/ProjectViewModelFactory.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.viewmodel; 2 | 3 | import android.arch.lifecycle.ViewModel; 4 | import android.arch.lifecycle.ViewModelProvider; 5 | 6 | import java.util.Map; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Provider; 10 | import javax.inject.Singleton; 11 | 12 | @Singleton 13 | public class ProjectViewModelFactory implements ViewModelProvider.Factory { 14 | private final Map, Provider> creators; 15 | 16 | @Inject 17 | public ProjectViewModelFactory(Map, Provider> creators) { 18 | this.creators = creators; 19 | } 20 | 21 | @SuppressWarnings("unchecked") 22 | @Override 23 | public T create(Class modelClass) { 24 | Provider creator = creators.get(modelClass); 25 | if (creator == null) { 26 | for (Map.Entry, Provider> entry : creators.entrySet()) { 27 | if (modelClass.isAssignableFrom(entry.getKey())) { 28 | creator = entry.getValue(); 29 | break; 30 | } 31 | } 32 | } 33 | if (creator == null) { 34 | throw new IllegalArgumentException("unknown model class " + modelClass); 35 | } 36 | try { 37 | return (T) creator.get(); 38 | } catch (Exception e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/viewmodel/SearchViewModel.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.viewmodel; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.lifecycle.MutableLiveData; 5 | import android.arch.lifecycle.Transformations; 6 | import android.arch.lifecycle.ViewModel; 7 | import android.support.annotation.NonNull; 8 | import android.support.annotation.VisibleForTesting; 9 | 10 | import com.thomaskioko.livedatademo.repository.TmdbRepository; 11 | import com.thomaskioko.livedatademo.db.entity.Movie; 12 | import com.thomaskioko.livedatademo.vo.Resource; 13 | import com.thomaskioko.livedatademo.utils.AbsentLiveData; 14 | import com.thomaskioko.livedatademo.utils.Objects; 15 | 16 | import java.util.List; 17 | import java.util.Locale; 18 | 19 | import javax.inject.Inject; 20 | 21 | import timber.log.Timber; 22 | 23 | 24 | public class SearchViewModel extends ViewModel { 25 | 26 | private final MutableLiveData query = new MutableLiveData<>(); 27 | private final LiveData>> searchResults; 28 | 29 | @Inject 30 | SearchViewModel(@NonNull TmdbRepository tmdbRepository) { 31 | searchResults = Transformations.switchMap(query, search -> { 32 | 33 | if (search == null || search.trim().length() == 0) { 34 | return AbsentLiveData.create(); 35 | } else { 36 | return tmdbRepository.searchMovie(search); 37 | } 38 | }); 39 | } 40 | 41 | @VisibleForTesting 42 | public LiveData>> getSearchResults() { 43 | return searchResults; 44 | } 45 | 46 | public void setSearchQuery(@NonNull String originalInput) { 47 | String input = originalInput.toLowerCase(Locale.getDefault()).trim(); 48 | if (Objects.equals(input, query.getValue())) { 49 | return; 50 | } 51 | query.setValue(input); 52 | } 53 | 54 | 55 | @Override 56 | protected void onCleared() { 57 | super.onCleared(); 58 | Timber.d("@onCleared called"); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/vo/Resource.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.vo; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.annotation.Nullable; 5 | 6 | import static com.thomaskioko.livedatademo.vo.Status.ERROR; 7 | import static com.thomaskioko.livedatademo.vo.Status.LOADING; 8 | import static com.thomaskioko.livedatademo.vo.Status.SUCCESS; 9 | 10 | 11 | /** 12 | * A generic class that holds a value with its loading status. 13 | * @param 14 | */ 15 | public class Resource { 16 | 17 | @NonNull 18 | public final Status status; 19 | 20 | @Nullable 21 | public final String message; 22 | 23 | @Nullable 24 | public final T data; 25 | 26 | public Resource(@NonNull Status status, @Nullable T data, @Nullable String message) { 27 | this.status = status; 28 | this.data = data; 29 | this.message = message; 30 | } 31 | 32 | public static Resource success(@Nullable T data) { 33 | return new Resource<>(SUCCESS, data, null); 34 | } 35 | 36 | public static Resource error(String msg, @Nullable T data) { 37 | return new Resource<>(ERROR, data, msg); 38 | } 39 | 40 | public static Resource loading(@Nullable T data) { 41 | return new Resource<>(LOADING, data, null); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) { 47 | return true; 48 | } 49 | if (o == null || getClass() != o.getClass()) { 50 | return false; 51 | } 52 | 53 | Resource resource = (Resource) o; 54 | 55 | if (status != resource.status) { 56 | return false; 57 | } 58 | if (message != null ? !message.equals(resource.message) : resource.message != null) { 59 | return false; 60 | } 61 | return data != null ? data.equals(resource.data) : resource.data == null; 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | int result = status.hashCode(); 67 | result = 31 * result + (message != null ? message.hashCode() : 0); 68 | result = 31 * result + (data != null ? data.hashCode() : 0); 69 | return result; 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "Resource{" + 75 | "status=" + status + 76 | ", message='" + message + '\'' + 77 | ", data=" + data + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/thomaskioko/livedatademo/vo/Status.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.vo; 2 | 3 | /** 4 | * Status of a resource that is provided to the UI. 5 | *

6 | * These are usually created by the Repository classes where they return 7 | * {@code LiveData>} to pass back the latest data to the UI with its fetch status. 8 | */ 9 | public enum Status { 10 | SUCCESS, 11 | ERROR, 12 | LOADING 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_10.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_4.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_5.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_6.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_7.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_8.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/badge_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/badge_9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bg_collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/bg_collection.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bg_watched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/bg_watched.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/bg_watchlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/bg_watchlist.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/fanart_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/fanart_dark.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_audience_rotten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_audience_rotten.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_audience_unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_audience_unknown.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_generic_man_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_generic_man_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_score_audience.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_score_audience.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_score_fresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_score_fresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_score_imdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_score_imdb.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_score_rotten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_score_rotten.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/ic_score_superfresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/ic_score_superfresh.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/poster_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/poster_placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/tmdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/tmdb.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/trakttv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/trakttv.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/user_trakt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/drawable-nodpi/user_trakt.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/selectable_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/black_translucent_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/custom_cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_headerbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_headerbar_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_headerbar_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_light_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /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/res/drawable/ic_play_arrow_.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_circle_outline_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/movie_bage.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selectable_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/include_toolbar.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_movie_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_search_movie_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 35 | 36 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_tagview.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video_trailer.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/movie_detail_fragment.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 24 | 25 | 37 | 38 | 44 | 45 | 53 | 54 | 55 | 56 | 63 | 64 | 65 | 66 | 67 | 68 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/src/main/res/layout/movie_list_fragment.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 27 | 28 | 29 | 37 | 38 | 39 | 43 | 44 | 55 | 56 | 57 | 65 | 66 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/movie_poster.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 21 | 22 | 29 | 30 | 38 | 39 | 47 | 48 | 54 | 55 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/res/layout/movie_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 20 | 21 | 30 | 31 | 39 | 40 | 45 | 46 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 74 | 75 | 76 | 81 | 82 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/src/main/res/layout/movie_profile_header_wrapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 19 | 20 | 27 | 28 | 33 | 34 | 35 | 43 | 44 | 61 | 62 | 75 | 76 | 96 | 97 | 102 | 103 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_play_arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-hdpi/ic_play_arrow_white.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_play_arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xhdpi/ic_play_arrow_white.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_play_arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxhdpi/ic_play_arrow_white.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_play_arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/app/src/main/res/mipmap-xxxhdpi/ic_play_arrow_white.png -------------------------------------------------------------------------------- /app/src/main/res/transition-v21/details_window_enter_transition.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/transition-v21/details_window_return_transition.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #016d1b 6 | #015415 7 | 8 | #212121 9 | #727272 10 | #ffffff 11 | #8affffff 12 | #FFFFFF 13 | #B6B6B6 14 | 15 | #ff0099cc 16 | 17 | #BB000000 18 | #70000000 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 170dp 5 | 48dp 6 | 7 | 8 | 12sp 9 | 14sp 10 | 16sp 11 | 18sp 12 | 21sp 13 | 25sp 14 | 15 | 280dp 16 | 17 | 18 | 2dp 19 | 20 | 21 | 160dp 22 | 220dp 23 | 100dp 24 | 25 | 10dp 26 | 5dp 27 | 28 | 29 | 4dp 30 | 140dp 31 | 180dp 32 | 240dp 33 | 34 | 35 | 20dp 36 | 25dp 37 | 38 | 39 | 90dp 40 | 100dp 41 | 42 | 43 | 4dp 44 | 8dp 45 | 16dp 46 | 24dp 47 | 48 | 240dp 49 | 240dp 50 | 51 | 360dp 52 | 0dp 53 | 16dp 54 | @dimen/fab_margin 55 | #ffffffff 56 | 57 | 120dp 58 | 115dp 59 | 80dp 60 | 61 | 90dp 62 | 60dp 63 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tmdb 3 | 4 | 5 | 6 | http://image.tmdb.org/t/p/ 7 | 8 | w185 9 | w500 10 | w780 11 | original 12 | 13 | 14 | Could not fetch movies! 15 | No Movie found :( 16 | 17 | Search Movies 18 | 19 | 20 | Release Year: 21 | Plot: 22 | Trailers: 23 | Reviews: 24 | Rating 25 | Popularity: 26 | Vote Count: 27 | 28 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? 29 | 30 | 31 | %s Ratings 32 | 33 | 34 | http://img.youtube.com/vi/%s/default.jpg 35 | http://img.youtube.com/vi/%s/hqdefault.jpg 36 | http://img.youtube.com/vi/%s/mqdefault.jpg 37 | http://img.youtube.com/vi/%s/sddefault.jpg 38 | http://img.youtube.com/vi/%s/maxresdefault.jpg 39 | 40 | 41 | TN_DetailIcon 42 | 43 | Image 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 39 | 40 | 44 | 45 | 46 | 55 | 56 | 63 | 64 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/test-common/java/com/thomaskioko/livedatademo/util/LiveDataTestUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.arch.lifecycle.LiveData; 4 | import android.arch.lifecycle.Observer; 5 | import android.support.annotation.Nullable; 6 | 7 | import java.util.concurrent.CountDownLatch; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class LiveDataTestUtil { 11 | public static T getValue(LiveData liveData) throws InterruptedException { 12 | final Object[] data = new Object[1]; 13 | CountDownLatch latch = new CountDownLatch(1); 14 | Observer observer = new Observer() { 15 | @Override 16 | public void onChanged(@Nullable T o) { 17 | data[0] = o; 18 | latch.countDown(); 19 | liveData.removeObserver(this); 20 | } 21 | }; 22 | liveData.observeForever(observer); 23 | latch.await(2, TimeUnit.SECONDS); 24 | //noinspection unchecked 25 | return (T) data[0]; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/api/ApiResponseTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.api; 2 | 3 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.JUnit4; 8 | 9 | import okhttp3.MediaType; 10 | import okhttp3.ResponseBody; 11 | import retrofit2.Response; 12 | 13 | import static org.hamcrest.CoreMatchers.is; 14 | import static org.hamcrest.CoreMatchers.notNullValue; 15 | import static org.hamcrest.CoreMatchers.nullValue; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | 18 | @RunWith(JUnit4.class) 19 | public class ApiResponseTest { 20 | 21 | @Test 22 | public void exception() { 23 | Exception exception = new Exception("foo"); 24 | ApiResponse apiResponse = new ApiResponse<>(exception); 25 | assertThat(apiResponse.links, notNullValue()); 26 | assertThat(apiResponse.body, nullValue()); 27 | assertThat(apiResponse.code, is(500)); 28 | assertThat(apiResponse.errorMessage, is("foo")); 29 | } 30 | 31 | @Test 32 | public void success() { 33 | ApiResponse apiResponse = new ApiResponse<>(Response.success("foo")); 34 | assertThat(apiResponse.errorMessage, nullValue()); 35 | assertThat(apiResponse.code, is(200)); 36 | assertThat(apiResponse.body, is("foo")); 37 | assertThat(apiResponse.getNextPage(), is(nullValue())); 38 | } 39 | 40 | @Test 41 | public void error() { 42 | ApiResponse response = new ApiResponse<>(Response.error(400, 43 | ResponseBody.create(MediaType.parse("application/txt"), "blah"))); 44 | assertThat(response.code, is(400)); 45 | assertThat(response.errorMessage, is("blah")); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/api/TmdbServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.api; 2 | 3 | import android.arch.core.executor.testing.InstantTaskExecutorRule; 4 | 5 | import com.thomaskioko.livedatademo.db.entity.Movie; 6 | import com.thomaskioko.livedatademo.db.entity.TmdbVideo; 7 | import com.thomaskioko.livedatademo.repository.api.MovieResult; 8 | import com.thomaskioko.livedatademo.repository.api.TmdbService; 9 | import com.thomaskioko.livedatademo.repository.api.VideoResult; 10 | import com.thomaskioko.livedatademo.repository.util.LiveDataCallAdapterFactory; 11 | 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runners.JUnit4; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import okhttp3.OkHttpClient; 28 | import okhttp3.mockwebserver.MockResponse; 29 | import okhttp3.mockwebserver.MockWebServer; 30 | import okhttp3.mockwebserver.RecordedRequest; 31 | import okio.BufferedSource; 32 | import okio.Okio; 33 | import retrofit2.Retrofit; 34 | import retrofit2.converter.gson.GsonConverterFactory; 35 | 36 | import static com.thomaskioko.livedatademo.util.LiveDataTestUtil.getValue; 37 | import static com.thomaskioko.livedatademo.utils.AppConstants.CONNECT_TIMEOUT; 38 | import static com.thomaskioko.livedatademo.utils.AppConstants.READ_TIMEOUT; 39 | import static com.thomaskioko.livedatademo.utils.AppConstants.WRITE_TIMEOUT; 40 | import static junit.framework.Assert.assertNotNull; 41 | import static junit.framework.Assert.assertTrue; 42 | import static org.hamcrest.CoreMatchers.is; 43 | import static org.junit.Assert.assertThat; 44 | 45 | @RunWith(JUnit4.class) 46 | public class TmdbServiceTest { 47 | 48 | @Rule 49 | public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); 50 | private TmdbService service; 51 | private MockWebServer mockWebServer; 52 | 53 | @Before 54 | public void createdService() throws IOException { 55 | mockWebServer = new MockWebServer(); 56 | service = new Retrofit.Builder() 57 | .baseUrl(mockWebServer.url("/")) 58 | .client(getHttpClient()) 59 | .addConverterFactory(GsonConverterFactory.create()) 60 | .addCallAdapterFactory(new LiveDataCallAdapterFactory()) 61 | .build() 62 | .create(TmdbService.class); 63 | } 64 | 65 | @After 66 | public void stopService() throws IOException { 67 | mockWebServer.shutdown(); 68 | } 69 | 70 | @Test 71 | public void testGetPopularMovies() throws IOException, InterruptedException { 72 | enqueueResponse("popular-movies.json"); 73 | MovieResult movieResult = getValue(service.getPopularMovies()).body; 74 | 75 | RecordedRequest request = mockWebServer.takeRequest(); 76 | assertThat(request.getPath(), is("/movie/popular?")); 77 | 78 | assertNotNull(movieResult); 79 | 80 | List movieList = movieResult.getResults(); 81 | assertTrue(movieList.size()> 0); 82 | } 83 | 84 | @Test 85 | public void testGetMovieVideos() throws IOException, InterruptedException { 86 | enqueueResponse("videos-by-movie-id.json"); 87 | VideoResult videoResult = getValue(service.getMovieVideos(354912)).body; 88 | 89 | RecordedRequest request = mockWebServer.takeRequest(); 90 | assertThat(request.getPath(), is("/movie/354912/videos")); 91 | 92 | assertNotNull(videoResult); 93 | 94 | List results = videoResult.getResults(); 95 | assertTrue(results.size()> 0); 96 | } 97 | 98 | @Test 99 | public void testGetSearchedMovie() throws IOException, InterruptedException { 100 | enqueueResponse("search-movie.json"); 101 | MovieResult movieResult = getValue(service.searchMovies("hitman")).body; 102 | 103 | RecordedRequest request = mockWebServer.takeRequest(); 104 | assertThat(request.getPath(), is("/search/movie?&query=hitman")); 105 | 106 | assertNotNull(movieResult); 107 | 108 | List movieList = movieResult.getResults(); 109 | assertTrue(movieList.size()> 0); 110 | } 111 | 112 | @Test 113 | public void testGetSearchedMovieById() throws IOException, InterruptedException { 114 | enqueueResponse("search-movie-id.json"); 115 | Movie movieResult = getValue(service.getMovieById(354912)).body; 116 | 117 | RecordedRequest request = mockWebServer.takeRequest(); 118 | assertThat(request.getPath(), is("/movie/354912")); 119 | 120 | assertNotNull(movieResult); 121 | 122 | assertThat(movieResult.title, is("Coco")); 123 | } 124 | 125 | 126 | private void enqueueResponse(String fileName) throws IOException { 127 | enqueueResponse(fileName, Collections.emptyMap()); 128 | } 129 | 130 | private void enqueueResponse(String fileName, Map headers) throws IOException { 131 | InputStream inputStream = getClass().getClassLoader() 132 | .getResourceAsStream("api-response/" + fileName); 133 | BufferedSource source = Okio.buffer(Okio.source(inputStream)); 134 | MockResponse mockResponse = new MockResponse(); 135 | for (Map.Entry header : headers.entrySet()) { 136 | mockResponse.addHeader(header.getKey(), header.getValue()); 137 | } 138 | mockWebServer.enqueue(mockResponse 139 | .setBody(source.readString(StandardCharsets.UTF_8))); 140 | } 141 | 142 | private OkHttpClient getHttpClient(){ 143 | return new OkHttpClient.Builder() 144 | .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) 145 | .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) 146 | .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) 147 | .build(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/util/ApiUtil.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | 4 | import android.arch.lifecycle.LiveData; 5 | import android.arch.lifecycle.MutableLiveData; 6 | 7 | import com.thomaskioko.livedatademo.repository.model.ApiResponse; 8 | 9 | import retrofit2.Response; 10 | 11 | public class ApiUtil { 12 | public static LiveData> successCall(T data) { 13 | return createCall(Response.success(data)); 14 | } 15 | public static LiveData> createCall(Response response) { 16 | MutableLiveData> data = new MutableLiveData<>(); 17 | data.setValue(new ApiResponse<>(response)); 18 | return data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/util/CountingAppExecutors.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.thomaskioko.livedatademo.repository.util.AppExecutors; 6 | 7 | import java.util.concurrent.Executor; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.TimeoutException; 11 | 12 | public class CountingAppExecutors { 13 | 14 | private final Object LOCK = new Object(); 15 | 16 | private int taskCount = 0; 17 | 18 | private AppExecutors appExecutors; 19 | 20 | public CountingAppExecutors() { 21 | Runnable increment = () -> { 22 | synchronized (LOCK) { 23 | taskCount--; 24 | if (taskCount == 0) { 25 | LOCK.notifyAll(); 26 | } 27 | } 28 | }; 29 | Runnable decrement = () -> { 30 | synchronized (LOCK) { 31 | taskCount++; 32 | } 33 | }; 34 | appExecutors = new AppExecutors( 35 | new CountingExecutor(increment, decrement), 36 | new CountingExecutor(increment, decrement), 37 | new CountingExecutor(increment, decrement)); 38 | } 39 | 40 | public AppExecutors getAppExecutors() { 41 | return appExecutors; 42 | } 43 | 44 | public void drainTasks(int time, TimeUnit timeUnit) 45 | throws InterruptedException, TimeoutException { 46 | long end = System.currentTimeMillis() + timeUnit.toMillis(time); 47 | while (true) { 48 | synchronized (LOCK) { 49 | if (taskCount == 0) { 50 | return; 51 | } 52 | long now = System.currentTimeMillis(); 53 | long remaining = end - now; 54 | if (remaining > 0) { 55 | LOCK.wait(remaining); 56 | } else { 57 | throw new TimeoutException("could not drain tasks"); 58 | } 59 | } 60 | } 61 | } 62 | 63 | private static class CountingExecutor implements Executor { 64 | 65 | private final Executor delegate = Executors.newSingleThreadExecutor(); 66 | 67 | private final Runnable increment; 68 | 69 | private final Runnable decrement; 70 | 71 | public CountingExecutor(Runnable increment, Runnable decrement) { 72 | this.increment = increment; 73 | this.decrement = decrement; 74 | } 75 | 76 | @Override 77 | public void execute(@NonNull Runnable command) { 78 | increment.run(); 79 | delegate.execute(() -> { 80 | try { 81 | command.run(); 82 | } finally { 83 | decrement.run(); 84 | } 85 | }); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/test/java/com/thomaskioko/livedatademo/util/InstantAppExecutors.java: -------------------------------------------------------------------------------- 1 | package com.thomaskioko.livedatademo.util; 2 | 3 | 4 | import com.thomaskioko.livedatademo.repository.util.AppExecutors; 5 | 6 | import java.util.concurrent.Executor; 7 | 8 | public class InstantAppExecutors extends AppExecutors { 9 | private static Executor instant = command -> command.run(); 10 | 11 | public InstantAppExecutors() { 12 | super(instant, instant, instant); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/test/resources/api-response/search-movie-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "adult": false, 3 | "backdrop_path": "/askg3SMvhqEl4OL52YuvdtY40Yb.jpg", 4 | "belongs_to_collection": null, 5 | "budget": 175000000, 6 | "genres": [ 7 | { 8 | "id": 10751, 9 | "name": "Family" 10 | }, 11 | { 12 | "id": 16, 13 | "name": "Animation" 14 | }, 15 | { 16 | "id": 12, 17 | "name": "Adventure" 18 | }, 19 | { 20 | "id": 35, 21 | "name": "Comedy" 22 | } 23 | ], 24 | "homepage": "", 25 | "id": 354912, 26 | "imdb_id": "tt2380307", 27 | "original_language": "en", 28 | "original_title": "Coco", 29 | "overview": "Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician like his idol, Ernesto de la Cruz. Desperate to prove his talent, Miguel finds himself in the stunning and colorful Land of the Dead following a mysterious chain of events. Along the way, he meets charming trickster Hector, and together, they set off on an extraordinary journey to unlock the real story behind Miguel's family history.", 30 | "popularity": 427.64022, 31 | "poster_path": "/eKi8dIrr8voobbaGzDpe8w0PVbC.jpg", 32 | "production_companies": [ 33 | { 34 | "name": "Disney Pixar", 35 | "id": 6806 36 | } 37 | ], 38 | "production_countries": [ 39 | { 40 | "iso_3166_1": "US", 41 | "name": "United States of America" 42 | } 43 | ], 44 | "release_date": "2017-10-27", 45 | "revenue": 700920729, 46 | "runtime": 109, 47 | "spoken_languages": [ 48 | { 49 | "iso_639_1": "en", 50 | "name": "English" 51 | }, 52 | { 53 | "iso_639_1": "es", 54 | "name": "Español" 55 | } 56 | ], 57 | "status": "Released", 58 | "tagline": "The celebration of a lifetime", 59 | "title": "Coco", 60 | "video": false, 61 | "vote_average": 7.7, 62 | "vote_count": 1952 63 | } -------------------------------------------------------------------------------- /app/src/test/resources/api-response/videos-by-movie-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 188927, 3 | "results": [ 4 | { 5 | "id": "571bf094c3a368525f006b86", 6 | "iso_639_1": "en", 7 | "iso_3166_1": "US", 8 | "key": "XRVD32rnzOw", 9 | "name": "Official Trailer", 10 | "site": "YouTube", 11 | "size": 1080, 12 | "type": "Trailer" 13 | }, 14 | { 15 | "id": "585032ad92514175ad00021e", 16 | "iso_639_1": "en", 17 | "iso_3166_1": "US", 18 | "key": "Tvq3y8BhZ2s", 19 | "name": "Official Trailer #2", 20 | "site": "YouTube", 21 | "size": 1080, 22 | "type": "Trailer" 23 | }, 24 | { 25 | "id": "585033299251416fa100b49d", 26 | "iso_639_1": "en", 27 | "iso_3166_1": "US", 28 | "key": "QSYLhl57pqM", 29 | "name": "Captain Kirk Featurette", 30 | "site": "YouTube", 31 | "size": 1080, 32 | "type": "Featurette" 33 | }, 34 | { 35 | "id": "58503359c3a3682fb800d603", 36 | "iso_639_1": "en", 37 | "iso_3166_1": "US", 38 | "key": "cr6bYn2EzvQ", 39 | "name": "Jaylah Featurette", 40 | "site": "YouTube", 41 | "size": 1080, 42 | "type": "Featurette" 43 | }, 44 | { 45 | "id": "5850338e9251416cd000b415", 46 | "iso_639_1": "en", 47 | "iso_3166_1": "US", 48 | "key": "kfPKpVYFSnU", 49 | "name": "Krall Featurette", 50 | "site": "YouTube", 51 | "size": 1080, 52 | "type": "Featurette" 53 | }, 54 | { 55 | "id": "585033b592514175ad0002db", 56 | "iso_639_1": "en", 57 | "iso_3166_1": "US", 58 | "key": "3MBXBMkcUNo", 59 | "name": "Official Trailer #3", 60 | "site": "YouTube", 61 | "size": 1080, 62 | "type": "Trailer" 63 | }, 64 | { 65 | "id": "585033e1c3a368315000afc5", 66 | "iso_639_1": "en", 67 | "iso_3166_1": "US", 68 | "key": "NwpvjQKdpvI", 69 | "name": "Official Trailer #4", 70 | "site": "YouTube", 71 | "size": 1080, 72 | "type": "Trailer" 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /art/HomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/art/HomeScreen.png -------------------------------------------------------------------------------- /art/MovieDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/art/MovieDetails.png -------------------------------------------------------------------------------- /art/archtiture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/art/archtiture.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.0' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | ext.supportLibVersion = '27.1.0' 19 | ext.archVersion = '1.0.0' 20 | ext.archRuntimeVersion = '1.1.1' 21 | ext.archExtensionVersion = '1.1.1' 22 | ext.retrofitVersion = '2.3.0' 23 | ext.okhttpVersion = '3.4.1' 24 | ext.daggerVersion = '2.11' 25 | ext.constraintLayoutVersion = "1.0.2" 26 | ext.mockitoVersion = "2.7.19" 27 | ext.timberVersion = "4.6.1" 28 | ext.glideVersion = "3.7.0" 29 | ext.butterKnifeVersion = "8.8.1" 30 | ext.junitVersion = "4.12" 31 | ext.coreTestingVersion = "1.0.0" 32 | ext.supportTestVersion = "1.0.0" 33 | ext.espressoCoreVersion = "3.0.1" 34 | ext.mockitoVersion = "2.7.19" 35 | ext.materialSearchViewVersion = "1.4.0" 36 | ext.circularProgressbarVersion = "1.1.1" 37 | ext.jodaTimeVersion = "2.9.9" 38 | ext.mockWebServerVersion = "3.8.1" 39 | 40 | 41 | allprojects { 42 | repositories { 43 | google() 44 | jcenter() 45 | } 46 | } 47 | 48 | task clean(type: Delete) { 49 | delete rootProject.buildDir 50 | } 51 | -------------------------------------------------------------------------------- /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 | TMDB_API_KEY="PUT_API_KEY" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomaskioko/android-liveData-viewModel/dd45b74e1e6d67fea4b10af0fe8eb3c098bde76b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 04 21:49:01 EAT 2018 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.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------