Skip to content

Commit

Permalink
Merge pull request #54 from wednesday-solutions/open-weather-api
Browse files Browse the repository at this point in the history
Gradle upgrade, Compose Dependencies and Switch Weather API
shounak-mulay authored May 31, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 56fa866 + b27389a commit f17b702
Showing 137 changed files with 2,287 additions and 1,323 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -24,6 +24,10 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup local.properties
env:
PROPS: ${{ secrets.LOCAL_PROPERTIES }}
run: echo -n "$PROPS" | base64 --decode > local.properties
- name: Build the Release APK
run: |
bash scripts/actions/build_file_according_to_flavour.sh
31 changes: 8 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -12,10 +12,9 @@ env:


jobs:
lint:
name: Lint
lint_test_build:
name: Lint, Test and Build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
@@ -30,30 +29,16 @@ jobs:
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup local.properties
env:
PROPS: ${{ secrets.LOCAL_PROPERTIES }}
run: echo -n "$PROPS" | base64 --decode > local.properties
- name: Ktlint
run: ./gradlew ktlint
- name: Lint
run: ./gradlew lintRelease

unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
run: ./gradlew lintDevRelease
- name: Unit tests
run: ./gradlew testDebugUnitTest
run: ./gradlew testDevDebugUnitTest
- name: Build the apk
run: ./gradlew assembleDevDebug

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -41,6 +41,12 @@
- Run the app by pressing the run button in android studio or by pressing `control + R`.
- Go through and setup the scripts in the [scripts](https://github.com/wednesday-solutions/android-template/tree/master/scripts) directory.

#### App Secrets
Sensitive information such as the api keys is managed via `local.properties` file. This file in not checked into version control to keep the sensitive information safe. If you want to run the project locally you need to add your own api keys.
Look at the [`local.skeleton.properties`](local.skeleton.properties) file for all the keys you need to include in your `local.properties` file.

You can get the Open Weather API key from [openweathermap.org](https://openweathermap.org/appid).

## Architecture
The architecture of the template facilitates separation of concerns and avoids tight coupling between it's various layers. The goal is to have the ability to make changes to individual layers without affecting the entire app. This architecture is an adaptation of concepts from [`The Clean Architecture`](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).

29 changes: 20 additions & 9 deletions android.gradle
Original file line number Diff line number Diff line change
@@ -8,17 +8,28 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
// coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
flavorDimensions "version"
productFlavors {
qa {
dimension "version"
}
prod {
dimension "version"
}
dev {
dimension "version"
}
}

Properties props = new Properties()
props.load(new FileInputStream(new File('local.properties')))

buildTypes {
release {}
debug {}
release {
buildConfigField 'String', 'OPEN_WEATHER_API_KEY', props['OPEN_WEATHER_API_KEY']
}
debug {
buildConfigField 'String', 'OPEN_WEATHER_API_KEY', props['DEBUG_OPEN_WEATHER_API_KEY']
}
}
}
53 changes: 31 additions & 22 deletions app/app.gradle.kts
Original file line number Diff line number Diff line change
@@ -2,40 +2,49 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
kotlin("android")
}

apply {
from("${rootProject.projectDir}/android.gradle")
from("${rootProject.projectDir}/lint.gradle")
}

android {

compileSdk = 31
buildToolsVersion = "30.0.3"

defaultConfig {
minSdk = 24
targetSdk = 31
applicationId = "com.wednesday.template"
versionCode = 7
versionName = "1.0"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
flavorDimensions("version")

flavorDimensions += "version"
productFlavors {
create("qa") {
dimension("version")
dimension = "version"
applicationIdSuffix = ".qa"
versionNameSuffix = "-qa"
}
create("prod") {
dimension("version")
dimension = "version"
applicationIdSuffix = ".prod"
versionNameSuffix = "-prod"
}
create("dev") {
dimension("version")
dimension = "version"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
}
}

buildTypes {
getByName("release") {
minifyEnabled(false)
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"${project.rootDir}/tools/proguard-rules.pro"
@@ -46,16 +55,16 @@ android {
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val flavour = variant.flavorName
val builtType = variant.buildType.name
val versionName = variant.versionName
val vCode = variant.versionCode
output.outputFileName =
"app-${flavour}-${builtType}-${versionName}(${vCode}).apk".replace(
"-${flavour}",
""
)
}
val flavour = variant.flavorName
val builtType = variant.buildType.name
val versionName = variant.versionName
val vCode = variant.versionCode
output.outputFileName =
"app-${flavour}-${builtType}-${versionName}(${vCode}).apk".replace(
"-${flavour}",
""
)
}
}
}
}
@@ -70,14 +79,14 @@ dependencies {
implementation(project(":repo-di"))
implementation(project(":service-di"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.koinCore)
implementation(Dependencies.koinAndroid)
implementation(Dependencies.Koin.core)
implementation(Dependencies.Koin.android)

implementation(Dependencies.material)
implementation(Dependencies.Material.material)

implementation(Dependencies.loggingTimber)
implementation(Dependencies.Logging.timber)

implementation(Dependencies.androidSplashScreen)
implementation(Dependencies.Android.splashScreen)
}
Original file line number Diff line number Diff line change
@@ -32,5 +32,4 @@ class AndroidTemplateApplication : Application() {
)
}
}

}
39 changes: 0 additions & 39 deletions build.gradle

This file was deleted.

33 changes: 33 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}

dependencies {
classpath(Dependencies.Android.gradlePlugin)
classpath(Dependencies.Google.services)
classpath(Dependencies.Kotlin.gradlePlugin)
classpath(Dependencies.Kotlin.serializationPlugin)
classpath(Dependencies.Android.navigationSafeArgsPlugin)
}
}

allprojects {
repositories {
google()
mavenCentral()
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
}

// TODO: Remove once ExperimentalCoroutinesApi: runTest is stable
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
107 changes: 0 additions & 107 deletions buildSrc/src/main/java/Dependencies.kt

This file was deleted.

120 changes: 120 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
object Dependencies {

object Compose {
const val activity = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val material = "androidx.compose.material:material:${Versions.compose}"
const val materialIconCore =
"androidx.compose.material:material-icons-core:${Versions.compose}"
const val materialIconExtended =
"androidx.compose.material:material-icons-extended:${Versions.compose}"
const val foundation = "androidx.compose.foundation:foundation:${Versions.compose}"
const val runtime = "androidx.compose.runtime:runtime:${Versions.compose}"
const val liveData = "androidx.compose.runtime:runtime-livedata:${Versions.compose}"
const val animation = "androidx.compose.animation:animation:${Versions.compose}"
const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}"
const val viewModel =
"androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.lifecycleViewModelCompose}"
const val uiTest = "androidx.compose.ui:ui-test-junit4:${Versions.compose}"
}

object Room {
const val runtime = "androidx.room:room-runtime:${Versions.room}"
const val ktx = "androidx.room:room-ktx:${Versions.room}"
const val compiler = "androidx.room:room-compiler:${Versions.room}"
}

object Kotlin {
const val stdLib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
const val serialization =
"org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}"
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val serializationPlugin = "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}"
}

object Coroutines {
const val core =
"org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val android =
"org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
}

object Koin {
const val core = "io.insert-koin:koin-core:${Versions.koin}"
const val android = "io.insert-koin:koin-android:${Versions.koin}"
const val workManager = "io.insert-koin:koin-androidx-workmanager:${Versions.koin}"
const val test = "io.insert-koin:koin-test:${Versions.koin}"
const val navigation = "io.insert-koin:koin-androidx-navigation:${Versions.koin}"
const val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}"
}

object Material {
const val material = "com.google.android.material:material:${Versions.material}"
}

object Google {
const val services = "com.google.gms:google-services:${Versions.googleServices}"
}

object Android {
const val gradlePlugin = "com.android.tools.build:gradle:${Versions.androidGradlePlugin}"
const val coreKtx = "androidx.core:core-ktx:${Versions.core}"
const val fragment = "androidx.fragment:fragment:${Versions.fragment}"
const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompat}"
const val constraintLayout =
"androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}"
const val recyclerView =
"androidx.recyclerview:recyclerview:${Versions.recyclerView}"
const val lifecycleCompiler =
"androidx.lifecycle:lifecycle-compiler:${Versions.lifecycle}"
const val recyclerViewSelection =
"androidx.recyclerview:recyclerview-selection:${Versions.recyclerViewSelection}"
const val lifecycleLiveDataCore =
"androidx.lifecycle:lifecycle-livedata-core:${Versions.lifecycle}"
const val lifecycleLiveDataKtx =
"androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle}"
const val lifecycleViewModelKtx =
"androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}"
const val lifecycleViewModel =
"androidx.lifecycle:lifecycle-viewmodel:${Versions.lifecycle}"
const val lifecycleRuntimeKtx =
"androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val navigationFragment =
"androidx.navigation:navigation-fragment-ktx:${Versions.nav}"
const val navigationUi = "androidx.navigation:navigation-ui-ktx:${Versions.nav}"
const val navigationSafeArgsPlugin = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.nav}"
const val splashScreen = "androidx.core:core-splashscreen:${Versions.splash}"
}

