├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hellohasan │ │ └── weatherforecast │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── city_list.json │ ├── java │ │ └── com │ │ │ └── hellohasan │ │ │ └── weatherforecast │ │ │ ├── common │ │ │ └── RequestCompleteListener.kt │ │ │ ├── features │ │ │ └── weather_info_show │ │ │ │ ├── model │ │ │ │ ├── WeatherInfoShowModel.kt │ │ │ │ ├── WeatherInfoShowModelImpl.kt │ │ │ │ └── data_class │ │ │ │ │ ├── City.kt │ │ │ │ │ ├── Clouds.kt │ │ │ │ │ ├── Coord.kt │ │ │ │ │ ├── Main.kt │ │ │ │ │ ├── Sys.kt │ │ │ │ │ ├── Weather.kt │ │ │ │ │ ├── WeatherDataModel.kt │ │ │ │ │ ├── WeatherInfoResponse.kt │ │ │ │ │ └── Wind.kt │ │ │ │ ├── presenter │ │ │ │ ├── WeatherInfoShowPresenter.kt │ │ │ │ └── WeatherInfoShowPresenterImpl.kt │ │ │ │ └── view │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainActivityView.kt │ │ │ ├── network │ │ │ ├── ApiInterface.kt │ │ │ ├── QueryParameterAddInterceptor.kt │ │ │ └── RetrofitClient.kt │ │ │ └── utils │ │ │ └── Extensions.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── haze.png │ │ ├── ic_launcher_background.xml │ │ ├── ic_sunrise.xml │ │ └── ic_sunset.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 │ └── weatherforecast │ └── ExampleUnitTest.java ├── build.gradle ├── data └── screenshot_1.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android MVP Architecture Sample (Kotlin + Retrofit) - Weather App 2 | 3 | MVP Architecture is one of the most popular architecture to develop a maintanable and managable codebase. We are developing a sample `Weater Forecast` Android App with `MVP Architecture` using `Kotlin` language and `Retrofit` network calling library. 4 | 5 | For simplicity, in this project I don't use any dependency injection framework. With the same project concept there is [another repository](https://github.com/hasancse91/weather-app-android-mvp-dagger) where I implemented `Dagger 2 dependency injection library`. You can check that repository after this one. 6 | 7 | **There is another repository for the same project in `MVVM Architecture.` Check MVVM repository [from here](https://github.com/hasancse91/weather-app-android-mvvm).** 8 | 9 | 10 | 11 | ### Project Description 12 | 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. 13 | 14 | ### Open Weather API 15 | 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. 16 | 17 | ### Project Setup 18 | 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. 19 | 20 | #### Use Sample API without creating account 21 | 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. 22 | ```properties 23 | #this is sample Base URL 24 | base_url=https://samples.openweathermap.org/data/2.5/ 25 | 26 | #this is sample App ID of Open Weather API 27 | app_id=b6907d289e10d714a6e88b30761fae22 28 | ``` 29 | #### Use Real APP ID after sign up and activation of your APP ID 30 | 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. 31 | ```properties 32 | #this is real Base URL 33 | base_url=http://api.openweathermap.org/data/2.5/ 34 | 35 | #this is real App ID of Open Weather API 36 | app_id=YOUR_OWN_APP_ID 37 | ``` 38 | 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`. 39 | 40 | **Note:** The free version of Open Weather API allows maximum 60 API calls per minute. 41 | ### Run the project 42 | 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 `MVP Architecture`. 43 | ### Disclaimer 44 | There are some other ways of implementation of `MVP`. There are some arguments about the responsibilities of `presenter` and `model`. After understanding the basic part of `MVP Architecture` you can test them one by one. For simplicity, I've ignored `dependency injection` in this project. I have created another repository of `MVP` with dependency injection `Dagger 2`. You can check `Android Weather App MVP Architecture with Dagger 2` repository [from here](https://github.com/hasancse91/weather-app-android-mvp-dagger). 45 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 33 7 | defaultConfig { 8 | applicationId "com.hellohasan.weatherforecast" 9 | minSdkVersion 21 10 | targetSdkVersion 33 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | 15 | buildConfigField "String", "BASE_URL", "\"" + getBaseUrl() + "\"" 16 | buildConfigField "String", "APP_ID", "\"" + getAppId() + "\"" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation 'androidx.appcompat:appcompat:1.6.0' 34 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 35 | testImplementation 'junit:junit:4.13.2' 36 | androidTestImplementation 'androidx.test:runner:1.5.2' 37 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 38 | implementation "androidx.core:core-ktx:1.9.0" 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 40 | 41 | // material design 42 | implementation 'com.google.android.material:material:1.6.0' 43 | 44 | // network call related libraries 45 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' // REST API calling library 46 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // JSON parsing library 47 | implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') { // HTTP pretty log printing library 48 | exclude group: 'org.json', module: 'json' 49 | } 50 | 51 | // glide image loading library 52 | implementation 'com.github.bumptech.glide:glide:4.13.2' 53 | annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' 54 | } 55 | 56 | def getBaseUrl() { 57 | Properties properties = new Properties() 58 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 59 | 60 | String baseUrl = properties.getProperty("base_url") 61 | if(baseUrl==null) 62 | throw new GradleException("Add 'base_url' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvp-architecture/blob/master/README.md") 63 | 64 | return baseUrl 65 | } 66 | 67 | def getAppId() { 68 | Properties properties = new Properties() 69 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 70 | 71 | String appId = properties.getProperty("app_id") 72 | if(appId==null) 73 | throw new GradleException("Add 'app_id' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvp-architecture/blob/master/README.md") 74 | 75 | return appId 76 | } -------------------------------------------------------------------------------- /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/weatherforecast/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.hellohasan.weatherforecast", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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/weatherforecast/common/RequestCompleteListener.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.common 2 | 3 | interface RequestCompleteListener { 4 | fun onRequestSuccess(data: T) 5 | fun onRequestFailed(errorMessage: String) 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/model/WeatherInfoShowModel.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.model 2 | 3 | import com.hellohasan.weatherforecast.common.RequestCompleteListener 4 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.City 5 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherInfoResponse 6 | 7 | interface WeatherInfoShowModel { 8 | fun getCityList(callback: RequestCompleteListener>) 9 | fun getWeatherInformation(cityId: Int, callback: RequestCompleteListener) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/model/WeatherInfoShowModelImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.model 2 | 3 | import android.content.Context 4 | import com.google.gson.GsonBuilder 5 | import com.hellohasan.weatherforecast.common.RequestCompleteListener 6 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.City 7 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherInfoResponse 8 | import com.hellohasan.weatherforecast.network.ApiInterface 9 | import com.hellohasan.weatherforecast.network.RetrofitClient 10 | import retrofit2.Call 11 | import retrofit2.Callback 12 | import retrofit2.Response 13 | import java.io.IOException 14 | import com.google.gson.reflect.TypeToken 15 | 16 | class WeatherInfoShowModelImpl(private val context: Context) : WeatherInfoShowModel { 17 | 18 | /** 19 | * Fetch city list from local. Yes, model only knows about data source. It doesn't know anything 20 | * about data validation, formatting or something about view. 21 | */ 22 | override fun getCityList(callback: RequestCompleteListener>) { 23 | 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(e.localizedMessage!!) //let presenter know about failure 42 | } 43 | 44 | } 45 | 46 | /** 47 | * Fetch weather information from remote server via HTTP network request. 48 | * Yes, model only knows about data source. Model's responsibility is fetch the data from source. 49 | * Model don't do anything about data formatting or checking any logic. Model will notify 50 | * presenter with raw data. Presenter will decide the logic and let know the view what should 51 | * show on the UI. 52 | */ 53 | override fun getWeatherInformation(cityId: Int, callback: RequestCompleteListener) { 54 | 55 | val apiInterface: ApiInterface = RetrofitClient.client.create(ApiInterface::class.java) 56 | val call: Call = apiInterface.callApiForWeatherInfo(cityId) 57 | 58 | call.enqueue(object : Callback { 59 | 60 | // if retrofit network call success, this method will be triggered 61 | override fun onResponse(call: Call, response: Response) { 62 | if (response.body() != null) 63 | callback.onRequestSuccess(response.body()!!) //let presenter know the weather information data 64 | else 65 | callback.onRequestFailed(response.message()) //let presenter know about failure 66 | } 67 | 68 | // this method will be triggered if network call failed 69 | override fun onFailure(call: Call, t: Throwable) { 70 | callback.onRequestFailed(t.localizedMessage!!) //let presenter know about failure 71 | } 72 | 73 | }) 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/model/data_class/City.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Clouds.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Coord.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Main.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Sys.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Weather.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/WeatherDataModel.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.model.data_class 2 | 3 | data class WeatherDataModel( 4 | var dateTime: String = "", 5 | var temperature: String = "0", 6 | var cityAndCountry: String = "", 7 | var weatherConditionIconUrl: String = "", 8 | var weatherConditionIconDescription: String = "", 9 | var humidity: String = "", 10 | var pressure: String = "", 11 | var visibility: String = "", 12 | var sunrise: String = "", 13 | var sunset: String = "" 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/model/data_class/WeatherInfoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/model/data_class/Wind.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.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/weatherforecast/features/weather_info_show/presenter/WeatherInfoShowPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.presenter 2 | 3 | interface WeatherInfoShowPresenter { 4 | fun fetchCityList() 5 | fun fetchWeatherInfo(cityId: Int) 6 | fun detachView() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/presenter/WeatherInfoShowPresenterImpl.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.presenter 2 | 3 | import android.view.View 4 | import com.hellohasan.weatherforecast.features.weather_info_show.model.WeatherInfoShowModel 5 | import com.hellohasan.weatherforecast.common.RequestCompleteListener 6 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.City 7 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherDataModel 8 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherInfoResponse 9 | import com.hellohasan.weatherforecast.features.weather_info_show.view.MainActivityView 10 | import com.hellohasan.weatherforecast.utils.kelvinToCelsius 11 | import com.hellohasan.weatherforecast.utils.unixTimestampToDateTimeString 12 | import com.hellohasan.weatherforecast.utils.unixTimestampToTimeString 13 | 14 | class WeatherInfoShowPresenterImpl( 15 | private var view: MainActivityView?, 16 | private val model: WeatherInfoShowModel) : WeatherInfoShowPresenter { 17 | 18 | override fun fetchCityList() { 19 | // call model's method for city list 20 | model.getCityList(object : RequestCompleteListener> { 21 | 22 | // if model successfully fetch the data from 'somewhere', this method will be called 23 | override fun onRequestSuccess(data: MutableList) { 24 | view?.onCityListFetchSuccess(data) //let view know the formatted city list data 25 | } 26 | 27 | // if model failed to fetch data then this method will be called 28 | override fun onRequestFailed(errorMessage: String) { 29 | view?.onCityListFetchFailure(errorMessage) //let view know about failure 30 | } 31 | }) 32 | } 33 | 34 | override fun fetchWeatherInfo(cityId: Int) { 35 | 36 | view?.handleProgressBarVisibility(View.VISIBLE) // let view know about progress bar visibility 37 | 38 | // call model's method for weather information 39 | model.getWeatherInformation(cityId, object : RequestCompleteListener { 40 | 41 | // if model successfully fetch the data from 'somewhere', this method will be called 42 | override fun onRequestSuccess(data: WeatherInfoResponse) { 43 | 44 | view?.handleProgressBarVisibility(View.GONE) // let view know about progress bar visibility 45 | 46 | // data formatting to show on UI 47 | val weatherDataModel = WeatherDataModel( 48 | dateTime = data.dt.unixTimestampToDateTimeString(), 49 | temperature = data.main.temp.kelvinToCelsius().toString(), 50 | cityAndCountry = "${data.name}, ${data.sys.country}", 51 | weatherConditionIconUrl = "http://openweathermap.org/img/w/${data.weather[0].icon}.png", 52 | weatherConditionIconDescription = data.weather[0].description, 53 | humidity = "${data.main.humidity}%", 54 | pressure = "${data.main.pressure} mBar", 55 | visibility = "${data.visibility/1000.0} KM", 56 | sunrise = data.sys.sunrise.unixTimestampToTimeString(), 57 | sunset = data.sys.sunset.unixTimestampToTimeString() 58 | ) 59 | 60 | view?.onWeatherInfoFetchSuccess(weatherDataModel) //let view know the formatted weather data 61 | } 62 | 63 | // if model failed to fetch data then this method will be called 64 | override fun onRequestFailed(errorMessage: String) { 65 | view?.handleProgressBarVisibility(View.GONE) // let view know about progress bar visibility 66 | 67 | view?.onWeatherInfoFetchFailure(errorMessage) //let view know about failure 68 | } 69 | }) 70 | } 71 | 72 | override fun detachView() { 73 | view = null 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/view/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.view 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.ArrayAdapter 8 | import android.widget.Toast 9 | import com.bumptech.glide.Glide 10 | import com.hellohasan.weatherforecast.R 11 | import com.hellohasan.weatherforecast.utils.convertToListOfCityName 12 | import com.hellohasan.weatherforecast.features.weather_info_show.model.WeatherInfoShowModel 13 | import com.hellohasan.weatherforecast.features.weather_info_show.model.WeatherInfoShowModelImpl 14 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.City 15 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherDataModel 16 | import com.hellohasan.weatherforecast.features.weather_info_show.presenter.WeatherInfoShowPresenter 17 | import com.hellohasan.weatherforecast.features.weather_info_show.presenter.WeatherInfoShowPresenterImpl 18 | import kotlinx.android.synthetic.main.activity_main.* 19 | import kotlinx.android.synthetic.main.layout_input_part.* 20 | import kotlinx.android.synthetic.main.layout_sunrise_sunset.* 21 | import kotlinx.android.synthetic.main.layout_weather_additional_info.* 22 | import kotlinx.android.synthetic.main.layout_weather_basic_info.* 23 | 24 | class MainActivity : AppCompatActivity(), MainActivityView { 25 | 26 | private lateinit var model: WeatherInfoShowModel 27 | private lateinit var presenter: WeatherInfoShowPresenter 28 | 29 | private var cityList: MutableList = mutableListOf() 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | setContentView(R.layout.activity_main) 34 | 35 | // initialize model and presenter 36 | model = WeatherInfoShowModelImpl(applicationContext) 37 | presenter = WeatherInfoShowPresenterImpl(this, model) 38 | 39 | // call for fetching city list 40 | presenter.fetchCityList() 41 | 42 | 43 | btn_view_weather.setOnClickListener { 44 | output_group.visibility = View.GONE 45 | 46 | val spinnerSelectedItemPos = spinner.selectedItemPosition 47 | 48 | // fetch weather info of specific city 49 | presenter.fetchWeatherInfo(cityList[spinnerSelectedItemPos].id) 50 | } 51 | } 52 | 53 | override fun onDestroy() { 54 | presenter.detachView() 55 | super.onDestroy() 56 | } 57 | 58 | /** 59 | * Activity doesn't know when should progress bar visible or hide. It only knows 60 | * how to show/hide it. 61 | * Presenter will decide the logic of progress bar visibility. 62 | * This method will be triggered by presenter when needed. 63 | */ 64 | override fun handleProgressBarVisibility(visibility: Int) { 65 | progressBar?.visibility = visibility 66 | } 67 | 68 | /** 69 | * This method will be triggered when city list successfully fetched. 70 | * From where this list will be come? From local db or network call or from somewhere else? 71 | * Activity/View doesn't know and doesn't care anything about it. Activity only knows how to 72 | * show the city list on the UI and listen the click event of the Spinner. 73 | * Model knows about the data source of city list. 74 | */ 75 | override fun onCityListFetchSuccess(cityList: MutableList) { 76 | this.cityList = cityList 77 | 78 | val arrayAdapter = ArrayAdapter( 79 | this, 80 | R.layout.support_simple_spinner_dropdown_item, 81 | cityList.convertToListOfCityName() 82 | ) 83 | 84 | spinner.adapter = arrayAdapter 85 | } 86 | 87 | /** 88 | * This method will triggered if city list fetching process failed 89 | */ 90 | override fun onCityListFetchFailure(errorMessage: String) { 91 | Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show() 92 | } 93 | 94 | /** 95 | * This method will triggered when weather information successfully fetched. 96 | * Activity/View doesn't know anything about the data source of weather API. 97 | * Only model knows about the data source of weather API. 98 | */ 99 | override fun onWeatherInfoFetchSuccess(weatherDataModel: WeatherDataModel) { 100 | output_group.visibility = View.VISIBLE 101 | tv_error_message.visibility = View.GONE 102 | 103 | tv_date_time?.text = weatherDataModel.dateTime 104 | tv_temperature?.text = weatherDataModel.temperature 105 | tv_city_country?.text = weatherDataModel.cityAndCountry 106 | Glide.with(this).load(weatherDataModel.weatherConditionIconUrl).into(iv_weather_condition) 107 | tv_weather_condition?.text = weatherDataModel.weatherConditionIconDescription 108 | 109 | tv_humidity_value?.text = weatherDataModel.humidity 110 | tv_pressure_value?.text = weatherDataModel.pressure 111 | tv_visibility_value?.text = weatherDataModel.visibility 112 | 113 | tv_sunrise_time?.text = weatherDataModel.sunrise 114 | tv_sunset_time?.text = weatherDataModel.sunset 115 | } 116 | 117 | /** 118 | * This method will triggered if weather information fetching process failed 119 | */ 120 | override fun onWeatherInfoFetchFailure(errorMessage: String) { 121 | output_group.visibility = View.GONE 122 | tv_error_message.visibility = View.VISIBLE 123 | tv_error_message.text = errorMessage 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/features/weather_info_show/view/MainActivityView.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.features.weather_info_show.view 2 | 3 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.City 4 | import com.hellohasan.weatherforecast.features.weather_info_show.model.data_class.WeatherDataModel 5 | 6 | interface MainActivityView { 7 | fun handleProgressBarVisibility(visibility: Int) 8 | fun onCityListFetchSuccess(cityList: MutableList) 9 | fun onCityListFetchFailure(errorMessage: String) 10 | fun onWeatherInfoFetchSuccess(weatherDataModel: WeatherDataModel) 11 | fun onWeatherInfoFetchFailure(errorMessage: String) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/network/ApiInterface.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.network 2 | 3 | import com.hellohasan.weatherforecast.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/weatherforecast/network/QueryParameterAddInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.network 2 | 3 | import com.hellohasan.weatherforecast.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/weatherforecast/network/RetrofitClient.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.network 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.hellohasan.weatherforecast.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 | 12 | object RetrofitClient { 13 | 14 | private var retrofit: Retrofit? = null 15 | private val gson = GsonBuilder().setLenient().create() 16 | 17 | val client: Retrofit 18 | get() { 19 | if (retrofit == null) { 20 | synchronized(Retrofit::class.java) { 21 | if (retrofit == null) { 22 | 23 | val httpClient = OkHttpClient.Builder() 24 | .addInterceptor(QueryParameterAddInterceptor()) 25 | 26 | // for pretty log of HTTP request-response 27 | httpClient.addInterceptor( 28 | LoggingInterceptor.Builder() 29 | .setLevel(if(BuildConfig.DEBUG) Level.BASIC else Level.NONE) 30 | .log(Platform.INFO) 31 | .request("LOG") 32 | .response("LOG") 33 | .build() 34 | ) 35 | 36 | val client = httpClient.build() 37 | 38 | retrofit = Retrofit.Builder() 39 | .baseUrl(BuildConfig.BASE_URL) 40 | .addConverterFactory(GsonConverterFactory.create(gson)) 41 | .client(client) 42 | .build() 43 | } 44 | } 45 | 46 | } 47 | return retrofit!! 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hellohasan/weatherforecast/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.hellohasan.weatherforecast.utils 2 | 3 | import com.hellohasan.weatherforecast.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/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/haze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasancse91/weather-app-android-mvp-architecture/1bb6aa2a428ce50491da96f4a5d8f3d77d9c74b8/app/src/main/res/drawable/haze.png -------------------------------------------------------------------------------- /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/drawable/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/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/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 |