├── .gitignore ├── LICENSE ├── README.md ├── img └── workshop_logo.png └── weatherapp ├── LICENSE ├── app ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── fr │ │ └── ekito │ │ └── myweatherapp │ │ ├── WeatherDAOTest.kt │ │ ├── WeatherRepositoryTest.kt │ │ └── room_test_modules.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── json │ │ │ ├── geocode_berlin.json │ │ │ ├── geocode_london.json │ │ │ ├── geocode_madrid.json │ │ │ ├── geocode_paris.json │ │ │ ├── geocode_toulouse.json │ │ │ ├── weather_berlin.json │ │ │ ├── weather_london.json │ │ │ ├── weather_madrid.json │ │ │ ├── weather_paris.json │ │ │ └── weather_toulouse.json │ │ └── koin.properties │ ├── kotlin │ │ └── fr │ │ │ └── ekito │ │ │ └── myweatherapp │ │ │ ├── MainApplication.kt │ │ │ ├── data │ │ │ ├── WeatherDataSource.kt │ │ │ ├── json │ │ │ │ ├── Geocode.kt │ │ │ │ └── Weather.kt │ │ │ └── local │ │ │ │ ├── AndroidJsonReader.kt │ │ │ │ ├── BaseReader.kt │ │ │ │ ├── FileDataSource.kt │ │ │ │ ├── JavaReader.kt │ │ │ │ └── JsonReader.kt │ │ │ ├── di │ │ │ ├── app_module.kt │ │ │ ├── local_datasource_module.kt │ │ │ └── remote_datasource_module.kt │ │ │ ├── domain │ │ │ ├── entity │ │ │ │ ├── DailyForecast.kt │ │ │ │ └── WeatherCode.kt │ │ │ ├── ext │ │ │ │ ├── GeocodeExt.kt │ │ │ │ └── WeatherExt.kt │ │ │ └── repository │ │ │ │ └── DailyForecastRepository.kt │ │ │ ├── util │ │ │ ├── android │ │ │ │ └── FragmentActivityExt.kt │ │ │ ├── mvp │ │ │ │ ├── BasePresenter.kt │ │ │ │ ├── BaseView.kt │ │ │ │ └── RxPresenter.kt │ │ │ ├── mvvm │ │ │ │ ├── RxViewModel.kt │ │ │ │ └── SingleLiveEvent.kt │ │ │ └── rx │ │ │ │ ├── ApplicationSchedulerProvider.kt │ │ │ │ ├── RxWithExt.kt │ │ │ │ └── SchedulerProvider.kt │ │ │ └── view │ │ │ ├── detail │ │ │ ├── DetailActivity.kt │ │ │ ├── DetailContract.kt │ │ │ └── DetailPresenter.kt │ │ │ ├── splash │ │ │ ├── SplashActivity.kt │ │ │ ├── SplashContract.kt │ │ │ └── SplashPresenter.kt │ │ │ └── weather │ │ │ ├── WeatherActivity.kt │ │ │ ├── WeatherHeaderContract.kt │ │ │ ├── WeatherHeaderFragment.kt │ │ │ ├── WeatherHeaderPresenter.kt │ │ │ ├── WeatherListContract.kt │ │ │ ├── WeatherListFragment.kt │ │ │ ├── WeatherListPresenter.kt │ │ │ └── list │ │ │ ├── WeatherItem.kt │ │ │ └── WeatherListAdapter.kt │ └── res │ │ ├── anim │ │ └── infinite_blinking_animation.xml │ │ ├── drawable │ │ ├── ic_edit_black_24dp.xml │ │ ├── ic_edit_location_black_24dp.xml │ │ ├── ic_location_on_black_24dp.xml │ │ └── ic_search_black_24dp.xml │ │ ├── font │ │ ├── indieflower.ttf │ │ └── opensans_regular.ttf │ │ ├── layout │ │ ├── activity_detail.xml │ │ ├── activity_result.xml │ │ ├── activity_splash.xml │ │ ├── fragment_result_header.xml │ │ ├── fragment_result_list.xml │ │ └── item_weather.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── 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 │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ ├── java │ └── fr │ │ └── ekito │ │ └── myweatherapp │ │ ├── ModuleCheckTest.kt │ │ ├── di │ │ └── test_modules.kt │ │ ├── integration │ │ └── WeatherRepositoryTest.kt │ │ ├── mock │ │ ├── MockedData.kt │ │ ├── mvp │ │ │ ├── DetailPresenterMockTest.kt │ │ │ ├── SplashPresenterMockTest.kt │ │ │ ├── WeatherHeaderPresenterMockTest.kt │ │ │ └── WeatherListPresenterMockTest.kt │ │ └── mvvm │ │ │ ├── DetailViewModelMockTest.kt │ │ │ ├── SplashViewModelMockTest.kt │ │ │ ├── WeatherHeaderViewModelMockTest.kt │ │ │ └── WeatherListViewModelMockTest.kt │ │ └── util │ │ ├── MockitoKotlinHelpers.kt │ │ └── TestSchedulerProvider.kt │ └── resources │ ├── json │ ├── geocode_berlin.json │ ├── geocode_london.json │ ├── geocode_madrid.json │ ├── geocode_paris.json │ ├── geocode_toulouse.json │ ├── weather_berlin.json │ ├── weather_london.json │ ├── weather_madrid.json │ ├── weather_paris.json │ └── weather_toulouse.json │ ├── koin.properties │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── versions.gradle /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Architecture & Architecture Components 2 | 3 | ![](./img/workshop_logo.png) 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 | -------------------------------------------------------------------------------- /img/workshop_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/img/workshop_logo.png -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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_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_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/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/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/main/assets/koin.properties: -------------------------------------------------------------------------------- 1 | SERVER_URL=https://my-weather-api.herokuapp.com/ -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/anim/infinite_blinking_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/drawable/ic_edit_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/drawable/ic_edit_location_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/drawable/ic_location_on_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/drawable/ic_search_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/font/indieflower.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/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/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/weatherapp/app/src/main/res/font/opensans_regular.ttf -------------------------------------------------------------------------------- /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/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/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | 23 | 32 | 33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /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/layout/fragment_result_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /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/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/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/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/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/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/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/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/weatherapp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KotlinAndroidWorkshops/2018-android-architecture-components-workshop/28b3dbddf6bb8002d1658b31bd08acf01f873f6e/weatherapp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /weatherapp/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /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/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/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/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 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 |