mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-04-26 18:58:32 +02:00
WIP: feature/abstractions (#45)
* Abstraction layer backup * Removed DataEntity, was unnecessary for now * Separated network, persistence, entities and interaction, closes #29 * Renamed binding * Removed build files, example tests Removed build files, example tests * Fixed build files were not being ignored all around app * Updated CI ymls * Small changes * Fixed legacy repository package names * Fixed CQ findings * Updated Fastlane * Packaging changes and version upgrades * Removed core from interactors * Version bumps * Added new module graph
This commit is contained in:
@@ -76,12 +76,60 @@ jobs:
|
|||||||
- store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
|
- store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/
|
||||||
path: features/detail/build/test-results
|
path: features/detail/build/test-results
|
||||||
|
|
||||||
## Repository
|
## Abstractions
|
||||||
- run:
|
- run:
|
||||||
name: Test Repository
|
name: Test Abstractions
|
||||||
command: |
|
command: |
|
||||||
fastlane test_repository
|
fastlane test_abstractions
|
||||||
./gradlew repository:jacocoTestReport
|
./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: Test Persistence
|
||||||
|
command: |
|
||||||
|
fastlane test_persistence
|
||||||
|
./gradlew data:persistence:jacocoTestReport
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
- store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
|
- store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/
|
||||||
path: repository/build/reports/tests
|
path: repository/build/reports/tests
|
||||||
|
|||||||
30
.github/workflows/android.yml
vendored
30
.github/workflows/android.yml
vendored
@@ -55,8 +55,32 @@ jobs:
|
|||||||
./gradlew features:detail:jacocoTestReport
|
./gradlew features:detail:jacocoTestReport
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
- name: Test Repository
|
- name: Test Abstractions
|
||||||
run: |
|
run: |
|
||||||
fastlane test_repository
|
fastlane test_abstractions
|
||||||
./gradlew repository:jacocoTestReport
|
./gradlew abstractions:jacocoTestReport
|
||||||
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
- name: Test Definitions
|
||||||
|
run: |
|
||||||
|
fastlane test_definitions
|
||||||
|
./gradlew data:definitions:jacocoTestReport
|
||||||
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
- name: Test Interactors
|
||||||
|
run: |
|
||||||
|
fastlane test_interactors
|
||||||
|
./gradlew data:interactors:jacocoTestReport
|
||||||
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
- name: Test Network
|
||||||
|
run: |
|
||||||
|
fastlane test_network
|
||||||
|
./gradlew data:network:jacocoTestReport
|
||||||
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
- name: Test Persistence
|
||||||
|
run: |
|
||||||
|
fastlane test_persistence
|
||||||
|
./gradlew data:persistence:jacocoTestReport
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
|
**/build
|
||||||
/fastlane/README.md
|
/fastlane/README.md
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
16
Gemfile.lock
16
Gemfile.lock
@@ -5,7 +5,7 @@ GEM
|
|||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
babosa (1.0.2)
|
babosa (1.0.3)
|
||||||
claide (1.0.3)
|
claide (1.0.3)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
@@ -18,7 +18,7 @@ GEM
|
|||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.5)
|
dotenv (2.7.5)
|
||||||
emoji_regex (1.0.1)
|
emoji_regex (1.0.1)
|
||||||
excon (0.66.0)
|
excon (0.67.0)
|
||||||
faraday (0.15.4)
|
faraday (0.15.4)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday-cookie_jar (0.0.6)
|
faraday-cookie_jar (0.0.6)
|
||||||
@@ -27,7 +27,7 @@ GEM
|
|||||||
faraday_middleware (0.13.1)
|
faraday_middleware (0.13.1)
|
||||||
faraday (>= 0.7.4, < 1.0)
|
faraday (>= 0.7.4, < 1.0)
|
||||||
fastimage (2.1.7)
|
fastimage (2.1.7)
|
||||||
fastlane (2.131.0)
|
fastlane (2.133.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.3, < 3.0.0)
|
addressable (>= 2.3, < 3.0.0)
|
||||||
babosa (>= 1.0.2, < 2.0.0)
|
babosa (>= 1.0.2, < 2.0.0)
|
||||||
@@ -37,9 +37,9 @@ GEM
|
|||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 2.0)
|
emoji_regex (>= 0.1, < 2.0)
|
||||||
excon (>= 0.45.0, < 1.0.0)
|
excon (>= 0.45.0, < 1.0.0)
|
||||||
faraday (~> 0.9)
|
faraday (< 0.16.0)
|
||||||
faraday-cookie_jar (~> 0.0.6)
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
faraday_middleware (~> 0.9)
|
faraday_middleware (< 0.16.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-api-client (>= 0.21.2, < 0.24.0)
|
google-api-client (>= 0.21.2, < 0.24.0)
|
||||||
@@ -52,7 +52,7 @@ GEM
|
|||||||
multipart-post (~> 2.0.0)
|
multipart-post (~> 2.0.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
public_suffix (~> 2.0.0)
|
public_suffix (~> 2.0.0)
|
||||||
rubyzip (>= 1.2.2, < 2.0.0)
|
rubyzip (>= 1.3.0, < 2.0.0)
|
||||||
security (= 0.1.3)
|
security (= 0.1.3)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||||
@@ -98,7 +98,7 @@ GEM
|
|||||||
memoist (0.16.0)
|
memoist (0.16.0)
|
||||||
mime-types (3.3)
|
mime-types (3.3)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2019.0904)
|
mime-types-data (3.2019.1009)
|
||||||
mini_magick (4.9.5)
|
mini_magick (4.9.5)
|
||||||
multi_json (1.13.1)
|
multi_json (1.13.1)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
@@ -114,7 +114,7 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
rubyzip (1.2.4)
|
rubyzip (1.3.0)
|
||||||
security (0.1.3)
|
security (0.1.3)
|
||||||
signet (0.11.0)
|
signet (0.11.0)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
|
|||||||
17
abstractions/build.gradle
Normal file
17
abstractions/build.gradle
Normal file
@@ -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
|
||||||
|
}
|
||||||
21
abstractions/proguard-rules.pro
vendored
Normal file
21
abstractions/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
3
abstractions/src/main/AndroidManifest.xml
Normal file
3
abstractions/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.melih.abstractions" />
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.melih.abstractions.data
|
||||||
|
|
||||||
|
interface ViewEntity
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.melih.abstractions.deliverable
|
||||||
|
|
||||||
|
abstract class Reason : Throwable() {
|
||||||
|
|
||||||
|
abstract val messageRes: Int
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
package com.melih.repository.interactors.base
|
package com.melih.abstractions.deliverable
|
||||||
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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<out T>
|
sealed class Result<out T>
|
||||||
|
|
||||||
//region Subclasses
|
//region Subclasses
|
||||||
@@ -22,7 +19,11 @@ sealed class State : Result<Nothing>() {
|
|||||||
|
|
||||||
//region Extensions
|
//region Extensions
|
||||||
|
|
||||||
inline fun <T> Result<T>.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) {
|
inline fun <T> Result<T>.handle(
|
||||||
|
stateBlock: (State) -> Unit,
|
||||||
|
failureBlock: (Reason) -> Unit,
|
||||||
|
successBlock: (T) -> Unit
|
||||||
|
) {
|
||||||
when (this) {
|
when (this) {
|
||||||
is Success -> successBlock(successData)
|
is Success -> successBlock(successData)
|
||||||
is Failure -> failureBlock(errorData)
|
is Failure -> failureBlock(errorData)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.melih.abstractions.mapper
|
||||||
|
|
||||||
|
import com.melih.abstractions.data.ViewEntity
|
||||||
|
|
||||||
|
abstract class Mapper<in T, out R : ViewEntity> {
|
||||||
|
|
||||||
|
abstract fun convert(t: T): R
|
||||||
|
}
|
||||||
@@ -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.called
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
@@ -8,12 +12,17 @@ import org.amshove.kluent.shouldBeInstanceOf
|
|||||||
import org.amshove.kluent.shouldEqualTo
|
import org.amshove.kluent.shouldEqualTo
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
class ResultTest {
|
class ResultTest {
|
||||||
|
|
||||||
private val number = 10
|
private val number = 10
|
||||||
|
|
||||||
private val success = Success(number)
|
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 state = State.Loading()
|
||||||
|
|
||||||
private val emptyStateBlock = spyk({ _: State -> })
|
private val emptyStateBlock = spyk({ _: State -> })
|
||||||
@@ -37,8 +46,7 @@ class ResultTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Failure should only invoke failureBlock with correct error`() {
|
fun `Failure should only invoke failureBlock with correct error`() {
|
||||||
val actualFailureBlock = spyk({ reason: Reason ->
|
val actualFailureBlock = spyk({ reason: Reason ->
|
||||||
reason shouldBeInstanceOf GenericError::class
|
reason.messageRes shouldEqualTo 10
|
||||||
(reason as GenericError).messageRes shouldEqualTo R.string.reason_generic
|
|
||||||
Unit
|
Unit
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
|
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/sources.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -31,10 +32,14 @@ dependencies {
|
|||||||
|
|
||||||
androidTestImplementation testLibraries.espresso
|
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(':repository')
|
|
||||||
|
compileOnly project(':abstractions')
|
||||||
|
compileOnly project(':data:interactors')
|
||||||
|
compileOnly project(':data:network')
|
||||||
|
compileOnly project(':data:definitions')
|
||||||
|
|
||||||
compileOnly libraries.retrofit
|
compileOnly libraries.retrofit
|
||||||
compileOnly libraries.room
|
|
||||||
compileOnly libraries.paging
|
compileOnly libraries.paging
|
||||||
compileOnly libraries.swipeRefreshLayout
|
compileOnly libraries.swipeRefreshLayout
|
||||||
|
|
||||||
|
|||||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -21,8 +21,5 @@
|
|||||||
@com.squareup.moshi.ToJson <methods>;
|
@com.squareup.moshi.ToJson <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepnames @kotlin.Metadata class com.myapp.packagename.model.**
|
|
||||||
-keep class com.myapp.packagnename.model.** { *; }
|
|
||||||
|
|
||||||
# Keeping entities intact
|
# Keeping entities intact
|
||||||
-keep class com.melih.repository.entities.** { *; }
|
-keep class com.melih.definitions.entities.** { *; }
|
||||||
|
|||||||
14
build.gradle
14
build.gradle
@@ -1,7 +1,7 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.3.50'
|
||||||
ext.nav_version = '2.2.0-alpha01'
|
ext.nav_version = '2.2.0-beta01'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -9,7 +9,7 @@ buildscript {
|
|||||||
|
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18"
|
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18"
|
||||||
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.1.0"
|
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.1.0"
|
||||||
@@ -20,9 +20,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "io.gitlab.arturbosch.detekt" version "1.0.0"
|
id 'io.gitlab.arturbosch.detekt' version '1.1.1'
|
||||||
id "org.jetbrains.dokka" version "0.9.18"
|
id 'org.jetbrains.dokka' version '0.9.18'
|
||||||
id "jacoco"
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@@ -97,7 +97,9 @@ task projectDependencyGraph {
|
|||||||
rootProjects.remove(dependency)
|
rootProjects.remove(dependency)
|
||||||
|
|
||||||
def graphKey = new Tuple2<Project, Project>(project, dependency)
|
def graphKey = new Tuple2<Project, Project>(project, dependency)
|
||||||
def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList<String>() }
|
def traits = dependencies.computeIfAbsent(graphKey) {
|
||||||
|
new ArrayList<String>()
|
||||||
|
}
|
||||||
|
|
||||||
if (config.name.toLowerCase().endsWith('implementation')) {
|
if (config.name.toLowerCase().endsWith('implementation')) {
|
||||||
traits.add('style=dotted')
|
traits.add('style=dotted')
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
apply from: "$rootProject.projectDir/scripts/default_android_config.gradle"
|
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/sources.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -14,8 +15,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
|
||||||
implementation project(':repository')
|
implementation libraries.coroutines
|
||||||
|
|
||||||
implementation libraries.fragment
|
implementation libraries.fragment
|
||||||
implementation libraries.paging
|
implementation libraries.paging
|
||||||
implementation libraries.lifecycle
|
implementation libraries.lifecycle
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import androidx.databinding.DataBindingUtil
|
|||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.melih.abstractions.deliverable.Reason
|
||||||
import com.melih.core.R
|
import com.melih.core.R
|
||||||
import com.melih.repository.interactors.base.Reason
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parent of all fragments.
|
* Parent of all fragments.
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.paging.PageKeyedDataSource
|
import androidx.paging.PageKeyedDataSource
|
||||||
import com.melih.repository.interactors.base.Reason
|
import com.melih.abstractions.data.ViewEntity
|
||||||
import com.melih.repository.interactors.base.Result
|
import com.melih.abstractions.deliverable.Reason
|
||||||
import com.melih.repository.interactors.base.State
|
import com.melih.abstractions.deliverable.Result
|
||||||
import com.melih.repository.interactors.base.onFailure
|
import com.melih.abstractions.deliverable.State
|
||||||
import com.melih.repository.interactors.base.onState
|
import com.melih.abstractions.deliverable.onFailure
|
||||||
import com.melih.repository.interactors.base.onSuccess
|
import com.melih.abstractions.deliverable.onState
|
||||||
|
import com.melih.abstractions.deliverable.onSuccess
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -35,11 +36,11 @@ const val INITIAL_PAGE = 0
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
abstract class BasePagingDataSource<R : ViewEntity> : PageKeyedDataSource<Int, R>() {
|
||||||
|
|
||||||
//region Abstractions
|
//region Abstractions
|
||||||
|
|
||||||
abstract fun loadDataForPage(page: Int): Flow<Result<List<T>>> // Load next page(s)
|
abstract fun loadDataForPage(page: Int): Flow<Result<List<R>>> // Load next page(s)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region Properties
|
//region Properties
|
||||||
@@ -63,7 +64,10 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
|||||||
|
|
||||||
//region Functions
|
//region Functions
|
||||||
|
|
||||||
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, T>) {
|
override fun loadInitial(
|
||||||
|
params: LoadInitialParams<Int>,
|
||||||
|
callback: LoadInitialCallback<Int, R>
|
||||||
|
) {
|
||||||
// Looping through channel as we'll receive any state, error or data here
|
// Looping through channel as we'll receive any state, error or data here
|
||||||
loadDataForPage(INITIAL_PAGE)
|
loadDataForPage(INITIAL_PAGE)
|
||||||
.onEach { result ->
|
.onEach { result ->
|
||||||
@@ -81,7 +85,7 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
|||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, T>) {
|
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, R>) {
|
||||||
// Key for which page to load is in params
|
// Key for which page to load is in params
|
||||||
val page = params.key
|
val page = params.key
|
||||||
|
|
||||||
@@ -104,7 +108,7 @@ abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
|||||||
/**
|
/**
|
||||||
* This loads previous pages, we don't have a use for it yet, so it's a no-op override
|
* 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<Int>, callback: LoadCallback<Int, T>) {
|
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, R>) {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.melih.core.base.paging
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.paging.DataSource
|
import androidx.paging.DataSource
|
||||||
|
import com.melih.abstractions.data.ViewEntity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project.
|
* Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project.
|
||||||
@@ -14,7 +15,7 @@ import androidx.paging.DataSource
|
|||||||
*
|
*
|
||||||
* Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource].
|
* Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource].
|
||||||
*/
|
*/
|
||||||
abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() {
|
abstract class BasePagingFactory<T : ViewEntity> : DataSource.Factory<Int, T>() {
|
||||||
|
|
||||||
//region Abstractions
|
//region Abstractions
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import androidx.lifecycle.Transformations.switchMap
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import androidx.paging.toLiveData
|
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.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].
|
* Base [ViewModel] for view models that will use [PagedList].
|
||||||
@@ -18,7 +19,7 @@ import com.melih.repository.interactors.base.State
|
|||||||
*
|
*
|
||||||
* If paging won't be used, use [BaseViewModel] instead.
|
* If paging won't be used, use [BaseViewModel] instead.
|
||||||
*/
|
*/
|
||||||
abstract class BasePagingViewModel<T> : ViewModel() {
|
abstract class BasePagingViewModel<T : ViewEntity> : ViewModel() {
|
||||||
|
|
||||||
//region Abstractions
|
//region Abstractions
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.melih.repository.interactors.base.Reason
|
import com.melih.abstractions.deliverable.Reason
|
||||||
import com.melih.repository.interactors.base.State
|
import com.melih.abstractions.deliverable.State
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -3,14 +3,15 @@
|
|||||||
package com.melih.core.paging
|
package com.melih.core.paging
|
||||||
|
|
||||||
import androidx.paging.PageKeyedDataSource
|
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.BaseTestWithMainThread
|
||||||
import com.melih.core.base.paging.BasePagingDataSource
|
import com.melih.core.base.paging.BasePagingDataSource
|
||||||
import com.melih.core.testObserve
|
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.mockk
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
@@ -28,7 +29,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
val failureSource = spyk(TestFailureSource())
|
val failureSource = spyk(TestFailureSource())
|
||||||
|
|
||||||
val data = 10
|
val data = 10
|
||||||
val errorMessage = "Generic Error"
|
val errorMessageResId = 1313
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class BasePagingSource {
|
inner class BasePagingSource {
|
||||||
@@ -37,10 +38,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
inner class LoadInitial {
|
inner class LoadInitial {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should update state accordingly`() {
|
fun `should update state accordingly`() {
|
||||||
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
||||||
@@ -54,10 +54,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should update error Error accordingly`() {
|
fun `should update error Error accordingly`() {
|
||||||
val params = PageKeyedDataSource.LoadInitialParams<Int>(10, false)
|
val params = PageKeyedDataSource.LoadInitialParams<Int>(10, false)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
failureSource.loadInitial(params, callback)
|
failureSource.loadInitial(params, callback)
|
||||||
|
|
||||||
failureSource.reasonData.testObserve {
|
failureSource.reasonData.testObserve {
|
||||||
it shouldBeInstanceOf GenericError::class
|
it shouldBeInstanceOf TestFailureReason::class
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,10 +74,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
inner class LoadAfter {
|
inner class LoadAfter {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should update state accordingly`() {
|
fun `should update state accordingly`() {
|
||||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
||||||
@@ -92,10 +90,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should update error Error accordingly`() {
|
fun `should update error Error accordingly`() {
|
||||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|
||||||
@@ -103,17 +100,16 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
failureSource.loadAfter(params, callback)
|
failureSource.loadAfter(params, callback)
|
||||||
|
|
||||||
failureSource.reasonData.testObserve {
|
failureSource.reasonData.testObserve {
|
||||||
it shouldBeInstanceOf GenericError::class
|
it shouldBeInstanceOf TestFailureReason::class
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should use loadDataForPage in loadInitial and transform emmited value`() {
|
fun `should use loadDataForPage in loadInitial and transform emmited value`() {
|
||||||
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
// Fake loading
|
// Fake loading
|
||||||
source.loadInitial(params, callback)
|
source.loadInitial(params, callback)
|
||||||
@@ -126,10 +122,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
||||||
fun `should use loadDataForPage in loadAfter and transform emmited value`() {
|
fun `should use loadDataForPage in loadAfter and transform emmited value`() {
|
||||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, TestViewEntity>>(relaxed = true)
|
||||||
|
|
||||||
// Fake loading
|
// Fake loading
|
||||||
source.loadAfter(params, callback)
|
source.loadAfter(params, callback)
|
||||||
@@ -142,25 +137,27 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class TestSource : BasePagingDataSource<Int>() {
|
inner class TestSource : BasePagingDataSource<TestViewEntity>() {
|
||||||
|
|
||||||
|
|
||||||
val result = flow {
|
val result = flow {
|
||||||
emit(State.Loading())
|
emit(State.Loading())
|
||||||
emit(Success(listOf(data)))
|
emit(Success(listOf(TestViewEntity(data))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun loadDataForPage(page: Int): Flow<Result<List<TestViewEntity>>> = result
|
||||||
override fun loadDataForPage(page: Int): Flow<Result<List<Int>>> = result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class TestFailureSource : BasePagingDataSource<Int>() {
|
inner class TestFailureSource : BasePagingDataSource<TestViewEntity>() {
|
||||||
|
|
||||||
val result = flow {
|
val result = flow {
|
||||||
emit(State.Loading())
|
emit(State.Loading())
|
||||||
emit(Failure(GenericError()))
|
emit(Failure(TestFailureReason(errorMessageResId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadDataForPage(page: Int): Flow<Result<List<Int>>> = result
|
override fun loadDataForPage(page: Int): Flow<Result<List<TestViewEntity>>> = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class TestViewEntity(data: Int) : ViewEntity
|
||||||
|
|
||||||
|
inner class TestFailureReason(override val messageRes: Int) : Reason()
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.melih.core.paging
|
package com.melih.core.paging
|
||||||
|
|
||||||
|
import com.melih.abstractions.data.ViewEntity
|
||||||
import com.melih.core.BaseTestWithMainThread
|
import com.melih.core.BaseTestWithMainThread
|
||||||
import com.melih.core.base.paging.BasePagingDataSource
|
import com.melih.core.base.paging.BasePagingDataSource
|
||||||
import com.melih.core.base.paging.BasePagingFactory
|
import com.melih.core.base.paging.BasePagingFactory
|
||||||
@@ -25,9 +26,10 @@ class BasePagingFactoryTest : BaseTestWithMainThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class TestFactory : BasePagingFactory<String>() {
|
inner class TestFactory : BasePagingFactory<TestViewEntity>() {
|
||||||
|
|
||||||
override fun createSource(): BasePagingDataSource<String> = mockk(relaxed = true)
|
|
||||||
|
|
||||||
|
override fun createSource(): BasePagingDataSource<TestViewEntity> = mockk(relaxed = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class TestViewEntity : ViewEntity
|
||||||
}
|
}
|
||||||
|
|||||||
17
data/definitions/build.gradle
Normal file
17
data/definitions/build.gradle
Normal file
@@ -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
|
||||||
|
}
|
||||||
0
data/definitions/consumer-rules.pro
Normal file
0
data/definitions/consumer-rules.pro
Normal file
21
data/definitions/proguard-rules.pro
vendored
Normal file
21
data/definitions/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
3
data/definitions/src/main/AndroidManifest.xml
Normal file
3
data/definitions/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.melih.definitions.entities" />
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.melih.repository
|
package com.melih.definitions
|
||||||
|
|
||||||
internal const val DEFAULT_NAME = "Default name"
|
internal const val DEFAULT_NAME = "Default name"
|
||||||
internal const val EMPTY_STRING = ""
|
internal const val EMPTY_STRING = ""
|
||||||
@@ -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 <T : ViewEntity> getNextLaunches(
|
||||||
|
count: Int,
|
||||||
|
page: Int,
|
||||||
|
mapper: Mapper<LaunchEntity, T>
|
||||||
|
): Result<List<T>>
|
||||||
|
|
||||||
|
suspend fun <T : ViewEntity> getLaunchById(id: Long, mapper: Mapper<LaunchEntity, T>): Result<T>
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.melih.repository.entities
|
package com.melih.definitions.entities
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.melih.repository.entities
|
package com.melih.definitions.entities
|
||||||
|
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.melih.repository.entities
|
package com.melih.definitions.entities
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import com.melih.repository.DEFAULT_NAME
|
import com.melih.definitions.DEFAULT_NAME
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.melih.repository.entities
|
package com.melih.definitions.entities
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import com.melih.repository.DEFAULT_NAME
|
import com.melih.definitions.DEFAULT_NAME
|
||||||
import com.melih.repository.EMPTY_STRING
|
import com.melih.definitions.EMPTY_STRING
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.melih.repository.entities
|
package com.melih.definitions.entities
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import com.melih.repository.DEFAULT_NAME
|
import com.melih.definitions.DEFAULT_NAME
|
||||||
import com.melih.repository.EMPTY_STRING
|
import com.melih.definitions.EMPTY_STRING
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
22
data/interactors/build.gradle
Normal file
22
data/interactors/build.gradle
Normal file
@@ -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
|
||||||
|
}
|
||||||
0
data/interactors/consumer-rules.pro
Normal file
0
data/interactors/consumer-rules.pro
Normal file
21
data/interactors/proguard-rules.pro
vendored
Normal file
21
data/interactors/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
3
data/interactors/src/main/AndroidManifest.xml
Normal file
3
data/interactors/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.melih.interactors" />
|
||||||
@@ -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<T : ViewEntity> @Inject constructor(
|
||||||
|
private val mapper: @JvmSuppressWildcards Mapper<LaunchEntity, T>
|
||||||
|
) : BaseInteractor<T, GetLaunchDetails.Params>() {
|
||||||
|
|
||||||
|
//region Properties
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var launchesSource: LaunchesSource
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Functions
|
||||||
|
|
||||||
|
override suspend fun FlowCollector<Result<T>>.run(params: Params) {
|
||||||
|
emit(launchesSource.getLaunchById(params.id, mapper))
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region Parameters
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val id: Long
|
||||||
|
) : InteractorParameters
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
@@ -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<T : ViewEntity> @Inject constructor(
|
||||||
|
private val mapper: @JvmSuppressWildcards Mapper<LaunchEntity, T>
|
||||||
|
) : BaseInteractor<List<T>, GetLaunches.Params>() {
|
||||||
|
|
||||||
|
//region Properties
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var launchesSource: LaunchesSource
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Functions
|
||||||
|
|
||||||
|
override suspend fun FlowCollector<Result<List<T>>>.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
|
||||||
|
}
|
||||||
@@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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<NetworkInfo>
|
||||||
|
) : 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 <T : ViewEntity> getNextLaunches(
|
||||||
|
count: Int,
|
||||||
|
page: Int, mapper: Mapper<LaunchEntity, T>
|
||||||
|
): Result<List<T>> {
|
||||||
|
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 <T : ViewEntity> getLaunchById(
|
||||||
|
id: Long,
|
||||||
|
mapper: Mapper<LaunchEntity, T>
|
||||||
|
): Result<T> {
|
||||||
|
return launchesDatabase
|
||||||
|
.launchesDao
|
||||||
|
.getLaunchById(id)
|
||||||
|
.takeIf { it != null }
|
||||||
|
?.run {
|
||||||
|
Success(mapper.convert(this))
|
||||||
|
} ?: loadLaunchFromNetwork(id, mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T : ViewEntity> loadLaunchFromNetwork(
|
||||||
|
id: Long,
|
||||||
|
mapper: Mapper<LaunchEntity, T>
|
||||||
|
): Result<T> =
|
||||||
|
safeExecute({
|
||||||
|
apiImpl.getLaunchById(id)
|
||||||
|
}) {
|
||||||
|
mapper.convert(
|
||||||
|
transformRocketImageUrl(it)
|
||||||
|
.saveLaunch()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun List<LaunchEntity>.saveLaunches() = run {
|
||||||
|
launchesDatabase.launchesDao.saveLaunches(this)
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun LaunchEntity.saveLaunch() = run {
|
||||||
|
launchesDatabase.launchesDao.saveLaunch(this)
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T, R> safeExecute(
|
||||||
|
block: () -> Response<T>,
|
||||||
|
transform: (T) -> R
|
||||||
|
) =
|
||||||
|
if (isNetworkConnected) {
|
||||||
|
try {
|
||||||
|
block().extractResponseBody(transform)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Failure(TimeoutError())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Failure(ConnectionError())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T, R> Response<T>.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) =
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
<string name="reason_generic">Something went wrong</string>
|
||||||
|
<string name="reason_persistance_empty">There are no saved launches</string>
|
||||||
<string name="reason_network">Network error</string>
|
<string name="reason_network">Network error</string>
|
||||||
<string name="reason_empty_body">Response is empty</string>
|
<string name="reason_empty_body">Response is empty</string>
|
||||||
<string name="reason_generic">Something went wrong</string>
|
|
||||||
<string name="reason_response">Woops, seems we got a server error</string>
|
<string name="reason_response">Woops, seems we got a server error</string>
|
||||||
<string name="reason_timeout">Server timed out</string>
|
<string name="reason_timeout">Server timed out</string>
|
||||||
<string name="reason_persistance_empty">There are no saved launches</string>
|
|
||||||
<string name="reason_no_network_persistance_empty">Seems there are no data and network</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -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.coVerify
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -10,7 +13,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import org.amshove.kluent.shouldBeInstanceOf
|
import org.amshove.kluent.shouldBeInstanceOf
|
||||||
import org.amshove.kluent.shouldEqualTo
|
import org.amshove.kluent.shouldEqualTo
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.util.*
|
import java.util.ArrayDeque
|
||||||
|
|
||||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
class BaseInteractorTest {
|
class BaseInteractorTest {
|
||||||
20
data/network/build.gradle
Normal file
20
data/network/build.gradle
Normal file
@@ -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
|
||||||
|
}
|
||||||
0
data/network/consumer-rules.pro
Normal file
0
data/network/consumer-rules.pro
Normal file
21
data/network/proguard-rules.pro
vendored
Normal file
21
data/network/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
3
data/network/src/main/AndroidManifest.xml
Normal file
3
data/network/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.melih.network" />
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.melih.repository.network
|
package com.melih.network
|
||||||
|
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.definitions.entities.LaunchEntity
|
||||||
import com.melih.repository.entities.LaunchesEntity
|
import com.melih.definitions.entities.LaunchesEntity
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.melih.repository.network
|
package com.melih.network
|
||||||
|
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.definitions.entities.LaunchEntity
|
||||||
import com.melih.repository.entities.LaunchesEntity
|
import com.melih.definitions.entities.LaunchesEntity
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -14,7 +14,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
internal const val TIMEOUT_DURATION = 7L
|
internal const val TIMEOUT_DURATION = 7L
|
||||||
|
|
||||||
internal class ApiImpl @Inject constructor() : Api {
|
class ApiImpl @Inject constructor() : Api {
|
||||||
|
|
||||||
//region Properties
|
//region Properties
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
apply from: "$rootProject.projectDir/scripts/module.gradle"
|
apply from: "$rootProject.projectDir/scripts/module.gradle"
|
||||||
|
apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -17,15 +18,13 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
|
||||||
implementation libraries.coroutines
|
implementation project(':data:definitions')
|
||||||
implementation libraries.liveDataKTX
|
|
||||||
implementation libraries.retrofit
|
|
||||||
implementation libraries.room
|
|
||||||
implementation libraries.moshiKotlin
|
implementation libraries.moshiKotlin
|
||||||
implementation libraries.okHttpLogger
|
implementation libraries.coroutines
|
||||||
|
implementation libraries.room
|
||||||
|
|
||||||
kapt annotationProcessors.roomCompiler
|
kapt annotationProcessors.roomCompiler
|
||||||
kapt annotationProcessors.moshi
|
|
||||||
|
|
||||||
testImplementation testLibraries.coroutinesCore
|
testImplementation testLibraries.coroutinesCore
|
||||||
testImplementation testLibraries.coroutinesTest
|
testImplementation testLibraries.coroutinesTest
|
||||||
0
data/persistence/consumer-rules.pro
Normal file
0
data/persistence/consumer-rules.pro
Normal file
21
data/persistence/proguard-rules.pro
vendored
Normal file
21
data/persistence/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||||
3
data/persistence/src/main/AndroidManifest.xml
Normal file
3
data/persistence/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.melih.persistence" />
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
package com.melih.repository.persistence
|
package com.melih.persistence
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.definitions.entities.LaunchEntity
|
||||||
import com.melih.repository.persistence.converters.LocationConverter
|
import com.melih.persistence.converters.LocationConverter
|
||||||
import com.melih.repository.persistence.converters.MissionConverter
|
import com.melih.persistence.converters.MissionConverter
|
||||||
import com.melih.repository.persistence.converters.RocketConverter
|
import com.melih.persistence.converters.RocketConverter
|
||||||
import com.melih.repository.persistence.dao.LaunchesDao
|
import com.melih.persistence.dao.LaunchesDao
|
||||||
|
|
||||||
const val DB_NAME = "LaunchesDB"
|
const val DB_NAME = "LaunchesDB"
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ const val DB_NAME = "LaunchesDB"
|
|||||||
RocketConverter::class,
|
RocketConverter::class,
|
||||||
MissionConverter::class
|
MissionConverter::class
|
||||||
)
|
)
|
||||||
internal abstract class LaunchesDatabase : RoomDatabase() {
|
abstract class LaunchesDatabase : RoomDatabase() {
|
||||||
|
|
||||||
//region Companion
|
//region Companion
|
||||||
|
|
||||||
@@ -48,6 +48,6 @@ internal abstract class LaunchesDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
//region Abstractions
|
//region Abstractions
|
||||||
|
|
||||||
internal abstract val launchesDao: LaunchesDao
|
abstract val launchesDao: LaunchesDao
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.melih.repository.persistence.converters
|
package com.melih.persistence.converters
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.melih.repository.persistence.converters
|
package com.melih.persistence.converters
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.melih.repository.persistence.converters
|
package com.melih.persistence.converters
|
||||||
|
|
||||||
import com.melih.repository.entities.LocationEntity
|
import com.melih.definitions.entities.LocationEntity
|
||||||
import com.melih.repository.entities.LocationEntityJsonAdapter
|
import com.melih.definitions.entities.LocationEntityJsonAdapter
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
|
||||||
@@ -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.JsonAdapter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.melih.repository.persistence.converters
|
package com.melih.persistence.converters
|
||||||
|
|
||||||
import com.melih.repository.entities.RocketEntity
|
import com.melih.definitions.entities.RocketEntity
|
||||||
import com.melih.repository.entities.RocketEntityJsonAdapter
|
import com.melih.definitions.entities.RocketEntityJsonAdapter
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.melih.repository.persistence.dao
|
package com.melih.persistence.dao
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.definitions.entities.LaunchEntity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DAO for list of [launches][LaunchEntity]
|
* DAO for list of [launches][LaunchEntity]
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
internal abstract class LaunchesDao {
|
abstract class LaunchesDao {
|
||||||
|
|
||||||
//region Queries
|
//region Queries
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 126 KiB |
@@ -50,9 +50,29 @@ platform :android do
|
|||||||
run_detail_tests()
|
run_detail_tests()
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Runs tests in repository module"
|
desc "Runs tests in abstraction module"
|
||||||
lane :test_repository do
|
lane :test_abstractions do
|
||||||
run_repository_tests()
|
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
|
end
|
||||||
|
|
||||||
# ================ Gradle tasks ================
|
# ================ Gradle tasks ================
|
||||||
@@ -81,7 +101,23 @@ platform :android do
|
|||||||
gradle(task: "features:detail:test --continue")
|
gradle(task: "features:detail:test --continue")
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_repository_tests
|
def run_abstractions_tests
|
||||||
gradle(task: "repository:test --continue")
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,5 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
|
||||||
implementation project(':repository')
|
|
||||||
|
|
||||||
testImplementation testLibraries.coroutinesTest
|
testImplementation testLibraries.coroutinesTest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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<LaunchEntity, LaunchDetailItem>() {
|
||||||
|
|
||||||
|
override fun convert(launchEntity: LaunchEntity) =
|
||||||
|
with(launchEntity) {
|
||||||
|
LaunchDetailItem(
|
||||||
|
id,
|
||||||
|
rocket.imageURL,
|
||||||
|
rocket.name,
|
||||||
|
if (!missions.isNullOrEmpty()) missions[0].description else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,15 @@ package com.melih.detail.di.modules
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import com.melih.abstractions.mapper.Mapper
|
||||||
import com.melih.core.di.keys.ViewModelKey
|
import com.melih.core.di.keys.ViewModelKey
|
||||||
|
import com.melih.definitions.entities.LaunchEntity
|
||||||
import com.melih.detail.ui.DetailFragment
|
import com.melih.detail.ui.DetailFragment
|
||||||
import com.melih.detail.ui.DetailFragmentArgs
|
import com.melih.detail.ui.DetailFragmentArgs
|
||||||
import com.melih.detail.ui.DetailViewModel
|
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.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -21,6 +25,9 @@ abstract class DetailFragmentModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(DetailViewModel::class)
|
@ViewModelKey(DetailViewModel::class)
|
||||||
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
|
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun detailMapper(mapper: LaunchDetailMapper): Mapper<LaunchEntity, LaunchDetailItem>
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
package com.melih.detail.ui
|
package com.melih.detail.ui
|
||||||
|
|
||||||
import androidx.lifecycle.Transformations.map
|
import androidx.lifecycle.Transformations.map
|
||||||
|
import com.melih.abstractions.deliverable.handle
|
||||||
import com.melih.core.base.viewmodel.BaseViewModel
|
import com.melih.core.base.viewmodel.BaseViewModel
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.interactors.GetLaunchDetails
|
||||||
import com.melih.repository.interactors.GetLaunchDetails
|
import com.melih.launches.data.LaunchDetailItem
|
||||||
import com.melih.repository.interactors.base.handle
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DetailViewModel @Inject constructor(
|
class DetailViewModel @Inject constructor(
|
||||||
private val getLaunchDetails: GetLaunchDetails,
|
private val getLaunchDetails: GetLaunchDetails<LaunchDetailItem>,
|
||||||
private val getLaunchDetailsParams: GetLaunchDetails.Params
|
private val getLaunchDetailsParams: GetLaunchDetails.Params
|
||||||
) : BaseViewModel<LaunchEntity>() {
|
) : BaseViewModel<LaunchDetailItem>() {
|
||||||
|
|
||||||
//region Properties
|
//region Properties
|
||||||
|
|
||||||
val rocketName = map(successData) {
|
val rocketName = map(successData) {
|
||||||
it.rocket.name
|
it.rocketName
|
||||||
}
|
}
|
||||||
|
|
||||||
val description = map(successData) {
|
val description = map(successData) {
|
||||||
if (it.missions.isEmpty()) {
|
it.missionDescription
|
||||||
""
|
|
||||||
} else {
|
|
||||||
it.missions[0].description
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageUrl = map(successData) {
|
val imageUrl = map(successData) {
|
||||||
it.rocket.imageURL
|
it.imageUrl
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout
|
<layout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data class="DetailBinding">
|
<data class="DetailBinding">
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="viewModel"
|
name="viewModel"
|
||||||
type="com.melih.detail.ui.DetailViewModel" />
|
type="com.melih.detail.ui.DetailViewModel" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imgRocket"
|
android:id="@+id/imgRocket"
|
||||||
imageUrl="@{viewModel.imageUrl}"
|
imageUrl="@{viewModel.imageUrl}"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="220dp"
|
android:layout_height="220dp"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginLeft="@dimen/padding_standard"
|
android:layout_marginLeft="@dimen/padding_standard"
|
||||||
android:layout_marginTop="@dimen/padding_standard"
|
android:layout_marginTop="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_standard"
|
android:layout_marginEnd="@dimen/padding_standard"
|
||||||
android:layout_marginRight="@dimen/padding_standard"
|
android:layout_marginRight="@dimen/padding_standard"
|
||||||
android:contentDescription="@string/cd_rocket_image"
|
android:contentDescription="@string/cd_rocket_image"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:srcCompat="@tools:sample/avatars[14]" />
|
tools:srcCompat="@tools:sample/avatars[14]" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/tvTitle"
|
android:id="@+id/tvTitle"
|
||||||
style="@style/AppTheme.TextViewStyle.Title"
|
style="@style/AppTheme.TextViewStyle.Title"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginLeft="@dimen/padding_standard"
|
android:layout_marginLeft="@dimen/padding_standard"
|
||||||
android:layout_marginTop="@dimen/padding_standard"
|
android:layout_marginTop="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_standard"
|
android:layout_marginEnd="@dimen/padding_standard"
|
||||||
android:layout_marginRight="@dimen/padding_standard"
|
android:layout_marginRight="@dimen/padding_standard"
|
||||||
android:text="@{viewModel.rocketName}"
|
android:text="@{viewModel.rocketName}"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
|
app:layout_constraintTop_toBottomOf="@+id/imgRocket"
|
||||||
tools:text="@sample/launches.json/launches/name" />
|
tools:text="@sample/launches.json/launches/name" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/tvDescription"
|
android:id="@+id/tvDescription"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginLeft="@dimen/padding_standard"
|
android:layout_marginLeft="@dimen/padding_standard"
|
||||||
android:layout_marginTop="@dimen/padding_standard"
|
android:layout_marginTop="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_standard"
|
android:layout_marginEnd="@dimen/padding_standard"
|
||||||
android:layout_marginRight="@dimen/padding_standard"
|
android:layout_marginRight="@dimen/padding_standard"
|
||||||
android:layout_marginBottom="@dimen/padding_standard"
|
android:layout_marginBottom="@dimen/padding_standard"
|
||||||
android:text="@{viewModel.description}"
|
android:text="@{viewModel.description}"
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||||
tools:text="@sample/launches.json/launches/missions/description" />
|
tools:text="@sample/launches.json/launches/missions/description" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.melih.detail
|
package com.melih.detail
|
||||||
|
|
||||||
import com.melih.detail.ui.DetailViewModel
|
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.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
@@ -19,7 +20,7 @@ import org.junit.jupiter.api.Test
|
|||||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
class DetailViewModelTest : BaseTestWithMainThread() {
|
class DetailViewModelTest : BaseTestWithMainThread() {
|
||||||
|
|
||||||
private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true)
|
private val getLaunchDetails: GetLaunchDetails<LaunchDetailItem> = mockk(relaxed = true)
|
||||||
private val getLaunchDetailsParams = GetLaunchDetails.Params(1013)
|
private val getLaunchDetailsParams = GetLaunchDetails.Params(1013)
|
||||||
|
|
||||||
private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams))
|
private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams))
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ apply from: "$rootProject.projectDir/scripts/feature_module.gradle"
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
|
||||||
implementation project(':repository')
|
|
||||||
|
|
||||||
implementation libraries.paging
|
implementation libraries.paging
|
||||||
implementation libraries.swipeRefreshLayout
|
implementation libraries.swipeRefreshLayout
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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<LaunchEntity, LaunchItem>() {
|
||||||
|
|
||||||
|
override fun convert(launchEntity: LaunchEntity) =
|
||||||
|
with(launchEntity) {
|
||||||
|
LaunchItem(
|
||||||
|
id,
|
||||||
|
rocket.imageURL,
|
||||||
|
rocket.name,
|
||||||
|
if (!missions.isNullOrEmpty()) missions[0].description else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,14 @@ package com.melih.launches.di.modules
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.paging.Config
|
import androidx.paging.Config
|
||||||
|
import com.melih.abstractions.mapper.Mapper
|
||||||
import com.melih.core.di.keys.ViewModelKey
|
import com.melih.core.di.keys.ViewModelKey
|
||||||
|
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 com.melih.launches.ui.vm.LaunchesViewModel
|
||||||
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
|
|
||||||
import com.melih.repository.interactors.GetLaunches
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -14,12 +18,15 @@ import dagger.multibindings.IntoMap
|
|||||||
@Module
|
@Module
|
||||||
abstract class LaunchesFragmentModule {
|
abstract class LaunchesFragmentModule {
|
||||||
|
|
||||||
//region ViewModels
|
//region Binds
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(LaunchesViewModel::class)
|
@ViewModelKey(LaunchesViewModel::class)
|
||||||
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
|
abstract fun launchesViewModel(listViewModel: LaunchesViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun launchMapper(mapper: LaunchMapper): Mapper<LaunchEntity, LaunchItem>
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.melih.abstractions.deliverable.State
|
||||||
import com.melih.core.actions.openDetail
|
import com.melih.core.actions.openDetail
|
||||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||||
import com.melih.core.extensions.observe
|
import com.melih.core.extensions.observe
|
||||||
|
import com.melih.interactors.error.PersistenceEmptyError
|
||||||
import com.melih.launches.R
|
import com.melih.launches.R
|
||||||
import com.melih.launches.databinding.ListBinding
|
import com.melih.launches.data.LaunchItem
|
||||||
|
import com.melih.launches.databinding.LaunchesBinding
|
||||||
import com.melih.launches.ui.adapters.LaunchesAdapter
|
import com.melih.launches.ui.adapters.LaunchesAdapter
|
||||||
import com.melih.launches.ui.vm.LaunchesViewModel
|
import com.melih.launches.ui.vm.LaunchesViewModel
|
||||||
import com.melih.repository.entities.LaunchEntity
|
|
||||||
import com.melih.repository.interactors.base.PersistenceEmpty
|
|
||||||
import com.melih.repository.interactors.base.State
|
|
||||||
|
|
||||||
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
|
class LaunchesFragment : BaseDaggerFragment<LaunchesBinding>(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
|
||||||
//region Properties
|
//region Properties
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
|
|||||||
|
|
||||||
// Observing error to show toast with retry action
|
// Observing error to show toast with retry action
|
||||||
observe(viewModel.errorData) {
|
observe(viewModel.errorData) {
|
||||||
if (it !is PersistenceEmpty) {
|
if (it !is PersistenceEmptyError) {
|
||||||
showSnackbarWithAction(it) {
|
showSnackbarWithAction(it) {
|
||||||
viewModel.retry()
|
viewModel.retry()
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.O
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemSelected(item: LaunchEntity) {
|
private fun onItemSelected(item: LaunchItem) {
|
||||||
openDetail(item.id)
|
openDetail(item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import android.view.ViewGroup
|
|||||||
import com.melih.core.base.recycler.BasePagingListAdapter
|
import com.melih.core.base.recycler.BasePagingListAdapter
|
||||||
import com.melih.core.base.recycler.BaseViewHolder
|
import com.melih.core.base.recycler.BaseViewHolder
|
||||||
import com.melih.core.extensions.createDiffCallback
|
import com.melih.core.extensions.createDiffCallback
|
||||||
|
import com.melih.launches.data.LaunchItem
|
||||||
import com.melih.launches.databinding.LaunchRowBinding
|
import com.melih.launches.databinding.LaunchRowBinding
|
||||||
import com.melih.repository.entities.LaunchEntity
|
|
||||||
|
|
||||||
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter<LaunchEntity>(
|
class LaunchesAdapter(itemClickListener: (LaunchItem) -> Unit) : BasePagingListAdapter<LaunchItem>(
|
||||||
createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
|
createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id },
|
||||||
itemClickListener
|
itemClickListener
|
||||||
) {
|
) {
|
||||||
@@ -19,21 +19,18 @@ class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingLis
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): BaseViewHolder<LaunchEntity> =
|
): BaseViewHolder<LaunchItem> =
|
||||||
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
|
LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false))
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
|
class LaunchesViewHolder(private val binding: LaunchRowBinding) :
|
||||||
|
BaseViewHolder<LaunchItem>(binding) {
|
||||||
|
|
||||||
//region Functions
|
//region Functions
|
||||||
|
|
||||||
override fun bind(item: LaunchEntity) {
|
override fun bind(item: LaunchItem) {
|
||||||
binding.entity = item
|
binding.entity = item
|
||||||
|
|
||||||
val missions = item.missions
|
|
||||||
binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else ""
|
|
||||||
|
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
package com.melih.launches.ui.paging
|
package com.melih.launches.ui.paging
|
||||||
|
|
||||||
import com.melih.core.base.paging.BasePagingDataSource
|
import com.melih.core.base.paging.BasePagingDataSource
|
||||||
import com.melih.repository.entities.LaunchEntity
|
import com.melih.interactors.GetLaunches
|
||||||
import com.melih.repository.interactors.GetLaunches
|
import com.melih.launches.data.LaunchItem
|
||||||
import com.melih.repository.interactors.base.Result
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses [GetLaunches] to get data for pagination
|
* Uses [GetLaunches] to get data for pagination
|
||||||
*/
|
*/
|
||||||
class LaunchesPagingSource @Inject constructor(
|
class LaunchesPagingSource @Inject constructor(
|
||||||
private val getLaunches: GetLaunches,
|
private val getLaunches: GetLaunches<LaunchItem>,
|
||||||
private val getLaunchesParams: GetLaunches.Params
|
private val getLaunchesParams: GetLaunches.Params
|
||||||
) : BasePagingDataSource<LaunchEntity>() {
|
) : BasePagingDataSource<LaunchItem>() {
|
||||||
|
|
||||||
//region Functions
|
//region Functions
|
||||||
|
|
||||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
override fun loadDataForPage(page: Int): Flow<Result<List<LaunchEntity>>> =
|
override fun loadDataForPage(page: Int) =
|
||||||
getLaunches(
|
getLaunches(
|
||||||
getLaunchesParams.copy(
|
getLaunchesParams.copy(
|
||||||
page = page
|
page = page
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ package com.melih.launches.ui.paging
|
|||||||
|
|
||||||
import com.melih.core.base.paging.BasePagingDataSource
|
import com.melih.core.base.paging.BasePagingDataSource
|
||||||
import com.melih.core.base.paging.BasePagingFactory
|
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.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
class LaunchesPagingSourceFactory @Inject constructor(
|
class LaunchesPagingSourceFactory @Inject constructor(
|
||||||
private val sourceProvider: Provider<LaunchesPagingSource>
|
private val sourceProvider: Provider<LaunchesPagingSource>
|
||||||
) : BasePagingFactory<LaunchEntity>() {
|
) : BasePagingFactory<LaunchItem>() {
|
||||||
|
|
||||||
//region Functions
|
//region Functions
|
||||||
|
|
||||||
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
|
override fun createSource(): BasePagingDataSource<LaunchItem> = sourceProvider.get()
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package com.melih.launches.ui.vm
|
|||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import com.melih.core.base.paging.BasePagingFactory
|
import com.melih.core.base.paging.BasePagingFactory
|
||||||
import com.melih.core.base.viewmodel.BasePagingViewModel
|
import com.melih.core.base.viewmodel.BasePagingViewModel
|
||||||
|
import com.melih.launches.data.LaunchItem
|
||||||
import com.melih.launches.ui.paging.LaunchesPagingSourceFactory
|
import com.melih.launches.ui.paging.LaunchesPagingSourceFactory
|
||||||
import com.melih.repository.entities.LaunchEntity
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LaunchesViewModel @Inject constructor(
|
class LaunchesViewModel @Inject constructor(
|
||||||
private val launchesPagingSourceFactory: LaunchesPagingSourceFactory,
|
private val launchesPagingSourceFactory: LaunchesPagingSourceFactory,
|
||||||
private val launchesPagingConfig: PagedList.Config
|
private val launchesPagingConfig: PagedList.Config
|
||||||
) : BasePagingViewModel<LaunchEntity>() {
|
) : BasePagingViewModel<LaunchItem>() {
|
||||||
|
|
||||||
//region Properties
|
//region Properties
|
||||||
|
|
||||||
override val factory: BasePagingFactory<LaunchEntity>
|
override val factory: BasePagingFactory<LaunchItem>
|
||||||
get() = launchesPagingSourceFactory
|
get() = launchesPagingSourceFactory
|
||||||
|
|
||||||
override val config: PagedList.Config
|
override val config: PagedList.Config
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data class="ListBinding">
|
<data class="LaunchesBinding">
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="viewModel"
|
name="viewModel"
|
||||||
|
|||||||
@@ -1,69 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout
|
<layout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data class="LaunchRowBinding">
|
<data class="LaunchRowBinding">
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="entity"
|
name="entity"
|
||||||
type="com.melih.repository.entities.LaunchEntity" />
|
type="com.melih.launches.data.LaunchItem" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginLeft="@dimen/padding_standard"
|
android:layout_marginLeft="@dimen/padding_standard"
|
||||||
android:layout_marginTop="@dimen/padding_standard"
|
android:layout_marginTop="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_standard"
|
android:layout_marginEnd="@dimen/padding_standard"
|
||||||
android:layout_marginRight="@dimen/padding_standard"
|
android:layout_marginRight="@dimen/padding_standard"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
app:cardElevation="10dp">
|
app:cardElevation="10dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="160dp">
|
android:layout_height="160dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imgRocket"
|
android:id="@+id/imgRocket"
|
||||||
imageUrl="@{entity.rocket.imageURL}"
|
imageUrl="@{entity.imageUrl}"
|
||||||
android:layout_width="120dp"
|
android:layout_width="120dp"
|
||||||
android:layout_height="120dp"
|
android:layout_height="120dp"
|
||||||
android:layout_marginStart="@dimen/padding_large"
|
android:layout_marginStart="@dimen/padding_large"
|
||||||
android:contentDescription="@string/cd_rocket_image"
|
android:contentDescription="@string/cd_rocket_image"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:srcCompat="@tools:sample/avatars[14]" />
|
tools:srcCompat="@tools:sample/avatars[14]" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/tvTitle"
|
android:id="@+id/tvTitle"
|
||||||
style="@style/AppTheme.TextViewStyle.Title"
|
style="@style/AppTheme.TextViewStyle.Title"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_large"
|
android:layout_marginEnd="@dimen/padding_large"
|
||||||
android:text="@{entity.name}"
|
android:text="@{entity.rocketName}"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||||
app:layout_constraintTop_toTopOf="@+id/imgRocket"
|
app:layout_constraintTop_toTopOf="@+id/imgRocket"
|
||||||
tools:text="@sample/launches.json/launches/name" />
|
tools:text="@sample/launches.json/launches/name" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/tvDescription"
|
android:id="@+id/tvDescription"
|
||||||
style="@style/AppTheme.TextViewStyle.Description"
|
style="@style/AppTheme.TextViewStyle.Description"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/padding_standard"
|
android:layout_marginStart="@dimen/padding_standard"
|
||||||
android:layout_marginEnd="@dimen/padding_large"
|
android:layout_marginEnd="@dimen/padding_large"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
|
android:text="@{entity.missionDescription}"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintBottom_toBottomOf="@+id/imgRocket"
|
||||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||||
tools:text="@sample/launches.json/launches/missions/description" />
|
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
tools:text="@sample/launches.json/launches/missions/description" />
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
1
repository/.gitignore
vendored
1
repository/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<manifest package="com.melih.repository" />
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.melih.repository
|
|
||||||
|
|
||||||
import com.melih.repository.entities.LaunchEntity
|
|
||||||
import com.melih.repository.interactors.base.Result
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract for sources to seperate low level business logic from build and return type
|
|
||||||
*/
|
|
||||||
abstract class Repository {
|
|
||||||
|
|
||||||
//region Abstractions
|
|
||||||
|
|
||||||
internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result<List<LaunchEntity>>
|
|
||||||
internal abstract suspend fun getLaunchById(id: Long): Result<LaunchEntity>
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
@@ -1,55 +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<LaunchEntity, GetLaunchDetails.Params>() {
|
|
||||||
|
|
||||||
//region Properties
|
|
||||||
|
|
||||||
@field:Inject
|
|
||||||
internal lateinit var networkSource: NetworkSource
|
|
||||||
|
|
||||||
@field:Inject
|
|
||||||
internal lateinit var persistenceSource: PersistenceSource
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Functions
|
|
||||||
|
|
||||||
override suspend fun FlowCollector<Result<LaunchEntity>>.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
data class Params(
|
|
||||||
val id: Long
|
|
||||||
) : InteractorParameters
|
|
||||||
}
|
|
||||||
@@ -1,54 +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<List<LaunchEntity>, GetLaunches.Params>() {
|
|
||||||
|
|
||||||
//region Properties
|
|
||||||
|
|
||||||
@field:Inject
|
|
||||||
internal lateinit var networkSource: NetworkSource
|
|
||||||
|
|
||||||
@field:Inject
|
|
||||||
internal lateinit var persistenceSource: PersistenceSource
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Functions
|
|
||||||
|
|
||||||
override suspend fun FlowCollector<Result<List<LaunchEntity>>>.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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
data class Params(
|
|
||||||
val count: Int = DEFAULT_LAUNCHES_AMOUNT,
|
|
||||||
val page: Int
|
|
||||||
) : InteractorParameters
|
|
||||||
}
|
|
||||||
@@ -1,21 +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)
|
|
||||||
|
|
||||||
//region Subclasses
|
|
||||||
|
|
||||||
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)
|
|
||||||
//endregion
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package com.melih.repository.sources
|
|
||||||
|
|
||||||
import android.net.NetworkInfo
|
|
||||||
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
|
|
||||||
|
|
||||||
private const val DEFAULT_IMAGE_SIZE = 480
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<NetworkInfo>
|
|
||||||
) : 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<List<LaunchEntity>> =
|
|
||||||
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<LaunchEntity> =
|
|
||||||
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 inline fun <T, R> safeExecute(
|
|
||||||
block: () -> Response<T>,
|
|
||||||
transform: (T) -> R
|
|
||||||
) =
|
|
||||||
if (isNetworkConnected) {
|
|
||||||
try {
|
|
||||||
block().extractResponseBody(transform)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Failure(TimeoutError())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Failure(NetworkError())
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <T, R> Response<T>.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
|
|
||||||
}
|
|
||||||
@@ -1,50 +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<List<LaunchEntity>> =
|
|
||||||
launchesDatabase
|
|
||||||
.launchesDao
|
|
||||||
.getLaunches(count, page)
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.run {
|
|
||||||
Success(this)
|
|
||||||
} ?: Failure(PersistenceEmpty())
|
|
||||||
|
|
||||||
override suspend fun getLaunchById(id: Long): Result<LaunchEntity> =
|
|
||||||
launchesDatabase
|
|
||||||
.launchesDao
|
|
||||||
.getLaunchById(id)
|
|
||||||
.takeIf { it != null }
|
|
||||||
?.run {
|
|
||||||
Success(this)
|
|
||||||
} ?: Failure(PersistenceEmpty())
|
|
||||||
|
|
||||||
internal suspend fun saveLaunches(launches: List<LaunchEntity>) {
|
|
||||||
launchesDatabase.launchesDao.saveLaunches(launches)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun saveLaunch(launch: LaunchEntity) {
|
|
||||||
launchesDatabase.launchesDao.saveLaunch(launch)
|
|
||||||
}
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
@@ -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<ApiImpl>(relaxed = true)
|
|
||||||
private val networkInfoProvider = mockk<Provider<NetworkInfo>>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Context>(relaxed = true)
|
|
||||||
private val dbImplementation = mockk<LaunchesDatabase>(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
53
scripts/cq/jacoco.gradle
Normal file
53
scripts/cq/jacoco.gradle
Normal file
@@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
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 {
|
android {
|
||||||
compileSdkVersion versions.compileSdkVersion
|
compileSdkVersion versions.compileSdkVersion
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
apply plugin: "de.mannodermaus.android-junit5"
|
apply plugin: "de.mannodermaus.android-junit5"
|
||||||
|
|
||||||
apply from: "$rootProject.projectDir/scripts/detekt.gradle"
|
|
||||||
apply from: "$rootProject.projectDir/scripts/dokka.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
|
||||||
|
implementation project(':abstractions')
|
||||||
|
|
||||||
implementation libraries.kotlin
|
implementation libraries.kotlin
|
||||||
implementation libraries.dagger
|
implementation libraries.dagger
|
||||||
implementation libraries.timber
|
implementation libraries.timber
|
||||||
@@ -18,57 +17,3 @@ dependencies {
|
|||||||
|
|
||||||
testRuntimeOnly testLibraries.jUnitEngine
|
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")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user