object Retrofit {
const val core = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
const val serialization =
"com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:${Versions.retrofitKotlinxSerializationConverter}"
const val logging =
"com.squareup.okhttp3:logging-interceptor:${Versions.retrofitLoggingInterceptor}"
}

object Logging {
const val timber = "com.jakewharton.timber:timber:${Versions.timber}"
const val chucker = "com.github.chuckerteam.chucker:library:${Versions.chucker}"
}

object Image {
const val coil = "io.coil-kt:coil:${Versions.coil}"
}

object Test {
const val androidxTestCore = "androidx.test:runner:1.3.0"
const val androidxArchCore = "androidx.arch.core:core-testing:2.1.0"
const val androidxTestRunner = "androidx.test:runner:1.3.0"
const val androidxTestRules = "androidx.test:rules:1.3.0"
const val androidxExt = "androidx.test.ext:junit:1.1.2"
const val androidxCoreTesting = "androidx.arch.core:core-testing:2.1.0"
const val androidxRoomTesting = "androidx.room:room-testing:2.2.5"
const val junit = "junit:junit:4.13.2"
const val coroutines =
"org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
const val flowTest = "app.cash.turbine:turbine:${Versions.turbine}"
const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}"
const val mockito = "org.mockito.kotlin:mockito-kotlin:3.2.0"
}
}
32 changes: 32 additions & 0 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
object Versions {
const val androidGradlePlugin = "7.2.0"
const val googleServices = "4.3.10"
const val compose = "1.1.1"
const val activityCompose = "1.4.0"
const val lifecycleViewModelCompose = "2.4.1"
const val kotlin = "1.6.10"
const val kotlinxSerialization = "1.3.2"
const val ktlint = "0.45.2"
const val coroutines = "1.6.1"
const val koin = "3.2.0-beta-1"
const val fragment = "1.3.5"
const val lifecycle = "2.4.1"
const val core = "1.7.0"
const val jUnit = "4.12"
const val androidxTest = "1.4.0"
const val retrofit = "2.9.0"
const val nav = "2.4.2"
const val room = "2.4.2"
const val splash = "1.0.0-beta02"
const val material = "1.6.0"
const val appCompat = "1.4.1"
const val constraintLayout = "2.0.4"
const val recyclerView = "1.2.1"
const val recyclerViewSelection = "1.2.0-alpha01"
const val retrofitKotlinxSerializationConverter = "0.8.0"
const val retrofitLoggingInterceptor = "4.9.3"
const val timber = "5.0.1"
const val chucker = "3.5.2"
const val turbine = "0.8.0"
const val coil = "2.1.0"
}
2 changes: 1 addition & 1 deletion domain-di/domain-di.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,5 +13,5 @@ dependencies {
implementation(project(":repo"))
implementation(project(":domain-impl"))

implementation(Dependencies.koinCore)
implementation(Dependencies.Koin.core)
}
8 changes: 4 additions & 4 deletions domain-entity/domain-entity.gradle.kts
Original file line number Diff line number Diff line change
@@ -10,9 +10,9 @@ apply {

dependencies {

implementation(Dependencies.androidCoreKtx)
implementation(Dependencies.androidAppCompat)
implementation(Dependencies.material)
implementation(Dependencies.Android.coreKtx)
implementation(Dependencies.Android.appCompat)
implementation(Dependencies.Material.material)

testImplementation(Dependencies.testJunit)
testImplementation(Dependencies.Test.junit)
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ package com.wednesday.template.domain.weather
data class City(
val id: Int,
val title: String,
val locationType: String,
val latitude: String
val country: String,
val state: String?,
val lat: Double,
val lon: Double
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,6 +2,12 @@ package com.wednesday.template.domain.weather

data class Weather(
val title: String,
val woeid: Int,
val dayWeatherList: List<DayWeather>
val description: String,
val lat: Double,
val lon: Double,
val minTemp: Double,
val maxTemp: Double,
val temp: Double,
val feelsLike: Double,
val iconUrl: String
)
16 changes: 8 additions & 8 deletions domain-impl/domain-impl.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,14 +13,14 @@ dependencies {
implementation(project(":domain-entity"))
implementation(project(":repo"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.Kotlin.stdLib)
implementation(Dependencies.Coroutines.core)

implementation(Dependencies.loggingTimber)
implementation(Dependencies.Logging.timber)

testImplementation(Dependencies.testJunit)
testImplementation(Dependencies.testKotlinTest)
testImplementation(Dependencies.testMockito)
testImplementation(Dependencies.testFlowTest)
testImplementation(Dependencies.testCoroutines)
testImplementation(Dependencies.Test.junit)
testImplementation(Dependencies.Test.kotlinTest)
testImplementation(Dependencies.Test.mockito)
testImplementation(Dependencies.Test.flowTest)
testImplementation(Dependencies.Test.coroutines)
}
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@ package com.wednesday.template.domain.weather
import com.wednesday.template.domain.TestException
import com.wednesday.template.domain.base.Result
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -12,6 +13,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
class FetchFavouriteCitiesWeatherUseCaseImplTest {

private lateinit var weatherRepo: WeatherRepository
@@ -24,7 +26,7 @@ class FetchFavouriteCitiesWeatherUseCaseImplTest {
}

@Test
fun `Given fetch is successful, When invoke is called, Then Success is returned`() = runBlocking {
fun `Given fetch is successful, When invoke is called, Then Success is returned`() = runTest {
// Given

// When
@@ -37,7 +39,7 @@ class FetchFavouriteCitiesWeatherUseCaseImplTest {
}

@Test
fun `Given fetch is not successful, When invoke is called, Then Error is returned`() = runBlocking {
fun `Given fetch is not successful, When invoke is called, Then Error is returned`() = runTest {
// Given
val testException = TestException()
whenever(weatherRepo.fetchWeatherForFavouriteCities()).thenThrow(testException)
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ import app.cash.turbine.test
import com.wednesday.template.domain.base.Result
import com.wednesday.template.domain.weather.models.city
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -14,6 +15,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalCoroutinesApi::class)
@ExperimentalTime
class GetFavouriteCitiesFlowUseCaseImplTest {

@@ -28,7 +30,7 @@ class GetFavouriteCitiesFlowUseCaseImplTest {

@Test
fun `Given repository returns flow, When invoke called, Then flow of cities list is returned`() =
runBlocking {
runTest {
// Given
val cities = listOf(city)
whenever(weatherRepository.getFavouriteCitiesFlow()).thenReturn(flowOf(cities))
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ import app.cash.turbine.test
import com.wednesday.template.domain.base.Result
import com.wednesday.template.domain.weather.models.weather
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -17,6 +18,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalCoroutinesApi::class)
@ExperimentalTime
class GetFavouriteCitiesWeatherFlowUseCaseImplTest {

@@ -32,7 +34,7 @@ class GetFavouriteCitiesWeatherFlowUseCaseImplTest {

@Test
fun `Given flow from repo, When invoke is called, Then flow of weather list is returned`(): Unit =
runBlocking {
runTest {
// Given
val weatherList = listOf(weather)
whenever(weatherRepository.getFavouriteCitiesWeatherFlow())
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@ import com.wednesday.template.domain.TestException
import com.wednesday.template.domain.base.Result
import com.wednesday.template.domain.weather.models.city
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -15,6 +16,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import kotlin.test.assertTrue

@OptIn(ExperimentalCoroutinesApi::class)
class RemoveCityFavouriteUseCaseImplTest {

private lateinit var weatherRepository: WeatherRepository
@@ -28,7 +30,7 @@ class RemoveCityFavouriteUseCaseImplTest {

@Test
fun `Given city removed as fav by repo, When invoke called, Then Success is returned`(): Unit =
runBlocking {
runTest {
// Given
val city = city
whenever(weatherRepository.removeCityAsFavourite(city)).thenReturn(Unit)
@@ -44,7 +46,7 @@ class RemoveCityFavouriteUseCaseImplTest {

@Test
fun `Given repo throws exception, When invoke called, Then Error is returned`(): Unit =
runBlocking {
runTest {
// Given
val city = city
val testException = TestException()
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@ import com.wednesday.template.domain.TestException
import com.wednesday.template.domain.base.Result
import com.wednesday.template.domain.weather.models.city
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -15,6 +16,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import kotlin.test.assertTrue

@OptIn(ExperimentalCoroutinesApi::class)
class SearchCitiesUseCaseImplTest {

private lateinit var weatherRepository: WeatherRepository
@@ -28,7 +30,7 @@ class SearchCitiesUseCaseImplTest {

@Test
fun `Given city searched by repo, When invoke called, Then Success is returned`(): Unit =
runBlocking {
runTest {
// Given
val searchTerm = "city"
val cityList = listOf(city)
@@ -45,7 +47,7 @@ class SearchCitiesUseCaseImplTest {

@Test
fun `Given repo throws exception, When invoke called, Then Error is returned`(): Unit =
runBlocking {
runTest {
// Given
val searchTerm = "city"
val testException = TestException()
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@ import com.wednesday.template.domain.TestException
import com.wednesday.template.domain.base.Result
import com.wednesday.template.domain.weather.models.city
import com.wednesday.template.repo.weather.WeatherRepository
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
@@ -14,6 +15,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
class SetCityFavouriteUseCaseImplTest {

private lateinit var weatherRepository: WeatherRepository
@@ -27,7 +29,7 @@ class SetCityFavouriteUseCaseImplTest {

@Test
fun `Given city searched by repo, When invoke called, Then Success is returned`(): Unit =
runBlocking {
runTest {
// Given
val city = city
whenever(weatherRepository.setCityAsFavourite(city)).thenReturn(Unit)
@@ -43,7 +45,7 @@ class SetCityFavouriteUseCaseImplTest {

@Test
fun `Given repo throws exception, When invoke called, Then Error is returned`(): Unit =
runBlocking {
runTest {
// Given
val city = city
val testException = TestException()
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import com.wednesday.template.domain.weather.City
val city = City(
id = 1,
title = "title 1",
locationType = "location 1",
latitude = "lat 1"
country = "location 1",
lat = 10.10,
lon = 30.55,
state = "state 1"
)
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.wednesday.template.domain.weather.models

import com.wednesday.template.domain.date.Date
import com.wednesday.template.domain.weather.DayWeather
import com.wednesday.template.domain.weather.Weather

val weather = Weather(
title = "title 1",
woeid = 1,
dayWeatherList = listOf(
DayWeather(
minTemp = 22,
maxTemp = 32,
temp = 30,
date = Date(1, 1, 1970),
isToday = false
)
)
title = "Pune, IN",
description = "description 1",
lat = 10.10,
lon = 30.55,
minTemp = 22.03,
maxTemp = 31.39,
temp = 26.82,
feelsLike = 28.0,
iconUrl = "https://openweathermap.org/img/wn/01d@4x.png"
)
6 changes: 4 additions & 2 deletions domain/domain.gradle.kts
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ apply {
dependencies {
implementation(project(":domain-entity"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.Kotlin.stdLib)
implementation(Dependencies.Coroutines.core)

implementation(Dependencies.Logging.timber)
}
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ package com.wednesday.template.domain.base
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlin.Exception
import timber.log.Timber

interface BaseFlowUseCase<IN, OUT> {

@@ -13,6 +13,7 @@ interface BaseFlowUseCase<IN, OUT> {
return@map Result.Success(it)
}
.catch { e ->
Timber.e(e)
emit(Result.Error(e as Exception))
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.wednesday.template.domain.base

import timber.log.Timber

interface BaseSuspendUseCase<IN, OUT> {

suspend operator fun invoke(param: IN): Result<OUT> {
return try {
Result.Success(invokeInternal(param))
} catch (e: Exception) {
Timber.e(e)
Result.Error(e)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.wednesday.template.domain.base

import timber.log.Timber

interface BaseUseCase<IN, OUT> {

operator fun invoke(param: IN): Result<OUT> {
return try {
Result.Success(invokeInternal(param))
} catch (e: Exception) {
Timber.e(e)
Result.Error(e)
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
2 changes: 1 addition & 1 deletion interactor-di/interactor-di.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,5 +13,5 @@ dependencies {
implementation(project(":interactor-impl"))
implementation(project(":domain"))

implementation(Dependencies.koinCore)
implementation(Dependencies.Koin.core)
}
Original file line number Diff line number Diff line change
@@ -9,8 +9,6 @@ import com.wednesday.template.interactor.weather.SearchCityInteractor
import com.wednesday.template.interactor.weather.UICityMapper
import com.wednesday.template.interactor.weather.UICityMapperImpl
import com.wednesday.template.interactor.weather.favourite.FavouriteWeatherInteractorImpl
import com.wednesday.template.interactor.weather.favourite.UIDayWeatherMapper
import com.wednesday.template.interactor.weather.favourite.UIDayWeatherMapperImpl
import com.wednesday.template.interactor.weather.favourite.UIWeatherListMapper
import com.wednesday.template.interactor.weather.favourite.UIWeatherListMapperImpl
import com.wednesday.template.interactor.weather.search.SearchCityInteractorImpl
@@ -31,9 +29,7 @@ val interactorModule = module {

single<UICitySearchResultsMapper> { UICitySearchResultsMapperImpl(get()) }

single<UIDayWeatherMapper> { UIDayWeatherMapperImpl(get()) }

single<UIWeatherListMapper> { UIWeatherListMapperImpl(get(), get()) }
single<UIWeatherListMapper> { UIWeatherListMapperImpl() }

factory<FavouriteWeatherInteractor> { FavouriteWeatherInteractorImpl(get(), get(), get(), get(), get(), get(), get(), get()) }

28 changes: 14 additions & 14 deletions interactor-impl/interactor-impl.gradle.kts
Original file line number Diff line number Diff line change
@@ -15,22 +15,22 @@ dependencies {
implementation(project(":domain-entity"))
implementation(project(":resources"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.coroutinesCore)
implementation(Dependencies.Coroutines.core)

implementation(Dependencies.androidCoreKtx)
implementation(Dependencies.Android.coreKtx)

implementation(Dependencies.loggingTimber)
implementation(Dependencies.Logging.timber)

testImplementation(Dependencies.testJunit)
testImplementation(Dependencies.testKotlinTest)
testImplementation(Dependencies.testMockito)
testImplementation(Dependencies.testFlowTest)
testImplementation(Dependencies.testCoroutines)
testImplementation(Dependencies.testAndroidxArchCore)
testImplementation(Dependencies.testAndroidxTestRunner)
testImplementation(Dependencies.testAndroidxTestRules)
testImplementation(Dependencies.testAndroidxExt)
testImplementation(Dependencies.testAndroidxCoreTesting)
testImplementation(Dependencies.Test.junit)
testImplementation(Dependencies.Test.kotlinTest)
testImplementation(Dependencies.Test.mockito)
testImplementation(Dependencies.Test.flowTest)
testImplementation(Dependencies.Test.coroutines)
testImplementation(Dependencies.Test.androidxArchCore)
testImplementation(Dependencies.Test.androidxTestRunner)
testImplementation(Dependencies.Test.androidxTestRules)
testImplementation(Dependencies.Test.androidxExt)
testImplementation(Dependencies.Test.androidxCoreTesting)
}
Original file line number Diff line number Diff line change
@@ -22,21 +22,25 @@ class UICityMapperImpl : UICityMapper {
return UICity(
cityId = from1.id,
title = from1.title,
locationType = from1.locationType,
locationType = from1.country,
displayTitle = UIText { block(from1.title) },
displayLocationType = UIText { block(from1.locationType) },
latitude = from1.latitude,
isFavourite = from2
displayLocationType = UIText { block(from1.country) },
latitude = "${from1.lat} ${from1.lon}",
isFavourite = from2,
state = from1.state
)
}

override fun mapUICity(from: UICity): City {
Timber.tag(TAG).d("mapUICity: from = $from")
val (lat, lon) = from.latitude.split(" ")
return City(
id = from.cityId,
title = from.title,
locationType = from.locationType,
latitude = from.latitude
country = from.locationType,
lat = lat.toDouble(),
lon = lon.toDouble(),
state = from.state
)
}

@@ -45,11 +49,12 @@ class UICityMapperImpl : UICityMapper {
return UICity(
cityId = from.id,
title = from.title,
locationType = from.locationType,
locationType = from.country,
displayTitle = UIText { block(from.title) },
displayLocationType = UIText { block(from.locationType) },
latitude = from.latitude,
isFavourite = true
displayLocationType = UIText { block(from.country) },
latitude = "${from.lat} ${from.lon}",
isFavourite = true,
state = from.state
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,53 +2,43 @@ package com.wednesday.template.interactor.weather.favourite

import com.wednesday.template.domain.weather.Weather
import com.wednesday.template.interactor.base.Mapper
import com.wednesday.template.interactor.base.datetime.UIDateMapper
import com.wednesday.template.interactor_impl.R
import com.wednesday.template.presentation.base.UIList
import com.wednesday.template.presentation.base.UIListItemBase
import com.wednesday.template.presentation.base.UIText
import com.wednesday.template.presentation.weather.UIDayWeatherHeading
import com.wednesday.template.presentation.weather.UISearchCitiesPlaceholder
import com.wednesday.template.presentation.weather.UIWeather
import timber.log.Timber
import java.util.*

interface UIWeatherListMapper : Mapper<List<Weather>, UIList>

class UIWeatherListMapperImpl(
private val dayWeatherMapper: UIDayWeatherMapper,
private val uiDateMapper: UIDateMapper
) : UIWeatherListMapper {
class UIWeatherListMapperImpl : UIWeatherListMapper {

override fun map(from: List<Weather>): UIList {
Timber.tag(TAG).d("map() called with: from = $from")
val weatherList = from
.sortedBy { it.title }
.map {

val currentWeather = it.dayWeatherList.firstOrNull { dayWeather -> dayWeather.isToday }
?: it.dayWeatherList.first()

val dayWeatherList = mutableListOf<UIListItemBase>()

dayWeatherList.add(
UIDayWeatherHeading(
text = UIText { block(R.string.forecast) }
)
)

dayWeatherList.addAll(
it.dayWeatherList
.filter { dayWeather -> !dayWeather.isToday }
.sortedBy { dayWeather -> uiDateMapper.map(dayWeather.date).timeAsLong }
.map { dayWeather -> dayWeatherMapper.map(dayWeather, it.woeid) }
)

UIWeather(
cityId = it.woeid,
lat = it.lat,
lon = it.lon,
title = UIText { block(it.title) },
currentTemp = UIText { block("${currentWeather.temp} °C") },
minMaxTemp = UIText { block("${currentWeather.minTemp} - ${currentWeather.maxTemp} °C") },
dayWeatherList = dayWeatherList
description = UIText {
block(
it.description.replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(
Locale.getDefault()
) else char.toString()
}
)
},
currentTemp = UIText { block("${it.temp} °C") },
minMaxTemp = UIText { block("With a high of ${it.maxTemp} °C and low of ${it.minTemp} °C") },
feelsLike = UIText {
block(R.string.feels_like)
block(" ${it.feelsLike} °C")
},
iconUrl = it.iconUrl
)
}

Original file line number Diff line number Diff line change
@@ -8,12 +8,13 @@ import com.wednesday.template.interactor.base.CoroutineContextController
import com.wednesday.template.interactor.weather.SearchCityInteractor
import com.wednesday.template.presentation.base.UIList
import com.wednesday.template.presentation.base.UIResult
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import timber.log.Timber

class SearchCityInteractorImpl(
@@ -23,13 +24,13 @@ class SearchCityInteractorImpl(
private val coroutineContextController: CoroutineContextController
) : SearchCityInteractor {

private val searchResultStateFlow = MutableSharedFlow<List<City>>()
private val searchResultStateFlow = Channel<List<City>>()

override val searchResultsFlow: Flow<UIResult<UIList>> = favouriteCitiesFlowUseCase(Unit)
.combine(searchResultStateFlow) { favouriteCities, searchResults ->
.combine(searchResultStateFlow.receiveAsFlow()) { favouriteCities, searchResults ->
when {
searchResults.isEmpty() -> {
UIResult.Error(Exception("The search list was empty"))
UIResult.Success(UIList())
}
favouriteCities is Result.Success -> {
UIResult.Success(
@@ -40,6 +41,7 @@ class SearchCityInteractorImpl(
)
}
favouriteCities is Result.Error -> {
Timber.e(favouriteCities.exception)
UIResult.Error(favouriteCities.exception)
}
else -> {
@@ -64,7 +66,7 @@ class SearchCityInteractorImpl(
}
is Result.Success -> citiesResult.data
}
searchResultStateFlow.emit(list)
searchResultStateFlow.send(list)
}

companion object {
Original file line number Diff line number Diff line change
@@ -2,19 +2,20 @@ package com.wednesday.template.interactor.base

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class CoroutineScopeRule(
private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {
class CoroutineDispatcherRule(
private val dispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {

val coroutineContextController: CoroutineContextController = TestCoroutineContextController(testCoroutineDispatcher = dispatcher)
val coroutineContextController: CoroutineContextController =
TestCoroutineContextController(testCoroutineDispatcher = dispatcher)

override fun starting(description: Description?) {
super.starting(description)
@@ -23,7 +24,6 @@ class CoroutineScopeRule(

override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.wednesday.template.interactor.base

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Rule

@ExperimentalCoroutinesApi
@@ -11,5 +15,12 @@ open class InteractorTest {
val instantTaskExecutorRule = InstantTaskExecutorRule()

@get:Rule
val coroutineScopeRule = CoroutineScopeRule()
val coroutineDispatcherRule = CoroutineDispatcherRule()

fun TestScope.launchInTestScope(block: suspend CoroutineScope.() -> Unit) {
launch(UnconfinedTestDispatcher(testScheduler)) {
println("running block")
block()
}
}
}
Original file line number Diff line number Diff line change
@@ -3,12 +3,12 @@ package com.wednesday.template.interactor.base
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.withContext

@ExperimentalCoroutinesApi
data class TestCoroutineContextController(
val testCoroutineDispatcher: TestCoroutineDispatcher
val testCoroutineDispatcher: TestDispatcher
) : CoroutineContextController {

override val dispatcherDefault: CoroutineDispatcher
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import com.wednesday.template.presentation.base.UIList
import com.wednesday.template.presentation.base.UIResult
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
@@ -42,7 +42,7 @@ class SearchCityInteractorImplTest : InteractorTest() {
searchCitiesUseCase = mock()
favouriteCitiesFlowUseCase = mock()
citySearchResultsMapper = mock()
coroutineContextController = coroutineScopeRule.coroutineContextController
coroutineContextController = coroutineDispatcherRule.coroutineContextController
}

private fun verifyNoMoreInteractions() {
@@ -67,7 +67,7 @@ class SearchCityInteractorImplTest : InteractorTest() {

@Test
fun `Given no error occurs, When search called, Then search result flow emits UIList of results`(): Unit =
coroutineScopeRule.runBlockingTest {
runTest {
// Given
val searchTerm = "Pune"
val uiList = UIList(uiCity)
@@ -76,27 +76,32 @@ class SearchCityInteractorImplTest : InteractorTest() {
whenever(favouriteCitiesFlowUseCase(Unit)).thenReturn(flowOf(Result.Success(cityList)))
whenever(citySearchResultsMapper.map(any(), any())).thenReturn(uiList)

createInteractor()

// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)

val result = awaitItem()
// Then
assertTrue(result is UIResult.Success)
assertEquals(actual = result.data, expected = uiList)
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(citySearchResultsMapper, times(1)).map(same(cityList), same(cityList))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
launchInTestScope {
println("Creating")
createInteractor()
println("Created")

// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)

val result = awaitItem()

// Then
assertTrue(result is UIResult.Success)
assertEquals(actual = result.data, expected = uiList)
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(citySearchResultsMapper, times(1)).map(same(cityList), same(cityList))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
}
}
}

@Test
fun `Given search use case returns error, When search called, Then search result flow emits empty list`(): Unit =
coroutineScopeRule.runBlockingTest {
runTest {
// Given
val searchTerm = "Pune"
val uiList = UIList()
@@ -106,50 +111,54 @@ class SearchCityInteractorImplTest : InteractorTest() {
whenever(favouriteCitiesFlowUseCase(Unit)).thenReturn(flowOf(Result.Success(cityList)))
whenever(citySearchResultsMapper.map(any(), any())).thenReturn(uiList)

createInteractor()
launchInTestScope {
createInteractor()

// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)
// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)

val result = awaitItem()
val result = awaitItem()

// Then
assertTrue(result is UIResult.Error)
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
// Then
assertTrue(result is UIResult.Success)
assertTrue(result.data.items.isEmpty())
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
}
}
}

@Test
fun `Given mapper throws error, When search called, Then search result flow emits empty list`(): Unit =
coroutineScopeRule.runBlockingTest {
runTest {
// Given
val searchTerm = "Pune"
val uiList = UIList()
val cityList = listOf(city)
val testException = TestException()
whenever(searchCitiesUseCase(searchTerm)).thenReturn(Result.Success(cityList))
whenever(favouriteCitiesFlowUseCase(Unit)).thenReturn(flowOf(Result.Success(cityList)))
whenever(citySearchResultsMapper.map(any(), any())).thenThrow(testException)

createInteractor()
launchInTestScope {
createInteractor()

// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)
// When
interactor.searchResultsFlow.test {
interactor.search(searchTerm)

val result = awaitItem()
val result = awaitItem()

// Then
assertTrue(result is UIResult.Error)
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(citySearchResultsMapper, times(1)).map(same(cityList), same(cityList))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
// Then
assertTrue(result is UIResult.Error)
verify(searchCitiesUseCase, times(1)).invoke(same(searchTerm))
verify(citySearchResultsMapper, times(1)).map(same(cityList), same(cityList))
verify(favouriteCitiesFlowUseCase, times(1)).invoke(Unit)
verifyNoMoreInteractions()
cancelAndConsumeRemainingEvents()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -7,16 +7,19 @@ import com.wednesday.template.presentation.weather.UICity
val city = City(
id = 1,
title = "title 1",
locationType = "location 1",
latitude = "lat 1"
country = "location 1",
lat = 10.10,
lon = 30.55,
state = "state 1"
)

val uiCity = UICity(
cityId = city.id,
title = city.title,
displayTitle = UIText { block(city.title) },
locationType = city.locationType,
displayLocationType = UIText { block(city.locationType) },
latitude = city.latitude,
isFavourite = false
locationType = city.country,
displayLocationType = UIText { block(city.country) },
latitude = "${city.lat} ${city.lon}",
isFavourite = false,
state = city.state
)
4 changes: 2 additions & 2 deletions interactor/interactor.gradle.kts
Original file line number Diff line number Diff line change
@@ -12,6 +12,6 @@ dependencies {
implementation(project(":domain-entity"))
implementation(project(":presentation-entity"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.Kotlin.stdLib)
implementation(Dependencies.Coroutines.core)
}
3 changes: 3 additions & 0 deletions local.skeleton.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This is a skeleton file. All all the properties here to your local.properties file.
OPEN_WEATHER_API_KEY="Your api key"
DEBUG_OPEN_WEATHER_API_KEY="Your api key"
6 changes: 3 additions & 3 deletions navigation-di/navigation-di.gradle.kts
Original file line number Diff line number Diff line change
@@ -12,10 +12,10 @@ dependencies {
implementation(project(":navigation"))
implementation(project(":navigation-impl"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.koinCore)
implementation(Dependencies.Koin.core)

implementation(Dependencies.androidAppCompat)
implementation(Dependencies.Android.appCompat)

}
16 changes: 8 additions & 8 deletions navigation-impl/navigation-impl.gradle.kts
Original file line number Diff line number Diff line change
@@ -12,14 +12,14 @@ dependencies {
implementation(project(":presentation-entity"))
implementation(project(":navigation"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.androidAppCompat)
implementation(Dependencies.androidNavigationUi)
implementation(Dependencies.androidNavigationFragment)
implementation(Dependencies.Android.appCompat)
implementation(Dependencies.Android.navigationUi)
implementation(Dependencies.Android.navigationFragment)

testImplementation(Dependencies.testAndroidxTestCore)
testImplementation(Dependencies.testAndroidxExt)
testImplementation(Dependencies.testKotlinTest)
testImplementation(Dependencies.testMockito)
testImplementation(Dependencies.Test.androidxTestCore)
testImplementation(Dependencies.Test.androidxExt)
testImplementation(Dependencies.Test.kotlinTest)
testImplementation(Dependencies.Test.mockito)
}
8 changes: 4 additions & 4 deletions navigation/navigation.gradle.kts
Original file line number Diff line number Diff line change
@@ -12,10 +12,10 @@ dependencies {
implementation(project(":presentation-entity"))
implementation(project(":resources"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.androidAppCompat)
implementation(Dependencies.Android.appCompat)

implementation(Dependencies.androidNavigationUi)
implementation(Dependencies.androidNavigationFragment)
implementation(Dependencies.Android.navigationUi)
implementation(Dependencies.Android.navigationFragment)
}
4 changes: 2 additions & 2 deletions presentation-di/presentation-di.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,6 +13,6 @@ dependencies {
implementation(project(":interactor"))
implementation(project(":navigation"))

implementation(Dependencies.koinCore)
implementation(Dependencies.koinAndroid)
implementation(Dependencies.Koin.core)
implementation(Dependencies.Koin.android)
}
6 changes: 3 additions & 3 deletions presentation-entity/presentation-entity.gradle.kts
Original file line number Diff line number Diff line change
@@ -11,8 +11,8 @@ apply {

dependencies {

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.androidCoreKtx)
implementation(Dependencies.Kotlin.stdLib)
implementation(Dependencies.Android.coreKtx)

implementation(Dependencies.androidAppCompat)
implementation(Dependencies.Android.appCompat)
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package com.wednesday.template.presentation.base

sealed class UIResult<out T> {

class Success<T>(val data: T) : UIResult<T>()
data class Success<T>(val data: T) : UIResult<T>()

class Error(val exception: Exception) : UIResult<Nothing>()
data class Error(val exception: Exception) : UIResult<Nothing>()
}
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import kotlinx.parcelize.Parcelize
data class UICity(
val cityId: Int,
val title: String,
val state: String?,
val displayTitle: UIText,
val locationType: String,
val displayLocationType: UIText,
Original file line number Diff line number Diff line change
@@ -6,9 +6,12 @@ import kotlinx.parcelize.Parcelize

@Parcelize
data class UIWeather(
val cityId: Int,
val lat: Double,
val lon: Double,
val title: UIText,
val description: UIText,
val currentTemp: UIText,
val minMaxTemp: UIText,
val dayWeatherList: List<UIListItemBase>
) : UIListItemBase(id = "UICity $cityId")
val feelsLike: UIText,
val iconUrl: String
) : UIListItemBase(id = "UICity $lat $lon")
82 changes: 54 additions & 28 deletions presentation/presentation.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,6 +13,16 @@ apply {
android {
buildFeatures {
viewBinding = true
compose = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

composeOptions {
kotlinCompilerExtensionVersion = "1.1.1"
}
}

@@ -22,32 +32,48 @@ dependencies {
implementation(project(":resources"))
implementation(project(":navigation"))

implementation(Dependencies.kotlinStdLib)

implementation(Dependencies.koinCore)
implementation(Dependencies.koinAndroid)

implementation(Dependencies.material)

implementation(Dependencies.loggingTimber)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
implementation(Dependencies.androidAppCompat)
implementation(Dependencies.androidCoreKtx)
implementation(Dependencies.androidFragment)
implementation(Dependencies.androidConstraintLayout)
implementation(Dependencies.androidRecyclerView)
implementation(Dependencies.androidLifecycleLiveDataCore)
implementation(Dependencies.androidLifecycleLiveDataKtx)
implementation(Dependencies.androidLifecycleViewModel)
implementation(Dependencies.androidLifecycleViewModelKtx)
implementation(Dependencies.androidNavigationFragment)
implementation(Dependencies.androidNavigationUi)
implementation(Dependencies.androidSplashScreen)

testImplementation(Dependencies.testAndroidxArchCore)
testImplementation(Dependencies.testCoroutines)
testImplementation(Dependencies.testKotlinTest)
testImplementation(Dependencies.testFlowTest)
testImplementation(Dependencies.testMockito)
implementation(Dependencies.Kotlin.stdLib)

implementation(Dependencies.Koin.core)
implementation(Dependencies.Koin.android)
implementation(Dependencies.Koin.navigation)
implementation(Dependencies.Koin.compose)

implementation(Dependencies.Material.material)

implementation(Dependencies.Compose.activity)
implementation(Dependencies.Compose.material)
implementation(Dependencies.Compose.animation)
implementation(Dependencies.Compose.uiTooling)
implementation(Dependencies.Compose.viewModel)
implementation(Dependencies.Compose.materialIconCore)
implementation(Dependencies.Compose.materialIconExtended)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.liveData)

implementation(Dependencies.Image.coil)

implementation(Dependencies.Logging.timber)
implementation(Dependencies.Coroutines.core)
implementation(Dependencies.Coroutines.android)
implementation(Dependencies.Android.appCompat)
implementation(Dependencies.Android.coreKtx)
implementation(Dependencies.Android.fragment)
implementation(Dependencies.Android.constraintLayout)
implementation(Dependencies.Android.recyclerView)
implementation(Dependencies.Android.lifecycleLiveDataCore)
implementation(Dependencies.Android.lifecycleLiveDataKtx)
implementation(Dependencies.Android.lifecycleViewModel)
implementation(Dependencies.Android.lifecycleViewModelKtx)
implementation(Dependencies.Android.navigationFragment)
implementation(Dependencies.Android.navigationUi)
implementation(Dependencies.Android.splashScreen)

testImplementation(Dependencies.Test.androidxArchCore)
testImplementation(Dependencies.Test.coroutines)
testImplementation(Dependencies.Test.kotlinTest)
testImplementation(Dependencies.Test.flowTest)
testImplementation(Dependencies.Test.mockito)

androidTestImplementation(Dependencies.Compose.uiTest)
}
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ class MainActivity : AppCompatActivity() {
startDestId: Int,
startDestinationArgs: Bundle? = null
) {
startDestination = startDestId
setStartDestination(startDestId)
if (startDestinationArgs != null) {
navController.setGraph(this, startDestinationArgs)
} else {
Original file line number Diff line number Diff line change
@@ -14,9 +14,7 @@ import com.wednesday.template.presentation.base.effect.Effect
import com.wednesday.template.presentation.base.viewmodel.BaseViewModel
import com.wednesday.template.presentation.screen.Screen
import com.wednesday.template.presentation.screen.ScreenState
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf

typealias BindingProvider<B> = (LayoutInflater, ViewGroup?, Boolean) -> B
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ abstract class BaseNestedListViewHolder<T : UIListItemBase>(
protected val renderers: MutableList<Pair<KClass<*>, ListItemRenderer<UIListItemBase>>> = mutableListOf()

private val nestedRecyclerView: RecyclerView
get() = itemView.findViewById(R.id.nestedRecyclerView)
get() = itemView.findViewById(/* R.id.nestedRecyclerView : uncomment after creating id */ R.id.recyclerView)

abstract fun getNestedListItems(item: T): List<UIListItemBase>

Original file line number Diff line number Diff line change
@@ -8,9 +8,9 @@ import kotlin.reflect.KProperty
class StatefulLiveData<T>(
private val savedStateHandle: SavedStateHandle,
private val defaultValueProvider: (() -> T)? = null
) : ReadOnlyProperty<Any, MutableLiveData<T>> {
) : ReadOnlyProperty<Any, MutableLiveData<T?>> {

override fun getValue(thisRef: Any, property: KProperty<*>): MutableLiveData<T> {
override fun getValue(thisRef: Any, property: KProperty<*>): MutableLiveData<T?> {
return savedStateHandle.getLiveData(property.name, defaultValueProvider?.invoke())
}
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,11 @@ package com.wednesday.template.presentation.weather.home

import com.wednesday.template.presentation.base.intent.Intent

interface HomeScreenIntent : Intent {
sealed interface HomeScreenIntent : Intent {

object Search : HomeScreenIntent

object Loading : HomeScreenIntent
object Loading2 : HomeScreenIntent
object Loading3 : HomeScreenIntent
}
Original file line number Diff line number Diff line change
@@ -34,7 +34,6 @@ class HomeViewModel(

override fun onCreate(fromRecreate: Boolean) {
if (fromRecreate) return

favouriteWeatherInteractor.getFavouriteCitiesFlow().onEach {
favouriteWeatherInteractor.fetchFavouriteCitiesWeather()
}.launchIn(viewModelScope)
@@ -62,6 +61,19 @@ class HomeViewModel(
is HomeScreenIntent.Search -> {
navigator.navigateTo(SearchScreen)
}
HomeScreenIntent.Loading -> {
setState { copy(showLoading = !showLoading) }
}
HomeScreenIntent.Loading2 -> setState { copy(toolbar = toolbar.copy(hasBackButton = !toolbar.hasBackButton)) }
HomeScreenIntent.Loading3 -> setState {
copy(
toolbar = toolbar.copy(
title = UIText {
block("${System.currentTimeMillis()}")
}
)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
package com.wednesday.template.presentation.weather.home.list

import com.wednesday.template.presentation.base.UIListItemBase
import coil.load
import coil.transform.CircleCropTransformation
import com.wednesday.template.presentation.base.extensions.setUIText
import com.wednesday.template.presentation.base.intent.Intent
import com.wednesday.template.presentation.base.list.viewholder.BaseNestedListViewHolder
import com.wednesday.template.presentation.base.list.viewholder.BaseViewHolder
import com.wednesday.template.presentation.weather.UIWeather
import com.wednesday.template.resources.databinding.ItemWeatherBinding
import kotlinx.coroutines.channels.Channel

class UIWeatherViewHolder(private val binding: ItemWeatherBinding) :
BaseNestedListViewHolder<UIWeather>(binding) {

init {
addRenderer(UIDayWeatherHeadingRenderer())
addRenderer(UIDayWeatherRenderer())
}

override fun getNestedListItems(item: UIWeather): List<UIListItemBase> {
return item.dayWeatherList
}
BaseViewHolder<UIWeather>(binding) {

override fun onSetupIntents(intentChannel: Channel<Intent>) = Unit

override fun onBindInternal() {
super.onBindInternal()
binding.run {
override fun onBindInternal() = binding.run {
compareAndSet({ title }) {
cityName.setUIText(it)
}

compareAndSet({ title }) {
cityName.setUIText(it)
compareAndSet({ iconUrl }) {
weatherIcon.load(it) {
crossfade(true)
transformations(CircleCropTransformation())
}
}

compareAndSet({ currentTemp }) {
cityTemp.setUIText(it)
}
compareAndSet({ currentTemp }) {
cityTemp.setUIText(it)
}

compareAndSet({ feelsLike }) {
feelsLike.setUIText(it)
}

compareAndSet({ minMaxTemp }) {
minMaxTemp.setUIText(it)
}

compareAndSet({ description }) {
description.setUIText(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -12,14 +12,12 @@ import com.wednesday.template.presentation.base.UIToolbar
import com.wednesday.template.presentation.base.effect.ShowSnackbarEffect
import com.wednesday.template.presentation.base.intent.IntentHandler
import com.wednesday.template.presentation.base.viewmodel.BaseViewModel
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@FlowPreview
class SearchViewModel(
private val searchCityInteractor: SearchCityInteractor,
private val favouriteWeatherInteractor: FavouriteWeatherInteractor,
Original file line number Diff line number Diff line change
@@ -21,8 +21,9 @@ class UICityListViewHolder(private val binding: CityItemListBinding) :

override fun onBindInternal() = binding.run {

compareAndSet({ title }) {
cityTextViewListItem.text = it
compareAndSet({ title to state }) {
val title = it.first + if (it.second != null) ", ${it.second}" else ""
cityTextViewListItem.text = title
}

compareAndSet({ latitude }) {
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package com.wednesday.template.presentation.base

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
@@ -13,7 +13,7 @@ class MainDispatcherTestRule : TestWatcher() {

override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(TestCoroutineDispatcher())
Dispatchers.setMain(StandardTestDispatcher())
}

override fun finished(description: Description?) {
Original file line number Diff line number Diff line change
@@ -2,17 +2,18 @@ package com.wednesday.template.presentation.weather.home

import com.wednesday.template.interactor.weather.FavouriteWeatherInteractor
import com.wednesday.template.navigation.home.HomeNavigator
import com.wednesday.template.presentation.R
import com.wednesday.template.presentation.base.BaseViewModelTest
import com.wednesday.template.presentation.base.UIList
import com.wednesday.template.presentation.base.UIResult
import com.wednesday.template.presentation.base.UIText
import com.wednesday.template.presentation.base.UIToolbar
import com.wednesday.template.presentation.weather.home.models.city
import com.wednesday.template.presentation.weather.search.SearchScreen
import com.wednesday.template.resources.R
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
@@ -53,7 +54,7 @@ class HomeViewModelTest : BaseViewModelTest() {

@Test
fun `Given fromRecreate = true, When onCreate, Then interactor was not called`() =
runBlocking {
runTest {
// Given
whenever(interactor.getFavouriteCitiesFlow())
.thenReturn(flowOf())
@@ -70,7 +71,7 @@ class HomeViewModelTest : BaseViewModelTest() {

@Test
fun `Given fromRecreate = false, When onCreate, Then FavouriteCitiesFlow and FavouriteWeatherUIList were called`(): Unit =
runBlocking {
runTest {
// Given
val fromRecreate = false
whenever(interactor.getFavouriteCitiesFlow())
@@ -82,6 +83,7 @@ class HomeViewModelTest : BaseViewModelTest() {
viewModel.onCreate(fromRecreate = fromRecreate)

// Then
advanceUntilIdle()
verify(interactor, times(1)).getFavouriteWeatherUIList()
verify(interactor, times(1)).getFavouriteCitiesFlow()
}
@@ -104,7 +106,7 @@ class HomeViewModelTest : BaseViewModelTest() {

@Test
fun `Given favourite city flow emits value, When new favourite city added, Then favourite city weather is fetched`(): Unit =
runBlocking {
runTest {
// Given
val favCityList = UIResult.Success(listOf(city))
whenever(interactor.getFavouriteCitiesFlow())
@@ -116,12 +118,13 @@ class HomeViewModelTest : BaseViewModelTest() {
viewModel.onCreate(false)

// Then
advanceUntilIdle()
verify(interactor, times(2)).fetchFavouriteCitiesWeather()
}

@Test
fun `Given weather ui list emits, When flow is collected, Then state is updated with the UI list`(): Unit =
runBlocking {
runTest {
// Given
val uiList = UIResult.Success(UIList(city))
whenever(interactor.getFavouriteCitiesFlow())
@@ -136,12 +139,14 @@ class HomeViewModelTest : BaseViewModelTest() {

// Then
val initialState = getInitialState()
advanceUntilIdle()
observer.inOrder {
verify().onChanged(null)
verify().onChanged(initialState)
verify().onChanged(initialState.copy(items = uiList.data))
verifyNoMoreInteractions()
}
verify(interactor, times(1)).getFavouriteWeatherUIList()
}

private fun getInitialState() = HomeScreenState(
Original file line number Diff line number Diff line change
@@ -10,5 +10,6 @@ val city = UICity(
locationType = "location 1",
displayLocationType = UIText { block("location 1") },
latitude = "latitude 1",
isFavourite = true
isFavourite = true,
state = "state 1"
)
2 changes: 1 addition & 1 deletion repo-di/repo-di.gradle.kts
Original file line number Diff line number Diff line change
@@ -13,5 +13,5 @@ dependencies {
implementation(project(":repo-impl"))
implementation(project(":service"))

implementation(Dependencies.koinCore)
implementation(Dependencies.Koin.core)
}
17 changes: 3 additions & 14 deletions repo-di/src/main/java/com/wednesday/template/repo/RepoModule.kt
Original file line number Diff line number Diff line change
@@ -4,14 +4,10 @@ import com.wednesday.template.repo.date.DateRepo
import com.wednesday.template.repo.date.DateRepoImpl
import com.wednesday.template.repo.weather.DomainCityMapper
import com.wednesday.template.repo.weather.DomainCityMapperImpl
import com.wednesday.template.repo.weather.DomainDayWeatherMapper
import com.wednesday.template.repo.weather.DomainDayWeatherMapperImpl
import com.wednesday.template.repo.weather.DomainWeatherMapper
import com.wednesday.template.repo.weather.DomainWeatherMapperImpl
import com.wednesday.template.repo.weather.LocalCityMapper
import com.wednesday.template.repo.weather.LocalCityMapperImpl
import com.wednesday.template.repo.weather.LocalDayWeatherMapper
import com.wednesday.template.repo.weather.LocalDayWeatherMapperImpl
import com.wednesday.template.repo.weather.LocalWeatherMapper
import com.wednesday.template.repo.weather.LocalWeatherMapperImpl
import com.wednesday.template.repo.weather.WeatherRepository
@@ -28,18 +24,11 @@ val repoModule = module {

single<LocalCityMapper> { LocalCityMapperImpl() }

single<DomainWeatherMapper> { DomainWeatherMapperImpl(get()) }
single<DomainWeatherMapper> { DomainWeatherMapperImpl() }

single<LocalWeatherMapper> { LocalWeatherMapperImpl() }

single<LocalDayWeatherMapper> { LocalDayWeatherMapperImpl() }

single<DomainDayWeatherMapper> { DomainDayWeatherMapperImpl(get()) }
single<LocalWeatherMapper> { LocalWeatherMapperImpl(get()) }

single<WeatherRepository> {
WeatherRepositoryImpl(
get(), get(), get(), get(), get(), get(),
get(), get()
)
WeatherRepositoryImpl(get(), get(), get(), get(), get(), get(), get())
}
}
16 changes: 8 additions & 8 deletions repo-impl/repo-impl.gradle.kts
Original file line number Diff line number Diff line change
@@ -15,14 +15,14 @@ dependencies {
implementation (project(":service"))
implementation (project(":service-impl"))

implementation(Dependencies.kotlinStdLib)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.Kotlin.stdLib)
implementation(Dependencies.Coroutines.core)

implementation(Dependencies.loggingTimber)
implementation(Dependencies.Logging.timber)

testImplementation(Dependencies.testJunit)
testImplementation(Dependencies.testKotlinTest)
testImplementation(Dependencies.testMockito)
testImplementation(Dependencies.testFlowTest)
testImplementation(Dependencies.testCoroutines)
testImplementation(Dependencies.Test.junit)
testImplementation(Dependencies.Test.kotlinTest)
testImplementation(Dependencies.Test.mockito)
testImplementation(Dependencies.Test.flowTest)
testImplementation(Dependencies.Test.coroutines)
}
Original file line number Diff line number Diff line change
@@ -54,6 +54,8 @@ class DateRepoImpl : DateRepo {

override fun convertToLong(dateTime: DateTime) = mapDateTime(dateTime).time

override fun nowDateTimeAsLong() = mapDateTime(nowDateTime()).time

override fun convertToDate(timeInMillis: Long): Date {
val calendar = Calendar.getInstance()
calendar.timeInMillis = timeInMillis
Original file line number Diff line number Diff line change
@@ -2,34 +2,38 @@ package com.wednesday.template.repo.weather

import com.wednesday.template.domain.weather.City
import com.wednesday.template.repo.util.Mapper
import com.wednesday.template.service.weather.LocalCity
import com.wednesday.template.service.weather.RemoteCity
import com.wednesday.template.service.openWeather.geoCoding.LocalLocation
import com.wednesday.template.service.openWeather.geoCoding.RemoteLocation
import timber.log.Timber

interface DomainCityMapper : Mapper<LocalCity, City> {
fun mapRemoteCity(from: RemoteCity): City
fun mapRemoteCity(from: List<RemoteCity>): List<City> = from.map(::mapRemoteCity)
interface DomainCityMapper : Mapper<LocalLocation, City> {
fun mapRemoteCity(from: RemoteLocation): City
fun mapRemoteCity(from: List<RemoteLocation>): List<City> = from.map(::mapRemoteCity)
}

class DomainCityMapperImpl : DomainCityMapper {

override fun mapRemoteCity(from: RemoteCity): City {
override fun mapRemoteCity(from: RemoteLocation): City {
Timber.tag(TAG).d("mapRemoteCity: from = $from")
return City(
id = from.woeid,
title = from.title,
locationType = from.locationType,
latitude = from.latitude
id = (from.lat + from.lon).toInt(),
title = from.name,
country = from.country,
lat = from.lat,
lon = from.lon,
state = from.state
)
}

override fun map(from: LocalCity): City {
override fun map(from: LocalLocation): City {
Timber.tag(TAG).d("map: from = $from")
return City(
id = from.woeid,
title = from.title,
locationType = from.locationType,
latitude = from.latitude
id = (from.lat + from.lon).toInt(),
title = from.name,
country = from.country,
lat = from.lat,
lon = from.lon,
state = from.state
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,21 +2,26 @@ package com.wednesday.template.repo.weather

import com.wednesday.template.domain.weather.Weather
import com.wednesday.template.repo.util.Mapper
import com.wednesday.template.service.weather.LocalCityWithWeather
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeather
import timber.log.Timber

interface DomainWeatherMapper : Mapper<LocalCityWithWeather, Weather>
interface DomainWeatherMapper : Mapper<LocalCurrentWeather, Weather>

class DomainWeatherMapperImpl(
private val dayWeatherMapper: DomainDayWeatherMapper
) : DomainWeatherMapper {
class DomainWeatherMapperImpl : DomainWeatherMapper {

override fun map(from: LocalCityWithWeather): Weather {
override fun map(from: LocalCurrentWeather): Weather {
Timber.tag(TAG).d("map() called with: from = $from")

return Weather(
title = from.weather.title,
woeid = from.city.woeid,
dayWeatherList = dayWeatherMapper.map(from.dayWeather)
title = "${from.name}, ${from.sys.country}",
description = from.weather.description,
lat = from.coord.lat,
lon = from.coord.lon,
minTemp = from.main.tempMin,
maxTemp = from.main.tempMax,
temp = from.main.temp,
feelsLike = from.main.feelsLike,
iconUrl = "https://openweathermap.org/img/wn/${from.weather.icon}@4x.png"
)
}

Original file line number Diff line number Diff line change
@@ -2,20 +2,21 @@ package com.wednesday.template.repo.weather

import com.wednesday.template.domain.weather.City
import com.wednesday.template.repo.util.Mapper
import com.wednesday.template.service.weather.LocalCity
import com.wednesday.template.service.openWeather.geoCoding.LocalLocation
import timber.log.Timber

interface LocalCityMapper : Mapper<City, LocalCity>
interface LocalCityMapper : Mapper<City, LocalLocation>

class LocalCityMapperImpl : LocalCityMapper {

override fun map(from: City): LocalCity {
override fun map(from: City): LocalLocation {
Timber.tag(TAG).d("map: from = $from")
return LocalCity(
woeid = from.id,
title = from.title,
locationType = from.locationType,
latitude = from.latitude
return LocalLocation(
country = from.country,
name = from.title,
lat = from.lat,
lon = from.lon,
state = from.state,
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,20 +1,76 @@
package com.wednesday.template.repo.weather

import com.wednesday.template.repo.util.Mapper2
import com.wednesday.template.service.weather.LocalWeather
import com.wednesday.template.service.weather.RemoteWeather
import com.wednesday.template.repo.date.DateRepo
import com.wednesday.template.repo.util.Mapper3
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeather
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherClouds
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherCoord
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherMain
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherSys
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherWeather
import com.wednesday.template.service.openWeather.currentWeather.local.LocalCurrentWeatherWind
import com.wednesday.template.service.openWeather.currentWeather.remote.RemoteCurrentWeather
import timber.log.Timber

interface LocalWeatherMapper : Mapper2<RemoteWeather, Int, LocalWeather>
interface LocalWeatherMapper : Mapper3<RemoteCurrentWeather, Double, Double, LocalCurrentWeather>

class LocalWeatherMapperImpl : LocalWeatherMapper {
class LocalWeatherMapperImpl(private val dateRepo: DateRepo) : LocalWeatherMapper {

override fun map(from1: RemoteWeather, from2: Int): LocalWeather {
Timber.tag(TAG).d("map() called with: from1 = $from1, from2 = $from2")
override fun map(from1: RemoteCurrentWeather, from2: Double, from3: Double): LocalCurrentWeather {
Timber.tag(TAG).d("map() called with: from1 = $from1, from2 = $from2, from3 = $from3")

return LocalWeather(
cityWoeid = from2,
title = from1.title
val clouds = LocalCurrentWeatherClouds(
all = from1.clouds.all
)

val coord = LocalCurrentWeatherCoord(
lat = from2,
lon = from3
)

val main = LocalCurrentWeatherMain(
feelsLike = from1.main.feelsLike,
humidity = from1.main.humidity,
pressure = from1.main.pressure,
temp = from1.main.temp,
tempMax = from1.main.tempMax,
tempMin = from1.main.tempMin
)

val sys = LocalCurrentWeatherSys(
country = from1.sys.country,
sunrise = from1.sys.sunrise,
sunset = from1.sys.sunset,
)

val remoteWeather = from1.weather.firstOrNull()
val weather = LocalCurrentWeatherWeather(
description = remoteWeather?.description ?: "",
icon = remoteWeather?.icon ?: "01d",
id = remoteWeather?.id ?: UInt.MIN_VALUE.toInt(),
main = remoteWeather?.main ?: ""
)

val wind = LocalCurrentWeatherWind(
deg = from1.wind.deg,
speed = from1.wind.speed
)

return LocalCurrentWeather(
base = from1.base,
cod = from1.cod,
dt = from1.dt,
id = from1.id,
name = from1.name,
timezone = from1.timezone,
visibility = from1.visibility,
updatedAt = dateRepo.mapDateTime(dateRepo.nowDateTime()),
clouds = clouds,
coord = coord,
main = main,
sys = sys,
weather = weather,
wind = wind
)
}

Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@ package com.wednesday.template.repo.weather
import com.wednesday.template.domain.weather.City
import com.wednesday.template.domain.weather.Weather
import com.wednesday.template.repo.date.DateRepo
import com.wednesday.template.service.weather.WeatherLocalService
import com.wednesday.template.service.weather.WeatherRemoteService
import com.wednesday.template.service.weather.OpenWeatherLocalService
import com.wednesday.template.service.weather.OpenWeatherRemoteService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -14,19 +14,18 @@ import kotlinx.coroutines.flow.onEach
import timber.log.Timber

class WeatherRepositoryImpl(
private val weatherRemoteService: WeatherRemoteService,
private val weatherLocalService: WeatherLocalService,
private val weatherRemoteService: OpenWeatherRemoteService,
private val weatherLocalService: OpenWeatherLocalService,
private val domainCityMapper: DomainCityMapper,
private val localCityMapper: LocalCityMapper,
private val localWeatherMapper: LocalWeatherMapper,
private val localDayWeatherMapper: LocalDayWeatherMapper,
private val domainWeatherMapper: DomainWeatherMapper,
private val dateRepo: DateRepo
) : WeatherRepository {

override suspend fun searchCities(searchTerm: String): List<City> {
Timber.tag(TAG).d("searchCities: searchTerm = $searchTerm")
return weatherRemoteService.searchCities(searchTerm)
return weatherRemoteService.geocodingSearch(searchTerm)
.let { domainCityMapper.mapRemoteCity(it) }
}

@@ -51,24 +50,27 @@ class WeatherRepositoryImpl(
override suspend fun removeCityAsFavourite(city: City) {
Timber.tag(TAG).d("removeCityAsFavourite: city = $city")
weatherLocalService.deleteFavoriteCity(localCityMapper.map(city))
weatherLocalService.deleteLocalCurrentWeather(city.lat, city.lon)
}

override suspend fun fetchWeatherForFavouriteCities(): Unit = coroutineScope {
Timber.tag(TAG).d("fetchWeatherForFavouriteCities() called")
val todayDate = dateRepo.todayDate()
val nowMillis = dateRepo.nowDateTimeAsLong()
val twoHours = 2 * 60 * 60 * 1000
weatherLocalService.getFavoriteCities().map {
async {
val dayWeatherList = weatherLocalService.getLocalDayWeather(woeid = it.woeid)
val isWeatherListStale =
dayWeatherList.find { dateRepo.mapDate(it.date) == todayDate } == null
val localCurrentWeather =
weatherLocalService.getLocalCurrentWeather(lat = it.lat, lon = it.lon)
val isWeatherDataStale =
localCurrentWeather != null && (nowMillis - localCurrentWeather.updatedAt.time) > twoHours

if (dayWeatherList.isEmpty() || isWeatherListStale) {
val remoteWeather = weatherRemoteService.weatherForCity(it.woeid)
if (localCurrentWeather == null || isWeatherDataStale) {
val searchQuery = it.name + if (it.state != null) ", ${it.state}" else ""
val remoteCurrentWeather =
weatherRemoteService.currentWeather(cityAndState = searchQuery)

weatherLocalService.deleteCurrentAndAddNewWeatherData(
woeid = it.woeid,
weather = localWeatherMapper.map(remoteWeather, it.woeid),
weatherList = localDayWeatherMapper.map(remoteWeather, it.woeid)
weatherLocalService.upsertLocalCurrentWeather(
weather = localWeatherMapper.map(remoteCurrentWeather, it.lat, it.lon)
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
package com.wednesday.template.repo.weather

import com.wednesday.template.repo.weather.models.dayWeatherMapperFromLocalDayWeather
import com.wednesday.template.repo.weather.models.localCityWithWeather
import com.wednesday.template.repo.weather.models.localDayWeather
import com.wednesday.template.repo.weather.models.weatherMappedFromLocalCityWithWeather
import com.wednesday.template.repo.weather.models.localWeather
import com.wednesday.template.repo.weather.models.weather
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.test.assertEquals

class DomainWeatherMapperImplTest {

private lateinit var domainDayWeatherMapper: DomainDayWeatherMapper
private lateinit var domainWeatherMapper: DomainWeatherMapperImpl

@Before
fun setUp() {
domainDayWeatherMapper = mock()
domainWeatherMapper = DomainWeatherMapperImpl(domainDayWeatherMapper)
domainWeatherMapper = DomainWeatherMapperImpl()
}

@Test
fun `Given LocalCityWithWeather, When map is called, Then Weather is returned with correct data`() {
// Given
val localCityWithWeather = localCityWithWeather
val localDayWeather = localDayWeather
whenever(domainDayWeatherMapper.map(listOf(localDayWeather))).thenReturn(
listOf(dayWeatherMapperFromLocalDayWeather)
)
val currentWeather = localWeather

// When
val result = domainWeatherMapper.map(localCityWithWeather)
val result = domainWeatherMapper.map(currentWeather)

// Then
assertEquals(expected = weatherMappedFromLocalCityWithWeather, actual = result)
verify(domainDayWeatherMapper, times(1)).map(listOf(localDayWeather))
assertEquals(expected = weather, actual = result)
}
}
Loading

0 comments on commit f17b702

Please sign in to comment.