├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hellohasan │ │ └── weatherappmvvmdagger │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── city_list.json │ ├── java │ │ └── com │ │ │ └── hellohasan │ │ │ └── weatherappmvvmdagger │ │ │ ├── common │ │ │ ├── App.kt │ │ │ └── RequestCompleteListener.kt │ │ │ ├── di │ │ │ ├── AppModule.kt │ │ │ ├── ApplicationComponent.kt │ │ │ └── MainActivityModule.kt │ │ │ ├── features │ │ │ └── weather_info_show │ │ │ │ ├── model │ │ │ │ ├── WeatherInfoShowModel.kt │ │ │ │ ├── WeatherInfoShowModelImpl.kt │ │ │ │ └── data_class │ │ │ │ │ ├── City.kt │ │ │ │ │ ├── Clouds.kt │ │ │ │ │ ├── Coord.kt │ │ │ │ │ ├── Main.kt │ │ │ │ │ ├── Sys.kt │ │ │ │ │ ├── Weather.kt │ │ │ │ │ ├── WeatherData.kt │ │ │ │ │ ├── WeatherInfoResponse.kt │ │ │ │ │ └── Wind.kt │ │ │ │ ├── view │ │ │ │ └── MainActivity.kt │ │ │ │ └── viewmodel │ │ │ │ ├── WeatherInfoViewModel.kt │ │ │ │ └── WeatherInfoViewModelFactory.kt │ │ │ ├── network │ │ │ ├── ApiInterface.kt │ │ │ ├── QueryParameterAddInterceptor.kt │ │ │ └── RetrofitClient.kt │ │ │ └── utils │ │ │ └── Extensions.kt │ └── res │ │ ├── drawable-v24 │ │ ├── haze.png │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_sunrise.xml │ │ └── ic_sunset.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── layout_input_part.xml │ │ ├── layout_sunrise_sunset.xml │ │ ├── layout_weather_additional_info.xml │ │ └── layout_weather_basic_info.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── hellohasan │ └── weatherappmvvmdagger │ └── ExampleUnitTest.kt ├── build.gradle ├── data └── screenshot_1.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the code 15 | uses: actions/checkout@v2 16 | 17 | - name: Fetch Secrets and Add in Environment 18 | env: 19 | BASE_URL: ${{ secrets.BASE_URL }} 20 | APP_ID: ${{ secrets.APP_ID }} 21 | run: echo "Base Url = $BASE_URL App Id = $APP_ID" 22 | 23 | - name: Add local.properties file 24 | run: echo -e "base_url= $BASE_URL \n app_id= $APP_ID" > ./local.properties 25 | 26 | - name: Generate Lint Report 27 | run: ./gradlew lintDebug 28 | 29 | - name: Upload Lint report 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: lint.html 33 | path: app/build/reports/lint-results-debug.html 34 | 35 | unit-test: 36 | needs: [ lint ] 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout the code 40 | uses: actions/checkout@v2 41 | 42 | - name: Fetch Secrets and Add in Environment 43 | env: 44 | BASE_URL: ${{ secrets.BASE_URL }} 45 | APP_ID: ${{ secrets.APP_ID }} 46 | run: echo "Base Url = $BASE_URL App Id = $APP_ID" 47 | 48 | - name: Add local.properties file 49 | run: echo -e "base_url= $BASE_URL \n app_id= $APP_ID" > ./local.properties 50 | - name: Generate test report 51 | run: ./gradlew test 52 | 53 | - name: Upload unit test report 54 | uses: actions/upload-artifact@v2 55 | with: 56 | name: unit-test-report 57 | path: app/build/reports/tests/testDebugUnitTest 58 | 59 | instrumentation-test: 60 | needs: [ unit-test ] 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout the code 64 | uses: actions/checkout@v2 65 | 66 | - name: Fetch Secrets and Add in Environment 67 | env: 68 | BASE_URL: ${{ secrets.BASE_URL }} 69 | APP_ID: ${{ secrets.APP_ID }} 70 | run: echo "Base Url = $BASE_URL App Id = $APP_ID" 71 | 72 | - name: Add local.properties file 73 | run: echo -e "base_url= $BASE_URL \n app_id= $APP_ID" > ./local.properties 74 | 75 | - name: Generate AndroidTest Report 76 | uses: reactivecircus/android-emulator-runner@v2 77 | with: 78 | api-level: 29 79 | script: ./gradlew connectedCheck 80 | 81 | - name: Upload AndroidTest report 82 | uses: actions/upload-artifact@v2 83 | with: 84 | name: instrumentation-test-report 85 | path: app/build/reports/androidTests/connected/ 86 | 87 | static-code-analysis: 88 | needs: [ instrumentation-test ] 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Checkout the code 92 | uses: actions/checkout@v2 93 | 94 | - name: Fetch Secrets and Add in Environment 95 | env: 96 | BASE_URL: ${{ secrets.BASE_URL }} 97 | APP_ID: ${{ secrets.APP_ID }} 98 | run: echo "Base Url = $BASE_URL App Id = $APP_ID" 99 | 100 | - name: Add local.properties file 101 | run: echo -e "base_url= $BASE_URL \n app_id= $APP_ID" > ./local.properties 102 | 103 | - name: set up JDK 11 104 | uses: actions/setup-java@v1 105 | with: 106 | java-version: 11 107 | 108 | - name: SonarCloud Scan 109 | run: ./gradlew app:sonarqube -D sonar.login=${{ secrets.SONAR_TOKEN }} 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | 113 | build-apk: 114 | needs: [ static-code-analysis ] 115 | runs-on: ubuntu-latest 116 | steps: 117 | - name: Checkout the code 118 | uses: actions/checkout@v2 119 | 120 | - name: Fetch Secrets and Add in Environment 121 | env: 122 | BASE_URL: ${{ secrets.BASE_URL }} 123 | APP_ID: ${{ secrets.APP_ID }} 124 | run: echo "Base Url = $BASE_URL App Id = $APP_ID" 125 | 126 | - name: Add local.properties file 127 | run: echo -e "base_url= $BASE_URL \n app_id= $APP_ID" > ./local.properties 128 | 129 | - name: set up JDK 11 130 | uses: actions/setup-java@v1 131 | with: 132 | java-version: 11 133 | 134 | - name: Build debug APK 135 | run: ./gradlew assembleDebug 136 | 137 | - name: Upload APK 138 | uses: actions/upload-artifact@v2 139 | with: 140 | name: app-debug.apk 141 | path: app/build/outputs/apk/debug/app-debug.apk 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | .DS_Store 25 | /build 26 | /captures 27 | 28 | # Local configuration file (sdk path, etc) 29 | local.properties 30 | 31 | # Proguard folder generated by Eclipse 32 | proguard/ 33 | 34 | # Log Files 35 | *.log 36 | 37 | # Android Studio Navigation editor temp files 38 | .navigation/ 39 | 40 | # Android Studio captures folder 41 | captures/ 42 | 43 | # IntelliJ 44 | *.iml 45 | .idea/ 46 | 47 | # Keystore files 48 | # Uncomment the following lines if you do not want to check your keystore files in. 49 | #*.jks 50 | #*.keystore 51 | 52 | # External native build folder generated in Android Studio 2.2 and later 53 | .externalNativeBuild 54 | .cxx/ 55 | 56 | # Google Services (e.g. APIs or Firebase) 57 | # google-services.json 58 | 59 | # Freeline 60 | freeline.py 61 | freeline/ 62 | freeline_project_description.json 63 | 64 | # fastlane 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | fastlane/readme.md 70 | 71 | # Version control 72 | vcs.xml 73 | 74 | # lint 75 | lint/intermediates/ 76 | lint/generated/ 77 | lint/outputs/ 78 | lint/tmp/ 79 | # lint/reports/ 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hasan Abdullah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android MVVM Weather App (Kotlin + Retrofit + Dagger) 2 | 3 | MVVM Architecture is one of the most popular and latest architecture to develop a maintanable and managable codebase. We are developing a sample `Weater Forecast` Android App with `MVVM Architecture` using `Kotlin` language and `Retrofit` network calling library. **For simplification, I didn't use `Coroutine` or `Rx` in this project.** My main focus is to implement `MVVM` and `Dagger 2` dependency injection library as simple as possible. 4 | 5 | > If you are not familiar with `MVVM` architecture, then I recommend you to check [this MVVM respository](https://github.com/hasancse91/weather-app-android-mvvm) with same Weather App project. In that project, you'll find simple `MVVM` implementation without `Dagger`. After completion of that project, you can continue this repository. 6 | 7 | Same weather app project is available for Flutter in [this repository](https://github.com/hasancse91/weather_app_flutter). 8 | 9 | 10 | 11 | ### Prerequisites 12 | Basic `Kotlin` and knowledge of `HTTP` request by `Retrofit` library are required for this project. If you already know `MVP Architecture` then this project will be very easy to understand. You can check my `MVP Architecture` Weather App [repository from here](https://github.com/hasancse91/weather-app-android-mvp-architecture). It will be helpful if you cover the MVP project then compare between MVP and MVVM. There is my [another repository](https://github.com/hasancse91/weather-app-android-mvvm) where I've implemented same weather App in MVVM architecture without Dagger. 13 | 14 | ### Project Description 15 | We will develop a weather forecast Android Application with MVP architecture. The UI will be as like as above screenshot. There is a `Spinner` with some `City` name. After selection a city user need to hit the `View Weather` button. Then App will send request to Open Weather web API and show the weather information in the UI. 16 | 17 | ### Open Weather API 18 | We will use [Open Weather Map API](https://openweathermap.org/api) for collecting weather information. To get the real weather information of a city, you need to sign up and get your own `APP ID`. Otherwise you can test the API with their sample `BASE URL` and sample `APP ID` without creating account. 19 | 20 | ### Project Setup 21 | Clone the project and open it using Android Studio. Then open your `local.properties` file under `Gradle Scripts`. You need to specify the `base_url` and `app_id` in your `local.properties` file. Store your API secret in plain string file or Kotlin file is very risky. For security purpose it's better store in local.properties file than plain string/Kotlin file. 22 | 23 | #### Use Sample API without creating account 24 | Add below lines at the end of your `local.properties` file. Then run the project. You'll get dummy or static API response from Open Weather API. 25 | ```properties 26 | #this is sample Base URL 27 | base_url=https://samples.openweathermap.org/data/2.5/ 28 | 29 | #this is sample App ID of Open Weather API 30 | app_id=b6907d289e10d714a6e88b30761fae22 31 | ``` 32 | #### Use Real APP ID after sign up and activation of your APP ID 33 | After Sign up at the website collect your own `APP ID` from their [API Keys page](https://home.openweathermap.org/api_keys). Then add below lines with your APP ID at the end of `local.properties` file. 34 | ```properties 35 | #this is real Base URL 36 | base_url=http://api.openweathermap.org/data/2.5/ 37 | 38 | #this is real App ID of Open Weather API 39 | app_id=YOUR_OWN_APP_ID 40 | ``` 41 | The BASE URL and APP ID will be fetched from `build.gradle` file and will be stored it in `BuildConfig`. And `Retrofit` API call will use the BASE URL and APP ID from `BuildConfig`. 42 | 43 | **Note:** The free version of Open Weather API allows maximum 60 API calls per minute. 44 | ### Run the project 45 | Sync the `Gradle` and run the project. Install APK on your emulator or real device. Turn on the internet of your testing device. For better understanding, please read the comments of every methods. Hope, these comments will help you to feel the `MVVM Architecture`. 46 | ### Disclaimer 47 | There are some other ways of implementation of `MVVM`. For simplicity, I've ignored `RxJava` in this project. I will create another repository of `MVVM` with `Rx`. 48 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: "org.sonarqube" 5 | 6 | android { 7 | compileSdkVersion 31 8 | defaultConfig { 9 | applicationId "com.hellohasan.weatherappmvvmdagger" 10 | minSdkVersion 19 11 | targetSdkVersion 31 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | 16 | buildConfigField "String", "BASE_URL", "\"" + getBaseUrl() + "\"" 17 | buildConfigField "String", "APP_ID", "\"" + getAppId() + "\"" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | viewBinding.enabled = true 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 36 | implementation 'androidx.appcompat:appcompat:1.4.1' 37 | implementation 'androidx.core:core-ktx:1.7.0' 38 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 39 | 40 | //Test 41 | testImplementation 'junit:junit:4.13.2' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 44 | 45 | // ViewModel and LiveData 46 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 47 | 48 | // material design 49 | implementation 'com.google.android.material:material:1.6.0-beta01' 50 | 51 | // dagger2 52 | def daggerVersion = "2.41" 53 | api "com.google.dagger:dagger:$daggerVersion" 54 | api "com.google.dagger:dagger-android:$daggerVersion" 55 | api "com.google.dagger:dagger-android-support:$daggerVersion" // if you use the support libraries 56 | kapt "com.google.dagger:dagger-android-processor:$daggerVersion" 57 | kapt "com.google.dagger:dagger-compiler:$daggerVersion" 58 | 59 | // network call related libraries 60 | def retrofitVersion = "2.9.0" 61 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" // REST API calling library 62 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" // JSON parsing library 63 | implementation('com.github.ihsanbal:LoggingInterceptor:3.0.0') { // HTTP pretty log printing library 64 | exclude group: 'org.json', module: 'json' 65 | } 66 | 67 | // glide image loading library 68 | def glideVersion = "4.12.0" 69 | implementation "com.github.bumptech.glide:glide:$glideVersion" 70 | annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" 71 | } 72 | 73 | def getBaseUrl() { 74 | Properties properties = new Properties() 75 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 76 | 77 | String baseUrl = properties.getProperty("base_url") 78 | if (baseUrl == null) 79 | throw new GradleException("Add 'base_url' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvvm-dagger/blob/master/README.md") 80 | 81 | return baseUrl 82 | } 83 | 84 | def getAppId() { 85 | Properties properties = new Properties() 86 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 87 | 88 | String appId = properties.getProperty("app_id") 89 | if (appId == null) 90 | throw new GradleException("Add 'app_id' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvvm-dagger/blob/master/README.md") 91 | 92 | return appId 93 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hellohasan/weatherappmvvmdagger/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.hellohasan.weatherappmvvmdagger", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/assets/city_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1185241, 4 | "name": "Dhaka", 5 | "country": "BD" 6 | }, 7 | { 8 | "id": 1336135, 9 | "name": "Khulna", 10 | "country": "BD" 11 | }, 12 | { 13 | "id": 1337200, 14 | "name": "Chittagong", 15 | "country": "BD" 16 | }, 17 | { 18 | "id": 1336134, 19 | "name": "Coxs Bazar", 20 | "country": "BD" 21 | }, 22 | { 23 | "id": 1185128, 24 | "name": "Rajshahi", 25 | "country": "BD" 26 | }, 27 | { 28 | "id": 1336137, 29 | "name": "Barisal", 30 | "country": "BD" 31 | }, 32 | { 33 | "id": 1185099, 34 | "name": "Sylhet", 35 | "country": "BD" 36 | }, 37 | { 38 | "id": 1185188, 39 | "name": "Rangpur", 40 | "country": "BD" 41 | }, 42 | { 43 | "id": 5056033, 44 | "name": "London", 45 | "country": "US" 46 | }, 47 | { 48 | "id": 1275004, 49 | "name": "Kolkata", 50 | "country": "IN" 51 | }, 52 | { 53 | "id": 108410, 54 | "name": "Riyadh", 55 | "country": "SA" 56 | }, 57 | { 58 | "id": 292968, 59 | "name": "Abu Dhabi", 60 | "country": "AE" 61 | }, 62 | { 63 | "id": 5128638, 64 | "name": "New York", 65 | "country": "US" 66 | }, 67 | { 68 | "id": 1850147, 69 | "name": "Tokyo", 70 | "country": "JP" 71 | }, 72 | { 73 | "id": 1176615, 74 | "name": "Islamabad", 75 | "country": "PK" 76 | }, 77 | { 78 | "id": 1261481, 79 | "name": "New Delhi", 80 | "country": "IN" 81 | } 82 | ] -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/common/App.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.common 2 | 3 | import com.hellohasan.weatherappmvvmdagger.di.DaggerApplicationComponent 4 | import dagger.android.AndroidInjector 5 | import dagger.android.DaggerApplication 6 | 7 | class App : DaggerApplication(){ 8 | 9 | override fun applicationInjector(): AndroidInjector { 10 | return DaggerApplicationComponent 11 | .builder() 12 | .application(this) 13 | .build() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/common/RequestCompleteListener.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.common 2 | 3 | interface RequestCompleteListener { 4 | fun onRequestSuccess(data: T) 5 | fun onRequestFailed(errorMessage: String) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | 6 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.view.MainActivity 7 | import com.hellohasan.weatherappmvvmdagger.network.ApiInterface 8 | import com.hellohasan.weatherappmvvmdagger.network.RetrofitClient 9 | 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.android.ContributesAndroidInjector 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | abstract class AppModule { 18 | 19 | @Binds 20 | abstract fun provideContext(application: Application): Context 21 | 22 | @ContributesAndroidInjector(modules = [MainActivityModule::class]) 23 | abstract fun mainActivityInjector(): MainActivity 24 | 25 | companion object { 26 | 27 | @Provides 28 | @Singleton 29 | @JvmStatic 30 | fun provideApiService() : ApiInterface { 31 | return RetrofitClient.client.create(ApiInterface::class.java) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/di/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.di 2 | 3 | import android.app.Application 4 | import com.hellohasan.weatherappmvvmdagger.common.App 5 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel.WeatherInfoViewModel 6 | 7 | import dagger.BindsInstance 8 | import dagger.Component 9 | 10 | import dagger.android.AndroidInjector 11 | import dagger.android.support.AndroidSupportInjectionModule 12 | import javax.inject.Singleton 13 | 14 | @Singleton 15 | @Component( 16 | modules = [ 17 | AndroidSupportInjectionModule::class, 18 | AppModule::class] 19 | ) 20 | interface ApplicationComponent : AndroidInjector { 21 | 22 | fun inject(weatherInfoViewModel: WeatherInfoViewModel) 23 | 24 | @Component.Builder 25 | interface Builder { 26 | 27 | @BindsInstance 28 | fun application(application: Application): Builder 29 | 30 | fun build(): ApplicationComponent 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/di/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.di 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.WeatherInfoShowModel 6 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.WeatherInfoShowModelImpl 7 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel.WeatherInfoViewModel 8 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel.WeatherInfoViewModelFactory 9 | 10 | import dagger.Binds 11 | import dagger.Module 12 | 13 | @Module 14 | abstract class MainActivityModule { 15 | 16 | @Binds 17 | abstract fun bindMainViewModel(viewModel: WeatherInfoViewModel): ViewModel 18 | 19 | @Binds 20 | abstract fun bindModel(weatherInfoShowModelImpl: WeatherInfoShowModelImpl): WeatherInfoShowModel 21 | 22 | @Binds 23 | abstract fun bindWeatherInfoViewModelFactory(factory: WeatherInfoViewModelFactory): ViewModelProvider.Factory 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/WeatherInfoShowModel.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model 2 | 3 | import com.hellohasan.weatherappmvvmdagger.common.RequestCompleteListener 4 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.City 5 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherInfoResponse 6 | 7 | interface WeatherInfoShowModel { 8 | fun getCityList(callback: RequestCompleteListener>) 9 | fun getWeatherInfo(cityId: Int, callback: RequestCompleteListener) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/WeatherInfoShowModelImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model 2 | 3 | import android.content.Context 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.reflect.TypeToken 6 | 7 | import com.hellohasan.weatherappmvvmdagger.common.RequestCompleteListener 8 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.City 9 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherInfoResponse 10 | import com.hellohasan.weatherappmvvmdagger.network.ApiInterface 11 | 12 | import retrofit2.Call 13 | import retrofit2.Callback 14 | import retrofit2.Response 15 | import java.io.IOException 16 | import javax.inject.Inject 17 | 18 | class WeatherInfoShowModelImpl @Inject constructor( 19 | private val context: Context, 20 | private val apiInterface: ApiInterface 21 | ) : WeatherInfoShowModel { 22 | 23 | override fun getCityList(callback: RequestCompleteListener>) { 24 | try { 25 | val stream = context.assets.open("city_list.json") 26 | 27 | val size = stream.available() 28 | val buffer = ByteArray(size) 29 | stream.read(buffer) 30 | stream.close() 31 | val tContents = String(buffer) 32 | 33 | val groupListType = object : TypeToken>() {}.type 34 | val gson = GsonBuilder().create() 35 | val cityList: MutableList = gson.fromJson(tContents, groupListType) 36 | 37 | callback.onRequestSuccess(cityList) //let presenter know the city list 38 | 39 | } catch (e: IOException) { 40 | e.printStackTrace() 41 | callback.onRequestFailed(requireNotNull(e.localizedMessage)) //let presenter know about failure 42 | } 43 | } 44 | 45 | override fun getWeatherInfo( 46 | cityId: Int, 47 | callback: RequestCompleteListener 48 | ) { 49 | 50 | val call: Call = apiInterface.callApiForWeatherInfo(cityId) 51 | 52 | call.enqueue(object : Callback { 53 | 54 | // if retrofit network call success, this method will be triggered 55 | override fun onResponse( 56 | call: Call, 57 | response: Response 58 | ) { 59 | if (response.body() != null) 60 | callback.onRequestSuccess(requireNotNull(response.body())) //let presenter know the weather information data 61 | else 62 | callback.onRequestFailed(response.message()) //let presenter know about failure 63 | } 64 | 65 | // this method will be triggered if network call failed 66 | override fun onFailure(call: Call, t: Throwable) { 67 | callback.onRequestFailed(requireNotNull(t.localizedMessage)) //let presenter know about failure 68 | } 69 | }) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/City.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.io.Serializable 5 | 6 | data class City( 7 | @SerializedName("id") 8 | val id: Int = 0, 9 | @SerializedName("name") 10 | val name: String = "", 11 | @SerializedName("country") 12 | val country: String = "" 13 | ): Serializable -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Clouds.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Clouds( 7 | @SerializedName("all") 8 | val all: Int = 0 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Coord.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Coord( 7 | @SerializedName("lon") 8 | val lon: Double = 0.0, 9 | @SerializedName("lat") 10 | val lat: Double = 0.0 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Main.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Main( 7 | @SerializedName("temp") 8 | val temp: Double = 0.0, 9 | @SerializedName("pressure") 10 | val pressure: Double = 0.0, 11 | @SerializedName("humidity") 12 | val humidity: Int = 0, 13 | @SerializedName("temp_min") 14 | val tempMin: Double = 0.0, 15 | @SerializedName("temp_max") 16 | val tempMax: Double = 0.0 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Sys.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Sys( 7 | @SerializedName("type") 8 | val type: Int = 0, 9 | @SerializedName("id") 10 | val id: Int = 0, 11 | @SerializedName("message") 12 | val message: Double = 0.0, 13 | @SerializedName("country") 14 | val country: String = "", 15 | @SerializedName("sunrise") 16 | val sunrise: Int = 0, 17 | @SerializedName("sunset") 18 | val sunset: Int = 0 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Weather.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Weather( 7 | @SerializedName("id") 8 | val id: Int = 0, 9 | @SerializedName("main") 10 | val main: String = "", 11 | @SerializedName("description") 12 | val description: String = "", 13 | @SerializedName("icon") 14 | val icon: String = "" 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/WeatherData.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | /** 4 | * this class will be used in UI to show weather data 5 | */ 6 | data class WeatherData( 7 | var dateTime: String = "", 8 | var temperature: String = "0", 9 | var cityAndCountry: String = "", 10 | var weatherConditionIconUrl: String = "", 11 | var weatherConditionIconDescription: String = "", 12 | var humidity: String = "", 13 | var pressure: String = "", 14 | var visibility: String = "", 15 | var sunrise: String = "", 16 | var sunset: String = "" 17 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/WeatherInfoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WeatherInfoResponse( 6 | @SerializedName("coord") 7 | val coord: Coord = Coord(), 8 | @SerializedName("weather") 9 | val weather: List = listOf(), 10 | @SerializedName("base") 11 | val base: String = "", 12 | @SerializedName("main") 13 | val main: Main = Main(), 14 | @SerializedName("visibility") 15 | val visibility: Int = 0, 16 | @SerializedName("wind") 17 | val wind: Wind = Wind(), 18 | @SerializedName("clouds") 19 | val clouds: Clouds = Clouds(), 20 | @SerializedName("dt") 21 | val dt: Int = 0, 22 | @SerializedName("sys") 23 | val sys: Sys = Sys(), 24 | @SerializedName("id") 25 | val id: Int = 0, 26 | @SerializedName("name") 27 | val name: String = "", 28 | @SerializedName("cod") 29 | val cod: Int = 0 30 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/model/data_class/Wind.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Wind( 7 | @SerializedName("speed") 8 | val speed: Double = 0.0, 9 | @SerializedName("deg") 10 | val deg: Double = 0.0 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.view 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.ArrayAdapter 6 | import android.widget.Toast 7 | import androidx.lifecycle.Observer 8 | import androidx.lifecycle.ViewModelProvider 9 | import com.bumptech.glide.Glide 10 | import com.hellohasan.weatherappmvvmdagger.R 11 | import com.hellohasan.weatherappmvvmdagger.databinding.ActivityMainBinding 12 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.City 13 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherData 14 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel.WeatherInfoViewModel 15 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel.WeatherInfoViewModelFactory 16 | import com.hellohasan.weatherappmvvmdagger.utils.convertToListOfCityName 17 | import dagger.android.support.DaggerAppCompatActivity 18 | import javax.inject.Inject 19 | 20 | class MainActivity : DaggerAppCompatActivity() { 21 | 22 | @Inject 23 | lateinit var factory: WeatherInfoViewModelFactory 24 | 25 | private lateinit var viewModel: WeatherInfoViewModel 26 | 27 | private var cityList: MutableList = mutableListOf() 28 | 29 | private lateinit var binding: ActivityMainBinding 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | binding = ActivityMainBinding.inflate(layoutInflater) 34 | setContentView(binding.root) 35 | 36 | // initialize ViewModel 37 | viewModel = ViewModelProvider(this, factory).get(WeatherInfoViewModel::class.java) 38 | 39 | // set LiveData and View click listeners before the call for data fetching 40 | setLiveDataListeners() 41 | setViewClickListener() 42 | 43 | /** 44 | * Fetch city list when Activity open. 45 | * It's not a very good way that, passing model in every methods of ViewModel. For the sake 46 | * of simplicity I did so. In real production level App, we can inject out model to ViewModel 47 | * as a parameter by any dependency injection library like Dagger. 48 | */ 49 | viewModel.getCityList() 50 | } 51 | 52 | private fun setViewClickListener() { 53 | // View Weather button click listener 54 | binding.layoutInput.apply { 55 | btnViewWeather.setOnClickListener { 56 | val selectedCityId = cityList[spinner.selectedItemPosition].id 57 | viewModel.getWeatherInfo(selectedCityId) // fetch weather info 58 | } 59 | } 60 | } 61 | 62 | private fun setLiveDataListeners() { 63 | 64 | /** 65 | * When ViewModel PUSH city list to LiveData then this `onChanged()`‍ method will be called. 66 | * Here we subscribe the LiveData of City list. We don't pull city list from ViewModel. 67 | * We subscribe to the data source for city list. When LiveData of city list is updated 68 | * inside ViewModel, below onChanged() method will triggered instantly. 69 | * City list is fetching from a small local JSON file. So we don't need any ProgressBar here. 70 | * 71 | * For better understanding, I didn't use lambda in this method call. Rather thant lambda I 72 | * implement `Observer` interface in general format. Hope you will understand the inline 73 | * implementation of `Observer` interface. Rest of the `observe()` method, I've used lambda 74 | * to short the code. 75 | */ 76 | viewModel.cityListLiveData.observe(this, object : Observer> { 77 | override fun onChanged(cities: MutableList) { 78 | setCityListSpinner(cities) 79 | } 80 | }) 81 | 82 | /** 83 | * If ViewModel failed to fetch City list from data source, this LiveData will be triggered. 84 | * I know it's not good to make separate LiveData both for Success and Failure, but for sake 85 | * of simplification I did it. We can handle all of our errors from our Activity or Fragment 86 | * Base classes. Another way is: using a Generic wrapper class where you can set the success 87 | * or failure status for any types of data model. 88 | * 89 | * Here I've used lambda expression to implement Observer interface in second parameter. 90 | */ 91 | viewModel.cityListFailureLiveData.observe(this, Observer { errorMessage -> 92 | Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show() 93 | }) 94 | 95 | /** 96 | * ProgressBar visibility will be handled by this LiveData. ViewModel decides when Activity 97 | * should show ProgressBar and when hide. 98 | * 99 | * Here I've used lambda expression to implement Observer interface in second parameter. 100 | */ 101 | viewModel.progressBarLiveData.observe(this, Observer { isShowLoader -> 102 | if (isShowLoader) 103 | binding.progressBar.visibility = View.VISIBLE 104 | else 105 | binding.progressBar.visibility = View.GONE 106 | }) 107 | 108 | /** 109 | * This method will be triggered when ViewModel successfully receive WeatherData from our 110 | * data source (I mean Model). Activity just observing (subscribing) this LiveData for showing 111 | * weather information on UI. ViewModel receives Weather data API response from Model via 112 | * Callback method of Model. Then ViewModel apply some business logic and manipulate data. 113 | * Finally ViewModel PUSH WeatherData to `weatherInfoLiveData`. After PUSHING into it, below 114 | * method triggered instantly! Then we set the data on UI. 115 | * 116 | * Here I've used lambda expression to implement Observer interface in second parameter. 117 | */ 118 | viewModel.weatherInfoLiveData.observe(this, Observer { weatherData -> 119 | setWeatherInfo(weatherData) 120 | }) 121 | 122 | /** 123 | * If ViewModel faces any error during Weather Info fetching API call by Model, then PUSH the 124 | * error message into `weatherInfoFailureLiveData`. After that, this method will be triggered. 125 | * Then we will hide the output view and show error message on UI. 126 | * 127 | * Here I've used lambda expression to implement Observer interface in second parameter. 128 | */ 129 | viewModel.weatherInfoFailureLiveData.observe(this, Observer { errorMessage -> 130 | binding.apply { 131 | outputGroup.visibility = View.GONE 132 | tvErrorMessage.visibility = View.VISIBLE 133 | tvErrorMessage.text = errorMessage 134 | } 135 | }) 136 | } 137 | 138 | private fun setCityListSpinner(cityList: MutableList) { 139 | this.cityList = cityList 140 | 141 | val arrayAdapter = ArrayAdapter( 142 | this, 143 | R.layout.support_simple_spinner_dropdown_item, 144 | this.cityList.convertToListOfCityName() 145 | ) 146 | 147 | binding.layoutInput.spinner.adapter = arrayAdapter 148 | } 149 | 150 | private fun setWeatherInfo(weatherData: WeatherData) { 151 | binding.apply { 152 | outputGroup.visibility = View.VISIBLE 153 | tvErrorMessage.visibility = View.GONE 154 | 155 | layoutWeatherBasic.apply { 156 | tvDateTime.text = weatherData.dateTime 157 | tvTemperature.text = weatherData.temperature 158 | tvCityCountry.text = weatherData.cityAndCountry 159 | Glide.with(this@MainActivity).load(weatherData.weatherConditionIconUrl).into(ivWeatherCondition) 160 | tvWeatherCondition.text = weatherData.weatherConditionIconDescription 161 | } 162 | 163 | layoutWeatherAdditional.apply { 164 | tvHumidityValue.text = weatherData.humidity 165 | tvPressureValue.text = weatherData.pressure 166 | tvVisibilityValue.text = weatherData.visibility 167 | } 168 | 169 | layoutSunsetSunrise.apply { 170 | tvSunriseTime.text = weatherData.sunrise 171 | tvSunsetTime.text = weatherData.sunset 172 | } 173 | 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/viewmodel/WeatherInfoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.hellohasan.weatherappmvvmdagger.common.RequestCompleteListener 6 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.WeatherInfoShowModel 7 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.City 8 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherData 9 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherInfoResponse 10 | import com.hellohasan.weatherappmvvmdagger.utils.kelvinToCelsius 11 | import com.hellohasan.weatherappmvvmdagger.utils.unixTimestampToDateTimeString 12 | import com.hellohasan.weatherappmvvmdagger.utils.unixTimestampToTimeString 13 | import javax.inject.Inject 14 | 15 | class WeatherInfoViewModel @Inject constructor(var model: WeatherInfoShowModel) : ViewModel() { 16 | 17 | /** 18 | * In our project, for sake for simplicity we used different LiveData for success and failure. 19 | * But it's not the only way. We can use a wrapper data class to implement success and failure 20 | * both using a single LiveData. Another good approach may be handle errors in BaseActivity. 21 | * For this project our objective is only understand about MVVM. So we made it easy to understand. 22 | */ 23 | val cityListLiveData = MutableLiveData>() 24 | val cityListFailureLiveData = MutableLiveData() 25 | val weatherInfoLiveData = MutableLiveData() 26 | val weatherInfoFailureLiveData = MutableLiveData() 27 | val progressBarLiveData = MutableLiveData() 28 | 29 | /**We can inject the instance of Model in Constructor using dependency injection. 30 | * For sake of simplicity, I am ignoring it now. So we have to pass instance of model in every 31 | * methods of ViewModel. Please be noted, it's not a good approach. 32 | */ 33 | fun getCityList() { 34 | 35 | model.getCityList(object : 36 | RequestCompleteListener> { 37 | override fun onRequestSuccess(data: MutableList) { 38 | cityListLiveData.postValue(data) // PUSH data to LiveData object 39 | } 40 | 41 | override fun onRequestFailed(errorMessage: String) { 42 | cityListFailureLiveData.postValue(errorMessage) // PUSH error message to LiveData object 43 | } 44 | }) 45 | } 46 | 47 | /**We can inject the instance of Model in Constructor using dependency injection. 48 | * For sake of simplicity, I am ignoring it now. So we have to pass instance of model in every 49 | * methods of ViewModel. Pleas be noted, it's not a good approach. 50 | */ 51 | fun getWeatherInfo(cityId: Int) { 52 | 53 | progressBarLiveData.postValue(true) // PUSH data to LiveData object to show progress bar 54 | 55 | model.getWeatherInfo(cityId, object : 56 | RequestCompleteListener { 57 | override fun onRequestSuccess(data: WeatherInfoResponse) { 58 | 59 | // business logic and data manipulation tasks should be done here 60 | val weatherData = WeatherData( 61 | dateTime = data.dt.unixTimestampToDateTimeString(), 62 | temperature = data.main.temp.kelvinToCelsius().toString(), 63 | cityAndCountry = "${data.name}, ${data.sys.country}", 64 | weatherConditionIconUrl = "http://openweathermap.org/img/w/${data.weather[0].icon}.png", 65 | weatherConditionIconDescription = data.weather[0].description, 66 | humidity = "${data.main.humidity}%", 67 | pressure = "${data.main.pressure} mBar", 68 | visibility = "${data.visibility/1000.0} KM", 69 | sunrise = data.sys.sunrise.unixTimestampToTimeString(), 70 | sunset = data.sys.sunset.unixTimestampToTimeString() 71 | ) 72 | 73 | progressBarLiveData.postValue(false) // PUSH data to LiveData object to hide progress bar 74 | 75 | // After applying business logic and data manipulation, we push data to show on UI 76 | weatherInfoLiveData.postValue(weatherData) // PUSH data to LiveData object 77 | } 78 | 79 | override fun onRequestFailed(errorMessage: String) { 80 | progressBarLiveData.postValue(false) // hide progress bar 81 | weatherInfoFailureLiveData.postValue(errorMessage) // PUSH error message to LiveData object 82 | } 83 | }) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/features/weather_info_show/viewmodel/WeatherInfoViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.features.weather_info_show.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.WeatherInfoShowModel 6 | import javax.inject.Inject 7 | 8 | class WeatherInfoViewModelFactory @Inject constructor(private val arg: WeatherInfoShowModel) : 9 | ViewModelProvider.Factory { 10 | 11 | override fun create(modelClass: Class): T { 12 | return modelClass.getConstructor(WeatherInfoShowModel::class.java) 13 | .newInstance(arg) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/network/ApiInterface.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.network 2 | 3 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.WeatherInfoResponse 4 | import retrofit2.Call 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface ApiInterface { 9 | @GET("weather") 10 | fun callApiForWeatherInfo(@Query("id") cityId: Int): Call 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/network/QueryParameterAddInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.network 2 | 3 | import com.hellohasan.weatherappmvvmdagger.BuildConfig 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | 7 | class QueryParameterAddInterceptor : Interceptor { 8 | 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | 11 | val url = chain.request().url().newBuilder() 12 | .addQueryParameter("appid", BuildConfig.APP_ID) 13 | .build() 14 | 15 | val request = chain.request().newBuilder() 16 | // .addHeader("Authorization", "Bearer token") 17 | .url(url) 18 | .build() 19 | 20 | return chain.proceed(request) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/network/RetrofitClient.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.network 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.hellohasan.weatherappmvvmdagger.BuildConfig 5 | import com.ihsanbal.logging.Level 6 | import com.ihsanbal.logging.LoggingInterceptor 7 | import okhttp3.OkHttpClient 8 | import okhttp3.internal.platform.Platform 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import java.util.concurrent.Executors 12 | 13 | object RetrofitClient { 14 | 15 | private var retrofit: Retrofit? = null 16 | private val gson = GsonBuilder().setLenient().create() 17 | 18 | val client: Retrofit 19 | get() { 20 | if (retrofit == null) { 21 | synchronized(Retrofit::class.java) { 22 | if (retrofit == null) { 23 | 24 | val httpClient = OkHttpClient.Builder() 25 | .addInterceptor(QueryParameterAddInterceptor()) 26 | 27 | // for pretty log of HTTP request-response 28 | httpClient.addInterceptor( 29 | LoggingInterceptor.Builder() 30 | .loggable(BuildConfig.DEBUG) 31 | .setLevel(Level.BASIC) 32 | .log(Platform.INFO) 33 | .request("LOG") 34 | .response("LOG") 35 | .executor(Executors.newSingleThreadExecutor()) 36 | .build()) 37 | 38 | val client = httpClient.build() 39 | 40 | retrofit = Retrofit.Builder() 41 | .baseUrl(BuildConfig.BASE_URL) 42 | .addConverterFactory(GsonConverterFactory.create(gson)) 43 | .client(client) 44 | .build() 45 | } 46 | } 47 | 48 | } 49 | return retrofit!! 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherappmvvmdagger/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherappmvvmdagger.utils 2 | 3 | import com.hellohasan.weatherappmvvmdagger.features.weather_info_show.model.data_class.City 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | 7 | fun Int.unixTimestampToDateTimeString() : String { 8 | 9 | try { 10 | val calendar = Calendar.getInstance() 11 | calendar.timeInMillis = this*1000.toLong() 12 | 13 | val outputDateFormat = SimpleDateFormat("dd MMM, yyyy - hh:mm a", Locale.ENGLISH) 14 | outputDateFormat.timeZone = TimeZone.getDefault() // user's default time zone 15 | return outputDateFormat.format(calendar.time) 16 | 17 | } catch (e: Exception) { 18 | e.printStackTrace() 19 | } 20 | 21 | return this.toString() 22 | } 23 | 24 | fun Int.unixTimestampToTimeString() : String { 25 | 26 | try { 27 | val calendar = Calendar.getInstance() 28 | calendar.timeInMillis = this*1000.toLong() 29 | 30 | val outputDateFormat = SimpleDateFormat("hh:mm a", Locale.ENGLISH) 31 | outputDateFormat.timeZone = TimeZone.getDefault() 32 | return outputDateFormat.format(calendar.time) 33 | 34 | } catch (e: Exception) { 35 | e.printStackTrace() 36 | } 37 | 38 | return this.toString() 39 | } 40 | 41 | fun MutableList.convertToListOfCityName() : MutableList { 42 | 43 | val cityNameList: MutableList = mutableListOf() 44 | 45 | for (city in this) { 46 | cityNameList.add(city.name) 47 | } 48 | 49 | return cityNameList 50 | } 51 | 52 | /** 53 | * The temperature T in degrees Celsius (°C) is equal to the temperature T in Kelvin (K) minus 273.15: 54 | * T(°C) = T(K) - 273.15 55 | * 56 | * Example 57 | * Convert 300 Kelvin to degrees Celsius: 58 | * T(°C) = 300K - 273.15 = 26.85 °C 59 | */ 60 | fun Double.kelvinToCelsius() : Int { 61 | 62 | return (this - 273.15).toInt() 63 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/haze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasancse91/weather-app-android-mvvm-dagger/e551fe53d832065df1ae508760d950b9d17b336e/app/src/main/res/drawable-v24/haze.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_sunrise.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_sunset.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 21 | 22 | 29 | 30 | 36 | 37 | 49 | 50 | 60 | 61 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_input_part.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 |