├── weatherapp
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── assets
│ │ │ │ ├── koin.properties
│ │ │ │ └── json
│ │ │ │ │ ├── geocode_berlin.json
│ │ │ │ │ ├── geocode_paris.json
│ │ │ │ │ ├── geocode_madrid.json
│ │ │ │ │ ├── geocode_toulouse.json
│ │ │ │ │ ├── geocode_london.json
│ │ │ │ │ ├── weather_madrid.json
│ │ │ │ │ └── weather_toulouse.json
│ │ │ ├── res
│ │ │ │ ├── menu
│ │ │ │ │ └── menu_main.xml
│ │ │ │ ├── font
│ │ │ │ │ ├── indieflower.ttf
│ │ │ │ │ └── opensans_regular.ttf
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values-w820dp
│ │ │ │ │ └── dimens.xml
│ │ │ │ ├── anim
│ │ │ │ │ └── infinite_blinking_animation.xml
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── dimens.xml
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── drawable
│ │ │ │ │ ├── ic_edit_black_24dp.xml
│ │ │ │ │ ├── ic_location_on_black_24dp.xml
│ │ │ │ │ ├── ic_edit_location_black_24dp.xml
│ │ │ │ │ └── ic_search_black_24dp.xml
│ │ │ │ └── layout
│ │ │ │ │ ├── fragment_result_list.xml
│ │ │ │ │ ├── activity_splash.xml
│ │ │ │ │ ├── item_weather.xml
│ │ │ │ │ ├── activity_result.xml
│ │ │ │ │ ├── fragment_result_header.xml
│ │ │ │ │ └── activity_detail.xml
│ │ │ ├── kotlin
│ │ │ │ └── fr
│ │ │ │ │ └── ekito
│ │ │ │ │ └── myweatherapp
│ │ │ │ │ ├── util
│ │ │ │ │ ├── mvp
│ │ │ │ │ │ ├── BaseView.kt
│ │ │ │ │ │ ├── BasePresenter.kt
│ │ │ │ │ │ └── RxPresenter.kt
│ │ │ │ │ ├── rx
│ │ │ │ │ │ ├── SchedulerProvider.kt
│ │ │ │ │ │ ├── ApplicationSchedulerProvider.kt
│ │ │ │ │ │ └── RxWithExt.kt
│ │ │ │ │ ├── android
│ │ │ │ │ │ └── FragmentActivityExt.kt
│ │ │ │ │ └── mvvm
│ │ │ │ │ │ ├── RxViewModel.kt
│ │ │ │ │ │ └── SingleLiveEvent.kt
│ │ │ │ │ ├── domain
│ │ │ │ │ ├── ext
│ │ │ │ │ │ ├── GeocodeExt.kt
│ │ │ │ │ │ └── WeatherExt.kt
│ │ │ │ │ ├── entity
│ │ │ │ │ │ ├── DailyForecast.kt
│ │ │ │ │ │ └── WeatherCode.kt
│ │ │ │ │ └── repository
│ │ │ │ │ │ └── DailyForecastRepository.kt
│ │ │ │ │ ├── data
│ │ │ │ │ ├── local
│ │ │ │ │ │ ├── JsonReader.kt
│ │ │ │ │ │ ├── JavaReader.kt
│ │ │ │ │ │ ├── AndroidJsonReader.kt
│ │ │ │ │ │ ├── BaseReader.kt
│ │ │ │ │ │ └── FileDataSource.kt
│ │ │ │ │ ├── WeatherDataSource.kt
│ │ │ │ │ └── json
│ │ │ │ │ │ ├── Geocode.kt
│ │ │ │ │ │ └── Weather.kt
│ │ │ │ │ ├── view
│ │ │ │ │ ├── splash
│ │ │ │ │ │ ├── SplashContract.kt
│ │ │ │ │ │ ├── SplashPresenter.kt
│ │ │ │ │ │ └── SplashActivity.kt
│ │ │ │ │ ├── detail
│ │ │ │ │ │ ├── DetailContract.kt
│ │ │ │ │ │ ├── DetailPresenter.kt
│ │ │ │ │ │ └── DetailActivity.kt
│ │ │ │ │ └── weather
│ │ │ │ │ │ ├── WeatherListContract.kt
│ │ │ │ │ │ ├── list
│ │ │ │ │ │ ├── WeatherItem.kt
│ │ │ │ │ │ └── WeatherListAdapter.kt
│ │ │ │ │ │ ├── WeatherHeaderContract.kt
│ │ │ │ │ │ ├── WeatherListPresenter.kt
│ │ │ │ │ │ ├── WeatherHeaderPresenter.kt
│ │ │ │ │ │ ├── WeatherActivity.kt
│ │ │ │ │ │ ├── WeatherListFragment.kt
│ │ │ │ │ │ └── WeatherHeaderFragment.kt
│ │ │ │ │ ├── MainApplication.kt
│ │ │ │ │ └── di
│ │ │ │ │ ├── local_datasource_module.kt
│ │ │ │ │ ├── remote_datasource_module.kt
│ │ │ │ │ └── app_module.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── test
│ │ │ ├── resources
│ │ │ │ ├── koin.properties
│ │ │ │ ├── mockito-extensions
│ │ │ │ │ └── org.mockito.plugins.MockMaker
│ │ │ │ └── json
│ │ │ │ │ ├── geocode_berlin.json
│ │ │ │ │ ├── geocode_paris.json
│ │ │ │ │ ├── geocode_madrid.json
│ │ │ │ │ ├── geocode_toulouse.json
│ │ │ │ │ ├── geocode_london.json
│ │ │ │ │ ├── weather_madrid.json
│ │ │ │ │ └── weather_toulouse.json
│ │ │ └── java
│ │ │ │ └── fr
│ │ │ │ └── ekito
│ │ │ │ └── myweatherapp
│ │ │ │ ├── util
│ │ │ │ ├── TestSchedulerProvider.kt
│ │ │ │ └── MockitoKotlinHelpers.kt
│ │ │ │ ├── di
│ │ │ │ └── test_modules.kt
│ │ │ │ ├── ModuleCheckTest.kt
│ │ │ │ ├── mock
│ │ │ │ ├── MockedData.kt
│ │ │ │ ├── mvp
│ │ │ │ │ ├── DetailPresenterMockTest.kt
│ │ │ │ │ ├── SplashPresenterMockTest.kt
│ │ │ │ │ ├── WeatherListPresenterMockTest.kt
│ │ │ │ │ └── WeatherHeaderPresenterMockTest.kt
│ │ │ │ └── mvvm
│ │ │ │ │ ├── WeatherListViewModelMockTest.kt
│ │ │ │ │ ├── SplashViewModelMockTest.kt
│ │ │ │ │ ├── DetailViewModelMockTest.kt
│ │ │ │ │ └── WeatherHeaderViewModelMockTest.kt
│ │ │ │ └── integration
│ │ │ │ └── WeatherRepositoryTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── fr
│ │ │ └── ekito
│ │ │ └── myweatherapp
│ │ │ ├── room_test_modules.kt
│ │ │ ├── WeatherRepositoryTest.kt
│ │ │ └── WeatherDAOTest.kt
│ └── build.gradle
├── settings.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── build.gradle
├── versions.gradle
├── gradle.properties
├── gradlew.bat
├── gradlew
└── LICENSE
├── img
└── workshop_logo.png
├── .gitignore
└── README.md
/weatherapp/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/weatherapp/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/koin.properties:
--------------------------------------------------------------------------------
1 | SERVER_URL=https://my-weather-api.herokuapp.com/
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/koin.properties:
--------------------------------------------------------------------------------
1 | SERVER_URL=https://my-weather-api.herokuapp.com/
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/img/workshop_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/img/workshop_logo.png
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/weatherapp/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/font/indieflower.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/font/indieflower.ttf
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/font/opensans_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/font/opensans_regular.ttf
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.apk
2 | .idea/
3 | *.iml
4 | .gradle
5 | local.properties
6 | /.idea/workspace.xml
7 | /.idea/libraries
8 | .DS_Store
9 | /build
10 | /captures
11 | .externalNativeBuild
12 | build/
13 |
14 |
15 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/HEAD/weatherapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Architecture & Architecture Components
2 |
3 | 
4 |
5 | ## Hands-on
6 |
7 | Go to [Hands-on page](https://github.com/Ekito/2018-android-architecture-components-workshop/wiki) hosted on wiki
8 |
9 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/mvp/BaseView.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.mvp
2 |
3 | /**
4 | * View
5 | */
6 | interface BaseView> {
7 |
8 | fun showError(error: Throwable)
9 |
10 | val presenter: T
11 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/mvp/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.mvp
2 |
3 | /**
4 | * Presenter
5 | */
6 | interface BasePresenter {
7 |
8 | fun subscribe(view: V)
9 |
10 | fun unSubscribe()
11 |
12 | var view : V?
13 |
14 | }
--------------------------------------------------------------------------------
/weatherapp/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Nov 11 01:00:28 CET 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
7 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/rx/SchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.rx
2 |
3 | import io.reactivex.Scheduler
4 |
5 | /**
6 | * Rx Scheduler Provider
7 | */
8 | interface SchedulerProvider {
9 | fun io(): Scheduler
10 | fun ui(): Scheduler
11 | fun computation(): Scheduler
12 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/domain/ext/GeocodeExt.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.domain.ext
2 |
3 | import fr.ekito.myweatherapp.data.json.Geocode
4 | import fr.ekito.myweatherapp.data.json.Location
5 |
6 | /**
7 | * Extract Location from Geocode
8 | */
9 | fun Geocode.getLocation(): Location? = results.firstOrNull()?.geometry?.location
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/android/FragmentActivityExt.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNCHECKED_CAST")
2 |
3 | package fr.ekito.myweatherapp.util.android
4 |
5 | import android.support.v4.app.FragmentActivity
6 |
7 | /**
8 | * Retrieve argument from Activity intent
9 | */
10 | fun FragmentActivity.argument(key: String) =
11 | lazy { intent.extras[key] as? T ?: error("Intent Argument $key is missing") }
12 |
13 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/util/TestSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util
2 |
3 | import io.reactivex.schedulers.Schedulers
4 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
5 |
6 | class TestSchedulerProvider : SchedulerProvider {
7 | override fun io() = Schedulers.trampoline()
8 |
9 | override fun ui() = Schedulers.trampoline()
10 |
11 | override fun computation() = Schedulers.trampoline()
12 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/anim/infinite_blinking_animation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/local/JsonReader.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.local
2 |
3 | import fr.ekito.myweatherapp.data.json.Geocode
4 | import fr.ekito.myweatherapp.data.json.Location
5 | import fr.ekito.myweatherapp.data.json.Weather
6 |
7 | /**
8 | * Json reader
9 | */
10 | interface JsonReader {
11 | fun getAllLocations(): Map
12 | fun getWeather(name: String): Weather
13 | fun getGeocode(name: String): Geocode
14 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/rx/ApplicationSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.rx
2 |
3 | import io.reactivex.android.schedulers.AndroidSchedulers
4 | import io.reactivex.schedulers.Schedulers
5 |
6 | /**
7 | * Application providers
8 | */
9 | class ApplicationSchedulerProvider : SchedulerProvider {
10 | override fun io() = Schedulers.io()
11 |
12 | override fun ui() = AndroidSchedulers.mainThread()
13 |
14 | override fun computation() = Schedulers.computation()
15 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My Weather App
3 | Error while loading weather
4 | Enter your location
5 | e.g: Paris
6 | SEARCH
7 | CANCEL
8 | Retry
9 | Loading location
10 |
11 |
--------------------------------------------------------------------------------
/weatherapp/app/src/androidTest/java/fr/ekito/myweatherapp/room_test_modules.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp
2 | //
3 | //import android.arch.persistence.room.Room
4 | //import fr.ekito.myweatherapp.data.room.WeatherDatabase
5 | //import org.koin.dsl.module.module
6 | //
7 | //// Room In memroy database
8 | //val roomTestModule = module(override = true) {
9 | // single {
10 | // Room.inMemoryDatabaseBuilder(get(), WeatherDatabase::class.java)
11 | // .allowMainThreadQueries()
12 | // .build()
13 | // }
14 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/splash/SplashContract.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.splash
2 |
3 | import fr.ekito.myweatherapp.util.mvp.BasePresenter
4 | import fr.ekito.myweatherapp.util.mvp.BaseView
5 |
6 | /**
7 | * Weather MVP Contract
8 | */
9 | interface SplashContract {
10 | interface View : BaseView {
11 | fun showIsLoaded()
12 | fun showIsLoading()
13 | }
14 |
15 | interface Presenter : BasePresenter {
16 | fun getLastWeather()
17 | }
18 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/drawable/ic_edit_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/weatherapp/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "versions.gradle"
2 |
3 | buildscript {
4 | apply from: "versions.gradle"
5 |
6 | repositories {
7 | google()
8 | jcenter()
9 | }
10 | dependencies {
11 | classpath "com.android.tools.build:gradle:$android_plugin_version"
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | // mavenLocal()
19 | jcenter()
20 | google()
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/detail/DetailContract.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.detail
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 | import fr.ekito.myweatherapp.util.mvp.BasePresenter
5 | import fr.ekito.myweatherapp.util.mvp.BaseView
6 |
7 | interface DetailContract {
8 | interface View : BaseView {
9 | fun showDetail(weather: DailyForecast)
10 | }
11 |
12 | interface Presenter : BasePresenter {
13 | fun getDetail(id: String)
14 | }
15 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/drawable/ic_location_on_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherListContract.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import fr.ekito.myweatherapp.util.mvp.BasePresenter
4 | import fr.ekito.myweatherapp.util.mvp.BaseView
5 | import fr.ekito.myweatherapp.view.weather.list.WeatherItem
6 |
7 | interface WeatherListContract {
8 | interface View : BaseView {
9 | fun showWeatherItemList(newList: List)
10 | }
11 |
12 | interface Presenter : BasePresenter {
13 | fun getWeatherList()
14 | }
15 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/list/WeatherItem.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather.list
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 |
5 | /**
6 | * Result Item - Display in ResultList View Adapter
7 | */
8 | data class WeatherItem(val id: String, val day: String, val icon: String, val color : Int) {
9 | companion object {
10 | fun from(weather: DailyForecast) = WeatherItem(
11 | weather.id,
12 | weather.day,
13 | weather.icon,
14 | weather.colorCode
15 | )
16 | }
17 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/drawable/ic_edit_location_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/weatherapp/versions.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | // Kotlin
3 | kotlin_version = '1.2.71'
4 | koin_version = '1.0.1'
5 |
6 | // Android Sdk and tools
7 | target_sdk_version = 27
8 | compile_sdk_version = 27
9 | android_plugin_version = "3.1.4"
10 | build_tools_version = '27.0.3'
11 |
12 | // Android Components
13 | support_lib_version = '27.1.1'
14 | android_arch_version = "1.1.1"
15 | android_room_version = "1.1.1"
16 |
17 | // Other frameworks
18 | okhttp_version = '3.8.1'
19 | retrofit_version = '2.2.0'
20 | rxjava_version = '2.1.7'
21 | anko_version='0.10.4'
22 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/drawable/ic_search_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/fragment_result_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/rx/RxWithExt.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.rx
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.Single
5 |
6 |
7 | /**
8 | * Use SchedulerProvider configuration for Observable
9 | */
10 | fun Completable.with(schedulerProvider: SchedulerProvider): Completable =
11 | this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())
12 |
13 | /**
14 | * Use SchedulerProvider configuration for Single
15 | */
16 | fun Single.with(schedulerProvider: SchedulerProvider): Single =
17 | this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp
2 |
3 | import android.app.Application
4 | import com.joanzapata.iconify.Iconify
5 | import com.joanzapata.iconify.fonts.WeathericonsModule
6 | import fr.ekito.myweatherapp.di.offlineWeatherApp
7 | import org.koin.android.ext.android.startKoin
8 |
9 | /**
10 | * Main Application
11 | */
12 | class MainApplication : Application() {
13 |
14 | override fun onCreate() {
15 | super.onCreate()
16 |
17 | // start Koin context
18 | startKoin(this, offlineWeatherApp)
19 |
20 | Iconify
21 | .with(WeathericonsModule())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/di/local_datasource_module.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.di
2 |
3 | import fr.ekito.myweatherapp.data.WeatherDataSource
4 | import fr.ekito.myweatherapp.data.local.AndroidJsonReader
5 | import fr.ekito.myweatherapp.data.local.FileDataSource
6 | import fr.ekito.myweatherapp.data.local.JsonReader
7 | import org.koin.android.ext.koin.androidApplication
8 | import org.koin.dsl.module.module
9 |
10 | /**
11 | * Local Json Files Datasource
12 | */
13 | val localAndroidDataSourceModule = module {
14 | single { AndroidJsonReader(androidApplication()) }
15 | single { FileDataSource(get(), true) }
16 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherHeaderContract.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 | import fr.ekito.myweatherapp.util.mvp.BasePresenter
5 | import fr.ekito.myweatherapp.util.mvp.BaseView
6 |
7 | interface WeatherHeaderContract {
8 | interface View : BaseView {
9 | fun showWeather(location : String, weather: DailyForecast)
10 | fun showLocationSearchSucceed(location: String)
11 | fun showLocationSearchFailed(location: String, error: Throwable)
12 | }
13 |
14 | interface Presenter : BasePresenter {
15 | fun getWeatherOfTheDay()
16 | fun loadNewLocation(location: String)
17 | }
18 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/mvvm/RxViewModel.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.mvvm
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.support.annotation.CallSuper
5 | import io.reactivex.disposables.CompositeDisposable
6 | import io.reactivex.disposables.Disposable
7 |
8 | /**
9 | * ViewModel for Rx Jobs
10 | *
11 | * launch() - launch a Rx request
12 | * clear all request on stop
13 | */
14 | abstract class RxViewModel : ViewModel() {
15 |
16 | private val disposables = CompositeDisposable()
17 |
18 | fun launch(job: () -> Disposable) {
19 | disposables.add(job())
20 | }
21 |
22 | @CallSuper
23 | override fun onCleared() {
24 | super.onCleared()
25 | disposables.clear()
26 | }
27 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/WeatherDataSource.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data
2 |
3 | import fr.ekito.myweatherapp.data.json.Geocode
4 | import fr.ekito.myweatherapp.data.json.Weather
5 | import io.reactivex.Single
6 | import retrofit2.http.GET
7 | import retrofit2.http.Headers
8 | import retrofit2.http.Query
9 |
10 | /**
11 | * Weather datasource - Retrofit tagged
12 | */
13 | interface WeatherDataSource {
14 |
15 | @GET("/geocode")
16 | @Headers("Content-type: application/json")
17 | fun geocode(@Query("location") address: String): Single
18 |
19 | @GET("/weather")
20 | @Headers("Content-type: application/json")
21 | fun weather(@Query("lat") lat: Double?, @Query("lon") lon: Double?, @Query("lang") lang: String): Single
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/local/JavaReader.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.local
2 |
3 | import java.io.File
4 |
5 | /**
6 | * Java Json reader for Tests
7 | */
8 | class JavaReader : BaseReader() {
9 |
10 | private fun basePath(): String? {
11 | val classLoader: ClassLoader = JavaReader::class.java.classLoader
12 | val path: String? = classLoader.getResource("json/")?.path
13 | return path
14 | }
15 |
16 | override fun getAllFiles(): List {
17 | return basePath()?.let {
18 | val list = File(it).list()
19 | list.toList()
20 | }!!
21 | }
22 |
23 | override fun readJsonFile(jsonFile: String): String =
24 | File("${basePath()}/$jsonFile").readLines().joinToString(separator = "\n")
25 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/mvp/RxPresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.mvp
2 |
3 | import android.support.annotation.CallSuper
4 | import io.reactivex.disposables.CompositeDisposable
5 | import io.reactivex.disposables.Disposable
6 |
7 | /**
8 | * Base Presenter feature - for Rx Jobs
9 | *
10 | * launch() - launch a Rx request
11 | * clear all request on stop
12 | */
13 | abstract class RxPresenter : BasePresenter {
14 |
15 | private val disposables = CompositeDisposable()
16 |
17 | fun launch(job: () -> Disposable) {
18 | disposables.add(job())
19 | }
20 |
21 | override fun subscribe(view: V) {
22 | this.view = view
23 | }
24 |
25 | @CallSuper
26 | override fun unSubscribe() {
27 | disposables.clear()
28 | view = null
29 | }
30 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/local/AndroidJsonReader.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.local
2 |
3 | import android.app.Application
4 | import java.io.BufferedReader
5 | import java.io.InputStreamReader
6 |
7 | /**
8 | * Read Json File from assets/json
9 | */
10 | class AndroidJsonReader(val application: Application) : BaseReader() {
11 |
12 | override fun getAllFiles(): List = application.assets.list("json").toList()
13 |
14 | override fun readJsonFile(jsonFile: String): String {
15 | val buf = StringBuilder()
16 | val json = application.assets.open("json/" + jsonFile)
17 | BufferedReader(InputStreamReader(json, "UTF-8"))
18 | .use {
19 | val list = it.lineSequence().toList()
20 | buf.append(list.joinToString("\n"))
21 | }
22 |
23 | return buf.toString()
24 | }
25 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 | 16dp
8 |
9 | 8dp
10 | 2dp
11 |
12 | 44dp
13 | 44dp
14 |
15 | 13sp
16 |
17 | 14sp
18 | 16sp
19 | 18sp
20 | 19sp
21 | 24sp
22 | 34sp
23 |
24 | 72dp
25 |
26 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/detail/DetailPresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.detail
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.util.mvp.RxPresenter
5 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
6 | import fr.ekito.myweatherapp.util.rx.with
7 |
8 | class DetailPresenter(
9 | private val dailyForecastRepository: DailyForecastRepository,
10 | private val schedulerProvider: SchedulerProvider
11 | ) : RxPresenter(), DetailContract.Presenter {
12 |
13 | override var view: DetailContract.View? = null
14 |
15 | override fun getDetail(id: String) {
16 | launch {
17 | dailyForecastRepository.getWeatherDetail(id).with(schedulerProvider).subscribe(
18 | { detail ->
19 | view?.showDetail(detail)
20 | }, { error -> view?.showError(error) })
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/di/test_modules.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.di
2 |
3 | import fr.ekito.myweatherapp.data.WeatherDataSource
4 | import fr.ekito.myweatherapp.data.local.FileDataSource
5 | import fr.ekito.myweatherapp.data.local.JavaReader
6 | import fr.ekito.myweatherapp.data.local.JsonReader
7 | import fr.ekito.myweatherapp.util.TestSchedulerProvider
8 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
9 | import org.koin.dsl.module.module
10 |
11 |
12 | /**
13 | * Local java json repository
14 | */
15 | val localJavaDatasourceModule = module(override = true) {
16 | single { JavaReader() }
17 | single { FileDataSource(get(), false) }
18 | }
19 |
20 | /**
21 | * Test Rx
22 | */
23 | val testRxModule = module(override = true) {
24 | // provided components
25 | single { TestSchedulerProvider() }
26 | }
27 |
28 | val testWeatherApp = offlineWeatherApp + testRxModule + localJavaDatasourceModule
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/splash/SplashPresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.splash
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.util.mvp.RxPresenter
5 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
6 | import fr.ekito.myweatherapp.util.rx.with
7 |
8 | class SplashPresenter(
9 | private val dailyForecastRepository: DailyForecastRepository,
10 | private val schedulerProvider: SchedulerProvider
11 | ) : RxPresenter(), SplashContract.Presenter {
12 |
13 | override var view: SplashContract.View? = null
14 |
15 | override fun getLastWeather() {
16 | view?.showIsLoading()
17 | launch {
18 | dailyForecastRepository.getWeather().with(schedulerProvider)
19 | .toCompletable()
20 | .subscribe({ view?.showIsLoaded() },
21 | { error -> view?.showError(error) })
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/domain/ext/WeatherExt.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.domain.ext
2 |
3 | import fr.ekito.myweatherapp.data.json.Forecastday
4 | import fr.ekito.myweatherapp.data.json.Forecastday_
5 | import fr.ekito.myweatherapp.data.json.Weather
6 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
7 | import fr.ekito.myweatherapp.domain.entity.PREFIX
8 |
9 | /**
10 | * Extract Weather DailyForecast list from Weather
11 | */
12 | fun Weather.getDailyForecasts(location: String): List {
13 | val txtList: List = forecast?.txtForecast?.forecastday.orEmpty()
14 | return forecast?.simpleforecast?.forecastday.orEmpty()
15 | .map { f: Forecastday_ ->
16 | DailyForecast.from(
17 | location,
18 | f
19 | )
20 | }
21 | .map { f ->
22 | f.copy(
23 | fullText = txtList.firstOrNull { it.title ?: "" == f.day }?.fcttext ?: ""
24 | )
25 | }
26 | .filter { f -> !f.icon.startsWith(PREFIX) }
27 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2196F3
4 | #1976D2
5 | #BBDEFB
6 | #ffa000
7 | #ffa000
8 |
9 | #212121
10 | #777777
11 | #212121
12 | @color/accent_dark
13 | #BDBDBD
14 |
15 | #fa315b
16 | #e4e3e4
17 | #F5F5F5
18 | #FAFAFA
19 | #FFFFFF
20 |
21 | #455a64
22 | #42a5f5
23 | #26a69a
24 | #ffb74d
25 | #e57373
26 |
27 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/ModuleCheckTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import fr.ekito.myweatherapp.di.offlineWeatherApp
6 | import fr.ekito.myweatherapp.di.onlineWeatherApp
7 | import fr.ekito.myweatherapp.di.testWeatherApp
8 | import org.junit.After
9 | import org.junit.Test
10 | import org.koin.dsl.module.module
11 | import org.koin.test.KoinTest
12 | import org.koin.test.checkModules
13 | import org.mockito.Mockito.mock
14 |
15 | /**
16 | * Test Koin modules
17 | */
18 | class ModuleCheckTest : KoinTest {
19 |
20 | val mockedAndroidContext = module {
21 | single { mock(Application::class.java) }
22 | }
23 |
24 | @After
25 | fun after() {
26 | }
27 |
28 | @Test
29 | fun testRemoteConfiguration() {
30 | checkModules(onlineWeatherApp)
31 | }
32 |
33 | @Test
34 | fun testLocalConfiguration() {
35 | checkModules(offlineWeatherApp + mockedAndroidContext)
36 | }
37 |
38 | @Test
39 | fun testTestConfiguration() {
40 | checkModules(testWeatherApp + mockedAndroidContext)
41 | }
42 | }
--------------------------------------------------------------------------------
/weatherapp/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 | #Enable daemon
10 | org.gradle.daemon=true
11 |
12 | # Specifies the JVM arguments used for the daemon process.
13 | # The setting is particularly useful for tweaking memory settings.
14 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
15 | org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
16 |
17 | # When configured, Gradle will run in incubating parallel mode.
18 | # This option should only be used with decoupled projects. More details, visit
19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
20 | org.gradle.parallel=true
21 |
22 | # Disable configure on demand.
23 | org.gradle.configureondemand=false
24 |
25 | # Android Studio 2.2+
26 | android.enableBuildCache=true
27 |
28 | # Kotlin
29 | kotlin.incremental=true
30 |
31 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/json/Geocode.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.json
2 |
3 | import java.util.*
4 |
5 | data class AddressComponent(
6 | val long_name: String? = null,
7 | val short_name: String? = null,
8 | val types: List = arrayListOf()
9 | )
10 |
11 | data class Geocode(
12 | val results: List = ArrayList(),
13 | val status: String? = null
14 | )
15 |
16 | data class Geometry(
17 | val location: Location? = null,
18 | val location_type: String? = null,
19 | val viewport: Viewport? = null
20 | )
21 |
22 | data class Location(
23 | val lat: Double? = null,
24 | val lng: Double? = null
25 | )
26 |
27 | data class Northeast(
28 | val lat: Double? = null,
29 | val lng: Double? = null
30 | )
31 |
32 | data class Result(
33 | val address_components: List = ArrayList(),
34 | val formatted_address: String? = null,
35 | val geometry: Geometry? = null,
36 | val placeId: String? = null,
37 | val types: List = ArrayList()
38 | )
39 |
40 | data class Southwest(
41 | val lat: Double? = null,
42 | val lng: Double? = null
43 | )
44 |
45 | class Viewport(
46 | val northeast: Northeast? = null,
47 | val southwest: Southwest? = null
48 | )
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/MockedData.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.mock
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 | import fr.ekito.myweatherapp.domain.entity.Humidity
5 | import fr.ekito.myweatherapp.domain.entity.Temperature
6 | import fr.ekito.myweatherapp.domain.entity.Wind
7 |
8 | /**
9 | * Mock Weather Data
10 | */
11 | object MockedData {
12 |
13 | val location = "Location"
14 |
15 | val mockList = listOf(
16 | DailyForecast(
17 | location,
18 | "d1",
19 | "",
20 | "",
21 | "",
22 | "",
23 | Temperature("1", "1"),
24 | Wind(0, ""),
25 | Humidity(0)
26 | ),
27 | DailyForecast(
28 | location,
29 | "d2",
30 | "",
31 | "",
32 | "",
33 | "",
34 | Temperature("1", "2"),
35 | Wind(0, ""),
36 | Humidity(0)
37 | ),
38 | DailyForecast(
39 | location,
40 | "d3",
41 | "",
42 | "",
43 | "",
44 | "",
45 | Temperature("3", "4"),
46 | Wind(0, ""),
47 | Humidity(0)
48 | )
49 | )
50 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherListPresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.util.mvp.RxPresenter
5 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
6 | import fr.ekito.myweatherapp.util.rx.with
7 | import fr.ekito.myweatherapp.view.weather.list.WeatherItem
8 |
9 | /**
10 | * Weather Presenter
11 | */
12 | class WeatherListPresenter(
13 | private val dailyForecastRepository: DailyForecastRepository,
14 | private val schedulerProvider: SchedulerProvider
15 | ) : RxPresenter(), WeatherListContract.Presenter {
16 |
17 | override var view: WeatherListContract.View? = null
18 |
19 | override fun getWeatherList() {
20 | launch {
21 | dailyForecastRepository.getWeather()
22 | .with(schedulerProvider)
23 | .subscribe(
24 | { weatherList ->
25 | view?.showWeatherItemList(
26 | weatherList.map { WeatherItem.from(it) }.takeLast(weatherList.size - 1)
27 | )
28 | },
29 | { error -> view?.showError(error) })
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/geocode_berlin.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Berlin",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Berlin",
17 | "types": [
18 | "administrative_area_level_1",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "DE",
25 | "types": [
26 | "country",
27 | "political"
28 | ]
29 | }
30 | ],
31 | "formatted_address": "Berlin, Germany",
32 | "geometry": {
33 | "location": {
34 | "lat": 52.52000659999999,
35 | "lng": 13.404954
36 | },
37 | "location_type": "APPROXIMATE",
38 | "viewport": {
39 | "northeast": {
40 | "lat": 52.6754542,
41 | "lng": 13.7611175
42 | },
43 | "southwest": {
44 | "lat": 52.33962959999999,
45 | "lng": 13.0911733
46 | }
47 | }
48 | },
49 | "place_id": "ChIJAVkDPzdOqEcRcDteW0YgIQQ",
50 | "types": [
51 | "locality",
52 | "political"
53 | ]
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/geocode_berlin.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Berlin",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Berlin",
17 | "types": [
18 | "administrative_area_level_1",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "DE",
25 | "types": [
26 | "country",
27 | "political"
28 | ]
29 | }
30 | ],
31 | "formatted_address": "Berlin, Germany",
32 | "geometry": {
33 | "location": {
34 | "lat": 52.52000659999999,
35 | "lng": 13.404954
36 | },
37 | "location_type": "APPROXIMATE",
38 | "viewport": {
39 | "northeast": {
40 | "lat": 52.6754542,
41 | "lng": 13.7611175
42 | },
43 | "southwest": {
44 | "lat": 52.33962959999999,
45 | "lng": 13.0911733
46 | }
47 | }
48 | },
49 | "place_id": "ChIJAVkDPzdOqEcRcDteW0YgIQQ",
50 | "types": [
51 | "locality",
52 | "political"
53 | ]
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/local/BaseReader.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.local
2 |
3 | import com.google.gson.Gson
4 | import fr.ekito.myweatherapp.data.json.Geocode
5 | import fr.ekito.myweatherapp.data.json.Location
6 | import fr.ekito.myweatherapp.data.json.Weather
7 |
8 | /**
9 | * Common parts for Json reader
10 | */
11 | abstract class BaseReader : JsonReader {
12 |
13 | private val gson = Gson()
14 | private val geocode_prefix = "geocode_"
15 | private val weather_prefix = "weather_"
16 | private val json_file = ".json"
17 |
18 | override fun getAllLocations(): Map {
19 | val list = getAllFiles()
20 | return list.filter { it.startsWith(geocode_prefix) }.map {
21 | val name = it.replace(geocode_prefix, "").replace(".json", "")
22 | val geocode = getGeocode(name)
23 | // pair result
24 | Pair(geocode.results[0].geometry!!.location!!, name)
25 | }.toMap() // direct to map
26 | }
27 |
28 | override fun getGeocode(name: String): Geocode =
29 | gson.fromJson(readJsonFile(geocode_prefix + name + json_file), Geocode::class.java)
30 |
31 | override fun getWeather(name: String): Weather =
32 | gson.fromJson(readJsonFile(weather_prefix + name + json_file), Weather::class.java)
33 |
34 | abstract fun getAllFiles(): List
35 |
36 | abstract fun readJsonFile(jsonFile: String): String
37 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherHeaderPresenter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.util.mvp.RxPresenter
5 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
6 | import fr.ekito.myweatherapp.util.rx.with
7 |
8 | class WeatherHeaderPresenter(
9 | private val dailyForecastRepository: DailyForecastRepository,
10 | private val schedulerProvider: SchedulerProvider
11 | ) : RxPresenter(), WeatherHeaderContract.Presenter {
12 |
13 | override var view: WeatherHeaderContract.View? = null
14 |
15 | override fun loadNewLocation(location: String) {
16 | launch {
17 | dailyForecastRepository.getWeather(location).toCompletable()
18 | .with(schedulerProvider)
19 | .subscribe(
20 | { view?.showLocationSearchSucceed(location) },
21 | { error -> view?.showLocationSearchFailed(location, error) })
22 | }
23 | }
24 |
25 | override fun getWeatherOfTheDay() {
26 | launch {
27 | dailyForecastRepository.getWeather()
28 | .map { it.first() }
29 | .with(schedulerProvider)
30 | .subscribe(
31 | { weather -> view?.showWeather(weather.location, weather) },
32 | { error -> view?.showError(error) })
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/geocode_paris.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Paris",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Paris",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Île-de-France",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "FR",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Paris, France",
40 | "geometry": {
41 | "location": {
42 | "lat": 48.856614,
43 | "lng": 2.3522219
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 48.9021449,
49 | "lng": 2.4699208
50 | },
51 | "southwest": {
52 | "lat": 48.815573,
53 | "lng": 2.225193
54 | }
55 | }
56 | },
57 | "place_id": "ChIJD7fiBh9u5kcRYJSMaMOCCwQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/geocode_paris.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Paris",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Paris",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Île-de-France",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "FR",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Paris, France",
40 | "geometry": {
41 | "location": {
42 | "lat": 48.856614,
43 | "lng": 2.3522219
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 48.9021449,
49 | "lng": 2.4699208
50 | },
51 | "southwest": {
52 | "lat": 48.815573,
53 | "lng": 2.225193
54 | }
55 | }
56 | },
57 | "place_id": "ChIJD7fiBh9u5kcRYJSMaMOCCwQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/geocode_madrid.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Madrid",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "M",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Community of Madrid",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "ES",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Madrid, Spain",
40 | "geometry": {
41 | "location": {
42 | "lat": 40.4167754,
43 | "lng": -3.7037902
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 40.5638447,
49 | "lng": -3.5249115
50 | },
51 | "southwest": {
52 | "lat": 40.3120639,
53 | "lng": -3.8341618
54 | }
55 | }
56 | },
57 | "place_id": "ChIJgTwKgJcpQg0RaSKMYcHeNsQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/geocode_toulouse.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Toulouse",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Haute-Garonne",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Occitanie",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "FR",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Toulouse, France",
40 | "geometry": {
41 | "location": {
42 | "lat": 43.604652,
43 | "lng": 1.444209
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 43.6686919,
49 | "lng": 1.515354
50 | },
51 | "southwest": {
52 | "lat": 43.532708,
53 | "lng": 1.350328
54 | }
55 | }
56 | },
57 | "place_id": "ChIJ_1J17G-7rhIRMBBBL5z2BgQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/geocode_madrid.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Madrid",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "M",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Community of Madrid",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "ES",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Madrid, Spain",
40 | "geometry": {
41 | "location": {
42 | "lat": 40.4167754,
43 | "lng": -3.7037902
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 40.5638447,
49 | "lng": -3.5249115
50 | },
51 | "southwest": {
52 | "lat": 40.3120639,
53 | "lng": -3.8341618
54 | }
55 | }
56 | },
57 | "place_id": "ChIJgTwKgJcpQg0RaSKMYcHeNsQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/geocode_toulouse.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "Toulouse",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "Haute-Garonne",
17 | "types": [
18 | "administrative_area_level_2",
19 | "political"
20 | ]
21 | },
22 | {
23 | "lang_name": null,
24 | "short_name": "Occitanie",
25 | "types": [
26 | "administrative_area_level_1",
27 | "political"
28 | ]
29 | },
30 | {
31 | "lang_name": null,
32 | "short_name": "FR",
33 | "types": [
34 | "country",
35 | "political"
36 | ]
37 | }
38 | ],
39 | "formatted_address": "Toulouse, France",
40 | "geometry": {
41 | "location": {
42 | "lat": 43.604652,
43 | "lng": 1.444209
44 | },
45 | "location_type": "APPROXIMATE",
46 | "viewport": {
47 | "northeast": {
48 | "lat": 43.6686919,
49 | "lng": 1.515354
50 | },
51 | "southwest": {
52 | "lat": 43.532708,
53 | "lng": 1.350328
54 | }
55 | }
56 | },
57 | "place_id": "ChIJ_1J17G-7rhIRMBBBL5z2BgQ",
58 | "types": [
59 | "locality",
60 | "political"
61 | ]
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/di/remote_datasource_module.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.di
2 |
3 | import fr.ekito.myweatherapp.data.WeatherDataSource
4 | import fr.ekito.myweatherapp.di.Properties.SERVER_URL
5 | import okhttp3.OkHttpClient
6 | import okhttp3.logging.HttpLoggingInterceptor
7 | import org.koin.dsl.module.module
8 | import retrofit2.Retrofit
9 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
10 | import retrofit2.converter.gson.GsonConverterFactory
11 | import java.util.concurrent.TimeUnit
12 |
13 | /**
14 | * Remote Web Service datasource
15 | */
16 | val remoteDataSourceModule = module {
17 | // provided web components
18 | single { createOkHttpClient() }
19 | // Fill property
20 | single { createWebService(get(), getProperty(SERVER_URL)) }
21 | }
22 |
23 | object Properties {
24 | const val SERVER_URL = "SERVER_URL"
25 | }
26 |
27 | fun createOkHttpClient(): OkHttpClient {
28 | val httpLoggingInterceptor = HttpLoggingInterceptor()
29 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC
30 | return OkHttpClient.Builder()
31 | .connectTimeout(60L, TimeUnit.SECONDS)
32 | .readTimeout(60L, TimeUnit.SECONDS)
33 | .addInterceptor(httpLoggingInterceptor).build()
34 | }
35 |
36 | inline fun createWebService(okHttpClient: OkHttpClient, url: String): T {
37 | val retrofit = Retrofit.Builder()
38 | .baseUrl(url)
39 | .client(okHttpClient)
40 | .addConverterFactory(GsonConverterFactory.create())
41 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
42 | return retrofit.create(T::class.java)
43 | }
44 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
22 |
23 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/geocode_london.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "London",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "London",
17 | "types": [
18 | "postal_town"
19 | ]
20 | },
21 | {
22 | "lang_name": null,
23 | "short_name": "Greater London",
24 | "types": [
25 | "administrative_area_level_2",
26 | "political"
27 | ]
28 | },
29 | {
30 | "lang_name": null,
31 | "short_name": "England",
32 | "types": [
33 | "administrative_area_level_1",
34 | "political"
35 | ]
36 | },
37 | {
38 | "lang_name": null,
39 | "short_name": "GB",
40 | "types": [
41 | "country",
42 | "political"
43 | ]
44 | }
45 | ],
46 | "formatted_address": "London, UK",
47 | "geometry": {
48 | "location": {
49 | "lat": 51.5073509,
50 | "lng": -0.1277583
51 | },
52 | "location_type": "APPROXIMATE",
53 | "viewport": {
54 | "northeast": {
55 | "lat": 51.6723432,
56 | "lng": 0.148271
57 | },
58 | "southwest": {
59 | "lat": 51.38494009999999,
60 | "lng": -0.3514683
61 | }
62 | }
63 | },
64 | "place_id": "ChIJdd4hrwug2EcRmSrV3Vo6llI",
65 | "types": [
66 | "locality",
67 | "political"
68 | ]
69 | }
70 | ]
71 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/geocode_london.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "OK",
3 | "results": [
4 | {
5 | "address_components": [
6 | {
7 | "lang_name": null,
8 | "short_name": "London",
9 | "types": [
10 | "locality",
11 | "political"
12 | ]
13 | },
14 | {
15 | "lang_name": null,
16 | "short_name": "London",
17 | "types": [
18 | "postal_town"
19 | ]
20 | },
21 | {
22 | "lang_name": null,
23 | "short_name": "Greater London",
24 | "types": [
25 | "administrative_area_level_2",
26 | "political"
27 | ]
28 | },
29 | {
30 | "lang_name": null,
31 | "short_name": "England",
32 | "types": [
33 | "administrative_area_level_1",
34 | "political"
35 | ]
36 | },
37 | {
38 | "lang_name": null,
39 | "short_name": "GB",
40 | "types": [
41 | "country",
42 | "political"
43 | ]
44 | }
45 | ],
46 | "formatted_address": "London, UK",
47 | "geometry": {
48 | "location": {
49 | "lat": 51.5073509,
50 | "lng": -0.1277583
51 | },
52 | "location_type": "APPROXIMATE",
53 | "viewport": {
54 | "northeast": {
55 | "lat": 51.6723432,
56 | "lng": 0.148271
57 | },
58 | "southwest": {
59 | "lat": 51.38494009999999,
60 | "lng": -0.3514683
61 | }
62 | }
63 | },
64 | "place_id": "ChIJdd4hrwug2EcRmSrV3Vo6llI",
65 | "types": [
66 | "locality",
67 | "political"
68 | ]
69 | }
70 | ]
71 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/integration/WeatherRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.integration
2 |
3 | import fr.ekito.myweatherapp.di.testWeatherApp
4 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
5 | import junit.framework.Assert
6 | import org.junit.After
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.koin.standalone.StandAloneContext.startKoin
10 | import org.koin.standalone.StandAloneContext.stopKoin
11 | import org.koin.standalone.inject
12 | import org.koin.test.KoinTest
13 |
14 | class WeatherRepositoryTest : KoinTest {
15 |
16 | val repository by inject()
17 |
18 | val location = "Paris"
19 |
20 | @Before
21 | fun before() {
22 | startKoin(testWeatherApp)
23 | }
24 |
25 | @After
26 | fun after() {
27 | stopKoin()
28 | }
29 |
30 | @Test
31 | fun testGetWeatherSuccess() {
32 | repository.getWeather(location).blockingGet()
33 | val test = repository.getWeather(location).test()
34 | test.awaitTerminalEvent()
35 | test.assertComplete()
36 | }
37 |
38 | @Test
39 | fun testCachedWeather() {
40 | val l1 = repository.getWeather("Paris").blockingGet()
41 | val l2 = repository.getWeather("Toulouse").blockingGet()
42 | val l3 = repository.getWeather().blockingGet()
43 |
44 | Assert.assertEquals(l3, l2)
45 | Assert.assertNotSame(l1, l2)
46 | }
47 |
48 | @Test
49 | fun testGetDetail() {
50 | repository.getWeather(location).blockingGet()
51 | val list = repository.getWeather(location).blockingGet()
52 | val first = list.first()
53 | val test = repository.getWeatherDetail(first.id).test()
54 | test.awaitTerminalEvent()
55 | test.assertValue { it == first }
56 | }
57 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/di/app_module.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.di
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepositoryImpl
5 | import fr.ekito.myweatherapp.util.rx.ApplicationSchedulerProvider
6 | import fr.ekito.myweatherapp.util.rx.SchedulerProvider
7 | import fr.ekito.myweatherapp.view.detail.DetailContract
8 | import fr.ekito.myweatherapp.view.detail.DetailPresenter
9 | import fr.ekito.myweatherapp.view.splash.SplashContract
10 | import fr.ekito.myweatherapp.view.splash.SplashPresenter
11 | import fr.ekito.myweatherapp.view.weather.WeatherHeaderContract
12 | import fr.ekito.myweatherapp.view.weather.WeatherHeaderPresenter
13 | import fr.ekito.myweatherapp.view.weather.WeatherListContract
14 | import fr.ekito.myweatherapp.view.weather.WeatherListPresenter
15 | import org.koin.dsl.module.module
16 |
17 | /**
18 | * App Components
19 | */
20 | val weatherAppModule = module {
21 | // Presenter for Search View
22 | factory { SplashPresenter(get(), get()) }
23 |
24 | // Presenter for ResultHeader View
25 | factory { WeatherHeaderPresenter(get(), get()) }
26 |
27 | // Presenter for ResultList View
28 | factory { WeatherListPresenter(get(), get()) }
29 |
30 | // Presenter for Detail View
31 | factory { DetailPresenter(get(), get()) }
32 |
33 | // Weather Data Repository
34 | single { DailyForecastRepositoryImpl(get()) }
35 |
36 | // Rx Schedulers
37 | single { ApplicationSchedulerProvider() }
38 | }
39 |
40 | // Gather all app modules
41 | val onlineWeatherApp = listOf(weatherAppModule, remoteDataSourceModule)
42 | val offlineWeatherApp = listOf(weatherAppModule, localAndroidDataSourceModule)
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/local/FileDataSource.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.local
2 |
3 | import fr.ekito.myweatherapp.data.WeatherDataSource
4 | import fr.ekito.myweatherapp.data.json.Geocode
5 | import fr.ekito.myweatherapp.data.json.Weather
6 | import io.reactivex.Single
7 | import java.util.concurrent.TimeUnit
8 |
9 | /**
10 | * Read json files and render weather date
11 | */
12 | class FileDataSource(val jsonReader: JsonReader, val delayed: Boolean) :
13 | WeatherDataSource {
14 |
15 | private val cities by lazy { jsonReader.getAllLocations() }
16 |
17 | private fun isKnownCity(address: String): Boolean = cities.values.contains(address)
18 |
19 | private fun cityFromLocation(lat: Double?, lng: Double?): String {
20 | return cities.filterKeys { it.lat == lat && it.lng == lng }.values.firstOrNull()
21 | ?: DEFAULT_CITY
22 | }
23 |
24 | override fun geocode(address: String): Single {
25 | val single = Single.create { s ->
26 | val addressToLC = address.toLowerCase()
27 | val geocode = if (isKnownCity(addressToLC)) {
28 | jsonReader.getGeocode(addressToLC)
29 | } else {
30 | jsonReader.getGeocode(DEFAULT_CITY)
31 | }
32 | s.onSuccess(geocode)
33 | }
34 | return if (delayed) single.delay(1, TimeUnit.SECONDS) else single
35 | }
36 |
37 | override fun weather(lat: Double?, lon: Double?, lang: String): Single {
38 | val single = Single.create { s ->
39 | val city = cityFromLocation(lat, lon)
40 | val weather = jsonReader.getWeather(city)
41 | s.onSuccess(weather)
42 | }
43 | return if (delayed) single.delay(1, TimeUnit.SECONDS) else single
44 | }
45 |
46 | companion object {
47 | const val DEFAULT_CITY = "toulouse"
48 | }
49 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherActivity.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import android.os.Bundle
4 | import android.support.design.widget.Snackbar
5 | import android.support.v7.app.AppCompatActivity
6 | import android.util.Log
7 | import android.view.View
8 | import fr.ekito.myweatherapp.R
9 | import kotlinx.android.synthetic.main.activity_result.*
10 | import org.jetbrains.anko.clearTask
11 | import org.jetbrains.anko.clearTop
12 | import org.jetbrains.anko.intentFor
13 | import org.jetbrains.anko.newTask
14 |
15 | /**
16 | * Weather Result View
17 | */
18 | class WeatherActivity : AppCompatActivity() {
19 |
20 | private val TAG = this::class.java.simpleName
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_result)
25 |
26 | val weatherTitleFragment = WeatherHeaderFragment()
27 | val resultListFragment = WeatherListFragment()
28 |
29 | supportFragmentManager
30 | .beginTransaction()
31 | .replace(R.id.weather_title, weatherTitleFragment)
32 | .commit()
33 | supportFragmentManager
34 | .beginTransaction()
35 | .replace(R.id.weather_list, resultListFragment)
36 | .commit()
37 | }
38 |
39 | fun showError(error: Throwable) {
40 | Log.e(TAG, "error $error while displaying weather")
41 | weather_views.visibility = View.GONE
42 | weather_error.visibility = View.VISIBLE
43 | Snackbar.make(
44 | weather_result,
45 | "WeatherActivity got error : $error",
46 | Snackbar.LENGTH_INDEFINITE
47 | )
48 | .setAction(R.string.retry) {
49 | startActivity(intentFor().clearTop().clearTask().newTask())
50 | }
51 | .show()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/item_weather.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
23 |
24 |
34 |
35 |
44 |
45 |
46 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/weatherapp/app/src/androidTest/java/fr/ekito/myweatherapp/WeatherRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp
2 | //
3 | //import android.support.test.runner.AndroidJUnit4
4 | //import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
5 | //import junit.framework.Assert
6 | //import org.junit.After
7 | //import org.junit.Before
8 | //import org.junit.Test
9 | //import org.junit.runner.RunWith
10 | //import org.koin.standalone.StandAloneContext.loadKoinModules
11 | //import org.koin.standalone.StandAloneContext.stopKoin
12 | //import org.koin.standalone.inject
13 | //import org.koin.test.KoinTest
14 | //
15 | //@RunWith(AndroidJUnit4::class)
16 | //class WeatherRepositoryTest : KoinTest {
17 | //
18 | // private val weatherRepository: DailyForecastRepository by inject()
19 | //
20 | // @Before()
21 | // fun before() {
22 | // loadKoinModules(roomTestModule)
23 | // }
24 | //
25 | // @After
26 | // fun after() {
27 | // stopKoin()
28 | // }
29 | //
30 | // @Test
31 | // fun testGetDefault() {
32 | // val defaultWeather = weatherRepository.getWeather().blockingGet()
33 | // val defaultWeather2 = weatherRepository.getWeather().blockingGet()
34 | // Assert.assertEquals(defaultWeather, defaultWeather2)
35 | // }
36 | //
37 | // @Test
38 | // fun testGetWeatherDetail() {
39 | // val defaultWeather = weatherRepository.getWeather().blockingGet()
40 | //
41 | // val result = defaultWeather.first()
42 | // val first = weatherRepository.getWeatherDetail(result.id).blockingGet()
43 | // Assert.assertEquals(result, first)
44 | // }
45 | //
46 | // @Test
47 | // fun testGetLatest() {
48 | // weatherRepository.getWeather().blockingGet()
49 | // weatherRepository.getWeather("London").blockingGet()
50 | // val toulouse = weatherRepository.getWeather("Toulouse").blockingGet()
51 | // val defaultWeather3 = weatherRepository.getWeather().blockingGet()
52 | // Assert.assertEquals(defaultWeather3, toulouse)
53 | // }
54 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/util/MockitoKotlinHelpers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package fr.ekito.myweatherapp.util
17 |
18 | /**
19 | * Helper functions that are workarounds to kotlin Runtime Exceptions when using kotlin.
20 | */
21 |
22 | import org.mockito.ArgumentCaptor
23 | import org.mockito.Mockito
24 |
25 | object MockitoHelper {
26 |
27 | // fun given(any: T) = Mockito.`when`(any)
28 |
29 | /**
30 | * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
31 | * null is returned.
32 | *
33 | * Generic T is nullable because implicitly bounded by Any?.
34 | */
35 | fun eq(obj: T): T = Mockito.eq(obj)
36 |
37 |
38 | /**
39 | * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
40 | * null is returned.
41 | */
42 | fun any(): T = Mockito.any()
43 |
44 |
45 | /**
46 | * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
47 | * when null is returned.
48 | */
49 | fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture()
50 |
51 |
52 | /**
53 | * Helper function for creating an argumentCaptor in kotlin.
54 | */
55 | inline fun argumentCaptor(): ArgumentCaptor =
56 | ArgumentCaptor.forClass(T::class.java)
57 | }
58 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvp/DetailPresenterMockTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.mock.mvp
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
5 | import fr.ekito.myweatherapp.util.MockitoHelper
6 | import fr.ekito.myweatherapp.util.TestSchedulerProvider
7 | import fr.ekito.myweatherapp.view.detail.DetailContract
8 | import fr.ekito.myweatherapp.view.detail.DetailPresenter
9 | import io.reactivex.Single
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.mockito.BDDMockito.given
13 | import org.mockito.Mock
14 | import org.mockito.Mockito.*
15 | import org.mockito.MockitoAnnotations
16 |
17 | class DetailPresenterMockTest {
18 |
19 | lateinit var presenter: DetailContract.Presenter
20 | @Mock
21 | lateinit var view: DetailContract.View
22 | @Mock
23 | lateinit var repository: DailyForecastRepository
24 |
25 | // TODO uncomment to use LiveData in Test
26 | // @get:Rule
27 | // val rule = InstantTaskExecutorRule()
28 |
29 | @Before
30 | fun before() {
31 | MockitoAnnotations.initMocks(this)
32 |
33 | presenter = DetailPresenter(repository, TestSchedulerProvider())
34 | presenter.view = view
35 | }
36 |
37 | @Test
38 | fun testGetLastWeather() {
39 | val weather = mock(DailyForecast::class.java)
40 | val id = "ID"
41 |
42 | given(repository.getWeatherDetail(id)).willReturn(Single.just(weather))
43 |
44 | presenter.getDetail(id)
45 |
46 | verify(view, never()).showError(MockitoHelper.any())
47 | verify(view).showDetail(weather)
48 | }
49 |
50 | @Test
51 | fun testGeLasttWeatherFailed() {
52 | val error = Throwable("Got error")
53 | val id = "ID"
54 |
55 | given(repository.getWeatherDetail(id)).willReturn(Single.error(error))
56 |
57 | presenter.getDetail(id)
58 |
59 | verify(view, never()).showDetail(MockitoHelper.any())
60 | verify(view).showError(error)
61 | }
62 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/domain/entity/DailyForecast.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.domain.entity
2 |
3 | import fr.ekito.myweatherapp.data.json.Forecastday_
4 | import java.util.*
5 |
6 | /**
7 | * Represents our weather forecast for one day
8 | */
9 | data class DailyForecast(
10 | val location: String,
11 | val day: String,
12 | val shortText: String,
13 | val fullText: String,
14 | val iconUrl: String,
15 | val icon: String,
16 | val temperature: Temperature,
17 | val wind: Wind,
18 | val humidity: Humidity,
19 | val id: String = UUID.randomUUID().toString()
20 | ) {
21 | val colorCode: Int by lazy { colorCodeFromTemperatureRange(temperature.high.toInt()) }
22 |
23 | private fun colorCodeFromTemperatureRange(avg: Int): Int {
24 | return when {
25 | avg in 0..8 -> 1
26 | avg in 8..13 -> 2
27 | avg in 14..20 -> 3
28 | avg > 21 -> 4
29 | else -> 0
30 | }
31 | }
32 |
33 | companion object {
34 | fun from(location: String, f: Forecastday_) =
35 | DailyForecast(
36 | location,
37 | f.date?.weekday ?: "",
38 | f.conditions ?: "",
39 | "",
40 | f.iconUrl ?: "",
41 | getWeatherCodeForIcon(f.icon ?: ""),
42 | Temperature(
43 | f.low?.celsius ?: "",
44 | f.high!!.celsius!!
45 | ),
46 | Wind(f.avewind?.kph ?: 0, f.avewind?.dir ?: ""),
47 | Humidity(f.avehumidity ?: 0)
48 | )
49 | }
50 | }
51 |
52 | data class Wind(val kph: Int, val dir: String) {
53 | override fun toString(): String {
54 | return "$kph KPH $dir"
55 | }
56 | }
57 |
58 | data class Temperature(val low: String, val high: String) {
59 | override fun toString(): String {
60 | return "$low°C - $high°C"
61 | }
62 | }
63 |
64 | data class Humidity(val humidity: Int) {
65 | override fun toString(): String {
66 | return "$humidity %"
67 | }
68 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/splash/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.splash
2 |
3 | import android.os.Bundle
4 | import android.support.design.widget.Snackbar
5 | import android.support.v7.app.AppCompatActivity
6 | import android.view.View
7 | import android.view.animation.AnimationUtils
8 | import fr.ekito.myweatherapp.R
9 | import fr.ekito.myweatherapp.view.weather.WeatherActivity
10 | import kotlinx.android.synthetic.main.activity_splash.*
11 | import org.jetbrains.anko.clearTask
12 | import org.jetbrains.anko.clearTop
13 | import org.jetbrains.anko.intentFor
14 | import org.jetbrains.anko.newTask
15 | import org.koin.android.ext.android.inject
16 |
17 | /**
18 | * Search Weather View
19 | */
20 | class SplashActivity : AppCompatActivity(), SplashContract.View {
21 |
22 | // Presenter
23 | override val presenter: SplashContract.Presenter by inject()
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_splash)
28 | }
29 |
30 | override fun onStart() {
31 | super.onStart()
32 | // Bind View
33 | presenter.subscribe(this)
34 | presenter.getLastWeather()
35 | }
36 |
37 | override fun onStop() {
38 | presenter.unSubscribe()
39 | super.onStop()
40 | }
41 |
42 | override fun showIsLoading() {
43 | val animation =
44 | AnimationUtils.loadAnimation(applicationContext, R.anim.infinite_blinking_animation)
45 | splashIcon.startAnimation(animation)
46 | }
47 |
48 | override fun showIsLoaded() {
49 | startActivity(intentFor().clearTop().clearTask().newTask())
50 | }
51 |
52 | override fun showError(error: Throwable) {
53 | splashIcon.visibility = View.GONE
54 | splashIconFail.visibility = View.VISIBLE
55 | Snackbar.make(splash, "SplashActivity got error : $error", Snackbar.LENGTH_INDEFINITE)
56 | .setAction(R.string.retry) {
57 | presenter.getLastWeather()
58 | }
59 | .show()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/util/mvvm/SingleLiveEvent.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.util.mvvm
2 |
3 | import android.arch.lifecycle.LifecycleOwner
4 | import android.arch.lifecycle.MutableLiveData
5 | import android.arch.lifecycle.Observer
6 | import android.support.annotation.MainThread
7 | import android.util.Log
8 | import java.util.concurrent.atomic.AtomicBoolean
9 |
10 | /**
11 | Extracted from MVVM Google Blueprints Project -
12 | https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live-kotlin/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.kt
13 | */
14 |
15 | /**
16 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like
17 | * navigation and Snackbar messages.
18 | *
19 | *
20 | * This avoids a common problem with events: on configuration change (like rotation) an update
21 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
22 | * explicit call to setValue() or call().
23 | *
24 | *
25 | * Note that only one observer is going to be notified of changes.
26 | */
27 | class SingleLiveEvent : MutableLiveData() {
28 |
29 | private val pending = AtomicBoolean(false)
30 |
31 | @MainThread
32 | override fun observe(owner: LifecycleOwner, observer: Observer) {
33 |
34 | if (hasActiveObservers()) {
35 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
36 | }
37 |
38 | // Observe the internal MutableLiveData
39 | super.observe(owner, Observer { t ->
40 | if (pending.compareAndSet(true, false)) {
41 | observer.onChanged(t)
42 | }
43 | })
44 | }
45 |
46 | @MainThread
47 | override fun setValue(t: T?) {
48 | pending.set(true)
49 | super.setValue(t)
50 | }
51 |
52 | /**
53 | * Used for cases where T is Void, to make calls cleaner.
54 | */
55 | @MainThread
56 | fun call() {
57 | value = null
58 | }
59 |
60 | companion object {
61 | private const val TAG = "SingleLiveEvent"
62 | }
63 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/list/WeatherListAdapter.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather.list
2 |
3 | import android.content.Context
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.LinearLayout
9 | import android.widget.TextView
10 | import com.joanzapata.iconify.widget.IconTextView
11 | import fr.ekito.myweatherapp.R
12 | import fr.ekito.myweatherapp.domain.entity.getColorFromCode
13 |
14 | class WeatherListAdapter(
15 | val context: Context,
16 | var list: List,
17 | private val onDetailSelected: (WeatherItem) -> Unit
18 | ) : RecyclerView.Adapter() {
19 |
20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherResultHolder {
21 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_weather, parent, false)
22 | return WeatherResultHolder(view)
23 | }
24 |
25 | override fun onBindViewHolder(holder: WeatherResultHolder, position: Int) {
26 | holder.display(list[position], context, onDetailSelected)
27 | }
28 |
29 | override fun getItemCount() = list.size
30 |
31 | inner class WeatherResultHolder(item: View) : RecyclerView.ViewHolder(item) {
32 | private val weatherItemLayout = item.findViewById(R.id.weatherItemLayout)
33 | private val weatherItemDay = item.findViewById(R.id.weatheItemrDay)
34 | private val weatherItemIcon = item.findViewById(R.id.weatherItemIcon)
35 |
36 | fun display(
37 | dailyForecastModel: WeatherItem,
38 | context: Context,
39 | onClick: (WeatherItem) -> Unit
40 | ) {
41 | weatherItemLayout.setOnClickListener { onClick(dailyForecastModel) }
42 | weatherItemDay.text = dailyForecastModel.day
43 | weatherItemIcon.text = dailyForecastModel.icon
44 | val color = context.getColorFromCode(dailyForecastModel)
45 | weatherItemDay.setTextColor(color)
46 | weatherItemIcon.setTextColor(color)
47 | }
48 |
49 | }
50 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/activity_result.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
20 |
21 |
25 |
26 |
27 |
35 |
36 |
46 |
47 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/domain/repository/DailyForecastRepository.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.domain.repository
2 |
3 | import fr.ekito.myweatherapp.data.WeatherDataSource
4 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
5 | import fr.ekito.myweatherapp.domain.ext.getDailyForecasts
6 | import fr.ekito.myweatherapp.domain.ext.getLocation
7 | import io.reactivex.Single
8 |
9 | /**
10 | * Weather repository
11 | */
12 | interface DailyForecastRepository {
13 | /**
14 | * Get weather from given location
15 | * if location is null, get last weather or default
16 | */
17 | fun getWeather(location: String? = null): Single>
18 |
19 | /**
20 | * Get weather for given id
21 | */
22 | fun getWeatherDetail(id: String): Single
23 | }
24 |
25 | /**
26 | * Weather repository
27 | * Make use of WeatherDataSource & add some cache
28 | */
29 | class DailyForecastRepositoryImpl(private val weatherDatasource: WeatherDataSource) :
30 | DailyForecastRepository {
31 |
32 | private fun lastLocationFromCache() = weatherCache.firstOrNull()?.location
33 |
34 | private val weatherCache = arrayListOf()
35 |
36 | override fun getWeatherDetail(id: String): Single =
37 | Single.just(weatherCache.first { it.id == id })
38 |
39 | override fun getWeather(
40 | location: String?
41 | ): Single> {
42 | // Take cache
43 | return if (location == null && weatherCache.isNotEmpty()) return Single.just(weatherCache)
44 | else {
45 | val targetLocation: String = location ?: lastLocationFromCache() ?: DEFAULT_LOCATION
46 | weatherCache.clear()
47 | weatherDatasource.geocode(targetLocation)
48 | .map { it.getLocation() ?: throw IllegalStateException("No Location data") }
49 | .flatMap { weatherDatasource.weather(it.lat, it.lng, DEFAULT_LANG) }
50 | .map { it.getDailyForecasts(targetLocation) }
51 | .doOnSuccess { weatherCache.addAll(it) }
52 | }
53 | }
54 |
55 | companion object {
56 | const val DEFAULT_LOCATION = "Paris"
57 | const val DEFAULT_LANG = "EN"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/detail/DetailActivity.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.detail
2 |
3 | import android.os.Bundle
4 | import android.support.design.widget.Snackbar
5 | import android.support.v7.app.AppCompatActivity
6 | import fr.ekito.myweatherapp.R
7 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
8 | import fr.ekito.myweatherapp.domain.entity.getColorFromCode
9 | import fr.ekito.myweatherapp.util.android.argument
10 | import kotlinx.android.synthetic.main.activity_detail.*
11 | import org.koin.android.ext.android.inject
12 |
13 | /**
14 | * Weather Detail View
15 | */
16 | class DetailActivity : AppCompatActivity(), DetailContract.View {
17 |
18 | // Get all needed data
19 | private val detailId by argument(INTENT_WEATHER_ID)
20 |
21 | override val presenter: DetailContract.Presenter by inject()
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContentView(R.layout.activity_detail)
26 | presenter.getDetail(detailId)
27 | }
28 |
29 | override fun onStart() {
30 | super.onStart()
31 | presenter.subscribe(this)
32 | }
33 |
34 | override fun onStop() {
35 | presenter.unSubscribe()
36 | super.onStop()
37 | }
38 |
39 | override fun showError(error: Throwable) {
40 | Snackbar.make(
41 | weatherItem,
42 | getString(R.string.loading_error) + " - $error",
43 | Snackbar.LENGTH_LONG
44 | ).show()
45 | }
46 |
47 | override fun showDetail(weather: DailyForecast) {
48 | weatherIcon.text = weather.icon
49 | weatherDay.text = weather.day
50 | weatherText.text = weather.fullText
51 | weatherWindText.text = weather.wind.toString()
52 | weatherTempText.text = weather.temperature.toString()
53 | weatherHumidityText.text = weather.humidity.toString()
54 | weatherItem.background.setTint(getColorFromCode(weather))
55 | // Set back on background click
56 | weatherItem.setOnClickListener {
57 | onBackPressed()
58 | }
59 | }
60 |
61 | companion object {
62 | const val INTENT_WEATHER_ID: String = "INTENT_WEATHER_ID"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvp/SplashPresenterMockTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.mock.mvp
2 |
3 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
4 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
5 | import fr.ekito.myweatherapp.util.MockitoHelper
6 | import fr.ekito.myweatherapp.util.TestSchedulerProvider
7 | import fr.ekito.myweatherapp.view.splash.SplashContract
8 | import fr.ekito.myweatherapp.view.splash.SplashPresenter
9 | import io.reactivex.Single
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.mockito.BDDMockito.given
13 | import org.mockito.Mock
14 | import org.mockito.Mockito.*
15 | import org.mockito.MockitoAnnotations
16 |
17 | class SplashPresenterMockTest {
18 |
19 | lateinit var presenter: SplashContract.Presenter
20 | @Mock
21 | lateinit var view: SplashContract.View
22 | @Mock
23 | lateinit var repository: DailyForecastRepository
24 |
25 | // TODO uncomment to use LiveData in Test
26 | // @get:Rule
27 | // val rule = InstantTaskExecutorRule()
28 |
29 | @Before
30 | fun before() {
31 | MockitoAnnotations.initMocks(this)
32 |
33 | presenter = SplashPresenter(repository, TestSchedulerProvider())
34 | presenter.view = view
35 | }
36 |
37 | @Test
38 | fun testGetLastWeather() {
39 | val list = listOf(mock(DailyForecast::class.java))
40 |
41 | given(repository.getWeather()).willReturn(Single.just(list))
42 |
43 | presenter.getLastWeather()
44 |
45 | verify(view, never()).showError(MockitoHelper.any())
46 | inOrder(view).apply {
47 | verify(view).showIsLoading()
48 | verify(view).showIsLoaded()
49 | }
50 | }
51 |
52 | @Test
53 | fun testGetLasttWeatherFailed() {
54 | val error = Throwable("Got an error")
55 | given(repository.getWeather()).willReturn(Single.error(error))
56 |
57 | presenter.getLastWeather()
58 |
59 | inOrder(view).apply {
60 | verify(view).showIsLoading()
61 | verify(view).showError(error)
62 | }
63 | verify(view, never()).showIsLoaded()
64 | }
65 |
66 | companion object {
67 | const val DEFAULT_LOCATION = "DEFAULT_LOCATION"
68 | }
69 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvp/WeatherListPresenterMockTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.mock.mvp
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.mock.MockedData.mockList
5 | import fr.ekito.myweatherapp.util.MockitoHelper
6 | import fr.ekito.myweatherapp.util.TestSchedulerProvider
7 | import fr.ekito.myweatherapp.view.weather.WeatherListContract
8 | import fr.ekito.myweatherapp.view.weather.WeatherListPresenter
9 | import fr.ekito.myweatherapp.view.weather.list.WeatherItem
10 | import io.reactivex.Single
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.mockito.BDDMockito.given
14 | import org.mockito.Mock
15 | import org.mockito.Mockito
16 | import org.mockito.Mockito.verify
17 | import org.mockito.MockitoAnnotations
18 |
19 | class WeatherListPresenterMockTest {
20 |
21 | lateinit var presenter: WeatherListContract.Presenter
22 | @Mock
23 | lateinit var view: WeatherListContract.View
24 | @Mock
25 | lateinit var repository: DailyForecastRepository
26 |
27 | // TODO uncomment to use LiveData in Test
28 | // @get:Rule
29 | // val rule = InstantTaskExecutorRule()
30 |
31 | @Before
32 | fun before() {
33 | MockitoAnnotations.initMocks(this)
34 |
35 | presenter = WeatherListPresenter(repository, TestSchedulerProvider())
36 | presenter.view = view
37 | }
38 |
39 | @Test
40 | fun testDisplayList() {
41 | val location = "DEFAULT_LOCATION"
42 | given(repository.getWeather()).willReturn(Single.just(mockList))
43 |
44 | presenter.getWeatherList()
45 |
46 | val itemList = mockList.takeLast(mockList.size - 1).map { WeatherItem.from(it) }
47 | verify(view, Mockito.never()).showError(MockitoHelper.any())
48 | verify(view).showWeatherItemList(itemList)
49 | }
50 |
51 | @Test
52 | fun testDisplayListFailed() {
53 | val error = Throwable("Got an error")
54 | val location = "DEFAULT_LOCATION"
55 | given(repository.getWeather()).willReturn(Single.error(error))
56 |
57 | presenter.getWeatherList()
58 |
59 | verify(view, Mockito.never()).showWeatherItemList(MockitoHelper.any())
60 | verify(view).showError(error)
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherListFragment.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.widget.LinearLayoutManager
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import fr.ekito.myweatherapp.R
10 | import fr.ekito.myweatherapp.view.detail.DetailActivity
11 | import fr.ekito.myweatherapp.view.weather.list.WeatherItem
12 | import fr.ekito.myweatherapp.view.weather.list.WeatherListAdapter
13 | import kotlinx.android.synthetic.main.fragment_result_list.*
14 | import org.jetbrains.anko.startActivity
15 | import org.koin.android.ext.android.inject
16 |
17 | class WeatherListFragment : Fragment(), WeatherListContract.View {
18 |
19 | override val presenter by inject()
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater,
23 | container: ViewGroup?,
24 | savedInstanceState: Bundle?
25 | ): View? {
26 | return inflater.inflate(R.layout.fragment_result_list, container, false)
27 | }
28 |
29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
30 | super.onViewCreated(view, savedInstanceState)
31 | prepareListView()
32 | }
33 |
34 | private fun prepareListView() {
35 | weatherList.layoutManager =
36 | LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
37 | weatherList.adapter = WeatherListAdapter(
38 | activity!!,
39 | emptyList(),
40 | ::onWeatherItemSelected
41 | )
42 | }
43 |
44 | private fun onWeatherItemSelected(resultItem: WeatherItem) {
45 | activity?.startActivity(
46 | DetailActivity.INTENT_WEATHER_ID to resultItem.id
47 | )
48 | }
49 |
50 | override fun showWeatherItemList(newList: List) {
51 | val adapter: WeatherListAdapter = weatherList.adapter as WeatherListAdapter
52 | adapter.list = newList
53 | adapter.notifyDataSetChanged()
54 | }
55 |
56 | override fun onResume() {
57 | super.onResume()
58 | presenter.subscribe(this)
59 | presenter.getWeatherList()
60 | }
61 |
62 | override fun onPause() {
63 | presenter.unSubscribe()
64 | super.onPause()
65 | }
66 |
67 | override fun showError(error: Throwable) {
68 | (activity as? WeatherActivity)?.showError(error)
69 | }
70 | }
--------------------------------------------------------------------------------
/weatherapp/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvp/WeatherHeaderPresenterMockTest.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.mock.mvp
2 |
3 | import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
4 | import fr.ekito.myweatherapp.mock.MockedData.mockList
5 | import fr.ekito.myweatherapp.util.MockitoHelper
6 | import fr.ekito.myweatherapp.util.TestSchedulerProvider
7 | import fr.ekito.myweatherapp.view.weather.WeatherHeaderContract
8 | import fr.ekito.myweatherapp.view.weather.WeatherHeaderPresenter
9 | import io.reactivex.Single
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.mockito.BDDMockito.given
13 | import org.mockito.Mock
14 | import org.mockito.Mockito.never
15 | import org.mockito.Mockito.verify
16 | import org.mockito.MockitoAnnotations
17 |
18 | class WeatherHeaderPresenterMockTest {
19 |
20 | lateinit var presenter: WeatherHeaderContract.Presenter
21 | @Mock
22 | lateinit var view: WeatherHeaderContract.View
23 | @Mock
24 | lateinit var repository: DailyForecastRepository
25 |
26 | // TODO uncomment to use LiveData in Test
27 | // @get:Rule
28 | // val rule = InstantTaskExecutorRule()
29 |
30 | @Before
31 | fun before() {
32 | MockitoAnnotations.initMocks(this)
33 |
34 | presenter =
35 | WeatherHeaderPresenter(repository, TestSchedulerProvider())
36 | presenter.view = view
37 | }
38 |
39 | @Test
40 | fun testDisplayList() {
41 | val first = mockList.first()
42 | given(repository.getWeather()).willReturn(Single.just(mockList))
43 |
44 | presenter.getWeatherOfTheDay()
45 | verify(view, never()).showError(MockitoHelper.any())
46 | verify(view).showWeather(first.location, first)
47 | }
48 |
49 | @Test
50 | fun testDisplayListFailed() {
51 | val error = Throwable("Got an error")
52 | given(repository.getWeather(MockitoHelper.any())).willReturn(Single.error(error))
53 |
54 | presenter.getWeatherOfTheDay()
55 |
56 | verify(view, never()).showWeather(MockitoHelper.any(), MockitoHelper.any())
57 | verify(view).showError(error)
58 | }
59 |
60 | @Test
61 | fun testSearchNewLocation() {
62 | val location = "new location"
63 | given(repository.getWeather(location)).willReturn(Single.just(mockList))
64 | presenter.loadNewLocation(location)
65 |
66 | verify(view, never()).showLocationSearchFailed(MockitoHelper.any(), MockitoHelper.any())
67 | verify(view).showLocationSearchSucceed(location)
68 | }
69 |
70 | @Test
71 | fun testSearchNewLocationFailed() {
72 | val location = "new location"
73 | val error = Throwable("Got an error")
74 |
75 | given(repository.getWeather(location)).willReturn(Single.error(error))
76 | presenter.loadNewLocation(location)
77 | verify(view, never()).showLocationSearchSucceed(MockitoHelper.any())
78 | verify(view).showLocationSearchFailed(location, error)
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvvm/WeatherListViewModelMockTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp.mock.mvvm
2 | //
3 | //import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | //import android.arch.lifecycle.Observer
5 | //import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
6 | //import fr.ekito.myweatherapp.mock.MockedData.mockList
7 | //import fr.ekito.myweatherapp.util.TestSchedulerProvider
8 | //import fr.ekito.myweatherapp.view.Failed
9 | //import fr.ekito.myweatherapp.view.Loading
10 | //import fr.ekito.myweatherapp.view.ViewModelState
11 | //import fr.ekito.myweatherapp.view.weather.WeatherViewModel
12 | //import io.reactivex.Single
13 | //import org.junit.Assert
14 | //import org.junit.Before
15 | //import org.junit.Rule
16 | //import org.junit.Test
17 | //import org.mockito.ArgumentCaptor
18 | //import org.mockito.BDDMockito.given
19 | //import org.mockito.Mock
20 | //import org.mockito.Mockito
21 | //import org.mockito.Mockito.verify
22 | //import org.mockito.MockitoAnnotations
23 | //
24 | //class WeatherListViewModelMockTest {
25 | //
26 | // lateinit var viewModel: WeatherViewModel
27 | // @Mock
28 | // lateinit var view: Observer
29 | // @Mock
30 | // lateinit var repository: DailyForecastRepository
31 | //
32 | // @get:Rule
33 | // val rule = InstantTaskExecutorRule()
34 | //
35 | // @Before
36 | // fun before() {
37 | // MockitoAnnotations.initMocks(this)
38 | //
39 | // viewModel = WeatherViewModel(repository, TestSchedulerProvider())
40 | // viewModel.states.observeForever(view)
41 | // }
42 | //
43 | // @Test
44 | // fun testDisplayList() {
45 | // given(repository.getWeather()).willReturn(Single.just(mockList))
46 | //
47 | // viewModel.getWeather()
48 | //
49 | // // setup ArgumentCaptor
50 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
51 | // // Here we expect 2 calls on view.onChanged
52 | // verify(view, Mockito.times(2)).onChanged(arg.capture())
53 | //
54 | // val states = arg.allValues
55 | // // Test obtained values in order
56 | // Assert.assertEquals(2, states.size)
57 | // Assert.assertEquals(Loading, states[0])
58 | // Assert.assertEquals(WeatherViewModel.WeatherListLoaded.from(mockList), states[1])
59 | // }
60 | //
61 | // @Test
62 | // fun testDisplayListFailed() {
63 | // val error = Throwable("Got an error")
64 | // given(repository.getWeather()).willReturn(Single.error(error))
65 | //
66 | // viewModel.getWeather()
67 | //
68 | // // setup ArgumentCaptor
69 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
70 | // // Here we expect 2 calls on view.onChanged
71 | // verify(view, Mockito.times(2)).onChanged(arg.capture())
72 | //
73 | // val states = arg.allValues
74 | // // Test obtained values in order
75 | // Assert.assertEquals(2, states.size)
76 | // Assert.assertEquals(Loading, states[0])
77 | // Assert.assertEquals(Failed(error), states[1])
78 | // }
79 | //
80 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvvm/SplashViewModelMockTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp.mock.mvvm
2 | //
3 | //import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | //import android.arch.lifecycle.Observer
5 | //import fr.ekito.myweatherapp.domain.entity.DailyForecast
6 | //import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
7 | //import fr.ekito.myweatherapp.util.TestSchedulerProvider
8 | //import fr.ekito.myweatherapp.view.Fail
9 | //import fr.ekito.myweatherapp.view.Pending
10 | //import fr.ekito.myweatherapp.view.Success
11 | //import fr.ekito.myweatherapp.view.ViewModelEvent
12 | //import fr.ekito.myweatherapp.view.splash.SplashViewModel
13 | //import io.reactivex.Single
14 | //import org.junit.Assert
15 | //import org.junit.Before
16 | //import org.junit.Rule
17 | //import org.junit.Test
18 | //import org.mockito.ArgumentCaptor
19 | //import org.mockito.BDDMockito.given
20 | //import org.mockito.Mock
21 | //import org.mockito.Mockito.*
22 | //import org.mockito.MockitoAnnotations
23 | //
24 | //class SplashViewModelMockTest {
25 | //
26 | // lateinit var viewModel: SplashViewModel
27 | //
28 | // @Mock
29 | // lateinit var view: Observer
30 | //
31 | // @Mock
32 | // lateinit var repository: DailyForecastRepository
33 | //
34 | // @get:Rule
35 | // val rule = InstantTaskExecutorRule()
36 | //
37 | // @Before
38 | // fun before() {
39 | // MockitoAnnotations.initMocks(this)
40 | //
41 | // viewModel = SplashViewModel(repository, TestSchedulerProvider())
42 | //
43 | // viewModel.events.observeForever(view)
44 | // }
45 | //
46 | // @Test
47 | // fun testGetLastWeather() {
48 | // val list = listOf(mock(DailyForecast::class.java))
49 | //
50 | // given(repository.getWeather()).willReturn(Single.just(list))
51 | //
52 | // viewModel.getLastWeather()
53 | //
54 | // // setup ArgumentCaptor
55 | // val arg = ArgumentCaptor.forClass(ViewModelEvent::class.java)
56 | // // Here we expect 2 calls on view.onChanged
57 | // verify(view, times(2)).onChanged(arg.capture())
58 | //
59 | // val values = arg.allValues
60 | // // Test obtained values in order
61 | // Assert.assertEquals(2, values.size)
62 | // Assert.assertEquals(Pending, values[0])
63 | // Assert.assertEquals(Success, values[1])
64 | // }
65 | //
66 | // @Test
67 | // fun testGetLasttWeatherFailed() {
68 | // val error = Throwable("Got an error")
69 | // given(repository.getWeather()).willReturn(Single.error(error))
70 | //
71 | // viewModel.getLastWeather()
72 | //
73 | // // setup ArgumentCaptor
74 | // val arg = ArgumentCaptor.forClass(ViewModelEvent::class.java)
75 | // // Here we expect 2 calls on view.onChanged
76 | // verify(view, times(2)).onChanged(arg.capture())
77 | //
78 | // val values = arg.allValues
79 | // // Test obtained values in order
80 | // Assert.assertEquals(2, values.size)
81 | // Assert.assertEquals(Pending, values[0])
82 | // Assert.assertEquals(Fail(error), values[1])
83 | // }
84 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvvm/DetailViewModelMockTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp.mock.mvvm
2 | //
3 | //import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | //import android.arch.lifecycle.Observer
5 | //import fr.ekito.myweatherapp.domain.entity.DailyForecast
6 | //import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
7 | //import fr.ekito.myweatherapp.util.TestSchedulerProvider
8 | //import fr.ekito.myweatherapp.view.Failed
9 | //import fr.ekito.myweatherapp.view.Loading
10 | //import fr.ekito.myweatherapp.view.ViewModelState
11 | //import fr.ekito.myweatherapp.view.detail.DetailViewModel
12 | //import io.reactivex.Single
13 | //import org.junit.Assert.assertEquals
14 | //import org.junit.Before
15 | //import org.junit.Rule
16 | //import org.junit.Test
17 | //import org.mockito.ArgumentCaptor
18 | //import org.mockito.BDDMockito.given
19 | //import org.mockito.Mock
20 | //import org.mockito.Mockito
21 | //import org.mockito.Mockito.times
22 | //import org.mockito.Mockito.verify
23 | //import org.mockito.MockitoAnnotations
24 | //
25 | //class DetailViewModelMockTest {
26 | //
27 | // lateinit var detailViewModel: DetailViewModel
28 | // @Mock
29 | // lateinit var view: Observer
30 | // @Mock
31 | // lateinit var repository: DailyForecastRepository
32 | //
33 | // @get:Rule
34 | // val rule = InstantTaskExecutorRule()
35 | //
36 | // val id = "ID"
37 | //
38 | // @Before
39 | // fun before() {
40 | // MockitoAnnotations.initMocks(this)
41 | //
42 | // detailViewModel = DetailViewModel()
43 | // detailViewModel.schedulerProvider = TestSchedulerProvider()
44 | // detailViewModel.dailyForecastRepository = repository
45 | //
46 | // detailViewModel.states.observeForever(view)
47 | // }
48 | //
49 | // @Test
50 | // fun testGetLastWeather() {
51 | // val weather = Mockito.mock(DailyForecast::class.java)
52 | //
53 | // given(repository.getWeatherDetail(id)).willReturn(Single.just(weather))
54 | //
55 | // detailViewModel.getDetail(id)
56 | //
57 | // // setup ArgumentCaptor
58 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
59 | // // Here we expect 2 calls on view.onChanged
60 | // verify(view, times(2)).onChanged(arg.capture())
61 | //
62 | // val values = arg.allValues
63 | // // Test obtained values in order
64 | // assertEquals(2, values.size)
65 | // assertEquals(Loading, values[0])
66 | // assertEquals(DetailViewModel.DetailLoaded(weather), values[1])
67 | // }
68 | //
69 | // @Test
70 | // fun testGeLasttWeatherFailed() {
71 | // val error = Throwable("Got error")
72 | //
73 | // given(repository.getWeatherDetail(id)).willReturn(Single.error(error))
74 | //
75 | // detailViewModel.getDetail(id)
76 | //
77 | // // setup ArgumentCaptor
78 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
79 | // // Here we expect 2 calls on view.onChanged
80 | // verify(view, times(2)).onChanged(arg.capture())
81 | //
82 | // val values = arg.allValues
83 | // // Test obtained values in order
84 | // assertEquals(2, values.size)
85 | // assertEquals(Loading, values[0])
86 | // assertEquals(Failed(error), values[1])
87 | // }
88 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/domain/entity/WeatherCode.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.domain.entity
2 |
3 | import android.content.Context
4 | import fr.ekito.myweatherapp.R
5 | import fr.ekito.myweatherapp.view.weather.list.WeatherItem
6 |
7 | internal const val PREFIX = "nt_"
8 |
9 | const val CHANCE_FLURRIES = "chanceflurries" // wi-snow-wind
10 | const val CHANCE_RAIN = "chancerain" // wi-rain
11 | const val CHANCE_SLEET = "chancesleet" // wi-rain-mix
12 | const val CHANCE_SNOW = "chancesnow" // wi-snow
13 | const val CHANCE_STORMS = "chancestorms" // wi-thunderstorm
14 |
15 | const val CLEAR = "clear" // wi-day-sunny
16 | const val CLOUDY = "cloudy" // wi-cloudy
17 | const val FLURRIES = "flurries" // wi-snow-wind
18 | const val FOG = "fog" // wi-fog
19 | const val HAZY = "hazy" // wi-fog
20 |
21 | const val MOSTLY_CLOUDY = "mostlycloudy" // wi-day-cloudy
22 | const val MOSTLY_SUNNY = "mostlysunny" // wi-day-cloudy
23 | const val PARTLY_CLOUDY = "partlycloudy" // wi-day-cloudy
24 | const val PARTLY_SUNNY = "partlysunny" // wi-day-cloudy
25 |
26 | const val RAIN = "rain" // wi-rain
27 | const val SLEET = "sleet" // wi-rain-mix
28 | const val SNOW = "snow" // wi-snow
29 | const val SUNNY = "sunny" // wi-day-sunny
30 | const val TSTORMS = "tstorms" // wi-thunderstorm
31 |
32 | const val WI_SNOW_WIND = "{wi_snow_wind}"
33 | const val WI_RAIN = "{wi_rain}"
34 | const val WI_RAIN_MIX = "{wi_rain_mix}"
35 | const val WI_SNOW = "{wi_snow}"
36 | const val WI_THUNDERSTORM = "{wi_thunderstorm}"
37 | const val WI_DAY_SUNNY = "{wi_day_sunny}"
38 | const val WI_CLOUDY = "{wi_cloudy}"
39 | const val WI_FOG = "{wi_fog}"
40 | const val WI_DAY_CLOUDY = "{wi_day_cloudy}"
41 |
42 | fun getWeatherCodeForIcon(icon: String): String {
43 | return when (icon) {
44 | CHANCE_STORMS, PREFIX + CHANCE_STORMS, TSTORMS, PREFIX + TSTORMS -> WI_THUNDERSTORM
45 | CHANCE_SNOW, PREFIX + CHANCE_SNOW, SNOW, PREFIX + SNOW -> WI_SNOW
46 | CHANCE_FLURRIES, PREFIX + CHANCE_FLURRIES, FLURRIES, PREFIX + FLURRIES -> WI_SNOW_WIND
47 | CHANCE_RAIN, PREFIX + CHANCE_RAIN, RAIN, PREFIX + RAIN -> WI_RAIN
48 | CHANCE_SLEET, PREFIX + CHANCE_SLEET, SLEET, PREFIX + SLEET -> WI_RAIN_MIX
49 | FOG, PREFIX + FOG, HAZY, PREFIX + HAZY -> WI_FOG
50 | CLOUDY, PREFIX + CLOUDY -> WI_CLOUDY
51 | MOSTLY_CLOUDY, PREFIX + MOSTLY_CLOUDY, MOSTLY_SUNNY, PREFIX + MOSTLY_SUNNY, PARTLY_CLOUDY, PREFIX + PARTLY_CLOUDY, PARTLY_SUNNY, PREFIX + PARTLY_SUNNY -> WI_DAY_CLOUDY
52 | CLEAR, PREFIX + CLEAR, SUNNY, PREFIX + SUNNY -> WI_DAY_SUNNY
53 | else -> WI_DAY_CLOUDY
54 | }
55 | }
56 |
57 | @Suppress("DEPRECATION")
58 | fun Context.getColorFromCode(w: DailyForecast): Int {
59 | return when (w.colorCode) {
60 | 1 -> resources.getColor(R.color.temp_1)
61 | 2 -> resources.getColor(R.color.temp_2)
62 | 3 -> resources.getColor(R.color.temp_3)
63 | 4 -> resources.getColor(R.color.temp_4)
64 | else -> resources.getColor(R.color.temp_0)
65 | }
66 | }
67 |
68 | @Suppress("DEPRECATION")
69 | fun Context.getColorFromCode(w: WeatherItem): Int {
70 | return when (w.color) {
71 | 1 -> resources.getColor(R.color.temp_1)
72 | 2 -> resources.getColor(R.color.temp_2)
73 | 3 -> resources.getColor(R.color.temp_3)
74 | 4 -> resources.getColor(R.color.temp_4)
75 | else -> resources.getColor(R.color.temp_0)
76 | }
77 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/data/json/Weather.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.data.json
2 |
3 | import java.util.*
4 |
5 | data class Avewind(
6 | val mph: Int? = null,
7 | val kph: Int? = null,
8 | val dir: String? = null,
9 | val degrees: Int? = null
10 | )
11 |
12 | data class Date(
13 | val epoch: String? = null,
14 | val pretty: String? = null,
15 | val day: Int? = null,
16 | val month: Int? = null,
17 | val year: Int? = null,
18 | val yday: Int? = null,
19 | val hour: Int? = null,
20 | val min: String? = null,
21 | val sec: Int? = null,
22 | val isdst: String? = null,
23 | val monthname_short: String? = null,
24 | val weekday_short: String? = null,
25 | val weekday: String? = null,
26 | val ampm: String? = null,
27 | val tz_short: String? = null,
28 | val tz_long: String? = null
29 | )
30 |
31 | data class Features(val forecast: Int? = null)
32 |
33 | data class Forecast(
34 | val txtForecast: TxtForecast? = null,
35 | val simpleforecast: Simpleforecast? = null
36 | )
37 |
38 | data class Forecastday(
39 | val period: Int? = null,
40 | val icon: String? = null,
41 | val iconUrl: String? = null,
42 | val title: String? = null,
43 | val fcttext: String? = null,
44 | val fcttextMetric: String? = null,
45 | val pop: String? = null
46 | )
47 |
48 | data class Forecastday_(
49 | val date: Date? = null,
50 | val period: Int? = null,
51 | val high: High? = null,
52 | val low: Low? = null,
53 | val conditions: String? = null,
54 | val icon: String? = null,
55 | val iconUrl: String? = null,
56 | val skyicon: String? = null,
57 | val pop: Int? = null,
58 | val qpf_allday: QpfAllday? = null,
59 | val qpf_day: QpfDay? = null,
60 | val qpf_night: QpfNight? = null,
61 | val snow_allday: SnowAllday? = null,
62 | val snow_day: SnowDay? = null,
63 | val snow_night: SnowNight? = null,
64 | val maxwind: Maxwind? = null,
65 | val avewind: Avewind? = null,
66 | val avehumidity: Int? = null,
67 | val maxhumidity: Int? = null,
68 | val minhumidity: Int? = null
69 | )
70 |
71 | data class High(
72 | val fahrenheit: String? = null,
73 | val celsius: String? = null
74 | )
75 |
76 | data class Low(
77 | val fahrenheit: String? = null,
78 | val celsius: String? = null
79 | )
80 |
81 | data class Maxwind(
82 | val mph: Int? = null,
83 | val kph: Int? = null,
84 | val dir: String? = null,
85 | val degrees: Int? = null
86 | )
87 |
88 | data class QpfAllday(
89 | val `in`: Double? = null,
90 | val mm: Int? = null
91 | )
92 |
93 | data class QpfDay(
94 | val `in`: Double? = null,
95 | val mm: Int? = null
96 | )
97 |
98 | data class QpfNight(
99 | val `in`: Double? = null,
100 | val mm: Int? = null
101 | )
102 |
103 | data class Response(
104 | val version: String? = null,
105 | val termsofService: String? = null,
106 | val features: Features? = null
107 | )
108 |
109 | data class Simpleforecast(val forecastday: List = ArrayList())
110 |
111 | data class SnowAllday(
112 | val `in`: Double? = null,
113 | val cm: Double? = null
114 | )
115 |
116 | data class SnowDay(
117 | val `in`: Double? = null,
118 | val cm: Double? = null
119 | )
120 |
121 | data class SnowNight(
122 | val `in`: Double? = null,
123 | val cm: Double? = null
124 | )
125 |
126 | data class TxtForecast(
127 | val date: String? = null,
128 | val forecastday: List = ArrayList()
129 | )
130 |
131 | data class Weather(
132 | val response: Response? = null,
133 | val forecast: Forecast? = null
134 | )
--------------------------------------------------------------------------------
/weatherapp/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 |
6 | android {
7 | compileSdkVersion rootProject.ext.compile_sdk_version
8 | buildToolsVersion rootProject.ext.build_tools_version
9 |
10 | defaultConfig {
11 | minSdkVersion 21
12 | targetSdkVersion rootProject.ext.target_sdk_version
13 | applicationId "koin.sampleapp"
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 |
19 | // used by Room, to test migrations
20 | javaCompileOptions {
21 | annotationProcessorOptions {
22 | arguments = ["room.schemaLocation":
23 | "$projectDir/schemas".toString()]
24 | }
25 | }
26 | }
27 | testOptions {
28 | execution 'ANDROID_TEST_ORCHESTRATOR'
29 | }
30 | buildTypes {
31 | release {
32 | minifyEnabled false
33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
34 | }
35 | }
36 | sourceSets {
37 | main.java.srcDirs += 'src/main/kotlin'
38 | test.java.srcDirs += 'src/test/kotlin'
39 |
40 | // used by Room, to test migrations
41 | androidTest.assets.srcDirs +=
42 | files("$projectDir/schemas".toString())
43 | }
44 | }
45 |
46 | dependencies {
47 | implementation fileTree(dir: 'libs', include: ['*.jar'])
48 |
49 | // Android Support
50 | implementation "com.android.support:appcompat-v7:$support_lib_version"
51 | implementation "com.android.support:design:$support_lib_version"
52 |
53 | // Android Test
54 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
55 | androidTestUtil 'com.android.support.test:orchestrator:1.0.2'
56 |
57 | // Kotlin
58 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
59 | // Anko
60 | implementation "org.jetbrains.anko:anko-commons:$anko_version"
61 | // Koin
62 | implementation "org.koin:koin-android-viewmodel:$koin_version"
63 | testImplementation "org.koin:koin-test:$koin_version"
64 | androidTestImplementation "org.koin:koin-test:$koin_version"
65 |
66 | // ViewModel and LiveData
67 | implementation "android.arch.lifecycle:extensions:$android_arch_version"
68 | annotationProcessor "android.arch.lifecycle:compiler:$android_arch_version"
69 | testImplementation "android.arch.core:core-testing:$android_arch_version"
70 |
71 | // Room
72 | implementation "android.arch.persistence.room:runtime:$android_room_version"
73 | implementation "android.arch.persistence.room:rxjava2:$android_room_version"
74 | kapt "android.arch.persistence.room:compiler:$android_room_version"
75 | annotationProcessor "android.arch.persistence.room:compiler:$android_room_version"
76 | testImplementation "android.arch.persistence.room:testing:$android_room_version"
77 |
78 | // Networking
79 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
80 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
81 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
82 | implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
83 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
84 |
85 | // Rx
86 | implementation "io.reactivex.rxjava2:rxjava:$rxjava_version"
87 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
88 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
89 |
90 | // UI
91 | implementation 'com.joanzapata.iconify:android-iconify-weathericons:2.2.2'
92 |
93 | // Gson
94 | implementation 'com.google.code.gson:gson:2.8.2'
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/weatherapp/app/src/androidTest/java/fr/ekito/myweatherapp/WeatherDAOTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp
2 | //
3 | //import android.support.test.runner.AndroidJUnit4
4 | //import fr.ekito.myweatherapp.data.WeatherDataSource
5 | //import fr.ekito.myweatherapp.data.room.WeatherDAO
6 | //import fr.ekito.myweatherapp.data.room.WeatherDatabase
7 | //import fr.ekito.myweatherapp.data.room.WeatherEntity
8 | //import fr.ekito.myweatherapp.domain.ext.getDailyForecasts
9 | //import fr.ekito.myweatherapp.domain.ext.getLocation
10 | //import junit.framework.Assert
11 | //import org.junit.After
12 | //import org.junit.Before
13 | //import org.junit.Test
14 | //import org.junit.runner.RunWith
15 | //import org.koin.standalone.StandAloneContext.loadKoinModules
16 | //import org.koin.standalone.StandAloneContext.stopKoin
17 | //import org.koin.standalone.inject
18 | //import org.koin.test.KoinTest
19 | //import java.util.*
20 | //
21 | //@RunWith(AndroidJUnit4::class)
22 | //class WeatherDAOTest : KoinTest {
23 | //
24 | // val weatherDatabase: WeatherDatabase by inject()
25 | // val weatherWebDatasource: WeatherDataSource by inject()
26 | // val weatherDAO: WeatherDAO by inject()
27 | //
28 | // @Before()
29 | // fun before() {
30 | // loadKoinModules(roomTestModule)
31 | // }
32 | //
33 | // @After
34 | // fun after() {
35 | // weatherDatabase.close()
36 | // stopKoin()
37 | // }
38 | //
39 | // @Test
40 | // fun testSave() {
41 | // val location = "Paris"
42 | //
43 | // val now = Date()
44 | // val entities = getWeatherAsEntities(location, now)
45 | //
46 | // weatherDAO.saveAll(entities)
47 | // val ids = entities.map { it.id }
48 | //
49 | // val requestedEntities = ids.map { weatherDAO.findWeatherById(it).blockingGet() }
50 | //
51 | // Assert.assertEquals(entities, requestedEntities)
52 | // }
53 | //
54 | // @Test
55 | // fun testFindAllBy() {
56 | // val locationParis = "Paris"
57 | // val dateParis = Date()
58 | // val weatherParis = getWeatherAsEntities(locationParis, dateParis)
59 | // weatherDAO.saveAll(weatherParis)
60 | //
61 | // val locationTlse = "Toulouse"
62 | // val dateToulouse = Date()
63 | // val weatherToulouse = getWeatherAsEntities(locationTlse, dateToulouse)
64 | // weatherDAO.saveAll(weatherToulouse)
65 | //
66 | // val resultList = weatherDAO.findAllBy(locationTlse, dateToulouse).blockingGet()
67 | //
68 | // Assert.assertEquals(weatherToulouse, resultList)
69 | // }
70 | //
71 | // @Test
72 | // fun testFindLatest() {
73 | // val locationParis = "Paris"
74 | // val dateParis = Date()
75 | // val weatherParis = getWeatherAsEntities(locationParis, dateParis)
76 | // weatherDAO.saveAll(weatherParis)
77 | //
78 | // val locationBerlin = "Berlin"
79 | // val dateBerlin = Date()
80 | // val weatherBerlin = getWeatherAsEntities(locationBerlin, dateBerlin)
81 | // weatherDAO.saveAll(weatherBerlin)
82 | //
83 | // val locationTlse = "Toulouse"
84 | // val dateToulouse = Date()
85 | // val weatherToulouse = getWeatherAsEntities(locationTlse, dateToulouse)
86 | // weatherDAO.saveAll(weatherToulouse)
87 | //
88 | // val result: WeatherEntity = weatherDAO.findLatestWeather().blockingGet().first()
89 | // val resultList = weatherDAO.findAllBy(result.location, result.date).blockingGet()
90 | //
91 | // Assert.assertEquals(weatherToulouse, resultList)
92 | // }
93 | //
94 | // private fun getWeatherAsEntities(
95 | // locationParis: String,
96 | // dateParis: Date
97 | // ): List {
98 | // return weatherWebDatasource.geocode(locationParis)
99 | // .map { it.getLocation() }
100 | // .flatMap { weatherWebDatasource.weather(it.lat, it.lng, "EN") }
101 | // .map { it.getDailyForecasts(locationParis) }
102 | // .map { list -> list.map { WeatherEntity.from(it, dateParis) } }
103 | // .blockingGet()
104 | // }
105 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/main/kotlin/fr/ekito/myweatherapp/view/weather/WeatherHeaderFragment.kt:
--------------------------------------------------------------------------------
1 | package fr.ekito.myweatherapp.view.weather
2 |
3 | import android.app.AlertDialog
4 | import android.os.Bundle
5 | import android.support.design.widget.Snackbar
6 | import android.support.v4.app.Fragment
7 | import android.text.InputType
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.EditText
12 | import fr.ekito.myweatherapp.R
13 | import fr.ekito.myweatherapp.domain.entity.DailyForecast
14 | import fr.ekito.myweatherapp.domain.entity.getColorFromCode
15 | import fr.ekito.myweatherapp.view.detail.DetailActivity
16 | import fr.ekito.myweatherapp.view.detail.DetailActivity.Companion.INTENT_WEATHER_ID
17 | import kotlinx.android.synthetic.main.fragment_result_header.*
18 | import org.jetbrains.anko.*
19 | import org.koin.android.ext.android.inject
20 |
21 | class WeatherHeaderFragment : Fragment(), WeatherHeaderContract.View {
22 |
23 | override val presenter: WeatherHeaderContract.Presenter by inject()
24 |
25 | override fun onCreateView(
26 | inflater: LayoutInflater,
27 | container: ViewGroup?,
28 | savedInstanceState: Bundle?
29 | ): View {
30 | return inflater.inflate(R.layout.fragment_result_header, container, false) as ViewGroup
31 | }
32 |
33 | override fun onResume() {
34 | presenter.subscribe(this)
35 | presenter.getWeatherOfTheDay()
36 | super.onResume()
37 | }
38 |
39 | override fun onPause() {
40 | presenter.unSubscribe()
41 | super.onPause()
42 | }
43 |
44 | override fun showWeather(location: String, weather: DailyForecast) {
45 | weatherCity.text = location
46 | weatherCityCard.setOnClickListener {
47 | promptLocationDialog()
48 | }
49 |
50 | weatherIcon.text = weather.icon
51 | weatherDay.text = weather.day
52 | weatherTempText.text = weather.temperature.toString()
53 | weatherText.text = weather.shortText
54 |
55 | val color = context!!.getColorFromCode(weather)
56 | weatherHeader.background.setTint(color)
57 |
58 | weatherHeader.setOnClickListener {
59 | activity?.startActivity(
60 | INTENT_WEATHER_ID to weather.id
61 | )
62 | }
63 | }
64 |
65 | private fun promptLocationDialog() {
66 | val dialog = AlertDialog.Builder(context)
67 | dialog.setTitle(getString(R.string.enter_location))
68 | val editText = EditText(context)
69 | editText.hint = getString(R.string.location_hint)
70 | editText.maxLines = 1
71 | editText.inputType = InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
72 | dialog.setView(editText)
73 | dialog.setPositiveButton(getString(R.string.search)) { dialogInterface, _ ->
74 | dialogInterface.dismiss()
75 | val newLocation = editText.text.trim().toString()
76 | presenter.loadNewLocation(newLocation)
77 | Snackbar.make(
78 | weatherHeader,
79 | getString(R.string.loading_location) + " $newLocation ...",
80 | Snackbar.LENGTH_LONG
81 | )
82 | .show()
83 | }
84 | dialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface, _ ->
85 | dialogInterface.dismiss()
86 | }
87 | dialog.show()
88 | }
89 |
90 | override fun showLocationSearchSucceed(location: String) {
91 | activity?.apply {
92 | startActivity(
93 | intentFor().clearTop().clearTask().newTask()
94 | )
95 | }
96 | }
97 |
98 | override fun showLocationSearchFailed(location: String, error: Throwable) {
99 | Snackbar.make(weatherHeader, getString(R.string.loading_error), Snackbar.LENGTH_LONG)
100 | .setAction(R.string.retry) {
101 | presenter.loadNewLocation(location)
102 | }
103 | .show()
104 | }
105 |
106 | override fun showError(error: Throwable) {
107 | (activity as? WeatherActivity)?.showError(error)
108 | }
109 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/fragment_result_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
30 |
31 |
39 |
40 |
41 |
42 |
43 |
50 |
51 |
61 |
62 |
71 |
72 |
81 |
82 |
88 |
89 |
97 |
98 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
34 |
35 |
43 |
44 |
52 |
53 |
58 |
59 |
63 |
64 |
67 |
68 |
73 |
74 |
77 |
78 |
81 |
82 |
86 |
87 |
90 |
91 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/weatherapp/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/weatherapp/app/src/test/java/fr/ekito/myweatherapp/mock/mvvm/WeatherHeaderViewModelMockTest.kt:
--------------------------------------------------------------------------------
1 | //package fr.ekito.myweatherapp.mock.mvvm
2 | //
3 | //import android.arch.core.executor.testing.InstantTaskExecutorRule
4 | //import android.arch.lifecycle.Observer
5 | //import fr.ekito.myweatherapp.domain.repository.DailyForecastRepository
6 | //import fr.ekito.myweatherapp.mock.MockedData.mockList
7 | //import fr.ekito.myweatherapp.util.MockitoHelper
8 | //import fr.ekito.myweatherapp.util.TestSchedulerProvider
9 | //import fr.ekito.myweatherapp.view.Failed
10 | //import fr.ekito.myweatherapp.view.Loading
11 | //import fr.ekito.myweatherapp.view.ViewModelEvent
12 | //import fr.ekito.myweatherapp.view.ViewModelState
13 | //import fr.ekito.myweatherapp.view.weather.WeatherViewModel
14 | //import io.reactivex.Single
15 | //import org.junit.Assert
16 | //import org.junit.Before
17 | //import org.junit.Rule
18 | //import org.junit.Test
19 | //import org.mockito.ArgumentCaptor
20 | //import org.mockito.BDDMockito.given
21 | //import org.mockito.Mock
22 | //import org.mockito.Mockito
23 | //import org.mockito.Mockito.verify
24 | //import org.mockito.MockitoAnnotations
25 | //
26 | //class WeatherHeaderViewModelMockTest {
27 | //
28 | // lateinit var viewModel: WeatherViewModel
29 | //
30 | // @Mock
31 | // lateinit var viewStates: Observer
32 | //
33 | // @Mock
34 | // lateinit var viewEvents: Observer
35 | //
36 | // @Mock
37 | // lateinit var repository: DailyForecastRepository
38 | //
39 | // @get:Rule
40 | // val rule = InstantTaskExecutorRule()
41 | //
42 | // @Before
43 | // fun before() {
44 | // MockitoAnnotations.initMocks(this)
45 | //
46 | // viewModel = WeatherViewModel(repository, TestSchedulerProvider())
47 | // viewModel.events.observeForever(viewEvents)
48 | // viewModel.states.observeForever(viewStates)
49 | // }
50 | //
51 | // @Test
52 | // fun testDisplayList() {
53 | // given(repository.getWeather()).willReturn(Single.just(mockList))
54 | //
55 | // viewModel.getWeather()
56 | //
57 | // // setup ArgumentCaptor
58 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
59 | // // Here we expect 2 calls on view.onChanged
60 | // verify(viewStates, Mockito.times(2)).onChanged(arg.capture())
61 | //
62 | // val states = arg.allValues
63 | // // Test obtained values in order
64 | // Assert.assertEquals(2, states.size)
65 | // Assert.assertEquals(Loading, states[0])
66 | // Assert.assertEquals(WeatherViewModel.WeatherListLoaded.from(mockList), states[1])
67 | // }
68 | //
69 | // @Test
70 | // fun testDisplayListFailed() {
71 | // val error = Throwable("Got an error")
72 | // given(repository.getWeather(MockitoHelper.any())).willReturn(Single.error(error))
73 | //
74 | // viewModel.getWeather()
75 | //
76 | // // setup ArgumentCaptor
77 | // val arg = ArgumentCaptor.forClass(ViewModelState::class.java)
78 | // // Here we expect 2 calls on view.onChanged
79 | // verify(viewStates, Mockito.times(2)).onChanged(arg.capture())
80 | //
81 | // val values = arg.allValues
82 | // // Test obtained values in order
83 | // Assert.assertEquals(2, values.size)
84 | // Assert.assertEquals(Loading, values[0])
85 | // Assert.assertEquals(Failed(error), values[1])
86 | // }
87 | //
88 | // @Test
89 | // fun testSearchNewLocation() {
90 | // val location = "new location"
91 | // given(repository.getWeather(location)).willReturn(Single.just(mockList))
92 | // viewModel.loadNewLocation(location)
93 | //
94 | // // setup ArgumentCaptor
95 | // val argStates = ArgumentCaptor.forClass(ViewModelState::class.java)
96 | // // Here we expect 2 calls on view.onChanged
97 | // verify(viewStates, Mockito.times(1)).onChanged(argStates.capture())
98 | //
99 | // val states = argStates.allValues
100 | // // Test obtained values in order
101 | // Assert.assertEquals(1, states.size)
102 | // Assert.assertEquals(WeatherViewModel.WeatherListLoaded.from(mockList), states[0])
103 | //
104 | // // setup ArgumentCaptor
105 | // val argEvents = ArgumentCaptor.forClass(ViewModelEvent::class.java)
106 | // // Here we expect 2 calls on view.onChanged
107 | // verify(viewEvents, Mockito.times(1)).onChanged(argEvents.capture())
108 | //
109 | // val events = argEvents.allValues
110 | // // Test obtained values in order
111 | // Assert.assertEquals(1, events.size)
112 | // Assert.assertEquals(WeatherViewModel.ProceedLocation(location), events[0])
113 | // }
114 | //
115 | // @Test
116 | // fun testSearchNewLocationFailed() {
117 | // val location = "new location"
118 | // val error = Throwable("Got an error")
119 | //
120 | // given(repository.getWeather(location)).willReturn(Single.error(error))
121 | // viewModel.loadNewLocation(location)
122 | //
123 | // // setup ArgumentCaptor
124 | // val argStates = ArgumentCaptor.forClass(ViewModelState::class.java)
125 | // // Here we expect 2 calls on view.onChanged
126 | // verify(viewStates, Mockito.times(0)).onChanged(argStates.capture())
127 | //
128 | // val states = argStates.allValues
129 | // // Test obtained values in order
130 | // Assert.assertEquals(0, states.size)
131 | //
132 | // // setup ArgumentCaptor
133 | // val argEvents = ArgumentCaptor.forClass(ViewModelEvent::class.java)
134 | // // Here we expect 2 calls on view.onChanged
135 | // verify(viewEvents, Mockito.times(2)).onChanged(argEvents.capture())
136 | //
137 | // val events = argEvents.allValues
138 | // // Test obtained values in order
139 | // Assert.assertEquals(2, events.size)
140 | // Assert.assertEquals(WeatherViewModel.ProceedLocation(location), events[0])
141 | // Assert.assertEquals(WeatherViewModel.ProceedLocationFail(location, error), events[1])
142 | // }
143 | //
144 | //}
--------------------------------------------------------------------------------
/weatherapp/app/src/main/res/layout/activity_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
26 |
27 |
44 |
45 |
63 |
64 |
65 |
77 |
78 |
94 |
95 |
96 |
108 |
109 |
124 |
125 |
126 |
138 |
139 |
154 |
155 |
161 |
162 |
168 |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/weatherapp/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/weather_madrid.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "version": "0.1",
4 | "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
5 | "features": {
6 | "forecast": 1
7 | }
8 | },
9 | "forecast": {
10 | "txtForecast": {
11 | "date": "4:04 PM CET",
12 | "forecastday": [
13 | {
14 | "period": 0,
15 | "icon": "clear",
16 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
17 | "title": "Friday",
18 | "fcttext": "Mostly clear. Lows overnight in the low 30s.",
19 | "fcttextMetric": "Mostly clear. Low 1C.",
20 | "pop": "10"
21 | },
22 | {
23 | "period": 1,
24 | "icon": "nt_clear",
25 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
26 | "title": "Friday Night",
27 | "fcttext": "Mostly clear. Low 33F. Winds light and variable.",
28 | "fcttextMetric": "Mainly clear skies. Low 1C. Winds light and variable.",
29 | "pop": "10"
30 | },
31 | {
32 | "period": 2,
33 | "icon": "clear",
34 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
35 | "title": "Saturday",
36 | "fcttext": "Mostly sunny skies. High 58F. Winds light and variable.",
37 | "fcttextMetric": "Mostly sunny skies. High around 15C. Winds light and variable.",
38 | "pop": "10"
39 | },
40 | {
41 | "period": 3,
42 | "icon": "nt_partlycloudy",
43 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
44 | "title": "Saturday Night",
45 | "fcttext": "Mostly cloudy skies early will become partly cloudy late. Low around 40F. Winds N at 5 to 10 mph.",
46 | "fcttextMetric": "Mostly cloudy skies early will become partly cloudy late. Low around 5C. Winds N at 10 to 15 km/h.",
47 | "pop": "10"
48 | },
49 | {
50 | "period": 4,
51 | "icon": "partlycloudy",
52 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
53 | "title": "Sunday",
54 | "fcttext": "Sunshine and clouds mixed. High 58F. Winds NE at 10 to 15 mph.",
55 | "fcttextMetric": "Sunshine and clouds mixed. High around 15C. Winds NE at 15 to 25 km/h.",
56 | "pop": "10"
57 | },
58 | {
59 | "period": 5,
60 | "icon": "nt_clear",
61 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
62 | "title": "Sunday Night",
63 | "fcttext": "Clear skies. Low 36F. Winds NNE at 5 to 10 mph.",
64 | "fcttextMetric": "Clear skies. Low 3C. Winds NNE at 10 to 15 km/h.",
65 | "pop": "10"
66 | },
67 | {
68 | "period": 6,
69 | "icon": "clear",
70 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
71 | "title": "Monday",
72 | "fcttext": "Sunny skies. High 58F. Winds NE at 10 to 15 mph.",
73 | "fcttextMetric": "A mainly sunny sky. High near 15C. Winds NE at 10 to 15 km/h.",
74 | "pop": "10"
75 | },
76 | {
77 | "period": 7,
78 | "icon": "nt_clear",
79 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
80 | "title": "Monday Night",
81 | "fcttext": "Clear skies. Low 36F. Winds NE at 5 to 10 mph.",
82 | "fcttextMetric": "Clear skies. Low 2C. Winds NE at 10 to 15 km/h.",
83 | "pop": "10"
84 | }
85 | ]
86 | },
87 | "simpleforecast": {
88 | "forecastday": [
89 | {
90 | "date": {
91 | "epoch": "1487354400",
92 | "pretty": "7:00 PM CET on February 17, 2017",
93 | "day": 17,
94 | "month": 2,
95 | "year": 2017,
96 | "yday": 47,
97 | "hour": 19,
98 | "min": "00",
99 | "sec": 0,
100 | "isdst": "0",
101 | "monthname": "February",
102 | "monthnameShort": "Feb",
103 | "weekdayShort": "Fri",
104 | "weekday": "Friday",
105 | "ampm": "PM",
106 | "tzShort": "CET",
107 | "tzLong": "Europe/Madrid"
108 | },
109 | "period": 1,
110 | "high": {
111 | "fahrenheit": "59",
112 | "celsius": "15"
113 | },
114 | "low": {
115 | "fahrenheit": "33",
116 | "celsius": "1"
117 | },
118 | "conditions": "Clear",
119 | "icon": "clear",
120 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
121 | "skyicon": "",
122 | "pop": 10,
123 | "qpfAllday": {
124 | "in": 0.0,
125 | "mm": 0
126 | },
127 | "qpfDay": {
128 | "in": null,
129 | "mm": null
130 | },
131 | "qpfNight": {
132 | "in": 0.0,
133 | "mm": 0
134 | },
135 | "snowAllday": {
136 | "in": 0.0,
137 | "cm": 0.0
138 | },
139 | "snowDay": {
140 | "in": null,
141 | "cm": null
142 | },
143 | "snowNight": {
144 | "in": 0.0,
145 | "cm": 0.0
146 | },
147 | "maxwind": {
148 | "mph": 2,
149 | "kph": 4,
150 | "dir": "North",
151 | "degrees": 0
152 | },
153 | "avewind": {
154 | "mph": 0,
155 | "kph": 0,
156 | "dir": "Variable",
157 | "degrees": 0
158 | },
159 | "avehumidity": 75,
160 | "maxhumidity": 0,
161 | "minhumidity": 0
162 | },
163 | {
164 | "date": {
165 | "epoch": "1487440800",
166 | "pretty": "7:00 PM CET on February 18, 2017",
167 | "day": 18,
168 | "month": 2,
169 | "year": 2017,
170 | "yday": 48,
171 | "hour": 19,
172 | "min": "00",
173 | "sec": 0,
174 | "isdst": "0",
175 | "monthname": "February",
176 | "monthnameShort": "Feb",
177 | "weekdayShort": "Sat",
178 | "weekday": "Saturday",
179 | "ampm": "PM",
180 | "tzShort": "CET",
181 | "tzLong": "Europe/Madrid"
182 | },
183 | "period": 2,
184 | "high": {
185 | "fahrenheit": "58",
186 | "celsius": "14"
187 | },
188 | "low": {
189 | "fahrenheit": "40",
190 | "celsius": "4"
191 | },
192 | "conditions": "Clear",
193 | "icon": "clear",
194 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
195 | "skyicon": "",
196 | "pop": 10,
197 | "qpfAllday": {
198 | "in": 0.0,
199 | "mm": 0
200 | },
201 | "qpfDay": {
202 | "in": 0.0,
203 | "mm": 0
204 | },
205 | "qpfNight": {
206 | "in": 0.0,
207 | "mm": 0
208 | },
209 | "snowAllday": {
210 | "in": 0.0,
211 | "cm": 0.0
212 | },
213 | "snowDay": {
214 | "in": 0.0,
215 | "cm": 0.0
216 | },
217 | "snowNight": {
218 | "in": 0.0,
219 | "cm": 0.0
220 | },
221 | "maxwind": {
222 | "mph": 5,
223 | "kph": 8,
224 | "dir": "W",
225 | "degrees": 268
226 | },
227 | "avewind": {
228 | "mph": 4,
229 | "kph": 6,
230 | "dir": "W",
231 | "degrees": 268
232 | },
233 | "avehumidity": 65,
234 | "maxhumidity": 0,
235 | "minhumidity": 0
236 | },
237 | {
238 | "date": {
239 | "epoch": "1487527200",
240 | "pretty": "7:00 PM CET on February 19, 2017",
241 | "day": 19,
242 | "month": 2,
243 | "year": 2017,
244 | "yday": 49,
245 | "hour": 19,
246 | "min": "00",
247 | "sec": 0,
248 | "isdst": "0",
249 | "monthname": "February",
250 | "monthnameShort": "Feb",
251 | "weekdayShort": "Sun",
252 | "weekday": "Sunday",
253 | "ampm": "PM",
254 | "tzShort": "CET",
255 | "tzLong": "Europe/Madrid"
256 | },
257 | "period": 3,
258 | "high": {
259 | "fahrenheit": "58",
260 | "celsius": "14"
261 | },
262 | "low": {
263 | "fahrenheit": "36",
264 | "celsius": "2"
265 | },
266 | "conditions": "Partly Cloudy",
267 | "icon": "partlycloudy",
268 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
269 | "skyicon": "",
270 | "pop": 10,
271 | "qpfAllday": {
272 | "in": 0.0,
273 | "mm": 0
274 | },
275 | "qpfDay": {
276 | "in": 0.0,
277 | "mm": 0
278 | },
279 | "qpfNight": {
280 | "in": 0.0,
281 | "mm": 0
282 | },
283 | "snowAllday": {
284 | "in": 0.0,
285 | "cm": 0.0
286 | },
287 | "snowDay": {
288 | "in": 0.0,
289 | "cm": 0.0
290 | },
291 | "snowNight": {
292 | "in": 0.0,
293 | "cm": 0.0
294 | },
295 | "maxwind": {
296 | "mph": 15,
297 | "kph": 24,
298 | "dir": "NE",
299 | "degrees": 48
300 | },
301 | "avewind": {
302 | "mph": 10,
303 | "kph": 16,
304 | "dir": "NE",
305 | "degrees": 48
306 | },
307 | "avehumidity": 64,
308 | "maxhumidity": 0,
309 | "minhumidity": 0
310 | },
311 | {
312 | "date": {
313 | "epoch": "1487613600",
314 | "pretty": "7:00 PM CET on February 20, 2017",
315 | "day": 20,
316 | "month": 2,
317 | "year": 2017,
318 | "yday": 50,
319 | "hour": 19,
320 | "min": "00",
321 | "sec": 0,
322 | "isdst": "0",
323 | "monthname": "February",
324 | "monthnameShort": "Feb",
325 | "weekdayShort": "Mon",
326 | "weekday": "Monday",
327 | "ampm": "PM",
328 | "tzShort": "CET",
329 | "tzLong": "Europe/Madrid"
330 | },
331 | "period": 4,
332 | "high": {
333 | "fahrenheit": "58",
334 | "celsius": "14"
335 | },
336 | "low": {
337 | "fahrenheit": "36",
338 | "celsius": "2"
339 | },
340 | "conditions": "Clear",
341 | "icon": "clear",
342 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
343 | "skyicon": "",
344 | "pop": 10,
345 | "qpfAllday": {
346 | "in": 0.0,
347 | "mm": 0
348 | },
349 | "qpfDay": {
350 | "in": 0.0,
351 | "mm": 0
352 | },
353 | "qpfNight": {
354 | "in": 0.0,
355 | "mm": 0
356 | },
357 | "snowAllday": {
358 | "in": 0.0,
359 | "cm": 0.0
360 | },
361 | "snowDay": {
362 | "in": 0.0,
363 | "cm": 0.0
364 | },
365 | "snowNight": {
366 | "in": 0.0,
367 | "cm": 0.0
368 | },
369 | "maxwind": {
370 | "mph": 15,
371 | "kph": 24,
372 | "dir": "NE",
373 | "degrees": 51
374 | },
375 | "avewind": {
376 | "mph": 10,
377 | "kph": 16,
378 | "dir": "NE",
379 | "degrees": 51
380 | },
381 | "avehumidity": 62,
382 | "maxhumidity": 0,
383 | "minhumidity": 0
384 | }
385 | ]
386 | }
387 | }
388 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/weather_madrid.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "version": "0.1",
4 | "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
5 | "features": {
6 | "forecast": 1
7 | }
8 | },
9 | "forecast": {
10 | "txtForecast": {
11 | "date": "4:04 PM CET",
12 | "forecastday": [
13 | {
14 | "period": 0,
15 | "icon": "clear",
16 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
17 | "title": "Friday",
18 | "fcttext": "Mostly clear. Lows overnight in the low 30s.",
19 | "fcttextMetric": "Mostly clear. Low 1C.",
20 | "pop": "10"
21 | },
22 | {
23 | "period": 1,
24 | "icon": "nt_clear",
25 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
26 | "title": "Friday Night",
27 | "fcttext": "Mostly clear. Low 33F. Winds light and variable.",
28 | "fcttextMetric": "Mainly clear skies. Low 1C. Winds light and variable.",
29 | "pop": "10"
30 | },
31 | {
32 | "period": 2,
33 | "icon": "clear",
34 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
35 | "title": "Saturday",
36 | "fcttext": "Mostly sunny skies. High 58F. Winds light and variable.",
37 | "fcttextMetric": "Mostly sunny skies. High around 15C. Winds light and variable.",
38 | "pop": "10"
39 | },
40 | {
41 | "period": 3,
42 | "icon": "nt_partlycloudy",
43 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
44 | "title": "Saturday Night",
45 | "fcttext": "Mostly cloudy skies early will become partly cloudy late. Low around 40F. Winds N at 5 to 10 mph.",
46 | "fcttextMetric": "Mostly cloudy skies early will become partly cloudy late. Low around 5C. Winds N at 10 to 15 km/h.",
47 | "pop": "10"
48 | },
49 | {
50 | "period": 4,
51 | "icon": "partlycloudy",
52 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
53 | "title": "Sunday",
54 | "fcttext": "Sunshine and clouds mixed. High 58F. Winds NE at 10 to 15 mph.",
55 | "fcttextMetric": "Sunshine and clouds mixed. High around 15C. Winds NE at 15 to 25 km/h.",
56 | "pop": "10"
57 | },
58 | {
59 | "period": 5,
60 | "icon": "nt_clear",
61 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
62 | "title": "Sunday Night",
63 | "fcttext": "Clear skies. Low 36F. Winds NNE at 5 to 10 mph.",
64 | "fcttextMetric": "Clear skies. Low 3C. Winds NNE at 10 to 15 km/h.",
65 | "pop": "10"
66 | },
67 | {
68 | "period": 6,
69 | "icon": "clear",
70 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
71 | "title": "Monday",
72 | "fcttext": "Sunny skies. High 58F. Winds NE at 10 to 15 mph.",
73 | "fcttextMetric": "A mainly sunny sky. High near 15C. Winds NE at 10 to 15 km/h.",
74 | "pop": "10"
75 | },
76 | {
77 | "period": 7,
78 | "icon": "nt_clear",
79 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
80 | "title": "Monday Night",
81 | "fcttext": "Clear skies. Low 36F. Winds NE at 5 to 10 mph.",
82 | "fcttextMetric": "Clear skies. Low 2C. Winds NE at 10 to 15 km/h.",
83 | "pop": "10"
84 | }
85 | ]
86 | },
87 | "simpleforecast": {
88 | "forecastday": [
89 | {
90 | "date": {
91 | "epoch": "1487354400",
92 | "pretty": "7:00 PM CET on February 17, 2017",
93 | "day": 17,
94 | "month": 2,
95 | "year": 2017,
96 | "yday": 47,
97 | "hour": 19,
98 | "min": "00",
99 | "sec": 0,
100 | "isdst": "0",
101 | "monthname": "February",
102 | "monthnameShort": "Feb",
103 | "weekdayShort": "Fri",
104 | "weekday": "Friday",
105 | "ampm": "PM",
106 | "tzShort": "CET",
107 | "tzLong": "Europe/Madrid"
108 | },
109 | "period": 1,
110 | "high": {
111 | "fahrenheit": "59",
112 | "celsius": "15"
113 | },
114 | "low": {
115 | "fahrenheit": "33",
116 | "celsius": "1"
117 | },
118 | "conditions": "Clear",
119 | "icon": "clear",
120 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
121 | "skyicon": "",
122 | "pop": 10,
123 | "qpfAllday": {
124 | "in": 0.0,
125 | "mm": 0
126 | },
127 | "qpfDay": {
128 | "in": null,
129 | "mm": null
130 | },
131 | "qpfNight": {
132 | "in": 0.0,
133 | "mm": 0
134 | },
135 | "snowAllday": {
136 | "in": 0.0,
137 | "cm": 0.0
138 | },
139 | "snowDay": {
140 | "in": null,
141 | "cm": null
142 | },
143 | "snowNight": {
144 | "in": 0.0,
145 | "cm": 0.0
146 | },
147 | "maxwind": {
148 | "mph": 2,
149 | "kph": 4,
150 | "dir": "North",
151 | "degrees": 0
152 | },
153 | "avewind": {
154 | "mph": 0,
155 | "kph": 0,
156 | "dir": "Variable",
157 | "degrees": 0
158 | },
159 | "avehumidity": 75,
160 | "maxhumidity": 0,
161 | "minhumidity": 0
162 | },
163 | {
164 | "date": {
165 | "epoch": "1487440800",
166 | "pretty": "7:00 PM CET on February 18, 2017",
167 | "day": 18,
168 | "month": 2,
169 | "year": 2017,
170 | "yday": 48,
171 | "hour": 19,
172 | "min": "00",
173 | "sec": 0,
174 | "isdst": "0",
175 | "monthname": "February",
176 | "monthnameShort": "Feb",
177 | "weekdayShort": "Sat",
178 | "weekday": "Saturday",
179 | "ampm": "PM",
180 | "tzShort": "CET",
181 | "tzLong": "Europe/Madrid"
182 | },
183 | "period": 2,
184 | "high": {
185 | "fahrenheit": "58",
186 | "celsius": "14"
187 | },
188 | "low": {
189 | "fahrenheit": "40",
190 | "celsius": "4"
191 | },
192 | "conditions": "Clear",
193 | "icon": "clear",
194 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
195 | "skyicon": "",
196 | "pop": 10,
197 | "qpfAllday": {
198 | "in": 0.0,
199 | "mm": 0
200 | },
201 | "qpfDay": {
202 | "in": 0.0,
203 | "mm": 0
204 | },
205 | "qpfNight": {
206 | "in": 0.0,
207 | "mm": 0
208 | },
209 | "snowAllday": {
210 | "in": 0.0,
211 | "cm": 0.0
212 | },
213 | "snowDay": {
214 | "in": 0.0,
215 | "cm": 0.0
216 | },
217 | "snowNight": {
218 | "in": 0.0,
219 | "cm": 0.0
220 | },
221 | "maxwind": {
222 | "mph": 5,
223 | "kph": 8,
224 | "dir": "W",
225 | "degrees": 268
226 | },
227 | "avewind": {
228 | "mph": 4,
229 | "kph": 6,
230 | "dir": "W",
231 | "degrees": 268
232 | },
233 | "avehumidity": 65,
234 | "maxhumidity": 0,
235 | "minhumidity": 0
236 | },
237 | {
238 | "date": {
239 | "epoch": "1487527200",
240 | "pretty": "7:00 PM CET on February 19, 2017",
241 | "day": 19,
242 | "month": 2,
243 | "year": 2017,
244 | "yday": 49,
245 | "hour": 19,
246 | "min": "00",
247 | "sec": 0,
248 | "isdst": "0",
249 | "monthname": "February",
250 | "monthnameShort": "Feb",
251 | "weekdayShort": "Sun",
252 | "weekday": "Sunday",
253 | "ampm": "PM",
254 | "tzShort": "CET",
255 | "tzLong": "Europe/Madrid"
256 | },
257 | "period": 3,
258 | "high": {
259 | "fahrenheit": "58",
260 | "celsius": "14"
261 | },
262 | "low": {
263 | "fahrenheit": "36",
264 | "celsius": "2"
265 | },
266 | "conditions": "Partly Cloudy",
267 | "icon": "partlycloudy",
268 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
269 | "skyicon": "",
270 | "pop": 10,
271 | "qpfAllday": {
272 | "in": 0.0,
273 | "mm": 0
274 | },
275 | "qpfDay": {
276 | "in": 0.0,
277 | "mm": 0
278 | },
279 | "qpfNight": {
280 | "in": 0.0,
281 | "mm": 0
282 | },
283 | "snowAllday": {
284 | "in": 0.0,
285 | "cm": 0.0
286 | },
287 | "snowDay": {
288 | "in": 0.0,
289 | "cm": 0.0
290 | },
291 | "snowNight": {
292 | "in": 0.0,
293 | "cm": 0.0
294 | },
295 | "maxwind": {
296 | "mph": 15,
297 | "kph": 24,
298 | "dir": "NE",
299 | "degrees": 48
300 | },
301 | "avewind": {
302 | "mph": 10,
303 | "kph": 16,
304 | "dir": "NE",
305 | "degrees": 48
306 | },
307 | "avehumidity": 64,
308 | "maxhumidity": 0,
309 | "minhumidity": 0
310 | },
311 | {
312 | "date": {
313 | "epoch": "1487613600",
314 | "pretty": "7:00 PM CET on February 20, 2017",
315 | "day": 20,
316 | "month": 2,
317 | "year": 2017,
318 | "yday": 50,
319 | "hour": 19,
320 | "min": "00",
321 | "sec": 0,
322 | "isdst": "0",
323 | "monthname": "February",
324 | "monthnameShort": "Feb",
325 | "weekdayShort": "Mon",
326 | "weekday": "Monday",
327 | "ampm": "PM",
328 | "tzShort": "CET",
329 | "tzLong": "Europe/Madrid"
330 | },
331 | "period": 4,
332 | "high": {
333 | "fahrenheit": "58",
334 | "celsius": "14"
335 | },
336 | "low": {
337 | "fahrenheit": "36",
338 | "celsius": "2"
339 | },
340 | "conditions": "Clear",
341 | "icon": "clear",
342 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
343 | "skyicon": "",
344 | "pop": 10,
345 | "qpfAllday": {
346 | "in": 0.0,
347 | "mm": 0
348 | },
349 | "qpfDay": {
350 | "in": 0.0,
351 | "mm": 0
352 | },
353 | "qpfNight": {
354 | "in": 0.0,
355 | "mm": 0
356 | },
357 | "snowAllday": {
358 | "in": 0.0,
359 | "cm": 0.0
360 | },
361 | "snowDay": {
362 | "in": 0.0,
363 | "cm": 0.0
364 | },
365 | "snowNight": {
366 | "in": 0.0,
367 | "cm": 0.0
368 | },
369 | "maxwind": {
370 | "mph": 15,
371 | "kph": 24,
372 | "dir": "NE",
373 | "degrees": 51
374 | },
375 | "avewind": {
376 | "mph": 10,
377 | "kph": 16,
378 | "dir": "NE",
379 | "degrees": 51
380 | },
381 | "avehumidity": 62,
382 | "maxhumidity": 0,
383 | "minhumidity": 0
384 | }
385 | ]
386 | }
387 | }
388 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/main/assets/json/weather_toulouse.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "version": "0.1",
4 | "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
5 | "features": {
6 | "forecast": 1
7 | }
8 | },
9 | "forecast": {
10 | "txtForecast": {
11 | "date": "2:30 PM CET",
12 | "forecastday": [
13 | {
14 | "period": 0,
15 | "icon": "clear",
16 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
17 | "title": "Friday",
18 | "fcttext": "Mostly clear. Lows overnight in the upper 30s.",
19 | "fcttextMetric": "Mainly clear. Low 3C.",
20 | "pop": "10"
21 | },
22 | {
23 | "period": 1,
24 | "icon": "nt_clear",
25 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
26 | "title": "Friday Night",
27 | "fcttext": "Clear to partly cloudy. Low 37F. Winds WSW at 5 to 10 mph.",
28 | "fcttextMetric": "A few passing clouds. Low 3C. Winds WSW at 10 to 15 km/h.",
29 | "pop": "10"
30 | },
31 | {
32 | "period": 2,
33 | "icon": "partlycloudy",
34 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
35 | "title": "Saturday",
36 | "fcttext": "Partly cloudy. High 61F. Winds light and variable.",
37 | "fcttextMetric": "Sunshine and clouds mixed. High 16C. Winds light and variable.",
38 | "pop": "10"
39 | },
40 | {
41 | "period": 3,
42 | "icon": "nt_partlycloudy",
43 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
44 | "title": "Saturday Night",
45 | "fcttext": "A few clouds. Low 39F. Winds ESE at 5 to 10 mph.",
46 | "fcttextMetric": "A few clouds. Low 4C. Winds light and variable.",
47 | "pop": "10"
48 | },
49 | {
50 | "period": 4,
51 | "icon": "clear",
52 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
53 | "title": "Sunday",
54 | "fcttext": "Except for a few afternoon clouds, mainly sunny. High 58F. Winds light and variable.",
55 | "fcttextMetric": "Mostly sunny skies. High 14C. Winds light and variable.",
56 | "pop": "10"
57 | },
58 | {
59 | "period": 5,
60 | "icon": "nt_partlycloudy",
61 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
62 | "title": "Sunday Night",
63 | "fcttext": "Partly cloudy. Low around 40F. Winds light and variable.",
64 | "fcttextMetric": "Partly cloudy skies. Low near 5C. Winds light and variable.",
65 | "pop": "10"
66 | },
67 | {
68 | "period": 6,
69 | "icon": "clear",
70 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
71 | "title": "Monday",
72 | "fcttext": "A few clouds early, otherwise mostly sunny. High 56F. Winds WNW at 5 to 10 mph.",
73 | "fcttextMetric": "A few clouds early, otherwise mostly sunny. High 13C. Winds WNW at 10 to 15 km/h.",
74 | "pop": "10"
75 | },
76 | {
77 | "period": 7,
78 | "icon": "nt_partlycloudy",
79 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
80 | "title": "Monday Night",
81 | "fcttext": "A few clouds. Low 37F. Winds light and variable.",
82 | "fcttextMetric": "Partly cloudy. Low 3C. Winds light and variable.",
83 | "pop": "10"
84 | }
85 | ]
86 | },
87 | "simpleforecast": {
88 | "forecastday": [
89 | {
90 | "date": {
91 | "epoch": "1487354400",
92 | "pretty": "7:00 PM CET on February 17, 2017",
93 | "day": 17,
94 | "month": 2,
95 | "year": 2017,
96 | "yday": 47,
97 | "hour": 19,
98 | "min": "00",
99 | "sec": 0,
100 | "isdst": "0",
101 | "monthname": "February",
102 | "monthnameShort": "Feb",
103 | "weekdayShort": "Fri",
104 | "weekday": "Friday",
105 | "ampm": "PM",
106 | "tzShort": "CET",
107 | "tzLong": "Europe/Paris"
108 | },
109 | "period": 1,
110 | "high": {
111 | "fahrenheit": "63",
112 | "celsius": "17"
113 | },
114 | "low": {
115 | "fahrenheit": "37",
116 | "celsius": "3"
117 | },
118 | "conditions": "Clear",
119 | "icon": "clear",
120 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
121 | "skyicon": "",
122 | "pop": 10,
123 | "qpfAllday": {
124 | "in": 0.0,
125 | "mm": 0
126 | },
127 | "qpfDay": {
128 | "in": null,
129 | "mm": null
130 | },
131 | "qpfNight": {
132 | "in": 0.0,
133 | "mm": 0
134 | },
135 | "snowAllday": {
136 | "in": 0.0,
137 | "cm": 0.0
138 | },
139 | "snowDay": {
140 | "in": null,
141 | "cm": null
142 | },
143 | "snowNight": {
144 | "in": 0.0,
145 | "cm": 0.0
146 | },
147 | "maxwind": {
148 | "mph": 4,
149 | "kph": 7,
150 | "dir": "",
151 | "degrees": 0
152 | },
153 | "avewind": {
154 | "mph": 1,
155 | "kph": 1,
156 | "dir": "WSW",
157 | "degrees": 248
158 | },
159 | "avehumidity": 81,
160 | "maxhumidity": 0,
161 | "minhumidity": 0
162 | },
163 | {
164 | "date": {
165 | "epoch": "1487440800",
166 | "pretty": "7:00 PM CET on February 18, 2017",
167 | "day": 18,
168 | "month": 2,
169 | "year": 2017,
170 | "yday": 48,
171 | "hour": 19,
172 | "min": "00",
173 | "sec": 0,
174 | "isdst": "0",
175 | "monthname": "February",
176 | "monthnameShort": "Feb",
177 | "weekdayShort": "Sat",
178 | "weekday": "Saturday",
179 | "ampm": "PM",
180 | "tzShort": "CET",
181 | "tzLong": "Europe/Paris"
182 | },
183 | "period": 2,
184 | "high": {
185 | "fahrenheit": "61",
186 | "celsius": "16"
187 | },
188 | "low": {
189 | "fahrenheit": "39",
190 | "celsius": "4"
191 | },
192 | "conditions": "Partly Cloudy",
193 | "icon": "partlycloudy",
194 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
195 | "skyicon": "",
196 | "pop": 10,
197 | "qpfAllday": {
198 | "in": 0.0,
199 | "mm": 0
200 | },
201 | "qpfDay": {
202 | "in": 0.0,
203 | "mm": 0
204 | },
205 | "qpfNight": {
206 | "in": 0.0,
207 | "mm": 0
208 | },
209 | "snowAllday": {
210 | "in": 0.0,
211 | "cm": 0.0
212 | },
213 | "snowDay": {
214 | "in": 0.0,
215 | "cm": 0.0
216 | },
217 | "snowNight": {
218 | "in": 0.0,
219 | "cm": 0.0
220 | },
221 | "maxwind": {
222 | "mph": 10,
223 | "kph": 16,
224 | "dir": "E",
225 | "degrees": 93
226 | },
227 | "avewind": {
228 | "mph": 5,
229 | "kph": 8,
230 | "dir": "E",
231 | "degrees": 93
232 | },
233 | "avehumidity": 74,
234 | "maxhumidity": 0,
235 | "minhumidity": 0
236 | },
237 | {
238 | "date": {
239 | "epoch": "1487527200",
240 | "pretty": "7:00 PM CET on February 19, 2017",
241 | "day": 19,
242 | "month": 2,
243 | "year": 2017,
244 | "yday": 49,
245 | "hour": 19,
246 | "min": "00",
247 | "sec": 0,
248 | "isdst": "0",
249 | "monthname": "February",
250 | "monthnameShort": "Feb",
251 | "weekdayShort": "Sun",
252 | "weekday": "Sunday",
253 | "ampm": "PM",
254 | "tzShort": "CET",
255 | "tzLong": "Europe/Paris"
256 | },
257 | "period": 3,
258 | "high": {
259 | "fahrenheit": "58",
260 | "celsius": "14"
261 | },
262 | "low": {
263 | "fahrenheit": "40",
264 | "celsius": "4"
265 | },
266 | "conditions": "Clear",
267 | "icon": "clear",
268 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
269 | "skyicon": "",
270 | "pop": 10,
271 | "qpfAllday": {
272 | "in": 0.0,
273 | "mm": 0
274 | },
275 | "qpfDay": {
276 | "in": 0.0,
277 | "mm": 0
278 | },
279 | "qpfNight": {
280 | "in": 0.0,
281 | "mm": 0
282 | },
283 | "snowAllday": {
284 | "in": 0.0,
285 | "cm": 0.0
286 | },
287 | "snowDay": {
288 | "in": 0.0,
289 | "cm": 0.0
290 | },
291 | "snowNight": {
292 | "in": 0.0,
293 | "cm": 0.0
294 | },
295 | "maxwind": {
296 | "mph": 5,
297 | "kph": 8,
298 | "dir": "SSW",
299 | "degrees": 212
300 | },
301 | "avewind": {
302 | "mph": 4,
303 | "kph": 6,
304 | "dir": "SSW",
305 | "degrees": 212
306 | },
307 | "avehumidity": 72,
308 | "maxhumidity": 0,
309 | "minhumidity": 0
310 | },
311 | {
312 | "date": {
313 | "epoch": "1487613600",
314 | "pretty": "7:00 PM CET on February 20, 2017",
315 | "day": 20,
316 | "month": 2,
317 | "year": 2017,
318 | "yday": 50,
319 | "hour": 19,
320 | "min": "00",
321 | "sec": 0,
322 | "isdst": "0",
323 | "monthname": "February",
324 | "monthnameShort": "Feb",
325 | "weekdayShort": "Mon",
326 | "weekday": "Monday",
327 | "ampm": "PM",
328 | "tzShort": "CET",
329 | "tzLong": "Europe/Paris"
330 | },
331 | "period": 4,
332 | "high": {
333 | "fahrenheit": "56",
334 | "celsius": "13"
335 | },
336 | "low": {
337 | "fahrenheit": "37",
338 | "celsius": "3"
339 | },
340 | "conditions": "Clear",
341 | "icon": "clear",
342 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
343 | "skyicon": "",
344 | "pop": 10,
345 | "qpfAllday": {
346 | "in": 0.0,
347 | "mm": 0
348 | },
349 | "qpfDay": {
350 | "in": 0.0,
351 | "mm": 0
352 | },
353 | "qpfNight": {
354 | "in": 0.0,
355 | "mm": 0
356 | },
357 | "snowAllday": {
358 | "in": 0.0,
359 | "cm": 0.0
360 | },
361 | "snowDay": {
362 | "in": 0.0,
363 | "cm": 0.0
364 | },
365 | "snowNight": {
366 | "in": 0.0,
367 | "cm": 0.0
368 | },
369 | "maxwind": {
370 | "mph": 10,
371 | "kph": 16,
372 | "dir": "WNW",
373 | "degrees": 297
374 | },
375 | "avewind": {
376 | "mph": 6,
377 | "kph": 10,
378 | "dir": "WNW",
379 | "degrees": 297
380 | },
381 | "avehumidity": 73,
382 | "maxhumidity": 0,
383 | "minhumidity": 0
384 | }
385 | ]
386 | }
387 | }
388 | }
--------------------------------------------------------------------------------
/weatherapp/app/src/test/resources/json/weather_toulouse.json:
--------------------------------------------------------------------------------
1 | {
2 | "response": {
3 | "version": "0.1",
4 | "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
5 | "features": {
6 | "forecast": 1
7 | }
8 | },
9 | "forecast": {
10 | "txtForecast": {
11 | "date": "2:30 PM CET",
12 | "forecastday": [
13 | {
14 | "period": 0,
15 | "icon": "clear",
16 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
17 | "title": "Friday",
18 | "fcttext": "Mostly clear. Lows overnight in the upper 30s.",
19 | "fcttextMetric": "Mainly clear. Low 3C.",
20 | "pop": "10"
21 | },
22 | {
23 | "period": 1,
24 | "icon": "nt_clear",
25 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_clear.gif",
26 | "title": "Friday Night",
27 | "fcttext": "Clear to partly cloudy. Low 37F. Winds WSW at 5 to 10 mph.",
28 | "fcttextMetric": "A few passing clouds. Low 3C. Winds WSW at 10 to 15 km/h.",
29 | "pop": "10"
30 | },
31 | {
32 | "period": 2,
33 | "icon": "partlycloudy",
34 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
35 | "title": "Saturday",
36 | "fcttext": "Partly cloudy. High 61F. Winds light and variable.",
37 | "fcttextMetric": "Sunshine and clouds mixed. High 16C. Winds light and variable.",
38 | "pop": "10"
39 | },
40 | {
41 | "period": 3,
42 | "icon": "nt_partlycloudy",
43 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
44 | "title": "Saturday Night",
45 | "fcttext": "A few clouds. Low 39F. Winds ESE at 5 to 10 mph.",
46 | "fcttextMetric": "A few clouds. Low 4C. Winds light and variable.",
47 | "pop": "10"
48 | },
49 | {
50 | "period": 4,
51 | "icon": "clear",
52 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
53 | "title": "Sunday",
54 | "fcttext": "Except for a few afternoon clouds, mainly sunny. High 58F. Winds light and variable.",
55 | "fcttextMetric": "Mostly sunny skies. High 14C. Winds light and variable.",
56 | "pop": "10"
57 | },
58 | {
59 | "period": 5,
60 | "icon": "nt_partlycloudy",
61 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
62 | "title": "Sunday Night",
63 | "fcttext": "Partly cloudy. Low around 40F. Winds light and variable.",
64 | "fcttextMetric": "Partly cloudy skies. Low near 5C. Winds light and variable.",
65 | "pop": "10"
66 | },
67 | {
68 | "period": 6,
69 | "icon": "clear",
70 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
71 | "title": "Monday",
72 | "fcttext": "A few clouds early, otherwise mostly sunny. High 56F. Winds WNW at 5 to 10 mph.",
73 | "fcttextMetric": "A few clouds early, otherwise mostly sunny. High 13C. Winds WNW at 10 to 15 km/h.",
74 | "pop": "10"
75 | },
76 | {
77 | "period": 7,
78 | "icon": "nt_partlycloudy",
79 | "iconUrl": "http://icons.wxug.com/i/c/k/nt_partlycloudy.gif",
80 | "title": "Monday Night",
81 | "fcttext": "A few clouds. Low 37F. Winds light and variable.",
82 | "fcttextMetric": "Partly cloudy. Low 3C. Winds light and variable.",
83 | "pop": "10"
84 | }
85 | ]
86 | },
87 | "simpleforecast": {
88 | "forecastday": [
89 | {
90 | "date": {
91 | "epoch": "1487354400",
92 | "pretty": "7:00 PM CET on February 17, 2017",
93 | "day": 17,
94 | "month": 2,
95 | "year": 2017,
96 | "yday": 47,
97 | "hour": 19,
98 | "min": "00",
99 | "sec": 0,
100 | "isdst": "0",
101 | "monthname": "February",
102 | "monthnameShort": "Feb",
103 | "weekdayShort": "Fri",
104 | "weekday": "Friday",
105 | "ampm": "PM",
106 | "tzShort": "CET",
107 | "tzLong": "Europe/Paris"
108 | },
109 | "period": 1,
110 | "high": {
111 | "fahrenheit": "63",
112 | "celsius": "17"
113 | },
114 | "low": {
115 | "fahrenheit": "37",
116 | "celsius": "3"
117 | },
118 | "conditions": "Clear",
119 | "icon": "clear",
120 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
121 | "skyicon": "",
122 | "pop": 10,
123 | "qpfAllday": {
124 | "in": 0.0,
125 | "mm": 0
126 | },
127 | "qpfDay": {
128 | "in": null,
129 | "mm": null
130 | },
131 | "qpfNight": {
132 | "in": 0.0,
133 | "mm": 0
134 | },
135 | "snowAllday": {
136 | "in": 0.0,
137 | "cm": 0.0
138 | },
139 | "snowDay": {
140 | "in": null,
141 | "cm": null
142 | },
143 | "snowNight": {
144 | "in": 0.0,
145 | "cm": 0.0
146 | },
147 | "maxwind": {
148 | "mph": 4,
149 | "kph": 7,
150 | "dir": "",
151 | "degrees": 0
152 | },
153 | "avewind": {
154 | "mph": 1,
155 | "kph": 1,
156 | "dir": "WSW",
157 | "degrees": 248
158 | },
159 | "avehumidity": 81,
160 | "maxhumidity": 0,
161 | "minhumidity": 0
162 | },
163 | {
164 | "date": {
165 | "epoch": "1487440800",
166 | "pretty": "7:00 PM CET on February 18, 2017",
167 | "day": 18,
168 | "month": 2,
169 | "year": 2017,
170 | "yday": 48,
171 | "hour": 19,
172 | "min": "00",
173 | "sec": 0,
174 | "isdst": "0",
175 | "monthname": "February",
176 | "monthnameShort": "Feb",
177 | "weekdayShort": "Sat",
178 | "weekday": "Saturday",
179 | "ampm": "PM",
180 | "tzShort": "CET",
181 | "tzLong": "Europe/Paris"
182 | },
183 | "period": 2,
184 | "high": {
185 | "fahrenheit": "61",
186 | "celsius": "16"
187 | },
188 | "low": {
189 | "fahrenheit": "39",
190 | "celsius": "4"
191 | },
192 | "conditions": "Partly Cloudy",
193 | "icon": "partlycloudy",
194 | "iconUrl": "http://icons.wxug.com/i/c/k/partlycloudy.gif",
195 | "skyicon": "",
196 | "pop": 10,
197 | "qpfAllday": {
198 | "in": 0.0,
199 | "mm": 0
200 | },
201 | "qpfDay": {
202 | "in": 0.0,
203 | "mm": 0
204 | },
205 | "qpfNight": {
206 | "in": 0.0,
207 | "mm": 0
208 | },
209 | "snowAllday": {
210 | "in": 0.0,
211 | "cm": 0.0
212 | },
213 | "snowDay": {
214 | "in": 0.0,
215 | "cm": 0.0
216 | },
217 | "snowNight": {
218 | "in": 0.0,
219 | "cm": 0.0
220 | },
221 | "maxwind": {
222 | "mph": 10,
223 | "kph": 16,
224 | "dir": "E",
225 | "degrees": 93
226 | },
227 | "avewind": {
228 | "mph": 5,
229 | "kph": 8,
230 | "dir": "E",
231 | "degrees": 93
232 | },
233 | "avehumidity": 74,
234 | "maxhumidity": 0,
235 | "minhumidity": 0
236 | },
237 | {
238 | "date": {
239 | "epoch": "1487527200",
240 | "pretty": "7:00 PM CET on February 19, 2017",
241 | "day": 19,
242 | "month": 2,
243 | "year": 2017,
244 | "yday": 49,
245 | "hour": 19,
246 | "min": "00",
247 | "sec": 0,
248 | "isdst": "0",
249 | "monthname": "February",
250 | "monthnameShort": "Feb",
251 | "weekdayShort": "Sun",
252 | "weekday": "Sunday",
253 | "ampm": "PM",
254 | "tzShort": "CET",
255 | "tzLong": "Europe/Paris"
256 | },
257 | "period": 3,
258 | "high": {
259 | "fahrenheit": "58",
260 | "celsius": "14"
261 | },
262 | "low": {
263 | "fahrenheit": "40",
264 | "celsius": "4"
265 | },
266 | "conditions": "Clear",
267 | "icon": "clear",
268 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
269 | "skyicon": "",
270 | "pop": 10,
271 | "qpfAllday": {
272 | "in": 0.0,
273 | "mm": 0
274 | },
275 | "qpfDay": {
276 | "in": 0.0,
277 | "mm": 0
278 | },
279 | "qpfNight": {
280 | "in": 0.0,
281 | "mm": 0
282 | },
283 | "snowAllday": {
284 | "in": 0.0,
285 | "cm": 0.0
286 | },
287 | "snowDay": {
288 | "in": 0.0,
289 | "cm": 0.0
290 | },
291 | "snowNight": {
292 | "in": 0.0,
293 | "cm": 0.0
294 | },
295 | "maxwind": {
296 | "mph": 5,
297 | "kph": 8,
298 | "dir": "SSW",
299 | "degrees": 212
300 | },
301 | "avewind": {
302 | "mph": 4,
303 | "kph": 6,
304 | "dir": "SSW",
305 | "degrees": 212
306 | },
307 | "avehumidity": 72,
308 | "maxhumidity": 0,
309 | "minhumidity": 0
310 | },
311 | {
312 | "date": {
313 | "epoch": "1487613600",
314 | "pretty": "7:00 PM CET on February 20, 2017",
315 | "day": 20,
316 | "month": 2,
317 | "year": 2017,
318 | "yday": 50,
319 | "hour": 19,
320 | "min": "00",
321 | "sec": 0,
322 | "isdst": "0",
323 | "monthname": "February",
324 | "monthnameShort": "Feb",
325 | "weekdayShort": "Mon",
326 | "weekday": "Monday",
327 | "ampm": "PM",
328 | "tzShort": "CET",
329 | "tzLong": "Europe/Paris"
330 | },
331 | "period": 4,
332 | "high": {
333 | "fahrenheit": "56",
334 | "celsius": "13"
335 | },
336 | "low": {
337 | "fahrenheit": "37",
338 | "celsius": "3"
339 | },
340 | "conditions": "Clear",
341 | "icon": "clear",
342 | "iconUrl": "http://icons.wxug.com/i/c/k/clear.gif",
343 | "skyicon": "",
344 | "pop": 10,
345 | "qpfAllday": {
346 | "in": 0.0,
347 | "mm": 0
348 | },
349 | "qpfDay": {
350 | "in": 0.0,
351 | "mm": 0
352 | },
353 | "qpfNight": {
354 | "in": 0.0,
355 | "mm": 0
356 | },
357 | "snowAllday": {
358 | "in": 0.0,
359 | "cm": 0.0
360 | },
361 | "snowDay": {
362 | "in": 0.0,
363 | "cm": 0.0
364 | },
365 | "snowNight": {
366 | "in": 0.0,
367 | "cm": 0.0
368 | },
369 | "maxwind": {
370 | "mph": 10,
371 | "kph": 16,
372 | "dir": "WNW",
373 | "degrees": 297
374 | },
375 | "avewind": {
376 | "mph": 6,
377 | "kph": 10,
378 | "dir": "WNW",
379 | "degrees": 297
380 | },
381 | "avehumidity": 73,
382 | "maxhumidity": 0,
383 | "minhumidity": 0
384 | }
385 | ]
386 | }
387 | }
388 | }
--------------------------------------------------------------------------------