From 6029facf73b7c0384e820e43e26ecf109f158b31 Mon Sep 17 00:00:00 2001 From: Melih Aksoy Date: Mon, 1 Jul 2019 15:56:55 +0200 Subject: [PATCH] Initial commit --- .gitignore | 11 + README.md | 1 + app/.gitignore | 1 + app/build.gradle | 40 + app/proguard-rules.pro | 28 + app/src/main/AndroidManifest.xml | 36 + .../kotlin/com/melih/rocketscience/App.kt | 23 + .../melih/rocketscience/di/AppComponent.kt | 20 + .../com/melih/rocketscience/di/AppModule.kt | 26 + .../com/melih/rocketscience/di/AppScope.kt | 6 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 171 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/strings.xml | 3 + build.gradle | 39 + core/.gitignore | 1 + core/build.gradle | 32 + core/consumer-rules.pro | 0 core/proguard-rules.pro | 21 + core/src/main/AndroidManifest.xml | 5 + .../kotlin/com/melih/core/actions/Actions.kt | 16 + .../melih/core/base/lifecycle/BaseActivity.kt | 58 + .../core/base/lifecycle/BaseDaggerFragment.kt | 44 + .../melih/core/base/lifecycle/BaseFragment.kt | 44 + .../core/base/recycler/BaseListAdapter.kt | 72 + .../core/base/viewmodel/BaseViewModel.kt | 89 ++ .../kotlin/com/melih/core/di/CoreComponent.kt | 22 + .../kotlin/com/melih/core/di/CoreModule.kt | 26 + .../com/melih/core/di/ViewModelFactory.kt | 30 + .../com/melih/core/di/keys/ViewModelKey.kt | 9 + .../melih/core/extensions/BindingAdapters.kt | 19 + .../core/extensions/LifecycleExtensions.kt | 32 + .../com/melih/core/utils/SnackbarBehaviour.kt | 23 + core/src/main/res/values/colors.xml | 7 + core/src/main/res/values/dimens.xml | 5 + core/src/main/res/values/strings.xml | 15 + core/src/main/res/values/styles.xml | 35 + core/src/main/res/values/values.xml | 4 + .../com/melih/core/BaseTestWithMainThread.kt | 49 + .../com/melih/core/base/BaseViewModelTest.kt | 37 + .../observers/OneShotObserverWithLifecycle.kt | 34 + default-detekt-config.yml | 523 +++++++ features/detail/.gitignore | 1 + features/detail/build.gradle | 22 + features/detail/consumer-rules.pro | 0 features/detail/proguard-rules.pro | 21 + features/detail/sampledata/launches.json | 1239 +++++++++++++++++ features/detail/src/main/AndroidManifest.xml | 2 + .../com/melih/detail/di/DetailContributor.kt | 23 + .../melih/detail/di/modules/DetailBinds.kt | 22 + .../com/melih/detail/ui/DetailActivity.kt | 39 + .../com/melih/detail/ui/DetailFragment.kt | 62 + .../com/melih/detail/ui/DetailViewModel.kt | 56 + .../src/main/res/layout/activity_detail.xml | 22 + .../src/main/res/layout/fragment_detail.xml | 75 + .../src/main/res/navigation/nav_detail.xml | 18 + .../detail/src/main/res/values/strings.xml | 3 + .../melih/detail/BaseTestWithMainThread.kt | 28 + .../com/melih/detail/DetailViewModelTest.kt | 42 + features/list/.gitignore | 1 + features/list/build.gradle | 14 + features/list/consumer-rules.pro | 0 features/list/proguard-rules.pro | 21 + features/list/sampledata/launches.json | 1239 +++++++++++++++++ features/list/src/main/AndroidManifest.xml | 1 + .../com/melih/list/di/LaunchesContributor.kt | 25 + .../melih/list/di/modules/LaunchesBinds.kt | 22 + .../melih/list/di/modules/LaunchesProvides.kt | 16 + .../com/melih/list/ui/LaunchesActivity.kt | 28 + .../com/melih/list/ui/LaunchesAdapter.kt | 41 + .../com/melih/list/ui/LaunchesFragment.kt | 114 ++ .../com/melih/list/ui/LaunchesViewModel.kt | 38 + .../list/src/main/res/anim/item_enter.xml | 22 + .../src/main/res/anim/layout_item_enter.xml | 5 + .../src/main/res/layout/activity_launches.xml | 22 + .../src/main/res/layout/fragment_launches.xml | 35 + .../list/src/main/res/layout/row_launch.xml | 77 + .../src/main/res/menu/menu_rocket_list.xml | 11 + .../src/main/res/navigation/nav_launches.xml | 13 + .../list/src/main/res/values/integers.xml | 4 + features/list/src/main/res/values/strings.xml | 4 + .../com/melih/list/BaseTestWithMainThread.kt | 28 + .../com/melih/list/LaunchesViewModelTest.kt | 33 + gradle.properties | 26 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++ gradlew.bat | 84 ++ repository/.gitignore | 1 + repository/build.gradle | 30 + repository/proguard-rules.pro | 0 repository/src/main/AndroidManifest.xml | 2 + .../kotlin/com/melih/repository/Constants.kt | 5 + .../kotlin/com/melih/repository/Repository.kt | 13 + .../melih/repository/entities/LaunchEntity.kt | 17 + .../repository/entities/LaunchesEntity.kt | 9 + .../repository/entities/LocationEntity.kt | 17 + .../repository/entities/MissionEntity.kt | 12 + .../melih/repository/entities/RocketEntity.kt | 14 + .../interactors/GetLaunchDetails.kt | 27 + .../repository/interactors/GetLaunches.kt | 27 + .../interactors/base/BaseInteractor.kt | 41 + .../repository/interactors/base/Reason.kt | 19 + .../repository/interactors/base/Result.kt | 45 + .../com/melih/repository/network/Api.kt | 19 + .../com/melih/repository/network/ApiImpl.kt | 43 + .../persistence/LaunchesDatabase.kt | 30 + .../persistence/converters/BaseConverter.kt | 29 + .../converters/BaseListConverter.kt | 29 + .../converters/LocationConverter.kt | 13 + .../converters/MissionConverter.kt | 20 + .../persistence/converters/RocketConverter.kt | 13 + .../repository/persistence/dao/LaunchesDao.kt | 44 + .../melih/repository/sources/NetworkSource.kt | 106 ++ .../repository/sources/PersistenceSource.kt | 44 + .../melih/repository/sources/SourceManager.kt | 55 + repository/src/main/res/values/strings.xml | 9 + .../interactors/base/BaseInteractorTest.kt | 67 + .../repository/interactors/base/ResultTest.kt | 65 + .../repository/sources/NetworkSourceTest.kt | 102 ++ .../sources/PersistanceSourceTest.kt | 56 + .../repository/sources/SourceManagerTest.kt | 141 ++ scripts/default_android_config.gradle | 13 + scripts/default_dependencies.gradle | 16 + scripts/dependencies.gradle | 146 ++ scripts/detekt.gradle | 14 + scripts/dokka.gradle | 10 + scripts/feature_module.gradle | 16 + scripts/flavors.gradle | 17 + scripts/module.gradle | 3 + scripts/sources.gradle | 12 + settings.gradle | 2 + 143 files changed, 6891 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/kotlin/com/melih/rocketscience/App.kt create mode 100644 app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt create mode 100644 app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt create mode 100644 app/src/main/kotlin/com/melih/rocketscience/di/AppScope.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/strings.xml create mode 100644 build.gradle create mode 100644 core/.gitignore create mode 100644 core/build.gradle create mode 100644 core/consumer-rules.pro create mode 100644 core/proguard-rules.pro create mode 100644 core/src/main/AndroidManifest.xml create mode 100644 core/src/main/kotlin/com/melih/core/actions/Actions.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/lifecycle/BaseActivity.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt create mode 100644 core/src/main/kotlin/com/melih/core/di/CoreComponent.kt create mode 100644 core/src/main/kotlin/com/melih/core/di/CoreModule.kt create mode 100644 core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt create mode 100644 core/src/main/kotlin/com/melih/core/di/keys/ViewModelKey.kt create mode 100644 core/src/main/kotlin/com/melih/core/extensions/BindingAdapters.kt create mode 100644 core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt create mode 100644 core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt create mode 100644 core/src/main/res/values/colors.xml create mode 100644 core/src/main/res/values/dimens.xml create mode 100644 core/src/main/res/values/strings.xml create mode 100644 core/src/main/res/values/styles.xml create mode 100644 core/src/main/res/values/values.xml create mode 100644 core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt create mode 100644 core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt create mode 100644 core/src/test/kotlin/com/melih/core/observers/OneShotObserverWithLifecycle.kt create mode 100644 default-detekt-config.yml create mode 100644 features/detail/.gitignore create mode 100644 features/detail/build.gradle create mode 100644 features/detail/consumer-rules.pro create mode 100644 features/detail/proguard-rules.pro create mode 100644 features/detail/sampledata/launches.json create mode 100644 features/detail/src/main/AndroidManifest.xml create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt create mode 100644 features/detail/src/main/res/layout/activity_detail.xml create mode 100644 features/detail/src/main/res/layout/fragment_detail.xml create mode 100644 features/detail/src/main/res/navigation/nav_detail.xml create mode 100644 features/detail/src/main/res/values/strings.xml create mode 100644 features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt create mode 100644 features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt create mode 100644 features/list/.gitignore create mode 100644 features/list/build.gradle create mode 100644 features/list/consumer-rules.pro create mode 100644 features/list/proguard-rules.pro create mode 100644 features/list/sampledata/launches.json create mode 100644 features/list/src/main/AndroidManifest.xml create mode 100644 features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt create mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt create mode 100644 features/list/src/main/res/anim/item_enter.xml create mode 100644 features/list/src/main/res/anim/layout_item_enter.xml create mode 100644 features/list/src/main/res/layout/activity_launches.xml create mode 100644 features/list/src/main/res/layout/fragment_launches.xml create mode 100644 features/list/src/main/res/layout/row_launch.xml create mode 100644 features/list/src/main/res/menu/menu_rocket_list.xml create mode 100644 features/list/src/main/res/navigation/nav_launches.xml create mode 100644 features/list/src/main/res/values/integers.xml create mode 100644 features/list/src/main/res/values/strings.xml create mode 100644 features/list/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt create mode 100644 features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 repository/.gitignore create mode 100644 repository/build.gradle create mode 100644 repository/proguard-rules.pro create mode 100644 repository/src/main/AndroidManifest.xml create mode 100644 repository/src/main/kotlin/com/melih/repository/Constants.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/Repository.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/network/Api.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt create mode 100644 repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt create mode 100644 repository/src/main/res/values/strings.xml create mode 100644 repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt create mode 100644 repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt create mode 100644 repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt create mode 100644 repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt create mode 100644 repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt create mode 100644 scripts/default_android_config.gradle create mode 100644 scripts/default_dependencies.gradle create mode 100644 scripts/dependencies.gradle create mode 100644 scripts/detekt.gradle create mode 100644 scripts/dokka.gradle create mode 100644 scripts/feature_module.gradle create mode 100644 scripts/flavors.gradle create mode 100644 scripts/module.gradle create mode 100644 scripts/sources.gradle create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84d7b28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild + +# Project reports +/reports diff --git a/README.md b/README.md new file mode 100644 index 0000000..05d7f6d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Rocket Science diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..b99eac8 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/sources.gradle" +apply from: "$rootProject.projectDir/scripts/flavors.gradle" + +android { + defaultConfig { + applicationId "com.melih.rocketscience" + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + dataBinding { + enabled = true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':core') + implementation project(':features:list') + implementation project(':features:detail') + + implementation libraries.coroutines + implementation libraries.navigation + + debugImplementation libraries.leakCanary + + androidTestImplementation testLibraries.espresso + + // These libraries required by dagger to create dependency graph, but not by app + compileOnly libraries.retrofit + compileOnly libraries.room + compileOnly libraries.paging +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..da2a358 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,28 @@ +#### OkHttp, Retrofit and Moshi +-dontwarn okhttp3.** +-dontwarn retrofit2.Platform$Java8 +-dontwarn okio.** +-dontwarn javax.annotation.** +-keepclasseswithmembers class * { + @retrofit2.http.* ; +} +-keepclasseswithmembers class * { + @com.squareup.moshi.* ; +} +-keep @com.squareup.moshi.JsonQualifier interface * +-dontwarn org.jetbrains.annotations.** +-keep class kotlin.Metadata { *; } +-keepclassmembers class kotlin.Metadata { + public ; +} + +-keepclassmembers class * { + @com.squareup.moshi.FromJson ; + @com.squareup.moshi.ToJson ; +} + +-keepnames @kotlin.Metadata class com.myapp.packagename.model.** +-keep class com.myapp.packagnename.model.** { *; } + +# Keeping entities intact +-keep class com.melih.repository.entities.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..edb02fb --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/kotlin/com/melih/rocketscience/App.kt b/app/src/main/kotlin/com/melih/rocketscience/App.kt new file mode 100644 index 0000000..b2d8e13 --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/App.kt @@ -0,0 +1,23 @@ +package com.melih.rocketscience + +import com.melih.core.di.DaggerCoreComponent +import com.melih.rocketscience.di.DaggerAppComponent +import dagger.android.AndroidInjector +import dagger.android.DaggerApplication +import timber.log.Timber + +class App : DaggerApplication() { + override fun applicationInjector(): AndroidInjector = + DaggerAppComponent.factory() + .create( + DaggerCoreComponent.factory() + .create(this) + ) + + + override fun onCreate() { + super.onCreate() + + Timber.plant(Timber.DebugTree()) + } +} diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt new file mode 100644 index 0000000..3ed9830 --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt @@ -0,0 +1,20 @@ +package com.melih.rocketscience.di + +import com.melih.core.di.CoreComponent +import com.melih.rocketscience.App +import dagger.Component +import dagger.android.AndroidInjectionModule +import dagger.android.AndroidInjector + +@AppScope +@Component( + modules = [AndroidInjectionModule::class, AppModule::class], + dependencies = [CoreComponent::class] +) +interface AppComponent : AndroidInjector { + + @Component.Factory + interface Factory { + fun create(component: CoreComponent): AppComponent + } +} diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt new file mode 100644 index 0000000..c80bd10 --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt @@ -0,0 +1,26 @@ +package com.melih.rocketscience.di + +import com.melih.detail.di.DetailContributor +import com.melih.detail.ui.DetailActivity +import com.melih.list.di.LaunchesContributor +import com.melih.list.ui.LaunchesActivity +import dagger.Module +import dagger.android.ContributesAndroidInjector + +@Module +abstract class AppModule { + + @ContributesAndroidInjector( + modules = [ + LaunchesContributor::class + ] + ) + abstract fun launchesActivity(): LaunchesActivity + + @ContributesAndroidInjector( + modules = [ + DetailContributor::class + ] + ) + abstract fun detailActivity(): DetailActivity +} diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppScope.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppScope.kt new file mode 100644 index 0000000..38a7fd0 --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppScope.kt @@ -0,0 +1,6 @@ +package com.melih.rocketscience.di + +import javax.inject.Scope + +@Scope +annotation class AppScope diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..6348baa --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..c206d43 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..898f3ed59ac9f3248734a00e5902736c9367d455 GIT binary patch literal 2963 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 JPDHLkV1g@XpTGbB literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dffca3601eba7bf5f409bdd520820e2eb5122c75 GIT binary patch literal 4905 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 lV%Z2*hu);6rydA(}wUDXPCF_W1vnaRBK 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..14ed0af35023e4f1901cf03487b6c524257b8483 GIT binary patch literal 6895 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b0907cac3bfd8fbfdc46e1108247f0a1055387ec GIT binary patch literal 6387 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ae03154975f397f8ed1b84f2d4bf9783ecfa26 GIT binary patch literal 10413 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 XdJ+W}DiIkV00000NkvXXu0mjfKBlg6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c18de9e66108411737e910f5c1972476f03ddbf GIT binary patch literal 9128 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 I20H400LcNok^lez literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..beed3cdd2c32af5114a7dc70b9ef5b698eb8797e GIT binary patch literal 15132 zcmZvDWmr_-8||54h>`B@4yC)hOQZ#cM!EzfhmdZRPLWXQlpaz*O1gvrk&^D_^84TW z@jlOq4`=WFp4extwb#3MjEilFPELs0YL1Js)Fn* zzr}qsbfZ_wbNOa4S@vf>;bE~>+%RD!>v%IFV#WTd^7(B=#T|Xno7mV6xS4f=u6692 zQq~7{i;;}Y46D{(Y+R?~SpnS3W=+e#JKDJX-SSUi>9(#}mwE5Tv-r0dn5ZY||9_k1 zWM~Q&Gt=O&6oAqZ3T;9&9$g)JWBOFs0NWF6vYJZJ24_?zn}`jXIHjr$^?F69z!2p< zy%t?XyTRP;!zMXPY^&6kR$$J?UW%?3bCC4XDqr@?ukqAzCEf6lUi%~QE1bZLYf8h# zNIFjy{z&gk+iBasaZQZklPN%Bhl~H-pewWJX`t_4w;I)?=gcrEWq1%u$-pwhg=Fn& zj3nJfbY`j%G4F^8@$CZRg?Lweh*w;b>{2YdOIAi*x9?W^yUNovn|q?NJ#6TPeU_fVowC-#v9#b~gYH6zAw5m28>MUeJ4Tj* znIVgljj#XhW$ zhiz?z_2X4xbgPrk6@%1I-IDPigjXj6D_rk=N!MHKhrgxgN|sX9wAG{r8mKBc5uYx! zD6;oWKPFPVaeKY+;_tfGk8dnA3*mxhD6c6ylsqfXvWFU-T3PF_*(Y_!aR4ycp@UiK zL{0B(1-*H{F=ezF{RJj(g)4PzJx50@A1Bg2>XU|TM&*KjHze0G!vbN}?9#L0`)Mh& zSDg1vm!sTu701b=n&--{Q{n2DpuDb{%No!D^gwg^bAW&J!~L20v4&-T0QrdY*80B?ozklkW% z0rk7=VB9&#oB_RdT&RhUD^ z<%mehua9i+?=)hn7$VmdJdx(xObB8b; zd)9+r z`yz+r{dSM5hDz=4ys1#(+WoWqC+KtBRNG8x2R zkNK+s#C-E*)s>kZCpyIRfB`}hQ6FwUXyKlgYs)!v{kjY>{yEe5^Qr5JEe^d*zcU@; zK#oE%1w&_PZ%A@P#G}S>`1qbU0tkHPO<2-5_Uhe0Y6$FovD9c;Ov~qVD?l$$zpcmn z8BGk}4~3UeEkzOUc<9FqtY1TqoY%qGS&?kSM=O3g}NY85}H(VQS~6J6eJsX=%$ zf%etV-q-i9X(#Qm$6xDNs6>@0-*1b4*6TC?1v|R@FkpbQLy%N<#0-I&1swvEMn?Y( zQKWmqz2#a=uq>R|^cdhnkaB3z*DB@@Q=Jpj%9EBXLuo{WDl~W0E}qH^aARnpD#`Dn zAO=+iepMRRSE1j%9nTDc{=3ACQK(De^37Zvsl54F9`aO8G+M-hmV$3r9l|3HavVov z=cO%-IOVsvo}L%}Jm> zX9gR60KV3P&h$KA;XH%c12K@uFzJy5i9S6?U7BKXLk4&WhD>E$HbfP_Ojp5OF9rfm zT$`)n#dWaGB<22Cl)AZ@Gv7i0;!*>IUJv7##H1X4+Wx!Jki<;jka&jGH6W2$nzJ4> z6yD|%yOMzcBZj~}DSWA5Qj5Q$P>edSrrCzs=X;k&irN=Q9KBAfO4RZ>klxjm*H%`2m5c(y7Pw zcP@DyYA!WftG!MB6T>V!I>_ym+&LEFyikRHI`-j@U5hGl(;JWZbO|orN^1|6{D4+0 z>5k@1pQ`!&UM0WB;(#4ds`}Zu6)B_YebI)X)jZRhJn}_frc0jF4SFi~JHS=t;knPP z&yEu(+8%qK>YIlcGahTfF6Ze^7edgT$J`6#2qm|n26OTFDY|d8s~3hl zpLtuXp@mq2GW8<6|E)D{#yU2)#iuPY!=|5Hmo-<*yo(QYr$3HQqx#%vtHjS|I7NiRxC6lDQq< zTXIalFx_Ncd(TZ(!iRaFymyh~tc4h-VJo_vaMKP(y_b-@V9j{@6aA&=*?g2r3#HBa z-Q(IP$--;P*a%%PO{^%D$`G{5nl&>sUgEN|s^PG}Jh>ISvD%;O|psp}p`-pKAK?pbIHTV?a9?u}(q*GCDRrVm> z0lC9`wd;C96R!Yg%?DnK2`W*_@jf%9IPnwdr@BgGxWS)z)J>cDasy)mt3Y7)p=txP zM)#~H^+!85n&7b%$l{U`iUrdD?1+BT#+yClM)OQek##8!6GFE0paMGl~ znJT5wR_VzqeBv^?U47rJ0!hXwG=8QSN^}EyUNDp2J?(D#FGFgCo^@;lRCMe2zczB^ zM%9XHn3ccHp;wqZ^Uy8mD<>D6R1W$5gqQ>%@AfWuiX0~?SIt2=9&6BS)f-v(V+-C6 zBfbm+ypV$sk2v=A1#JUeO~Sbved*o%-1Huvn%MCF?%m%fP5;xCPP|-(b1@laO;e4- zd6?k_0KN;j`6NXEVgi#X0MXBw38O@O`lZ=y4(f@Vx@QT9*Vpgk{{$@lzYwyh%?NrN zGtU^kn)F6?fKBPA{djTaw^L#(7F&HK0b>+C#os)3 zXBq#MC^QE6lzK^4733pD>UE36G;-{`GpU&0a|`(V-vTwp@G~>2EL6F$*&3YMPp-<3 z$pGu8`_-xR9b-}m{9;+irLXejrTbK_!ep%zGnh;U{^iGo^_=F2)RW>Gnr99OXB*dm zfO+ugGg0L-0>cKR_lG&~a#|_x2{kD1`&ncdCyi6M^Lm931EU`O+-XCCFYRAnjs5f6 zUa^V+z|fk5UB$rN`lRE$u7^I~$Cjw-;Cp6f)HA(2LU;};f)pd4T8-D?I2up+3G(m$&;vg0~+JOD};L`gqqk*eJg+xpbq{T}SE4${0xj>in~=ldQi1rE&?>CiYw2 z#vg0Xtv2hPZfP@t{cR}nkn`imMzN%Ni-Y?Fuhn*~A(k1`mx6vQI)vLRy&;WKU0n}B z@ZJ|)Fn=>TPu!<>B>2~#eYSLuW5D_)A)V?!{Y4XguE!i#eiyl1d{uE|RTBFea zM(g%RB^85qT#!n$qYwxcyR1CEXmt{nlJiLD0Zs8{OI%+d`MxVXSwT?e&2t6`t3 za4o!LrCv}!1now|E(qC6Hf>E@-0qF^3NbW7_qjxU<9CDT$8j)VXDt{8H;2Pzmw@Nb zJ}1NB7;d^GlLw5^EU`sTe0n9Pg~GmQIXwnxEAeh@zS%X#f?&FG!fvUXW1I^%m4Huq zFb9-|D>sEz%pg}Dy}4S#5$%jBg@1FfhQKlNSk?MlP{oDv8s=i*#C%7KTfKRpT((!vAA*0?h5%4doY~|3yq_DA32&6T2RHbNq-AItD)b&W z5)Ng>T|a!hlRxqb6(lwy3n#TR>Q{5$zoTQ(7Yp23btrx0L6lb;lMIld_ZsBm;X65W zhL~-DK~O*?iR1lG`e>ZDti=^0@Hu{22rk-ri$|Mhlfjx zz}x1wtNp{S65T4sftJev1F_{RMAe{B#a1+VB3lE#HN&bH7Rc8 z9d*c27p;2oA4ZYZSk)abazBuwEu8=L?5J?TG~{R3V8o868I?F z#Lt>o_|ohZd7psYl9Vtz6-np(@R&^Q6yKF@# zKK_Phwv=G^eE6%t(B0N4(**az{Z$|8Nab8SLz)m@0bPk@Wo;!3I&BJu}Fl z{}e^!Iy||DQ~DlD9=@%{OB>I8fpV4ZTC})4v8^-k&+wR4`hMI|wtCe3@xtk*M_gV& zT7}a{1ERd3c8RiWPPBvInQ4k+GPxSExF}CJt9v>(EoD>AsA|3ioYaprn4PVQ}7|zFbK2=iyU{SL8K#I2+N-*;IUC zGNwTD;XDPHkYcjzxc(jT?|J#?A9c3l*&Jc_`dkI4Rs7QC{PM6ty6TzkxCMvgm=@WZ zf59SoAflkydVV7?TYoT5`U(N`-HxGa2z_V)YRIz`HRRE3`12J1-lEtmojvMCPtH+1 z)V=IiqG9TR@`K%FOk2#6!1{1OD;*%xRAYo%)EDc|<)I;%EXi}?^()_B6K`pYE*`4Sg)tmZ&*^v8jAGJgK-rh(nO znii&AGyPojK+Ee9+EI?hH-rm&m>=`lAO7{E>D1JKm7n{&r&z%Cwi})WQZ*k0bJ6u=B0Pn1}ek~+ch_lXwn zuc_uu@YRZb$iGWq5BG|g|^Wd_oh(t2hEHAQ>~0CE_L3eNN1(NZ={TZ z*Q&K4gY{whUfZO+x8Pi73^^HTU(N+4u|z~}-7IGjQufEje1K4zazaTk96zyU#Oomt z{bZ_BZ#I(ren>G~3QNkj-ElHS()&+TCR+bjq4vO-*_o`jyU7mwVd?J!edfIxKubK~ znqmum7Gd^m1|fh?4|kW$?Yo6*!cTvq_fNlm%+Olmz3Wf^I(4mQ zO~z#3)9fPojD(VbPK-c6xq)}DM$borMa#X!P?x0&SBqzQG-BST1On6bd~bfeDWpmL zg;dMkgsT6muQ^9L>bR6T?+9!G07EA3XvMR&Q}8^MSfgNeA zEzFXFyts}my(yK#E3|dx>wH+PW-82HFn_p_ z{;sH%Izw2f?je+3ZGMKbJJ%-MUk6I$Q3lW`X#vZ{OC+X9zuDb|vQX4W2a2z2W*Oj)w$<7+lPbGYqEE4!Y z5j4*J(;o`UAc^wryi7M1qZAX{UySopT5y$cT@|8wdo0j-F+*z55(QN4-0X9E2(%0w z->Pj3_BQrPW?JjaUyorsqkqgQ;wow+pkug_qLB3byas`FE+^x`c+_Iv!A2o)GczmY zAV6d5;m~?7FDJ}pHp;5ORZwuDRq(s2BNghbg+aq0nsM$z_3LiUp~h}O&p9WQTkF%8 zM=j%0_<0RSBT*koU?wS=bWkoexJwQclztyKASoPa^=_gN4ebgz`-%PQ4pC%-=4Vq0 zfe#O}LUsDlrtPI4qXRa|3{g~nzfS$+u@EI(83`y$`zM*F4ZrP)V>J3FyYXx}ZGKDg zcnAHvt{Rs*n3G9nWAYgvN_?47{`Qg%8)$u7L&yUCg=`X~0xo?Nm zOT?BaawiXVZT^N9@PB8m9mlRme!pMhW#CUp&O)q1Ff49V5&%z22#hJ2F`M#8APaP0 z$_Rp4aJOUiQWa7(@mp|%WL)nG$d&Zv_rF<$bdOHX?n0#JYw}R-L?73ZR{Dh~d)_hC zut16KfP{BGRQ-I6p%4Q2bsb~&j&!tu<3}y`>iw3ht$>i661@OYn_Xr&XV#5d@S|oP zA@W{))lxW_UJQXd+s5{jYwPj)u*;o$QivH&LtwNF#bMPtindqcy_Sg_0jNOW`lS26z`VMFkJaH+Sv!=ug__rdCdmKpW)`?T6Ob{o>w!vsy+D z-B>}mgAw_|pUbN&6M&;nPF~<=LStpG+Z5n5r71uf?m?gQ-F4dx9x_V$5%CbECK$Gw zzJ2<^i95T446#0C`xOGneN913e!;7o!R%C)^uMCe0=Tn<*P?H{k7Z&~3QPz=NJW=T zj3CEU61-h1U6W|>zbw|;d_CCnt>k5|J0cEO>N_La+8&pSKU3E{M-On-Vw%ehQ{LlX zxIB8%LF!fTxKT!H6<|d62Qh9ehYjV*#xl%&Z~JpAI7ZChyU6I`b9k!^*geM*&r!)0 z`P_*C_$(P{7dfN3zXX2lZVtYo4StL|JW2|=e>3xO1G$K#=;n=dYTEcI0n01mkFdT* zZlxjCcP7Y5aQ>oPVpawo8YKRl#hc>oIaxO{*fKmVk?3H*sQ8bIy$$PNS zm^QUJj;!T<|8X&Tmhjigq?%e(ppMY%uLMndna;mU(!hA{kXVc%0H6AUgIMB;Y2q3as&sY398#kE0 zW83CIlm!|%OO&SzQ41d zS$iN9BrRi!79O=xyI?ngbQV~+RpO` zgt2WYwEdm=V<3qZ)gKkzTAP9Zf$LsE<)l0?cLpV{+UkiYYIQGnS~Bad;H{xUx0IA93P!Z$Ub zRs}&&XlPF1+UESgi+B-d`JNY2Bfq~xE9@Kpnx?;#;mg;m75vQ*?*d4Tztw|nTLS^Y zH-`iqEf>b-r);F3Q~_D`cZH$BGWu)siXg~pRDs3)1|az7kgqJm2#$NR_{p2Y23-4BY)ULyBEa^$KdzDc9uq0^ACB~H-gaD=Y4z@9VVD}V$kHmZY*Zd--RR|Y0w6WlPWsSq`9?!a)pOu312EGz zk4m+W%p>D^0mr(5WfHSjGm4$@-XbLhSU&;M=<@H`iuaG1?)qq49eVAA5|f{k5V){} z8uBYG8s*=a?&=i4q?=aPx<^%phdi8kO`X$JJFg~83BLUMcYF-+MJbGo^^{rW9Z@->vG69q4q3;`%j1PYG2lz1;eHLUAMDldZP&8yIZ=zAT!_W^5Gh_b#n%EiU zZ%Fin+oCFPL;K`A8?8xGtUp%fnKU^o)jCC>R2*P%Cfi#_LmHjMEJxhmc}|a?*)R;# zbyHfgLFFpb00`ZaHUnRQmT#aiiK}x0gu+pd23%n_RUjE4QhiC3{(j_k)DA`~jo|p# z#u5J(u73}=8;tpFvdM1RcA}^T|4=?G_T`x+6LdEhUm=K9erRBQI z%4?gf+wXzRB%6mX!*t}t3Kv1nsQ~!hZbTr0bFyUkaDfV!snDh2##9g(Hhul2EW747 zgi;TxQ%{3b>Mc4N=|y#vIG(4HW=>NnpTpmFun$Rj02m`#o`ex0ONfET z4F{r7@emkC;R~!#dbkG?-M#lhIS+y-buu?tP{T}iowTIQI|Q3D*0|PFM=K&Z8(ngl zIFhy237n_38l?NRLR4+dQiB2V$&rEkfgtk?a6l=H7ExIM41_<)P%KaggZNGFqMZAL zMY&tS8=|yPYSZZFA&!dSI@Tu^@(_*Fml5a%4cZC)7jK+63+eEuZ3PCX_~(AjQOo`= zNPnlQ)GVKn42^BzfT?X|&6O%hoWj^?UbjQVlhMl_0`x{xa=q49T>Mx-$^2R5#O^pn z>2!Sz?&CdJ65j%GFWASd4pIV3tzxpdURHySx^q=6dVRBZ3a7`JP?PSBjkcQPh@?pe)x&( zA66UTKY_1wx3-Ur8yZU zi(!nn?u&oDM9#cLFP7RGZ@liCG@JKro%!fz2GqHc@fk04klM@5*ths6nRZJ%lI|p) ztyuO1VIcggf?H~xX6i7k&p4~V9`G>zjntUEflyoQ^SD~$lBIr*#v)di`!hHHzZ~Wd zJ-QNEBRBq)fz4l2#_xXm8YV8KB%v!-2Is(P`1=|D+zIhS-F?ZUgd{4ZvFP};cKr74 zvi0T|HHv$hL!f3guj8b`g!f?>1v>B0gS~UEbJ?|HOB?fc^jFhtGDY1pfHBHP3X70`g0Pl;1%{(WPrw) zLA={hi)#y_&B|CHDe{&@tUa4*`Gx7EV=fZARJ1+2VgS0L3UZC@{Wc`R>bF^Y|J_=) z6@zu_xnjZE0yN`sSuL5S5%*$tR?_Sn;IN zk+q_-5?}{FkQtG0br0boxa+}qf_r@ocNJU^!H6bY#l--XDfxMU;d>>l#G-kxw=U|n z4oX{wIsAKre7G+PF-;OsE5di0T5MG_-(T zhUl%sTLJ_I(vT32H{#nS1y2{d~Bk*>z;1fMDT#15#7$-u6_Yo!o9QuS!|5#-{ zC0)T!;?6@2clqJa$)sMARqIYV;r+ zk0)L=B>56L%h)=EE^|VE0=oK*K#|t8- zuPFs$^fLQzLGuZ2ZmXe@id)*N@}ZDUnL1)Z8A52hime?+&Bx7u|5)K3ImXEMUQge< zM`(Zo{DDFnt^k6F1jF&@18xC^>12aHE)&2k zs@Nwb?4XI^>w*cbU-d#dTM%R#VlaWL2MW8>deH&l@xZNi1uJB>M`h5y{I|JcKhaAgcz;0;FDw2<~EhliI5igwCTS&^FLFZSoB$eD>H zD10LcRu|WoR}}rm2%pHJGsgh+eOu9q0~qG^b(v)v%8_%bfYg<>q0IYcTAhF-kNC49 zGRJPK;g!YDNi0#B-0xu-ox&gG{wQ(DTXtXWgzKH6KjnvR?85x$A$ZN+G0#8>XkFb9 z9zWb_5-`)TxAZ%jIz@ik!2)usZWY?tyjjOd<;04s^5^fjU8zy`7I$70NYN82zW6h| z$X=NbEUMsfM*!<{`)e40n^{H-)`KJX!(mZdv-cC!9L+JvSVnSO(VKcNP;t?UGtk!b zSPgVYsnD9ejE;FGyPg{6YW6R5Q$rGiy%J(H)2LXP4eT;Slga?wulT3;iy&;Ia=@Rj z!U(jtPyK}8ZWprMhYw6rMgQS66{Y=o_anEEOn1Vj*{8icX-1vaY{+vNoJDFj0{pO( zMG_NH%h3QMU|oF!Z9ocohL5ayn*Z36RiYk>2PU&{vAU1j? zkRdJ8tizF;3llfJ+zh|bK4_O(7pI-9w^Y4gTB0F9sU?J)5ad=AE{p>o;579Jw#@~5OWbag~+3Mnyph?f@wbwu8 z=fB{(_w#nycZtQsdzOuJ=!+1W3GvhPtLJ9m8OpCA&1MCEcLm9=MUSexJUgvMnqDuz zd3!`HT>912mxR#8IDT6FH+LT`QmrCDq@~pdJ?clm$SLSgUD~0uNXRqN&U+KZqw7Df zzDBzgap!mUAGRk7ciu7Jh?&{>=jdQn1ag0rfaz2*?e8k)dfhWih%4+tNn18&)E9RC<4z zeXoG((fW36d;|?kq_y=zW+bjMr=HBC9G6~Oz67sXY9iWf{^(T=lY^M^#K>_LyRTd# zP2auGUqc^`u^ubR5w4Vs@kxf)dChil)2=KRi>a|4o@pNTPdUTmaKG~`#_vwS6!#k6 z{+4VvCc;c#xdy8hCDR;Cl~`TpA&O_}1i*3^LT54QK|MZcr> z_WFbw0$>}L+Ody2Uo6A7WL7!Jjsi|{&4b%5B5BgX4~e|uY}|YIqYsLi98Q<{`IYRM zg6GJnsy+;=)vhXW#}ZcT6Xz)uFQxpe`U{DB-KsDH#Ubr*#odC)p9`{S*v9t${JC%W zNwRP4qvDI=x+u!)g-*90R-vYQbpgwWYEHiCSSi3znGDt6hfK_&?&t8e#l%}MMpBFl zxE>$Q97^qR@(KeM*(xar8JyGv7=1lKpu)}4U@!(Ggn@EP+h#cPr~OUH-`QqXhlhNd zjl-d^u9-i0$Gp!aVs!#8LeIRnr-PZYrSHxBwm7LpU-rGj%`%3{jJ$YGlC;!ih7QtL z?Zt!uX4Po`%PTiH$H>#58o08=3zvG`f%ntyD#+pAjuhI>e65GIil-1!j zY|&2)#*BgVwZTom3H=~rSH4u71~5Evh9-a_APuJ-&g8=GsZ%XZ`qc>;Jya=i6~{(4 zze`0_$3fz?k)M$&6Q&2k9O@)|ms0J}WX+PQI!AD_7a~rK?MmT=*{6>HgTC8@7F?wW zQvP*i_&d*0XyEkG>uvdgHGS``HxH~dcZ(_r(SdxGqHQ%PTNR$W9pbwF`p%+Ykchrg zd;ZKP$e_{BKpcRu)<0Yc9BtI9zz>QDE10>pjI*RY^gW>ul4rjnPF^nE9*z_fjWPsx z;rz(NO!21+*w8E;HQ$iEs5?KQdY&WrS6@)|)f2@QGGUNb`pZ9QAe|~5VNk^MzNK=| z;9mAK2uc9Z4dpSjUqcHr9b7A0l!Z0R|#ihlchp@I~KLoS?6Doh)_ zu=K%3UGOn9lpxZdn;Jp5l_rCG^PfI$I}&ztJSpaMC0Dy0lkx;${plYda`3~ne*P2} z9ns|~NVrt6b{V?dJkGZr?$|N@3Us`o=$|_;^#S3=1iixlG*FRl!;~WTtHWQYrv4vi zfe1%Iyo&Usa1;vcWijV9f7lG3%s-7n>1JhqP#>q+%Q)cm8&5xe%t7J#7D4;Pq!ZrW z*g^ioamw?yQzmW9rs}H{8t5HMq^f8a;yr5&UFlvWAEjU8sr=MHK{6`(@8X=pB5QW2 z)rThuRkfKID&7*$00)V;uz|kjA&u<%qJ(-ftQI~Y0{FUqmAQ!dX>BIlbU4uR1a+&@ zkmj#sFi6@RVdl;od8!Nb$k?GwV+%UZN9AD$I^SFxGhyZiYBo6^FlHMmi!Ic%74vOR zTbAhK$tdDL$9G>b!@nzjgEd46*Yv8FuSvFht22=+*rv|+4$3b zZ!3S9Pw}ln%eG1#?EZ^BG{yxDUxw|9&~c^5s(?Zdx-((jv z13BIiNg7v<)1Ffv6D%?fSr_TBhX^49!*M=iw(6`RQc?jsR0}$}pNjkz<6%^oMiYn`-l$ug_5e zS1DRhObQInw-Hk}ce)nOJZ9INf!2B`WzZ4KR@X3E!~FpiZ)K(=-8Jv@E0_O7vHoC^ z*mjWnD^9@x&n<51a}BtoDA5<;<}xSCC+OaWNZ$ME3m&cIdTfwC4Zm$M?e4xF(O$|$ zrSzuPFiN2WDjj&+{!K)`jnAnWe@$`zFB!7C_VUHc>G-^C$sIK&2Yo??dG8%0cY(-P z1rmXM{)O0gYP&rAn2vYb`0|l9nE3ECc_<5>4C^-IkP5A?DipVEh9TOz&DpiYx%6@C z#Dno^dc`iX8XU-yP(<05{clKW%B~$F$=^>896~*gwp&*&IxfA9fhpjF$7_{qs|GRM zLX+R8N{JxU6-9q%_r?JeOsI^WN_t7?pj&xEkHMow{;zu80jt}tvI zFD>(I?F<}NeZm5#`PrYw0M)P3Kz3*VPJFh2r$Th$n@AOsr`1dhA9WkD|k=MnY0PQDYtoFoJo3AVzoQ(6}uJ5 zwBXm2)hE`7bwu6b&XTa}cPj9p2ZnQpcF_$!1-P{a=mYqW?0lIKJ;w@^$6in|X0*YF`$DQZHSS134zF#>yPW_`4AM znjWs@7CMvwH&w=voOp3Nmp*fLCy%HIhrP5`8tIG_zpnAcnl=|XlAwc5huL$3P(55h z>c_yBe?U^0$VIy65!`OulJGuDnbnWNi(Y(X%(q+=wc|?Q2Wu_JnDJ&$*`0Aw!ZUIi zLNC5ADY4@dQNnc>jc?!5JbOc?nNQyEX>`M5$mfqT$&v=S?+6QQU0tZYtev?)e4p?- zY{z1l6g8L;7w5*j(|auG#MUb~C2FLD6F18@z+LutDU_~ID;*L^^u`B!#;k#f{-zo9?Ko4_oPY}^K;S}Z+?xf&NYM^|v z*pkvo9N^|^q7*<0z0x+Hj+W+}ccPQ$H(-$H-?fpVpC<>uExt9k+(1qEU9M}vo%HvX0RkxaW5 z=KK>pm4^BzfJRm1U%B1g>RZ@jDfLn$`jQ>x1y$v|mymsRDCL?c!YkXHKGa-HgE^c< z&YfRD-oQYl9&jEJOV>1l30cc7hM{sP6OEbF4?M=-nqywL<U9Y?sIr@s$(G5wcSm@dzPD$+RR=zaQD*X%5`4WL^3uN+b)z#*3hP*#P%bC@!UE zZ>`)nYW}1sbTh`W{0WJAY;H1vzX&xGt4PFK9HgIS)leN-3# literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..7308232 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Rocket Science + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..523c5df --- /dev/null +++ b/build.gradle @@ -0,0 +1,39 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = '1.3.40' + ext.nav_version = '2.1.0-alpha05' + + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0-beta05' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.4.2.1" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +plugins { + id "io.gitlab.arturbosch.detekt" version "1.0.0-RC14" + id "org.jetbrains.dokka" version "0.9.18" +} + +allprojects { + repositories { + google() + jcenter() + + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +apply from: "scripts/dependencies.gradle" diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..5407e3d --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/sources.gradle" +apply from: "$rootProject.projectDir/scripts/flavors.gradle" + +android { + dataBinding { + enabled = true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api project(":repository") + + implementation libraries.fragment + implementation libraries.paging + implementation libraries.lifecycle + implementation libraries.navigation + implementation libraries.picasso + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent + testImplementation testLibraries.coroutinesTest + + compileOnly libraries.room +} diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7a7f527 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/core/src/main/kotlin/com/melih/core/actions/Actions.kt b/core/src/main/kotlin/com/melih/core/actions/Actions.kt new file mode 100644 index 0000000..8fbd473 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/actions/Actions.kt @@ -0,0 +1,16 @@ +package com.melih.core.actions + +import android.content.Intent + +const val EXTRA_LAUNCH_ID = "extras:detail:launchid" + +/** + * Navigation actions for navigation between feature activities + */ +object Actions { + + fun openDetailFor(id: Long) = + Intent("action.dashboard.open") + .putExtra(EXTRA_LAUNCH_ID, id) + +} diff --git a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseActivity.kt b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseActivity.kt new file mode 100644 index 0000000..33f8285 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseActivity.kt @@ -0,0 +1,58 @@ +package com.melih.core.base.lifecycle + +import android.os.Bundle +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.NavigationUI +import dagger.android.support.DaggerAppCompatActivity +import kotlinx.coroutines.ExperimentalCoroutinesApi + +const val NAV_HOST_FRAGMENT_TAG = "nav_host_fragment_tag" + +/** + * Base class of all Activity classes + */ +abstract class BaseActivity : DaggerAppCompatActivity() { + + protected lateinit var binding: T + protected lateinit var navHostFragment: NavHostFragment + + @ExperimentalCoroutinesApi + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, getLayoutId()) + binding.lifecycleOwner = this + + if (savedInstanceState == null) { + navHostFragment = createNavHostFragment() + + supportFragmentManager + .beginTransaction() + .add(addNavHostTo(), navHostFragment, NAV_HOST_FRAGMENT_TAG) + .commitNow() + } else { + navHostFragment = supportFragmentManager + .findFragmentByTag(NAV_HOST_FRAGMENT_TAG) as NavHostFragment + } + } + + override fun onSupportNavigateUp(): Boolean { + if (!NavigationUI.navigateUp(navHostFragment.navController, null)) { + onBackPressed() + } + + return true + } + + @LayoutRes + abstract fun getLayoutId(): Int + + abstract fun createNavHostFragment(): NavHostFragment + + @IdRes + abstract fun addNavHostTo(): Int +} diff --git a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt new file mode 100644 index 0000000..7660275 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt @@ -0,0 +1,44 @@ +package com.melih.core.base.lifecycle + +import android.content.Context +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import com.melih.core.di.ViewModelFactory +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.AndroidSupportInjection +import dagger.android.support.HasSupportFragmentInjector +import javax.inject.Inject + + +/** + * Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which + * won't need any injection. + * + * Note that fragments that extends from [BaseDaggerFragment] should contribute android injector. + * + * This class provides [viewModelFactory] which serves as factory for view models + * in the project. It's injected by map of view models that this app is serving. Check [ViewModelFactory] + * to see how it works. + */ +abstract class BaseDaggerFragment : BaseFragment(), HasSupportFragmentInjector { + + // region Properties + + @get:Inject + internal var childFragmentInjector: DispatchingAndroidInjector? = null + + @Inject + lateinit var viewModelFactory: ViewModelFactory + // endregion + + // region Functions + + override fun onAttach(context: Context) { + AndroidSupportInjection.inject(this) + super.onAttach(context) + } + + override fun supportFragmentInjector(): AndroidInjector? = childFragmentInjector + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt new file mode 100644 index 0000000..86f2ed9 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt @@ -0,0 +1,44 @@ +package com.melih.core.base.lifecycle + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment + +/** + * Parent of all fragments. + * + * Purpose of [BaseFragment] is to simplify view creation and provide easy access to fragment's + * [navController] and [binding]. + */ +abstract class BaseFragment : Fragment() { + + // region Properties + + protected lateinit var navController: NavController + protected lateinit var binding: T + // endregion + + // region Functions + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + navController = NavHostFragment.findNavController(this) + binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false) + binding.lifecycleOwner = this + return binding.root + } + + @LayoutRes + abstract fun getLayoutId(): Int + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt b/core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt new file mode 100644 index 0000000..8ec796b --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt @@ -0,0 +1,72 @@ +package com.melih.core.base.recycler + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView + +/** + * Base adapter to reduce boilerplate on creating / binding view holders. + * + * + */ +abstract class BaseListAdapter( + callback: DiffUtil.ItemCallback, + private val clickListener: (T) -> Unit +) : ListAdapter>(callback) { + + private var itemClickListener: ((T) -> Unit)? = null + + /** + * This method will be called to create view holder to obfuscate layout inflation creation / process + * + * @param inflater layout inflator + * @param parent parent view group + * @param viewType viewType of holder + */ + abstract fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): BaseViewHolder + + /** + * [createViewHolder] will provide holders, no need to override this + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = + createViewHolder( + LayoutInflater.from(parent.context), + parent, + viewType + ) + + /** + * Calls [bind][BaseViewHolder.bind] on view holders + */ + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + val item = getItem(position) + + holder.itemView.setOnClickListener { + clickListener(item) + } + + holder.bind(item) + + } +} + +/** + * Base view holder takes view data binding + */ +abstract class BaseViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + + /** + * Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder] + * + * @param item entity + * @param position position from adapter + */ + abstract fun bind(item: T) +} diff --git a/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt b/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..d30406b --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt @@ -0,0 +1,89 @@ +package com.melih.core.base.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result + +/** + * Base [ViewModel] for view models that will process data. + * + * This view model provides state & error with [stateData] & [errorData] respectively. + */ +abstract class BaseViewModel : ViewModel() { + + // region Abstractions + + abstract fun loadData() + // endregion + + // region Properties + + private val _successData = MutableLiveData() + private val _stateData = MutableLiveData() + private val _errorData = MutableLiveData() + + /** + * Observe [successData] to get notified of data if it's successfuly fetched + */ + val successData: LiveData + get() = _successData + + /** + * Observe [stateData] to get notified of state of data + */ + val stateData: LiveData + get() = _stateData + + /** + * Observe [errorData] to get notified if an error occurs + */ + val errorData: LiveData + get() = _errorData + // endregion + + // region Functions + + /** + * Default success handler which assigns given [data] to [successData] + * + * @param data success data + */ + protected fun handleSuccess(data: T) { + _successData.value = data + } + + /** + * Default state handler which assigns given [state] to [stateData] + * + * @param state state of operation + */ + protected fun handleState(state: Result.State) { + _stateData.value = state + } + + /** + * Default error handler which assign received [error] to [errorData] + * + * @param error check [Error] class for possible error types + */ + protected fun handleFailure(reason: Reason) { + _errorData.value = reason + } + + /** + * Reload data + */ + fun refresh() { + loadData() + } + + /** + * Retry loading data, incase there's difference between refresh and retry, should go here + */ + fun retry() { + loadData() + } + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt b/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt new file mode 100644 index 0000000..859796c --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt @@ -0,0 +1,22 @@ +package com.melih.core.di + +import android.app.Application +import android.net.NetworkInfo +import com.melih.repository.persistence.LaunchesDatabase +import dagger.BindsInstance +import dagger.Component +import javax.inject.Singleton + +@Singleton +@Component(modules = [CoreModule::class]) +interface CoreComponent { + + fun getNetworkInfo(): NetworkInfo? + + fun getLaunchesDatabase(): LaunchesDatabase + + @Component.Factory + interface Factory { + fun create(@BindsInstance app: Application): CoreComponent + } +} diff --git a/core/src/main/kotlin/com/melih/core/di/CoreModule.kt b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt new file mode 100644 index 0000000..1867c58 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt @@ -0,0 +1,26 @@ +package com.melih.core.di + +import android.app.Application +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import androidx.room.Room +import com.melih.repository.persistence.DB_NAME +import com.melih.repository.persistence.LaunchesDatabase +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class CoreModule { + + @Provides + fun provideNetworkInfo(app: Application): NetworkInfo? = + (app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo + + @Provides + @Singleton + fun provideLaunchesDatabase(app: Application) = + Room.databaseBuilder(app.applicationContext, LaunchesDatabase::class.java, DB_NAME) + .build() +} diff --git a/core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt b/core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt new file mode 100644 index 0000000..665b430 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt @@ -0,0 +1,30 @@ +package com.melih.core.di + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider + +/** + * [Factory][ViewModelProvider.Factory] that provides view models allowing injection. [viewModelMap] is provided via dagger + * injection. To be able to inject a view model, it must be bound to map via [dagger.Binds] [dagger.multibindings.IntoMap] + * by using [ViewModelKey][com.melih.core.di.keys.ViewModelKey]. + * + */ +@Suppress("UNCHECKED_CAST") +class ViewModelFactory @Inject constructor( + private val viewModelMap: Map, @JvmSuppressWildcards Provider> +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + val viewModelProvider: Provider = viewModelMap[modelClass] + ?: throw IllegalArgumentException("Unknown ViewModel") + + return try { + viewModelProvider.get() as T + ?: throw IllegalArgumentException("Provider's contained value is null") + } catch (e: ClassCastException) { + throw e + } + } +} diff --git a/core/src/main/kotlin/com/melih/core/di/keys/ViewModelKey.kt b/core/src/main/kotlin/com/melih/core/di/keys/ViewModelKey.kt new file mode 100644 index 0000000..ee0c358 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/di/keys/ViewModelKey.kt @@ -0,0 +1,9 @@ +package com.melih.core.di.keys + +import androidx.lifecycle.ViewModel +import dagger.MapKey +import kotlin.reflect.KClass + +@MapKey +@Target(AnnotationTarget.FUNCTION) +annotation class ViewModelKey(val value: KClass) diff --git a/core/src/main/kotlin/com/melih/core/extensions/BindingAdapters.kt b/core/src/main/kotlin/com/melih/core/extensions/BindingAdapters.kt new file mode 100644 index 0000000..e3a13b3 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/extensions/BindingAdapters.kt @@ -0,0 +1,19 @@ +package com.melih.core.extensions + +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.squareup.picasso.Picasso + +/** + * Loads image in given [url] to this [ImageView] + * + * @param url url of image + */ +@BindingAdapter("imageUrl") +fun ImageView.loadImage(url: String?) { + if (!url.isNullOrBlank()) { + Picasso.get() + .load(url) + .into(this) + } +} diff --git a/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt b/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt new file mode 100644 index 0000000..5798815 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt @@ -0,0 +1,32 @@ +package com.melih.core.extensions + +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders + +/** + * Reduces required boilerplate code to observe a live data + * + * @param data [LiveData] to observe + * @param block receive and process data + */ +fun Fragment.observe(data: LiveData, block: (T) -> Unit) { + data.observe(this, Observer(block)) +} + +/** + * Method for getting viewModel from factory and run a block over it if required for easy access + * + * crossinline for unwanted returns + */ +inline fun ViewModelProvider.Factory.createFor( + fragment: Fragment, + crossinline block: T.() -> Unit = {} +): T { + val viewModel = ViewModelProviders.of(fragment, this)[T::class.java] + viewModel.apply(block) + return viewModel +} diff --git a/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt b/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt new file mode 100644 index 0000000..2528400 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt @@ -0,0 +1,23 @@ +package com.melih.core.utils + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.snackbar.Snackbar + +class SnackbarBehaviour constructor( + context: Context, + attributeSet: AttributeSet +) : CoordinatorLayout.Behavior() { + + override fun layoutDependsOn(parent: CoordinatorLayout, child: SwipeRefreshLayout, dependency: View): Boolean = + dependency is Snackbar.SnackbarLayout + + override fun onDependentViewChanged(parent: CoordinatorLayout, child: SwipeRefreshLayout, dependency: View): Boolean { + val translationY = Math.min(0.0f, (dependency.translationY - dependency.height)) + child.translationY = translationY + return true + } +} diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml new file mode 100644 index 0000000..270aec4 --- /dev/null +++ b/core/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #008577 + #00574B + #D81B60 + #8F8F8F + diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml new file mode 100644 index 0000000..ab4df8e --- /dev/null +++ b/core/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 8dp + 11dp + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml new file mode 100644 index 0000000..22788f1 --- /dev/null +++ b/core/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia + deserunt mollit anim id est laborum + + + Retry + + + action.detail.open + diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml new file mode 100644 index 0000000..f7ae6d6 --- /dev/null +++ b/core/src/main/res/values/styles.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/res/values/values.xml b/core/src/main/res/values/values.xml new file mode 100644 index 0000000..33e2994 --- /dev/null +++ b/core/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + 5 + \ No newline at end of file diff --git a/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt new file mode 100644 index 0000000..203a012 --- /dev/null +++ b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt @@ -0,0 +1,49 @@ +package com.melih.core + +import androidx.arch.core.executor.ArchTaskExecutor +import androidx.arch.core.executor.TaskExecutor +import androidx.lifecycle.LiveData +import com.melih.core.observers.OneShotObserverWithLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import java.util.concurrent.Executors +import kotlin.coroutines.suspendCoroutine + +abstract class BaseTestWithMainThread { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + @BeforeEach + @ExperimentalCoroutinesApi + fun setUp() { + Dispatchers.setMain(dispatcher) + ArchTaskExecutor.getInstance() + .setDelegate(object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) = runnable.run() + + override fun isMainThread(): Boolean = true + + override fun postToMainThread(runnable: Runnable) = runnable.run() + }) + } + + @AfterEach + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + dispatcher.close() + ArchTaskExecutor.getInstance() + .setDelegate(null) + } +} + +suspend fun LiveData.testObserve(onChangeHandler: (T) -> Unit) { + suspendCoroutine { + val observer = OneShotObserverWithLifecycle(onChangeHandler, it) + observe(observer, observer) + } +} diff --git a/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt new file mode 100644 index 0000000..4436517 --- /dev/null +++ b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt @@ -0,0 +1,37 @@ +package com.melih.core.base + +import com.melih.core.base.viewmodel.BaseViewModel +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class BaseViewModelTest { + + val baseVm = spyk(TestViewModel()) + + @Test + fun `refresh should invoke loadData`() { + baseVm.refresh() + + verify(exactly = 1) { baseVm.loadData() } + } + + @Test + fun `retry should invoke loadData`() { + baseVm.retry() + + verify(exactly = 1) { baseVm.loadData() } + } +} + +class TestViewModel : BaseViewModel() { + override public fun loadData() { + // no - op + } + +} diff --git a/core/src/test/kotlin/com/melih/core/observers/OneShotObserverWithLifecycle.kt b/core/src/test/kotlin/com/melih/core/observers/OneShotObserverWithLifecycle.kt new file mode 100644 index 0000000..dcdad09 --- /dev/null +++ b/core/src/test/kotlin/com/melih/core/observers/OneShotObserverWithLifecycle.kt @@ -0,0 +1,34 @@ +package com.melih.core.observers + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.Observer +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +/** + * This class is both [Observer] & [LifecycleOwner], used to observe on live data via + * [testObserve]. + * + * Taking continuation is due to suspending coroutine, else scope is getting closed right away after + * reaching end of suspending job and test is over. + */ +class OneShotObserverWithLifecycle( + val block: (T) -> Unit, val + continuation: Continuation +) : LifecycleOwner, Observer { + + private val lifecycle = LifecycleRegistry(this) + + init { + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun onChanged(t: T) { + block(t) + continuation.resume(Unit) + } +} diff --git a/default-detekt-config.yml b/default-detekt-config.yml new file mode 100644 index 0000000..326fe3c --- /dev/null +++ b/default-detekt-config.yml @@ -0,0 +1,523 @@ +autoCorrect: true + +test-pattern: # Configure exclusions for test sources + active: true + patterns: # Test file regexes + - '.*/test/.*' + - '.*/androidTest/.*' + - '.*Test.kt' + - '.*Spec.kt' + - '.*Spek.kt' + exclude-rule-sets: + - 'comments' + exclude-rules: + - 'NamingRules' + - 'WildcardImport' + - 'MagicNumber' + - 'MaxLineLength' + - 'LateinitUsage' + - 'StringLiteralDuplication' + - 'SpreadOperator' + - 'TooManyFunctions' + - 'ForEachOnRange' + - 'FunctionMaxLength' + - 'TooGenericExceptionCaught' + - 'InstanceOfCheckForException' + +build: + maxIssues: 1 + weights: +# complexity: 1 +# LongParameterList: 1 +# style: 1 +# comments: 0 + +processors: + active: true + exclude: + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: + # - 'ProjectStatisticsReport' + # - 'ComplexityReport' + # - 'NotificationReport' + # - 'FindingsReport' + # - 'BuildFailureReport' + +comments: + active: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + ComplexMethod: + active: true + threshold: 10 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + LabeledExpression: + active: false + ignoredLabels: "" + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + threshold: 6 + ignoreDefaultParameters: false + MethodOverloading: + active: false + threshold: 6 + NestedBlockDepth: + active: true + threshold: 4 + StringLiteralDuplication: + active: false + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverriddenFunctions: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: false + methodNames: 'toString,hashCode,equals,finalize' + InstanceOfCheckForException: + active: false + NotImplementedDeclaration: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: false + ReturnFromFinally: + active: false + SwallowedException: + active: false + ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' + ThrowingExceptionFromFinally: + active: false + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + exceptions: 'IllegalArgumentException,IllegalStateException,IOException' + ThrowingNewInstanceOfSameException: + active: false + TooGenericExceptionCaught: + active: false + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + TooGenericExceptionThrown: + active: true + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +formatting: + active: true + android: false + autoCorrect: true + ChainWrapping: + active: true + autoCorrect: true + CommentSpacing: + active: true + autoCorrect: true + Filename: + active: true + FinalNewline: + active: true + autoCorrect: true + ImportOrdering: + active: false + Indentation: + active: true + autoCorrect: true + indentSize: 4 + continuationIndentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 150 + ModifierOrdering: + active: true + autoCorrect: true + NoBlankLineBeforeRbrace: + active: true + autoCorrect: true + NoConsecutiveBlankLines: + active: true + autoCorrect: true + NoEmptyClassBody: + active: true + autoCorrect: true + NoItParamInMultilineLambda: + active: false + NoLineBreakAfterElse: + active: true + autoCorrect: true + NoLineBreakBeforeAssignment: + active: true + autoCorrect: true + NoMultipleSpaces: + active: true + autoCorrect: true + NoSemicolons: + active: true + autoCorrect: true + NoTrailingSpaces: + active: true + autoCorrect: true + NoUnitReturn: + active: true + autoCorrect: true + NoUnusedImports: + active: true + autoCorrect: true + NoWildcardImports: + active: true + autoCorrect: true + PackageName: + active: true + autoCorrect: true + ParameterListWrapping: + active: true + autoCorrect: true + indentSize: 4 + SpacingAroundColon: + active: true + autoCorrect: true + SpacingAroundComma: + active: true + autoCorrect: true + SpacingAroundCurly: + active: true + autoCorrect: true + SpacingAroundKeyword: + active: true + autoCorrect: true + SpacingAroundOperators: + active: true + autoCorrect: true + SpacingAroundParens: + active: true + autoCorrect: true + SpacingAroundRangeOperator: + active: true + autoCorrect: true + StringTemplate: + active: true + autoCorrect: true + +naming: + active: true + ClassNaming: + active: true + classPattern: '[A-Z$][a-zA-Z0-9$]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: '' + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreOverridden: true + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverriddenFunctions: true + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: false + ignoreOverriddenFunction: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: false + ForEachOnRange: + active: true + SpreadOperator: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: false + IteratorHasNextCallsNextMethod: + active: false + IteratorNotThrowingNoSuchElementException: + active: false + LateinitUsage: + active: false + excludeAnnotatedProperties: "" + ignoreOnClassesPattern: "" + UnconditionalJumpStatementInLoop: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: false + UnsafeCast: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: false + +style: + active: true + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + EqualsNullCall: + active: false + EqualsOnSignatureLine: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: true + values: 'TODO:,FIXME:,STOPSHIP:' + ForbiddenImport: + active: false + imports: '' + ForbiddenVoid: + active: false + FunctionOnlyReturningConstant: + active: false + ignoreOverridableFunction: true + excludedFunctions: 'describeContents' + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MagicNumber: + active: true + ignoreNumbers: '-1,0,1,2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + MandatoryBracesIfStatements: + active: false + MaxLineLength: + active: true + maxLineLength: 150 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: false + ModifierOrder: + active: true + NestedClassesVisibility: + active: false + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: "equals" + excludeLabeled: false + excludeReturnFromLambda: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: false + SpacingBetweenPackageAndImports: + active: false + ThrowsCount: + active: true + max: 2 + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 5 + UnnecessaryAbstractClass: + active: false + excludeAnnotatedClasses: "dagger.Module" + UnnecessaryApply: + active: false + UnnecessaryInheritance: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: false + UnusedPrivateMember: + active: false + allowedNames: "(_|ignored|expected|serialVersionUID)" + UseDataClass: + active: false + excludeAnnotatedClasses: "" + UtilityClassWithPublicConstructor: + active: false + VarCouldBeVal: + active: false + WildcardImport: + active: true + excludeImports: 'java.util.*,kotlinx.android.synthetic.*' diff --git a/features/detail/.gitignore b/features/detail/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/features/detail/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/detail/build.gradle b/features/detail/build.gradle new file mode 100644 index 0000000..e380aa6 --- /dev/null +++ b/features/detail/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: "androidx.navigation.safeargs" + +apply from: "$rootProject.projectDir/scripts/feature_module.gradle" + +android { + packagingOptions { + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/LICENSE-notice.md' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent + testImplementation testLibraries.coroutinesTest +} diff --git a/features/detail/consumer-rules.pro b/features/detail/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/detail/proguard-rules.pro b/features/detail/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/features/detail/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/features/detail/sampledata/launches.json b/features/detail/sampledata/launches.json new file mode 100644 index 0000000..3e8569c --- /dev/null +++ b/features/detail/sampledata/launches.json @@ -0,0 +1,1239 @@ +{ + "launches": [ + { + "id": 1946, + "name": "Long March 3B/E | Beidou-3 IGSO-2", + "windowstart": "June 24, 2019 17:52:00 UTC", + "windowend": "June 24, 2019 18:28:00 UTC", + "net": "June 24, 2019 17:52:00 UTC", + "wsstamp": 1561398720, + "westamp": 1561400880, + "netstamp": 1561398720, + "isostart": "20190624T175200Z", + "isoend": "20190624T182800Z", + "isonet": "20190624T175200Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-21 07:01:11", + "location": { + "pads": [ + { + "id": 143, + "name": "Launch Complex 3 ( LC-3 ) ( LA-1 ), Xichang Satellite Launch Center", + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/Xichang_Satellite_Launch_Center", + "mapURL": "https://www.google.com/maps/?q=28.246017,102.026556", + "latitude": 28.246017, + "longitude": 102.026556, + "agencies": [ + { + "id": 17, + "name": "China National Space Administration", + "abbrev": "CNSA", + "countryCode": "CHN", + "type": 1, + "infoURL": "http://www.cnsa.gov.cn/", + "wikiURL": "http://en.wikipedia.org/wiki/China_National_Space_Administration", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.cnsa.gov.cn/" + ] + } + ] + } + ], + "id": 25, + "name": "Xichang Satellite Launch Center, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 69, + "name": "Long March 3B/E", + "configuration": "B/E", + "familyname": "Long March 3", + "agencies": [ + { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Long_March_3B", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/LongMarch3BE.jpg_1024.jpg" + }, + "missions": [ + { + "id": 1216, + "name": "Beidou-3 IGSO-2", + "description": "These two satellites will be used to provide global navigation coverage as part of the Chinese Beidou (Compass) satellite navigation system.", + "type": 15, + "wikiURL": "https://en.wikipedia.org/wiki/BeiDou_Navigation_Satellite_System", + "typeName": "Navigation", + "agencies": null, + "payloads": [] + } + ], + "lsp": { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + }, + { + "id": 318, + "name": "Falcon Heavy | STP-2", + "windowstart": "June 25, 2019 03:30:00 UTC", + "windowend": "June 25, 2019 07:30:00 UTC", + "net": "June 25, 2019 03:30:00 UTC", + "wsstamp": 1561433400, + "westamp": 1561447800, + "netstamp": 1561433400, + "isostart": "20190625T033000Z", + "isoend": "20190625T073000Z", + "isonet": "20190625T033000Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [ + "http://www.spacex.com/webcast/" + ], + "vidURL": null, + "infoURLs": [ + "https://www.spacex.com/stp-2" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": 70, + "hashtag": null, + "changed": "2019-06-21 16:24:03", + "location": { + "pads": [ + { + "id": 87, + "name": "Launch Complex 39A, Kennedy Space Center, FL", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Kennedy_Space_Center_Launch_Complex_39#Launch_Pad_39A", + "mapURL": "http://maps.google.com/maps?q=28.608+N,+80.604+W", + "latitude": 28.60822681, + "longitude": -80.60428186, + "agencies": [] + } + ], + "id": 17, + "name": "Kennedy Space Center, FL, USA", + "infoURL": "", + "wikiURL": "", + "countryCode": "USA" + }, + "rocket": { + "id": 58, + "name": "Falcon Heavy", + "configuration": "Heavy", + "familyname": "Falcon", + "agencies": [ + { + "id": 121, + "name": "SpaceX", + "abbrev": "SpX", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/SpaceX", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.spacex.com/", + "https://twitter.com/SpaceX", + "https://www.youtube.com/channel/UCtI0Hodo5o5dUb67FeUjDeA" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Falcon_Heavy", + "infoURLs": [ + "http://www.spacex.com/falcon-heavy" + ], + "infoURL": "http://www.spacex.com/falcon-heavy", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920, + 2560 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/FalconHeavy.jpg_2560.jpg" + }, + "missions": [ + { + "id": 687, + "name": "STP-2", + "description": "The STP-2 payload is composed of 25 small spacecraft. Included is COSMIC-2 constellation to provide radio occultation data, along with 8 cubesat nanosatellites. \nOther payloads include LightSail carried by the Prox-1 nanosatellite, Oculus-ASR nanosatellite, GPIM and the Deep Space Atomic Clock.", + "type": 14, + "wikiURL": "https://en.wikipedia.org/wiki/Space_Test_Program", + "typeName": "Dedicated Rideshare", + "agencies": [ + { + "id": 161, + "name": "United States Air Force", + "abbrev": "USAF", + "countryCode": "USA", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/United_States_Air_Force", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.af.mil", + "https://www.facebook.com/USairforce", + "https://twitter.com/USairforce", + "https://www.youtube.com/afbluetube", + "https://www.instagram.com/usairforce", + "https://www.flickr.com/photos/usairforce" + ] + } + ], + "payloads": [ + { + "id": 444, + "name": "DSX (Demonstration & Science Experiments)" + }, + { + "id": 445, + "name": "Formosat-7/Cosmic-2" + }, + { + "id": 446, + "name": "GPIM (Green Propellant Infusion Mission)" + }, + { + "id": 447, + "name": "OTB-1" + }, + { + "id": 448, + "name": "Oculus-ASR" + }, + { + "id": 449, + "name": "NPSAT1" + }, + { + "id": 450, + "name": "Prox-1" + }, + { + "id": 451, + "name": "Lightsail-B" + }, + { + "id": 452, + "name": "E-TBex A, B" + }, + { + "id": 453, + "name": "PSAT-2 (ParkinsonSAT-2)" + }, + { + "id": 454, + "name": "TEPCE 1, 2" + }, + { + "id": 455, + "name": "CP 9 (LEO)" + }, + { + "id": 456, + "name": "StangSat" + } + ] + } + ], + "lsp": { + "id": 121, + "name": "SpaceX", + "abbrev": "SpX", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/SpaceX", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.spacex.com/", + "https://twitter.com/SpaceX", + "https://www.youtube.com/channel/UCtI0Hodo5o5dUb67FeUjDeA" + ] + } + }, + { + "id": 1932, + "name": "Electron | Make It Rain", + "windowstart": "June 27, 2019 04:30:00 UTC", + "windowend": "June 27, 2019 06:30:00 UTC", + "net": "June 27, 2019 04:30:00 UTC", + "wsstamp": 1561609800, + "westamp": 1561617000, + "netstamp": 1561609800, + "isostart": "20190627T043000Z", + "isoend": "20190627T063000Z", + "isonet": "20190627T043000Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-17 07:50:23", + "location": { + "pads": [ + { + "id": 166, + "name": "Rocket Lab Launch Complex 1", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Rocket_Lab_Launch_Complex_1", + "mapURL": "https://www.google.ee/maps/place/39°15'46.2\"S+177°51'52.1\"E/", + "latitude": -39.262833, + "longitude": 177.864469, + "agencies": [ + { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + ] + } + ], + "id": 40, + "name": "Onenui Station, Mahia Peninsula, New Zealand", + "infoURL": "", + "wikiURL": "", + "countryCode": "NZL" + }, + "rocket": { + "id": 148, + "name": "Electron", + "configuration": "", + "familyname": "Electron", + "agencies": [ + { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Rocket_Lab#Electron_Launch_Vehicle", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/Electron.jpg_1440.jpg" + }, + "missions": [ + { + "id": 1209, + "name": "Make It Rain", + "description": "Rideshare mission for Spaceflight. The mission is named \"Make it Rain\" in a nod to the high volume of rainfall in Seattle, where Spaceflight is headquartered, as well in New Zealand where Launch Complex 1 is located. Among the satellites on the mission for Spaceflight are BlackSky’s Global-4, two U.S. Special Operations Command (USSOCOM) Prometheus and Melbourne Space Program’s ACRUX-1.", + "type": 14, + "wikiURL": "", + "typeName": "Dedicated Rideshare", + "agencies": null, + "payloads": [ + { + "id": 457, + "name": "BlackSky Global 3" + }, + { + "id": 458, + "name": "ACRUX 1" + }, + { + "id": 459, + "name": "SpaceBEE 8" + }, + { + "id": 460, + "name": "SpaceBEE 9" + }, + { + "id": 461, + "name": "Prometheus-2 5,6" + } + ] + } + ], + "lsp": { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + }, + { + "id": 1937, + "name": "Kuaizhou-11 | Maiden Flight", + "windowstart": "June 30, 2019 00:00:00 UTC", + "windowend": "June 30, 2019 00:00:00 UTC", + "net": "June 30, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190630T000000Z", + "isoend": "20190630T000000Z", + "isonet": "20190630T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-06 17:15:06", + "location": { + "pads": [ + { + "id": 115, + "name": "Unknown Pad, Jiuquan", + "infoURL": "", + "wikiURL": "", + "mapURL": "", + "latitude": 40.958, + "longitude": 100.291, + "agencies": null + } + ], + "id": 1, + "name": "Jiuquan, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 223, + "name": "Kuaizhou-11", + "configuration": "11", + "familyname": "Kuaizhou", + "agencies": null, + "wikiURL": "https://en.wikipedia.org/wiki/Kuaizhou#Models", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 1212, + "name": "Maiden Flight", + "description": "First flight of the new solid launcher developed by ExPace, subsidiary of CASIC. It will carry 2 communication satellites on this launch.", + "type": 13, + "wikiURL": "", + "typeName": "Test Flight", + "agencies": null, + "payloads": [] + } + ], + "lsp": { + "id": 194, + "name": "ExPace", + "abbrev": "EP", + "countryCode": "CHN", + "type": 3, + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/ExPace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [] + } + }, + { + "id": 1137, + "name": "Long March 4B | CBERS-4A", + "windowstart": "July 1, 2019 00:00:00 UTC", + "windowend": "July 1, 2019 00:00:00 UTC", + "net": "July 1, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190701T000000Z", + "isoend": "20190701T000000Z", + "isonet": "20190701T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2017-02-21 00:00:00", + "location": { + "pads": [ + { + "id": 116, + "name": "Unknown Pad, Taiyuan", + "infoURL": "", + "wikiURL": "", + "mapURL": "", + "latitude": 38.849, + "longitude": 111.608, + "agencies": null + } + ], + "id": 2, + "name": "Taiyuan, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 16, + "name": "Long March 4B", + "configuration": "B", + "familyname": "Long March 4", + "agencies": [ + { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + ], + "wikiURL": "http://en.wikipedia.org/wiki/Long_March_4B", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [], + "lsp": { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + }, + { + "id": 1432, + "name": "Soyuz 2.1b/Fregat | Meteor-M №2-2", + "windowstart": "July 5, 2019 05:41:00 UTC", + "windowend": "July 5, 2019 05:41:00 UTC", + "net": "July 5, 2019 05:41:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190705T054100Z", + "isoend": "20190705T054100Z", + "isonet": "20190705T054100Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-04 11:48:46", + "location": { + "pads": [ + { + "id": 170, + "name": "Cosmodrome Site 1S, Vostochny Cosmodrome, Siberia, Russian Federation", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Vostochny_Cosmodrome", + "mapURL": "https://www.google.ee/maps/place/51°53'03.8\"N+128°20'02.2\"E/", + "latitude": 51.884395, + "longitude": 128.333932, + "agencies": [ + { + "id": 63, + "name": "Russian Federal Space Agency (ROSCOSMOS)", + "abbrev": "RFSA", + "countryCode": "RUS", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Russian_Federal_Space_Agency", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://en.roscosmos.ru/", + "https://www.youtube.com/channel/UCOcpUgXosMCIlOsreUfNFiA", + "https://twitter.com/Roscosmos", + "https://www.facebook.com/Roscosmos" + ] + } + ] + } + ], + "id": 34, + "name": "Vostochny Cosmodrome, Siberia, Russian Federation", + "infoURL": "https://en.wikipedia.org/wiki/Vostochny_Cosmodrome", + "wikiURL": "", + "countryCode": "RUS" + }, + "rocket": { + "id": 65, + "name": "Soyuz 2.1b/Fregat", + "configuration": "2.1b/Fregat", + "familyname": "Soyuz", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Soyuz-2", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [], + "lsp": { + "id": 96, + "name": "Khrunichev State Research and Production Space Center", + "abbrev": "KhSC", + "countryCode": "RUS", + "type": 1, + "infoURL": "http://www.khrunichev.ru/main.php?lang=en", + "wikiURL": "http://en.wikipedia.org/wiki/Khrunichev_State_Research_and_Production_Space_Center", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.khrunichev.ru/main.php?lang=en" + ] + } + }, + { + "id": 1671, + "name": "Vega | Falcon Eye 1", + "windowstart": "July 6, 2019 01:53:00 UTC", + "windowend": "July 6, 2019 01:53:00 UTC", + "net": "July 6, 2019 01:53:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190706T015300Z", + "isoend": "20190706T015300Z", + "isonet": "20190706T015300Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-12 01:11:19", + "location": { + "pads": [ + { + "id": 18, + "name": "Ariane Launch Area 1, Kourou", + "infoURL": "http://www.esa.int/Our_Activities/Launchers/Europe_s_Spaceport/Europe_s_Spaceport2", + "wikiURL": "https://en.wikipedia.org/wiki/ELA-1", + "mapURL": "https://www.google.com/maps/?q=5.239,-52.775", + "latitude": 5.236, + "longitude": -52.775, + "agencies": [ + { + "id": 115, + "name": "Arianespace", + "abbrev": "ASA", + "countryCode": "FRA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Arianespace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.arianespace.com", + "https://www.youtube.com/channel/UCRn9F2D9j-t4A-HgudM7aLQ", + "https://www.facebook.com/ArianeGroup", + "https://twitter.com/arianespace", + "https://www.instagram.com/arianespace" + ] + } + ] + } + ], + "id": 3, + "name": "Kourou, French Guiana", + "infoURL": "", + "wikiURL": "", + "countryCode": "GUF" + }, + "rocket": { + "id": 18, + "name": "Vega", + "configuration": "", + "familyname": "Vega", + "agencies": [], + "wikiURL": "http://en.wikipedia.org/wiki/Vega_rocket", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 1169, + "name": "Falcon Eye 1", + "description": "Falcon Eye 1 is a high-resolution Earth-imaging satellite for the United Arab Emirates. Built by Airbus Defense and Space with an optical imaging payload from Thales Alenia Space, Falcon Eye 1 is the first of two surveillance satellites ordered by the UAE’s military.", + "type": 7, + "wikiURL": "", + "typeName": "Government/Top Secret", + "agencies": [], + "payloads": [] + } + ], + "lsp": { + "id": 115, + "name": "Arianespace", + "abbrev": "ASA", + "countryCode": "FRA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Arianespace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.arianespace.com", + "https://www.youtube.com/channel/UCRn9F2D9j-t4A-HgudM7aLQ", + "https://www.facebook.com/ArianeGroup", + "https://twitter.com/arianespace", + "https://www.instagram.com/arianespace" + ] + } + }, + { + "id": 1203, + "name": "Atlas V 551 | AEHF-5", + "windowstart": "July 9, 2019 12:27:00 UTC", + "windowend": "July 9, 2019 14:27:00 UTC", + "net": "July 9, 2019 12:27:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190709T122700Z", + "isoend": "20190709T142700Z", + "isonet": "20190709T122700Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [ + "https://www.ulalaunch.com/missions/atlas-v-aehf-5" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-23 14:01:16", + "location": { + "pads": [ + { + "id": 85, + "name": "Space Launch Complex 41, Cape Canaveral, FL", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Cape_Canaveral_Air_Force_Station_Space_Launch_Complex_41", + "mapURL": "http://maps.google.com/maps?q=28.58341025,-80.58303644", + "latitude": 28.58341025, + "longitude": -80.58303644, + "agencies": [] + } + ], + "id": 16, + "name": "Cape Canaveral, FL, USA", + "infoURL": "", + "wikiURL": "", + "countryCode": "USA" + }, + "rocket": { + "id": 37, + "name": "Atlas V 551", + "configuration": "551", + "familyname": "Atlas", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Atlas_V", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/Atlas+V+551_1920.jpg" + }, + "missions": [ + { + "id": 405, + "name": "AEHF-5", + "description": "This is the fifth satellite in the Advanced Extremely High Frequency (AEHF) system, which is a series of communications satellites operated by the United States Air Force Space Command. It provides global, survivable, protected communications capabilities for strategic command and tactical warfighters operating on ground, sea and air platforms.", + "type": 10, + "wikiURL": "https://en.wikipedia.org/wiki/Advanced_Extremely_High_Frequency", + "typeName": "Communications", + "agencies": [], + "payloads": [] + } + ], + "lsp": { + "id": 124, + "name": "United Launch Alliance", + "abbrev": "ULA", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/United_Launch_Alliance", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.ulalaunch.com/", + "https://www.youtube.com/channel/UCnrGPRKAg1PgvuSHrRIl3jg", + "https://twitter.com/ulalaunch", + "https://www.facebook.com/ulalaunch", + "https://www.instagram.com/ulalaunch/" + ] + } + }, + { + "id": 1112, + "name": "Proton-M/Blok DM-03 | Spektr-RG", + "windowstart": "July 12, 2019 00:00:00 UTC", + "windowend": "July 12, 2019 00:00:00 UTC", + "net": "July 12, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190712T000000Z", + "isoend": "20190712T000000Z", + "isonet": "20190712T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-21 14:28:53", + "location": { + "pads": [ + { + "id": 30, + "name": "31/6, Baikonur Cosmodrome, Kazakhstan", + "infoURL": "", + "wikiURL": "", + "mapURL": "http://maps.google.com/maps?q=45.996+N,+63.564+E", + "latitude": 45.996034, + "longitude": 63.564003, + "agencies": [] + } + ], + "id": 10, + "name": "Baikonur Cosmodrome, Republic of Kazakhstan", + "infoURL": "", + "wikiURL": "", + "countryCode": "KAZ" + }, + "rocket": { + "id": 62, + "name": "Proton-M/Blok DM-03", + "configuration": "-M/Blok DM-03", + "familyname": "Proton / UR-500", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Proton-M", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 748, + "name": "Spektr-RG", + "description": "Spektr-RG is a joint Russian-German observatory-class mission. It is intented to study the interplanetary magnetic field, galaxies and black holes.", + "type": 3, + "wikiURL": "https://en.wikipedia.org/wiki/Spektr-RG", + "typeName": "Astrophysics", + "agencies": [], + "payloads": [ + { + "id": 109, + "name": "Spektr-RG" + } + ] + } + ], + "lsp": { + "id": 96, + "name": "Khrunichev State Research and Production Space Center", + "abbrev": "KhSC", + "countryCode": "RUS", + "type": 1, + "infoURL": "http://www.khrunichev.ru/main.php?lang=en", + "wikiURL": "http://en.wikipedia.org/wiki/Khrunichev_State_Research_and_Production_Space_Center", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.khrunichev.ru/main.php?lang=en" + ] + } + }, + { + "id": 1133, + "name": "GSLV Mk III | Chandrayaan-2", + "windowstart": "July 14, 2019 21:21:00 UTC", + "windowend": "July 14, 2019 21:21:00 UTC", + "net": "July 14, 2019 21:21:00 UTC", + "wsstamp": 1563139260, + "westamp": 1563139260, + "netstamp": 1563139260, + "isostart": "20190714T212100Z", + "isoend": "20190714T212100Z", + "isonet": "20190714T212100Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [ + "https://en.wikipedia.org/wiki/Chandrayaan-2", + "http://www.isro.gov.in/chandrayaan-2" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-12 13:37:40", + "location": { + "pads": [ + { + "id": 145, + "name": "Satish Dhawan Space Centre Second Launch Pad", + "infoURL": "https://en.wikipedia.org/wiki/Satish_Dhawan_Space_Centre_Second_Launch_Pad", + "wikiURL": "https://en.wikipedia.org/wiki/Satish_Dhawan_Space_Centre_Second_Launch_Pad", + "mapURL": "https://www.google.com/maps?q=13.7199,80.2304", + "latitude": 13.7199, + "longitude": 80.2304, + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ] + } + ], + "id": 5, + "name": "Sriharikota, Republic of India", + "infoURL": "", + "wikiURL": "", + "countryCode": "IND" + }, + "rocket": { + "id": 85, + "name": "GSLV Mk III", + "configuration": "Mk III", + "familyname": "GSLV", + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Geosynchronous_Satellite_Launch_Vehicle_Mk_III", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 657, + "name": "Chandrayaan-2", + "description": "Chandrayaan-2 is India's second mission to the Moon. It consists of an orbiter, lander and rover. After reaching the 100 km lunar orbit, the lander housing the rover will separate from the orbiter. After a controlled descent, the lander will perform a soft landing on the lunar surface at a specified site and deploy the rover. Six-wheeled rover weighs around 20 kg and will operate on solar power. It will move around the landing site, performing lunar surface chemical analysis and relaying data back to Earth through the orbiter. The lander will be collecting data on Moon-quakes, thermal properties of the lunar surface, the density and variation of lunar surface plasma. The orbiter will be mapping lunar surface. Altogether, Chandrayaan-2 mission will collect scientific information on lunar topography, mineralogy, elemental abundance, lunar exosphere and signatures of hydroxyl and water-ice.", + "type": 2, + "wikiURL": "https://en.wikipedia.org/wiki/Chandrayaan-2", + "typeName": "Planetary Science", + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ], + "payloads": [ + { + "id": 5, + "name": "Orbiter" + }, + { + "id": 6, + "name": "Vikram Lander" + }, + { + "id": 7, + "name": "Pragyan Rover" + } + ] + } + ], + "lsp": { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + } + ], + "total": 201, + "offset": 0, + "count": 10 +} diff --git a/features/detail/src/main/AndroidManifest.xml b/features/detail/src/main/AndroidManifest.xml new file mode 100644 index 0000000..07efa7a --- /dev/null +++ b/features/detail/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt b/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt new file mode 100644 index 0000000..22a2c54 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt @@ -0,0 +1,23 @@ +package com.melih.detail.di + +import com.melih.detail.di.modules.DetailBinds +import com.melih.detail.ui.DetailFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Contributes fragments & view models in this module + */ +@Module +abstract class DetailContributor { + + // region Contributes + + @ContributesAndroidInjector( + modules = [ + DetailBinds::class + ] + ) + abstract fun detailFragment(): DetailFragment + // endregion +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt new file mode 100644 index 0000000..578d247 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt @@ -0,0 +1,22 @@ +package com.melih.detail.di.modules + +import androidx.lifecycle.ViewModel +import com.melih.core.di.keys.ViewModelKey +import com.melih.detail.ui.DetailViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@Module +abstract class DetailBinds { + + // region ViewModels + + @Binds + @IntoMap + @ViewModelKey(DetailViewModel::class) + @ExperimentalCoroutinesApi + abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel + // endregion +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt new file mode 100644 index 0000000..519c20e --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt @@ -0,0 +1,39 @@ +package com.melih.detail.ui + +import android.os.Bundle +import androidx.navigation.fragment.NavHostFragment +import com.melih.core.actions.EXTRA_LAUNCH_ID +import com.melih.core.base.lifecycle.BaseActivity +import com.melih.detail.R +import com.melih.detail.databinding.DetailActivityBinding +import kotlinx.coroutines.ExperimentalCoroutinesApi + +const val INVALID_LAUNCH_ID = -1L + +class DetailActivity : BaseActivity() { + + // region Functions + + @ExperimentalCoroutinesApi + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true); + supportActionBar?.setDisplayShowHomeEnabled(true); + } + + override fun getLayoutId(): Int = R.layout.activity_detail + + override fun createNavHostFragment() = + NavHostFragment.create( + R.navigation.nav_detail, + DetailFragmentArgs.Builder() + .setLaunchId(intent?.extras?.getLong(EXTRA_LAUNCH_ID) ?: INVALID_LAUNCH_ID) + .build() + .toBundle() + ) + + override fun addNavHostTo(): Int = R.id.container + // endregion +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt new file mode 100644 index 0000000..204ebbf --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt @@ -0,0 +1,62 @@ +package com.melih.detail.ui + +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.view.View +import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar +import com.melih.core.base.lifecycle.BaseDaggerFragment +import com.melih.core.extensions.createFor +import com.melih.core.extensions.observe +import com.melih.detail.R +import com.melih.detail.databinding.DetailBinding +import kotlinx.coroutines.ExperimentalCoroutinesApi +import timber.log.Timber + +class DetailFragment : BaseDaggerFragment() { + + // region Properties + + private val args: DetailFragmentArgs by navArgs() + + @ExperimentalCoroutinesApi + private val viewModel: DetailViewModel + get() = viewModelFactory.createFor(this) + // endregion + + // region Functions + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.tvDescription.movementMethod = ScrollingMovementMethod() + binding.viewModel = viewModel + + viewModel.createParamsFor(args.launchId) + viewModel.loadData() + + // Observing state to show loading + observe(viewModel.stateData) { + // Loading can go here, skipping for now + } + + // Observing error to show toast with retry action + observe(viewModel.errorData) { + Snackbar.make( + binding.root, + resources.getString(it.messageRes), + Snackbar.LENGTH_INDEFINITE + ).setAction(com.melih.core.R.string.retry) { + viewModel.retry() + }.show() + } + + observe(viewModel.successData) { + Timber.i("") + } + } + + override fun getLayoutId(): Int = R.layout.fragment_detail + // endregion +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt new file mode 100644 index 0000000..ad17cbb --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt @@ -0,0 +1,56 @@ +package com.melih.detail.ui + +import androidx.lifecycle.Transformations +import androidx.lifecycle.viewModelScope +import com.melih.core.base.viewmodel.BaseViewModel +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.GetLaunchDetails +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ExperimentalCoroutinesApi +class DetailViewModel @Inject constructor( + private val getLaunchDetails: GetLaunchDetails +) : BaseViewModel() { + + // region Properties + + private var params = GetLaunchDetails.Params(INVALID_LAUNCH_ID) + + val rocketName = Transformations.map(successData) { + it.rocket.name + } + + val description = Transformations.map(successData) { + if (it.missions.isEmpty()) { + "" + } else { + it.missions[0].description + } + } + + val imageUrl = Transformations.map(successData) { + it.rocket.imageURL + } + // endregion + + // region Functions + + fun createParamsFor(id: Long) { + params = GetLaunchDetails.Params(id) + } + + /** + * Triggering interactor in view model scope + */ + override fun loadData() { + viewModelScope.launch { + getLaunchDetails(params).collect { + it.handle(::handleState, ::handleFailure, ::handleSuccess) + } + } + } + // endregion +} diff --git a/features/detail/src/main/res/layout/activity_detail.xml b/features/detail/src/main/res/layout/activity_detail.xml new file mode 100644 index 0000000..89c60f2 --- /dev/null +++ b/features/detail/src/main/res/layout/activity_detail.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/features/detail/src/main/res/layout/fragment_detail.xml b/features/detail/src/main/res/layout/fragment_detail.xml new file mode 100644 index 0000000..abf4e90 --- /dev/null +++ b/features/detail/src/main/res/layout/fragment_detail.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/features/detail/src/main/res/navigation/nav_detail.xml b/features/detail/src/main/res/navigation/nav_detail.xml new file mode 100644 index 0000000..2f2f844 --- /dev/null +++ b/features/detail/src/main/res/navigation/nav_detail.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/features/detail/src/main/res/values/strings.xml b/features/detail/src/main/res/values/strings.xml new file mode 100644 index 0000000..c9dc1ae --- /dev/null +++ b/features/detail/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Image of the rocket + diff --git a/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt b/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt new file mode 100644 index 0000000..94e3614 --- /dev/null +++ b/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt @@ -0,0 +1,28 @@ +package com.melih.list + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach + +abstract class BaseTestWithMainThread { + + @ExperimentalCoroutinesApi + protected val dispatcher = TestCoroutineDispatcher() + + @BeforeEach + @ExperimentalCoroutinesApi + fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @AfterEach + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + dispatcher.cleanupTestCoroutines() + } +} diff --git a/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt new file mode 100644 index 0000000..0e6a4d4 --- /dev/null +++ b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt @@ -0,0 +1,42 @@ +package com.melih.detail + +import com.melih.detail.ui.DetailViewModel +import com.melih.list.BaseTestWithMainThread +import com.melih.repository.interactors.GetLaunchDetails +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class DetailViewModelTest : BaseTestWithMainThread() { + + private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) + + @ExperimentalCoroutinesApi + private val viewModel = spyk(DetailViewModel(getLaunchDetails)) + + @Test + @ExperimentalCoroutinesApi + fun `loadData should invoke getLauchDetails with provided params`() { + dispatcher.runBlockingTest { + + val paramsSlot = slot() + + viewModel.createParamsFor(1013) + viewModel.loadData() + + // init should have called it already due to creation above + verify(exactly = 1) { getLaunchDetails(capture(paramsSlot)) } + paramsSlot.captured.id shouldEqualTo 1013 + } + } +} diff --git a/features/list/.gitignore b/features/list/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/features/list/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/list/build.gradle b/features/list/build.gradle new file mode 100644 index 0000000..df7af5b --- /dev/null +++ b/features/list/build.gradle @@ -0,0 +1,14 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/feature_module.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent + testImplementation testLibraries.coroutinesTest +} diff --git a/features/list/consumer-rules.pro b/features/list/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/features/list/proguard-rules.pro b/features/list/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/features/list/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/features/list/sampledata/launches.json b/features/list/sampledata/launches.json new file mode 100644 index 0000000..3e8569c --- /dev/null +++ b/features/list/sampledata/launches.json @@ -0,0 +1,1239 @@ +{ + "launches": [ + { + "id": 1946, + "name": "Long March 3B/E | Beidou-3 IGSO-2", + "windowstart": "June 24, 2019 17:52:00 UTC", + "windowend": "June 24, 2019 18:28:00 UTC", + "net": "June 24, 2019 17:52:00 UTC", + "wsstamp": 1561398720, + "westamp": 1561400880, + "netstamp": 1561398720, + "isostart": "20190624T175200Z", + "isoend": "20190624T182800Z", + "isonet": "20190624T175200Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-21 07:01:11", + "location": { + "pads": [ + { + "id": 143, + "name": "Launch Complex 3 ( LC-3 ) ( LA-1 ), Xichang Satellite Launch Center", + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/Xichang_Satellite_Launch_Center", + "mapURL": "https://www.google.com/maps/?q=28.246017,102.026556", + "latitude": 28.246017, + "longitude": 102.026556, + "agencies": [ + { + "id": 17, + "name": "China National Space Administration", + "abbrev": "CNSA", + "countryCode": "CHN", + "type": 1, + "infoURL": "http://www.cnsa.gov.cn/", + "wikiURL": "http://en.wikipedia.org/wiki/China_National_Space_Administration", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.cnsa.gov.cn/" + ] + } + ] + } + ], + "id": 25, + "name": "Xichang Satellite Launch Center, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 69, + "name": "Long March 3B/E", + "configuration": "B/E", + "familyname": "Long March 3", + "agencies": [ + { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Long_March_3B", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/LongMarch3BE.jpg_1024.jpg" + }, + "missions": [ + { + "id": 1216, + "name": "Beidou-3 IGSO-2", + "description": "These two satellites will be used to provide global navigation coverage as part of the Chinese Beidou (Compass) satellite navigation system.", + "type": 15, + "wikiURL": "https://en.wikipedia.org/wiki/BeiDou_Navigation_Satellite_System", + "typeName": "Navigation", + "agencies": null, + "payloads": [] + } + ], + "lsp": { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + }, + { + "id": 318, + "name": "Falcon Heavy | STP-2", + "windowstart": "June 25, 2019 03:30:00 UTC", + "windowend": "June 25, 2019 07:30:00 UTC", + "net": "June 25, 2019 03:30:00 UTC", + "wsstamp": 1561433400, + "westamp": 1561447800, + "netstamp": 1561433400, + "isostart": "20190625T033000Z", + "isoend": "20190625T073000Z", + "isonet": "20190625T033000Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [ + "http://www.spacex.com/webcast/" + ], + "vidURL": null, + "infoURLs": [ + "https://www.spacex.com/stp-2" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": 70, + "hashtag": null, + "changed": "2019-06-21 16:24:03", + "location": { + "pads": [ + { + "id": 87, + "name": "Launch Complex 39A, Kennedy Space Center, FL", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Kennedy_Space_Center_Launch_Complex_39#Launch_Pad_39A", + "mapURL": "http://maps.google.com/maps?q=28.608+N,+80.604+W", + "latitude": 28.60822681, + "longitude": -80.60428186, + "agencies": [] + } + ], + "id": 17, + "name": "Kennedy Space Center, FL, USA", + "infoURL": "", + "wikiURL": "", + "countryCode": "USA" + }, + "rocket": { + "id": 58, + "name": "Falcon Heavy", + "configuration": "Heavy", + "familyname": "Falcon", + "agencies": [ + { + "id": 121, + "name": "SpaceX", + "abbrev": "SpX", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/SpaceX", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.spacex.com/", + "https://twitter.com/SpaceX", + "https://www.youtube.com/channel/UCtI0Hodo5o5dUb67FeUjDeA" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Falcon_Heavy", + "infoURLs": [ + "http://www.spacex.com/falcon-heavy" + ], + "infoURL": "http://www.spacex.com/falcon-heavy", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920, + 2560 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/FalconHeavy.jpg_2560.jpg" + }, + "missions": [ + { + "id": 687, + "name": "STP-2", + "description": "The STP-2 payload is composed of 25 small spacecraft. Included is COSMIC-2 constellation to provide radio occultation data, along with 8 cubesat nanosatellites. \nOther payloads include LightSail carried by the Prox-1 nanosatellite, Oculus-ASR nanosatellite, GPIM and the Deep Space Atomic Clock.", + "type": 14, + "wikiURL": "https://en.wikipedia.org/wiki/Space_Test_Program", + "typeName": "Dedicated Rideshare", + "agencies": [ + { + "id": 161, + "name": "United States Air Force", + "abbrev": "USAF", + "countryCode": "USA", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/United_States_Air_Force", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.af.mil", + "https://www.facebook.com/USairforce", + "https://twitter.com/USairforce", + "https://www.youtube.com/afbluetube", + "https://www.instagram.com/usairforce", + "https://www.flickr.com/photos/usairforce" + ] + } + ], + "payloads": [ + { + "id": 444, + "name": "DSX (Demonstration & Science Experiments)" + }, + { + "id": 445, + "name": "Formosat-7/Cosmic-2" + }, + { + "id": 446, + "name": "GPIM (Green Propellant Infusion Mission)" + }, + { + "id": 447, + "name": "OTB-1" + }, + { + "id": 448, + "name": "Oculus-ASR" + }, + { + "id": 449, + "name": "NPSAT1" + }, + { + "id": 450, + "name": "Prox-1" + }, + { + "id": 451, + "name": "Lightsail-B" + }, + { + "id": 452, + "name": "E-TBex A, B" + }, + { + "id": 453, + "name": "PSAT-2 (ParkinsonSAT-2)" + }, + { + "id": 454, + "name": "TEPCE 1, 2" + }, + { + "id": 455, + "name": "CP 9 (LEO)" + }, + { + "id": 456, + "name": "StangSat" + } + ] + } + ], + "lsp": { + "id": 121, + "name": "SpaceX", + "abbrev": "SpX", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/SpaceX", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.spacex.com/", + "https://twitter.com/SpaceX", + "https://www.youtube.com/channel/UCtI0Hodo5o5dUb67FeUjDeA" + ] + } + }, + { + "id": 1932, + "name": "Electron | Make It Rain", + "windowstart": "June 27, 2019 04:30:00 UTC", + "windowend": "June 27, 2019 06:30:00 UTC", + "net": "June 27, 2019 04:30:00 UTC", + "wsstamp": 1561609800, + "westamp": 1561617000, + "netstamp": 1561609800, + "isostart": "20190627T043000Z", + "isoend": "20190627T063000Z", + "isonet": "20190627T043000Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-17 07:50:23", + "location": { + "pads": [ + { + "id": 166, + "name": "Rocket Lab Launch Complex 1", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Rocket_Lab_Launch_Complex_1", + "mapURL": "https://www.google.ee/maps/place/39°15'46.2\"S+177°51'52.1\"E/", + "latitude": -39.262833, + "longitude": 177.864469, + "agencies": [ + { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + ] + } + ], + "id": 40, + "name": "Onenui Station, Mahia Peninsula, New Zealand", + "infoURL": "", + "wikiURL": "", + "countryCode": "NZL" + }, + "rocket": { + "id": 148, + "name": "Electron", + "configuration": "", + "familyname": "Electron", + "agencies": [ + { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Rocket_Lab#Electron_Launch_Vehicle", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/Electron.jpg_1440.jpg" + }, + "missions": [ + { + "id": 1209, + "name": "Make It Rain", + "description": "Rideshare mission for Spaceflight. The mission is named \"Make it Rain\" in a nod to the high volume of rainfall in Seattle, where Spaceflight is headquartered, as well in New Zealand where Launch Complex 1 is located. Among the satellites on the mission for Spaceflight are BlackSky’s Global-4, two U.S. Special Operations Command (USSOCOM) Prometheus and Melbourne Space Program’s ACRUX-1.", + "type": 14, + "wikiURL": "", + "typeName": "Dedicated Rideshare", + "agencies": null, + "payloads": [ + { + "id": 457, + "name": "BlackSky Global 3" + }, + { + "id": 458, + "name": "ACRUX 1" + }, + { + "id": 459, + "name": "SpaceBEE 8" + }, + { + "id": 460, + "name": "SpaceBEE 9" + }, + { + "id": 461, + "name": "Prometheus-2 5,6" + } + ] + } + ], + "lsp": { + "id": 147, + "name": "Rocket Lab Ltd", + "abbrev": "RL", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Rocket_Lab", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.rocketlabusa.com/", + "https://twitter.com/rocketlab", + "https://www.youtube.com/user/RocketLabNZ", + "https://www.facebook.com/RocketLabUSA", + "https://www.linkedin.com/company/rocket-lab-limited" + ] + } + }, + { + "id": 1937, + "name": "Kuaizhou-11 | Maiden Flight", + "windowstart": "June 30, 2019 00:00:00 UTC", + "windowend": "June 30, 2019 00:00:00 UTC", + "net": "June 30, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190630T000000Z", + "isoend": "20190630T000000Z", + "isonet": "20190630T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-06 17:15:06", + "location": { + "pads": [ + { + "id": 115, + "name": "Unknown Pad, Jiuquan", + "infoURL": "", + "wikiURL": "", + "mapURL": "", + "latitude": 40.958, + "longitude": 100.291, + "agencies": null + } + ], + "id": 1, + "name": "Jiuquan, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 223, + "name": "Kuaizhou-11", + "configuration": "11", + "familyname": "Kuaizhou", + "agencies": null, + "wikiURL": "https://en.wikipedia.org/wiki/Kuaizhou#Models", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 1212, + "name": "Maiden Flight", + "description": "First flight of the new solid launcher developed by ExPace, subsidiary of CASIC. It will carry 2 communication satellites on this launch.", + "type": 13, + "wikiURL": "", + "typeName": "Test Flight", + "agencies": null, + "payloads": [] + } + ], + "lsp": { + "id": 194, + "name": "ExPace", + "abbrev": "EP", + "countryCode": "CHN", + "type": 3, + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/ExPace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [] + } + }, + { + "id": 1137, + "name": "Long March 4B | CBERS-4A", + "windowstart": "July 1, 2019 00:00:00 UTC", + "windowend": "July 1, 2019 00:00:00 UTC", + "net": "July 1, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190701T000000Z", + "isoend": "20190701T000000Z", + "isonet": "20190701T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2017-02-21 00:00:00", + "location": { + "pads": [ + { + "id": 116, + "name": "Unknown Pad, Taiyuan", + "infoURL": "", + "wikiURL": "", + "mapURL": "", + "latitude": 38.849, + "longitude": 111.608, + "agencies": null + } + ], + "id": 2, + "name": "Taiyuan, People's Republic of China", + "infoURL": "", + "wikiURL": "", + "countryCode": "CHN" + }, + "rocket": { + "id": 16, + "name": "Long March 4B", + "configuration": "B", + "familyname": "Long March 4", + "agencies": [ + { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + ], + "wikiURL": "http://en.wikipedia.org/wiki/Long_March_4B", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [], + "lsp": { + "id": 88, + "name": "China Aerospace Science and Technology Corporation", + "abbrev": "CASC", + "countryCode": "CHN", + "type": 1, + "infoURL": null, + "wikiURL": "https://en.wikipedia.org/wiki/China_Aerospace_Science_and_Technology_Corporation", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://english.spacechina.com/", + "http://www.cast.cn/item/list.asp?id=1561" + ] + } + }, + { + "id": 1432, + "name": "Soyuz 2.1b/Fregat | Meteor-M №2-2", + "windowstart": "July 5, 2019 05:41:00 UTC", + "windowend": "July 5, 2019 05:41:00 UTC", + "net": "July 5, 2019 05:41:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190705T054100Z", + "isoend": "20190705T054100Z", + "isonet": "20190705T054100Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-04 11:48:46", + "location": { + "pads": [ + { + "id": 170, + "name": "Cosmodrome Site 1S, Vostochny Cosmodrome, Siberia, Russian Federation", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Vostochny_Cosmodrome", + "mapURL": "https://www.google.ee/maps/place/51°53'03.8\"N+128°20'02.2\"E/", + "latitude": 51.884395, + "longitude": 128.333932, + "agencies": [ + { + "id": 63, + "name": "Russian Federal Space Agency (ROSCOSMOS)", + "abbrev": "RFSA", + "countryCode": "RUS", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Russian_Federal_Space_Agency", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://en.roscosmos.ru/", + "https://www.youtube.com/channel/UCOcpUgXosMCIlOsreUfNFiA", + "https://twitter.com/Roscosmos", + "https://www.facebook.com/Roscosmos" + ] + } + ] + } + ], + "id": 34, + "name": "Vostochny Cosmodrome, Siberia, Russian Federation", + "infoURL": "https://en.wikipedia.org/wiki/Vostochny_Cosmodrome", + "wikiURL": "", + "countryCode": "RUS" + }, + "rocket": { + "id": 65, + "name": "Soyuz 2.1b/Fregat", + "configuration": "2.1b/Fregat", + "familyname": "Soyuz", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Soyuz-2", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [], + "lsp": { + "id": 96, + "name": "Khrunichev State Research and Production Space Center", + "abbrev": "KhSC", + "countryCode": "RUS", + "type": 1, + "infoURL": "http://www.khrunichev.ru/main.php?lang=en", + "wikiURL": "http://en.wikipedia.org/wiki/Khrunichev_State_Research_and_Production_Space_Center", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.khrunichev.ru/main.php?lang=en" + ] + } + }, + { + "id": 1671, + "name": "Vega | Falcon Eye 1", + "windowstart": "July 6, 2019 01:53:00 UTC", + "windowend": "July 6, 2019 01:53:00 UTC", + "net": "July 6, 2019 01:53:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190706T015300Z", + "isoend": "20190706T015300Z", + "isonet": "20190706T015300Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-12 01:11:19", + "location": { + "pads": [ + { + "id": 18, + "name": "Ariane Launch Area 1, Kourou", + "infoURL": "http://www.esa.int/Our_Activities/Launchers/Europe_s_Spaceport/Europe_s_Spaceport2", + "wikiURL": "https://en.wikipedia.org/wiki/ELA-1", + "mapURL": "https://www.google.com/maps/?q=5.239,-52.775", + "latitude": 5.236, + "longitude": -52.775, + "agencies": [ + { + "id": 115, + "name": "Arianespace", + "abbrev": "ASA", + "countryCode": "FRA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Arianespace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.arianespace.com", + "https://www.youtube.com/channel/UCRn9F2D9j-t4A-HgudM7aLQ", + "https://www.facebook.com/ArianeGroup", + "https://twitter.com/arianespace", + "https://www.instagram.com/arianespace" + ] + } + ] + } + ], + "id": 3, + "name": "Kourou, French Guiana", + "infoURL": "", + "wikiURL": "", + "countryCode": "GUF" + }, + "rocket": { + "id": 18, + "name": "Vega", + "configuration": "", + "familyname": "Vega", + "agencies": [], + "wikiURL": "http://en.wikipedia.org/wiki/Vega_rocket", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 1169, + "name": "Falcon Eye 1", + "description": "Falcon Eye 1 is a high-resolution Earth-imaging satellite for the United Arab Emirates. Built by Airbus Defense and Space with an optical imaging payload from Thales Alenia Space, Falcon Eye 1 is the first of two surveillance satellites ordered by the UAE’s military.", + "type": 7, + "wikiURL": "", + "typeName": "Government/Top Secret", + "agencies": [], + "payloads": [] + } + ], + "lsp": { + "id": 115, + "name": "Arianespace", + "abbrev": "ASA", + "countryCode": "FRA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Arianespace", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.arianespace.com", + "https://www.youtube.com/channel/UCRn9F2D9j-t4A-HgudM7aLQ", + "https://www.facebook.com/ArianeGroup", + "https://twitter.com/arianespace", + "https://www.instagram.com/arianespace" + ] + } + }, + { + "id": 1203, + "name": "Atlas V 551 | AEHF-5", + "windowstart": "July 9, 2019 12:27:00 UTC", + "windowend": "July 9, 2019 14:27:00 UTC", + "net": "July 9, 2019 12:27:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190709T122700Z", + "isoend": "20190709T142700Z", + "isonet": "20190709T122700Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [ + "https://www.ulalaunch.com/missions/atlas-v-aehf-5" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-23 14:01:16", + "location": { + "pads": [ + { + "id": 85, + "name": "Space Launch Complex 41, Cape Canaveral, FL", + "infoURL": "", + "wikiURL": "https://en.wikipedia.org/wiki/Cape_Canaveral_Air_Force_Station_Space_Launch_Complex_41", + "mapURL": "http://maps.google.com/maps?q=28.58341025,-80.58303644", + "latitude": 28.58341025, + "longitude": -80.58303644, + "agencies": [] + } + ], + "id": 16, + "name": "Cape Canaveral, FL, USA", + "infoURL": "", + "wikiURL": "", + "countryCode": "USA" + }, + "rocket": { + "id": 37, + "name": "Atlas V 551", + "configuration": "551", + "familyname": "Atlas", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Atlas_V", + "infoURLs": [], + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/Atlas+V+551_1920.jpg" + }, + "missions": [ + { + "id": 405, + "name": "AEHF-5", + "description": "This is the fifth satellite in the Advanced Extremely High Frequency (AEHF) system, which is a series of communications satellites operated by the United States Air Force Space Command. It provides global, survivable, protected communications capabilities for strategic command and tactical warfighters operating on ground, sea and air platforms.", + "type": 10, + "wikiURL": "https://en.wikipedia.org/wiki/Advanced_Extremely_High_Frequency", + "typeName": "Communications", + "agencies": [], + "payloads": [] + } + ], + "lsp": { + "id": 124, + "name": "United Launch Alliance", + "abbrev": "ULA", + "countryCode": "USA", + "type": 3, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/United_Launch_Alliance", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.ulalaunch.com/", + "https://www.youtube.com/channel/UCnrGPRKAg1PgvuSHrRIl3jg", + "https://twitter.com/ulalaunch", + "https://www.facebook.com/ulalaunch", + "https://www.instagram.com/ulalaunch/" + ] + } + }, + { + "id": 1112, + "name": "Proton-M/Blok DM-03 | Spektr-RG", + "windowstart": "July 12, 2019 00:00:00 UTC", + "windowend": "July 12, 2019 00:00:00 UTC", + "net": "July 12, 2019 00:00:00 UTC", + "wsstamp": 0, + "westamp": 0, + "netstamp": 0, + "isostart": "20190712T000000Z", + "isoend": "20190712T000000Z", + "isonet": "20190712T000000Z", + "status": 2, + "inhold": 0, + "tbdtime": 1, + "vidURLs": [], + "vidURL": null, + "infoURLs": [], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 1, + "probability": -1, + "hashtag": null, + "changed": "2019-06-21 14:28:53", + "location": { + "pads": [ + { + "id": 30, + "name": "31/6, Baikonur Cosmodrome, Kazakhstan", + "infoURL": "", + "wikiURL": "", + "mapURL": "http://maps.google.com/maps?q=45.996+N,+63.564+E", + "latitude": 45.996034, + "longitude": 63.564003, + "agencies": [] + } + ], + "id": 10, + "name": "Baikonur Cosmodrome, Republic of Kazakhstan", + "infoURL": "", + "wikiURL": "", + "countryCode": "KAZ" + }, + "rocket": { + "id": 62, + "name": "Proton-M/Blok DM-03", + "configuration": "-M/Blok DM-03", + "familyname": "Proton / UR-500", + "agencies": [], + "wikiURL": "https://en.wikipedia.org/wiki/Proton-M", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 748, + "name": "Spektr-RG", + "description": "Spektr-RG is a joint Russian-German observatory-class mission. It is intented to study the interplanetary magnetic field, galaxies and black holes.", + "type": 3, + "wikiURL": "https://en.wikipedia.org/wiki/Spektr-RG", + "typeName": "Astrophysics", + "agencies": [], + "payloads": [ + { + "id": 109, + "name": "Spektr-RG" + } + ] + } + ], + "lsp": { + "id": 96, + "name": "Khrunichev State Research and Production Space Center", + "abbrev": "KhSC", + "countryCode": "RUS", + "type": 1, + "infoURL": "http://www.khrunichev.ru/main.php?lang=en", + "wikiURL": "http://en.wikipedia.org/wiki/Khrunichev_State_Research_and_Production_Space_Center", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "http://www.khrunichev.ru/main.php?lang=en" + ] + } + }, + { + "id": 1133, + "name": "GSLV Mk III | Chandrayaan-2", + "windowstart": "July 14, 2019 21:21:00 UTC", + "windowend": "July 14, 2019 21:21:00 UTC", + "net": "July 14, 2019 21:21:00 UTC", + "wsstamp": 1563139260, + "westamp": 1563139260, + "netstamp": 1563139260, + "isostart": "20190714T212100Z", + "isoend": "20190714T212100Z", + "isonet": "20190714T212100Z", + "status": 1, + "inhold": 0, + "tbdtime": 0, + "vidURLs": [], + "vidURL": null, + "infoURLs": [ + "https://en.wikipedia.org/wiki/Chandrayaan-2", + "http://www.isro.gov.in/chandrayaan-2" + ], + "infoURL": null, + "holdreason": null, + "failreason": null, + "tbddate": 0, + "probability": -1, + "hashtag": null, + "changed": "2019-06-12 13:37:40", + "location": { + "pads": [ + { + "id": 145, + "name": "Satish Dhawan Space Centre Second Launch Pad", + "infoURL": "https://en.wikipedia.org/wiki/Satish_Dhawan_Space_Centre_Second_Launch_Pad", + "wikiURL": "https://en.wikipedia.org/wiki/Satish_Dhawan_Space_Centre_Second_Launch_Pad", + "mapURL": "https://www.google.com/maps?q=13.7199,80.2304", + "latitude": 13.7199, + "longitude": 80.2304, + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ] + } + ], + "id": 5, + "name": "Sriharikota, Republic of India", + "infoURL": "", + "wikiURL": "", + "countryCode": "IND" + }, + "rocket": { + "id": 85, + "name": "GSLV Mk III", + "configuration": "Mk III", + "familyname": "GSLV", + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ], + "wikiURL": "https://en.wikipedia.org/wiki/Geosynchronous_Satellite_Launch_Vehicle_Mk_III", + "infoURLs": [], + "imageURL": "https://s3.amazonaws.com/launchlibrary/RocketImages/placeholder_1920.png", + "imageSizes": [ + 320, + 480, + 640, + 720, + 768, + 800, + 960, + 1024, + 1080, + 1280, + 1440, + 1920 + ] + }, + "missions": [ + { + "id": 657, + "name": "Chandrayaan-2", + "description": "Chandrayaan-2 is India's second mission to the Moon. It consists of an orbiter, lander and rover. After reaching the 100 km lunar orbit, the lander housing the rover will separate from the orbiter. After a controlled descent, the lander will perform a soft landing on the lunar surface at a specified site and deploy the rover. Six-wheeled rover weighs around 20 kg and will operate on solar power. It will move around the landing site, performing lunar surface chemical analysis and relaying data back to Earth through the orbiter. The lander will be collecting data on Moon-quakes, thermal properties of the lunar surface, the density and variation of lunar surface plasma. The orbiter will be mapping lunar surface. Altogether, Chandrayaan-2 mission will collect scientific information on lunar topography, mineralogy, elemental abundance, lunar exosphere and signatures of hydroxyl and water-ice.", + "type": 2, + "wikiURL": "https://en.wikipedia.org/wiki/Chandrayaan-2", + "typeName": "Planetary Science", + "agencies": [ + { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + ], + "payloads": [ + { + "id": 5, + "name": "Orbiter" + }, + { + "id": 6, + "name": "Vikram Lander" + }, + { + "id": 7, + "name": "Pragyan Rover" + } + ] + } + ], + "lsp": { + "id": 31, + "name": "Indian Space Research Organization", + "abbrev": "ISRO", + "countryCode": "IND", + "type": 1, + "infoURL": null, + "wikiURL": "http://en.wikipedia.org/wiki/Indian_Space_Research_Organization", + "changed": "2017-02-21 00:00:00", + "infoURLs": [ + "https://www.isro.gov.in/", + "https://twitter.com/ISRO", + "https://www.facebook.com/ISRO" + ] + } + } + ], + "total": 201, + "offset": 0, + "count": 10 +} diff --git a/features/list/src/main/AndroidManifest.xml b/features/list/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a50e1f6 --- /dev/null +++ b/features/list/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt b/features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt new file mode 100644 index 0000000..a3065ee --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt @@ -0,0 +1,25 @@ +package com.melih.list.di + +import com.melih.list.di.modules.LaunchesBinds +import com.melih.list.di.modules.LaunchesProvides +import com.melih.list.ui.LaunchesFragment +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Contributes fragments & view models in this module + */ +@Module +abstract class LaunchesContributor { + + // region Contributes + + @ContributesAndroidInjector( + modules = [ + LaunchesProvides::class, + LaunchesBinds::class + ] + ) + abstract fun listFragment(): LaunchesFragment + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt b/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt new file mode 100644 index 0000000..35a77a4 --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt @@ -0,0 +1,22 @@ +package com.melih.list.di.modules + +import androidx.lifecycle.ViewModel +import com.melih.core.di.keys.ViewModelKey +import com.melih.list.ui.LaunchesViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@Module +abstract class LaunchesBinds { + + // region ViewModels + + @Binds + @IntoMap + @ViewModelKey(LaunchesViewModel::class) + @ExperimentalCoroutinesApi + abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt b/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt new file mode 100644 index 0000000..22f91ea --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt @@ -0,0 +1,16 @@ +package com.melih.list.di.modules + +import com.melih.list.ui.LaunchesAdapter +import com.melih.repository.interactors.GetLaunches +import dagger.Module +import dagger.Provides + +@Module +class LaunchesProvides { + + /** + * Provides lauches, using default value of 10 + */ + @Provides + fun provideGetLaunchesParams() = GetLaunches.Params() +} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt new file mode 100644 index 0000000..3043883 --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt @@ -0,0 +1,28 @@ +package com.melih.list.ui + +import android.os.Bundle +import androidx.navigation.fragment.NavHostFragment +import com.melih.core.base.lifecycle.BaseActivity +import com.melih.list.R +import com.melih.list.databinding.LaunchesActivityBinding +import kotlinx.coroutines.ExperimentalCoroutinesApi + +class LaunchesActivity : BaseActivity() { + + // region Functions + + @ExperimentalCoroutinesApi + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setSupportActionBar(binding.toolbar) + } + + override fun getLayoutId(): Int = R.layout.activity_launches + + override fun createNavHostFragment() = + NavHostFragment.create(R.navigation.nav_launches) + + override fun addNavHostTo(): Int = R.id.container + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt new file mode 100644 index 0000000..a47c3e5 --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt @@ -0,0 +1,41 @@ +package com.melih.list.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import com.melih.core.base.recycler.BaseListAdapter +import com.melih.core.base.recycler.BaseViewHolder +import com.melih.list.databinding.LaunchRowBinding +import com.melih.repository.entities.LaunchEntity + +class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BaseListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean = + oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean = + oldItem.name == newItem.name + + }, + itemClickListener +) { + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): BaseViewHolder = + LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false)) + +} + +class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder(binding) { + + override fun bind(item: LaunchEntity) { + binding.entity = item + + val missions = item.missions + binding.tvDescription.text = if (missions.isNotEmpty()) missions[0].description else "" + + binding.executePendingBindings() + } +} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt new file mode 100644 index 0000000..bda4dc9 --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt @@ -0,0 +1,114 @@ +package com.melih.list.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.View +import androidx.appcompat.widget.SearchView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.snackbar.Snackbar +import com.melih.core.actions.Actions +import com.melih.core.base.lifecycle.BaseDaggerFragment +import com.melih.core.extensions.createFor +import com.melih.core.extensions.observe +import com.melih.list.R +import com.melih.list.databinding.ListBinding +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Result +import kotlinx.coroutines.ExperimentalCoroutinesApi +import timber.log.Timber + +class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { + + // region Properties + + @ExperimentalCoroutinesApi + private val viewModel: LaunchesViewModel + get() = viewModelFactory.createFor(this) + + private val launchesAdapter = LaunchesAdapter(::onItemSelected) + private val itemList = mutableListOf() + // endregion + + // region Functions + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + + binding.rocketList.adapter = launchesAdapter + binding.swipeRefreshLayout.setOnRefreshListener(this) + + // Observing state to show loading + observe(viewModel.stateData) { + binding.swipeRefreshLayout.isRefreshing = it is Result.State.Loading + } + + // Observing error to show toast with retry action + observe(viewModel.errorData) { + Snackbar.make( + binding.root, + resources.getString(it.messageRes), + Snackbar.LENGTH_INDEFINITE + ).setAction(com.melih.core.R.string.retry) { + viewModel.retry() + }.show() + } + + observe(viewModel.successData) { + itemList.addAll(it) + launchesAdapter.submitList(itemList) + binding.rocketList.scheduleLayoutAnimation() + } + } + + private fun onItemSelected(item: LaunchEntity) { + Timber.i("${item.id}") + startActivity(Actions.openDetailFor(item.id)) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_rocket_list, menu) + + (menu.findItem(R.id.search).actionView as SearchView).apply { + setOnQueryTextListener(object : SearchView.OnQueryTextListener { + + override fun onQueryTextSubmit(query: String?): Boolean { + clearFocus() + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + launchesAdapter.submitList( + if (!newText.isNullOrBlank()) { + itemList.filter { + it.rocket.name.contains( + newText, + true + ) || (it.missions.size > 0 && it.missions[0].description.contains( + newText, + true + )) + } + } else { + itemList + } + ) + + return true + } + }) + } + + super.onCreateOptionsMenu(menu, inflater) + } + + @ExperimentalCoroutinesApi + override fun onRefresh() { + viewModel.refresh() + } + + override fun getLayoutId(): Int = R.layout.fragment_launches + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt new file mode 100644 index 0000000..4f05f86 --- /dev/null +++ b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt @@ -0,0 +1,38 @@ +package com.melih.list.ui + +import androidx.lifecycle.viewModelScope +import com.melih.core.base.viewmodel.BaseViewModel +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.GetLaunches +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ExperimentalCoroutinesApi +class LaunchesViewModel @Inject constructor( + private val getLaunches: GetLaunches, + private val getLaunchesParams: GetLaunches.Params +) : BaseViewModel>() { + + // region Initialization + + init { + loadData() + } + // endregion + + // region Functions + + /** + * Triggering interactor in view model scope + */ + override fun loadData() { + viewModelScope.launch { + getLaunches(getLaunchesParams).collect { + it.handle(::handleState, ::handleFailure, ::handleSuccess) + } + } + } + // endregion +} diff --git a/features/list/src/main/res/anim/item_enter.xml b/features/list/src/main/res/anim/item_enter.xml new file mode 100644 index 0000000..209a1ac --- /dev/null +++ b/features/list/src/main/res/anim/item_enter.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/features/list/src/main/res/anim/layout_item_enter.xml b/features/list/src/main/res/anim/layout_item_enter.xml new file mode 100644 index 0000000..7c889a6 --- /dev/null +++ b/features/list/src/main/res/anim/layout_item_enter.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/features/list/src/main/res/layout/activity_launches.xml b/features/list/src/main/res/layout/activity_launches.xml new file mode 100644 index 0000000..4369e55 --- /dev/null +++ b/features/list/src/main/res/layout/activity_launches.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/features/list/src/main/res/layout/fragment_launches.xml b/features/list/src/main/res/layout/fragment_launches.xml new file mode 100644 index 0000000..5835a11 --- /dev/null +++ b/features/list/src/main/res/layout/fragment_launches.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/features/list/src/main/res/layout/row_launch.xml b/features/list/src/main/res/layout/row_launch.xml new file mode 100644 index 0000000..bf9fcaa --- /dev/null +++ b/features/list/src/main/res/layout/row_launch.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/features/list/src/main/res/menu/menu_rocket_list.xml b/features/list/src/main/res/menu/menu_rocket_list.xml new file mode 100644 index 0000000..6932d9a --- /dev/null +++ b/features/list/src/main/res/menu/menu_rocket_list.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/features/list/src/main/res/navigation/nav_launches.xml b/features/list/src/main/res/navigation/nav_launches.xml new file mode 100644 index 0000000..03ee663 --- /dev/null +++ b/features/list/src/main/res/navigation/nav_launches.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/features/list/src/main/res/values/integers.xml b/features/list/src/main/res/values/integers.xml new file mode 100644 index 0000000..d20544d --- /dev/null +++ b/features/list/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ + + + 350 + \ No newline at end of file diff --git a/features/list/src/main/res/values/strings.xml b/features/list/src/main/res/values/strings.xml new file mode 100644 index 0000000..848414d --- /dev/null +++ b/features/list/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Image of the rocket + Search + diff --git a/features/list/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt b/features/list/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt new file mode 100644 index 0000000..94e3614 --- /dev/null +++ b/features/list/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt @@ -0,0 +1,28 @@ +package com.melih.list + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach + +abstract class BaseTestWithMainThread { + + @ExperimentalCoroutinesApi + protected val dispatcher = TestCoroutineDispatcher() + + @BeforeEach + @ExperimentalCoroutinesApi + fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @AfterEach + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + dispatcher.cleanupTestCoroutines() + } +} diff --git a/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt b/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt new file mode 100644 index 0000000..7a28959 --- /dev/null +++ b/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt @@ -0,0 +1,33 @@ +package com.melih.list + +import com.melih.list.ui.LaunchesViewModel +import com.melih.repository.interactors.GetLaunches +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class LaunchesViewModelTest : BaseTestWithMainThread() { + + val getLaunches: GetLaunches = mockk(relaxed = true) + val getLaunchesParams: GetLaunches.Params = mockk(relaxed = true) + + @Test + @ExperimentalCoroutinesApi + fun `loadData should invoke getLauches with provided params`() { + spyk(LaunchesViewModel(getLaunches, getLaunchesParams)) + + dispatcher.runBlockingTest { + + // init should have called it already due to creation above + verify(exactly = 1) { getLaunches(getLaunchesParams) } + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..70e38b1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# For build performance +kotlin.incremental=true +kapt.incremental.apt=true +kapt.use.worker.api=true +kapt.include.compile.classpath=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;sSAZcXxMpcXxLe;_mLA z5F_paad+bGZV*oh@8h0(|D2P!q# zTHjmiphJ=AazSeKQPkGOR-D8``LjzToyx{lfK-1CDD6M7?pMZOdLKFtjZaZMPk4}k zW)97Fh(Z+_Fqv(Q_CMH-YYi?fR5fBnz7KOt0*t^cxmDoIokc=+`o# zrud|^h_?KW=Gv%byo~(Ln@({?3gnd?DUf-j2J}|$Mk>mOB+1{ZQ8HgY#SA8END(Zw z3T+W)a&;OO54~m}ffemh^oZ!Vv;!O&yhL0~hs(p^(Yv=(3c+PzPXlS5W79Er8B1o* z`c`NyS{Zj_mKChj+q=w)B}K za*zzPhs?c^`EQ;keH{-OXdXJet1EsQ)7;{3eF!-t^4_Srg4(Ot7M*E~91gwnfhqaM zNR7dFaWm7MlDYWS*m}CH${o?+YgHiPC|4?X?`vV+ws&Hf1ZO-w@OGG^o4|`b{bLZj z&9l=aA-Y(L11!EvRjc3Zpxk7lc@yH1e$a}8$_-r$)5++`_eUr1+dTb@ zU~2P1HM#W8qiNN3b*=f+FfG1!rFxnNlGx{15}BTIHgxO>Cq4 z;#9H9YjH%>Z2frJDJ8=xq>Z@H%GxXosS@Z>cY9ppF+)e~t_hWXYlrO6)0p7NBMa`+ z^L>-#GTh;k_XnE)Cgy|0Dw;(c0* zSzW14ZXozu)|I@5mRFF1eO%JM=f~R1dkNpZM+Jh(?&Zje3NgM{2ezg1N`AQg5%+3Y z64PZ0rPq6;_)Pj-hyIOgH_Gh`1$j1!jhml7ksHA1`CH3FDKiHLz+~=^u@kUM{ilI5 z^FPiJ7mSrzBs9{HXi2{sFhl5AyqwUnU{sPcUD{3+l-ZHAQ)C;c$=g1bdoxeG(5N01 zZy=t8i{*w9m?Y>V;uE&Uy~iY{pY4AV3_N;RL_jT_QtLFx^KjcUy~q9KcLE3$QJ{!)@$@En{UGG7&}lc*5Kuc^780;7Bj;)X?1CSy*^^ zPP^M)Pr5R>mvp3_hmCtS?5;W^e@5BjE>Cs<`lHDxj<|gtOK4De?Sf0YuK5GX9G93i zMYB{8X|hw|T6HqCf7Cv&r8A$S@AcgG1cF&iJ5=%+x;3yB`!lQ}2Hr(DE8=LuNb~Vs z=FO&2pdc16nD$1QL7j+!U^XWTI?2qQKt3H8=beVTdHHa9=MiJ&tM1RRQ-=+vy!~iz zj3O{pyRhCQ+b(>jC*H)J)%Wq}p>;?@W*Eut@P&?VU+Sdw^4kE8lvX|6czf{l*~L;J zFm*V~UC;3oQY(ytD|D*%*uVrBB}BbAfjK&%S;z;7$w68(8PV_whC~yvkZmX)xD^s6 z{$1Q}q;99W?*YkD2*;)tRCS{q2s@JzlO~<8x9}X<0?hCD5vpydvOw#Z$2;$@cZkYrp83J0PsS~!CFtY%BP=yxG?<@#{7%2sy zOc&^FJxsUYN36kSY)d7W=*1-{7ghPAQAXwT7z+NlESlkUH&8ODlpc8iC*iQ^MAe(B z?*xO4i{zFz^G=^G#9MsLKIN64rRJykiuIVX5~0#vAyDWc9-=6BDNT_aggS2G{B>dD ze-B%d3b6iCfc5{@yz$>=@1kdK^tX9qh0=ocv@9$ai``a_ofxT=>X7_Y0`X}a^M?d# z%EG)4@`^Ej_=%0_J-{ga!gFtji_byY&Vk@T1c|ucNAr(JNr@)nCWj?QnCyvXg&?FW;S-VOmNL6^km_dqiVjJuIASVGSFEos@EVF7St$WE&Z%)`Q##+0 zjaZ=JI1G@0!?l|^+-ZrNd$WrHBi)DA0-Eke>dp=_XpV<%CO_Wf5kQx}5e<90dt>8k zAi00d0rQ821nA>B4JHN7U8Zz=0;9&U6LOTKOaC1FC8GgO&kc=_wHIOGycL@c*$`ce703t%>S}mvxEnD-V!;6c`2(p74V7D0No1Xxt`urE66$0(ThaAZ1YVG#QP$ zy~NN%kB*zhZ2Y!kjn826pw4bh)75*e!dse+2Db(;bN34Uq7bLpr47XTX{8UEeC?2i z*{$`3dP}32${8pF$!$2Vq^gY|#w+VA_|o(oWmQX8^iw#n_crb(K3{69*iU?<%C-%H zuKi)3M1BhJ@3VW>JA`M>L~5*_bxH@Euy@niFrI$82C1}fwR$p2E&ZYnu?jlS}u7W9AyfdXh2pM>78bIt3 z)JBh&XE@zA!kyCDfvZ1qN^np20c1u#%P6;6tU&dx0phT1l=(mw7`u!-0e=PxEjDds z9E}{E!7f9>jaCQhw)&2TtG-qiD)lD(4jQ!q{`x|8l&nmtHkdul# zy+CIF8lKbp9_w{;oR+jSLtTfE+B@tOd6h=QePP>rh4@~!8c;Hlg9m%%&?e`*Z?qz5-zLEWfi>`ord5uHF-s{^bexKAoMEV@9nU z^5nA{f{dW&g$)BAGfkq@r5D)jr%!Ven~Q58c!Kr;*Li#`4Bu_?BU0`Y`nVQGhNZk@ z!>Yr$+nB=`z#o2nR0)V3M7-eVLuY`z@6CT#OTUXKnxZn$fNLPv7w1y7eGE=Qv@Hey`n;`U=xEl|q@CCV^#l)s0ZfT+mUf z^(j5r4)L5i2jnHW4+!6Si3q_LdOLQi<^fu?6WdohIkn79=jf%Fs3JkeXwF(?_tcF? z?z#j6iXEd(wJy4|p6v?xNk-)iIf2oX5^^Y3q3ziw16p9C6B;{COXul%)`>nuUoM*q zzmr|NJ5n)+sF$!yH5zwp=iM1#ZR`O%L83tyog-qh1I z0%dcj{NUs?{myT~33H^(%0QOM>-$hGFeP;U$puxoJ>>o-%Lk*8X^rx1>j|LtH$*)>1C!Pv&gd16%`qw5LdOIUbkNhaBBTo}5iuE%K&ZV^ zAr_)kkeNKNYJRgjsR%vexa~&8qMrQYY}+RbZ)egRg9_$vkoyV|Nc&MH@8L)`&rpqd zXnVaI@~A;Z^c3+{x=xgdhnocA&OP6^rr@rTvCnhG6^tMox$ulw2U7NgUtW%|-5VeH z_qyd47}1?IbuKtqNbNx$HR`*+9o=8`%vM8&SIKbkX9&%TS++x z5|&6P<%=F$C?owUI`%uvUq^yW0>`>yz!|WjzsoB9dT;2Dx8iSuK%%_XPgy0dTD4kd zDXF@&O_vBVVKQq(9YTClUPM30Sk7B!v7nOyV`XC!BA;BIVwphh+c)?5VJ^(C;GoQ$ zvBxr7_p*k$T%I1ke}`U&)$uf}I_T~#3XTi53OX)PoXVgxEcLJgZG^i47U&>LY(l%_ z;9vVDEtuMCyu2fqZeez|RbbIE7@)UtJvgAcVwVZNLccswxm+*L&w`&t=ttT=sv6Aq z!HouSc-24Y9;0q$>jX<1DnnGmAsP))- z^F~o99gHZw`S&Aw7e4id6Lg7kMk-e)B~=tZ!kE7sGTOJ)8@q}np@j7&7Sy{2`D^FH zI7aX%06vKsfJ168QnCM2=l|i>{I{%@gcr>ExM0Dw{PX6ozEuqFYEt z087%MKC;wVsMV}kIiuu9Zz9~H!21d!;Cu#b;hMDIP7nw3xSX~#?5#SSjyyg+Y@xh| z%(~fv3`0j#5CA2D8!M2TrG=8{%>YFr(j)I0DYlcz(2~92?G*?DeuoadkcjmZszH5& zKI@Lis%;RPJ8mNsbrxH@?J8Y2LaVjUIhRUiO-oqjy<&{2X~*f|)YxnUc6OU&5iac= z*^0qwD~L%FKiPmlzi&~a*9sk2$u<7Al=_`Ox^o2*kEv?p`#G(p(&i|ot8}T;8KLk- zPVf_4A9R`5^e`Om2LV*cK59EshYXse&IoByj}4WZaBomoHAPKqxRKbPcD`lMBI)g- zeMRY{gFaUuecSD6q!+b5(?vAnf>c`Z(8@RJy%Ulf?W~xB1dFAjw?CjSn$ph>st5bc zUac1aD_m6{l|$#g_v6;=32(mwpveQDWhmjR7{|B=$oBhz`7_g7qNp)n20|^^op3 zSfTdWV#Q>cb{CMKlWk91^;mHap{mk)o?udk$^Q^^u@&jd zfZ;)saW6{e*yoL6#0}oVPb2!}r{pAUYtn4{P~ES9tTfC5hXZnM{HrC8^=Pof{G4%Bh#8 ze~?C9m*|fd8MK;{L^!+wMy>=f^8b&y?yr6KnTq28$pFMBW9Oy7!oV5z|VM$s-cZ{I|Xf@}-)1=$V&x7e;9v81eiTi4O5-vs?^5pCKy2l>q);!MA zS!}M48l$scB~+Umz}7NbwyTn=rqt@`YtuwiQSMvCMFk2$83k50Q>OK5&fe*xCddIm)3D0I6vBU<+!3=6?(OhkO|b4fE_-j zimOzyfBB_*7*p8AmZi~X2bgVhyPy>KyGLAnOpou~sx9)S9%r)5dE%ADs4v%fFybDa_w*0?+>PsEHTbhKK^G=pFz z@IxLTCROWiKy*)cV3y%0FwrDvf53Ob_XuA1#tHbyn%Ko!1D#sdhBo`;VC*e1YlhrC z?*y3rp86m#qI|qeo8)_xH*G4q@70aXN|SP+6MQ!fJQqo1kwO_v7zqvUfU=Gwx`CR@ zRFb*O8+54%_8tS(ADh}-hUJzE`s*8wLI>1c4b@$al)l}^%GuIXjzBK!EWFO8W`>F^ ze7y#qPS0NI7*aU)g$_ziF(1ft;2<}6Hfz10cR8P}67FD=+}MfhrpOkF3hFhQu;Q1y zu%=jJHTr;0;oC94Hi@LAF5quAQ(rJG(uo%BiRQ@8U;nhX)j0i?0SL2g-A*YeAqF>RVCBOTrn{0R27vu}_S zS>tX4!#&U4W;ikTE!eFH+PKw%p+B(MR2I%n#+m0{#?qRP_tR@zpgCb=4rcrL!F=;A zh%EIF8m6%JG+qb&mEfuFTLHSxUAZEvC-+kvZKyX~SA3Umt`k}}c!5dy?-sLIM{h@> z!2=C)@nx>`;c9DdwZ&zeUc(7t<21D7qBj!|1^Mp1eZ6)PuvHx+poKSDCSBMFF{bKy z;9*&EyKitD99N}%mK8431rvbT+^%|O|HV23{;RhmS{$5tf!bIPoH9RKps`-EtoW5h zo6H_!s)Dl}2gCeGF6>aZtah9iLuGd19^z0*OryPNt{70RvJSM<#Ox9?HxGg04}b^f zrVEPceD%)#0)v5$YDE?f`73bQ6TA6wV;b^x*u2Ofe|S}+q{s5gr&m~4qGd!wOu|cZ||#h_u=k*fB;R6&k?FoM+c&J;ISg70h!J7*xGus)ta4veTdW)S^@sU@ z4$OBS=a~@F*V0ECic;ht4@?Jw<9kpjBgHfr2FDPykCCz|v2)`JxTH55?b3IM={@DU z!^|9nVO-R#s{`VHypWyH0%cs;0GO3E;It6W@0gX6wZ%W|Dzz&O%m17pa19db(er}C zUId1a4#I+Ou8E1MU$g=zo%g7K(=0Pn$)Rk z<4T2u<0rD)*j+tcy2XvY+0 z0d2pqm4)4lDewsAGThQi{2Kc3&C=|OQF!vOd#WB_`4gG3@inh-4>BoL!&#ij8bw7? zqjFRDaQz!J-YGitV4}$*$hg`vv%N)@#UdzHFI2E<&_@0Uw@h_ZHf}7)G;_NUD3@18 zH5;EtugNT0*RXVK*by>WS>jaDDfe!A61Da=VpIK?mcp^W?!1S2oah^wowRnrYjl~`lgP-mv$?yb6{{S55CCu{R z$9;`dyf0Y>uM1=XSl_$01Lc1Iy68IosWN8Q9Op=~I(F<0+_kKfgC*JggjxNgK6 z-3gQm6;sm?J&;bYe&(dx4BEjvq}b`OT^RqF$J4enP1YkeBK#>l1@-K`ajbn05`0J?0daOtnzh@l3^=BkedW1EahZlRp;`j*CaT;-21&f2wU z+Nh-gc4I36Cw+;3UAc<%ySb`#+c@5y ze~en&bYV|kn?Cn|@fqmGxgfz}U!98$=drjAkMi`43I4R%&H0GKEgx-=7PF}y`+j>r zg&JF`jomnu2G{%QV~Gf_-1gx<3Ky=Md9Q3VnK=;;u0lyTBCuf^aUi?+1+`4lLE6ZK zT#(Bf`5rmr(tgTbIt?yA@y`(Ar=f>-aZ}T~>G32EM%XyFvhn&@PWCm#-<&ApLDCXT zD#(9m|V(OOo7PmE@`vD4$S5;+9IQm19dd zvMEU`)E1_F+0o0-z>YCWqg0u8ciIknU#{q02{~YX)gc_u;8;i233D66pf(IkTDxeN zL=4z2)?S$TV9=ORVr&AkZMl<4tTh(v;Ix1{`pPVqI3n2ci&4Dg+W|N8TBUfZ*WeLF zqCH_1Q0W&f9T$lx3CFJ$o@Lz$99 zW!G&@zFHxTaP!o#z^~xgF|(vrHz8R_r9eo;TX9}2ZyjslrtH=%6O)?1?cL&BT(Amp zTGFU1%%#xl&6sH-UIJk_PGk_McFn7=%yd6tAjm|lnmr8bE2le3I~L{0(ffo}TQjyo zHZZI{-}{E4ohYTlZaS$blB!h$Jq^Rf#(ch}@S+Ww&$b);8+>g84IJcLU%B-W?+IY& zslcZIR>+U4v3O9RFEW;8NpCM0w1ROG84=WpKxQ^R`{=0MZCubg3st z48AyJNEvyxn-jCPTlTwp4EKvyEwD3e%kpdY?^BH0!3n6Eb57_L%J1=a*3>|k68A}v zaW`*4YitylfD}ua8V)vb79)N_Ixw_mpp}yJGbNu+5YYOP9K-7nf*jA1#<^rb4#AcS zKg%zCI)7cotx}L&J8Bqo8O1b0q;B1J#B5N5Z$Zq=wX~nQFgUfAE{@u0+EnmK{1hg> zC{vMfFLD;L8b4L+B51&LCm|scVLPe6h02rws@kGv@R+#IqE8>Xn8i|vRq_Z`V;x6F zNeot$1Zsu`lLS92QlLWF54za6vOEKGYQMdX($0JN*cjG7HP&qZ#3+bEN$8O_PfeAb z0R5;=zXac2IZ?fxu59?Nka;1lKm|;0)6|#RxkD05P5qz;*AL@ig!+f=lW5^Jbag%2 z%9@iM0ph$WFlxS!`p31t92z~TB}P-*CS+1Oo_g;7`6k(Jyj8m8U|Q3Sh7o-Icp4kV zK}%qri5>?%IPfamXIZ8pXbm-#{ytiam<{a5A+3dVP^xz!Pvirsq7Btv?*d7eYgx7q zWFxrzb3-%^lDgMc=Vl7^={=VDEKabTG?VWqOngE`Kt7hs236QKidsoeeUQ_^FzsXjprCDd@pW25rNx#6x&L6ZEpoX9Ffzv@olnH3rGOSW( zG-D|cV0Q~qJ>-L}NIyT?T-+x+wU%;+_GY{>t(l9dI%Ximm+Kmwhee;FK$%{dnF;C% zFjM2&$W68Sz#d*wtfX?*WIOXwT;P6NUw}IHdk|)fw*YnGa0rHx#paG!m=Y6GkS4VX zX`T$4eW9k1W!=q8!(#8A9h67fw))k_G)Q9~Q1e3f`aV@kbcSv7!priDUN}gX(iXTy zr$|kU0Vn%*ylmyDCO&G0Z3g>%JeEPFAW!5*H2Ydl>39w3W+gEUjL&vrRs(xGP{(ze zy7EMWF14@Qh>X>st8_029||TP0>7SG9on_xxeR2Iam3G~Em$}aGsNt$iES9zFa<3W zxtOF*!G@=PhfHO!=9pVPXMUVi30WmkPoy$02w}&6A7mF)G6-`~EVq5CwD2`9Zu`kd)52``#V zNSb`9dG~8(dooi1*-aSMf!fun7Sc`-C$-E(3BoSC$2kKrVcI!&yC*+ff2+C-@!AT_ zsvlAIV+%bRDfd{R*TMF><1&_a%@yZ0G0lg2K;F>7b+7A6pv3-S7qWIgx+Z?dt8}|S z>Qbb6x(+^aoV7FQ!Ph8|RUA6vXWQH*1$GJC+wXLXizNIc9p2yLzw9 z0=MdQ!{NnOwIICJc8!+Jp!zG}**r#E!<}&Te&}|B4q;U57$+pQI^}{qj669zMMe_I z&z0uUCqG%YwtUc8HVN7?0GHpu=bL7&{C>hcd5d(iFV{I5c~jpX&!(a{yS*4MEoYXh z*X4|Y@RVfn;piRm-C%b@{0R;aXrjBtvx^HO;6(>i*RnoG0Rtcd25BT6edxTNOgUAOjn zJ2)l{ipj8IP$KID2}*#F=M%^n&=bA0tY98@+2I+7~A&T-tw%W#3GV>GTmkHaqftl)#+E zMU*P(Rjo>8%P@_@#UNq(_L{}j(&-@1iY0TRizhiATJrnvwSH0v>lYfCI2ex^><3$q znzZgpW0JlQx?JB#0^^s-Js1}}wKh6f>(e%NrMwS`Q(FhazkZb|uyB@d%_9)_xb$6T zS*#-Bn)9gmobhAtvBmL+9H-+0_0US?g6^TOvE8f3v=z3o%NcPjOaf{5EMRnn(_z8- z$|m0D$FTU zDy;21v-#0i)9%_bZ7eo6B9@Q@&XprR&oKl4m>zIj-fiRy4Dqy@VVVs?rscG| zmzaDQ%>AQTi<^vYCmv#KOTd@l7#2VIpsj?nm_WfRZzJako`^uU%Nt3e;cU*y*|$7W zLm%fX#i_*HoUXu!NI$ey>BA<5HQB=|nRAwK!$L#n-Qz;~`zACig0PhAq#^5QS<8L2 zS3A+8%vbVMa7LOtTEM?55apt(DcWh#L}R^P2AY*c8B}Cx=6OFAdMPj1f>k3#^#+Hk z6uW1WJW&RlBRh*1DLb7mJ+KO>!t^t8hX1#_Wk`gjDio9)9IGbyCAGI4DJ~orK+YRv znjxRMtshZQHc$#Y-<-JOV6g^Cr@odj&Xw5B(FmI)*qJ9NHmIz_r{t)TxyB`L-%q5l ztzHgD;S6cw?7Atg*6E1!c6*gPRCb%t7D%z<(xm+K{%EJNiI2N0l8ud0Ch@_av_RW? zIr!nO4dL5466WslE6MsfMss7<)-S!e)2@r2o=7_W)OO`~CwklRWzHTfpB)_HYwgz=BzLhgZ9S<{nLBOwOIgJU=94uj6r!m>Xyn9>&xP+=5!zG_*yEoRgM0`aYts z^)&8(>z5C-QQ*o_s(8E4*?AX#S^0)aqB)OTyX>4BMy8h(cHjA8ji1PRlox@jB*1n? zDIfyDjzeg91Ao(;Q;KE@zei$}>EnrF6I}q&Xd=~&$WdDsyH0H7fJX|E+O~%LS*7^Q zYzZ4`pBdY{b7u72gZm6^5~O-57HwzwAz{)NvVaowo`X02tL3PpgLjwA`^i9F^vSpN zAqH3mRjG8VeJNHZ(1{%!XqC+)Z%D}58Qel{_weSEHoygT9pN@i zi=G;!Vj6XQk2tuJC>lza%ywz|`f7TIz*EN2Gdt!s199Dr4Tfd_%~fu8gXo~|ogt5Q zlEy_CXEe^BgsYM^o@L?s33WM14}7^T(kqohOX_iN@U?u;$l|rAvn{rwy>!yfZw13U zB@X9)qt&4;(C6dP?yRsoTMI!j-f1KC!<%~i1}u7yLXYn)(#a;Z6~r>hp~kfP));mi zcG%kdaB9H)z9M=H!f>kM->fTjRVOELNwh1amgKQT=I8J66kI)u_?0@$$~5f`u%;zl zC?pkr^p2Fe=J~WK%4ItSzKA+QHqJ@~m|Cduv=Q&-P8I5rQ-#G@bYH}YJr zUS(~(w|vKyU(T(*py}jTUp%I%{2!W!K(i$uvotcPjVddW z8_5HKY!oBCwGZcs-q`4Yt`Zk~>K?mcxg51wkZlX5e#B08I75F7#dgn5yf&Hrp`*%$ zQ;_Qg>TYRzBe$x=T(@WI9SC!ReSas9vDm(yslQjBJZde5z8GDU``r|N(MHcxNopGr z_}u39W_zwWDL*XYYt>#Xo!9kL#97|EAGyGBcRXtLTd59x%m=3i zL^9joWYA)HfL15l9%H?q`$mY27!<9$7GH(kxb%MV>`}hR4a?+*LH6aR{dzrX@?6X4 z3e`9L;cjqYb`cJmophbm(OX0b)!AFG?5`c#zLagzMW~o)?-!@e80lvk!p#&CD8u5_r&wp4O0zQ>y!k5U$h_K;rWGk=U)zX!#@Q%|9g*A zWx)qS1?fq6X<$mQTB$#3g;;5tHOYuAh;YKSBz%il3Ui6fPRv#v62SsrCdMRTav)Sg zTq1WOu&@v$Ey;@^+_!)cf|w_X<@RC>!=~+A1-65O0bOFYiH-)abINwZvFB;hJjL_$ z(9iScmUdMp2O$WW!520Hd0Q^Yj?DK%YgJD^ez$Z^?@9@Ab-=KgW@n8nC&88)TDC+E zlJM)L3r+ZJfZW_T$;Imq*#2<(j+FIk8ls7)WJ6CjUu#r5PoXxQs4b)mZza<8=v{o)VlLRM<9yw^0En#tXAj`Sylxvki{<1DPe^ zhjHwx^;c8tb?Vr$6ZB;$Ff$+3(*oinbwpN-#F)bTsXq@Sm?43MC#jQ~`F|twI=7oC zH4TJtu#;ngRA|Y~w5N=UfMZi?s0%ZmKUFTAye&6Y*y-%c1oD3yQ%IF2q2385Zl+=> zfz=o`Bedy|U;oxbyb^rB9ixG{Gb-{h$U0hVe`J;{ql!s_OJ_>>eoQn(G6h7+b^P48 zG<=Wg2;xGD-+d@UMZ!c;0>#3nws$9kIDkK13IfloGT@s14AY>&>>^#>`PT7GV$2Hp zN<{bN*ztlZu_%W=&3+=#3bE(mka6VoHEs~0BjZ$+=0`a@R$iaW)6>wp2w)=v2@|2d z%?34!+iOc5S@;AAC4hELWLH56RGxo4jw8MDMU0Wk2k_G}=Vo(>eRFo(g3@HjG|`H3 zm8b*dK=moM*oB<)*A$M9!!5o~4U``e)wxavm@O_R(`P|u%9^LGi(_%IF<6o;NLp*0 zKsfZ0#24GT8(G`i4UvoMh$^;kOhl?`0yNiyrC#HJH=tqOH^T_d<2Z+ zeN>Y9Zn!X4*DMCK^o75Zk2621bdmV7Rx@AX^alBG4%~;G_vUoxhfhFRlR&+3WwF^T zaL)8xPq|wCZoNT^>3J0K?e{J-kl+hu2rZI>CUv#-z&u@`hjeb+bBZ>bcciQVZ{SbW zez04s9oFEgc8Z+Kp{XFX`MVf-s&w9*dx7wLen(_@y34}Qz@&`$2+osqfxz4&d}{Ql z*g1ag00Gu+$C`0avds{Q65BfGsu9`_`dML*rX~hyWIe$T>CsPRoLIr%MTk3pJ^2zH1qub1MBzPG}PO;Wmav9w%F7?%l=xIf#LlP`! z_Nw;xBQY9anH5-c8A4mME}?{iewjz(Sq-29r{fV;Fc>fv%0!W@(+{={Xl-sJ6aMoc z)9Q+$bchoTGTyWU_oI19!)bD=IG&OImfy;VxNXoIO2hYEfO~MkE#IXTK(~?Z&!ae! zl8z{D&2PC$Q*OBC(rS~-*-GHNJ6AC$@eve>LB@Iq;jbBZj`wk4|LGogE||Ie=M5g= z9d`uYQ1^Sr_q2wmZE>w2WG)!F%^KiqyaDtIAct?}D~JP4shTJy5Bg+-(EA8aXaxbd~BKMtTf2iQ69jD1o* zZF9*S3!v-TdqwK$%&?91Sh2=e63;X0Lci@n7y3XOu2ofyL9^-I767eHESAq{m+@*r zbVDx!FQ|AjT;!bYsXv8ilQjy~Chiu&HNhFXt3R_6kMC8~ChEFqG@MWu#1Q1#=~#ix zrkHpJre_?#r=N0wv`-7cHHqU`phJX2M_^{H0~{VP79Dv{6YP)oA1&TSfKPEPZn2)G z9o{U1huZBLL;Tp_0OYw@+9z(jkrwIGdUrOhKJUbwy?WBt zlIK)*K0lQCY0qZ!$%1?3A#-S70F#YyUnmJF*`xx?aH5;gE5pe-15w)EB#nuf6B*c~ z8Z25NtY%6Wlb)bUA$w%HKs5$!Z*W?YKV-lE0@w^{4vw;J>=rn?u!rv$&eM+rpU6rc=j9>N2Op+C{D^mospMCjF2ZGhe4eADA#skp2EA26%p3Ex9wHW8l&Y@HX z$Qv)mHM}4*@M*#*ll5^hE9M^=q~eyWEai*P;4z<9ZYy!SlNE5nlc7gm;M&Q zKhKE4d*%A>^m0R?{N}y|i6i^k>^n4(wzKvlQeHq{l&JuFD~sTsdhs`(?lFK@Q{pU~ zb!M3c@*3IwN1RUOVjY5>uT+s-2QLWY z4T2>fiSn>>Fob+%B868-v9D@AfWr#M8eM6w#eAlhc#zk6jkLxGBGk`E3$!A@*am!R zy>29&ptYK6>cvP`b!syNp)Q$0UOW|-O@)8!?94GOYF_}+zlW%fCEl|Tep_zx05g6q z>tp47e-&R*hSNe{6{H!mL?+j$c^TXT{C&@T-xIaesNCl05 z9SLb@q&mSb)I{VXMaiWa3PWj=Ed!>*GwUe;^|uk=Pz$njNnfFY^MM>E?zqhf6^{}0 zx&~~dA5#}1ig~7HvOQ#;d9JZBeEQ+}-~v$at`m!(ai z$w(H&mWCC~;PQ1$%iuz3`>dWeb3_p}X>L2LK%2l59Tyc}4m0>9A!8rhoU3m>i2+hl zx?*qs*c^j}+WPs>&v1%1Ko8_ivAGIn@QK7A`hDz-Emkcgv2@wTbYhkiwX2l=xz*XG zaiNg+j4F-I>9v+LjosI-QECrtKjp&0T@xIMKVr+&)gyb4@b3y?2CA?=ooN zT#;rU86WLh(e@#mF*rk(NV-qSIZyr z$6!ZUmzD)%yO-ot`rw3rp6?*_l*@Z*IB0xn4|BGPWHNc-1ZUnNSMWmDh=EzWJRP`) zl%d%J613oXzh5;VY^XWJi{lB`f#u+ThvtP7 zq(HK<4>tw(=yzSBWtYO}XI`S1pMBe3!jFxBHIuwJ(@%zdQFi1Q_hU2eDuHqXte7Ki zOV55H2D6u#4oTfr7|u*3p75KF&jaLEDpxk!4*bhPc%mpfj)Us3XIG3 zIKMX^s^1wt8YK7Ky^UOG=w!o5e7W-<&c|fw2{;Q11vm@J{)@N3-p1U>!0~sKWHaL= zWV(0}1IIyt1p%=_-Fe5Kfzc71wg}`RDDntVZv;4!=&XXF-$48jS0Sc;eDy@Sg;+{A zFStc{dXT}kcIjMXb4F7MbX~2%i;UrBxm%qmLKb|2=?uPr00-$MEUIGR5+JG2l2Nq` zkM{{1RO_R)+8oQ6x&-^kCj)W8Z}TJjS*Wm4>hf+4#VJP)OBaDF%3pms7DclusBUw} z{ND#!*I6h85g6DzNvdAmnwWY{&+!KZM4DGzeHI?MR@+~|su0{y-5-nICz_MIT_#FE zm<5f3zlaKq!XyvY3H`9s&T};z!cK}G%;~!rpzk9-6L}4Rg7vXtKFsl}@sT#U#7)x- z7UWue5sa$R>N&b{J61&gvKcKlozH*;OjoDR+elkh|4bJ!_3AZNMOu?n9&|L>OTD78 z^i->ah_Mqc|Ev)KNDzfu1P3grBIM#%`QZqj5W{qu(HocQhjyS;UINoP`{J+DvV?|1 z_sw6Yr3z6%e7JKVDY<$P=M)dbk@~Yw9|2!Cw!io3%j92wTD!c^e9Vj+7VqXo3>u#= zv#M{HHJ=e$X5vQ>>ML?E8#UlmvJgTnb73{PSPTf*0)mcj6C z{KsfUbDK|F$E(k;ER%8HMdDi`=BfpZzP3cl5yJHu;v^o2FkHNk;cXc17tL8T!CsYI zfeZ6sw@;8ia|mY_AXjCS?kUfxdjDB28)~Tz1dGE|{VfBS9`0m2!m1yG?hR})er^pl4c@9Aq+|}ZlDaHL)K$O| z%9Jp-imI-Id0|(d5{v~w6mx)tUKfbuVD`xNt04Mry%M+jXzE>4(TBsx#&=@wT2Vh) z1yeEY&~17>0%P(eHP0HB^|7C+WJxQBTG$uyOWY@iDloRIb-Cf!p<{WQHR!422#F34 zG`v|#CJ^G}y9U*7jgTlD{D&y$Iv{6&PYG>{Ixg$pGk?lWrE#PJ8KunQC@}^6OP!|< zS;}p3to{S|uZz%kKe|;A0bL0XxPB&Q{J(9PyX`+Kr`k~r2}yP^ND{8!v7Q1&vtk& z2Y}l@J@{|2`oA%sxvM9i0V+8IXrZ4;tey)d;LZI70Kbim<4=WoTPZy=Yd|34v#$Kh zx|#YJ8s`J>W&jt#GcMpx84w2Z3ur-rK7gf-p5cE)=w1R2*|0mj12hvapuUWM0b~dG zMg9p8FmAZI@i{q~0@QuY44&mMUNXd7z>U58shA3o`p5eVLpq>+{(<3->DWuSFVZwC zxd50Uz(w~LxC4}bgag#q#NNokK@yNc+Q|Ap!u>Ddy+df>v;j@I12CDNN9do+0^n8p zMQs7X#+FVF0C5muGfN{r0|Nkql%BQT|K(DDNdR2pzM=_ea5+GO|J67`05AV92t@4l z0Qno0078PIHdaQGHZ~Scw!dzgqjK~3B7kf>BcP__&lLyU(cu3B^uLo%{j|Mb0NR)tkeT7Hcwp4O# z)yzu>cvG(d9~0a^)eZ;;%3ksk@F&1eEBje~ zW+-_s)&RgiweQc!otF>4%vbXKaOU41{!hw?|2`Ld3I8$&#WOsq>EG)1ANb!{N4z9@ zsU!bPG-~-bqCeIDzo^Q;gnucB{tRzm{ZH^Orphm2U+REA!*<*J6YQV83@&xoDl%#wnl5qcBqCcAF-vX5{30}(oJrnSH z{RY85hylK2dMOh2%oO1J8%)0?8TOL%rS8)+CsDv}aQ>4D)Jv+DLK)9gI^n-T^$)Tc zFPUD75qJm!Y-KBqj;JP4dV4 z`X{lGmn<)1IGz330}s}Jrjtf{(lnuuNHe5(ezA(pYa=1|Ff-LhPFK8 zyJh_b{yzu0yll6ZkpRzRjezyYivjyjW7QwO;@6X`m;2Apn2EK2!~7S}-*=;5*7K$B z`x(=!^?zgj(-`&ApZJXI09aDLXaT@<;CH=?fBOY5d|b~wBA@@p^K#nxr`)?i?SqTupI_PJ(A3cx`z~9mX_*)>L F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..944f2c1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jun 13 10:50:25 CEST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/repository/.gitignore b/repository/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/repository/.gitignore @@ -0,0 +1 @@ +/build diff --git a/repository/build.gradle b/repository/build.gradle new file mode 100644 index 0000000..27970fb --- /dev/null +++ b/repository/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" + +android { + defaultConfig { + javaCompileOptions { + annotationProcessorOptions { + arguments = ["room.schemaLocation": "$rootProject.projectDir/reports/room".toString()] + } + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation libraries.coroutines + implementation libraries.retrofit + implementation libraries.room + implementation libraries.moshiKotlin + implementation libraries.okHttpLogger + + kapt annotationProcessors.roomCompiler + + testImplementation testLibraries.coroutinesCore + testImplementation testLibraries.coroutinesTest +} diff --git a/repository/proguard-rules.pro b/repository/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/repository/src/main/AndroidManifest.xml b/repository/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1af640d --- /dev/null +++ b/repository/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/repository/src/main/kotlin/com/melih/repository/Constants.kt b/repository/src/main/kotlin/com/melih/repository/Constants.kt new file mode 100644 index 0000000..05de778 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/Constants.kt @@ -0,0 +1,5 @@ +package com.melih.repository + +const val DEFAULT_NAME = "Default name" +const val EMPTY_STRING = "" +const val DEFAULT_IMAGE_SIZE = 480 diff --git a/repository/src/main/kotlin/com/melih/repository/Repository.kt b/repository/src/main/kotlin/com/melih/repository/Repository.kt new file mode 100644 index 0000000..297ac78 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/Repository.kt @@ -0,0 +1,13 @@ +package com.melih.repository + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Result + +/** + * Abstract class to create contract in sources to seperate low level business logic from build and return type + */ +abstract class Repository { + + internal abstract suspend fun getNextLaunches(count: Int): Result> + internal abstract suspend fun getLaunchById(id: Long): Result +} diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt new file mode 100644 index 0000000..c7c3f27 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt @@ -0,0 +1,17 @@ +package com.melih.repository.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.melih.repository.DEFAULT_NAME +import com.squareup.moshi.Json + +@Entity(tableName = "Launches") +data class LaunchEntity( + @PrimaryKey val id: Long = 0L, + val name: String = DEFAULT_NAME, + @field:Json(name = "wsstamp") val launchStartTime: Long = 0L, + @field:Json(name = "westamp") val launchEndTime: Long = 0L, + val location: LocationEntity = LocationEntity(), + val rocket: RocketEntity = RocketEntity(), + val missions: List = listOf(MissionEntity()) +) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt new file mode 100644 index 0000000..5119207 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt @@ -0,0 +1,9 @@ +package com.melih.repository.entities + +data class LaunchesEntity( + val id: Long = 0L, + val launches: List = listOf(), + val total: Int = 0, + val offset: Int = 0, + val count: Int = 0 +) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt new file mode 100644 index 0000000..13bf125 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt @@ -0,0 +1,17 @@ +package com.melih.repository.entities + +import androidx.room.ColumnInfo +import com.melih.repository.DEFAULT_NAME + +data class LocationEntity( + @ColumnInfo(name = "id_location") val id: Long = 0L, + @ColumnInfo(name = "name_location") val name: String = DEFAULT_NAME, + val pads: List = listOf(PadEntity()) +) + +data class PadEntity( + @ColumnInfo(name = "id_pad") val id: Long = 0L, + @ColumnInfo(name = "name_pad") val name: String = DEFAULT_NAME, + val lat: Long = 0L, + val long: Long = 0L +) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt new file mode 100644 index 0000000..ab641e8 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt @@ -0,0 +1,12 @@ +package com.melih.repository.entities + +import androidx.room.ColumnInfo +import com.melih.repository.DEFAULT_NAME +import com.melih.repository.EMPTY_STRING + +data class MissionEntity( + @ColumnInfo(name = "id_mission") val id: Long = 0L, + @ColumnInfo(name = "name_mission") val name: String = DEFAULT_NAME, + val description: String = EMPTY_STRING, + val typeName: String = EMPTY_STRING +) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt new file mode 100644 index 0000000..3f220e5 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt @@ -0,0 +1,14 @@ +package com.melih.repository.entities + +import androidx.room.ColumnInfo +import com.melih.repository.DEFAULT_NAME +import com.melih.repository.EMPTY_STRING +import com.squareup.moshi.Json + +data class RocketEntity( + @ColumnInfo(name = "id_rocket") val id: Long = 0L, + @ColumnInfo(name = "name_rocket") val name: String = DEFAULT_NAME, + @field:Json(name = "familyname") val familyName: String = DEFAULT_NAME, + val imageSizes: IntArray = intArrayOf(), + val imageURL: String = EMPTY_STRING +) diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt new file mode 100644 index 0000000..c38bffa --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt @@ -0,0 +1,27 @@ +package com.melih.repository.interactors + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.BaseInteractor +import com.melih.repository.interactors.base.InteractorParameters +import com.melih.repository.interactors.base.Result +import com.melih.repository.sources.SourceManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.FlowCollector +import javax.inject.Inject + +/** + * Gets next given number of launches + */ +class GetLaunchDetails @Inject constructor( + private val sourceManager: SourceManager +) : BaseInteractor() { + + @ExperimentalCoroutinesApi + override suspend fun run(collector: FlowCollector>, params: Params) { + collector.emit(sourceManager.getLaunchById(params.id)) + } + + data class Params( + val id: Long + ) : InteractorParameters +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt new file mode 100644 index 0000000..ae4aa43 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt @@ -0,0 +1,27 @@ +package com.melih.repository.interactors + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.BaseInteractor +import com.melih.repository.interactors.base.InteractorParameters +import com.melih.repository.interactors.base.Result +import com.melih.repository.sources.SourceManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.FlowCollector +import javax.inject.Inject + +/** + * Gets next given number of launches + */ +class GetLaunches @Inject constructor( + private val sourceManager: SourceManager +) : BaseInteractor, GetLaunches.Params>() { + + @ExperimentalCoroutinesApi + override suspend fun run(collector: FlowCollector>>, params: Params) { + collector.emit(sourceManager.getNextLaunches(params.count)) + } + + data class Params( + val count: Int = 10 + ) : InteractorParameters +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt new file mode 100644 index 0000000..e33ecfa --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt @@ -0,0 +1,41 @@ +package com.melih.repository.interactors.base + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +/** + * Base use case that wraps [suspending][suspend] [run] function with [flow][Flow] and returns it for later usage. + */ +abstract class BaseInteractor { + + // region Abstractions + + @ExperimentalCoroutinesApi + protected abstract suspend fun run(collector: FlowCollector>, params: P) + // endregion + + // region Functions + + @ExperimentalCoroutinesApi + operator fun invoke(params: P) = + flow> { + emit(Result.State.Loading()) + run(this, params) + emit(Result.State.Loaded()) + }.flowOn(Dispatchers.IO) + // endregion +} + +/** + * Contract for parameter classes + */ +interface InteractorParameters + +/** + * Symbolizes absence of parameters for an [interactor][BaseInteractor] + */ +class None : Any(), InteractorParameters diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt new file mode 100644 index 0000000..5431133 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt @@ -0,0 +1,19 @@ +package com.melih.repository.interactors.base + +import androidx.annotation.StringRes +import com.melih.repository.R + + +/** + * [Result.Failure] reasons + */ +sealed class Reason(@StringRes val messageRes: Int) { + + class NetworkError : Reason(R.string.reason_network) + class EmptyResultError : Reason(R.string.reason_empty_body) + class GenericError : Reason(R.string.reason_generic) + class ResponseError : Reason(R.string.reason_response) + class TimeoutError : Reason(R.string.reason_timeout) + class PersistenceEmpty : Reason(R.string.reason_persistance_empty) + class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty) +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt new file mode 100644 index 0000000..c4d5306 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt @@ -0,0 +1,45 @@ +package com.melih.repository.interactors.base + + +/** + * Result class that wraps any [Success], [Failure] or [State] that can be generated by any derivation of [BaseInteractor] + */ +sealed class Result { + + // region Subclasses + + class Success(val successData: T) : Result() + class Failure(val errorData: Reason) : Result() + + sealed class State : Result() { + class Loading : State() + class Loaded : State() + } + // endregion + + // region Functions + + inline fun handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { + when (this) { + is Success -> successBlock(successData) + is Failure -> failureBlock(errorData) + is State -> stateBlock(this) + } + } + + inline fun handleSuccess(successBlock: (T) -> Unit) { + if (this is Success) + successBlock(successData) + } + + inline fun handleFailure(errorBlock: (Reason) -> Unit) { + if (this is Failure) + errorBlock(errorData) + } + + inline fun handleState(stateBlock: (State) -> Unit) { + if (this is State) + stateBlock(this) + } + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/network/Api.kt b/repository/src/main/kotlin/com/melih/repository/network/Api.kt new file mode 100644 index 0000000..5ce9cad --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/network/Api.kt @@ -0,0 +1,19 @@ +package com.melih.repository.network + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.entities.LaunchesEntity +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +/** + * Retrofit interface for networking + */ +interface Api { + + @GET("launch/next/{count}") + suspend fun getNextLaunches(@Path("count") count: Int): Response + + @GET("launch/{id}") + suspend fun getLaunchById(@Path("id") id: Long): Response +} diff --git a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt b/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt new file mode 100644 index 0000000..3a03cac --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt @@ -0,0 +1,43 @@ +package com.melih.repository.network + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.entities.LaunchesEntity +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Inject + +class ApiImpl @Inject constructor() : Api { + + // region Properties + + private val service by lazy { + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + Retrofit.Builder() + .client( + OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .setLevel(HttpLoggingInterceptor.Level.BODY) + ).build() + ) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .baseUrl("https://launchlibrary.net/1.4/") + .build() + .create(Api::class.java) + } + // endregion + + override suspend fun getNextLaunches(count: Int): Response = + service.getNextLaunches(count) + + override suspend fun getLaunchById(id: Long): Response = + service.getLaunchById(id) +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt b/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt new file mode 100644 index 0000000..304e2a8 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt @@ -0,0 +1,30 @@ +package com.melih.repository.persistence + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.persistence.converters.LocationConverter +import com.melih.repository.persistence.converters.MissionConverter +import com.melih.repository.persistence.converters.RocketConverter +import com.melih.repository.persistence.dao.LaunchesDao + +const val DB_NAME = "LaunchesDB" + +/** + * DB that manages launches + */ +@Database( + entities = [LaunchEntity::class], + exportSchema = true, + version = 1 +) +@TypeConverters( + LocationConverter::class, + RocketConverter::class, + MissionConverter::class +) +abstract class LaunchesDatabase : RoomDatabase() { + + abstract val launchesDao: LaunchesDao +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt new file mode 100644 index 0000000..735d615 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt @@ -0,0 +1,29 @@ +package com.melih.repository.persistence.converters + +import androidx.room.TypeConverter +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +/** + * Base converter for reduced boilerplate code + */ +abstract class BaseConverter { + + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + abstract fun getAdapter(moshi: Moshi): JsonAdapter + + // region Functions + + @TypeConverter + fun convertFrom(item: T) = + getAdapter(moshi).toJson(item) + + @TypeConverter + fun convertTo(string: String) = + getAdapter(moshi).fromJson(string) + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt new file mode 100644 index 0000000..f304c0a --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt @@ -0,0 +1,29 @@ +package com.melih.repository.persistence.converters + +import androidx.room.TypeConverter +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +/** + * Base converter for reduced boilerplate code + */ +abstract class BaseListConverter { + + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + abstract fun getAdapter(moshi: Moshi): JsonAdapter> + + // region Functions + + @TypeConverter + fun convertFrom(items: List) = + getAdapter(moshi).toJson(items) + + @TypeConverter + fun convertTo(string: String): List? = + getAdapter(moshi).fromJson(string) + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt new file mode 100644 index 0000000..a05da1b --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt @@ -0,0 +1,13 @@ +package com.melih.repository.persistence.converters + +import com.melih.repository.entities.LocationEntity +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi + +/** + * Converts [location][LocationEntity] + */ +class LocationConverter : BaseConverter() { + override fun getAdapter(moshi: Moshi): JsonAdapter = + moshi.adapter(LocationEntity::class.java) +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt new file mode 100644 index 0000000..1d25e9a --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt @@ -0,0 +1,20 @@ +package com.melih.repository.persistence.converters + +import com.melih.repository.entities.MissionEntity +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types + +/** + * Converts [mission][MissionEntity] + */ +class MissionConverter : BaseListConverter() { + + override fun getAdapter(moshi: Moshi): JsonAdapter> = + moshi.adapter( + Types.newParameterizedType( + List::class.java, + MissionEntity::class.java + ) + ) +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt new file mode 100644 index 0000000..980d64d --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt @@ -0,0 +1,13 @@ +package com.melih.repository.persistence.converters + +import com.melih.repository.entities.RocketEntity +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi + +/** + * Converts [rocket][RocketEntity] + */ +class RocketConverter : BaseConverter() { + override fun getAdapter(moshi: Moshi): JsonAdapter = + moshi.adapter(RocketEntity::class.java) +} diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt b/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt new file mode 100644 index 0000000..97f12bb --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt @@ -0,0 +1,44 @@ +package com.melih.repository.persistence.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import com.melih.repository.entities.LaunchEntity + +/** + * DAO for list of [launches][LaunchEntity] + */ +@Dao +abstract class LaunchesDao { + + // region Queries + + @Query("SELECT * FROM Launches LIMIT :count") + abstract suspend fun getLaunches(count: Int): List + + @Query("SELECT * FROM Launches WHERE id=:id LIMIT 1") + abstract suspend fun getLaunchById(id: Long): LaunchEntity? + + @Query("DELETE FROM Launches") + abstract suspend fun nukeLaunches() + // endregion + + // region Insertion + + @Insert + abstract suspend fun saveLaunches(launches: List) + + @Insert + abstract suspend fun saveLaunch(launch: LaunchEntity) + // endregion + + // region Transactions + + @Transaction + open suspend fun updateLaunches(launches: List) { + nukeLaunches() + saveLaunches(launches) + } + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt b/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt new file mode 100644 index 0000000..b64cd5c --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt @@ -0,0 +1,106 @@ +package com.melih.repository.sources + +import android.net.NetworkInfo +import com.melih.repository.DEFAULT_IMAGE_SIZE +import com.melih.repository.Repository +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import com.melih.repository.network.ApiImpl +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject +import javax.inject.Provider + +/** + * NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository], + * returning either [failure][Result.Failure] with proper [reason][Reason] or [success][Result.Success] with data + */ +class NetworkSource @Inject constructor( + private val apiImpl: ApiImpl, + private val networkInfoProvider: Provider +) : Repository() { + // region Properties + + private val isNetworkConnected: Boolean + get() { + val networkInfo = networkInfoProvider.get() + return networkInfo != null && networkInfo.isConnected + } + // endregion + + // region Functions + + override suspend fun getNextLaunches(count: Int): Result> = + safeExecute(apiImpl::getNextLaunches, count) { entity -> + entity.launches.map { launch -> + if (!launch.rocket.imageURL.isNotBlank()) { + launch.copy( + rocket = launch.rocket.copy( + imageURL = transformImageUrl( + launch.rocket.imageURL, + launch.rocket.imageSizes + ) + ) + ) + } else { + launch + } + } + } + + override suspend fun getLaunchById(id: Long): Result = + safeExecute(apiImpl::getLaunchById, id) { + if (!it.rocket.imageURL.isNotBlank()) { + it.copy( + rocket = it.rocket.copy( + imageURL = transformImageUrl(it.rocket.imageURL, it.rocket.imageSizes) + ) + ) + } else { + it + } + } + + private suspend inline fun safeExecute( + block: suspend (param: P) -> Response, + param: P, + transform: (T) -> R + ) = + if (isNetworkConnected) { + try { + block(param).extractResponseBody(transform) + } catch (e: IOException) { + Result.Failure(Reason.TimeoutError()) + } + } else { + Result.Failure(Reason.NetworkError()) + } + + private inline fun Response.extractResponseBody(transform: (T) -> R) = + if (isSuccessful) { + body()?.let { + Result.Success(transform(it)) + } ?: Result.Failure(Reason.EmptyResultError()) + } else { + Result.Failure(Reason.ResponseError()) + } + + private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) = + try { + val urlSplit = imageUrl.split("_") + val url = urlSplit[0] + val format = urlSplit[1].split(".")[1] + + var requestedSize = DEFAULT_IMAGE_SIZE + + if (!supportedSizes.contains(requestedSize)) { + requestedSize = supportedSizes.last { it < requestedSize } + } + + "${url}_$requestedSize.$format" + } catch (e: Exception) { + imageUrl + } + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt b/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt new file mode 100644 index 0000000..6d6c974 --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt @@ -0,0 +1,44 @@ +package com.melih.repository.sources + +import com.melih.repository.Repository +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import com.melih.repository.persistence.LaunchesDatabase +import javax.inject.Inject + +/** + * Persistance source using Room database to save / read objects for SST - offline usage + */ +class PersistenceSource @Inject constructor( + private val launchesDatabase: LaunchesDatabase +) : Repository() { + // region Functions + + override suspend fun getNextLaunches(count: Int): Result> = + launchesDatabase + .launchesDao + .getLaunches(count) + .takeIf { it.isNotEmpty() } + ?.run { + Result.Success(this) + } ?: Result.Failure(Reason.PersistenceEmpty()) + + override suspend fun getLaunchById(id: Long): Result = + launchesDatabase + .launchesDao + .getLaunchById(id) + .takeIf { it != null } + ?.run { + Result.Success(this) + } ?: Result.Failure(Reason.PersistenceEmpty()) + + internal suspend fun saveLaunches(launches: List) { + launchesDatabase.launchesDao.updateLaunches(launches) + } + + internal suspend fun saveLaunch(launch: LaunchEntity) { + launchesDatabase.launchesDao.saveLaunch(launch) + } + // endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt b/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt new file mode 100644 index 0000000..999225b --- /dev/null +++ b/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt @@ -0,0 +1,55 @@ +package com.melih.repository.sources + +import com.melih.repository.Repository +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import javax.inject.Inject + +/** + * Manages SST by using network & persistance sources + */ +class SourceManager @Inject constructor( + private val networkSource: NetworkSource, + private val persistenceSource: PersistenceSource +) : Repository() { + + // region Functions + + override suspend fun getNextLaunches(count: Int): Result> { + networkSource + .getNextLaunches(count) + .takeIf { it is Result.Success } + ?.let { + persistenceSource.saveLaunches((it as Result.Success).successData) + } + + return persistenceSource + .getNextLaunches(count) + .takeIf { + it is Result.Success && it.successData.isNotEmpty() + } + ?: Result.Failure(Reason.NoNetworkPersistenceEmpty()) + } + + override suspend fun getLaunchById(id: Long): Result { + val result = + persistenceSource + .getLaunchById(id) + + return if (result is Result.Failure) { + networkSource + .getLaunchById(id) + .takeIf { it is Result.Success } + ?.let { + persistenceSource.saveLaunch((it as Result.Success).successData) + } + + persistenceSource + .getLaunchById(id) + } else { + result + } + } + // endregion +} diff --git a/repository/src/main/res/values/strings.xml b/repository/src/main/res/values/strings.xml new file mode 100644 index 0000000..92cdbc9 --- /dev/null +++ b/repository/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + Network error + Response is empty + Something went wrong + Woops, seems we got a server error + Server timed out + There are no saved launches + Seems there are no data and network + diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt b/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt new file mode 100644 index 0000000..f1d6430 --- /dev/null +++ b/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt @@ -0,0 +1,67 @@ +package com.melih.repository.interactors.base + +import io.mockk.coVerify +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.toCollection +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Test +import java.util.* + + +class BaseInteractorTest { + + val testInteractor = spyk(TestInteractor()) + val testParams = TestParams() + + @Test + @ExperimentalCoroutinesApi + fun `BaseInteractor should send states and items emmited by run`() { + // Using run blocking due to threading problems in runBlockingTest + // See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 + + runBlocking { + // Get result by invoking + val result = testInteractor(testParams) + + // Verify we invoked interactor exactly once + coVerify(exactly = 1) { testInteractor.invoke(any()) } + + // Verify result is type of Flow + result shouldBeInstanceOf Flow::class + + // This will actually collec the flow + val resultDeque = ArrayDeque>() + result.toCollection(resultDeque) + + // We sent exactly 3 items, verify size + resultDeque.size shouldEqualTo 3 + + // Verify first item is Loading state + resultDeque.poll() shouldBeInstanceOf Result.State.Loading::class + + // Verify second item is Success, with default value we set below in TestParams class + resultDeque.poll().also { + it shouldBeInstanceOf Result.Success::class + (it as Result.Success).successData shouldEqualTo 10 + } + + // Verify last item is Loaded state + resultDeque.poll() shouldBeInstanceOf Result.State.Loaded::class + } + } + + inner class TestInteractor : BaseInteractor() { + + @ExperimentalCoroutinesApi + override suspend fun run(collector: FlowCollector>, params: TestParams) { + collector.emit(Result.Success(params.testValue)) + } + } + + data class TestParams(val testValue: Int = 10) : InteractorParameters +} \ No newline at end of file diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt b/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt new file mode 100644 index 0000000..79cfc02 --- /dev/null +++ b/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt @@ -0,0 +1,65 @@ +package com.melih.repository.interactors.base + +import com.melih.repository.R +import io.mockk.called +import io.mockk.spyk +import io.mockk.verify +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Test + +class ResultTest { + + private val number = 10 + + private val success = Result.Success(number) + private val failure = Result.Failure(Reason.GenericError()) + private val state = Result.State.Loading() + + private val emptyStateBlock = spyk({ _: Result.State -> }) + private val emptyFailureBlock = spyk({ _: Reason -> }) + private val emptySuccessBlock = spyk({ _: Int -> }) + + @Test + fun `Success should only invoke successBlock with correct data`() { + val actualSuccessBlock = spyk({ data: Int -> + data shouldEqualTo number + Unit + }) + + success.handle(emptyStateBlock, emptyFailureBlock, actualSuccessBlock) + + verify { emptyStateBlock wasNot called } + verify { emptyFailureBlock wasNot called } + verify(exactly = 1) { actualSuccessBlock.invoke(any()) } + } + + @Test + fun `Failure should only invoke failureBlock with correct error`() { + val actualFailureBlock = spyk({ reason: Reason -> + reason shouldBeInstanceOf Reason.GenericError::class + (reason as Reason.GenericError).messageRes shouldEqualTo R.string.reason_generic + Unit + }) + + failure.handle(emptyStateBlock, actualFailureBlock, emptySuccessBlock) + + verify { emptySuccessBlock wasNot called } + verify { emptyStateBlock wasNot called } + verify(exactly = 1) { actualFailureBlock.invoke(any()) } + } + + @Test + fun `State should only invoke stateBlock with correct state`() { + val actualSuccessBlock = spyk({ state: Result.State -> + state shouldBeInstanceOf Result.State.Loading::class + Unit + }) + + state.handle(actualSuccessBlock, emptyFailureBlock, emptySuccessBlock) + + verify { emptySuccessBlock wasNot called } + verify { emptyFailureBlock wasNot called } + verify(exactly = 1) { actualSuccessBlock.invoke(any()) } + } +} diff --git a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt new file mode 100644 index 0000000..55f9528 --- /dev/null +++ b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt @@ -0,0 +1,102 @@ +package com.melih.repository.sources + +import android.net.NetworkInfo +import com.melih.repository.R +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.entities.LaunchesEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import com.melih.repository.network.ApiImpl +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import javax.inject.Provider + +class NetworkSourceTest { + + private val apiImpl = mockk(relaxed = true) + private val networkInfoProvider = mockk>(relaxed = true) { + every { get() } returns mockk(relaxed = true) + } + + private val source = spyk(NetworkSource(apiImpl, networkInfoProvider)) + + @Nested + inner class GetNextLaunches { + + @Test + @ExperimentalCoroutinesApi + fun `should return network error when internet is not connected`() { + every { networkInfoProvider.get().isConnected } returns false + + runBlockingTest { + val result = source.getNextLaunches(1) + + result shouldBeInstanceOf Result.Failure::class + result.handleFailure { + it shouldBeInstanceOf Reason.NetworkError::class + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should return response error when it is not successful`() { + every { networkInfoProvider.get().isConnected } returns true + coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns false + + runBlockingTest { + val result = source.getNextLaunches(1) + + result shouldBeInstanceOf Result.Failure::class + result.handleFailure { + it shouldBeInstanceOf Reason.ResponseError::class + (it as Reason.ResponseError).messageRes shouldEqualTo R.string.reason_response + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should return empty result error when body is null`() { + every { networkInfoProvider.get().isConnected } returns true + coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true + coEvery { apiImpl.getNextLaunches(any()).body() } returns null + + runBlockingTest { + val result = source.getNextLaunches(1) + + result shouldBeInstanceOf Result.Failure::class + result.handleFailure { + it shouldBeInstanceOf Reason.EmptyResultError::class + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should return success with data if execution is successful`() { + every { networkInfoProvider.get().isConnected } returns true + coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true + coEvery { apiImpl.getNextLaunches(any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013))) + + runBlockingTest { + val result = source.getNextLaunches(1) + + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it shouldBeInstanceOf List::class + it.size shouldEqualTo 1 + it[0].id shouldEqualTo 1013 + } + } + } + } +} diff --git a/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt new file mode 100644 index 0000000..a423423 --- /dev/null +++ b/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt @@ -0,0 +1,56 @@ +package com.melih.repository.sources + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import com.melih.repository.persistence.LaunchesDatabase +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class PersistanceSourceTest { + + private val dbImplementation = mockk(relaxed = true) + private val source = spyk(PersistenceSource(dbImplementation)) + + @Nested + inner class GetNextLaunches { + + @Test + @ExperimentalCoroutinesApi + fun `should return persistance empty error when db is empty`() { + runBlockingTest { + coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns emptyList() + + val result = source.getNextLaunches(10) + result shouldBeInstanceOf Result.Failure::class + result.handleFailure { + it shouldBeInstanceOf Reason.PersistenceEmpty::class + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should return success with data if db is not empty`() { + runBlockingTest { + coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns listOf(LaunchEntity(id = 1013)) + + val result = source.getNextLaunches(10) + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it.isEmpty() shouldBe false + it.size shouldEqualTo 1 + it[0].id shouldEqualTo 1013 + } + } + } + } +} diff --git a/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt new file mode 100644 index 0000000..8415a8f --- /dev/null +++ b/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt @@ -0,0 +1,141 @@ +package com.melih.repository.sources + +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import io.mockk.called +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runBlockingTest +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class SourceManagerTest { + + private val networkSource = mockk(relaxed = true) + private val persistenceSource = mockk(relaxed = true) + + private val sourceManager = spyk(SourceManager(networkSource, persistenceSource)) + + @Nested + inner class GetNextLaunches { + + @Test + @ExperimentalCoroutinesApi + fun `should try to fetch, save and return result from persistance`() { + runBlockingTest { + val amount = 10 + coEvery { networkSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1012))) + coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013))) + + val result = sourceManager.getNextLaunches(amount) + + coVerifyOrder { + networkSource.getNextLaunches(any()) + persistenceSource.saveLaunches(any()) + persistenceSource.getNextLaunches(any()) + } + + coVerify(exactly = 1) { networkSource.getNextLaunches(any()) } + coVerify(exactly = 1) { persistenceSource.saveLaunches(any()) } + coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } + + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it.size shouldEqualTo 1 + it[0].id shouldEqualTo 1013 + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should not save response if fetching was failure and return result from persistance`() { + runBlockingTest { + val amount = 10 + coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError()) + coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013))) + + val result = sourceManager.getNextLaunches(amount) + + coVerify { persistenceSource.saveLaunches(any()) wasNot called } + coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } + + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it.size shouldEqualTo 1 + it[0].id shouldEqualTo 1013 + } + } + } + + @Test + @ExperimentalCoroutinesApi + fun `should return failure if network and persistance fails`() { + runBlockingTest { + val amount = 10 + coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError()) + coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Failure(Reason.PersistenceEmpty()) + + val result = sourceManager.getNextLaunches(amount) + + coVerify { persistenceSource.saveLaunches(any()) wasNot called } + coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } + + result shouldBeInstanceOf Result.Failure::class + result.handleFailure { + it shouldBeInstanceOf Reason.NoNetworkPersistenceEmpty::class + } + } + } + } + + @Nested + inner class GetLaunchDetails { + + @Test + fun `should return result from persistance immediately if it's found`() { + runBlocking { + coEvery { persistenceSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013)) + + val result = sourceManager.getLaunchById(1) + + coVerify { networkSource.getLaunchById(any()) wasNot called } + + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it.id shouldEqualTo 1013 + } + } + } + + @Test + fun `should fetch result from network if it's not found in persistance`() { + runBlocking { + coEvery { + persistenceSource.getLaunchById(any()) + } returns Result.Failure(Reason.PersistenceEmpty()) andThen Result.Success(LaunchEntity(id = 1013)) + + coEvery { networkSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013)) + + val result = sourceManager.getLaunchById(1) + + coVerify(exactly = 1) { networkSource.getLaunchById(any()) } + coVerify(exactly = 1) { persistenceSource.saveLaunch(any()) } + coVerify(exactly = 2) { persistenceSource.getLaunchById(any()) } + + result shouldBeInstanceOf Result.Success::class + result.handleSuccess { + it.id shouldEqualTo 1013 + } + } + } + } +} diff --git a/scripts/default_android_config.gradle b/scripts/default_android_config.gradle new file mode 100644 index 0000000..b5ab234 --- /dev/null +++ b/scripts/default_android_config.gradle @@ -0,0 +1,13 @@ +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" + +android { + compileSdkVersion versions.compileSdkVersion + + defaultConfig { + minSdkVersion versions.minSdkVersion + targetSdkVersion versions.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } +} diff --git a/scripts/default_dependencies.gradle b/scripts/default_dependencies.gradle new file mode 100644 index 0000000..e5c9685 --- /dev/null +++ b/scripts/default_dependencies.gradle @@ -0,0 +1,16 @@ +apply from: "$rootProject.projectDir/scripts/detekt.gradle" +apply from: "$rootProject.projectDir/scripts/dokka.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation libraries.kotlin + implementation libraries.dagger + implementation libraries.timber + + kapt annotationProcessors.daggerCompiler + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent +} diff --git a/scripts/dependencies.gradle b/scripts/dependencies.gradle new file mode 100644 index 0000000..b48f236 --- /dev/null +++ b/scripts/dependencies.gradle @@ -0,0 +1,146 @@ +ext { + + versions = [ + minSdkVersion : 16, + minSdkVersionDev : 21, + compileSdkVersion : 28, + targetSdkVersion : 28, + buildToolsVersion : "28.0.3", + supportLibraryVersion : "28.0.0", + appCompatVersion : "1.1.0-alpha04", + lifecycleVersion : "2.2.0-alpha01", + fragmentVersion : "1.1.0-beta01", + workManagerVersion : "2.1.0-alpha03", + constraintLayoutVesion: "2.0.0-beta1", + cardViewVersion : "1.0.0", + recyclerViewVersion : "1.1.0-alpha06", + pagingVersion : "2.1.0", + viewPagerVersion : "1.0.0-alpha05", + collectionVersion : "1.1.0", + roomVersion : "2.1.0", + daggerVersion : "2.22.1", + okHttpVersion : "3.12.0", + retrofitVersion : "2.6.0", + picassoVersion : "2.71828", + moshiVersion : "1.8.0", + coroutinesVersion : "1.3.0-M1", + leakCanaryVersion : "2.0-alpha-2", + timberVersion : "4.7.1", + jUnitVersion : "5.4.2", + espressoVersion : "3.2.0", + mockkVersion : "1.9.3", + kluentVersion : "1.49", + ] + + libraries = [ + /** + * Android libraries + */ + appCompat : "androidx.appcompat:appcompat:${versions.appCompatVersion}", + recyclerView : "androidx.recyclerview:recyclerview:${versions.recyclerViewVersion}", + cardView : "androidx.cardview:cardview:${versions.cardViewVersion}", + constraintLayout: "androidx.constraintlayout:constraintlayout:${versions.constraintLayoutVesion}", + multixDex : "androidx.multidex:multidex:2.0.1", + fragment : "androidx.fragment:fragment-ktx:${versions.fragmentVersion}", + + /** + * Jetpack + */ + navigation : [ + "androidx.navigation:navigation-fragment-ktx:$nav_version", + "androidx.navigation:navigation-ui-ktx:$nav_version" + ], + + room : [ + "androidx.room:room-runtime:${versions.roomVersion}", + "androidx.room:room-ktx:${versions.roomVersion}" + ], + + lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}", + liveData : "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycleVersion}", + workManager : "androidx.work:work-runtime-ktx:${versions.workManagerVersion}", + paging : "androidx.paging:paging-runtime-ktx:${versions.pagingVersion}", + viewPager : "androidx.viewpager2:viewpager2:${versions.viewPagerVersion}", + collection : "androidx.collection:collection:${versions.collectionVersion}", + + /** + * Kotlin + */ + kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version", + coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}", + + /** + * Dagger + */ + dagger : [ + "com.google.dagger:dagger:${versions.daggerVersion}", + "com.google.dagger:dagger-android:${versions.daggerVersion}", + "com.google.dagger:dagger-android-support:${versions.daggerVersion}" + ], + + /** + * OkHttp + */ + okHttp : [ + "com.squareup.okhttp3:okhttp:${versions.okHttpVersion}", + "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}" + ], + + okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}", + + /** + * Retrofit + */ + retrofit : [ + "com.squareup.retrofit2:retrofit:${versions.retrofitVersion}", + "com.squareup.retrofit2:converter-moshi:${versions.retrofitVersion}" + ], + + /** + * Moshi + */ + moshi : [ + "com.squareup.moshi:moshi:${versions.moshiVersion}", + "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}" + ], + + moshiKotlin : "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}", + + /** + * Picasso for image loading + */ + picasso : "com.squareup.picasso:picasso:${versions.picassoVersion}", + + /** + * LeakCanary + */ + leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}", + + /** + * Timber + */ + timber : "com.jakewharton.timber:timber:${versions.timberVersion}" + ] + + annotationProcessors = [ + roomCompiler : "androidx.room:room-compiler:${versions.roomVersion}", + daggerCompiler: [ + "com.google.dagger:dagger-compiler:${versions.daggerVersion}", + "com.google.dagger:dagger-android-processor:${versions.daggerVersion}" + ], + ] + + testLibraries = [ + jUnitApi : "org.junit.jupiter:junit-jupiter-api:${versions.jUnitVersion}", + jUnitEngine : "org.junit.jupiter:junit-jupiter-engine:${versions.jUnitVersion}", + jUnitVintage : "org.junit.vintage:junit-vintage-engine:${versions.jUnitVersion}", + jUnitAndroid : "androidx.test.ext:junit:1.1.0", + fragmentTest : "androidx.fragment:fragment-testing:${versions.fragmentVersion}", + multidexInstrumentation: "androidx.multidex:multidex-instrumentation:2.0.0", + coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutinesVersion}", + coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.coroutinesVersion}", + espresso : "androidx.test.espresso:espresso-core:${versions.espressoVersion}", + mockk : "io.mockk:mockk:${versions.mockkVersion}", + kluent : "org.amshove.kluent:kluent-android:${versions.kluentVersion}" + ] +} diff --git a/scripts/detekt.gradle b/scripts/detekt.gradle new file mode 100644 index 0000000..17bbe39 --- /dev/null +++ b/scripts/detekt.gradle @@ -0,0 +1,14 @@ +apply plugin: 'io.gitlab.arturbosch.detekt' + +detekt { + toolVersion = "1.0.0-RC14" + config = files("$rootProject.projectDir/default-detekt-config.yml") + filters = ".*/resources/.*,.*/build/.*" + + reports { + html { + enabled = true + destination = file("$rootProject.projectDir/reports/detekt/$projectDir.name-report.html") + } + } +} diff --git a/scripts/dokka.gradle b/scripts/dokka.gradle new file mode 100644 index 0000000..59bed4c --- /dev/null +++ b/scripts/dokka.gradle @@ -0,0 +1,10 @@ +apply plugin: 'org.jetbrains.dokka-android' + +dokka { + outputFormat = "html" + outputDirectory = "$rootProject.projectDir/reports/javadoc" + jdkVersion = 8 + + reportUndocumented = true + skipEmptyPackages = true +} diff --git a/scripts/feature_module.gradle b/scripts/feature_module.gradle new file mode 100644 index 0000000..146c113 --- /dev/null +++ b/scripts/feature_module.gradle @@ -0,0 +1,16 @@ +apply from: "$rootProject.projectDir/scripts/module.gradle" + +android { + dataBinding { + enabled = true + } +} + +dependencies { + implementation project(':core') + + implementation libraries.fragment + implementation libraries.lifecycle + implementation libraries.navigation + implementation libraries.constraintLayout +} diff --git a/scripts/flavors.gradle b/scripts/flavors.gradle new file mode 100644 index 0000000..49de32e --- /dev/null +++ b/scripts/flavors.gradle @@ -0,0 +1,17 @@ +android { + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + dev { + initWith debug + } + } +} diff --git a/scripts/module.gradle b/scripts/module.gradle new file mode 100644 index 0000000..cd8295f --- /dev/null +++ b/scripts/module.gradle @@ -0,0 +1,3 @@ +apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/sources.gradle" +apply from: "$rootProject.projectDir/scripts/flavors.gradle" \ No newline at end of file diff --git a/scripts/sources.gradle b/scripts/sources.gradle new file mode 100644 index 0000000..e44f000 --- /dev/null +++ b/scripts/sources.gradle @@ -0,0 +1,12 @@ +android { + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + androidTest.java.srcDirs += "src/androidTest/kotlin" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6bb6ae3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app', ':repository', ':core', ':features:list', ':features:detail' +rootProject.name = 'Rocket Science'