diff --git a/.circleci/config.yml b/.circleci/config.yml index 4a7b433..e4542dd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,19 +26,114 @@ jobs: command: fastlane detekt - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ path: reports/detekt - - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ - path: reports # Tests + ## App + - run: + name: Test App + command: | + fastlane test_app + ./gradlew app:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: app/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: app/build/test-results + + ## Core + - run: + name: Test Core + command: | + fastlane test_core + ./gradlew core:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: core/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: core/build/test-results + + ## Launches + - run: + name: Test Launches + command: | + fastlane test_launches + ./gradlew features:launches:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: features/launches/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: features/launches/build/test-results + + ## Detail + - run: + name: Test Detail + command: | + fastlane test_detail + ./gradlew features:detail:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: features/detail/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: features/detail/build/test-results + + ## Abstractions + - run: + name: Test Abstractions + command: | + fastlane test_abstractions + ./gradlew abstractions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Definitions + - run: + name: Test Definitions + command: | + fastlane test_definitions + ./gradlew data:definitions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Interactors + - run: + name: Test Interactors + command: | + fastlane test_interactors + ./gradlew data:interactors:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Network + - run: + name: Test Network + command: | + fastlane test_network + ./gradlew data:network:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Persistence - run: - name: Tests + name: Test Persistence command: | - fastlane test_all - ./gradlew jacocoTestReport + fastlane test_persistence + ./gradlew data:persistence:jacocoTestReport bash <(curl -s https://codecov.io/bash) - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ - path: build/reports/tests + path: repository/build/reports/tests - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ - path: reports + path: repository/build/test-results # See https://circleci.com/docs/2.0/deployment-integrations/ for deploy examples diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..a6ec318 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,86 @@ +name: Android CI + +on: [push, pull_request] + +jobs: + + code_quality: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v1 + + - name: Setup + run: | + gem install bundler + bundle install + + - name: Code Quality ( Detekt ) + run: fastlane detekt + + test: + + runs-on: macOS-latest + + steps: + - uses: actions/checkout@v1 + + - name: Setup + run: | + gem install bundler + bundle install + + - name: Test App + run: | + bundle exec fastlane test_app + ./gradlew app:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Core + run: | + bundle exec fastlane test_core + ./gradlew core:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Launches + run: | + bundle exec fastlane test_launches + ./gradlew features:launches:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Detail + run: | + bundle exec fastlane test_detail + ./gradlew features:detail:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Abstractions + run: | + bundle exec fastlane test_abstractions + ./gradlew abstractions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Definitions + run: | + bundle exec fastlane test_definitions + ./gradlew data:definitions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Interactors + run: | + bundle exec fastlane test_interactors + ./gradlew data:interactors:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Network + run: | + bundle exec fastlane test_network + ./gradlew data:network:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Persistence + run: | + bundle exec fastlane test_persistence + ./gradlew data:persistence:jacocoTestReport + bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 90bb429..4cdb0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ /.idea .DS_Store /build +**/build +/fastlane/README.md +/fastlane/report.xml /captures .externalNativeBuild /projectFilesBackup @@ -11,3 +14,6 @@ # Project reports /reports + +#jacoco.exec +jacoco.exec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..40a7662 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,159 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + atomos (0.1.3) + babosa (1.0.3) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.10) + declarative-option (0.1.0) + digest-crc (0.4.1) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.5) + emoji_regex (1.0.1) + excon (0.71.1) + faraday (0.17.3) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + faraday_middleware (0.13.1) + faraday (>= 0.7.4, < 1.0) + fastimage (2.1.7) + fastlane (2.139.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + babosa (>= 1.0.2, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 2.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 0.17) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 0.13.1) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.29.2, < 0.37.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (~> 2.1.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_xml (~> 0.5) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + public_suffix (~> 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-api-client (0.36.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-cloud-core (1.4.1) + google-cloud-env (~> 1.0) + google-cloud-env (1.3.0) + faraday (~> 0.11) + google-cloud-storage (1.25.0) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-api-client (~> 0.33) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.10.0) + faraday (~> 0.12) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.12) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + json (2.3.0) + jwt (2.1.0) + memoist (0.16.2) + mini_magick (4.9.5) + mini_mime (1.0.2) + multi_json (1.14.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nanaimo (0.2.6) + naturally (2.2.0) + os (1.0.1) + plist (3.5.0) + public_suffix (2.0.5) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rouge (2.0.7) + rubyzip (1.3.0) + security (0.1.3) + signet (0.12.0) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.7) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-cursor (0.7.0) + tty-screen (0.7.0) + tty-spinner (0.9.2) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.6) + unicode-display_width (1.6.0) + word_wrap (1.0.0) + xcodeproj (1.14.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.0) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.0.1 diff --git a/README.md b/README.md index e700c93..f4bd56d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# Rocket Science +# Android-Kotlin-Modulerized-CleanArchitecture -[![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![CircleCI](https://circleci.com/gh/melihaksoy/RocketScience/tree/master.svg?style=svg&circle-token=705e399a0116be0a5bb10bddc72fc7ef19b568e3)](https://circleci.com/gh/melihaksoy/RocketScience/tree/master) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) +[![Actions](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/workflows/Android%20CI/badge.svg)](https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/actions) [![CircleCI](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master.svg?style=svg)](https://circleci.com/gh/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture/tree/master) [![codebeat badge](https://codebeat.co/badges/542fb08a-b3cc-4ff8-b1bb-35a66932f12f)](https://codebeat.co/projects/github-com-melihaksoy-rocketscience-master) [![codecov](https://codecov.io/gh/melihaksoy/RocketScience/branch/master/graph/badge.svg?token=pXPKpV5dz6)](https://codecov.io/gh/melihaksoy/RocketScience) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) -RocketScience is a prototype application tries to serve an example for modularization & clean architecture in Android. +This is a prototype application tries to serve an example for modularization & clean architecture in Android. While there are many blogs about good practices, it's hard to come by a complete example that merges these different popular topics & approaches. -RocketScience takes popular approaches and libraries to create an example on how to actually bind these components with each other. +This project takes popular approaches and libraries to create an example on how to actually bind these components with each other. I'll soon be writing small series about roadmap, challanges, alternatives and try - fails I've encountered during development in [Medium](https://medium.com/@aksoymelihcan). diff --git a/core/.gitignore b/abstractions/.gitignore similarity index 100% rename from core/.gitignore rename to abstractions/.gitignore diff --git a/abstractions/build.gradle b/abstractions/build.gradle new file mode 100644 index 0000000..86354c0 --- /dev/null +++ b/abstractions/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: "de.mannodermaus.android-junit5" + +apply from: "$rootProject.projectDir/scripts/module.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation libraries.kotlin + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent + + testRuntimeOnly testLibraries.jUnitEngine +} diff --git a/repository/proguard-rules.pro b/abstractions/consumer-rules.pro similarity index 100% rename from repository/proguard-rules.pro rename to abstractions/consumer-rules.pro diff --git a/abstractions/proguard-rules.pro b/abstractions/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/abstractions/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/abstractions/src/main/AndroidManifest.xml b/abstractions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c706bc8 --- /dev/null +++ b/abstractions/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt b/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt new file mode 100644 index 0000000..6d9668e --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt @@ -0,0 +1,3 @@ +package com.melih.abstractions.data + +interface ViewEntity diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt new file mode 100644 index 0000000..030a02e --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt @@ -0,0 +1,6 @@ +package com.melih.abstractions.deliverable + +abstract class Reason : Throwable() { + + abstract val messageRes: Int +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt similarity index 64% rename from repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt rename to abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt index a5684c7..f81e27e 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt +++ b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt @@ -1,15 +1,12 @@ -package com.melih.repository.interactors.base - -import kotlinx.coroutines.ExperimentalCoroutinesApi +package com.melih.abstractions.deliverable /** - * Result class that wraps any [Success], [Failure] or [State] that can be generated by any derivation of [BaseInteractor] + * Result class that wraps any [Success], [Failure] or [State] */ -@UseExperimental(ExperimentalCoroutinesApi::class) sealed class Result -// region Subclasses +//region Subclasses class Success(val successData: T) : Result() class Failure(val errorData: Reason) : Result() @@ -18,11 +15,15 @@ sealed class State : Result() { class Loading : State() class Loaded : State() } -// endregion +//endregion -// region Extensions +//region Extensions -inline fun Result.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { +inline fun Result.handle( + stateBlock: (State) -> Unit, + failureBlock: (Reason) -> Unit, + successBlock: (T) -> Unit +) { when (this) { is Success -> successBlock(successData) is Failure -> failureBlock(errorData) @@ -50,4 +51,4 @@ inline fun Result.onState(stateBlock: (State) -> Unit): Result { return this } -// endregion +//endregion diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt b/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt new file mode 100644 index 0000000..0eba505 --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt @@ -0,0 +1,8 @@ +package com.melih.abstractions.mapper + +import com.melih.abstractions.data.ViewEntity + +interface Mapper { + + fun convert(t: T): R +} diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt b/abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt similarity index 80% rename from repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt rename to abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt index cc770df..a21f935 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt +++ b/abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt @@ -1,6 +1,10 @@ -package com.melih.repository.interactors.base +package com.melih.abstractions -import com.melih.repository.R +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success +import com.melih.abstractions.deliverable.handle import io.mockk.called import io.mockk.spyk import io.mockk.verify @@ -8,12 +12,17 @@ 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 = Success(number) - private val failure = Failure(GenericError()) + private val failure = Failure(object : Reason() { + override val messageRes: Int + get() = 10 + + }) private val state = State.Loading() private val emptyStateBlock = spyk({ _: State -> }) @@ -37,8 +46,7 @@ class ResultTest { @Test fun `Failure should only invoke failureBlock with correct error`() { val actualFailureBlock = spyk({ reason: Reason -> - reason shouldBeInstanceOf GenericError::class - (reason as GenericError).messageRes shouldEqualTo R.string.reason_generic + reason.messageRes shouldEqualTo 10 Unit }) diff --git a/app/build.gradle b/app/build.gradle index abf4201..8dd64c3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,8 +3,8 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle" -apply from: "$rootProject.projectDir/scripts/flavors.gradle" android { defaultConfig { @@ -14,8 +14,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - dataBinding { - enabled = true + buildFeatures{ + dataBinding = true } } @@ -26,15 +26,23 @@ dependencies { implementation project(':features:launches') 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 + // These libraries required by dagger to create dependency graph and application compilation, but not used by app + + compileOnly project(':abstractions') + compileOnly project(':data:interactors') + compileOnly project(':data:network') + compileOnly project(':data:definitions') + compileOnly libraries.retrofit - compileOnly libraries.room compileOnly libraries.paging + compileOnly libraries.swipeRefreshLayout + + // Need for proper renders in xml previews + compileOnly libraries.constraintLayout } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index da2a358..ce0ec64 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,8 +21,5 @@ @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.** { *; } +-keep class com.melih.definitions.entities.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e6e52f..6013c48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,10 @@ - + + tools:ignore="GoogleAppIndexingWarning"> + + + + + + + + + + diff --git a/app/src/main/kotlin/com/melih/rocketscience/App.kt b/app/src/main/kotlin/com/melih/rocketscience/App.kt index b2d8e13..803856c 100644 --- a/app/src/main/kotlin/com/melih/rocketscience/App.kt +++ b/app/src/main/kotlin/com/melih/rocketscience/App.kt @@ -1,10 +1,12 @@ package com.melih.rocketscience import com.melih.core.di.DaggerCoreComponent +import com.melih.launches.data.LaunchDetailItem import com.melih.rocketscience.di.DaggerAppComponent import dagger.android.AndroidInjector import dagger.android.DaggerApplication import timber.log.Timber +import javax.inject.Inject class App : DaggerApplication() { override fun applicationInjector(): AndroidInjector = @@ -14,7 +16,6 @@ class App : DaggerApplication() { .create(this) ) - override fun onCreate() { super.onCreate() diff --git a/app/src/main/kotlin/com/melih/rocketscience/MainActivity.kt b/app/src/main/kotlin/com/melih/rocketscience/MainActivity.kt new file mode 100644 index 0000000..696b4dd --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/MainActivity.kt @@ -0,0 +1,29 @@ +package com.melih.rocketscience + +import android.os.Bundle +import androidx.appcompat.widget.Toolbar +import androidx.navigation.NavController +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI +import dagger.android.support.DaggerAppCompatActivity + +class MainActivity : DaggerAppCompatActivity() { + + private lateinit var navController: NavController + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + navController = findNavController(R.id.nav_host_fragment) + NavigationUI.setupWithNavController(findViewById(R.id.toolbar), navController) + } + + override fun onSupportNavigateUp(): Boolean { + if (!NavigationUI.navigateUp(navController, null)) { + onBackPressed() + } + + return true + } +} diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt index ffa55c8..ebbc8a3 100644 --- a/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt @@ -1,8 +1,6 @@ package com.melih.rocketscience.di import com.melih.core.di.CoreComponent -import com.melih.detail.di.DetailFeatureModule -import com.melih.list.di.LaunchesFeatureModule import com.melih.rocketscience.App import dagger.Component import dagger.android.AndroidInjectionModule @@ -10,9 +8,10 @@ import dagger.android.AndroidInjector @AppScope @Component( - modules = [AndroidInjectionModule::class, - LaunchesFeatureModule::class, - DetailFeatureModule::class], + modules = [ + AndroidInjectionModule::class, + AppModule::class + ], dependencies = [CoreComponent::class] ) 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..0d0630b --- /dev/null +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt @@ -0,0 +1,21 @@ +package com.melih.rocketscience.di + +import com.melih.detail.di.DetailContributor +import com.melih.launches.data.LaunchDetailItem +import com.melih.launches.di.LaunchesContributor +import com.melih.rocketscience.MainActivity +import dagger.Module +import dagger.Provides +import dagger.android.ContributesAndroidInjector + +@Module +abstract class AppModule { + + + @ContributesAndroidInjector( + modules = [ + LaunchesContributor::class, + DetailContributor::class] + ) + abstract fun mainActivity(): MainActivity +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..51075f4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml new file mode 100644 index 0000000..c390eb6 --- /dev/null +++ b/app/src/main/res/navigation/nav_main.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/build.gradle b/build.gradle index 1314a64..52cec02 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,18 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.41' - ext.nav_version = '2.2.0-alpha01' + ext.kotlin_version = '1.3.72' + ext.nav_version = '2.3.0' repositories { google() jcenter() - + maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0-rc03' + classpath 'com.android.tools.build:gradle:4.2.0-alpha03' 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.5.0.0" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.6.0.1-SNAPSHOT" 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 @@ -20,23 +20,19 @@ buildscript { } plugins { - id "io.gitlab.arturbosch.detekt" version "1.0.0" - id "org.jetbrains.dokka" version "0.9.18" - id "jacoco" + id 'io.gitlab.arturbosch.detekt' version '1.10.0' + id 'org.jetbrains.dokka' version '0.10.1' + id 'jacoco' } allprojects { repositories { google() jcenter() - + maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } } } -task clean(type: Delete) { - delete rootProject.buildDir -} - task removeReports(type: Delete) { delete fileTree(rootProject.projectDir.path + "/reports") { include '**/*.*' @@ -97,7 +93,9 @@ task projectDependencyGraph { rootProjects.remove(dependency) def graphKey = new Tuple2(project, dependency) - def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList() } + def traits = dependencies.computeIfAbsent(graphKey) { + new ArrayList() + } if (config.name.toLowerCase().endsWith('implementation')) { traits.add('style=dotted') diff --git a/core/build.gradle b/core/build.gradle index adb92c2..325677e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,31 +3,30 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle" -apply from: "$rootProject.projectDir/scripts/flavors.gradle" android { - dataBinding { - enabled = true + + buildFeatures{ + dataBinding = true } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - api project(":repository") - + implementation libraries.coroutines implementation libraries.fragment implementation libraries.paging implementation libraries.lifecycle implementation libraries.liveDataKTX implementation libraries.navigation implementation libraries.picasso + implementation libraries.material testImplementation testLibraries.jUnitApi testImplementation testLibraries.mockk testImplementation testLibraries.kluent testImplementation testLibraries.coroutinesTest - - compileOnly libraries.room } diff --git a/core/jacoco.exec b/core/jacoco.exec deleted file mode 100644 index 45a3fec..0000000 Binary files a/core/jacoco.exec and /dev/null differ diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 7a7f527..e3436a3 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + - + diff --git a/core/src/main/kotlin/com/melih/core/actions/Actions.kt b/core/src/main/kotlin/com/melih/core/actions/Actions.kt index 8fbd473..ca813c9 100644 --- a/core/src/main/kotlin/com/melih/core/actions/Actions.kt +++ b/core/src/main/kotlin/com/melih/core/actions/Actions.kt @@ -1,16 +1,12 @@ package com.melih.core.actions -import android.content.Intent - -const val EXTRA_LAUNCH_ID = "extras:detail:launchid" +import android.net.Uri +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment +import com.melih.core.R /** * Navigation actions for navigation between feature activities */ -object Actions { - - fun openDetailFor(id: Long) = - Intent("action.dashboard.open") - .putExtra(EXTRA_LAUNCH_ID, id) - -} +fun Fragment.openDetail(id: Long) = + NavHostFragment.findNavController(this).navigate(Uri.parse(getString(R.string.detail_uri, 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 deleted file mode 100644 index ba672c4..0000000 --- a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseActivity.kt +++ /dev/null @@ -1,56 +0,0 @@ -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 - -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 - - 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 index 0ca715f..23014aa 100644 --- a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseDaggerFragment.kt @@ -8,13 +8,14 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import dagger.android.support.AndroidSupportInjection import javax.inject.Inject +import kotlin.properties.Delegates /** * 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. + * Note that fragments that extends from [BaseDaggerFragment] should contribute their 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] @@ -22,22 +23,22 @@ import javax.inject.Inject */ abstract class BaseDaggerFragment : BaseFragment(), HasAndroidInjector { - // region Properties + //region Properties - @get:Inject - internal var androidInjector: DispatchingAndroidInjector? = null + @Inject + protected lateinit var androidInjector: DispatchingAndroidInjector @Inject - lateinit var viewModelFactory: ViewModelFactory - // endregion + protected lateinit var viewModelFactory: ViewModelFactory + //endregion - // region Functions + //region Functions override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) super.onAttach(context) } - override fun androidInjector(): AndroidInjector? = androidInjector - // endregion + override fun androidInjector(): AndroidInjector = androidInjector + //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 index 4224e72..368a89e 100644 --- a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt @@ -8,10 +8,9 @@ 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 import com.google.android.material.snackbar.Snackbar -import com.melih.repository.interactors.base.Reason +import com.melih.abstractions.deliverable.Reason +import com.melih.core.R /** * Parent of all fragments. @@ -21,20 +20,24 @@ import com.melih.repository.interactors.base.Reason */ abstract class BaseFragment : Fragment() { - // region Properties + //region Abstractions + + @LayoutRes + abstract fun getLayoutId(): Int + //endregion + + //region Properties - protected lateinit var navController: NavController protected lateinit var binding: T - // endregion + //endregion - // region Functions + //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 @@ -45,12 +48,9 @@ abstract class BaseFragment : Fragment() { binding.root, resources.getString(reason.messageRes), Snackbar.LENGTH_INDEFINITE - ).setAction(com.melih.core.R.string.retry) { + ).setAction(R.string.retry) { block() }.show() } - - @LayoutRes - abstract fun getLayoutId(): Int - // endregion + //endregion } diff --git a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt index 47b6126..9cd5fa7 100644 --- a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt @@ -4,12 +4,13 @@ import androidx.annotation.CallSuper import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.PageKeyedDataSource -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result -import com.melih.repository.interactors.base.State -import com.melih.repository.interactors.base.onFailure -import com.melih.repository.interactors.base.onState -import com.melih.repository.interactors.base.onSuccess +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.onFailure +import com.melih.abstractions.deliverable.onState +import com.melih.abstractions.deliverable.onSuccess import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,15 +35,15 @@ const val INITIAL_PAGE = 0 * It's cancelled automatically when source factory [invalidates][invalidate] the source. */ -@UseExperimental(ExperimentalCoroutinesApi::class) -abstract class BasePagingDataSource : PageKeyedDataSource() { +@OptIn(ExperimentalCoroutinesApi::class) +abstract class BasePagingDataSource : PageKeyedDataSource() { - // region Abstractions + //region Abstractions - abstract fun loadDataForPage(page: Int): Flow>> // Load next page(s) - // endregion + abstract fun loadDataForPage(page: Int): Flow>> // Load next page(s) + //endregion - // region Properties + //region Properties private val _stateData = MutableLiveData() private val _reasonData = MutableLiveData() @@ -59,11 +60,14 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { */ val reasonData: LiveData get() = _reasonData - // endregion + //endregion - // region Functions + //region Functions - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + override fun loadInitial( + params: LoadInitialParams, + callback: LoadInitialCallback + ) { // Looping through channel as we'll receive any state, error or data here loadDataForPage(INITIAL_PAGE) .onEach { result -> @@ -81,7 +85,7 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { .launchIn(coroutineScope) } - override fun loadAfter(params: LoadParams, callback: LoadCallback) { + override fun loadAfter(params: LoadParams, callback: LoadCallback) { // Key for which page to load is in params val page = params.key @@ -104,7 +108,7 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { /** * This loads previous pages, we don't have a use for it yet, so it's a no-op override */ - override fun loadBefore(params: LoadParams, callback: LoadCallback) { + override fun loadBefore(params: LoadParams, callback: LoadCallback) { // no-op } @@ -136,5 +140,5 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { coroutineScope.cancel() super.invalidate() } - // endregion + //endregion } diff --git a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt index e948600..ddd12b6 100644 --- a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt @@ -3,6 +3,7 @@ package com.melih.core.base.paging import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.DataSource +import com.melih.abstractions.data.ViewEntity /** * Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project. @@ -14,22 +15,22 @@ import androidx.paging.DataSource * * Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource]. */ -abstract class BasePagingFactory : DataSource.Factory() { +abstract class BasePagingFactory : DataSource.Factory() { - // region Abstractions + //region Abstractions abstract fun createSource(): BasePagingDataSource - // endregion + //endregion - // region Properties + //region Properties private val _currentSource = MutableLiveData>() val currentSource: LiveData> get() = _currentSource - // endregion + //endregion - // region Functions + //region Functions override fun create(): DataSource = createSource().apply { _currentSource.postValue(this) } @@ -38,5 +39,5 @@ abstract class BasePagingFactory : DataSource.Factory() { * by calling [BasePagingDataSource.invalidate] */ fun invalidateDataSource() = currentSource.value?.invalidate() - // endregion + //endregion } diff --git a/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt b/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt index cfe44bc..3c9cdb4 100644 --- a/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt +++ b/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt @@ -15,6 +15,8 @@ abstract class BasePagingListAdapter( private val clickListener: (T) -> Unit ) : PagedListAdapter>(callback) { + //region Abstractions + /** * This method will be called to create view holder to obfuscate layout inflation creation / process * @@ -27,6 +29,9 @@ abstract class BasePagingListAdapter( parent: ViewGroup, viewType: Int ): BaseViewHolder + //endregion + + //region Functions /** * [createViewHolder] will provide holders, no need to override this @@ -51,6 +56,7 @@ abstract class BasePagingListAdapter( holder.bind(item) } } + //endregion } /** @@ -58,6 +64,8 @@ abstract class BasePagingListAdapter( */ abstract class BaseViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + //region Functions + /** * Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder] * @@ -65,4 +73,5 @@ abstract class BaseViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHo * @param position position from adapter */ abstract fun bind(item: T) + //endregion } diff --git a/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt b/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt index 57da05b..7a0caad 100644 --- a/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt @@ -1,13 +1,14 @@ package com.melih.core.base.viewmodel import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.Transformations.switchMap import androidx.lifecycle.ViewModel import androidx.paging.PagedList import androidx.paging.toLiveData +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State import com.melih.core.base.paging.BasePagingFactory -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.State /** * Base [ViewModel] for view models that will use [PagedList]. @@ -18,21 +19,21 @@ import com.melih.repository.interactors.base.State * * If paging won't be used, use [BaseViewModel] instead. */ -abstract class BasePagingViewModel : ViewModel() { +abstract class BasePagingViewModel : ViewModel() { - // region Abstractions + //region Abstractions abstract val factory: BasePagingFactory abstract val config: PagedList.Config - // endregion + //endregion - // region Properties + //region Properties /** * Observe [stateData] to get notified of state of data */ val stateData: LiveData by lazy { - Transformations.switchMap(factory.currentSource) { + switchMap(factory.currentSource) { it.stateData } } @@ -41,7 +42,7 @@ abstract class BasePagingViewModel : ViewModel() { * Observe [errorData] to get notified if an error occurs */ val errorData: LiveData by lazy { - Transformations.switchMap(factory.currentSource) { + switchMap(factory.currentSource) { it.reasonData } } @@ -52,9 +53,9 @@ abstract class BasePagingViewModel : ViewModel() { val pagedList: LiveData> by lazy { factory.toLiveData(config) } - // endregion + //endregion - // region Functions + //region Functions fun refresh() { factory.currentSource.value?.invalidate() @@ -66,5 +67,5 @@ abstract class BasePagingViewModel : ViewModel() { fun retry() { factory.currentSource.value?.invalidate() } - // endregion + //endregion } 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 index 2e3246b..4a5be25 100644 --- a/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt @@ -3,10 +3,8 @@ package com.melih.core.base.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.State -import kotlinx.coroutines.launch +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State /** * Base [ViewModel] for view models that will process data. @@ -15,18 +13,7 @@ import kotlinx.coroutines.launch */ abstract class BaseViewModel : ViewModel() { - // region Abstractions - - abstract suspend fun loadData() - // endregion - - init { - viewModelScope.launch { - loadData() - } - } - - // region Properties + //region Properties private val _successData = MutableLiveData() private val _stateData = MutableLiveData() @@ -49,9 +36,9 @@ abstract class BaseViewModel : ViewModel() { */ val errorData: LiveData get() = _errorData - // endregion + //endregion - // region Functions + //region Functions /** * Default success handler which assigns given [data] to [successData] @@ -79,23 +66,5 @@ abstract class BaseViewModel : ViewModel() { protected fun handleFailure(reason: Reason) { _errorData.value = reason } - - /** - * Reload data - */ - fun refresh() { - viewModelScope.launch { - loadData() - } - } - - /** - * Retry loading data, incase there's difference between refresh and retry, should go here - */ - fun retry() { - viewModelScope.launch { - loadData() - } - } - // endregion + //endregion } diff --git a/core/src/main/kotlin/com/melih/core/di/CoreModule.kt b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt index bc8aab2..fe9bbf5 100644 --- a/core/src/main/kotlin/com/melih/core/di/CoreModule.kt +++ b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt @@ -14,6 +14,10 @@ class CoreModule { fun proivdeAppContext(app: Application): Context = app.applicationContext @Provides - fun provideNetworkInfo(app: Application): NetworkInfo? = - (app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo + fun provideNetworkInfo(app: Application): NetworkInfo? { + val connectivityManager = + app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return connectivityManager.activeNetworkInfo + } + } diff --git a/core/src/main/kotlin/com/melih/core/extensions/DiffUtilHelper.kt b/core/src/main/kotlin/com/melih/core/extensions/DiffUtilHelper.kt new file mode 100644 index 0000000..55bca7a --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/extensions/DiffUtilHelper.kt @@ -0,0 +1,27 @@ +package com.melih.core.extensions + +import androidx.recyclerview.widget.DiffUtil + +/** + * Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker. + * It uses [itemCheck] for both [DiffUtil.ItemCallback.areItemsTheSame] and [DiffUtil.ItemCallback.areContentsTheSame]. + */ +inline fun createDiffCallback(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = createDiffCallback(itemCheck, itemCheck) + +/** + * Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker + */ +inline fun createDiffCallback( + crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean, + crossinline contentCheck: (oldItem: T, newItem: T) -> Boolean +) = object : DiffUtil.ItemCallback() { + + //region Functions + + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = + itemCheck(oldItem, newItem) + + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = + contentCheck(oldItem, newItem) + //endregion +} diff --git a/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt b/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt index bf31e90..1396b42 100644 --- a/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt +++ b/core/src/main/kotlin/com/melih/core/extensions/LifecycleExtensions.kt @@ -3,9 +3,6 @@ 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 @@ -16,13 +13,3 @@ import androidx.lifecycle.ViewModelProviders 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 = ViewModelProviders.of(fragment, this)[T::class.java].apply(block) diff --git a/core/src/main/kotlin/com/melih/core/extensions/RecyclerExtensions.kt b/core/src/main/kotlin/com/melih/core/extensions/RecyclerExtensions.kt deleted file mode 100644 index 011fac9..0000000 --- a/core/src/main/kotlin/com/melih/core/extensions/RecyclerExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.melih.core.extensions - -import androidx.recyclerview.widget.DiffUtil - -/** - * Get [diff callback][DiffUtil.ItemCallback] for given type based on provided checker - */ -inline fun getDiffCallbackForType(crossinline itemCheck: (oldItem: T, newItem: T) -> Boolean) = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = - itemCheck(oldItem, newItem) - - override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = - itemCheck(oldItem, newItem) -} diff --git a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt b/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt deleted file mode 100644 index c57840f..0000000 --- a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.melih.core.extensions - -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import com.melih.core.utils.ClearFocusQueryTextListener - -/** - * Shorthand for [contains] with ignoreCase set [true] - */ -fun CharSequence.containsIgnoreCase(other: CharSequence) = contains(other, true) - -/** - * Adds [ClearFocusQueryTextListener] as [SearchView.OnQueryTextListener] - */ -fun SearchView.setOnQueryChangedListener(block: (String?) -> Unit) = setOnQueryTextListener(ClearFocusQueryTextListener(this, block)) - -/** - * Shortening set menu item expands / collapses - */ -fun MenuItem.onExpandOrCollapse(onExpand: () -> Unit, onCollapse: () -> Unit) { - setOnActionExpandListener(object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { - onCollapse() - return true - } - - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { - onExpand() - return true - } - }) -} diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 270aec4..da95615 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -1,7 +1,11 @@ - #008577 - #00574B - #D81B60 - #8F8F8F + #f9a825 + #ffd95a + #c17900 + #757575 + #a4a4a4 + #494949 + #000000 + #686868 diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index ab4df8e..7bf765c 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -1,5 +1,7 @@ 8dp + 16dp + 4dp 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 index 22788f1..9268d45 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,15 +1,12 @@ - - 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 + + Retry - - action.detail.open + ¶mLaunchId; + &deeplinkDetailPath;/{¶mLaunchId;} + &deeplinkDetailPath;/%1d diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index f7ae6d6..1d6d73c 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -1,35 +1,40 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt index d8d68fb..92deea2 100644 --- a/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt +++ b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt @@ -13,7 +13,7 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import kotlin.coroutines.suspendCoroutine -@UseExperimental(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) abstract class BaseTestWithMainThread { private val dispatcher = TestCoroutineDispatcher() diff --git a/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt index b195388..4b527fa 100644 --- a/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt +++ b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt @@ -11,7 +11,7 @@ class BaseViewModelTest : BaseTestWithMainThread() { @Test fun `refresh should invoke loadData`() { val baseVm = spyk(TestViewModel()) - baseVm.refresh() + baseVm.loadData() coVerify(exactly = 1) { baseVm.loadData() } } @@ -19,14 +19,14 @@ class BaseViewModelTest : BaseTestWithMainThread() { @Test fun `retry should invoke loadData`() { val baseVm = spyk(TestViewModel()) - baseVm.retry() + baseVm.loadData() coVerify(exactly = 1) { baseVm.loadData() } } } class TestViewModel : BaseViewModel() { - override suspend fun loadData() { - // no - op + fun loadData() { + } } diff --git a/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt index 5750ad1..11e3515 100644 --- a/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt @@ -1,16 +1,15 @@ -@file:UseExperimental(ExperimentalCoroutinesApi::class) - package com.melih.core.paging import androidx.paging.PageKeyedDataSource +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success import com.melih.core.BaseTestWithMainThread import com.melih.core.base.paging.BasePagingDataSource import com.melih.core.testObserve -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.GenericError -import com.melih.repository.interactors.base.Result -import com.melih.repository.interactors.base.State -import com.melih.repository.interactors.base.Success import io.mockk.mockk import io.mockk.spyk import io.mockk.verify @@ -28,7 +27,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { val failureSource = spyk(TestFailureSource()) val data = 10 - val errorMessage = "Generic Error" + val errorMessageResId = 1313 @Nested inner class BasePagingSource { @@ -37,10 +36,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { inner class LoadInitial { @Test - fun `should update state accordingly`() { val params = mockk>(relaxed = true) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -54,10 +52,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should update error Error accordingly`() { val params = PageKeyedDataSource.LoadInitialParams(10, false) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -65,7 +62,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { failureSource.loadInitial(params, callback) failureSource.reasonData.testObserve { - it shouldBeInstanceOf GenericError::class + it shouldBeInstanceOf TestFailureReason::class } } } @@ -75,10 +72,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { inner class LoadAfter { @Test - fun `should update state accordingly`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -92,10 +88,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should update error Error accordingly`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -103,17 +98,16 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { failureSource.loadAfter(params, callback) failureSource.reasonData.testObserve { - it shouldBeInstanceOf GenericError::class + it shouldBeInstanceOf TestFailureReason::class } } } } @Test - fun `should use loadDataForPage in loadInitial and transform emmited value`() { val params = mockk>(relaxed = true) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) // Fake loading source.loadInitial(params, callback) @@ -126,10 +120,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should use loadDataForPage in loadAfter and transform emmited value`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) // Fake loading source.loadAfter(params, callback) @@ -142,25 +135,27 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } } - inner class TestSource : BasePagingDataSource() { - + inner class TestSource : BasePagingDataSource() { val result = flow { emit(State.Loading()) - emit(Success(listOf(data))) + emit(Success(listOf(TestViewEntity(data)))) } - - override fun loadDataForPage(page: Int): Flow>> = result + override fun loadDataForPage(page: Int): Flow>> = result } - inner class TestFailureSource : BasePagingDataSource() { + inner class TestFailureSource : BasePagingDataSource() { val result = flow { emit(State.Loading()) - emit(Failure(GenericError())) + emit(Failure(TestFailureReason(errorMessageResId))) } - override fun loadDataForPage(page: Int): Flow>> = result + override fun loadDataForPage(page: Int): Flow>> = result } -} \ No newline at end of file + + inner class TestViewEntity(data: Int) : ViewEntity + + inner class TestFailureReason(override val messageRes: Int) : Reason() +} diff --git a/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt index 5829189..cfb6145 100644 --- a/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt @@ -1,5 +1,6 @@ package com.melih.core.paging +import com.melih.abstractions.data.ViewEntity import com.melih.core.BaseTestWithMainThread import com.melih.core.base.paging.BasePagingDataSource import com.melih.core.base.paging.BasePagingFactory @@ -25,9 +26,10 @@ class BasePagingFactoryTest : BaseTestWithMainThread() { } } - inner class TestFactory : BasePagingFactory() { - - override fun createSource(): BasePagingDataSource = mockk(relaxed = true) + inner class TestFactory : BasePagingFactory() { + override fun createSource(): BasePagingDataSource = mockk(relaxed = true) } + + inner class TestViewEntity : ViewEntity } diff --git a/data/definitions/build.gradle b/data/definitions/build.gradle new file mode 100644 index 0000000..b234e17 --- /dev/null +++ b/data/definitions/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':abstractions') + + implementation libraries.room + implementation libraries.moshiKotlin + + kapt annotationProcessors.roomCompiler + kapt annotationProcessors.moshi +} diff --git a/data/definitions/consumer-rules.pro b/data/definitions/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/definitions/proguard-rules.pro b/data/definitions/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/definitions/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/data/definitions/src/main/AndroidManifest.xml b/data/definitions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8363ce0 --- /dev/null +++ b/data/definitions/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt b/data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt new file mode 100644 index 0000000..533f176 --- /dev/null +++ b/data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt @@ -0,0 +1,4 @@ +package com.melih.definitions + +internal const val DEFAULT_NAME = "Default name" +internal const val EMPTY_STRING = "" diff --git a/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt b/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt new file mode 100644 index 0000000..289304f --- /dev/null +++ b/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt @@ -0,0 +1,23 @@ +package com.melih.definitions + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity + +/** + * Contract for sources to seperate business logic from build and return type + */ +interface Source { + + //region Abstractions + + suspend fun getNextLaunches( + count: Int, + page: Int, + mapper: Mapper + ): Result> + + suspend fun getLaunchById(id: Long, mapper: Mapper): Result + //endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt similarity index 87% rename from repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt index 2a7bdbd..43779e8 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.Entity import androidx.room.PrimaryKey -import com.melih.repository.DEFAULT_NAME +import com.melih.definitions.DEFAULT_NAME import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt similarity index 86% rename from repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt index eaa55df..f6f3762 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt @@ -1,4 +1,4 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import com.squareup.moshi.JsonClass diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt similarity index 87% rename from repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt index 652c4d3..af217ef 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt @@ -1,7 +1,7 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME +import com.melih.definitions.DEFAULT_NAME import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt similarity index 73% rename from repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt index 971ec00..a8d1141 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME -import com.melih.repository.EMPTY_STRING +import com.melih.definitions.DEFAULT_NAME +import com.melih.definitions.EMPTY_STRING import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt similarity index 78% rename from repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt index 7fd8c3d..1b3f01e 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME -import com.melih.repository.EMPTY_STRING +import com.melih.definitions.DEFAULT_NAME +import com.melih.definitions.EMPTY_STRING import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/data/interactors/build.gradle b/data/interactors/build.gradle new file mode 100644 index 0000000..b09d7d2 --- /dev/null +++ b/data/interactors/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':data:definitions') + implementation project(':data:network') + implementation project(':data:persistence') + + implementation libraries.coroutines + implementation libraries.retrofit + + testImplementation testLibraries.coroutinesCore + testImplementation testLibraries.coroutinesTest + + compileOnly libraries.room +} diff --git a/data/interactors/consumer-rules.pro b/data/interactors/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/interactors/proguard-rules.pro b/data/interactors/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/interactors/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/data/interactors/src/main/AndroidManifest.xml b/data/interactors/src/main/AndroidManifest.xml new file mode 100644 index 0000000..86a5aa4 --- /dev/null +++ b/data/interactors/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt new file mode 100644 index 0000000..50fef98 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt @@ -0,0 +1,40 @@ +package com.melih.interactors + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.base.BaseInteractor +import com.melih.interactors.base.InteractorParameters +import com.melih.interactors.sources.LaunchesSource +import kotlinx.coroutines.flow.FlowCollector +import javax.inject.Inject + +/** + * Gets next given number of launches + */ +class GetLaunchDetails @Inject constructor( + private val mapper: @JvmSuppressWildcards Mapper +) : BaseInteractor() { + + //region Properties + + @Inject + internal lateinit var launchesSource: LaunchesSource + //endregion + + //region Functions + + override suspend fun FlowCollector>.run(params: Params) { + emit(launchesSource.getLaunchById(params.id, mapper)) + } + //endregion + + + //region Parameters + + data class Params( + val id: Long + ) : InteractorParameters + //endregion +} diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt new file mode 100644 index 0000000..0f3e601 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt @@ -0,0 +1,44 @@ +package com.melih.interactors + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.base.BaseInteractor +import com.melih.interactors.base.InteractorParameters +import com.melih.interactors.sources.LaunchesSource +import kotlinx.coroutines.flow.FlowCollector +import javax.inject.Inject + +const val DEFAULT_LAUNCHES_AMOUNT = 15 + +/** + * Gets next given number of launches + */ +class GetLaunches @Inject constructor( + private val mapper: @JvmSuppressWildcards Mapper +) : BaseInteractor, GetLaunches.Params>() { + + //region Properties + + @Inject + internal lateinit var launchesSource: LaunchesSource + //endregion + + //region Functions + + override suspend fun FlowCollector>>.run(params: Params) { + + // Start network fetch - we're not handling state here to ommit them + emit( + launchesSource + .getNextLaunches(params.count, params.page, mapper) + ) + } + //endregion + + data class Params( + val count: Int = DEFAULT_LAUNCHES_AMOUNT, + val page: Int + ) : InteractorParameters +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt b/data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt similarity index 74% rename from repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt rename to data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt index e431a2d..d8a7a85 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt +++ b/data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt @@ -1,5 +1,7 @@ -package com.melih.repository.interactors.base +package com.melih.interactors.base +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -10,15 +12,15 @@ import kotlinx.coroutines.flow.flowOn /** * Base use case that wraps [suspending][suspend] [run] function with [flow][Flow] and returns it for later usage. */ -@UseExperimental(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) abstract class BaseInteractor { - // region Abstractions + //region Abstractions protected abstract suspend fun FlowCollector>.run(params: P) - // endregion + //endregion - // region Functions + //region Functions operator fun invoke(params: P) = flow> { @@ -26,7 +28,7 @@ abstract class BaseInteractor { run(params) emit(State.Loaded()) }.flowOn(Dispatchers.IO) - // endregion + //endregion } /** @@ -37,4 +39,4 @@ interface InteractorParameters /** * Symbolizes absence of parameters for an [interactor][BaseInteractor] */ -class None : Any(), InteractorParameters +class None : InteractorParameters diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt b/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt new file mode 100644 index 0000000..3242f88 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt @@ -0,0 +1,17 @@ +package com.melih.interactors.error + +import androidx.annotation.StringRes +import com.melih.abstractions.deliverable.Reason +import com.melih.interactors.R + +sealed class InteractionErrorReason(@StringRes override val messageRes: Int) : Reason() + +class GenericError(@StringRes override val messageRes: Int = R.string.reason_generic) : InteractionErrorReason(messageRes) + +sealed class NetworkError(override val messageRes: Int) : InteractionErrorReason(messageRes) +class ConnectionError : NetworkError(R.string.reason_network) +class EmptyResultError : NetworkError(R.string.reason_empty_body) +class ResponseError : NetworkError(R.string.reason_response) +class TimeoutError : NetworkError(R.string.reason_timeout) + +class PersistenceEmptyError : InteractionErrorReason(R.string.reason_persistance_empty) diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt b/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt new file mode 100644 index 0000000..7abce3c --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt @@ -0,0 +1,160 @@ +package com.melih.interactors.sources + +import android.content.Context +import android.net.NetworkInfo +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.Success +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.Source +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.interactors.error.ConnectionError +import com.melih.interactors.error.EmptyResultError +import com.melih.interactors.error.NetworkError +import com.melih.interactors.error.PersistenceEmptyError +import com.melih.interactors.error.ResponseError +import com.melih.interactors.error.TimeoutError +import com.melih.network.ApiImpl +import com.melih.persistence.LaunchesDatabase +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject +import javax.inject.Provider + +private const val DEFAULT_IMAGE_SIZE = 480 + +internal class LaunchesSource @Inject constructor( + ctx: Context, + private val apiImpl: ApiImpl, + private val networkInfoProvider: Provider +) : Source { + + //region Properties + + private val launchesDatabase = LaunchesDatabase.getInstance(ctx) + + private val isNetworkConnected: Boolean + get() { + val networkInfo = networkInfoProvider.get() + return networkInfo != null && networkInfo.isConnected + } + //endregion + + //region Functions + + override suspend fun getNextLaunches( + count: Int, + page: Int, mapper: Mapper + ): Result> { + val networkResponse = safeExecute({ + apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT) + }) { entity -> + entity.launches + .map(::transformRocketImageUrl) + .saveLaunches() + .map(mapper::convert) + } + + return if (networkResponse is NetworkError) { + launchesDatabase + .launchesDao + .getLaunches(count, page) + .takeUnless { it.isNullOrEmpty() } + ?.run { + Success(map(mapper::convert)) + } ?: Failure(PersistenceEmptyError()) + } else { + networkResponse + } + } + + override suspend fun getLaunchById( + id: Long, + mapper: Mapper + ): Result { + return launchesDatabase + .launchesDao + .getLaunchById(id) + .takeIf { it != null } + ?.run { + Success(mapper.convert(this)) + } ?: loadLaunchFromNetwork(id, mapper) + } + + private suspend fun loadLaunchFromNetwork( + id: Long, + mapper: Mapper + ): Result = + safeExecute({ + apiImpl.getLaunchById(id) + }) { + mapper.convert( + transformRocketImageUrl(it) + .saveLaunch() + ) + } + + private suspend fun List.saveLaunches() = run { + launchesDatabase.launchesDao.saveLaunches(this) + this + } + + private suspend fun LaunchEntity.saveLaunch() = run { + launchesDatabase.launchesDao.saveLaunch(this) + this + } + + private inline fun safeExecute( + block: () -> Response, + transform: (T) -> R + ) = + if (isNetworkConnected) { + try { + block().extractResponseBody(transform) + } catch (e: IOException) { + Failure(TimeoutError()) + } + } else { + Failure(ConnectionError()) + } + + private inline fun Response.extractResponseBody(transform: (T) -> R) = + if (isSuccessful) { + body()?.let { + Success(transform(it)) + } ?: Failure(EmptyResultError()) + } else { + Failure(ResponseError()) + } + + private fun transformRocketImageUrl(launch: LaunchEntity) = + if (!launch.rocket.imageURL.isNotBlank()) { + launch.copy( + rocket = launch.rocket.copy( + imageURL = transformImageUrl( + launch.rocket.imageURL, + launch.rocket.imageSizes + ) + ) + ) + } else { + launch + } + + private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray): String { + val urlSplit = imageUrl.split("_") + val url = urlSplit[0] + val format = urlSplit[1].split(".")[1] + + val requestedSize = if (!supportedSizes.contains(DEFAULT_IMAGE_SIZE)) { + supportedSizes.last { it < DEFAULT_IMAGE_SIZE } + } else { + DEFAULT_IMAGE_SIZE + } + + return "${url}_$requestedSize.$format" + } + //endregion +} diff --git a/repository/src/main/res/values/strings.xml b/data/interactors/src/main/res/values/strings.xml similarity index 81% rename from repository/src/main/res/values/strings.xml rename to data/interactors/src/main/res/values/strings.xml index 92cdbc9..13e42df 100644 --- a/repository/src/main/res/values/strings.xml +++ b/data/interactors/src/main/res/values/strings.xml @@ -1,9 +1,8 @@ + Something went wrong + There are no saved launches 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/data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt similarity index 90% rename from repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt rename to data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt index 35d922c..0799822 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt +++ b/data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt @@ -1,5 +1,8 @@ -package com.melih.repository.interactors.base +package com.melih.interactors.base +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success import io.mockk.coVerify import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -10,9 +13,8 @@ import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeInstanceOf import org.amshove.kluent.shouldEqualTo import org.junit.jupiter.api.Test -import java.util.* +import java.util.ArrayDeque -@UseExperimental(ExperimentalCoroutinesApi::class) class BaseInteractorTest { val testInteractor = spyk(TestInteractor()) diff --git a/data/network/build.gradle b/data/network/build.gradle new file mode 100644 index 0000000..cc02d5c --- /dev/null +++ b/data/network/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':data:definitions') + + implementation libraries.okHttpLogger + implementation libraries.moshiKotlin + implementation libraries.coroutines + implementation libraries.retrofit + + testImplementation testLibraries.coroutinesCore + testImplementation testLibraries.coroutinesTest +} diff --git a/data/network/consumer-rules.pro b/data/network/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/network/proguard-rules.pro b/data/network/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/network/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/data/network/src/main/AndroidManifest.xml b/data/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..32d8ca6 --- /dev/null +++ b/data/network/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/repository/src/main/kotlin/com/melih/repository/network/Api.kt b/data/network/src/main/kotlin/com/melih/network/api/Api.kt similarity index 73% rename from repository/src/main/kotlin/com/melih/repository/network/Api.kt rename to data/network/src/main/kotlin/com/melih/network/api/Api.kt index 0b410d8..bef517d 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/Api.kt +++ b/data/network/src/main/kotlin/com/melih/network/api/Api.kt @@ -1,7 +1,7 @@ -package com.melih.repository.network +package com.melih.network -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.entities.LaunchesEntity +import com.melih.definitions.entities.LaunchEntity +import com.melih.definitions.entities.LaunchesEntity import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -12,6 +12,8 @@ import retrofit2.http.Query */ internal interface Api { + //region Get + @GET("launch/next/{count}") suspend fun getNextLaunches( @Path("count") count: Int, @@ -22,4 +24,5 @@ internal interface Api { suspend fun getLaunchById( @Path("id") id: Long ): Response + //endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt b/data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt similarity index 85% rename from repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt rename to data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt index 85ca556..ff195c6 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt +++ b/data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt @@ -1,7 +1,7 @@ -package com.melih.repository.network +package com.melih.network -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.entities.LaunchesEntity +import com.melih.definitions.entities.LaunchEntity +import com.melih.definitions.entities.LaunchesEntity import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.OkHttpClient @@ -14,9 +14,9 @@ import javax.inject.Inject internal const val TIMEOUT_DURATION = 7L -internal class ApiImpl @Inject constructor() : Api { +class ApiImpl @Inject constructor() : Api { - // region Properties + //region Properties private val service by lazy { val moshi = Moshi.Builder() @@ -39,7 +39,9 @@ internal class ApiImpl @Inject constructor() : Api { .build() .create(Api::class.java) } - // endregion + //endregion + + //region Functions override suspend fun getNextLaunches( count: Int, @@ -51,4 +53,5 @@ internal class ApiImpl @Inject constructor() : Api { id: Long ): Response = service.getLaunchById(id) + //endregion } diff --git a/repository/build.gradle b/data/persistence/build.gradle similarity index 82% rename from repository/build.gradle rename to data/persistence/build.gradle index 843f46d..fcc536e 100644 --- a/repository/build.gradle +++ b/data/persistence/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" android { defaultConfig { @@ -17,15 +18,13 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':data:definitions') + + implementation libraries.moshiKotlin implementation libraries.coroutines - implementation libraries.liveDataKTX - implementation libraries.retrofit implementation libraries.room - implementation libraries.moshiKotlin - implementation libraries.okHttpLogger kapt annotationProcessors.roomCompiler - kapt annotationProcessors.moshi testImplementation testLibraries.coroutinesCore testImplementation testLibraries.coroutinesTest diff --git a/data/persistence/consumer-rules.pro b/data/persistence/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/persistence/proguard-rules.pro b/data/persistence/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/persistence/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/data/persistence/src/main/AndroidManifest.xml b/data/persistence/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d3d76b --- /dev/null +++ b/data/persistence/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt b/data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt similarity index 62% rename from repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt index 4ab8fb7..da7a83c 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt @@ -1,15 +1,15 @@ -package com.melih.repository.persistence +package com.melih.persistence import android.content.Context import androidx.room.Database import androidx.room.Room 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 +import com.melih.definitions.entities.LaunchEntity +import com.melih.persistence.converters.LocationConverter +import com.melih.persistence.converters.MissionConverter +import com.melih.persistence.converters.RocketConverter +import com.melih.persistence.dao.LaunchesDao const val DB_NAME = "LaunchesDB" @@ -26,7 +26,9 @@ const val DB_NAME = "LaunchesDB" RocketConverter::class, MissionConverter::class ) -internal abstract class LaunchesDatabase : RoomDatabase() { +abstract class LaunchesDatabase : RoomDatabase() { + + //region Companion companion object { @@ -42,6 +44,10 @@ internal abstract class LaunchesDatabase : RoomDatabase() { } } + //endregion + + //region Abstractions - internal abstract val launchesDao: LaunchesDao + abstract val launchesDao: LaunchesDao + //endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt similarity index 78% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt index 735d615..a0bb43f 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt @@ -1,4 +1,4 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters import androidx.room.TypeConverter import com.squareup.moshi.JsonAdapter @@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory */ abstract class BaseConverter { + //region Abstractions + + abstract fun getAdapter(moshi: Moshi): JsonAdapter + //endregion + + //region Properties + private val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() + //endregion - abstract fun getAdapter(moshi: Moshi): JsonAdapter - - // region Functions + //region Functions @TypeConverter fun convertFrom(item: T) = @@ -25,5 +31,5 @@ abstract class BaseConverter { @TypeConverter fun convertTo(string: String) = getAdapter(moshi).fromJson(string) - // endregion + //endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt similarity index 79% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt index f304c0a..df5fe73 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt @@ -1,4 +1,4 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters import androidx.room.TypeConverter import com.squareup.moshi.JsonAdapter @@ -10,13 +10,19 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory */ abstract class BaseListConverter { + //region Abstractions + + abstract fun getAdapter(moshi: Moshi): JsonAdapter> + //endregion + + //region Properties + private val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() + //endregion - abstract fun getAdapter(moshi: Moshi): JsonAdapter> - - // region Functions + //region Functions @TypeConverter fun convertFrom(items: List) = @@ -25,5 +31,5 @@ abstract class BaseListConverter { @TypeConverter fun convertTo(string: String): List? = getAdapter(moshi).fromJson(string) - // endregion + //endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt similarity index 63% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt index 6742ecb..a4d375d 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt @@ -1,7 +1,7 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.LocationEntity -import com.melih.repository.entities.LocationEntityJsonAdapter +import com.melih.definitions.entities.LocationEntity +import com.melih.definitions.entities.LocationEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi * Converts [location][LocationEntity] */ class LocationConverter : BaseConverter() { + override fun getAdapter(moshi: Moshi): JsonAdapter = LocationEntityJsonAdapter(moshi) } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt similarity index 81% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt index 1d25e9a..6a524e4 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt @@ -1,6 +1,6 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.MissionEntity +import com.melih.definitions.entities.MissionEntity import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt similarity index 54% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt index de2e3b8..d98102b 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt @@ -1,7 +1,7 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.RocketEntity -import com.melih.repository.entities.RocketEntityJsonAdapter +import com.melih.definitions.entities.RocketEntity +import com.melih.definitions.entities.RocketEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,6 +9,7 @@ import com.squareup.moshi.Moshi * Converts [rocket][RocketEntity] */ class RocketConverter : BaseConverter() { + override fun getAdapter(moshi: Moshi): JsonAdapter = - RocketEntityJsonAdapter(moshi) + RocketEntityJsonAdapter(moshi) } diff --git a/data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt b/data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt new file mode 100644 index 0000000..36f366b --- /dev/null +++ b/data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt @@ -0,0 +1,35 @@ +package com.melih.persistence.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.melih.definitions.entities.LaunchEntity + +/** + * DAO for list of [launches][LaunchEntity] + */ +@Dao +interface LaunchesDao { + + //region Queries + + @Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count") + suspend fun getLaunches(count: Int, page: Int): List + + @Query("SELECT * FROM Launches WHERE id=:id LIMIT 1") + suspend fun getLaunchById(id: Long): LaunchEntity? + + @Query("DELETE FROM Launches") + suspend fun nukeLaunches() + //endregion + + //region Insertion + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveLaunches(launches: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveLaunch(launch: LaunchEntity) + //endregion +} diff --git a/default-detekt-config.yml b/default-detekt-config.yml index 326fe3c..738f638 100644 --- a/default-detekt-config.yml +++ b/default-detekt-config.yml @@ -1,40 +1,21 @@ -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 + excludeCorrectable: false weights: -# complexity: 1 -# LongParameterList: 1 -# style: 1 -# comments: 0 + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' processors: active: true exclude: + - 'DetektProgressListener' # - 'FunctionCountProcessor' # - 'PropertyCountProcessor' # - 'ClassCountProcessor' @@ -44,21 +25,25 @@ processors: console-reports: active: true exclude: - # - 'ProjectStatisticsReport' - # - 'ComplexityReport' - # - 'NotificationReport' - # - 'FindingsReport' - # - 'BuildFailureReport' + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + # - 'FindingsReport' + - 'FileBasedFindingsReport' comments: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false EndOfSentenceFormat: active: false - endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' UndocumentedPublicClass: active: false searchInNestedClass: true @@ -67,6 +52,8 @@ comments: searchInInnerInterface: true UndocumentedPublicFunction: active: false + UndocumentedPublicProperty: + active: false complexity: active: true @@ -77,14 +64,17 @@ complexity: active: false threshold: 10 includeStaticDeclarations: false + includePrivateDeclarations: false ComplexMethod: active: true - threshold: 10 + threshold: 15 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: run,let,apply,with,also,use,forEach,isNotNull,ifNull LabeledExpression: active: false - ignoredLabels: "" + ignoredLabels: '' LargeClass: active: true threshold: 600 @@ -93,8 +83,10 @@ complexity: threshold: 60 LongParameterList: active: true - threshold: 6 + functionThreshold: 6 + constructorThreshold: 7 ignoreDefaultParameters: false + ignoreDataClasses: true MethodOverloading: active: false threshold: 6 @@ -103,12 +95,14 @@ complexity: threshold: 4 StringLiteralDuplication: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 @@ -118,11 +112,18 @@ complexity: ignorePrivate: false ignoreOverridden: false +coroutines: + active: true + GlobalCoroutineUsage: + active: false + RedundantSuspendModifier: + active: false + empty-blocks: active: true EmptyCatchBlock: active: true - allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + allowedExceptionNameRegex: '^(_|(ignore|expected).*)' EmptyClassBlock: active: true EmptyDefaultConstructor: @@ -137,7 +138,7 @@ empty-blocks: active: true EmptyFunctionBlock: active: true - ignoreOverriddenFunctions: false + ignoreOverridden: false EmptyIfBlock: active: true EmptyInitBlock: @@ -146,6 +147,8 @@ empty-blocks: active: true EmptySecondaryConstructor: active: true + EmptyTryBlock: + active: true EmptyWhenBlock: active: true EmptyWhileBlock: @@ -158,6 +161,7 @@ exceptions: methodNames: 'toString,hashCode,equals,finalize' InstanceOfCheckForException: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' NotImplementedDeclaration: active: false PrintStackTrace: @@ -166,9 +170,11 @@ exceptions: active: false ReturnFromFinally: active: false + ignoreLabeled: false SwallowedException: active: false ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException' + allowedExceptionNameRegex: '^(_|(ignore|expected).*)' ThrowingExceptionFromFinally: active: false ThrowingExceptionInMain: @@ -179,53 +185,65 @@ exceptions: ThrowingNewInstanceOfSameException: active: false TooGenericExceptionCaught: - active: false + active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' exceptionNames: - - ArrayIndexOutOfBoundsException - - Error - - Exception - - IllegalMonitorStateException - - NullPointerException - - IndexOutOfBoundsException - - RuntimeException - - Throwable - allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: '^(_|(ignore|expected).*)' TooGenericExceptionThrown: active: true exceptionNames: - - Error - - Exception - - Throwable - - RuntimeException + - Error + - Exception + - Throwable + - RuntimeException formatting: active: true android: false autoCorrect: true + AnnotationOnSeparateLine: + active: false + autoCorrect: true ChainWrapping: active: true autoCorrect: true CommentSpacing: active: true autoCorrect: true + EnumEntryNameCase: + active: false + autoCorrect: true Filename: active: true FinalNewline: active: true autoCorrect: true + insertFinalNewLine: true ImportOrdering: active: false + autoCorrect: true Indentation: - active: true + active: false autoCorrect: true indentSize: 4 continuationIndentSize: 4 MaximumLineLength: active: true - maxLineLength: 150 + maxLineLength: 173 ModifierOrdering: active: true autoCorrect: true + MultiLineIfElse: + active: true + autoCorrect: true NoBlankLineBeforeRbrace: active: true autoCorrect: true @@ -235,8 +253,9 @@ formatting: NoEmptyClassBody: active: true autoCorrect: true - NoItParamInMultilineLambda: + NoEmptyFirstLineInMethodBlock: active: false + autoCorrect: true NoLineBreakAfterElse: active: true autoCorrect: true @@ -260,7 +279,6 @@ formatting: autoCorrect: true NoWildcardImports: active: true - autoCorrect: true PackageName: active: true autoCorrect: true @@ -277,6 +295,9 @@ formatting: SpacingAroundCurly: active: true autoCorrect: true + SpacingAroundDot: + active: true + autoCorrect: true SpacingAroundKeyword: active: true autoCorrect: true @@ -297,60 +318,79 @@ naming: active: true ClassNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' classPattern: '[A-Z$][a-zA-Z0-9$]*' ConstructorParameterNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' + ignoreOverridden: true EnumNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' forbiddenName: '' FunctionMaxLength: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' maximumFunctionNameLength: 30 FunctionMinLength: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' minimumFunctionNameLength: 3 FunctionNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' excludeClassPattern: '$^' ignoreOverridden: true FunctionParameterNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverriddenFunctions: true + ignoreOverridden: true + InvalidPackageDeclaration: + active: false + rootPackage: '' MatchingDeclarationName: active: true + mustBeFirst: true MemberNameEqualsClassName: - active: false - ignoreOverriddenFunction: true + active: true + ignoreOverridden: true ObjectPropertyNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' 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 + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' TopLevelPropertyNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' maximumVariableNameLength: 64 VariableMinLength: active: false + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' minimumVariableNameLength: 1 VariableNaming: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' @@ -359,46 +399,61 @@ naming: performance: active: true ArrayPrimitive: - active: false + active: true ForEachOnRange: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' SpreadOperator: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true + Deprecation: + active: false DuplicateCaseInWhenExpression: active: true EqualsAlwaysReturnsTrueOrFalse: - active: false + active: true EqualsWithHashCodeExist: active: true ExplicitGarbageCollectionCall: active: true - InvalidRange: + HasPlatformType: active: false - IteratorHasNextCallsNextMethod: + ImplicitDefaultLocale: active: false + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true IteratorNotThrowingNoSuchElementException: - active: false + active: true LateinitUsage: active: false - excludeAnnotatedProperties: "" - ignoreOnClassesPattern: "" + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' + excludeAnnotatedProperties: '' + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: false + MissingWhenCase: + active: true + RedundantElseInWhen: + active: true UnconditionalJumpStatementInLoop: active: false UnreachableCode: active: true UnsafeCallOnNullableType: - active: false + active: true UnsafeCast: active: false UselessPostfixExpression: active: false WrongEqualsTypeParameter: - active: false + active: true style: active: true @@ -407,10 +462,14 @@ style: DataClassContainsFunctions: active: false conversionFunctionPrefix: 'to' - EqualsNullCall: + DataClassShouldBeImmutable: active: false + EqualsNullCall: + active: true EqualsOnSignatureLine: active: false + ExplicitCollectionElementAccessMethod: + active: false ExplicitItLambdaParameter: active: false ExpressionBodySyntax: @@ -419,38 +478,54 @@ style: ForbiddenComment: active: true values: 'TODO:,FIXME:,STOPSHIP:' + allowedPatterns: '' ForbiddenImport: active: false - imports: '' + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: '' + ForbiddenPublicDataClass: + active: false + ignorePackages: '*.internal,*.internal.*' ForbiddenVoid: active: false + ignoreOverridden: false + ignoreUsageInGenerics: false FunctionOnlyReturningConstant: - active: false + active: true ignoreOverridableFunction: true excludedFunctions: 'describeContents' + excludeAnnotatedFunction: 'dagger.Provides' + LibraryCodeMustSpecifyReturnType: + active: true LoopWithTooManyJumpStatements: - active: false + active: true maxJumpCount: 1 MagicNumber: active: true + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' ignoreNumbers: '-1,0,1,2' ignoreHashCodeFunction: true ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false ignoreConstantDeclaration: true ignoreCompanionObjectPropertyDeclaration: true ignoreAnnotation: false ignoreNamedArgument: true ignoreEnums: false + ignoreRanges: false MandatoryBracesIfStatements: active: false MaxLineLength: active: true - maxLineLength: 150 + maxLineLength: 173 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: false MayBeConst: - active: false + active: true ModifierOrder: active: true NestedClassesVisibility: @@ -468,15 +543,18 @@ style: PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: active: false RedundantVisibilityModifierRule: active: false ReturnCount: active: true max: 2 - excludedFunctions: "equals" + excludedFunctions: 'equals' excludeLabeled: false excludeReturnFromLambda: true + excludeGuardClauses: false SafeCast: active: true SerialVersionUIDInSerializableClass: @@ -492,12 +570,14 @@ style: active: false acceptableDecimalLength: 5 UnnecessaryAbstractClass: + active: true + excludeAnnotatedClasses: 'dagger.Module' + UnnecessaryAnnotationUseSiteTarget: active: false - excludeAnnotatedClasses: "dagger.Module" UnnecessaryApply: active: false UnnecessaryInheritance: - active: false + active: true UnnecessaryLet: active: false UnnecessaryParentheses: @@ -507,17 +587,29 @@ style: UnusedImports: active: false UnusedPrivateClass: - active: false + active: true UnusedPrivateMember: active: false - allowedNames: "(_|ignored|expected|serialVersionUID)" + allowedNames: '(_|ignored|expected|serialVersionUID)' + UseArrayLiteralsInAnnotations: + active: false + UseCheckOrError: + active: false UseDataClass: active: false - excludeAnnotatedClasses: "" - UtilityClassWithPublicConstructor: + excludeAnnotatedClasses: '' + allowVars: false + UseIfInsteadOfWhen: + active: false + UseRequire: active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true VarCouldBeVal: active: false WildcardImport: active: true - excludeImports: 'java.util.*,kotlinx.android.synthetic.*' + excludes: '**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt' + excludeImports: 'java.util.*,kotlinx.android.synthetic.*' \ No newline at end of file diff --git a/docs/module_graph.png b/docs/module_graph.png index 93bab79..eab7666 100644 Binary files a/docs/module_graph.png and b/docs/module_graph.png differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d30e454..43c06ce 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -30,9 +30,49 @@ platform :android do run_detekt() end - desc "Runs all tests in all modules" - lane :test_all do - run_all_tests() + desc "Runs tests in app module" + lane :test_app do + run_app_tests() + end + + desc "Runs tests in core module" + lane :test_core do + run_core_tests() + end + + desc "Runs tests in launches module" + lane :test_launches do + run_launches_tests() + end + + desc "Runs tests in detail module" + lane :test_detail do + run_detail_tests() + end + + desc "Runs tests in abstraction module" + lane :test_abstractions do + run_abstractions_tests() + end + + desc "Runs tests in definitions module" + lane :test_definitions do + run_definitions_tests() + end + + desc "Runs tests in interactors module" + lane :test_interactors do + run_interactors_tests() + end + + desc "Runs tests in network module" + lane :test_network do + run_network_tests() + end + + desc "Runs tests in persistence module" + lane :test_persistence do + run_persistence_tests() end # ================ Gradle tasks ================ @@ -45,7 +85,39 @@ platform :android do gradle(task: "removeReports") end - def run_all_tests - gradle(task: "clean test --continue") + def run_app_tests + gradle(task: "app:test --continue") + end + + def run_core_tests + gradle(task: "core:test --continue") + end + + def run_launches_tests + gradle(task: "features:launches:test --continue") + end + + def run_detail_tests + gradle(task: "features:detail:test --continue") + end + + def run_abstractions_tests + gradle(task: "abstractions:test --continue") + end + + def run_definitions_tests + gradle(task: "data:definitions:test --continue") + end + + def run_interactors_tests + gradle(task: "data:interactors:test --continue") + end + + def run_network_tests + gradle(task: "data:network:test --continue") + end + + def run_persistence_tests + gradle(task: "data:persistence:test --continue") end end diff --git a/features/detail/build.gradle b/features/detail/build.gradle index 0b93486..cea8317 100644 --- a/features/detail/build.gradle +++ b/features/detail/build.gradle @@ -1,7 +1,6 @@ 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" diff --git a/features/detail/jacoco.exec b/features/detail/jacoco.exec deleted file mode 100644 index b1922d8..0000000 Binary files a/features/detail/jacoco.exec and /dev/null differ diff --git a/features/detail/src/main/AndroidManifest.xml b/features/detail/src/main/AndroidManifest.xml index e7838c8..748078e 100644 --- a/features/detail/src/main/AndroidManifest.xml +++ b/features/detail/src/main/AndroidManifest.xml @@ -1,14 +1 @@ - - - - - - - - - - - + diff --git a/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt new file mode 100644 index 0000000..e6676f4 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt @@ -0,0 +1,10 @@ +package com.melih.launches.data + +import com.melih.abstractions.data.ViewEntity + +data class LaunchDetailItem( + val id: Long, + val imageUrl: String, + val rocketName: String, + val missionDescription: String +) : ViewEntity diff --git a/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt new file mode 100644 index 0000000..363e16d --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt @@ -0,0 +1,18 @@ +package com.melih.launches.data + +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import javax.inject.Inject + +class LaunchDetailMapper @Inject constructor() : Mapper { + + override fun convert(launchEntity: LaunchEntity) = + with(launchEntity) { + LaunchDetailItem( + id, + rocket.imageURL, + rocket.name, + if (!missions.isNullOrEmpty()) missions[0].description else "" + ) + } +} 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 index f97d26a..ad1b7fa 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt @@ -1,8 +1,8 @@ package com.melih.detail.di import com.melih.detail.di.modules.DetailFragmentModule +import com.melih.detail.di.scopes.DetailFragmentScope import com.melih.detail.ui.DetailFragment -import com.melih.list.di.scopes.DetailFragmentScope import dagger.Module import dagger.android.ContributesAndroidInjector @@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector @Module abstract class DetailContributor { - // region Contributes + //region Contributes @ContributesAndroidInjector( modules = [DetailFragmentModule::class] ) @DetailFragmentScope abstract fun detailFragment(): DetailFragment - // endregion + //endregion } diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/DetailFeatureModule.kt b/features/detail/src/main/kotlin/com/melih/detail/di/DetailFeatureModule.kt deleted file mode 100644 index 86b41bf..0000000 --- a/features/detail/src/main/kotlin/com/melih/detail/di/DetailFeatureModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.melih.detail.di - -import com.melih.detail.ui.DetailActivity -import com.melih.list.di.scopes.DetailScope -import dagger.Module -import dagger.android.ContributesAndroidInjector - -/** - * Contributes fragments & view models in this module - */ -@Module -abstract class DetailFeatureModule { - - // region Contributes - - @ContributesAndroidInjector( - modules = [ - DetailContributor::class - ] - ) - @DetailScope - abstract fun detailActivity(): DetailActivity - // endregion -} diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt index 314e70e..946e852 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt @@ -2,11 +2,15 @@ package com.melih.detail.di.modules import androidx.lifecycle.ViewModel import androidx.navigation.fragment.navArgs +import com.melih.abstractions.mapper.Mapper import com.melih.core.di.keys.ViewModelKey +import com.melih.definitions.entities.LaunchEntity import com.melih.detail.ui.DetailFragment import com.melih.detail.ui.DetailFragmentArgs import com.melih.detail.ui.DetailViewModel -import com.melih.repository.interactors.GetLaunchDetails +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem +import com.melih.launches.data.LaunchDetailMapper import dagger.Binds import dagger.Module import dagger.Provides @@ -15,13 +19,16 @@ import dagger.multibindings.IntoMap @Module abstract class DetailFragmentModule { - // region ViewModels + //region ViewModels @Binds @IntoMap @ViewModelKey(DetailViewModel::class) abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel - // endregion + + @Binds + abstract fun detailMapper(mapper: LaunchDetailMapper): Mapper + //endregion @Module companion object { @@ -32,7 +39,7 @@ abstract class DetailFragmentModule { @Provides @JvmStatic fun provideGetLaunchDetailParams(fragment: DetailFragment): GetLaunchDetails.Params { - val args: DetailFragmentArgs by fragment.navArgs() + val args by fragment.navArgs() return GetLaunchDetails.Params(args.launchId) } } diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt index c5a983b..61e38e2 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt @@ -1,4 +1,4 @@ -package com.melih.list.di.scopes +package com.melih.detail.di.scopes import javax.inject.Scope diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt index e264fde..93d96b0 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt @@ -1,4 +1,4 @@ -package com.melih.list.di.scopes +package com.melih.detail.di.scopes import javax.inject.Scope 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 deleted file mode 100644 index 0e7fbae..0000000 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt +++ /dev/null @@ -1,37 +0,0 @@ -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 - -const val INVALID_LAUNCH_ID = -1L - -class DetailActivity : BaseActivity() { - - // region Functions - - 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 index b9a9ca5..db6a8a3 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt @@ -3,21 +3,20 @@ package com.melih.detail.ui import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.view.View +import androidx.fragment.app.viewModels 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 class DetailFragment : BaseDaggerFragment() { - // region Properties + //region Properties - private val viewModel: DetailViewModel - get() = viewModelFactory.createFor(this) - // endregion + private val viewModel by viewModels { viewModelFactory } + //endregion - // region Functions + //region Functions override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -28,11 +27,11 @@ class DetailFragment : BaseDaggerFragment() { // Observing error to show toast with retry action observe(viewModel.errorData) { showSnackbarWithAction(it) { - viewModel.retry() + viewModel.loadData() } } } override fun getLayoutId(): Int = R.layout.fragment_detail - // endregion + //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 index 83c28dd..14b2f41 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt @@ -1,46 +1,52 @@ package com.melih.detail.ui -import androidx.lifecycle.Transformations +import androidx.lifecycle.Transformations.map +import androidx.lifecycle.viewModelScope +import com.melih.abstractions.deliverable.handle import com.melih.core.base.viewmodel.BaseViewModel -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.GetLaunchDetails -import com.melih.repository.interactors.base.handle +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import javax.inject.Inject class DetailViewModel @Inject constructor( - private val getLaunchDetails: GetLaunchDetails, + private val getLaunchDetails: GetLaunchDetails, private val getLaunchDetailsParams: GetLaunchDetails.Params -) : BaseViewModel() { +) : BaseViewModel() { - // region Properties + //region Properties - val rocketName = Transformations.map(successData) { - it.rocket.name + val rocketName by lazy { + val nameData = map(successData) { + it.rocketName + } + + loadData() + + nameData } - val description = Transformations.map(successData) { - if (it.missions.isEmpty()) { - "" - } else { - it.missions[0].description - } + val description = map(successData) { + it.missionDescription } - val imageUrl = Transformations.map(successData) { - it.rocket.imageURL + val imageUrl = map(successData) { + it.imageUrl } - // endregion + //endregion - // region Functions + //region Functions /** * Triggering interactor in view model scope */ - override suspend fun loadData() { - getLaunchDetails(getLaunchDetailsParams).collect { - it.handle(::handleState, ::handleFailure, ::handleSuccess) + fun loadData() { + viewModelScope.launch { + getLaunchDetails(getLaunchDetailsParams).collect { + it.handle(::handleState, ::handleFailure, ::handleSuccess) + } } } - // endregion + //endregion } diff --git a/features/detail/src/main/res/layout/activity_detail.xml b/features/detail/src/main/res/layout/activity_detail.xml deleted file mode 100644 index 89c60f2..0000000 --- a/features/detail/src/main/res/layout/activity_detail.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - diff --git a/features/detail/src/main/res/layout/fragment_detail.xml b/features/detail/src/main/res/layout/fragment_detail.xml index abf4e90..e58cded 100644 --- a/features/detail/src/main/res/layout/fragment_detail.xml +++ b/features/detail/src/main/res/layout/fragment_detail.xml @@ -11,65 +11,59 @@ type="com.melih.detail.ui.DetailViewModel" /> - - + - + - - - - - + + diff --git a/features/detail/src/main/res/navigation/nav_detail.xml b/features/detail/src/main/res/navigation/nav_detail.xml index 2f2f844..cbab98f 100644 --- a/features/detail/src/main/res/navigation/nav_detail.xml +++ b/features/detail/src/main/res/navigation/nav_detail.xml @@ -1,18 +1,22 @@ - + - - - + + + + diff --git a/features/detail/src/main/res/values/strings.xml b/features/detail/src/main/res/values/strings.xml index c9dc1ae..f4f6713 100644 --- a/features/detail/src/main/res/values/strings.xml +++ b/features/detail/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ + - Image of the rocket + 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 index 82056d4..135cb5e 100644 --- a/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt +++ b/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -@UseExperimental(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) abstract class BaseTestWithMainThread { protected val dispatcher = TestCoroutineDispatcher() diff --git a/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt index 69ad7c9..738d89c 100644 --- a/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt +++ b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt @@ -1,12 +1,18 @@ package com.melih.detail +import androidx.lifecycle.viewModelScope import com.melih.detail.ui.DetailViewModel -import com.melih.repository.interactors.GetLaunchDetails +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem +import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest import org.amshove.kluent.shouldEqualTo import org.junit.jupiter.api.Test @@ -16,25 +22,10 @@ import org.junit.jupiter.api.Test * * See [testing documentation](http://d.android.com/tools/testing). */ -@UseExperimental(ExperimentalCoroutinesApi::class) class DetailViewModelTest : BaseTestWithMainThread() { - private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) + private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) private val getLaunchDetailsParams = GetLaunchDetails.Params(1013) private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams)) - - @Test - fun `loadData should invoke getLauchDetails with provided params`() { - dispatcher.runBlockingTest { - - val paramsSlot = slot() - - 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/launches/build.gradle b/features/launches/build.gradle index 1128480..740d414 100644 --- a/features/launches/build.gradle +++ b/features/launches/build.gradle @@ -8,6 +8,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation libraries.paging + implementation libraries.swipeRefreshLayout testImplementation testLibraries.coroutinesTest } diff --git a/features/launches/jacoco.exec b/features/launches/jacoco.exec deleted file mode 100644 index 65d2458..0000000 Binary files a/features/launches/jacoco.exec and /dev/null differ diff --git a/features/launches/src/main/AndroidManifest.xml b/features/launches/src/main/AndroidManifest.xml index 2323aa1..01b8e2a 100644 --- a/features/launches/src/main/AndroidManifest.xml +++ b/features/launches/src/main/AndroidManifest.xml @@ -1,14 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt new file mode 100644 index 0000000..8d50cb4 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt @@ -0,0 +1,10 @@ +package com.melih.launches.data + +import com.melih.abstractions.data.ViewEntity + +data class LaunchItem( + val id: Long, + val imageUrl: String, + val rocketName: String, + val missionDescription: String +) : ViewEntity diff --git a/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt new file mode 100644 index 0000000..4a24bee --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt @@ -0,0 +1,18 @@ +package com.melih.launches.data + +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import javax.inject.Inject + +class LaunchMapper @Inject constructor() : Mapper { + + override fun convert(launchEntity: LaunchEntity) = + with(launchEntity) { + LaunchItem( + id, + rocket.imageURL, + rocket.name, + if (!missions.isNullOrEmpty()) missions[0].description else "" + ) + } +} diff --git a/features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt b/features/launches/src/main/kotlin/com/melih/launches/di/LaunchesContributor.kt similarity index 60% rename from features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt rename to features/launches/src/main/kotlin/com/melih/launches/di/LaunchesContributor.kt index 9b1d947..a69725e 100644 --- a/features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/di/LaunchesContributor.kt @@ -1,8 +1,8 @@ -package com.melih.list.di +package com.melih.launches.di -import com.melih.list.di.modules.LaunchesFragmentModule -import com.melih.list.di.scopes.LaunchesFragmentScope -import com.melih.list.ui.LaunchesFragment +import com.melih.launches.di.modules.LaunchesFragmentModule +import com.melih.launches.di.scopes.LaunchesFragmentScope +import com.melih.launches.ui.LaunchesFragment import dagger.Module import dagger.android.ContributesAndroidInjector @@ -12,12 +12,12 @@ import dagger.android.ContributesAndroidInjector @Module abstract class LaunchesContributor { - // region Contributes + //region Contributes @ContributesAndroidInjector( modules = [LaunchesFragmentModule::class] ) @LaunchesFragmentScope abstract fun launchesFragment(): LaunchesFragment - // endregion + //endregion } diff --git a/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesFragmentModule.kt b/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt similarity index 56% rename from features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesFragmentModule.kt rename to features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt index eadf9b4..8841558 100644 --- a/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesFragmentModule.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt @@ -1,11 +1,15 @@ -package com.melih.list.di.modules +package com.melih.launches.di.modules import androidx.lifecycle.ViewModel import androidx.paging.Config +import com.melih.abstractions.mapper.Mapper import com.melih.core.di.keys.ViewModelKey -import com.melih.list.ui.vm.LaunchesViewModel -import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT -import com.melih.repository.interactors.GetLaunches +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.interactors.GetLaunches +import com.melih.launches.data.LaunchItem +import com.melih.launches.data.LaunchMapper +import com.melih.launches.ui.vm.LaunchesViewModel import dagger.Binds import dagger.Module import dagger.Provides @@ -14,13 +18,16 @@ import dagger.multibindings.IntoMap @Module abstract class LaunchesFragmentModule { - // region ViewModels + //region Binds @Binds @IntoMap @ViewModelKey(LaunchesViewModel::class) - abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel - // endregion + abstract fun launchesViewModel(listViewModel: LaunchesViewModel): ViewModel + + @Binds + abstract fun launchMapper(mapper: LaunchMapper): Mapper + //endregion @Module companion object { diff --git a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt b/features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesFragmentScope.kt similarity index 75% rename from features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt rename to features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesFragmentScope.kt index e5b898b..eb5898f 100644 --- a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesFragmentScope.kt @@ -1,4 +1,4 @@ -package com.melih.list.di.scopes +package com.melih.launches.di.scopes import javax.inject.Scope diff --git a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt b/features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesScope.kt similarity index 73% rename from features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt rename to features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesScope.kt index 31b4887..24baafb 100644 --- a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/di/scopes/LaunchesScope.kt @@ -1,4 +1,4 @@ -package com.melih.list.di.scopes +package com.melih.launches.di.scopes import javax.inject.Scope diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt new file mode 100644 index 0000000..af60af3 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt @@ -0,0 +1,93 @@ +package com.melih.launches.ui + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.melih.abstractions.deliverable.State +import com.melih.core.actions.openDetail +import com.melih.core.base.lifecycle.BaseDaggerFragment +import com.melih.core.extensions.observe +import com.melih.interactors.error.PersistenceEmptyError +import com.melih.launches.R +import com.melih.launches.data.LaunchItem +import com.melih.launches.databinding.LaunchesBinding +import com.melih.launches.ui.adapters.LaunchesAdapter +import com.melih.launches.ui.vm.LaunchesViewModel +import javax.inject.Inject + +class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { + + //region Properties + + private val viewModel by viewModels { viewModelFactory } + + private val launchesAdapter = LaunchesAdapter(::onItemSelected) + //endregion + + //region Lifecyle + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //setHasOptionsMenu(true) + + binding.rocketList.adapter = launchesAdapter + binding.swipeRefreshLayout.setOnRefreshListener(this) + + observeDataChanges() + } + + override fun onResume() { + super.onResume() + + // Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683 + binding.swipeRefreshLayout.isEnabled = true + } + + override fun onPause() { + super.onPause() + + // Workaround for SwipeRefreshLayout leak -> https://issuetracker.google.com/issues/136153683 + binding.swipeRefreshLayout.isEnabled = false + } + + override fun onDestroyView() { + super.onDestroyView() + binding.rocketList.adapter = null + } + //endregion + + //region Functions + + private fun observeDataChanges() { + + // Observing state to show loading + observe(viewModel.stateData) { + binding.swipeRefreshLayout.isRefreshing = it is State.Loading + } + + // Observing error to show toast with retry action + observe(viewModel.errorData) { + if (it !is PersistenceEmptyError) { + showSnackbarWithAction(it) { + viewModel.retry() + } + } + } + + observe(viewModel.pagedList) { + launchesAdapter.submitList(it) + } + } + + private fun onItemSelected(item: LaunchItem) { + openDetail(item.id) + } + + override fun onRefresh() { + viewModel.refresh() + } + + override fun getLayoutId(): Int = R.layout.fragment_launches + //endregion +} diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt new file mode 100644 index 0000000..7f8f423 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt @@ -0,0 +1,37 @@ +package com.melih.launches.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.melih.core.base.recycler.BasePagingListAdapter +import com.melih.core.base.recycler.BaseViewHolder +import com.melih.core.extensions.createDiffCallback +import com.melih.launches.data.LaunchItem +import com.melih.launches.databinding.LaunchRowBinding + +class LaunchesAdapter(itemClickListener: (LaunchItem) -> Unit) : BasePagingListAdapter( + createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id }, + itemClickListener +) { + + //region Functions + + override fun createViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int + ): BaseViewHolder = + LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false)) + //endregion +} + +class LaunchesViewHolder(private val binding: LaunchRowBinding) : + BaseViewHolder(binding) { + + //region Functions + + override fun bind(item: LaunchItem) { + binding.entity = item + binding.executePendingBindings() + } + //endregion +} diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt new file mode 100644 index 0000000..599165b --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt @@ -0,0 +1,26 @@ +package com.melih.launches.ui.paging + +import com.melih.core.base.paging.BasePagingDataSource +import com.melih.interactors.GetLaunches +import com.melih.launches.data.LaunchItem +import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Inject + +/** + * Uses [GetLaunches] to get data for pagination + */ +class LaunchesPagingSource @Inject constructor( + private val getLaunches: GetLaunches, + private val getLaunchesParams: GetLaunches.Params +) : BasePagingDataSource() { + + //region Functions + + override fun loadDataForPage(page: Int) = + getLaunches( + getLaunchesParams.copy( + page = page + ) + ) + //endregion +} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt similarity index 53% rename from features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt rename to features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt index d176076..7a6cfcf 100644 --- a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt @@ -1,14 +1,17 @@ -package com.melih.list.ui.paging +package com.melih.launches.ui.paging import com.melih.core.base.paging.BasePagingDataSource import com.melih.core.base.paging.BasePagingFactory -import com.melih.repository.entities.LaunchEntity +import com.melih.launches.data.LaunchItem import javax.inject.Inject import javax.inject.Provider class LaunchesPagingSourceFactory @Inject constructor( private val sourceProvider: Provider -) : BasePagingFactory() { +) : BasePagingFactory() { - override fun createSource(): BasePagingDataSource = sourceProvider.get() + //region Functions + + override fun createSource(): BasePagingDataSource = sourceProvider.get() + //endregion } diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt new file mode 100644 index 0000000..d026a54 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt @@ -0,0 +1,23 @@ +package com.melih.launches.ui.vm + +import androidx.paging.PagedList +import com.melih.core.base.paging.BasePagingFactory +import com.melih.core.base.viewmodel.BasePagingViewModel +import com.melih.launches.data.LaunchItem +import com.melih.launches.ui.paging.LaunchesPagingSourceFactory +import javax.inject.Inject + +class LaunchesViewModel @Inject constructor( + private val launchesPagingSourceFactory: LaunchesPagingSourceFactory, + private val launchesPagingConfig: PagedList.Config +) : BasePagingViewModel() { + + //region Properties + + override val factory: BasePagingFactory + get() = launchesPagingSourceFactory + + override val config: PagedList.Config + get() = launchesPagingConfig + //endregion +} diff --git a/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt b/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt deleted file mode 100644 index 68ae1be..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.melih.list.di - -import com.melih.list.di.scopes.LaunchesScope -import com.melih.list.ui.LaunchesActivity -import dagger.Module -import dagger.android.ContributesAndroidInjector - -/** - * Contributes fragments & view models in this module - */ -@Module -abstract class LaunchesFeatureModule { - - // region Contributes - - @ContributesAndroidInjector( - modules = [ - LaunchesContributor::class - ] - ) - @LaunchesScope - abstract fun launchesActivity(): LaunchesActivity - // endregion -} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt deleted file mode 100644 index 529b4ae..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt +++ /dev/null @@ -1,26 +0,0 @@ -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 - -class LaunchesActivity : BaseActivity() { - - // region Functions - - 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/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt deleted file mode 100644 index 816f40c..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.melih.list.ui - -import android.os.Bundle -import android.view.View -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -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.list.ui.adapters.LaunchesAdapter -import com.melih.list.ui.vm.LaunchesViewModel -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.State - -class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { - - // region Properties - - private val viewModel: LaunchesViewModel - get() = viewModelFactory.createFor(this) - - private val launchesAdapter = LaunchesAdapter(::onItemSelected) - // endregion - - // region Functions - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - //setHasOptionsMenu(true) - - binding.rocketList.adapter = launchesAdapter - binding.swipeRefreshLayout.setOnRefreshListener(this) - - observeDataChanges() - } - - //override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - // inflater.inflate(R.menu.menu_rocket_list, menu) - // - // with(menu.findItem(R.id.search)) { - // onExpandOrCollapse(::onSearchExpand, ::onSearchCollapse) - // setSearchQueryListener(actionView as SearchView) - // } - // - // super.onCreateOptionsMenu(menu, inflater) - //} - - private fun observeDataChanges() { - - // Observing state to show loading - observe(viewModel.stateData) { - binding.swipeRefreshLayout.isRefreshing = it is State.Loading - } - - // Observing error to show toast with retry action - observe(viewModel.errorData) { - showSnackbarWithAction(it) { - viewModel.retry() - } - } - - observe(viewModel.pagedList) { - launchesAdapter.submitList(it) - } - - //observe(viewModel.filteredItems) { - // launchesAdapter.submitList(it) - //} - } - - private fun onItemSelected(item: LaunchEntity) { - startActivity(Actions.openDetailFor(item.id)) - } - - //private fun onSearchExpand() { - // binding.swipeRefreshLayout.isEnabled = false - //} - - //private fun onSearchCollapse() { - // binding.swipeRefreshLayout.isEnabled = true - //} - - //private fun setSearchQueryListener(searchView: SearchView) { - // searchView.setOnQueryChangedListener { - // viewModel.filterItemListBy(it) - // } - //} - - override fun onRefresh() { - viewModel.refresh() - } - - override fun getLayoutId(): Int = R.layout.fragment_launches - // endregion -} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt b/features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt deleted file mode 100644 index c998b2c..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.melih.list.ui.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.melih.core.base.recycler.BasePagingListAdapter -import com.melih.core.base.recycler.BaseViewHolder -import com.melih.core.extensions.getDiffCallbackForType -import com.melih.list.databinding.LaunchRowBinding -import com.melih.repository.entities.LaunchEntity - -class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter( - getDiffCallbackForType { oldItem, newItem -> oldItem.id == newItem.id }, - 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.isNullOrEmpty()) missions[0].description else "" - - binding.executePendingBindings() - } -} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt b/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt deleted file mode 100644 index 9ec0bf8..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.melih.list.ui.paging - -import com.melih.core.base.paging.BasePagingDataSource -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.GetLaunches -import com.melih.repository.interactors.base.Result -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject - -class LaunchesPagingSource @Inject constructor( - private val getLaunches: GetLaunches, - private val getLaunchesParams: GetLaunches.Params -) : BasePagingDataSource() { - - //region Functions - - @UseExperimental(ExperimentalCoroutinesApi::class) - override fun loadDataForPage(page: Int): Flow>> = - getLaunches( - getLaunchesParams.copy( - page = page - ) - ) - //endregion -} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt b/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt deleted file mode 100644 index 4576d9c..0000000 --- a/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.melih.list.ui.vm - -import androidx.paging.PagedList -import com.melih.core.base.paging.BasePagingFactory -import com.melih.core.base.viewmodel.BasePagingViewModel -import com.melih.list.ui.paging.LaunchesPagingSourceFactory -import com.melih.repository.entities.LaunchEntity -import javax.inject.Inject - -class LaunchesViewModel @Inject constructor( - private val launchesPagingSourceFactory: LaunchesPagingSourceFactory, - private val launchesPagingConfig: PagedList.Config -) : BasePagingViewModel() { - - // region Properties - - override val factory: BasePagingFactory - get() = launchesPagingSourceFactory - - override val config: PagedList.Config - get() = launchesPagingConfig - - //private val _filteredItems = MediatorLiveData>() - - //val filteredItems: LiveData> - // get() = _filteredItems - // endregion - - //init { - // _filteredItems.addSource(pagedList, _filteredItems::setValue) - //} - - // region Functions - - //fun filterItemListBy(query: String?) { - // - // _filteredItems.value = if (!query.isNullOrBlank()) { - // pagedList.value - // ?.snapshot() as PagedList - // } else { - // pagedList.value - // } - //} - // endregion -} diff --git a/features/launches/src/main/res/layout/activity_launches.xml b/features/launches/src/main/res/layout/activity_launches.xml deleted file mode 100644 index 4369e55..0000000 --- a/features/launches/src/main/res/layout/activity_launches.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - diff --git a/features/launches/src/main/res/layout/fragment_launches.xml b/features/launches/src/main/res/layout/fragment_launches.xml index cac321d..967e940 100644 --- a/features/launches/src/main/res/layout/fragment_launches.xml +++ b/features/launches/src/main/res/layout/fragment_launches.xml @@ -1,37 +1,36 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + - - + + - + - + - - - + + + diff --git a/features/launches/src/main/res/layout/row_launch.xml b/features/launches/src/main/res/layout/row_launch.xml index bb62bbf..731d166 100644 --- a/features/launches/src/main/res/layout/row_launch.xml +++ b/features/launches/src/main/res/layout/row_launch.xml @@ -8,10 +8,10 @@ + type="com.melih.launches.data.LaunchItem" /> - - - - + - + diff --git a/features/launches/src/main/res/navigation/nav_launches.xml b/features/launches/src/main/res/navigation/nav_launches.xml index 03ee663..4df04f4 100644 --- a/features/launches/src/main/res/navigation/nav_launches.xml +++ b/features/launches/src/main/res/navigation/nav_launches.xml @@ -1,13 +1,15 @@ - + - - \ No newline at end of file + + diff --git a/features/launches/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt b/features/launches/src/test/kotlin/com/melih/launches/BaseTestWithMainThread.kt similarity index 88% rename from features/launches/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt rename to features/launches/src/test/kotlin/com/melih/launches/BaseTestWithMainThread.kt index 358b7d3..43d86ae 100644 --- a/features/launches/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt +++ b/features/launches/src/test/kotlin/com/melih/launches/BaseTestWithMainThread.kt @@ -1,6 +1,4 @@ -@file:UseExperimental(ExperimentalCoroutinesApi::class) - -package com.melih.list +package com.melih.launches import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -10,6 +8,7 @@ import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +@OptIn(ExperimentalCoroutinesApi::class) abstract class BaseTestWithMainThread { protected val dispatcher = TestCoroutineDispatcher() diff --git a/gradle.properties b/gradle.properties index 70e38b1..0f7ea80 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,26 +1,22 @@ -# 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 +## 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 +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # 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 +#Wed Jan 01 15:32:47 CET 2020 +android.databinding.incremental=true android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# For build performance -kotlin.incremental=true +android.useAndroidX=true +kapt.include.compile.classpath=false kapt.incremental.apt=true kapt.use.worker.api=true -kapt.include.compile.classpath=false +kotlin.code.style=official +kotlin.incremental=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 944f2c1..ca90413 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jun 13 10:50:25 CEST 2019 +#Mon Jul 06 12:03:00 CEST 2020 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 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-rc-1-all.zip diff --git a/repository/.gitignore b/repository/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/repository/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/repository/src/main/AndroidManifest.xml b/repository/src/main/AndroidManifest.xml deleted file mode 100644 index 1af640d..0000000 --- a/repository/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/repository/src/main/kotlin/com/melih/repository/Constants.kt b/repository/src/main/kotlin/com/melih/repository/Constants.kt deleted file mode 100644 index 05de778..0000000 --- a/repository/src/main/kotlin/com/melih/repository/Constants.kt +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 82a9d41..0000000 --- a/repository/src/main/kotlin/com/melih/repository/Repository.kt +++ /dev/null @@ -1,13 +0,0 @@ -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, page: Int): Result> - internal abstract suspend fun getLaunchById(id: Long): Result -} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt deleted file mode 100644 index 7bf13d5..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.melih.repository.interactors - -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.BaseInteractor -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.InteractorParameters -import com.melih.repository.interactors.base.Result -import com.melih.repository.interactors.base.Success -import com.melih.repository.sources.NetworkSource -import com.melih.repository.sources.PersistenceSource -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.FlowCollector -import javax.inject.Inject - -/** - * Gets next given number of launches - */ -@UseExperimental(ExperimentalCoroutinesApi::class) -class GetLaunchDetails @Inject constructor() : BaseInteractor() { - - @field:Inject - internal lateinit var networkSource: NetworkSource - - @field:Inject - internal lateinit var persistenceSource: PersistenceSource - - override suspend fun FlowCollector>.run(params: Params) { - val result = persistenceSource.getLaunchById(params.id) - - if (result !is Success) { - when (val response = networkSource.getLaunchById(params.id)) { - // Save result and return again from persistence - is Success -> { - persistenceSource.saveLaunch(response.successData) - emit(persistenceSource.getLaunchById(params.id)) - } - - // Redirect failure as it is - is Failure -> emit(response) - } - } else { - emit(result) - } - } - - 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 deleted file mode 100644 index 5cc80ec..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt +++ /dev/null @@ -1,48 +0,0 @@ -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.interactors.base.Success -import com.melih.repository.sources.NetworkSource -import com.melih.repository.sources.PersistenceSource -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.FlowCollector -import javax.inject.Inject - -const val DEFAULT_LAUNCHES_AMOUNT = 15 - -/** - * Gets next given number of launches - */ -@UseExperimental(ExperimentalCoroutinesApi::class) -class GetLaunches @Inject constructor() : BaseInteractor, GetLaunches.Params>() { - - @field:Inject - internal lateinit var networkSource: NetworkSource - - @field:Inject - internal lateinit var persistenceSource: PersistenceSource - - override suspend fun FlowCollector>>.run(params: Params) { - - // Start network fetch - we're not handling state here to ommit them - networkSource - .getNextLaunches(params.count, params.page) - .also { - if (it is Success) { - persistenceSource.saveLaunches(it.successData) - emit(persistenceSource.getNextLaunches(params.count, params.page)) - } else { - emit(it) - emit(persistenceSource.getNextLaunches(params.count, params.page)) - } - } - } - - data class Params( - val count: Int = DEFAULT_LAUNCHES_AMOUNT, - val page: Int - ) : 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 deleted file mode 100644 index e14b96e..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.melih.repository.interactors.base - -import androidx.annotation.StringRes -import com.melih.repository.R - - -/** - * [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/persistence/dao/LaunchesDao.kt b/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt deleted file mode 100644 index df4b014..0000000 --- a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.melih.repository.persistence.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.melih.repository.entities.LaunchEntity - -/** - * DAO for list of [launches][LaunchEntity] - */ -@Dao -internal abstract class LaunchesDao { - - // region Queries - - @Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count") - abstract suspend fun getLaunches(count: Int, page: 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(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun saveLaunches(launches: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun saveLaunch(launch: LaunchEntity) - // 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 deleted file mode 100644 index ed14fbe..0000000 --- a/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt +++ /dev/null @@ -1,116 +0,0 @@ -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.DEFAULT_LAUNCHES_AMOUNT -import com.melih.repository.interactors.base.EmptyResultError -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.NetworkError -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.ResponseError -import com.melih.repository.interactors.base.Result -import com.melih.repository.interactors.base.Success -import com.melih.repository.interactors.base.TimeoutError -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][Failure] with proper [reason][Reason] or [success][Success] with data - */ -internal 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, page: Int): Result> = - safeExecute({ - apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT) - }) { 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 () -> Response, - transform: (T) -> R - ) = - if (isNetworkConnected) { - try { - block().extractResponseBody(transform) - } catch (e: IOException) { - Failure(TimeoutError()) - } - } else { - Failure(NetworkError()) - } - - private inline fun Response.extractResponseBody(transform: (T) -> R) = - if (isSuccessful) { - body()?.let { - Success(transform(it)) - } ?: Failure(EmptyResultError()) - } else { - Failure(ResponseError()) - } - - private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) = - try { - val urlSplit = imageUrl.split("_") - val url = urlSplit[0] - val format = urlSplit[1].split(".")[1] - - val requestedSize = if (!supportedSizes.contains(DEFAULT_IMAGE_SIZE)) { - supportedSizes.last { it < DEFAULT_IMAGE_SIZE } - } else { - DEFAULT_IMAGE_SIZE - } - - "${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 deleted file mode 100644 index 366c683..0000000 --- a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.melih.repository.sources - -import android.content.Context -import com.melih.repository.Repository -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.PersistenceEmpty -import com.melih.repository.interactors.base.Result -import com.melih.repository.interactors.base.Success -import com.melih.repository.persistence.LaunchesDatabase -import javax.inject.Inject - -/** - * Persistance source using Room database to save / read objects for SST - offline usage - */ -internal class PersistenceSource @Inject constructor( - ctx: Context -) : Repository() { - // region Functions - - private val launchesDatabase = LaunchesDatabase.getInstance(ctx) - - override suspend fun getNextLaunches(count: Int, page: Int): Result> = - launchesDatabase - .launchesDao - .getLaunches(count, page) - .takeIf { it.isNotEmpty() } - ?.run { - Success(this) - } ?: Failure(PersistenceEmpty()) - - override suspend fun getLaunchById(id: Long): Result = - launchesDatabase - .launchesDao - .getLaunchById(id) - .takeIf { it != null } - ?.run { - Success(this) - } ?: Failure(PersistenceEmpty()) - - internal suspend fun saveLaunches(launches: List) { - launchesDatabase.launchesDao.saveLaunches(launches) - } - - internal suspend fun saveLaunch(launch: LaunchEntity) { - launchesDatabase.launchesDao.saveLaunch(launch) - } - // endregion -} diff --git a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt deleted file mode 100644 index 343aac6..0000000 --- a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -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.EmptyResultError -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.NetworkError -import com.melih.repository.interactors.base.ResponseError -import com.melih.repository.interactors.base.Success -import com.melih.repository.interactors.base.onFailure -import com.melih.repository.interactors.base.onSuccess -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 - -@UseExperimental(ExperimentalCoroutinesApi::class) -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 - fun `should return network error when internet is not connected`() { - every { networkInfoProvider.get().isConnected } returns false - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf NetworkError::class - } - } - } - - @Test - fun `should return response error when it is not successful`() { - every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns false - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf ResponseError::class - (it as ResponseError).messageRes shouldEqualTo R.string.reason_response - } - } - } - - @Test - fun `should return empty result error when body is null`() { - every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns null - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf EmptyResultError::class - } - } - } - - @Test - fun `should return success with data if execution is successful`() { - every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013))) - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Success::class - result.onSuccess { - 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 deleted file mode 100644 index e40fa14..0000000 --- a/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.melih.repository.sources - -import android.content.Context -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Failure -import com.melih.repository.interactors.base.PersistenceEmpty -import com.melih.repository.interactors.base.Success -import com.melih.repository.interactors.base.onFailure -import com.melih.repository.interactors.base.onSuccess -import com.melih.repository.persistence.LaunchesDatabase -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.spyk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldBeInstanceOf -import org.amshove.kluent.shouldEqualTo -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class PersistanceSourceTest { - - private val ctx = mockk(relaxed = true) - private val dbImplementation = mockk(relaxed = true) - private val source = spyk(PersistenceSource(ctx)) - - private val scope = CoroutineScope(Dispatchers.IO) - - @BeforeEach - fun setup() { - mockkObject(LaunchesDatabase) - every { LaunchesDatabase.getInstance(ctx) } returns dbImplementation - } - - @Nested - inner class GetNextLaunches { - - @Test - fun `should return persistance empty error when db is empty`() { - coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns emptyList() - - scope.launch { - val result = source.getNextLaunches(10, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf PersistenceEmpty::class - } - } - } - - @Test - fun `should return success with data if db is not empty`() { - coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns listOf(LaunchEntity(id = 1013)) - - scope.launch { - val result = source.getNextLaunches(10, 0) - - result shouldBeInstanceOf Success::class - result.onSuccess { - it.isEmpty() shouldBe false - it.size shouldEqualTo 1 - it[0].id shouldEqualTo 1013 - } - } - } - } -} diff --git a/scripts/detekt.gradle b/scripts/cq/detekt.gradle similarity index 81% rename from scripts/detekt.gradle rename to scripts/cq/detekt.gradle index 5f07c3c..68347a6 100644 --- a/scripts/detekt.gradle +++ b/scripts/cq/detekt.gradle @@ -1,9 +1,8 @@ apply plugin: 'io.gitlab.arturbosch.detekt' detekt { - toolVersion = "1.0.0" + failFast = true config = files("$rootProject.projectDir/default-detekt-config.yml") - filters = ".*/resources/.*,.*/build/.*" reports { html { diff --git a/scripts/cq/dokka.gradle b/scripts/cq/dokka.gradle new file mode 100644 index 0000000..2590827 --- /dev/null +++ b/scripts/cq/dokka.gradle @@ -0,0 +1,13 @@ +apply plugin: 'org.jetbrains.dokka' + +dokka { + outputFormat = "html" + outputDirectory = "$rootProject.projectDir/reports/javadoc" + + configuration { + jdkVersion = 8 + + reportUndocumented = true + skipEmptyPackages = true + } +} diff --git a/scripts/cq/jacoco.gradle b/scripts/cq/jacoco.gradle new file mode 100644 index 0000000..0493b26 --- /dev/null +++ b/scripts/cq/jacoco.gradle @@ -0,0 +1,53 @@ +apply plugin: 'jacoco' + +jacoco { + toolVersion = "0.8.1" + reportsDir = file("$rootProject.projectDir/reports/jacoco/$project.name") +} + +task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { + group = "Reporting" + description = "Generate Jacoco coverage reports for Debug build" + + reports { + xml.enabled = true + html.enabled = true + } + + // what to exclude from coverage report + // UI, "noise", generated classes, platform classes, etc. + def excludes = [ + '**/R.class', + '**/R$*.class', + '**/*$ViewInjector*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + '**/*Fragment.*', + '**/*Activity.*' + ] + + // generated classes + getClassDirectories().setFrom( + fileTree( + dir: "$buildDir/intermediates/classes/debug", + excludes: excludes + ) + fileTree( + dir: "$buildDir/tmp/kotlin-classes/debug", + excludes: excludes + ) + ) + + // sources + getSourceDirectories().setFrom( + files([ + android.sourceSets.main.java.srcDirs, + "src/main/kotlin" + ]) + ) + + getExecutionData().setFrom( + files("$buildDir/jacoco/testDebugUnitTest.exec") + ) +} \ No newline at end of file diff --git a/scripts/default_android_config.gradle b/scripts/default_android_config.gradle index b5ab234..ed21087 100644 --- a/scripts/default_android_config.gradle +++ b/scripts/default_android_config.gradle @@ -1,7 +1,10 @@ -apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" +apply from: "$rootProject.projectDir/scripts/cq/detekt.gradle" +apply from: "$rootProject.projectDir/scripts/cq/dokka.gradle" +apply from: "$rootProject.projectDir/scripts/cq/jacoco.gradle" android { compileSdkVersion versions.compileSdkVersion + buildToolsVersion versions.buildToolsVersion defaultConfig { minSdkVersion versions.minSdkVersion @@ -10,4 +13,18 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + + testOptions.unitTests.all { + jvmArgs "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap" + } + + defaultConfig { + javaCompileOptions { + annotationProcessorOptions { + arguments = [ + "dagger.experimentalDaggerErrorMessages": "enabled" + ] + } + } + } } diff --git a/scripts/default_dependencies.gradle b/scripts/default_dependencies.gradle index b9ee1c9..9e6003a 100644 --- a/scripts/default_dependencies.gradle +++ b/scripts/default_dependencies.gradle @@ -1,11 +1,10 @@ apply plugin: "de.mannodermaus.android-junit5" -apply from: "$rootProject.projectDir/scripts/detekt.gradle" -apply from: "$rootProject.projectDir/scripts/dokka.gradle" - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':abstractions') + implementation libraries.kotlin implementation libraries.dagger implementation libraries.timber @@ -18,57 +17,3 @@ dependencies { testRuntimeOnly testLibraries.jUnitEngine } - -apply plugin: 'jacoco' - -jacoco { - toolVersion = "0.8.1" - reportsDir = file("$rootProject.projectDir/reports/jacoco/$project.name") -} - -task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { - group = "Reporting" - description = "Generate Jacoco coverage reports for Debug build" - - reports { - xml.enabled = true - html.enabled = true - } - - // what to exclude from coverage report - // UI, "noise", generated classes, platform classes, etc. - def excludes = [ - '**/R.class', - '**/R$*.class', - '**/*$ViewInjector*.*', - '**/BuildConfig.*', - '**/Manifest*.*', - '**/*Test*.*', - 'android/**/*.*', - '**/*Fragment.*', - '**/*Activity.*' - ] - - // generated classes - getClassDirectories().setFrom( - fileTree( - dir: "$buildDir/intermediates/classes/debug", - excludes: excludes - ) + fileTree( - dir: "$buildDir/tmp/kotlin-classes/debug", - excludes: excludes - ) - ) - - // sources - getSourceDirectories().setFrom( - files([ - android.sourceSets.main.java.srcDirs, - "src/main/kotlin" - ]) - ) - - getExecutionData().setFrom( - files("$buildDir/jacoco/testDebugUnitTest.exec") - ) -} diff --git a/scripts/dependencies.gradle b/scripts/dependencies.gradle index 2c3811a..17d113a 100644 --- a/scripts/dependencies.gradle +++ b/scripts/dependencies.gradle @@ -1,77 +1,80 @@ ext { versions = [ - minSdkVersion : 21, - compileSdkVersion : 28, - targetSdkVersion : 28, - buildToolsVersion : "28.0.3", - supportLibraryVersion : "28.0.0", - appCompatVersion : "1.1.0-rc01", - lifecycleVersion : "2.2.0-alpha03", - fragmentVersion : "1.2.0-alpha02", - workManagerVersion : "2.2.0-rc01", - constraintLayoutVesion: "2.0.0-beta1", - cardViewVersion : "1.0.0", - recyclerViewVersion : "1.1.0-beta02", - pagingVersion : "2.1.0", - viewPagerVersion : "1.0.0-beta03", - collectionVersion : "1.1.0", - roomVersion : "2.2.0-alpha02", - daggerVersion : "2.24", - okHttpVersion : "4.0.1", - retrofitVersion : "2.6.1", - picassoVersion : "2.71828", - moshiVersion : "1.8.0", - coroutinesVersion : "1.3.0-RC2", - leakCanaryVersion : "2.0-beta-2", - timberVersion : "4.7.1", - jUnitVersion : "5.5.1", - espressoVersion : "3.2.0", - mockkVersion : "1.9.3", - kluentVersion : "1.53", + minSdkVersion : 21, + compileSdkVersion : 29, + targetSdkVersion : 29, + buildToolsVersion : "29.0.3", + appCompatVersion : "1.1.0", + lifecycleVersion : "2.2.0", + fragmentVersion : "1.3.0-alpha06", + workManagerVersion : "2.4.0-alpha01", + constraintLayoutVesion : "2.0.0-beta7", + cardViewVersion : "1.0.0", + recyclerViewVersion : "1.2.0-alpha01", + pagingVersion : "2.1.2", + viewPagerVersion : "1.0.0", + materialVersion : "1.3.0-alpha01", + swipeRefreshLayoutVersion: "1.1.0", + collectionVersion : "1.1.0", + roomVersion : "2.2.5", + daggerVersion : "2.27", + okHttpVersion : "4.7.2", + retrofitVersion : "2.9.0", + picassoVersion : "2.71828", + moshiVersion : "1.9.3", + coroutinesVersion : "1.3.5", + leakCanaryVersion : "2.2", + timberVersion : "4.7.1", + jUnitVersion : "5.6.2", + espressoVersion : "3.2.0", + mockkVersion : "1.10.0", + kluentVersion : "1.60", ] 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}", + 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}", + material : "com.google.android.material:material:${versions.materialVersion}", /** * Jetpack */ - navigation : [ + navigation : [ "androidx.navigation:navigation-fragment-ktx:$nav_version", "androidx.navigation:navigation-ui-ktx:$nav_version" ], - room : [ + room : [ "androidx.room:room-runtime:${versions.roomVersion}", "androidx.room:room-ktx:${versions.roomVersion}" ], - lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}", - liveDataKTX : "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-ktx:${versions.collectionVersion}", + lifecycle : "androidx.lifecycle:lifecycle-extensions:${versions.lifecycleVersion}", + liveDataKTX : "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-ktx:${versions.collectionVersion}", + swipeRefreshLayout: "androidx.swiperefreshlayout:swiperefreshlayout:${versions.swipeRefreshLayoutVersion}", /** * Kotlin */ - kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version", - coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}", + kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version", + coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutinesVersion}", /** * Dagger */ - dagger : [ + dagger : [ "com.google.dagger:dagger:${versions.daggerVersion}", "com.google.dagger:dagger-android:${versions.daggerVersion}", "com.google.dagger:dagger-android-support:${versions.daggerVersion}" @@ -80,17 +83,17 @@ ext { /** * OkHttp */ - okHttp : [ + okHttp : [ "com.squareup.okhttp3:okhttp:${versions.okHttpVersion}", "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}" ], - okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}", + okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okHttpVersion}", /** * Retrofit */ - retrofit : [ + retrofit : [ "com.squareup.retrofit2:retrofit:${versions.retrofitVersion}", "com.squareup.retrofit2:converter-moshi:${versions.retrofitVersion}" ], @@ -98,27 +101,27 @@ ext { /** * Moshi */ - moshi : [ + moshi : [ "com.squareup.moshi:moshi:${versions.moshiVersion}", "com.squareup.moshi:moshi-kotlin:${versions.moshiVersion}" ], - moshiKotlin : "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}", + picasso : "com.squareup.picasso:picasso:${versions.picassoVersion}", /** * LeakCanary */ - leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}", + leakCanary : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanaryVersion}", /** * Timber */ - timber : "com.jakewharton.timber:timber:${versions.timberVersion}" + timber : "com.jakewharton.timber:timber:${versions.timberVersion}" ] annotationProcessors = [ diff --git a/scripts/dokka.gradle b/scripts/dokka.gradle deleted file mode 100644 index 59bed4c..0000000 --- a/scripts/dokka.gradle +++ /dev/null @@ -1,10 +0,0 @@ -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 index e6ceeb1..83bba26 100644 --- a/scripts/feature_module.gradle +++ b/scripts/feature_module.gradle @@ -1,13 +1,19 @@ +apply plugin: "androidx.navigation.safeargs" + apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" android { - dataBinding { - enabled = true + + buildFeatures{ + dataBinding = true } } dependencies { implementation project(':core') + implementation project(':data:interactors') + implementation project(':data:definitions') implementation libraries.fragment implementation libraries.lifecycle diff --git a/scripts/flavors.gradle b/scripts/flavors.gradle deleted file mode 100644 index 496ab22..0000000 --- a/scripts/flavors.gradle +++ /dev/null @@ -1,18 +0,0 @@ -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' - testCoverageEnabled = true - } - - dev { - initWith debug - } - } -} diff --git a/scripts/module.gradle b/scripts/module.gradle index cd8295f..de40f3f 100644 --- a/scripts/module.gradle +++ b/scripts/module.gradle @@ -1,3 +1,2 @@ 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 index dcbd8fc..a6af0a8 100644 --- a/scripts/sources.gradle +++ b/scripts/sources.gradle @@ -14,4 +14,21 @@ android { jvmTarget = '1.8' freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" } + + 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' + testCoverageEnabled = true + } + + dev { + initWith debug + } + } } diff --git a/settings.gradle b/settings.gradle index 7a766a4..ad1f600 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,11 @@ -include ':app', ':repository', ':core', ':features:launches', ':features:detail' -rootProject.name = 'Rocket Science' +rootProject.name = 'RocketScience' + +include ':abstractions', + ':core', + 'app', + ':features:launches', + ':features:detail', + ':data:interactors', + ':data:network', + ':data:persistence', + ':data:definitions'