├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── dependencies.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── patloew │ │ └── countries │ │ └── EspressoUtils.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── patloew │ │ │ └── countries │ │ │ ├── CountriesApp.kt │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── CountryRepo.kt │ │ │ │ ├── PrefRepo.kt │ │ │ │ ├── RealmCountryRepo.kt │ │ │ │ └── SharedPrefRepo.kt │ │ │ ├── model │ │ │ │ ├── Country.kt │ │ │ │ └── RealmStringMapEntry.kt │ │ │ └── remote │ │ │ │ └── CountryApi.kt │ │ │ ├── injection │ │ │ ├── components │ │ │ │ ├── ActivityComponent.kt │ │ │ │ ├── ActivityViewHolderComponent.kt │ │ │ │ ├── AppComponent.kt │ │ │ │ ├── FragmentComponent.kt │ │ │ │ └── FragmentViewHolderComponent.kt │ │ │ ├── modules │ │ │ │ ├── ActivityModule.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── FragmentModule.kt │ │ │ │ ├── NetModule.kt │ │ │ │ ├── ViewHolderModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── qualifier │ │ │ │ ├── ActivityContext.kt │ │ │ │ ├── ActivityFragmentManager.kt │ │ │ │ ├── AppContext.kt │ │ │ │ └── ChildFragmentManager.kt │ │ │ └── scopes │ │ │ │ ├── PerActivity.kt │ │ │ │ ├── PerApplication.kt │ │ │ │ ├── PerFragment.kt │ │ │ │ └── PerViewHolder.kt │ │ │ ├── ui │ │ │ ├── BaseCountryViewModel.kt │ │ │ ├── ICountryViewModel.kt │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseActivityViewHolder.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── BaseFragmentViewHolder.kt │ │ │ │ ├── RtfmException.kt │ │ │ │ ├── feedback │ │ │ │ │ ├── ActivitySnacker.kt │ │ │ │ │ ├── ApplicationToaster.kt │ │ │ │ │ ├── Snacker.kt │ │ │ │ │ └── Toaster.kt │ │ │ │ ├── navigator │ │ │ │ │ ├── ActivityNavigator.kt │ │ │ │ │ ├── ChildFragmentNavigator.kt │ │ │ │ │ ├── FragmentNavigator.kt │ │ │ │ │ └── Navigator.kt │ │ │ │ ├── validation │ │ │ │ │ ├── BaseValidator.kt │ │ │ │ │ └── ValidationException.kt │ │ │ │ ├── view │ │ │ │ │ └── MvvmView.kt │ │ │ │ └── viewmodel │ │ │ │ │ ├── AdapterMvvmViewModel.kt │ │ │ │ │ ├── BaseStateViewModel.kt │ │ │ │ │ ├── BaseViewModel.kt │ │ │ │ │ ├── MvvmViewModel.kt │ │ │ │ │ └── NoOpViewModel.kt │ │ │ ├── detail │ │ │ │ ├── DetailActivity.kt │ │ │ │ ├── DetailMvvm.kt │ │ │ │ └── DetailViewModel.kt │ │ │ └── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── recyclerview │ │ │ │ ├── CountryAdapter.kt │ │ │ │ ├── CountryMvvm.kt │ │ │ │ ├── CountryViewHolder.kt │ │ │ │ └── CountryViewModel.kt │ │ │ │ └── viewpager │ │ │ │ ├── CountriesFragment.kt │ │ │ │ ├── CountriesView.kt │ │ │ │ ├── MainAdapter.kt │ │ │ │ ├── all │ │ │ │ ├── AllCountriesFragment.kt │ │ │ │ ├── AllCountriesViewModel.kt │ │ │ │ └── IAllCountriesViewModel.kt │ │ │ │ └── favorites │ │ │ │ ├── FavoriteCountriesFragment.kt │ │ │ │ ├── FavoriteCountriesViewModel.kt │ │ │ │ └── IFavoriteCountriesViewModel.kt │ │ │ └── util │ │ │ ├── CountryTypeAdapter.kt │ │ │ ├── DelegatedProperties.kt │ │ │ ├── JsonUtils.kt │ │ │ ├── ObservableBooleanPaperParcelTypeConverter.kt │ │ │ ├── ObservableDoublePaperParcelTypeConverter.kt │ │ │ ├── ObservableFieldPaperParcelTypeConverter.kt │ │ │ ├── ObservableFloatPaperParcelTypeConverter.kt │ │ │ ├── ObservableIntPaperParcelTypeConverter.kt │ │ │ ├── ObservableLongPaperParcelTypeConverter.kt │ │ │ ├── RealmListPaperParcelTypeConverter.kt │ │ │ ├── RealmStringListTypeAdapter.kt │ │ │ ├── RealmStringMapEntryListTypeAdapter.kt │ │ │ ├── Utils.kt │ │ │ ├── bindingadapter │ │ │ ├── BindingAdapters.kt │ │ │ └── ViewPagerBindingAdapter.java │ │ │ └── extensions │ │ │ ├── ContextExtensions.kt │ │ │ ├── Extensions.kt │ │ │ └── RealmExtensions.kt │ └── res │ │ ├── drawable │ │ ├── ic_bookmark_black.xml │ │ ├── ic_bookmark_border_black.xml │ │ └── ic_map_black.xml │ │ ├── layout │ │ ├── activity_detail.xml │ │ ├── activity_main.xml │ │ ├── card_country.xml │ │ └── fragment_recyclerview.xml │ │ ├── menu │ │ ├── menu_details.xml │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── aboutlibs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── patloew │ └── countries │ ├── AllCountriesViewModelUnitTest.java │ ├── BaseCountryViewModelUnitTest.java │ ├── FavoriteCountriesViewModelUnitTest.java │ └── RxSchedulersOverrideRule.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: 3 | - oraclejdk8 4 | android: 5 | components: 6 | # Uncomment the lines below if you want to 7 | # use the latest revision of Android SDK Tools 8 | - platform-tools 9 | - tools 10 | 11 | # The BuildTools version used by your project 12 | - build-tools-27.0.3 13 | 14 | # The SDK version used to compile your project 15 | - android-27 16 | 17 | # Additional components 18 | - extra-google-google_play_services 19 | - extra-google-m2repository 20 | - extra-android-m2repository 21 | 22 | before_install: 23 | - export JAVA8_HOME=/usr/lib/jvm/java-8-oracle 24 | - export JAVA_HOME=$JAVA8_HOME 25 | before_cache: 26 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 27 | cache: 28 | directories: 29 | - $HOME/.gradle/caches/ 30 | - $HOME/.gradle/wrapper/ 31 | 32 | script: 33 | - ./gradlew build 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Patrick Löwenstein 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Countries 2 | 3 | [![Build Status](https://travis-ci.org/patloew/countries.svg?branch=kotlin)](https://travis-ci.org/patloew/countries) 4 | 5 | A sample Android app written in Kotlin, which lists all countries with some additional information (currencies, languages, …). The app uses the MVVM pattern with the [Android data binding lib](http://developer.android.com/tools/data-binding/guide.html). Countries can be bookmarked and are then stored locally with [Realm](https://github.com/realm/realm-java). [Retrofit](https://github.com/square/retrofit) is used to fetch the country information from the free [REST Countries](http://restcountries.eu) service. For JSON parsing, custom [Gson](https://github.com/google/gson) TypeAdapters are used. [PaperParcel](https://github.com/grandstaish/paperparcel) is used to make the Country objects Parcelable. Also, [Dagger 2](https://github.com/google/dagger) is used for dependency injection. 6 | 7 | The purpose of this is app is to show how: 8 | * [the MVVM architectural pattern can be used with the data binding library](https://nullpointer.wtf/android/mvvm-architecture-data-binding-library/) 9 | * [Retrofit, Realm, PaperParcel and Gson with custom TypeAdapters work together](https://nullpointer.wtf/android/using-retrofit-realm-parceler/) 10 | * Dagger 2 can be used with different Scopes 11 | 12 | This project can also be used as a template for new apps. Check out the template branch for a cleaned up version of this project. 13 | 14 | # License 15 | 16 | Copyright 2016 Patrick Löwenstein 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'realm-android' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 27 8 | buildToolsVersion "27.0.3" 9 | defaultConfig { 10 | applicationId "com.patloew.countries" 11 | minSdkVersion 21 12 | targetSdkVersion 27 13 | versionCode 1 14 | versionName "1.0.0" 15 | vectorDrawables.useSupportLibrary = true 16 | 17 | setProperty("archivesBaseName", "Countries-v$versionName-b$versionCode") 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled true 23 | shrinkResources true 24 | proguardFiles 'proguard-rules.pro' 25 | } 26 | } 27 | flavorDimensions "backend" 28 | productFlavors { 29 | devBackend { 30 | applicationIdSuffix ".dev" 31 | resValue "string", "app_name", "Countries Dev" 32 | buildConfigField "String", "BASE_URL", "\"https://restcountries.eu/\"" 33 | dimension "backend" 34 | } 35 | productionBackend { 36 | resValue "string", "app_name", "Countries" 37 | buildConfigField "String", "BASE_URL", "\"https://restcountries.eu/\"" 38 | dimension "backend" 39 | } 40 | } 41 | dataBinding { 42 | enabled = true 43 | } 44 | lintOptions { 45 | abortOnError false 46 | } 47 | testOptions { 48 | unitTests.returnDefaultValues = true 49 | } 50 | packagingOptions { 51 | // Remove unneeded Realm native libs (nearly no devices use these) 52 | exclude 'lib/mips/librealm-jni.so' 53 | exclude 'lib/x86_64/librealm-jni.so' 54 | } 55 | sourceSets { 56 | main.java.srcDirs += 'src/main/kotlin' 57 | } 58 | } 59 | 60 | apply from: "dependencies.gradle" 61 | -------------------------------------------------------------------------------- /app/dependencies.gradle: -------------------------------------------------------------------------------- 1 | def dependencyGroup(Closure closure) { 2 | closure.delegate = dependencies 3 | return closure 4 | } 5 | 6 | def local = dependencyGroup { 7 | implementation fileTree(dir: 'libs', include: ['*.jar']) 8 | } 9 | 10 | def ui = dependencyGroup { 11 | implementation "com.android.support:support-fragment:$supportLibVersion" 12 | implementation "com.android.support:support-core-ui:$supportLibVersion" 13 | implementation "com.android.support:appcompat-v7:$supportLibVersion" 14 | implementation "com.android.support:cardview-v7:$supportLibVersion" 15 | implementation "com.android.support:recyclerview-v7:$supportLibVersion" 16 | implementation "com.android.support:design:$supportLibVersion" 17 | 18 | kapt "com.android.databinding:compiler:$gradlePluginVersion" 19 | 20 | implementation 'com.simplecityapps:recyclerview-fastscroll:1.0.17' 21 | 22 | implementation('com.mikepenz:aboutlibraries:6.0.6@aar') { transitive = true } 23 | } 24 | 25 | def network = dependencyGroup { 26 | implementation 'com.google.code.gson:gson:2.8.2' 27 | 28 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" 29 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" 30 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" 31 | 32 | implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" 33 | implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion" 34 | } 35 | 36 | def util = dependencyGroup { 37 | implementation 'com.jakewharton.timber:timber:4.6.1' 38 | 39 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' 40 | releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' 41 | 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 43 | 44 | implementation "nz.bradcampbell:paperparcel:$paperParcelVersion" 45 | implementation "nz.bradcampbell:paperparcel-kotlin:$paperParcelVersion" 46 | kapt "nz.bradcampbell:paperparcel-compiler:$paperParcelVersion" 47 | } 48 | 49 | def realm = dependencyGroup { 50 | kapt "io.realm:realm-annotations-processor:$realmVersion" 51 | kapt "io.realm:realm-annotations:$realmVersion" 52 | } 53 | 54 | def test = dependencyGroup { 55 | testImplementation 'junit:junit:4.12' 56 | testImplementation "org.mockito:mockito-core:$mockitoVersion" 57 | testImplementation "org.powermock:powermock-module-junit4:$powerMockVersion" 58 | testImplementation "org.powermock:powermock-module-junit4-rule:$powerMockVersion" 59 | testImplementation "org.powermock:powermock-api-mockito2:$powerMockVersion" 60 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 61 | testImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0" 62 | testImplementation "com.google.code.findbugs:jsr305:3.0.2" 63 | 64 | androidTestImplementation "com.android.support:support-annotations:$supportLibVersion" 65 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 66 | androidTestImplementation 'com.android.support.test:rules:1.0.1' 67 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 68 | androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.1' 69 | androidTestImplementation "org.mockito:mockito-android:$mockitoVersion" 70 | androidTestImplementation('com.android.support.test.espresso:espresso-contrib:3.0.1') { 71 | exclude group: 'com.android.support', module: 'appcompat' 72 | exclude group: 'com.android.support', module: 'support-v4' 73 | exclude group: 'com.android.support', module: 'appcompat-v7' 74 | exclude group: 'com.android.support', module: 'design' 75 | exclude group: 'com.android.support', module: 'recyclerview-v7' 76 | } 77 | androidTestImplementation "com.squareup.okhttp3:okhttp:$okHttpVersion" 78 | androidTestImplementation 'com.jakewharton.espresso:okhttp3-idling-resource:1.0.0' 79 | androidTestImplementation "com.google.code.findbugs:jsr305:3.0.2" 80 | } 81 | 82 | def di = dependencyGroup { 83 | kapt "com.google.dagger:dagger-compiler:$daggerVersion" 84 | implementation "com.google.dagger:dagger:$daggerVersion" 85 | } 86 | 87 | 88 | def rx = dependencyGroup { 89 | implementation 'io.reactivex.rxjava2:rxjava:2.1.10' 90 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 91 | } 92 | 93 | dependencies { 94 | local() 95 | 96 | ui() 97 | di() 98 | rx() 99 | network() 100 | util() 101 | realm() 102 | test() 103 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/patloew/countries/EspressoUtils.java: -------------------------------------------------------------------------------- 1 | package com.patloew.countries; 2 | 3 | import android.content.res.Resources; 4 | import android.support.test.espresso.NoMatchingViewException; 5 | import android.support.test.espresso.UiController; 6 | import android.support.test.espresso.ViewAction; 7 | import android.support.test.espresso.ViewAssertion; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.View; 10 | 11 | import junit.framework.AssertionFailedError; 12 | 13 | import org.hamcrest.Description; 14 | import org.hamcrest.Matcher; 15 | import org.hamcrest.TypeSafeMatcher; 16 | 17 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 18 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast; 19 | 20 | /* Copyright 2017 Tailored Media GmbH 21 | * 22 | * Licensed under the Apache License, Version 2.0 (the "License"); 23 | * you may not use this file except in compliance with the License. 24 | * You may obtain a copy of the License at 25 | * 26 | * http://www.apache.org/licenses/LICENSE-2.0 27 | * 28 | * Unless required by applicable law or agreed to in writing, software 29 | * distributed under the License is distributed on an "AS IS" BASIS, 30 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | * See the License for the specific language governing permissions and 32 | * limitations under the License. */ 33 | public class EspressoUtils { 34 | 35 | public static Matcher matchFirstDisplayedWithId(final int id) { 36 | return new TypeSafeMatcher() { 37 | 38 | private boolean alreadyMatched = false; 39 | private Resources resources = null; 40 | 41 | @Override 42 | public void describeTo(Description description) { 43 | String idDescription = Integer.toString(id); 44 | if (resources != null) { 45 | try { 46 | idDescription = resources.getResourceName(id); 47 | } catch (Resources.NotFoundException e) { 48 | // No big deal, will just use the int value. 49 | idDescription = String.format("%s (resource name not found)", id); 50 | } 51 | } 52 | description.appendText("with id: " + idDescription); 53 | } 54 | 55 | @Override 56 | public boolean matchesSafely(View view) { 57 | if(alreadyMatched) { 58 | return false; 59 | } else { 60 | resources = view.getResources(); 61 | alreadyMatched = isDisplayed().matches(view) && id == view.getId(); 62 | return alreadyMatched; 63 | } 64 | } 65 | }; 66 | } 67 | 68 | public static ViewAction clickChildViewWithId(final int id) { 69 | return new ViewAction() { 70 | @Override 71 | public Matcher getConstraints() { 72 | return isDisplayingAtLeast(90); 73 | } 74 | 75 | @Override 76 | public String getDescription() { 77 | return "Click on a child view with id " + id + "."; 78 | } 79 | 80 | @Override 81 | public void perform(UiController uiController, View view) { 82 | View v = view.findViewById(id); 83 | if (v != null) { v.performClick(); } 84 | } 85 | }; 86 | } 87 | 88 | public static ViewAssertion recyclerViewItemCount(final int count) { 89 | return new ViewAssertion() { 90 | @Override 91 | public void check(View view, NoMatchingViewException noViewFoundException) { 92 | if(view instanceof RecyclerView) { 93 | RecyclerView recyclerView = (RecyclerView) view; 94 | if(recyclerView.getAdapter().getItemCount() != count) { 95 | throw new AssertionFailedError("RecyclerView with id=" + recyclerView.getId() + " has " + recyclerView.getAdapter().getItemCount() + " items, expected " + count + " items"); 96 | } 97 | } else { 98 | throw new AssertionFailedError("View is not a RecyclerView"); 99 | } 100 | } 101 | }; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/CountriesApp.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries 2 | 3 | import android.app.Application 4 | import android.content.res.Resources 5 | import com.patloew.countries.injection.components.AppComponent 6 | import com.patloew.countries.injection.components.DaggerAppComponent 7 | import com.patloew.countries.injection.modules.AppModule 8 | import com.patloew.countries.util.* 9 | import com.squareup.leakcanary.LeakCanary 10 | import io.reactivex.plugins.RxJavaPlugins 11 | import io.realm.Realm 12 | import io.realm.RealmConfiguration 13 | import paperparcel.Adapter 14 | import paperparcel.ProcessorConfig 15 | import timber.log.Timber 16 | 17 | /* Copyright 2016 Patrick Löwenstein 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. */ 30 | 31 | @ProcessorConfig( 32 | adapters = arrayOf( 33 | Adapter(RealmListPaperParcelTypeConverter::class), 34 | Adapter(ObservableFieldPaperParcelTypeConverter::class), 35 | Adapter(ObservableBooleanPaperParcelTypeConverter::class), 36 | Adapter(ObservableDoublePaperParcelTypeConverter::class), 37 | Adapter(ObservableFloatPaperParcelTypeConverter::class), 38 | Adapter(ObservableIntPaperParcelTypeConverter::class), 39 | Adapter(ObservableLongPaperParcelTypeConverter::class) 40 | ) 41 | ) 42 | class CountriesApp : Application() { 43 | 44 | override fun onCreate() { 45 | super.onCreate() 46 | if (LeakCanary.isInAnalyzerProcess(this)) return 47 | 48 | Timber.plant(Timber.DebugTree()) 49 | 50 | Realm.init(this) 51 | Realm.setDefaultConfiguration(RealmConfiguration.Builder().build()) 52 | 53 | instance = this 54 | appComponent = DaggerAppComponent.builder() 55 | .appModule(AppModule(this)) 56 | .build() 57 | 58 | RxJavaPlugins.setErrorHandler({ Timber.e(it) }) 59 | } 60 | 61 | companion object { 62 | 63 | lateinit var instance: CountriesApp 64 | private set 65 | 66 | lateinit var appComponent: AppComponent 67 | private set 68 | 69 | val realm: Realm 70 | get() = appComponent.realm() 71 | 72 | val res: Resources 73 | get() = instance.resources 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/local/CountryRepo.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.local 2 | 3 | import com.patloew.countries.data.model.Country 4 | import io.reactivex.Flowable 5 | 6 | import io.reactivex.Observable 7 | import io.realm.Sort 8 | 9 | /* Copyright 2016 Patrick Löwenstein 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. */ 22 | interface CountryRepo { 23 | val favoriteChangeObservable: Observable 24 | 25 | fun findAllSorted(sortField: String?, sort: Sort, detached: Boolean): List 26 | fun findAllSortedWithChanges(sortField: String?, sort: Sort): Flowable> 27 | 28 | fun getByField(field: String?, value: String?, detached: Boolean): Country? 29 | 30 | fun save(country: Country) 31 | fun delete(country: Country) 32 | 33 | fun detach(country: Country): Country 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/local/PrefRepo.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.local 2 | 3 | /* Copyright 2017 Tailored Media GmbH 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. */ 16 | interface PrefRepo { 17 | var realmEncryptionKey: ByteArray? 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/local/RealmCountryRepo.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.local 2 | 3 | import com.patloew.countries.data.model.Country 4 | import com.patloew.countries.injection.scopes.PerApplication 5 | import io.reactivex.Flowable 6 | import io.reactivex.Observable 7 | import io.reactivex.subjects.PublishSubject 8 | import io.realm.Realm 9 | import io.realm.Sort 10 | import javax.inject.Inject 11 | import javax.inject.Provider 12 | 13 | /* Copyright 2016 Patrick Löwenstein 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. */ 26 | @PerApplication 27 | class RealmCountryRepo 28 | @Inject 29 | constructor(private val realmProvider: Provider) : CountryRepo { 30 | 31 | private val favoriteChangeSubject = PublishSubject.create() 32 | override val favoriteChangeObservable: Observable 33 | get() = favoriteChangeSubject 34 | 35 | override fun findAllSorted(sortField: String?, sort: Sort, detached: Boolean): List { 36 | realmProvider.get().use { realm -> 37 | val realmResults = realm.where(Country::class.java).findAllSorted(sortField, sort) 38 | 39 | if (detached) { 40 | return realm.copyFromRealm(realmResults) 41 | } else { 42 | return realmResults 43 | } 44 | } 45 | } 46 | 47 | override fun findAllSortedWithChanges(sortField: String?, sort: Sort): Flowable> { 48 | realmProvider.get().use { realm -> 49 | return realm.where(Country::class.java).findAllSortedAsync(sortField, sort).asFlowable() 50 | .filter{ it.isLoaded } 51 | .map { it } 52 | } 53 | } 54 | 55 | override fun getByField(field: String?, value: String?, detached: Boolean): Country? { 56 | realmProvider.get().use { realm -> 57 | var realmCountry: Country? = realm.where(Country::class.java).equalTo(field, value).findFirst() 58 | if (detached && realmCountry != null) { 59 | realmCountry = realm.copyFromRealm(realmCountry) 60 | } 61 | return realmCountry 62 | } 63 | } 64 | 65 | override fun save(country: Country) { 66 | realmProvider.get().use { realm -> 67 | realm.executeTransaction { r -> r.copyToRealmOrUpdate(country) } 68 | favoriteChangeSubject.onNext(country.alpha2Code!!) 69 | } 70 | } 71 | 72 | override fun delete(realmCountry: Country) { 73 | if (realmCountry.isValid) { 74 | realmProvider.get().use { realm -> 75 | val alpha2Code = realmCountry.alpha2Code 76 | 77 | realm.executeTransaction { r -> 78 | realmCountry.borders?.deleteAllFromRealm() 79 | realmCountry.currencies?.deleteAllFromRealm() 80 | realmCountry.languages?.deleteAllFromRealm() 81 | realmCountry.translations?.deleteAllFromRealm() 82 | realmCountry.deleteFromRealm() 83 | } 84 | 85 | favoriteChangeSubject.onNext(alpha2Code!!) 86 | } 87 | } 88 | } 89 | 90 | override fun detach(country: Country): Country { 91 | if (country.isManaged) { 92 | realmProvider.get().use { realm -> return realm.copyFromRealm(country) } 93 | } else { 94 | return country 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/local/SharedPrefRepo.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.local 2 | 3 | import com.patloew.countries.injection.qualifier.AppContext 4 | import com.patloew.countries.injection.scopes.PerApplication 5 | 6 | /* Copyright 2017 Tailored Media GmbH 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | @PerApplication 20 | class SharedPrefRepo @javax.inject.Inject 21 | constructor(@AppContext context: android.content.Context) : PrefRepo { 22 | 23 | private val prefs: android.content.SharedPreferences = android.preference.PreferenceManager.getDefaultSharedPreferences(context) 24 | 25 | override var realmEncryptionKey: ByteArray? 26 | get() { 27 | if (prefs.contains(com.patloew.countries.data.local.SharedPrefRepo.Companion.REALM_ENCRYPTION_KEY)) { 28 | return android.util.Base64.decode(prefs.getString(com.patloew.countries.data.local.SharedPrefRepo.Companion.REALM_ENCRYPTION_KEY, null), android.util.Base64.DEFAULT) 29 | } else { 30 | return null 31 | } 32 | } 33 | set(key) = prefs.edit().putString(com.patloew.countries.data.local.SharedPrefRepo.Companion.REALM_ENCRYPTION_KEY, android.util.Base64.encodeToString(key, android.util.Base64.DEFAULT)).apply() 34 | 35 | companion object { 36 | private val REALM_ENCRYPTION_KEY = "realm_encryption_key" 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/model/Country.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.model 2 | 3 | import io.realm.RealmList 4 | import io.realm.RealmObject 5 | import io.realm.annotations.PrimaryKey 6 | import paperparcel.PaperParcel 7 | import paperparcel.PaperParcelable 8 | 9 | /* Copyright 2016 Patrick Löwenstein 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. */ 22 | @PaperParcel 23 | open class Country : RealmObject(), Comparable, PaperParcelable { 24 | 25 | companion object { 26 | @JvmField val CREATOR = PaperParcelCountry.CREATOR 27 | } 28 | 29 | @PrimaryKey 30 | open var alpha2Code: String? = null 31 | open var alpha3Code: String? = null 32 | open var name: String? = null 33 | open var nativeName: String? = null 34 | open var region: String? = null 35 | open var capital: String? = null 36 | open var currencies: RealmList? = null 37 | open var borders: RealmList? = null 38 | open var languages: RealmList? = null 39 | open var translations: RealmList? = null 40 | open var population: Int? = null 41 | open var lat: Float? = null 42 | open var lng: Float? = null 43 | 44 | override fun compareTo(other: Country): Int { 45 | if (name != null && other.name != null) { 46 | return name!!.compareTo(other.name!!) 47 | } else { 48 | return 0 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/model/RealmStringMapEntry.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.model 2 | 3 | import io.realm.RealmObject 4 | import paperparcel.PaperParcel 5 | import paperparcel.PaperParcelable 6 | 7 | /* Copyright 2016 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | @PaperParcel 21 | open class RealmStringMapEntry : RealmObject(), PaperParcelable { 22 | 23 | companion object { 24 | @JvmField val CREATOR = PaperParcelRealmStringMapEntry.CREATOR 25 | } 26 | 27 | open var key: String? = null 28 | open var value: String? = null 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/data/remote/CountryApi.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.data.remote 2 | 3 | import com.patloew.countries.data.model.Country 4 | 5 | import io.reactivex.Single 6 | import retrofit2.http.GET 7 | 8 | /* Copyright 2016 Patrick Löwenstein 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. */ 21 | interface CountryApi { 22 | 23 | @GET("rest/v1/all") 24 | fun getAllCountries() : Single> 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/components/ActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.components 2 | 3 | import android.content.Context 4 | import android.support.v4.app.FragmentManager 5 | import com.patloew.countries.injection.modules.ActivityModule 6 | import com.patloew.countries.injection.modules.ViewModelModule 7 | import com.patloew.countries.injection.qualifier.ActivityContext 8 | import com.patloew.countries.injection.qualifier.ActivityFragmentManager 9 | import com.patloew.countries.injection.scopes.PerActivity 10 | import com.patloew.countries.ui.base.feedback.Snacker 11 | import com.patloew.countries.ui.base.navigator.Navigator 12 | import com.patloew.countries.ui.detail.DetailActivity 13 | import com.patloew.countries.ui.main.MainActivity 14 | import dagger.Component 15 | 16 | /* Copyright 2016 Patrick Löwenstein 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); 19 | * you may not use this file except in compliance with the License. 20 | * You may obtain a copy of the License at 21 | * 22 | * http://www.apache.org/licenses/LICENSE-2.0 23 | * 24 | * Unless required by applicable law or agreed to in writing, software 25 | * distributed under the License is distributed on an "AS IS" BASIS, 26 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | * See the License for the specific language governing permissions and 28 | * limitations under the License. 29 | * 30 | * ------ 31 | * 32 | * FILE MODIFIED 2017 Tailored Media GmbH */ 33 | @PerActivity 34 | @Component(dependencies = arrayOf(AppComponent::class), modules = arrayOf(ActivityModule::class, ViewModelModule::class)) 35 | interface ActivityComponent : ActivityComponentProvides { 36 | // create inject methods for your Activities here 37 | 38 | fun inject(activity: MainActivity) 39 | fun inject(activity: DetailActivity) 40 | 41 | } 42 | 43 | interface ActivityComponentProvides : AppComponentProvides { 44 | @ActivityContext fun activityContext(): Context 45 | @ActivityFragmentManager fun defaultFragmentManager(): FragmentManager 46 | fun navigator(): Navigator 47 | fun snacker(): Snacker 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/components/ActivityViewHolderComponent.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.components 2 | 3 | import com.patloew.countries.injection.modules.ViewHolderModule 4 | import com.patloew.countries.injection.modules.ViewModelModule 5 | import com.patloew.countries.injection.scopes.PerViewHolder 6 | import com.patloew.countries.ui.main.recyclerview.CountryViewHolder 7 | 8 | import dagger.Component 9 | 10 | /* Copyright 2017 Tailored Media GmbH 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. */ 23 | @PerViewHolder 24 | @Component(dependencies = arrayOf(ActivityComponent::class), modules = arrayOf(ViewHolderModule::class, ViewModelModule::class)) 25 | interface ActivityViewHolderComponent { 26 | fun inject(viewHolder: CountryViewHolder) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/components/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.components 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import com.patloew.countries.data.local.CountryRepo 6 | import com.patloew.countries.data.local.PrefRepo 7 | import com.patloew.countries.data.remote.CountryApi 8 | import com.patloew.countries.injection.modules.AppModule 9 | import com.patloew.countries.injection.modules.DataModule 10 | import com.patloew.countries.injection.modules.NetModule 11 | import com.patloew.countries.injection.qualifier.AppContext 12 | import com.patloew.countries.injection.scopes.PerApplication 13 | import com.patloew.countries.ui.base.feedback.Toaster 14 | import com.squareup.leakcanary.RefWatcher 15 | import dagger.Component 16 | import io.realm.Realm 17 | 18 | 19 | /* Copyright 2016 Patrick Löwenstein 20 | * 21 | * Licensed under the Apache License, Version 2.0 (the "License"); 22 | * you may not use this file except in compliance with the License. 23 | * You may obtain a copy of the License at 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software 28 | * distributed under the License is distributed on an "AS IS" BASIS, 29 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | * See the License for the specific language governing permissions and 31 | * limitations under the License. 32 | * 33 | * ------ 34 | * 35 | * FILE MODIFIED 2017 Tailored Media GmbH */ 36 | @PerApplication 37 | @Component(modules = arrayOf(AppModule::class, NetModule::class, DataModule::class)) 38 | interface AppComponent : AppComponentProvides { 39 | 40 | } 41 | 42 | interface AppComponentProvides { 43 | @AppContext fun appContext(): Context 44 | fun resources(): Resources 45 | fun refWatcher(): RefWatcher 46 | 47 | fun realm(): Realm 48 | fun countryRepo(): CountryRepo 49 | fun prefRepo(): PrefRepo 50 | fun countryApi(): CountryApi 51 | 52 | fun toaster(): Toaster 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/components/FragmentComponent.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.components 2 | 3 | import com.patloew.countries.injection.modules.FragmentModule 4 | import com.patloew.countries.injection.modules.ViewModelModule 5 | import com.patloew.countries.injection.scopes.PerFragment 6 | import com.patloew.countries.ui.main.viewpager.all.AllCountriesFragment 7 | import com.patloew.countries.ui.main.viewpager.favorites.FavoriteCountriesFragment 8 | 9 | import dagger.Component 10 | 11 | /* Copyright 2016 Patrick Löwenstein 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | * 25 | * ------ 26 | * 27 | * FILE MODIFIED 2017 Tailored Media GmbH */ 28 | @PerFragment 29 | @Component(dependencies = arrayOf(ActivityComponent::class), modules = arrayOf(FragmentModule::class, ViewModelModule::class)) 30 | interface FragmentComponent : FragmentComponentProvides { 31 | // create inject methods for your Fragments here 32 | 33 | fun inject(fragment: AllCountriesFragment) 34 | fun inject(fragment: FavoriteCountriesFragment) 35 | } 36 | 37 | interface FragmentComponentProvides : ActivityComponentProvides { 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/components/FragmentViewHolderComponent.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.components 2 | 3 | import com.patloew.countries.injection.modules.ViewHolderModule 4 | import com.patloew.countries.injection.modules.ViewModelModule 5 | import com.patloew.countries.injection.scopes.PerViewHolder 6 | import dagger.Component 7 | 8 | /* Copyright 2017 Tailored Media GmbH 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. */ 21 | @PerViewHolder 22 | @Component(dependencies = arrayOf(FragmentComponent::class), modules = arrayOf(ViewHolderModule::class, ViewModelModule::class)) 23 | interface FragmentViewHolderComponent { 24 | 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import android.content.Context 4 | import android.support.v4.app.FragmentManager 5 | import android.support.v7.app.AppCompatActivity 6 | 7 | import com.patloew.countries.injection.qualifier.ActivityContext 8 | import com.patloew.countries.injection.qualifier.ActivityFragmentManager 9 | import com.patloew.countries.injection.scopes.PerActivity 10 | import com.patloew.countries.ui.base.feedback.ActivitySnacker 11 | import com.patloew.countries.ui.base.feedback.Snacker 12 | import com.patloew.countries.ui.base.navigator.ActivityNavigator 13 | import com.patloew.countries.ui.base.navigator.Navigator 14 | 15 | import dagger.Module 16 | import dagger.Provides 17 | 18 | /* Copyright 2016 Patrick Löwenstein 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. */ 31 | @Module 32 | class ActivityModule(private val activity: AppCompatActivity) { 33 | 34 | @Provides 35 | @PerActivity 36 | @ActivityContext 37 | internal fun provideActivityContext(): Context = activity 38 | 39 | @Provides 40 | @PerActivity 41 | @ActivityFragmentManager 42 | internal fun provideFragmentManager(): FragmentManager = activity.supportFragmentManager 43 | 44 | @Provides 45 | @PerActivity 46 | internal fun provideNavigator(): Navigator = ActivityNavigator(activity) 47 | 48 | @Provides 49 | @PerActivity 50 | internal fun provideSnacker(): Snacker = ActivitySnacker(activity) 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import com.patloew.countries.injection.qualifier.AppContext 7 | import com.patloew.countries.injection.scopes.PerApplication 8 | import com.patloew.countries.ui.base.feedback.ApplicationToaster 9 | import com.patloew.countries.ui.base.feedback.Toaster 10 | import com.squareup.leakcanary.LeakCanary 11 | import com.squareup.leakcanary.RefWatcher 12 | import dagger.Module 13 | import dagger.Provides 14 | import io.realm.Realm 15 | 16 | /* Copyright 2016 Patrick Löwenstein 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); 19 | * you may not use this file except in compliance with the License. 20 | * You may obtain a copy of the License at 21 | * 22 | * http://www.apache.org/licenses/LICENSE-2.0 23 | * 24 | * Unless required by applicable law or agreed to in writing, software 25 | * distributed under the License is distributed on an "AS IS" BASIS, 26 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | * See the License for the specific language governing permissions and 28 | * limitations under the License. 29 | * 30 | * ------ 31 | * 32 | * FILE MODIFIED 2017 Tailored Media GmbH */ 33 | @Module 34 | class AppModule(private val app: Application) { 35 | 36 | @Provides 37 | @PerApplication 38 | @AppContext 39 | internal fun provideAppContext(): Context = app 40 | 41 | @Provides 42 | @PerApplication 43 | internal fun provideResources(): Resources = app.resources 44 | 45 | @Provides 46 | @PerApplication 47 | internal fun provideRefWatcher(): RefWatcher = LeakCanary.install(app) 48 | 49 | @Provides 50 | internal fun provideRealm(): Realm = Realm.getDefaultInstance() 51 | 52 | @Provides 53 | @PerApplication 54 | internal fun provideToaster(): Toaster = ApplicationToaster(app) 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import com.patloew.countries.data.local.CountryRepo 4 | import com.patloew.countries.data.local.PrefRepo 5 | import com.patloew.countries.data.local.RealmCountryRepo 6 | import com.patloew.countries.data.local.SharedPrefRepo 7 | import dagger.Binds 8 | import dagger.Module 9 | 10 | /* Copyright 2016 Patrick Löwenstein 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. */ 23 | @Module 24 | abstract class DataModule { 25 | 26 | @Binds 27 | internal abstract fun bindCountryRepo(realmCountryRepo: RealmCountryRepo): CountryRepo 28 | 29 | @Binds 30 | internal abstract fun bindPrefRepo(repo: SharedPrefRepo): PrefRepo 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/FragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import android.support.v4.app.Fragment 4 | import android.support.v4.app.FragmentManager 5 | 6 | import com.patloew.countries.injection.qualifier.ChildFragmentManager 7 | import com.patloew.countries.injection.scopes.PerFragment 8 | import com.patloew.countries.ui.base.navigator.ChildFragmentNavigator 9 | import com.patloew.countries.ui.base.navigator.FragmentNavigator 10 | 11 | import dagger.Module 12 | import dagger.Provides 13 | 14 | /* Copyright 2016 Patrick Löwenstein 15 | * 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. */ 27 | @Module 28 | class FragmentModule(private val fragment: Fragment) { 29 | 30 | @Provides 31 | @PerFragment 32 | @ChildFragmentManager 33 | internal fun provideChildFragmentManager(): FragmentManager { 34 | return fragment.childFragmentManager 35 | } 36 | 37 | @Provides 38 | @PerFragment 39 | internal fun provideFragmentNavigator(): FragmentNavigator { 40 | return ChildFragmentNavigator(fragment) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/NetModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.reflect.TypeToken 6 | import com.patloew.countries.BuildConfig 7 | import com.patloew.countries.data.model.Country 8 | import com.patloew.countries.data.model.RealmStringMapEntry 9 | import com.patloew.countries.data.remote.CountryApi 10 | import com.patloew.countries.injection.scopes.PerApplication 11 | import com.patloew.countries.util.CountryTypeAdapter 12 | import com.patloew.countries.util.RealmStringListTypeAdapter 13 | import com.patloew.countries.util.RealmStringMapEntryListTypeAdapter 14 | import dagger.Module 15 | import dagger.Provides 16 | import io.reactivex.schedulers.Schedulers 17 | import io.realm.RealmList 18 | import okhttp3.OkHttpClient 19 | import okhttp3.logging.HttpLoggingInterceptor 20 | import retrofit2.Retrofit 21 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 22 | import retrofit2.converter.gson.GsonConverterFactory 23 | 24 | /* Copyright 2016 Patrick Löwenstein 25 | * 26 | * Licensed under the Apache License, Version 2.0 (the "License"); 27 | * you may not use this file except in compliance with the License. 28 | * You may obtain a copy of the License at 29 | * 30 | * http://www.apache.org/licenses/LICENSE-2.0 31 | * 32 | * Unless required by applicable law or agreed to in writing, software 33 | * distributed under the License is distributed on an "AS IS" BASIS, 34 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | * See the License for the specific language governing permissions and 36 | * limitations under the License. 37 | * 38 | * ------ 39 | * 40 | * FILE MODIFIED 2017 Tailored Media GmbH 41 | * */ 42 | @Module 43 | class NetModule() { 44 | 45 | @Provides 46 | @PerApplication 47 | internal fun provideGson(): Gson { 48 | return GsonBuilder() 49 | // Custom type adapters for models are not needed when using Gson, but this 50 | // type adapter is a good example if you want to write one yourself. 51 | .registerTypeAdapter(Country::class.java, CountryTypeAdapter.INSTANCE) 52 | // These type adapters for RealmLists are needed, since RealmString and RealmStringMapEntry 53 | // wrappers are not recognized by Gson in the default configuration. 54 | .registerTypeAdapter(object : TypeToken>() {}.type, RealmStringListTypeAdapter.INSTANCE) 55 | .registerTypeAdapter(object : TypeToken>() {}.type, RealmStringMapEntryListTypeAdapter.INSTANCE) 56 | .create() 57 | } 58 | 59 | @Provides 60 | @PerApplication 61 | internal fun provideOkHttpClient(): OkHttpClient { 62 | return OkHttpClient() 63 | } 64 | 65 | @Provides 66 | @PerApplication 67 | internal fun provideCountryApi(gson: Gson, okHttpClient: OkHttpClient): CountryApi { 68 | val httpClientBuilder = okHttpClient.newBuilder() 69 | 70 | if (BuildConfig.DEBUG) { 71 | val loggingInterceptor = HttpLoggingInterceptor() 72 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY 73 | httpClientBuilder.addInterceptor(loggingInterceptor) 74 | } 75 | 76 | return Retrofit.Builder() 77 | .baseUrl(BuildConfig.BASE_URL) 78 | .addConverterFactory(GsonConverterFactory.create(gson)) 79 | .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) 80 | .callFactory(httpClientBuilder.build()) 81 | .build().create(CountryApi::class.java) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/ViewHolderModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import dagger.Module 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Module 19 | class ViewHolderModule 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/modules/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.modules 2 | 3 | import com.patloew.countries.ui.detail.DetailMvvm 4 | import com.patloew.countries.ui.detail.DetailViewModel 5 | import com.patloew.countries.ui.main.recyclerview.CountryMvvm 6 | import com.patloew.countries.ui.main.recyclerview.CountryViewModel 7 | import com.patloew.countries.ui.main.viewpager.all.AllCountriesViewModel 8 | import com.patloew.countries.ui.main.viewpager.all.IAllCountriesViewModel 9 | import com.patloew.countries.ui.main.viewpager.favorites.FavoriteCountriesViewModel 10 | import com.patloew.countries.ui.main.viewpager.favorites.IFavoriteCountriesViewModel 11 | 12 | import dagger.Binds 13 | import dagger.Module 14 | 15 | /* Copyright 2016 Patrick Löwenstein 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software 24 | * distributed under the License is distributed on an "AS IS" BASIS, 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | * See the License for the specific language governing permissions and 27 | * limitations under the License. */ 28 | @Module 29 | abstract class ViewModelModule { 30 | 31 | // Activities 32 | 33 | @Binds 34 | internal abstract fun bindDetailViewModel(detailViewModel: DetailViewModel): DetailMvvm.ViewModel 35 | 36 | 37 | // Fragments 38 | 39 | @Binds 40 | internal abstract fun bindAllCountriesViewModel(allCountriesViewModel: AllCountriesViewModel): IAllCountriesViewModel 41 | 42 | @Binds 43 | internal abstract fun bindFavoriteCountriesViewModel(countryViewModel: FavoriteCountriesViewModel): IFavoriteCountriesViewModel 44 | 45 | 46 | // View Holders 47 | 48 | @Binds 49 | internal abstract fun bindCountryViewModel(countryViewModel: CountryViewModel): CountryMvvm.ViewModel 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/qualifier/ActivityContext.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.qualifier 2 | import javax.inject.Qualifier 3 | 4 | /* Copyright 2016 Patrick Löwenstein 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. */ 17 | 18 | @Qualifier 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class ActivityContext 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/qualifier/ActivityFragmentManager.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.qualifier 2 | 3 | import javax.inject.Qualifier 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Qualifier 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class ActivityFragmentManager 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/qualifier/AppContext.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.qualifier 2 | 3 | import javax.inject.Qualifier 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Qualifier 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class AppContext 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/qualifier/ChildFragmentManager.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.qualifier 2 | 3 | import javax.inject.Qualifier 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Qualifier 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class ChildFragmentManager 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/scopes/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.scopes 2 | 3 | 4 | import javax.inject.Scope 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | @Scope 20 | @Retention(AnnotationRetention.RUNTIME) 21 | annotation class PerActivity 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/scopes/PerApplication.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.scopes 2 | 3 | 4 | import javax.inject.Scope 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | @Scope 20 | @Retention(AnnotationRetention.RUNTIME) 21 | annotation class PerApplication 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/scopes/PerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.scopes 2 | 3 | import javax.inject.Scope 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Scope 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class PerFragment 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/injection/scopes/PerViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.injection.scopes 2 | 3 | import javax.inject.Scope 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | @Scope 19 | @Retention(AnnotationRetention.RUNTIME) 20 | annotation class PerViewHolder 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/BaseCountryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.databinding.Bindable 7 | import android.graphics.drawable.Drawable 8 | import android.net.Uri 9 | import android.support.v7.widget.AppCompatDrawableManager 10 | import android.text.SpannableStringBuilder 11 | import android.text.TextUtils 12 | import com.patloew.countries.BR 13 | import com.patloew.countries.R 14 | import com.patloew.countries.data.local.CountryRepo 15 | import com.patloew.countries.data.model.Country 16 | import com.patloew.countries.injection.qualifier.AppContext 17 | import com.patloew.countries.ui.base.navigator.Navigator 18 | import com.patloew.countries.ui.base.view.MvvmView 19 | import com.patloew.countries.ui.base.viewmodel.BaseViewModel 20 | import java.text.DecimalFormat 21 | import java.util.* 22 | 23 | /* Copyright 2016 Patrick Löwenstein 24 | * 25 | * Licensed under the Apache License, Version 2.0 (the "License"); 26 | * you may not use this file except in compliance with the License. 27 | * You may obtain a copy of the License at 28 | * 29 | * http://www.apache.org/licenses/LICENSE-2.0 30 | * 31 | * Unless required by applicable law or agreed to in writing, software 32 | * distributed under the License is distributed on an "AS IS" BASIS, 33 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | * See the License for the specific language governing permissions and 35 | * limitations under the License. 36 | * 37 | * ------- 38 | * 39 | * FILE MODIFIED 2017 Tailored Media GmbH 40 | * */ 41 | abstract class BaseCountryViewModel(@AppContext context: Context, protected val countryRepo: CountryRepo, protected val navigator: Navigator) : BaseViewModel(), ICountryViewModel { 42 | 43 | companion object { 44 | val DISPLAY_LOCALE = Locale("EN") 45 | val DECIMAL_FORMAT = DecimalFormat() 46 | } 47 | 48 | protected val ctx: Context = context.applicationContext 49 | protected val mapsAvailable: Boolean 50 | 51 | 52 | override lateinit var country: Country 53 | protected set 54 | 55 | private var isLast = false 56 | 57 | init { 58 | var mapsAvailable = false 59 | 60 | try { 61 | context.packageManager.getPackageInfo("com.google.android.apps.maps", 0) 62 | mapsAvailable = true 63 | } catch (ignore: PackageManager.NameNotFoundException) { } 64 | 65 | this.mapsAvailable = mapsAvailable 66 | } 67 | 68 | override fun onMapClick() { 69 | val gmmIntentUri = Uri.parse("geo:" + country.lat + "," + country.lng + "?q=" + country.name + "&z=2") 70 | navigator.startActivity(Intent.ACTION_VIEW, gmmIntentUri) 71 | } 72 | 73 | override fun onBookmarkClick() { 74 | val realmCountry = countryRepo.getByField("alpha2Code", country.alpha2Code, false) 75 | 76 | if (realmCountry == null) { 77 | countryRepo.save(country) 78 | } else { 79 | country = countryRepo.detach(realmCountry) 80 | countryRepo.delete(realmCountry) 81 | } 82 | 83 | notifyPropertyChanged(BR.bookmarkDrawable) 84 | } 85 | 86 | override fun update(country: Country, isLast: Boolean) { 87 | this.isLast = isLast 88 | this.country = country 89 | 90 | notifyChange() 91 | } 92 | 93 | override val name: String 94 | get() { 95 | var nameInfo = country.name + " (" + country.alpha2Code 96 | if (country.name != country.nativeName) { 97 | nameInfo += ", " + country.nativeName 98 | } 99 | return nameInfo + ")" 100 | } 101 | 102 | override val region: CharSequence 103 | get() = SpannableStringBuilder(ctx.getText(R.string.country_region)) 104 | .append(country.region) 105 | 106 | override val isCapitalVisible: Boolean 107 | get() = !TextUtils.isEmpty(country.capital) 108 | 109 | override val capital: CharSequence 110 | get() = SpannableStringBuilder(ctx.getText(R.string.country_capital)) 111 | .append(country.capital) 112 | 113 | override val population: CharSequence 114 | get() = SpannableStringBuilder(ctx.getText(R.string.country_population)) 115 | .append(DECIMAL_FORMAT.format(country.population)) 116 | 117 | override val isLocationVisible: Boolean 118 | get() = country.lat != null && country.lng != null 119 | 120 | override val location: CharSequence 121 | get() { 122 | if (isLocationVisible) { 123 | return SpannableStringBuilder(ctx.getText(R.string.country_location)) 124 | .append(DECIMAL_FORMAT.format(country.lat)) 125 | .append(", ") 126 | .append(DECIMAL_FORMAT.format(country.lng)) 127 | } else { 128 | return "" 129 | } 130 | } 131 | 132 | override val bookmarkDrawable: Drawable 133 | @Bindable 134 | get() = AppCompatDrawableManager.get().getDrawable(ctx, if (countryRepo.getByField("alpha2Code", country.alpha2Code!!, false) != null) R.drawable.ic_bookmark_black else R.drawable.ic_bookmark_border_black) 135 | 136 | override val isMapVisible: Boolean 137 | get() = mapsAvailable && country.lat != null && country.lng != null 138 | 139 | override val cardBottomMargin: Int 140 | get() = if (isLast) ctx.resources.getDimension(R.dimen.card_outer_padding).toInt() else 0 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/ICountryViewModel.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2016 Patrick Löwenstein 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | * 15 | * FILE MODIFIED 2017 Tailored Media GmbH */ 16 | 17 | package com.patloew.countries.ui 18 | 19 | import android.graphics.drawable.Drawable 20 | import com.patloew.countries.data.model.Country 21 | import com.patloew.countries.ui.base.view.MvvmView 22 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 23 | 24 | interface ICountryViewModel : MvvmViewModel { 25 | 26 | fun onMapClick() 27 | fun onBookmarkClick() 28 | fun update(country: Country, isLast: Boolean) 29 | 30 | // Properties 31 | 32 | val country: Country? 33 | val name: String 34 | val region: CharSequence 35 | val isCapitalVisible: Boolean 36 | val capital: CharSequence 37 | val population: CharSequence 38 | val isLocationVisible: Boolean 39 | val location: CharSequence 40 | val bookmarkDrawable: Drawable 41 | val isMapVisible: Boolean 42 | val cardBottomMargin: Int 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.databinding.ViewDataBinding 5 | import android.os.Bundle 6 | import android.support.annotation.CallSuper 7 | import android.support.annotation.LayoutRes 8 | import android.support.v7.app.AppCompatActivity 9 | import com.patloew.countries.BR 10 | import com.patloew.countries.CountriesApp 11 | import com.patloew.countries.injection.components.ActivityComponent 12 | import com.patloew.countries.injection.components.DaggerActivityComponent 13 | import com.patloew.countries.injection.modules.ActivityModule 14 | import com.patloew.countries.ui.base.view.MvvmView 15 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 16 | import com.patloew.countries.util.extensions.attachViewOrThrowRuntimeException 17 | import com.squareup.leakcanary.RefWatcher 18 | import io.realm.Realm 19 | import javax.inject.Inject 20 | 21 | /* Copyright 2016 Patrick Löwenstein 22 | * 23 | * Licensed under the Apache License, Version 2.0 (the "License"); 24 | * you may not use this file except in compliance with the License. 25 | * You may obtain a copy of the License at 26 | * 27 | * http://www.apache.org/licenses/LICENSE-2.0 28 | * 29 | * Unless required by applicable law or agreed to in writing, software 30 | * distributed under the License is distributed on an "AS IS" BASIS, 31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | * See the License for the specific language governing permissions and 33 | * limitations under the License. 34 | * 35 | * ------- 36 | * 37 | * FILE MODIFIED 2017 Tailored Media GmbH 38 | * */ 39 | 40 | /* Base class for Activities when using a view model with data binding. 41 | * This class provides the binding and the view model to the subclass. The 42 | * view model is injected and the binding is created when the content view is set. 43 | * Each subclass therefore has to call the following code in onCreate(): 44 | * setAndBindContentView(savedInstanceState, R.layout.my_activity_layout) 45 | * 46 | * After calling this method, the binding and the view model is initialized. 47 | * saveInstanceState() and restoreInstanceState() methods of the view model 48 | * are automatically called in the appropriate lifecycle events when above calls 49 | * are made. 50 | * 51 | * Your subclass must implement the MvvmView implementation that you use in your 52 | * view model. */ 53 | abstract class BaseActivity> : AppCompatActivity(), MvvmView { 54 | 55 | 56 | // Inject a Realm INSTANCE into every Activity, since the INSTANCE 57 | // is cached and reused for a thread (avoids create/destroy overhead) 58 | @Inject protected lateinit var realm: Realm 59 | 60 | protected lateinit var binding: B 61 | @Inject protected lateinit var viewModel: VM 62 | 63 | @Inject 64 | protected lateinit var refWatcher: RefWatcher 65 | 66 | internal val activityComponent: ActivityComponent by lazy { 67 | DaggerActivityComponent.builder() 68 | .activityModule(ActivityModule(this)) 69 | .appComponent(CountriesApp.appComponent) 70 | .build() 71 | } 72 | 73 | @CallSuper 74 | override fun onSaveInstanceState(outState: Bundle) { 75 | super.onSaveInstanceState(outState) 76 | viewModel.saveInstanceState(outState) 77 | } 78 | 79 | @CallSuper 80 | override fun onCreate(savedInstanceState: Bundle?) { 81 | super.onCreate(savedInstanceState) 82 | 83 | try { 84 | ActivityComponent::class.java.getDeclaredMethod("inject", this::class.java).invoke(activityComponent, this) 85 | } catch(e: NoSuchMethodException) { 86 | throw RtfmException("You forgot to add \"fun inject(activity: ${this::class.java.simpleName})\" in ActivityComponent") 87 | } 88 | } 89 | 90 | @CallSuper 91 | override fun onDestroy() { 92 | super.onDestroy() 93 | viewModel.detachView() 94 | refWatcher.watch(activityComponent) 95 | refWatcher.watch(viewModel) 96 | realm.close() 97 | } 98 | 99 | /* Sets the content view, creates the binding and attaches the view to the view model */ 100 | protected fun setAndBindContentView(savedInstanceState: Bundle?, @LayoutRes layoutResID: Int) { 101 | binding = DataBindingUtil.setContentView(this, layoutResID) 102 | binding.setVariable(BR.vm, viewModel) 103 | viewModel.attachViewOrThrowRuntimeException(this, savedInstanceState) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/BaseActivityViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.databinding.ViewDataBinding 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.View 7 | import com.patloew.countries.BR 8 | import com.patloew.countries.injection.components.ActivityViewHolderComponent 9 | import com.patloew.countries.injection.components.DaggerActivityViewHolderComponent 10 | import com.patloew.countries.ui.base.view.MvvmView 11 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 12 | import com.patloew.countries.util.extensions.attachViewOrThrowRuntimeException 13 | import com.patloew.countries.util.extensions.castWithUnwrap 14 | import javax.inject.Inject 15 | 16 | /* Copyright 2016 Patrick Löwenstein 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); 19 | * you may not use this file except in compliance with the License. 20 | * You may obtain a copy of the License at 21 | * 22 | * http://www.apache.org/licenses/LICENSE-2.0 23 | * 24 | * Unless required by applicable law or agreed to in writing, software 25 | * distributed under the License is distributed on an "AS IS" BASIS, 26 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | * See the License for the specific language governing permissions and 28 | * limitations under the License. 29 | * 30 | * ------ 31 | * 32 | * FILE MODIFIED 2017 Tailored Media GmbH 33 | */ 34 | 35 | /* Base class for ViewHolders when using a view model in an Activity with data binding. 36 | * This class provides the binding and the view model to the subclass. The 37 | * view model is injected and the binding is created when the content view is bound. 38 | * Each subclass therefore has to call the following code in the constructor: 39 | * bindContentView(view) 40 | * 41 | * After calling this method, the binding and the view model is initialized. 42 | * saveInstanceState() and restoreInstanceState() are not called/used for ViewHolder 43 | * view models. 44 | * 45 | * Your subclass must implement the MvvmView implementation that you use in your 46 | * view model. */ 47 | abstract class BaseActivityViewHolder>(itemView: View) : RecyclerView.ViewHolder(itemView), MvvmView { 48 | 49 | protected lateinit var binding: B 50 | @Inject lateinit var viewModel: VM 51 | protected set 52 | 53 | protected val viewHolderComponent: ActivityViewHolderComponent by lazy { 54 | DaggerActivityViewHolderComponent.builder() 55 | .activityComponent(itemView.context.castWithUnwrap>()?.activityComponent) 56 | .build() 57 | } 58 | 59 | protected fun bindContentView(view: View) { 60 | try { 61 | ActivityViewHolderComponent::class.java.getDeclaredMethod("inject", this::class.java).invoke(viewHolderComponent, this) 62 | } catch(e: NoSuchMethodException) { 63 | throw RtfmException("You forgot to add \"fun inject(viewHolder: ${this::class.java.simpleName})\" in ActivityViewHolderComponent") 64 | } 65 | 66 | binding = DataBindingUtil.bind(view) 67 | binding.setVariable(BR.vm, viewModel) 68 | viewModel.attachViewOrThrowRuntimeException(this, null) 69 | } 70 | 71 | fun executePendingBindings() { 72 | binding.executePendingBindings() 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base 2 | 3 | import android.databinding.DataBindingUtil 4 | import android.databinding.ViewDataBinding 5 | import android.os.Bundle 6 | import android.support.annotation.CallSuper 7 | import android.support.annotation.LayoutRes 8 | import android.support.v4.app.Fragment 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import com.patloew.countries.BR 13 | import com.patloew.countries.injection.components.DaggerFragmentComponent 14 | import com.patloew.countries.injection.components.FragmentComponent 15 | import com.patloew.countries.injection.modules.FragmentModule 16 | import com.patloew.countries.injection.scopes.PerFragment 17 | import com.patloew.countries.ui.base.view.MvvmView 18 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 19 | import com.patloew.countries.util.extensions.attachViewOrThrowRuntimeException 20 | import com.squareup.leakcanary.RefWatcher 21 | import javax.inject.Inject 22 | 23 | /* Copyright 2016 Patrick Löwenstein 24 | * 25 | * Licensed under the Apache License, Version 2.0 (the "License"); 26 | * you may not use this file except in compliance with the License. 27 | * You may obtain a copy of the License at 28 | * 29 | * http://www.apache.org/licenses/LICENSE-2.0 30 | * 31 | * Unless required by applicable law or agreed to in writing, software 32 | * distributed under the License is distributed on an "AS IS" BASIS, 33 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | * See the License for the specific language governing permissions and 35 | * limitations under the License. 36 | * 37 | * ------ 38 | * 39 | * FILE MODIFIED 2017 Tailored Media GmbH */ 40 | 41 | /* Base class for Fragments when using a view model with data binding. 42 | * This class provides the binding and the view model to the subclass. The 43 | * view model is injected and the binding is created when the content view is set. 44 | * Each subclass therefore has to call the following code in onCreateView(): 45 | * return setAndBindContentView(inflater, container, savedInstanceState, R.layout.my_fragment_layout) 46 | * 47 | * After calling this method, the binding and the view model is initialized. 48 | * saveInstanceState() and restoreInstanceState() methods of the view model 49 | * are automatically called in the appropriate lifecycle events when above calls 50 | * are made. 51 | * 52 | * Your subclass must implement the MvvmView implementation that you use in your 53 | * view model. */ 54 | abstract class BaseFragment> : Fragment(), MvvmView { 55 | 56 | protected lateinit var binding: B 57 | @Inject protected lateinit var viewModel: VM 58 | @Inject protected lateinit var refWatcher: RefWatcher 59 | 60 | 61 | internal val fragmentComponent : FragmentComponent by lazy { 62 | DaggerFragmentComponent.builder() 63 | .fragmentModule(FragmentModule(this)) 64 | .activityComponent((activity as BaseActivity<*, *>).activityComponent) 65 | .build() 66 | } 67 | 68 | @CallSuper 69 | override fun onSaveInstanceState(outState: Bundle) { 70 | super.onSaveInstanceState(outState) 71 | viewModel.saveInstanceState(outState) 72 | } 73 | 74 | @CallSuper 75 | override fun onCreate(savedInstanceState: Bundle?) { 76 | super.onCreate(savedInstanceState) 77 | 78 | try { 79 | FragmentComponent::class.java.getDeclaredMethod("inject", this::class.java).invoke(fragmentComponent, this) 80 | } catch(e: NoSuchMethodException) { 81 | throw RtfmException("You forgot to add \"fun inject(fragment: ${this::class.java.simpleName})\" in FragmentComponent") 82 | } 83 | } 84 | 85 | @CallSuper 86 | override fun onDestroyView() { 87 | super.onDestroyView() 88 | viewModel.detachView() 89 | if (!viewModel.javaClass.isAnnotationPresent(PerFragment::class.java)) { 90 | refWatcher.watch(viewModel) 91 | } 92 | } 93 | 94 | @CallSuper 95 | override fun onDestroy() { 96 | super.onDestroy() 97 | refWatcher.watch(this) 98 | refWatcher.watch(fragmentComponent) 99 | } 100 | 101 | 102 | /* Sets the content view, creates the binding and attaches the view to the view model */ 103 | protected fun setAndBindContentView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?, @LayoutRes layoutResID: Int): View { 104 | binding = DataBindingUtil.inflate(inflater, layoutResID, container, false) 105 | binding.setVariable(BR.vm, viewModel) 106 | viewModel.attachViewOrThrowRuntimeException(this, savedInstanceState) 107 | return binding.root 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/BaseFragmentViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base 2 | 3 | import android.content.Context 4 | import android.databinding.DataBindingUtil 5 | import android.databinding.ViewDataBinding 6 | import android.support.v4.app.Fragment 7 | import android.support.v4.app.FragmentActivity 8 | import android.support.v7.widget.RecyclerView 9 | import android.view.View 10 | import com.patloew.countries.BR 11 | import com.patloew.countries.injection.components.DaggerFragmentViewHolderComponent 12 | import com.patloew.countries.injection.components.FragmentViewHolderComponent 13 | import com.patloew.countries.ui.base.view.MvvmView 14 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 15 | import com.patloew.countries.util.extensions.attachViewOrThrowRuntimeException 16 | import com.patloew.countries.util.extensions.castWithUnwrap 17 | import javax.inject.Inject 18 | 19 | /* Base class for ViewHolders when using a view model in a Fragment with data binding. 20 | * This class provides the binding and the view model to the subclass. The 21 | * view model is injected and the binding is created when the content view is bound. 22 | * Each subclass therefore has to call the following code in the constructor: 23 | * bindContentView(view) 24 | * 25 | * After calling this method, the binding and the view model is initialized. 26 | * saveInstanceState() and restoreInstanceState() are not called/used for ViewHolder 27 | * view models. 28 | * 29 | * Your subclass must implement the MvvmView implementation that you use in your 30 | * view model. */ 31 | abstract class BaseFragmentViewHolder>(itemView: View) : RecyclerView.ViewHolder(itemView), MvvmView { 32 | 33 | protected lateinit var binding: B 34 | @Inject lateinit var viewModel: VM 35 | protected set 36 | 37 | protected abstract val fragmentContainerId : Int 38 | 39 | protected val viewHolderComponent: FragmentViewHolderComponent by lazy { 40 | DaggerFragmentViewHolderComponent.builder() 41 | .fragmentComponent(itemView.context.getFragment>(fragmentContainerId)!!.fragmentComponent) 42 | .build() 43 | } 44 | 45 | protected fun bindContentView(view: View) { 46 | try { 47 | FragmentViewHolderComponent::class.java.getDeclaredMethod("inject", this::class.java).invoke(viewHolderComponent, this) 48 | } catch(e: NoSuchMethodException) { 49 | throw RtfmException("You forgot to add \"fun inject(viewHolder: ${this::class.java.simpleName})\" in FragmentViewHolderComponent") 50 | } 51 | 52 | binding = DataBindingUtil.bind(view) 53 | binding.setVariable(BR.vm, viewModel) 54 | viewModel.attachViewOrThrowRuntimeException(this, null) 55 | } 56 | 57 | private inline fun Context.getFragment(containerId: Int) = 58 | castWithUnwrap()?.run { supportFragmentManager.findFragmentById(containerId) as? T } 59 | 60 | fun executePendingBindings() { 61 | binding.executePendingBindings() 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/RtfmException.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License.*/ 14 | 15 | package com.patloew.countries.ui.base 16 | 17 | /* 18 | * Well ... next time please read the manual/readme before doing stuff ;) 19 | * https://github.com/tailoredmedia/AndroidTemplate 20 | */ 21 | class RtfmException(message: String) : NoSuchMethodException(message) -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/feedback/ActivitySnacker.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License.*/ 14 | 15 | package com.patloew.countries.ui.base.feedback 16 | 17 | import android.support.annotation.StringRes 18 | import android.support.design.widget.CoordinatorLayout 19 | import android.support.design.widget.Snackbar 20 | import android.support.v4.app.FragmentActivity 21 | import android.view.ViewGroup 22 | 23 | open class ActivitySnacker(val activity: FragmentActivity) : Snacker { 24 | 25 | private var actionSnackbar: Snackbar? = null 26 | private var snackbar: Snackbar? = null 27 | 28 | override fun show(title: CharSequence) = showInternal(title, null, null) 29 | override fun show(@StringRes titleRes: Int) = showInternal(activity.getString(titleRes), null, null) 30 | 31 | override fun show(title: CharSequence, actionText: CharSequence, action: () -> Unit) = showInternal(title, action, actionText) 32 | override fun show(titleRes: Int, actionTextRes: Int, action: () -> Unit) = showInternal(activity.getString(titleRes), action, activity.getString(actionTextRes)) 33 | 34 | 35 | private fun showInternal(title: CharSequence, action: (() -> Unit)?, actionText: CharSequence?) { 36 | hideSnack() 37 | 38 | val coordinator = activity.findViewById(android.R.id.content).getChildAt(0) 39 | val attachView = if (coordinator != null && coordinator is CoordinatorLayout) coordinator else activity.findViewById(android.R.id.content) 40 | 41 | if (action != null && actionText != null) { 42 | actionSnackbar = Snackbar.make(attachView, title, Snackbar.LENGTH_INDEFINITE) 43 | .setAction(actionText) { action() } 44 | .apply { show() } 45 | } else { 46 | snackbar = Snackbar.make(attachView, title, Snackbar.LENGTH_LONG) 47 | .apply { show() } 48 | } 49 | } 50 | 51 | override fun hideSnack() { 52 | actionSnackbar?.dismiss() 53 | actionSnackbar = null 54 | snackbar?.dismiss() 55 | snackbar = null 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/feedback/ApplicationToaster.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License.*/ 14 | 15 | package com.patloew.countries.ui.base.feedback 16 | 17 | import android.content.Context 18 | import android.support.annotation.StringRes 19 | import android.widget.Toast 20 | 21 | open class ApplicationToaster(val context: Context) : Toaster { 22 | 23 | private var toast: Toast? = null 24 | 25 | override fun show(title: String, duration: Int) { 26 | showInternal(title, duration) 27 | } 28 | 29 | override fun show(@StringRes titleRes: Int, duration: Int) { 30 | showInternal(context.getString(titleRes), duration) 31 | } 32 | 33 | private fun showInternal(title: String, duration: Int) { 34 | toast?.cancel() 35 | toast = null 36 | toast = Toast.makeText(context, title, if (duration != Toast.LENGTH_LONG || duration != Toast.LENGTH_SHORT) duration else Toast.LENGTH_SHORT) 37 | .apply { show() } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/feedback/Snacker.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License.*/ 14 | 15 | package com.patloew.countries.ui.base.feedback 16 | 17 | import android.support.annotation.StringRes 18 | 19 | 20 | interface Snacker { 21 | 22 | fun show(title: CharSequence) 23 | fun show(@StringRes titleRes: Int) 24 | 25 | fun show(title: CharSequence, actionText: CharSequence, action: (() -> Unit)) 26 | fun show(@StringRes titleRes: Int, @StringRes actionTextRes: Int, action: (() -> Unit)) 27 | 28 | fun hideSnack() 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/feedback/Toaster.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License.*/ 14 | 15 | package com.patloew.countries.ui.base.feedback 16 | 17 | import android.support.annotation.StringRes 18 | import android.widget.Toast 19 | 20 | 21 | interface Toaster { 22 | 23 | fun show(title: String, duration: Int = Toast.LENGTH_LONG) 24 | fun show(@StringRes titleRes: Int, duration: Int = Toast.LENGTH_LONG) 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/navigator/ActivityNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.navigator 2 | 3 | import android.app.Activity 4 | import android.app.DialogFragment 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.support.annotation.IdRes 8 | import android.support.v4.app.Fragment 9 | import android.support.v4.app.FragmentActivity 10 | import android.support.v4.app.FragmentManager 11 | 12 | /* Copyright 2016 Patrick Löwenstein 13 | * 14 | * Licensed under the Apache License, Version 2.0 (the "License"); 15 | * you may not use this file except in compliance with the License. 16 | * You may obtain a copy of the License at 17 | * 18 | * http://www.apache.org/licenses/LICENSE-2.0 19 | * 20 | * Unless required by applicable law or agreed to in writing, software 21 | * distributed under the License is distributed on an "AS IS" BASIS, 22 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | * See the License for the specific language governing permissions and 24 | * limitations under the License. 25 | * 26 | * 27 | * ------ 28 | * 29 | * FILE CHANGED 2017 Tailored Media GmbH 30 | * 31 | */ 32 | open class ActivityNavigator(protected val activity: FragmentActivity) : Navigator { 33 | 34 | override fun finishActivity() { 35 | activity.finish() 36 | } 37 | 38 | override fun finishActivityWithResult(resultCode: Int, resultIntentFun: (Intent.() -> Unit)?) { 39 | val intent = resultIntentFun?.let { Intent().apply(it) } 40 | activity.setResult(resultCode, intent) 41 | activity.finish() 42 | } 43 | 44 | override fun finishAffinity() { 45 | activity.finishAffinity() 46 | } 47 | 48 | override fun startActivity(intent: Intent) { 49 | activity.startActivity(intent) 50 | } 51 | 52 | override fun startActivity(action: String, uri: Uri?) { 53 | activity.startActivity(Intent(action, uri)) 54 | } 55 | 56 | override fun startActivity(activityClass: Class, adaptIntentFun: (Intent.() -> Unit)?) { 57 | startActivityInternal(activityClass, null, adaptIntentFun) 58 | } 59 | 60 | override fun startActivityForResult(activityClass: Class, requestCode: Int, adaptIntentFun: (Intent.() -> Unit)?) { 61 | startActivityInternal(activityClass, requestCode, adaptIntentFun) 62 | } 63 | 64 | private fun startActivityInternal(activityClass: Class, requestCode: Int?, adaptIntentFun: (Intent.() -> Unit)?) { 65 | val intent = Intent(activity, activityClass) 66 | adaptIntentFun?.invoke(intent) 67 | 68 | if (requestCode != null) { 69 | activity.startActivityForResult(intent, requestCode) 70 | } else { 71 | activity.startActivity(intent) 72 | } 73 | } 74 | 75 | override fun replaceFragment(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String?) { 76 | replaceFragmentInternal(activity.supportFragmentManager, containerId, fragment, fragmentTag, false, null) 77 | } 78 | 79 | override fun replaceFragmentAndAddToBackStack(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String?, backstackTag: String?) { 80 | replaceFragmentInternal(activity.supportFragmentManager, containerId, fragment, fragmentTag, true, backstackTag) 81 | } 82 | 83 | protected fun replaceFragmentInternal(fm: FragmentManager, @IdRes containerId: Int, fragment: Fragment, fragmentTag: String?, addToBackstack: Boolean, backstackTag: String?) { 84 | val ft = fm.beginTransaction().replace(containerId, fragment, fragmentTag) 85 | if (addToBackstack) { 86 | ft.addToBackStack(backstackTag).commit() 87 | fm.executePendingTransactions() 88 | } else { 89 | ft.commitNow() 90 | } 91 | } 92 | 93 | override fun showDialogFragment(dialog: T, fragmentTag: String) { 94 | dialog.show(activity.fragmentManager, fragmentTag) 95 | } 96 | 97 | override fun popFragmentBackStackImmediate() { 98 | activity.supportFragmentManager.popBackStackImmediate() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/navigator/ChildFragmentNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.navigator 2 | 3 | import android.support.annotation.IdRes 4 | import android.support.v4.app.Fragment 5 | 6 | /* Copyright 2017 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * 20 | * -------------- 21 | * 22 | * FILE MODIFIED 2017 Tailored Media GmbH */ 23 | class ChildFragmentNavigator(private val fragment: Fragment) : ActivityNavigator(fragment.activity!!), FragmentNavigator { 24 | 25 | override fun replaceChildFragment(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String?) { 26 | replaceFragmentInternal(fragment.childFragmentManager, containerId, fragment, fragmentTag, false, null) 27 | } 28 | 29 | override fun replaceChildFragmentAndAddToBackStack(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String?, backstackTag: String?) { 30 | replaceFragmentInternal(fragment.childFragmentManager, containerId, fragment, fragmentTag, true, backstackTag) 31 | } 32 | 33 | override fun popChildFragmentBackstackImmediate() { 34 | fragment.childFragmentManager.popBackStackImmediate() 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/navigator/FragmentNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.navigator 2 | 3 | import android.support.annotation.IdRes 4 | import android.support.v4.app.Fragment 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | interface FragmentNavigator : Navigator { 20 | 21 | fun replaceChildFragment(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String? = null) 22 | fun replaceChildFragmentAndAddToBackStack(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String?, backstackTag: String?) 23 | fun popChildFragmentBackstackImmediate() 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/navigator/Navigator.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.navigator 2 | 3 | import android.app.Activity 4 | import android.app.DialogFragment 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.support.annotation.IdRes 8 | import android.support.v4.app.Fragment 9 | 10 | /* Copyright 2016 Patrick Löwenstein 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | * 24 | * ------ 25 | * 26 | * FILE CHANGED 2017 Tailored Media GmbH 27 | * 28 | * */ 29 | interface Navigator { 30 | 31 | companion object { 32 | const val EXTRA_ARG = "_args" 33 | } 34 | 35 | fun finishActivity() 36 | fun finishActivityWithResult(resultCode: Int, resultIntentFun: (Intent.() -> Unit)? = null) 37 | fun finishAffinity() 38 | 39 | fun startActivity(intent: Intent) 40 | fun startActivity(action: String, uri: Uri? = null) 41 | fun startActivity(activityClass: Class, adaptIntentFun: (Intent.() -> Unit)? = null) 42 | 43 | fun startActivityForResult(activityClass: Class, requestCode: Int, adaptIntentFun: (Intent.() -> Unit)? = null) 44 | 45 | fun showDialogFragment(dialog: T, fragmentTag: String = dialog.javaClass.name) 46 | 47 | fun replaceFragment(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String? = null) 48 | fun replaceFragmentAndAddToBackStack(@IdRes containerId: Int, fragment: Fragment, fragmentTag: String? = null, backstackTag: String? = null) 49 | fun popFragmentBackStackImmediate() 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/validation/BaseValidator.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.ui.base.validation 16 | 17 | import io.reactivex.Completable 18 | 19 | /*** 20 | * Base class for validators of a view state. 21 | 22 | * @param The state class to be validated 23 | */ 24 | abstract class BaseValidator { 25 | 26 | /*** 27 | * Validates the state. 28 | 29 | * @param state The state to be validated. 30 | * * 31 | * @return A Completable that completes when the state is valid and 32 | * * provides a ValidationException in onError when the state is invalid. 33 | */ 34 | fun validate(state: S): Completable { 35 | return Completable.fromAction { validateState(state) } 36 | } 37 | 38 | /*** 39 | * Implement this method to validate the state. Here you can also 40 | * set error fields, e.g. for showing errors on an EditText. 41 | * If the state is invalid, throw a ValidationException. 42 | 43 | * @param state The state to be validated. 44 | * * 45 | * @throws ValidationException When the state is invalid. 46 | */ 47 | @Throws(ValidationException::class) 48 | protected abstract fun validateState(state: S) 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/validation/ValidationException.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.ui.base.validation 16 | 17 | class ValidationException : Exception() 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/view/MvvmView.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.view 2 | 3 | /* Copyright 2016 Patrick Löwenstein 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. */ 16 | interface MvvmView 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/viewmodel/AdapterMvvmViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.viewmodel 2 | 3 | import android.support.v7.widget.RecyclerView 4 | 5 | import com.patloew.countries.ui.base.view.MvvmView 6 | 7 | /* Copyright 2016 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | interface AdapterMvvmViewModel : MvvmViewModel { 21 | 22 | val adapter: RecyclerView.Adapter<*> 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/viewmodel/BaseStateViewModel.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.ui.base.viewmodel 16 | 17 | import android.databinding.BaseObservable 18 | import android.os.Bundle 19 | import android.os.Parcelable 20 | import android.support.annotation.CallSuper 21 | 22 | import com.patloew.countries.ui.base.view.MvvmView 23 | import com.patloew.countries.util.extensions.getParcelable 24 | import javax.inject.Inject 25 | 26 | /** 27 | * Base class that implements the ViewModel interface and provides a base implementation for 28 | * attachView() and detachView(). It also handles keeping a reference to the mvvmView that 29 | * can be accessed from the children classes by calling getMvpView(). 30 | 31 | * When saving state is required, restoring is handled automatically when calling attachView(). 32 | * However, saveInstanceState() must still be called in the corresponding lifecycle callback. 33 | 34 | * Create an inner class in your view model implementations that keeps the view state, so that 35 | * it can be injected here. Also, state saving/restoring is automatically handled by this base class. 36 | */ 37 | abstract class BaseStateViewModel : BaseObservable(), MvvmViewModel { 38 | 39 | companion object { 40 | private const val KEY_STATE = "state" 41 | } 42 | 43 | var view: V? = null 44 | private set 45 | 46 | @Inject protected lateinit var state: S 47 | 48 | @CallSuper 49 | override fun attachView(view: V, savedInstanceState: Bundle?) { 50 | this.view = view 51 | if (savedInstanceState != null) { restoreInstanceState(savedInstanceState) } 52 | } 53 | 54 | @CallSuper 55 | override fun detachView() { 56 | view = null 57 | } 58 | 59 | @CallSuper 60 | override fun saveInstanceState(outState: Bundle) { 61 | outState.putParcelable(KEY_STATE, state) 62 | } 63 | 64 | @CallSuper 65 | protected open fun restoreInstanceState(savedInstanceState: Bundle) { 66 | if (savedInstanceState.containsKey(KEY_STATE)) { 67 | state = savedInstanceState.getParcelable(KEY_STATE, state) 68 | } 69 | } 70 | 71 | } 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/viewmodel/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.viewmodel 2 | 3 | import android.databinding.BaseObservable 4 | import android.os.Bundle 5 | import android.support.annotation.CallSuper 6 | 7 | import com.patloew.countries.ui.base.view.MvvmView 8 | 9 | /* Copyright 2016 Patrick Löwenstein 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. */ 22 | /** 23 | * Base class that implements the ViewModel interface and provides a base implementation for 24 | * attachView() and detachView(). It also handles keeping a reference to the mvvmView that 25 | * can be accessed from the children classes by calling getMvpView(). 26 | 27 | * When saving state is required, restoring is handled automatically when calling attachView(). 28 | * However, saveInstanceState() must still be called in the corresponding lifecycle callback. 29 | 30 | * ------ 31 | 32 | * FILE MODIFIED 2017 Tailored Media GmbH 33 | */ 34 | abstract class BaseViewModel : BaseObservable(), MvvmViewModel { 35 | 36 | var view: V? = null 37 | private set 38 | 39 | @CallSuper 40 | override fun attachView(view: V, savedInstanceState: Bundle?) { 41 | this.view = view 42 | if (savedInstanceState != null) { restoreInstanceState(savedInstanceState) } 43 | } 44 | 45 | @CallSuper 46 | override fun detachView() { 47 | view = null 48 | } 49 | 50 | protected open fun restoreInstanceState(savedInstanceState: Bundle) { } 51 | 52 | override fun saveInstanceState(outState: Bundle) { } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/viewmodel/MvvmViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.viewmodel 2 | 3 | import android.databinding.Observable 4 | import android.os.Bundle 5 | 6 | import com.patloew.countries.ui.base.view.MvvmView 7 | 8 | /* Copyright 2016 Patrick Löwenstein 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. 21 | * 22 | * ---------- 23 | * 24 | * FILE CHANGED 2017 Tailored Media GmbH 25 | */ 26 | interface MvvmViewModel : Observable { 27 | fun attachView(view: V, savedInstanceState: Bundle?) 28 | fun detachView() 29 | 30 | fun saveInstanceState(outState: Bundle) 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/base/viewmodel/NoOpViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.base.viewmodel 2 | 3 | import android.databinding.BaseObservable 4 | import android.os.Bundle 5 | 6 | import com.patloew.countries.ui.base.view.MvvmView 7 | 8 | import javax.inject.Inject 9 | 10 | /* Copyright 2016 Patrick Löwenstein 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | * 24 | * ------- 25 | * 26 | * FILE CHANGED 2017 Tailored Media GmbH 27 | * */ 28 | class NoOpViewModel 29 | @Inject 30 | constructor() : BaseObservable(), MvvmViewModel { 31 | 32 | override fun attachView(view: V, savedInstanceState: Bundle?) {} 33 | 34 | override fun saveInstanceState(outState: Bundle) {} 35 | 36 | override fun detachView() {} 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/detail/DetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.detail 2 | 3 | import android.databinding.Observable 4 | import android.os.Bundle 5 | import android.support.v4.graphics.drawable.DrawableCompat 6 | import android.view.Menu 7 | import android.view.MenuItem 8 | import com.patloew.countries.BR 9 | import com.patloew.countries.R 10 | import com.patloew.countries.databinding.ActivityDetailBinding 11 | import com.patloew.countries.ui.base.BaseActivity 12 | import com.patloew.countries.ui.base.navigator.Navigator 13 | 14 | /* Copyright 2016 Patrick Löwenstein 15 | * 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. */ 27 | class DetailActivity : BaseActivity(), DetailMvvm.View { 28 | 29 | private var menu: Menu? = null 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | setAndBindContentView(savedInstanceState, R.layout.activity_detail) 34 | 35 | setSupportActionBar(binding.toolbar) 36 | supportActionBar?.setTitle(R.string.toolbar_title_detail) 37 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 38 | 39 | viewModel.update(intent.getParcelableExtra(Navigator.EXTRA_ARG), false) 40 | 41 | viewModel.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { 42 | override fun onPropertyChanged(observable: Observable, propertyId: Int) { 43 | if (propertyId == BR.bookmarkDrawable && menu != null) { 44 | val favoriteItem = menu!!.findItem(R.id.menu_item_favorite) 45 | favoriteItem.icon = viewModel.bookmarkDrawable 46 | tintMenuIcon(favoriteItem) 47 | } 48 | } 49 | }) 50 | } 51 | 52 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 53 | menuInflater.inflate(R.menu.menu_details, menu) 54 | this.menu = menu 55 | val favoriteItem = menu.findItem(R.id.menu_item_favorite) 56 | val mapItem = menu.findItem(R.id.menu_item_maps) 57 | favoriteItem.icon = viewModel.bookmarkDrawable 58 | tintMenuIcon(favoriteItem) 59 | tintMenuIcon(mapItem) 60 | return super.onCreateOptionsMenu(menu) 61 | } 62 | 63 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 64 | when (item.itemId) { 65 | android.R.id.home -> { finish() } 66 | R.id.menu_item_favorite -> { viewModel.onBookmarkClick() } 67 | R.id.menu_item_maps -> { viewModel.onMapClick() } 68 | } 69 | 70 | return super.onOptionsItemSelected(item) 71 | } 72 | 73 | private fun tintMenuIcon(menuItem: MenuItem) { 74 | val favoriteIcon = DrawableCompat.wrap(menuItem.icon.mutate()) 75 | DrawableCompat.setTint(favoriteIcon, 0xFFFFFFFF.toInt()) 76 | menuItem.icon = favoriteIcon 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/detail/DetailMvvm.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.detail 2 | 3 | import android.databinding.ObservableBoolean 4 | import android.databinding.ObservableField 5 | 6 | import com.patloew.countries.ui.ICountryViewModel 7 | import com.patloew.countries.ui.base.view.MvvmView 8 | 9 | /* Copyright 2016 Patrick Löwenstein 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. */ 22 | interface DetailMvvm { 23 | 24 | interface View : MvvmView 25 | 26 | interface ViewModel : ICountryViewModel { 27 | // Properties 28 | 29 | val isLoaded: ObservableBoolean 30 | val borders: ObservableField 31 | val currencies: ObservableField 32 | val languages: ObservableField 33 | val nameTranslations: ObservableField 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuItem 6 | 7 | import com.mikepenz.aboutlibraries.Libs 8 | import com.mikepenz.aboutlibraries.LibsBuilder 9 | import com.patloew.countries.R 10 | import com.patloew.countries.databinding.ActivityMainBinding 11 | import com.patloew.countries.ui.base.BaseActivity 12 | import com.patloew.countries.ui.base.view.MvvmView 13 | import com.patloew.countries.ui.base.viewmodel.NoOpViewModel 14 | import com.patloew.countries.ui.main.viewpager.MainAdapter 15 | 16 | import javax.inject.Inject 17 | 18 | /* Copyright 2016 Patrick Löwenstein 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | * 32 | * FILE MODIFIED 2017 Tailored Media GmbH */ 33 | class MainActivity : BaseActivity>(), MvvmView { 34 | 35 | @Inject lateinit var adapter: MainAdapter 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | setAndBindContentView(savedInstanceState, R.layout.activity_main) 40 | 41 | setSupportActionBar(binding.toolbar) 42 | 43 | binding.viewPager.adapter = adapter 44 | binding.tabLayout.setupWithViewPager(binding.viewPager) 45 | } 46 | 47 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 48 | menuInflater.inflate(R.menu.menu_main, menu) 49 | return true 50 | } 51 | 52 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 53 | if (item.itemId == R.id.menu_item_licenses) { 54 | LibsBuilder() 55 | .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) 56 | .withActivityTitle(getString(R.string.menu_item_licenses)) 57 | .withLibraries("rxJavaAndroid", "parceler", "recyclerview_fastscroll", "gradle_retrolambda") 58 | .withLicenseShown(true) 59 | .start(this) 60 | } 61 | 62 | return super.onOptionsItemSelected(item) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/recyclerview/CountryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.recyclerview 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.ViewGroup 5 | import com.patloew.countries.R 6 | import com.patloew.countries.data.model.Country 7 | import com.patloew.countries.injection.scopes.PerFragment 8 | import com.patloew.countries.util.Utils 9 | import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView 10 | import javax.inject.Inject 11 | 12 | /* Copyright 2016 Patrick Löwenstein 13 | * 14 | * Licensed under the Apache License, Version 2.0 (the "License"); 15 | * you may not use this file except in compliance with the License. 16 | * You may obtain a copy of the License at 17 | * 18 | * http://www.apache.org/licenses/LICENSE-2.0 19 | * 20 | * Unless required by applicable law or agreed to in writing, software 21 | * distributed under the License is distributed on an "AS IS" BASIS, 22 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | * See the License for the specific language governing permissions and 24 | * limitations under the License. */ 25 | @PerFragment 26 | class CountryAdapter 27 | @Inject 28 | constructor() : RecyclerView.Adapter(), FastScrollRecyclerView.SectionedAdapter { 29 | 30 | var countryList = emptyList() 31 | 32 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): CountryViewHolder { 33 | return Utils.createViewHolder(viewGroup, R.layout.card_country, ::CountryViewHolder) 34 | } 35 | 36 | override fun onBindViewHolder(countryViewHolder: CountryViewHolder, position: Int) { 37 | countryViewHolder.viewModel.update(countryList[position], position == countryList.size - 1) 38 | countryViewHolder.executePendingBindings() 39 | } 40 | 41 | override fun getItemCount(): Int { 42 | return countryList.size 43 | } 44 | 45 | override fun getSectionName(position: Int): String { 46 | return countryList[position].name!!.substring(0, 1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/recyclerview/CountryMvvm.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.recyclerview 2 | 3 | import com.patloew.countries.ui.ICountryViewModel 4 | import com.patloew.countries.ui.base.view.MvvmView 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | interface CountryMvvm { 20 | 21 | interface ViewModel : ICountryViewModel { 22 | fun onCardClick() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/recyclerview/CountryViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.recyclerview 2 | 3 | import android.view.View 4 | 5 | import com.patloew.countries.databinding.CardCountryBinding 6 | import com.patloew.countries.ui.base.BaseActivityViewHolder 7 | import com.patloew.countries.ui.base.view.MvvmView 8 | 9 | /* Copyright 2016 Patrick Löwenstein 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. */ 22 | class CountryViewHolder(v: View) : BaseActivityViewHolder(v), MvvmView { 23 | 24 | init { 25 | bindContentView(v) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/recyclerview/CountryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.recyclerview 2 | 3 | import android.content.Context 4 | import com.patloew.countries.data.local.CountryRepo 5 | import com.patloew.countries.injection.qualifier.AppContext 6 | import com.patloew.countries.injection.scopes.PerViewHolder 7 | import com.patloew.countries.ui.BaseCountryViewModel 8 | import com.patloew.countries.ui.base.navigator.Navigator 9 | import com.patloew.countries.ui.base.view.MvvmView 10 | import com.patloew.countries.ui.detail.DetailActivity 11 | import javax.inject.Inject 12 | 13 | /* Copyright 2016 Patrick Löwenstein 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | * 27 | * ------- 28 | * 29 | * FILE MODIFIED 2017 Tailored Media GmbH */ 30 | 31 | @PerViewHolder 32 | class CountryViewModel 33 | @Inject 34 | constructor(@AppContext context: Context, navigator: Navigator, countryRepo: CountryRepo) : BaseCountryViewModel(context, countryRepo, navigator), CountryMvvm.ViewModel { 35 | 36 | override fun onCardClick() { 37 | navigator.startActivity(DetailActivity::class.java) { putExtra(Navigator.EXTRA_ARG, countryRepo.detach(country)) } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/CountriesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager 2 | 3 | import android.os.Bundle 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | 9 | import com.patloew.countries.R 10 | import com.patloew.countries.databinding.FragmentRecyclerviewBinding 11 | import com.patloew.countries.ui.base.BaseFragment 12 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 13 | import com.patloew.countries.ui.main.recyclerview.CountryAdapter 14 | 15 | import javax.inject.Inject 16 | 17 | /* Copyright 2016 Patrick Löwenstein 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | * 31 | * FILE MODIFIED 2017 Tailored Media GmbH*/ 32 | abstract class CountriesFragment> : BaseFragment(), CountriesView { 33 | 34 | @Inject protected lateinit var adapter: CountryAdapter 35 | 36 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 37 | return setAndBindContentView(inflater, container, savedInstanceState, R.layout.fragment_recyclerview) 38 | } 39 | 40 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 41 | super.onViewCreated(view, savedInstanceState) 42 | 43 | binding.recyclerView.setHasFixedSize(true) 44 | binding.recyclerView.layoutManager = LinearLayoutManager(context) 45 | binding.recyclerView.adapter = adapter 46 | } 47 | 48 | // View 49 | 50 | override fun onRefresh(success: Boolean) { 51 | binding.swipeRefreshLayout.isRefreshing = false 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/CountriesView.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager 2 | 3 | import com.patloew.countries.ui.base.view.MvvmView 4 | 5 | /* Copyright 2016 Patrick Löwenstein 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. */ 18 | interface CountriesView : MvvmView { 19 | fun onRefresh(success: Boolean) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/MainAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager 2 | 3 | import android.content.res.Resources 4 | import android.support.v4.app.Fragment 5 | import android.support.v4.app.FragmentManager 6 | import android.support.v4.app.FragmentPagerAdapter 7 | 8 | import com.patloew.countries.R 9 | import com.patloew.countries.injection.qualifier.ActivityFragmentManager 10 | import com.patloew.countries.injection.scopes.PerActivity 11 | import com.patloew.countries.ui.main.viewpager.all.AllCountriesFragment 12 | import com.patloew.countries.ui.main.viewpager.favorites.FavoriteCountriesFragment 13 | 14 | import javax.inject.Inject 15 | 16 | /* Copyright 2016 Patrick Löwenstein 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); 19 | * you may not use this file except in compliance with the License. 20 | * You may obtain a copy of the License at 21 | * 22 | * http://www.apache.org/licenses/LICENSE-2.0 23 | * 24 | * Unless required by applicable law or agreed to in writing, software 25 | * distributed under the License is distributed on an "AS IS" BASIS, 26 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | * See the License for the specific language governing permissions and 28 | * limitations under the License. */ 29 | 30 | @PerActivity 31 | class MainAdapter 32 | @Inject 33 | constructor(@ActivityFragmentManager fm: FragmentManager, private val res: Resources) : FragmentPagerAdapter(fm) { 34 | 35 | override fun getItem(position: Int): Fragment { 36 | return when(position) { 37 | 0 -> AllCountriesFragment() 38 | else -> FavoriteCountriesFragment() 39 | } 40 | } 41 | 42 | override fun getPageTitle(position: Int): CharSequence { 43 | return res.getString( 44 | when(position) { 45 | 0 -> R.string.tab_title_all 46 | else -> R.string.tab_title_favorites 47 | } 48 | ); 49 | } 50 | 51 | override fun getCount(): Int { 52 | return 2 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/all/AllCountriesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.all 2 | 3 | import android.os.Bundle 4 | import android.support.design.widget.Snackbar 5 | import android.view.View 6 | import com.patloew.countries.R 7 | import com.patloew.countries.ui.main.viewpager.CountriesFragment 8 | import com.patloew.countries.ui.main.viewpager.CountriesView 9 | 10 | /* Copyright 2016 Patrick Löwenstein 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. */ 23 | class AllCountriesFragment : CountriesFragment(), CountriesView { 24 | 25 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 26 | super.onViewCreated(view, savedInstanceState) 27 | 28 | binding.swipeRefreshLayout.setOnRefreshListener { viewModel.reloadData() } 29 | if (savedInstanceState == null) { binding.swipeRefreshLayout.isRefreshing = true } 30 | viewModel.reloadData() 31 | } 32 | 33 | 34 | // View 35 | 36 | override fun onRefresh(success: Boolean) { 37 | super.onRefresh(success) 38 | 39 | if (!success) { 40 | Snackbar.make(binding.recyclerView, "Could not load countries", Snackbar.LENGTH_INDEFINITE) 41 | .setAction(R.string.snackbar_action_retry) { viewModel.reloadData() } 42 | .show() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/all/AllCountriesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.all 2 | 3 | import android.os.Bundle 4 | import com.patloew.countries.data.local.CountryRepo 5 | import com.patloew.countries.data.remote.CountryApi 6 | import com.patloew.countries.injection.scopes.PerFragment 7 | import com.patloew.countries.ui.base.viewmodel.BaseViewModel 8 | import com.patloew.countries.ui.main.recyclerview.CountryAdapter 9 | import com.patloew.countries.ui.main.viewpager.CountriesView 10 | import io.reactivex.android.schedulers.AndroidSchedulers 11 | import io.reactivex.disposables.CompositeDisposable 12 | import timber.log.Timber 13 | import java.util.* 14 | import javax.inject.Inject 15 | 16 | /* Copyright 2016 Patrick Löwenstein 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); 19 | * you may not use this file except in compliance with the License. 20 | * You may obtain a copy of the License at 21 | * 22 | * http://www.apache.org/licenses/LICENSE-2.0 23 | * 24 | * Unless required by applicable law or agreed to in writing, software 25 | * distributed under the License is distributed on an "AS IS" BASIS, 26 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | * See the License for the specific language governing permissions and 28 | * limitations under the License. */ 29 | 30 | @PerFragment 31 | class AllCountriesViewModel 32 | @Inject 33 | constructor(override val adapter: CountryAdapter, private val countryApi: CountryApi, private val countryRepo: CountryRepo) : BaseViewModel(), IAllCountriesViewModel { 34 | 35 | private val compositeDisposable = CompositeDisposable() 36 | 37 | override fun attachView(view: CountriesView, savedInstanceState: Bundle?) { 38 | super.attachView(view, savedInstanceState) 39 | 40 | compositeDisposable.add( 41 | countryRepo.favoriteChangeObservable 42 | .subscribe({ adapter.notifyDataSetChanged() }, { Timber.e(it) }) 43 | ) 44 | } 45 | 46 | override fun detachView() { 47 | super.detachView() 48 | compositeDisposable.clear() 49 | } 50 | 51 | override fun reloadData() { 52 | compositeDisposable.add(countryApi.getAllCountries() 53 | .doOnSuccess({ Collections.sort(it) }) 54 | .observeOn(AndroidSchedulers.mainThread()) 55 | .subscribe({ 56 | adapter.countryList = it 57 | adapter.notifyDataSetChanged() 58 | view?.onRefresh(true) 59 | }, { 60 | Timber.e(it, "Could not load countries") 61 | view?.onRefresh(false) 62 | }) 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/all/IAllCountriesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.all 2 | 3 | import com.patloew.countries.ui.base.viewmodel.AdapterMvvmViewModel 4 | import com.patloew.countries.ui.main.viewpager.CountriesView 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | interface IAllCountriesViewModel : AdapterMvvmViewModel { 20 | fun reloadData() 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/favorites/FavoriteCountriesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.favorites 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.patloew.countries.ui.main.viewpager.CountriesFragment 6 | 7 | /* Copyright 2016 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class FavoriteCountriesFragment : CountriesFragment() { 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.isRefreshing = false } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/favorites/FavoriteCountriesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.favorites 2 | 3 | import android.os.Bundle 4 | import com.patloew.countries.data.local.CountryRepo 5 | import com.patloew.countries.data.model.Country 6 | import com.patloew.countries.injection.scopes.PerFragment 7 | import com.patloew.countries.ui.base.viewmodel.BaseViewModel 8 | import com.patloew.countries.ui.main.recyclerview.CountryAdapter 9 | import com.patloew.countries.ui.main.viewpager.CountriesView 10 | import io.reactivex.disposables.Disposable 11 | import io.realm.Sort 12 | import timber.log.Timber 13 | import javax.inject.Inject 14 | 15 | /* Copyright 2016 Patrick Löwenstein 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software 24 | * distributed under the License is distributed on an "AS IS" BASIS, 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | * See the License for the specific language governing permissions and 27 | * limitations under the License. */ 28 | 29 | @PerFragment 30 | class FavoriteCountriesViewModel 31 | @Inject 32 | constructor(override val adapter: CountryAdapter, private val countryRepo: CountryRepo) : BaseViewModel(), IFavoriteCountriesViewModel { 33 | private var disposable: Disposable? = null 34 | 35 | override fun attachView(view: CountriesView, savedInstanceState: Bundle?) { 36 | super.attachView(view, savedInstanceState) 37 | 38 | disposable = countryRepo.findAllSortedWithChanges("name", Sort.ASCENDING) 39 | .subscribe({ refreshView(it) }, { Timber.e(it) }) 40 | } 41 | 42 | private fun refreshView(countryList: List) { 43 | adapter.countryList = countryList 44 | adapter.notifyDataSetChanged() 45 | view?.onRefresh(true) 46 | } 47 | 48 | override fun detachView() { 49 | super.detachView() 50 | 51 | if (disposable != null) { 52 | disposable!!.dispose() 53 | disposable = null 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/ui/main/viewpager/favorites/IFavoriteCountriesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.ui.main.viewpager.favorites 2 | 3 | import com.patloew.countries.ui.base.viewmodel.AdapterMvvmViewModel 4 | import com.patloew.countries.ui.main.viewpager.CountriesView 5 | 6 | /* Copyright 2016 Patrick Löwenstein 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. */ 19 | interface IFavoriteCountriesViewModel : AdapterMvvmViewModel 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/CountryTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import com.google.gson.TypeAdapter 4 | import com.google.gson.stream.JsonReader 5 | import com.google.gson.stream.JsonToken 6 | import com.google.gson.stream.JsonWriter 7 | import com.patloew.countries.data.model.Country 8 | 9 | import java.io.IOException 10 | 11 | /* Copyright 2016 Patrick Löwenstein 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. */ 24 | class CountryTypeAdapter private constructor() : TypeAdapter() { 25 | 26 | companion object { 27 | val INSTANCE = CountryTypeAdapter().nullSafe() 28 | } 29 | 30 | @Throws(IOException::class) 31 | override fun write(writer: JsonWriter, value: Country) { 32 | throw UnsupportedOperationException() 33 | } 34 | 35 | @Throws(IOException::class) 36 | override fun read(reader: JsonReader): Country { 37 | try { 38 | val country = Country() 39 | 40 | reader.beginObject() 41 | 42 | while (reader.hasNext()) { 43 | val name = reader.nextName() 44 | 45 | when (name) { 46 | "alpha2Code" -> { 47 | // cannot be null, because it is the primary key 48 | country.alpha2Code = reader.nextString() 49 | } 50 | "alpha3Code" -> { 51 | country.alpha3Code = JsonUtils.readNullSafeString(reader) 52 | } 53 | "name" -> { 54 | country.name = JsonUtils.readNullSafeString(reader) 55 | } 56 | "nativeName" -> { 57 | country.nativeName = JsonUtils.readNullSafeString(reader) 58 | } 59 | "region" -> { 60 | country.region = JsonUtils.readNullSafeString(reader) 61 | } 62 | "capital" -> { 63 | country.capital = JsonUtils.readNullSafeString(reader) 64 | } 65 | "population" -> { 66 | country.population = JsonUtils.readNullSafeInteger(reader) 67 | } 68 | "latlng" -> { 69 | if (reader.peek() == JsonToken.NULL) { 70 | reader.nextNull() 71 | } else { 72 | reader.beginArray() 73 | if (reader.hasNext()) { 74 | country.lat = JsonUtils.readNullSafeFloat(reader) 75 | } 76 | if (reader.hasNext()) { 77 | country.lng = JsonUtils.readNullSafeFloat(reader) 78 | } 79 | reader.endArray() 80 | } 81 | } 82 | "currencies" -> { 83 | country.currencies = RealmStringListTypeAdapter.INSTANCE.read(reader) 84 | } 85 | "borders" -> { 86 | country.borders = RealmStringListTypeAdapter.INSTANCE.read(reader) 87 | } 88 | "languages" -> { 89 | country.languages = RealmStringListTypeAdapter.INSTANCE.read(reader) 90 | } 91 | "translations" -> { 92 | country.translations = RealmStringMapEntryListTypeAdapter.INSTANCE.read(reader) 93 | } 94 | else -> reader.skipValue() 95 | } 96 | } 97 | 98 | reader.endObject() 99 | 100 | return country 101 | } catch (e: Exception) { 102 | throw IOException(e) 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/DelegatedProperties.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.util 16 | 17 | import android.databinding.BaseObservable 18 | import android.support.v7.widget.RecyclerView 19 | import kotlin.properties.ReadWriteProperty 20 | import kotlin.reflect.KProperty 21 | 22 | /** 23 | * Delegated property that calls notifyChange() every time the property get's changed. 24 | */ 25 | class NotifyChangeDelegate(private var value: T) : ReadWriteProperty{ 26 | override fun getValue(thisRef: BaseObservable, property: KProperty<*>): T { 27 | return value 28 | } 29 | 30 | override fun setValue(thisRef: BaseObservable, property: KProperty<*>, value: T) { 31 | if (this.value !== value) { 32 | this.value = value 33 | thisRef.notifyChange() 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Delegated property that calls notifyPropertyChanged(propertyId) every time the property get's changed. 40 | */ 41 | class NotifyPropertyChangedDelegate(private var value: T, private val propertyId: Int) : ReadWriteProperty { 42 | override fun getValue(thisRef: BaseObservable, property: KProperty<*>): T = value 43 | 44 | override fun setValue(thisRef: BaseObservable, property: KProperty<*>, value: T) { 45 | if (this.value !== value) { 46 | this.value = value 47 | thisRef.notifyPropertyChanged(propertyId) 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Delegated property that calls notifyDataSetChanged() every time the property get's changed. 54 | */ 55 | class NotifyDatasetChangedDelegate(private var value: T) : ReadWriteProperty, T> { 56 | override fun getValue(thisRef: RecyclerView.Adapter<*>, property: KProperty<*>): T = value 57 | 58 | override fun setValue(thisRef: RecyclerView.Adapter<*>, property: KProperty<*>, value: T) { 59 | if (this.value !== value) { 60 | this.value = value 61 | thisRef.notifyDataSetChanged() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import com.google.gson.stream.JsonReader 4 | import com.google.gson.stream.JsonToken 5 | 6 | import java.io.IOException 7 | 8 | /* Copyright 2016 Patrick Löwenstein 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. */ 21 | object JsonUtils { 22 | @Throws(IOException::class) 23 | fun readNullSafeString(reader: JsonReader): String? { 24 | if (reader.peek() == JsonToken.NULL) { 25 | reader.nextNull() 26 | return null 27 | } else { 28 | return reader.nextString() 29 | } 30 | } 31 | 32 | @Throws(IOException::class) 33 | fun readNullSafeLong(reader: JsonReader): Long? { 34 | if (reader.peek() == JsonToken.NULL) { 35 | reader.nextNull() 36 | return null 37 | } else { 38 | return reader.nextLong() 39 | } 40 | } 41 | 42 | @Throws(IOException::class) 43 | fun readNullSafeInteger(reader: JsonReader): Int? { 44 | if (reader.peek() == JsonToken.NULL) { 45 | reader.nextNull() 46 | return null 47 | } else { 48 | return reader.nextInt() 49 | } 50 | } 51 | 52 | @Throws(IOException::class) 53 | fun readNullSafeDouble(reader: JsonReader): Double? { 54 | if (reader.peek() == JsonToken.NULL) { 55 | reader.nextNull() 56 | return null 57 | } else { 58 | return reader.nextDouble() 59 | } 60 | } 61 | 62 | @Throws(IOException::class) 63 | fun readNullSafeFloat(reader: JsonReader): Float? { 64 | if (reader.peek() == JsonToken.NULL) { 65 | reader.nextNull() 66 | return null 67 | } else { 68 | return reader.nextDouble().toFloat() 69 | } 70 | } 71 | 72 | @Throws(IOException::class) 73 | fun readNullSafeBoolean(reader: JsonReader): Boolean? { 74 | if (reader.peek() == JsonToken.NULL) { 75 | reader.nextNull() 76 | return null 77 | } else { 78 | return reader.nextBoolean() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableBooleanPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableBoolean 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableBooleanPaperParcelTypeConverter() : TypeAdapter { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableBoolean { 23 | return ObservableBoolean(source.readByte() == 1.toByte()) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableBoolean, dest: Parcel, flags: Int) { 27 | dest.writeByte(if(value.get()) 1.toByte() else 0.toByte()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableDoublePaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableDouble 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableDoublePaperParcelTypeConverter() : TypeAdapter { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableDouble { 23 | return ObservableDouble(source.readDouble()) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableDouble, dest: Parcel, flags: Int) { 27 | dest.writeDouble(value.get()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableFieldPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableField 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableFieldPaperParcelTypeConverter(val itemAdapter : TypeAdapter) : TypeAdapter> { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableField { 23 | return ObservableField(itemAdapter.readFromParcel(source)) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableField, dest: Parcel, flags: Int) { 27 | itemAdapter.writeToParcel(value.get(), dest, flags) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableFloatPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableFloat 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableFloatPaperParcelTypeConverter() : TypeAdapter { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableFloat { 23 | return ObservableFloat(source.readFloat()) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableFloat, dest: Parcel, flags: Int) { 27 | dest.writeFloat(value.get()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableIntPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableInt 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableIntPaperParcelTypeConverter() : TypeAdapter { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableInt { 23 | return ObservableInt(source.readInt()) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableInt, dest: Parcel, flags: Int) { 27 | dest.writeInt(value.get()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/ObservableLongPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.databinding.ObservableLong 4 | import android.os.Parcel 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class ObservableLongPaperParcelTypeConverter() : TypeAdapter { 21 | 22 | override fun readFromParcel(source: Parcel): ObservableLong { 23 | return ObservableLong(source.readLong()) 24 | } 25 | 26 | override fun writeToParcel(value: ObservableLong, dest: Parcel, flags: Int) { 27 | dest.writeLong(value.get()) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/RealmListPaperParcelTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.os.Parcel 4 | import io.realm.RealmList 5 | import paperparcel.TypeAdapter 6 | 7 | /* Copyright 2017 Patrick Löwenstein 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. */ 20 | class RealmListPaperParcelTypeConverter(val itemAdapter : TypeAdapter) : TypeAdapter> { 21 | 22 | override fun readFromParcel(source: Parcel): RealmList { 23 | val size = source.readInt() 24 | val realmList = RealmList() 25 | for (i in 0 until size) { realmList.add(itemAdapter.readFromParcel(source)) } 26 | return realmList 27 | } 28 | 29 | override fun writeToParcel(value: RealmList, dest: Parcel, flags: Int) { 30 | dest.writeInt(value.size) 31 | for (i in 0 until value.size) { itemAdapter.writeToParcel(value[i], dest, flags) } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/RealmStringListTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import com.google.gson.TypeAdapter 4 | import com.google.gson.stream.JsonReader 5 | import com.google.gson.stream.JsonToken 6 | import com.google.gson.stream.JsonWriter 7 | import io.realm.RealmList 8 | import java.io.IOException 9 | 10 | /* Copyright 2016 Patrick Löwenstein 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. */ 23 | class RealmStringListTypeAdapter private constructor() : TypeAdapter>() { 24 | 25 | companion object { 26 | val INSTANCE = RealmStringListTypeAdapter().nullSafe() 27 | } 28 | 29 | @Throws(IOException::class) 30 | override fun write(writer: JsonWriter, src: RealmList) { 31 | writer.beginArray() 32 | for (realmString in src) { writer.value(realmString) } 33 | writer.endArray() 34 | } 35 | 36 | @Throws(IOException::class) 37 | override fun read(reader: JsonReader): RealmList { 38 | val realmStrings = RealmList() 39 | 40 | reader.beginArray() 41 | 42 | while (reader.hasNext()) { 43 | if (reader.peek() == JsonToken.NULL) { 44 | reader.nextNull() 45 | } else { 46 | realmStrings.add(reader.nextString()) 47 | } 48 | } 49 | 50 | reader.endArray() 51 | 52 | return realmStrings 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/RealmStringMapEntryListTypeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import com.google.gson.TypeAdapter 4 | import com.google.gson.stream.JsonReader 5 | import com.google.gson.stream.JsonToken 6 | import com.google.gson.stream.JsonWriter 7 | import com.patloew.countries.data.model.RealmStringMapEntry 8 | import io.realm.RealmList 9 | import java.io.IOException 10 | 11 | /* Copyright 2016 Patrick Löwenstein 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. */ 24 | class RealmStringMapEntryListTypeAdapter private constructor() : TypeAdapter>() { 25 | 26 | companion object { 27 | val INSTANCE = RealmStringMapEntryListTypeAdapter().nullSafe() 28 | } 29 | 30 | @Throws(IOException::class) 31 | override fun write(writer: JsonWriter, src: RealmList) { 32 | writer.beginObject() 33 | 34 | for (mapEntry in src) { 35 | writer.name(mapEntry.key) 36 | writer.value(mapEntry.value) 37 | } 38 | 39 | writer.endObject() 40 | } 41 | 42 | @Throws(IOException::class) 43 | override fun read(reader: JsonReader): RealmList { 44 | val mapEntries = RealmList() 45 | 46 | reader.beginObject() 47 | 48 | while (reader.hasNext()) { 49 | val realmStringMapEntry = RealmStringMapEntry() 50 | realmStringMapEntry.key = reader.nextName() 51 | 52 | if (reader.peek() == JsonToken.NULL) { 53 | reader.nextNull() 54 | realmStringMapEntry.value = null 55 | } else { 56 | realmStringMapEntry.value = reader.nextString() 57 | } 58 | 59 | mapEntries.add(realmStringMapEntry) 60 | } 61 | 62 | reader.endObject() 63 | 64 | return mapEntries 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util 2 | 3 | import android.support.annotation.LayoutRes 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | 9 | /* Copyright 2017 Tailored Media GmbH 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | * 23 | * ----------------- 24 | * 25 | * FILE MODIFIED 2017 Patrick Löwenstein */ 26 | 27 | object Utils { 28 | 29 | fun createViewHolder(viewGroup: ViewGroup, @LayoutRes layoutResId: Int, newViewHolderAction: (View) -> T): T { 30 | val view = LayoutInflater.from(viewGroup.context).inflate(layoutResId, viewGroup, false) 31 | return newViewHolderAction(view) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/bindingadapter/BindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util.bindingadapter 2 | 3 | import android.databinding.BindingAdapter 4 | import android.view.View 5 | import android.view.ViewGroup 6 | 7 | 8 | object BindingAdapters { 9 | 10 | @BindingAdapter("android:visibility") 11 | @JvmStatic 12 | fun setVisibility(view: View, visible: Boolean) { 13 | view.visibility = if (visible) View.VISIBLE else View.GONE 14 | } 15 | 16 | @BindingAdapter("android:layout_marginBottom") 17 | @JvmStatic 18 | fun setLayoutMarginBottom(v: View, bottomMargin: Int) { 19 | val layoutParams = v.layoutParams as ViewGroup.MarginLayoutParams 20 | layoutParams.bottomMargin = bottomMargin 21 | } 22 | 23 | @BindingAdapter("android:onClick") 24 | @JvmStatic 25 | fun setOnClickListener(v: View, runnable: Runnable) { 26 | v.setOnClickListener { runnable.run() } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/bindingadapter/ViewPagerBindingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.patloew.countries.util.bindingadapter; 2 | 3 | import android.databinding.BindingAdapter; 4 | import android.databinding.InverseBindingAdapter; 5 | import android.databinding.InverseBindingListener; 6 | import android.support.v4.view.ViewPager; 7 | 8 | /* Copyright 2017 Tailored Media GmbH 9 | * 10 | * Licensed under the Apache License, Version 2.0 (the "License"); 11 | * you may not use this file except in compliance with the License. 12 | * You may obtain a copy of the License at 13 | * 14 | * http://www.apache.org/licenses/LICENSE-2.0 15 | * 16 | * Unless required by applicable law or agreed to in writing, software 17 | * distributed under the License is distributed on an "AS IS" BASIS, 18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | * See the License for the specific language governing permissions and 20 | * limitations under the License. */ 21 | public class ViewPagerBindingAdapter { 22 | 23 | @InverseBindingAdapter(attribute = "selectedPagePosition", event = "selectedPagePositionAttrChanged") 24 | public static int captureSelectedPagePosition(ViewPager viewPager) { 25 | return viewPager.getCurrentItem(); 26 | } 27 | 28 | @BindingAdapter("selectedPagePosition") 29 | public static void setSelectedPagePosition(ViewPager viewPager, int position) { 30 | viewPager.setCurrentItem(position); 31 | } 32 | 33 | 34 | @BindingAdapter(value = {"onPageChangeListener", "selectedPagePositionAttrChanged"}, requireAll = false) 35 | public static void setOnPageChangeListener(ViewPager viewPager, final ViewPager.OnPageChangeListener pageChangeListener, final InverseBindingListener attrChanged) { 36 | if (pageChangeListener == null && attrChanged == null) { 37 | viewPager.setOnPageChangeListener(null); 38 | } else { 39 | viewPager.setOnPageChangeListener(new BindingOnPageChangedListener(pageChangeListener, attrChanged)); 40 | } 41 | } 42 | 43 | private static class BindingOnPageChangedListener implements ViewPager.OnPageChangeListener { 44 | 45 | private final ViewPager.OnPageChangeListener pageChangeListener; 46 | private final InverseBindingListener inverseBindingListener; 47 | 48 | BindingOnPageChangedListener(ViewPager.OnPageChangeListener pageChangeListener, InverseBindingListener inverseBindingListener) { 49 | this.pageChangeListener = pageChangeListener; 50 | this.inverseBindingListener = inverseBindingListener; 51 | } 52 | 53 | @Override 54 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 55 | if(pageChangeListener != null) { pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } 56 | } 57 | 58 | @Override 59 | public void onPageSelected(int position) { 60 | if(pageChangeListener != null) { pageChangeListener.onPageSelected(position); } 61 | if(inverseBindingListener != null) { inverseBindingListener.onChange(); } 62 | } 63 | 64 | @Override 65 | public void onPageScrollStateChanged(int state) { 66 | if(pageChangeListener != null) { pageChangeListener.onPageScrollStateChanged(state); } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/extensions/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.util.extensions 16 | 17 | import android.content.Context 18 | import android.content.ContextWrapper 19 | import android.os.Build 20 | import java.util.* 21 | 22 | 23 | fun Context.getCurrentLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 24 | this.resources.configuration.locales.get(0) 25 | } else { 26 | @Suppress("DEPRECATION") 27 | this.resources.configuration.locale 28 | } 29 | 30 | inline fun Context.castWithUnwrap(): T? { 31 | if (this is T) return this 32 | 33 | var context = this 34 | while (context is ContextWrapper) { 35 | context = context.baseContext 36 | if (context is T) { 37 | return context 38 | } 39 | } 40 | return null 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/extensions/Extensions.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.util.extensions 16 | 17 | import android.os.Bundle 18 | import android.os.Parcelable 19 | import android.support.v4.app.Fragment 20 | import com.patloew.countries.ui.base.view.MvvmView 21 | import com.patloew.countries.ui.base.viewmodel.MvvmViewModel 22 | import com.patloew.countries.ui.base.viewmodel.NoOpViewModel 23 | 24 | 25 | // Bundle 26 | 27 | fun Bundle.getParcelable(key: String, defaultObject: T): T = if (containsKey(key)) { 28 | getParcelable(key) 29 | } else { 30 | defaultObject 31 | } 32 | 33 | 34 | // ViewModel 35 | 36 | fun MvvmViewModel.attachViewOrThrowRuntimeException(view: MvvmView, savedInstanceState: Bundle?) { 37 | try { 38 | @Suppress("UNCHECKED_CAST") 39 | this.attachView(view as V, savedInstanceState) 40 | } catch (e: ClassCastException) { 41 | if (this !is NoOpViewModel<*>) { 42 | throw RuntimeException(javaClass.simpleName + " must implement MvvmView subclass as declared in " + this.javaClass.simpleName) 43 | } 44 | } 45 | } 46 | 47 | // Fragment 48 | 49 | inline fun Fragment.withArgs(argsFun: Bundle.() -> Unit) = apply { arguments = Bundle().apply(argsFun) } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/patloew/countries/util/extensions/RealmExtensions.kt: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Tailored Media GmbH 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. */ 14 | 15 | package com.patloew.countries.util.extensions 16 | 17 | import io.realm.Realm 18 | import io.realm.RealmModel 19 | import timber.log.Timber 20 | import java.io.Closeable 21 | import javax.inject.Provider 22 | import kotlin.reflect.KMutableProperty1 23 | 24 | 25 | inline fun Provider.use(consumer: (T) -> R): R = get().use(consumer) 26 | 27 | fun Realm.executeRealmTransactionWithResult(transaction: (Realm) -> T): T? { 28 | beginTransaction() 29 | return try { 30 | val result = transaction.invoke(this) 31 | commitTransaction() 32 | result 33 | } catch (t: Throwable) { 34 | if (isInTransaction) { 35 | cancelTransaction() 36 | } else { 37 | Timber.w(t, "Could not cancel transaction, not currently in a transaction.") 38 | } 39 | null 40 | } 41 | } 42 | 43 | inline fun Realm.containsAtLeastOne(): Boolean = where(T::class.java).count() > 0 44 | 45 | inline fun Realm.getFieldFromFirstResult(mapper: (M) -> T): T? = where(M::class.java).findFirst()?.let(mapper) 46 | 47 | inline fun Realm.setFieldOnFirstResult(crossinline setter: M.(T) -> Unit, value: T) { 48 | where(M::class.java).findFirst()?.let { model -> executeTransaction { model.setter(value) } } 49 | } 50 | 51 | inline fun Realm.setFieldOnFirstResult(setter: KMutableProperty1, value: T) { 52 | where(M::class.java).findFirst()?.let { model -> executeTransaction { setter.set(model, value) } } 53 | } 54 | 55 | inline fun Realm.findAll(detach: Boolean = false): List { 56 | var list: List = where(M::class.java).findAll() 57 | if (detach) { 58 | list = copyFromRealm(list) 59 | } 60 | return list 61 | } 62 | 63 | inline fun Realm.findFirst(detach: Boolean = false): M? { 64 | var first: M? = where(M::class.java).findFirst() 65 | if (first != null && detach) { 66 | first = copyFromRealm(first) 67 | } 68 | return first 69 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bookmark_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bookmark_border_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 15 | 16 | 21 | 22 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/card_country.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 23 | 24 | 32 | 33 | 41 | 42 | 51 | 52 | 62 | 63 | 71 | 72 | 81 | 82 | 83 | 84 | 95 | 96 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_recyclerview.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/aboutlibs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | John Ericksen 5 | 6 | Parceler 7 | Android Parcelables made easy through code generation. 8 | https://github.com/johncarl81/parceler 9 | 1.0.4 10 | 11 | true 12 | https://github.com/johncarl81/parceler 13 | 14 | org.parceler.Parcels 15 | 16 | apache_2_0 17 | 18 | 19 | 20 | 21 | Tim Malseed 22 | 23 | RecyclerView-FastScroll 24 | A simple FastScroller for Android\'s RecyclerView 25 | https://github.com/timusus/RecyclerView-FastScroll 26 | 1.0.6 27 | 28 | true 29 | https://github.com/timusus/RecyclerView-FastScroll 30 | 31 | com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView 32 | 33 | apache_2_0 34 | 35 | 36 | 37 | 38 | 39 | Evan Tatarka 40 | 41 | gradle-retrolambda 42 | A gradle plugin for getting java lambda support in java 6, 7 and android 43 | https://github.com/evant/gradle-retrolambda 44 | 3.3.0-beta4 45 | 46 | true 47 | https://github.com/evant/gradle-retrolambda 48 | 49 | apache_2_0 50 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 6dp 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Name translations: 4 | Region: 5 | Borders: 6 | Capital: 7 | Population: 8 | Languages: 9 | Currencies: 10 | Location: 11 | Retry 12 | Licenses 13 | All 14 | Favorites 15 | Favorite 16 | Show on map 17 | Country Detail 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/patloew/countries/AllCountriesViewModelUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.patloew.countries; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | import com.patloew.countries.data.local.CountryRepo; 6 | import com.patloew.countries.data.model.Country; 7 | import com.patloew.countries.data.remote.CountryApi; 8 | import com.patloew.countries.ui.main.recyclerview.CountryAdapter; 9 | import com.patloew.countries.ui.main.viewpager.CountriesView; 10 | import com.patloew.countries.ui.main.viewpager.all.AllCountriesViewModel; 11 | 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.MockitoAnnotations; 18 | import org.mockito.invocation.InvocationOnMock; 19 | import org.mockito.stubbing.Answer; 20 | import org.powermock.api.mockito.PowerMockito; 21 | import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; 22 | import org.powermock.modules.junit4.PowerMockRunner; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | import io.reactivex.Observable; 28 | import io.reactivex.Single; 29 | 30 | import static org.mockito.Mockito.doReturn; 31 | import static org.mockito.Mockito.times; 32 | import static org.mockito.Mockito.verify; 33 | import static org.mockito.Mockito.verifyZeroInteractions; 34 | 35 | /* Copyright 2016 Patrick Löwenstein 36 | * 37 | * Licensed under the Apache License, Version 2.0 (the "License"); 38 | * you may not use this file except in compliance with the License. 39 | * You may obtain a copy of the License at 40 | * 41 | * http://www.apache.org/licenses/LICENSE-2.0 42 | * 43 | * Unless required by applicable law or agreed to in writing, software 44 | * distributed under the License is distributed on an "AS IS" BASIS, 45 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | * See the License for the specific language governing permissions and 47 | * limitations under the License. */ 48 | 49 | @RunWith(PowerMockRunner.class) 50 | @PrepareOnlyThisForTest({ CountryAdapter.class, RecyclerView.Adapter.class }) 51 | public class AllCountriesViewModelUnitTest { 52 | 53 | @Rule RxSchedulersOverrideRule rxSchedulersOverrideRule = new RxSchedulersOverrideRule(); 54 | 55 | @Mock CountryApi countryApi; 56 | CountryAdapter adapter; 57 | @Mock CountryRepo countryRepo; 58 | 59 | @Mock CountriesView countriesView; 60 | AllCountriesViewModel allCountriesViewModel; 61 | 62 | @Before 63 | public void setup() { 64 | MockitoAnnotations.initMocks(this); 65 | 66 | adapter = PowerMockito.mock(CountryAdapter.class); 67 | PowerMockito.doAnswer(new Answer() { 68 | @Override 69 | public Object answer(InvocationOnMock invocation) throws Throwable { 70 | return null; 71 | } 72 | }).when((RecyclerView.Adapter)adapter).notifyDataSetChanged(); 73 | 74 | doReturn(Observable.never()).when(countryRepo).getFavoriteChangeObservable(); 75 | 76 | allCountriesViewModel = new AllCountriesViewModel(adapter, countryApi, countryRepo); 77 | allCountriesViewModel.attachView(countriesView, null); 78 | } 79 | 80 | @Test 81 | public void onRefresh_success() { 82 | List countryList = new ArrayList<>(); 83 | countryList.add(new Country()); 84 | 85 | doReturn(Single.just(countryList)).when(countryApi).getAllCountries(); 86 | 87 | allCountriesViewModel.reloadData(); 88 | 89 | verify(adapter, times(1)).setCountryList(countryList); 90 | verify(adapter, times(1)).notifyDataSetChanged(); 91 | verify(countriesView, times(1)).onRefresh(true); 92 | } 93 | 94 | @Test 95 | public void onRefresh_error() { 96 | doReturn(Single.error(new RuntimeException("Error getting countries"))).when(countryApi).getAllCountries(); 97 | 98 | allCountriesViewModel.reloadData(); 99 | 100 | verifyZeroInteractions(adapter); 101 | verify(countriesView, times(1)).onRefresh(false); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/test/java/com/patloew/countries/BaseCountryViewModelUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.patloew.countries; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.net.Uri; 7 | import android.view.View; 8 | 9 | import com.patloew.countries.data.local.CountryRepo; 10 | import com.patloew.countries.data.model.Country; 11 | import com.patloew.countries.ui.base.navigator.Navigator; 12 | import com.patloew.countries.ui.base.view.MvvmView; 13 | import com.patloew.countries.ui.main.recyclerview.CountryViewModel; 14 | 15 | import org.junit.Before; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.mockito.Matchers; 20 | import org.mockito.Mock; 21 | import org.mockito.Mockito; 22 | import org.mockito.MockitoAnnotations; 23 | import org.mockito.invocation.InvocationOnMock; 24 | import org.mockito.stubbing.Answer; 25 | import org.powermock.api.mockito.PowerMockito; 26 | import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; 27 | import org.powermock.modules.junit4.PowerMockRunner; 28 | import org.powermock.reflect.Whitebox; 29 | 30 | import static org.mockito.ArgumentMatchers.any; 31 | import static org.mockito.ArgumentMatchers.nullable; 32 | import static org.mockito.Mockito.doReturn; 33 | import static org.mockito.Mockito.verify; 34 | import static org.mockito.Mockito.when; 35 | 36 | /* Copyright 2016 Patrick Löwenstein 37 | * 38 | * Licensed under the Apache License, Version 2.0 (the "License"); 39 | * you may not use this file except in compliance with the License. 40 | * You may obtain a copy of the License at 41 | * 42 | * http://www.apache.org/licenses/LICENSE-2.0 43 | * 44 | * Unless required by applicable law or agreed to in writing, software 45 | * distributed under the License is distributed on an "AS IS" BASIS, 46 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 47 | * See the License for the specific language governing permissions and 48 | * limitations under the License. 49 | * 50 | * FILE MODIFIED 2017 Tailored Media GmbH */ 51 | 52 | @RunWith(PowerMockRunner.class) 53 | @PrepareOnlyThisForTest({Uri.class}) 54 | public class BaseCountryViewModelUnitTest { 55 | 56 | @Rule RxSchedulersOverrideRule rxSchedulersOverrideRule = new RxSchedulersOverrideRule(); 57 | 58 | @Mock Context ctx; 59 | @Mock PackageManager packageManager; 60 | @Mock CountryRepo countryRepo; 61 | @Mock View view; 62 | 63 | @Mock MvvmView mvvmView; 64 | @Mock Navigator navigator; 65 | CountryViewModel countryViewModel; 66 | 67 | Country internalCountry = new Country(); 68 | 69 | @Before 70 | public void setup() throws PackageManager.NameNotFoundException { 71 | MockitoAnnotations.initMocks(this); 72 | 73 | when(ctx.getApplicationContext()).thenReturn(ctx); 74 | when(ctx.getPackageManager()).thenReturn(packageManager); 75 | //noinspection WrongConstant 76 | when(packageManager.getPackageInfo(Matchers.anyString(), Matchers.anyInt())).thenReturn(null); 77 | 78 | countryViewModel = new CountryViewModel(ctx, navigator, countryRepo); 79 | countryViewModel.attachView(mvvmView, null); 80 | 81 | Whitebox.setInternalState(countryViewModel, "country", internalCountry); 82 | } 83 | 84 | @Test 85 | public void onMapClick_startActivity() { 86 | final Uri uri = Mockito.mock(Uri.class); 87 | PowerMockito.mockStatic(Uri.class, new Answer() { 88 | @Override 89 | public Object answer(InvocationOnMock invocation) throws Throwable { 90 | return uri; 91 | } 92 | }); 93 | countryViewModel.onMapClick(); 94 | verify(navigator).startActivity(Matchers.eq(Intent.ACTION_VIEW), Matchers.eq(uri)); 95 | } 96 | 97 | @Test 98 | public void onBookmarkClick_wasBookmarked() { 99 | Country country = new Country(); 100 | doReturn(country).when(countryRepo).getByField(Matchers.anyString(), nullable(String.class), Matchers.anyBoolean()); 101 | doReturn(country).when(countryRepo).detach(country); 102 | 103 | countryViewModel.onBookmarkClick(); 104 | verify(countryRepo).delete(country); 105 | } 106 | 107 | @Test 108 | public void onBookmarkClick_wasNotBookmarked() { 109 | doReturn(null).when(countryRepo).getByField(Matchers.anyString(), Matchers.anyString(), Matchers.anyBoolean()); 110 | 111 | countryViewModel.onBookmarkClick(); 112 | verify(countryRepo).save(internalCountry); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/test/java/com/patloew/countries/FavoriteCountriesViewModelUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.patloew.countries; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | 5 | import com.patloew.countries.data.local.CountryRepo; 6 | import com.patloew.countries.data.model.Country; 7 | import com.patloew.countries.ui.main.recyclerview.CountryAdapter; 8 | import com.patloew.countries.ui.main.viewpager.CountriesView; 9 | import com.patloew.countries.ui.main.viewpager.favorites.FavoriteCountriesViewModel; 10 | 11 | import org.junit.Before; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Matchers; 16 | import org.mockito.Mock; 17 | import org.mockito.MockitoAnnotations; 18 | import org.mockito.invocation.InvocationOnMock; 19 | import org.mockito.stubbing.Answer; 20 | import org.powermock.api.mockito.PowerMockito; 21 | import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest; 22 | import org.powermock.modules.junit4.PowerMockRunner; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | import io.reactivex.Flowable; 28 | import io.reactivex.Observable; 29 | import io.realm.RealmResults; 30 | import io.realm.Sort; 31 | 32 | import static org.mockito.Mockito.doReturn; 33 | import static org.mockito.Mockito.never; 34 | import static org.mockito.Mockito.times; 35 | import static org.mockito.Mockito.verify; 36 | 37 | /* Copyright 2016 Patrick Löwenstein 38 | * 39 | * Licensed under the Apache License, Version 2.0 (the "License"); 40 | * you may not use this file except in compliance with the License. 41 | * You may obtain a copy of the License at 42 | * 43 | * http://www.apache.org/licenses/LICENSE-2.0 44 | * 45 | * Unless required by applicable law or agreed to in writing, software 46 | * distributed under the License is distributed on an "AS IS" BASIS, 47 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 48 | * See the License for the specific language governing permissions and 49 | * limitations under the License. */ 50 | 51 | @RunWith(PowerMockRunner.class) 52 | @PrepareOnlyThisForTest({ RealmResults.class, CountryAdapter.class, RecyclerView.Adapter.class }) 53 | public class FavoriteCountriesViewModelUnitTest { 54 | 55 | @Rule 56 | RxSchedulersOverrideRule rxSchedulersOverrideRule = new RxSchedulersOverrideRule(); 57 | 58 | @Mock CountryRepo countryRepo; 59 | CountryAdapter adapter; 60 | 61 | @Mock CountriesView mainActivityView; 62 | FavoriteCountriesViewModel favoriteCountriesViewModel; 63 | 64 | @Before 65 | public void setup() { 66 | MockitoAnnotations.initMocks(this); 67 | 68 | adapter = PowerMockito.mock(CountryAdapter.class); 69 | PowerMockito.doAnswer(new Answer() { 70 | @Override 71 | public Object answer(InvocationOnMock invocation) throws Throwable { 72 | return null; 73 | } 74 | }).when((RecyclerView.Adapter)adapter).notifyDataSetChanged(); 75 | 76 | favoriteCountriesViewModel = new FavoriteCountriesViewModel(adapter, countryRepo); 77 | } 78 | 79 | @Test 80 | public void onRealmChangeListener_threeTimes() { 81 | List countryList = new ArrayList<>(0); 82 | 83 | doReturn(Flowable.just(countryList, countryList, countryList)).when(countryRepo).findAllSortedWithChanges(Matchers.anyString(), Matchers.any(Sort.class)); 84 | 85 | favoriteCountriesViewModel.attachView(mainActivityView, null); 86 | 87 | verify(adapter, times(3)).setCountryList(countryList); 88 | verify(adapter, times(3)).notifyDataSetChanged(); 89 | verify(mainActivityView, times(3)).onRefresh(true); 90 | 91 | favoriteCountriesViewModel.detachView(); 92 | } 93 | 94 | @Test 95 | public void onRealmChangeListener_never() { 96 | List countryList = new ArrayList<>(0); 97 | 98 | doReturn(Flowable.empty()).when(countryRepo).findAllSortedWithChanges(Matchers.anyString(), Matchers.any(Sort.class)); 99 | 100 | favoriteCountriesViewModel.attachView(mainActivityView, null); 101 | 102 | verify(adapter, never()).setCountryList(countryList); 103 | verify(adapter, never()).notifyDataSetChanged(); 104 | verify(mainActivityView, never()).onRefresh(true); 105 | 106 | favoriteCountriesViewModel.detachView(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/test/java/com/patloew/countries/RxSchedulersOverrideRule.kt: -------------------------------------------------------------------------------- 1 | package com.patloew.countries 2 | 3 | 4 | import io.reactivex.Scheduler 5 | import io.reactivex.android.plugins.RxAndroidPlugins 6 | import io.reactivex.functions.Function 7 | import io.reactivex.plugins.RxJavaPlugins 8 | import io.reactivex.schedulers.Schedulers 9 | import org.junit.rules.TestRule 10 | import org.junit.runner.Description 11 | import org.junit.runners.model.Statement 12 | 13 | /* Copyright 2015 Ribot Ltd. 14 | 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | 27 | 28 | FILE CHANGED 2016 by Patrick Löwenstein */ 29 | 30 | class RxSchedulersOverrideRule : TestRule { 31 | 32 | override fun apply(base: Statement, description: Description): Statement { 33 | return object : Statement() { 34 | @Throws(Throwable::class) 35 | override fun evaluate() { 36 | val schedulerFunction = Function { Schedulers.trampoline() } 37 | 38 | RxAndroidPlugins.reset() 39 | RxAndroidPlugins.setMainThreadSchedulerHandler(schedulerFunction) 40 | 41 | RxJavaPlugins.reset() 42 | RxJavaPlugins.setIoSchedulerHandler(schedulerFunction) 43 | RxJavaPlugins.setComputationSchedulerHandler(schedulerFunction) 44 | RxJavaPlugins.setNewThreadSchedulerHandler(schedulerFunction) 45 | 46 | base.evaluate() 47 | 48 | RxAndroidPlugins.reset() 49 | RxJavaPlugins.reset() 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | apply plugin: 'com.github.ben-manes.versions' 3 | 4 | buildscript { 5 | ext.kotlinVersion = '1.2.30' 6 | ext.realmVersion = '4.3.4' 7 | ext.gradlePluginVersion = '3.0.1' 8 | ext.mockitoVersion = '2.15.0' 9 | ext.powerMockVersion = '2.0.0-beta.5' 10 | ext.supportLibVersion = '27.1.0' 11 | ext.retrofitVersion = '2.3.0' 12 | ext.okHttpVersion = '3.10.0' 13 | ext.daggerVersion = '2.15' 14 | ext.paperParcelVersion = '2.0.4' 15 | 16 | repositories { 17 | jcenter() 18 | google() 19 | } 20 | 21 | dependencies { 22 | classpath "com.android.tools.build:gradle:$gradlePluginVersion" 23 | classpath "io.realm:realm-gradle-plugin:$realmVersion" 24 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 25 | classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' 26 | } 27 | } 28 | 29 | allprojects { 30 | repositories { 31 | jcenter() 32 | google() 33 | } 34 | } 35 | 36 | task clean(type: Delete) { 37 | delete rootProject.buildDir 38 | } 39 | -------------------------------------------------------------------------------- /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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | org.gradle.jvmargs=-Xmx2048M 15 | kotlin.incremental=true 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patloew/countries/615fa4851705bd48c1dd73f4147bc46a52a7d55c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------