How to create a weather app in jetpack compose ( Clean Architecture/MVVM ,dagger -hilt)
In this blog i will share how can we create a weather app in jetpack compose , using clean architecture pattern

📱 Hey there! I'm Vyom Singh, a regular learner diving into Android development. Come with me as I figure out how to make apps and share what I learn. Let's explore Android together! 🌟📚 #AndroidDev #LearningAsWeGo
Prerequisites
Before we dive into building the weather app, make sure you have the following prerequisites:
Android Studio: Ensure you have Android Studio installed on your development machine.
Api Used from here: https://open-meteo.com/en/docs
Jetpack Compose: Make sure you're familiar with Jetpack Compose, the modern Android UI toolkit.
MVVM Architecture: Understand the MVVM architectural pattern, which separates your app's logic into distinct layers.
Add Plugins in the build.gradle(app -level):
com. android.application: The Android Gradle plugin for building Android apps.org. jetBrains.kotlin.android: Kotlin Android Extensions for Android development.kotlin-kapt: The Kotlin annotation processing plugin.dagger.hilt.android.plugin: Dagger-Hilt plugin for dependency injection.
Dependency Highlights:
androidx.lifecycle: AndroidX Lifecycle libraries for managing Android component lifecycles.com.google.dagger:hilt-android: Dagger-Hilt library for dependency injection.com.squareup.retrofit2: Retrofit library for making HTTP requests.com.google.android.gms:play-services-location: Google Play services for location-related functionality.implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0-alpha01" // Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.1' // Coroutine Lifecycle Scopes implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" //Dagger - Hilt implementation "com.google.dagger:hilt-android:2.46.1" kapt "com.google.dagger:hilt-android-compiler:2.46.1" kapt "com.google.dagger:hilt-android-compiler:2.46.1" kapt "androidx.hilt:hilt-compiler:1.0.0" implementation "androidx.hilt:hilt-navigation-compose:1.0.0" // Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2' kapt 'androidx.hilt:hilt-compiler:1.0.0' // flow layout implementation 'com.google.accompanist:accompanist-flowlayout:0.31.4-beta' implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) // for location implementation 'com.google.android.gms:play-services-location:21.0.1'- ```xml buildscript {
dependencies { classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" classpath 'com.google.dagger:hilt-android-gradle-plugin:2.46.1' } } plugins { id 'com.android.application' version '8.0.0' apply false id 'com.android.library' version '8.0.0' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false }
* ### **Step 1: Data Layer - Data Classes and Retrofit Interface**
1. create a model package inside data layer package , in model package we will create data classes , you can also create these data classes using "kotlin data classes from json" tool in android studio
```kotlin
data class AllWeather_Dto(
val current_weather: CurrentWeather_Dto,
val daily: Daily_Dto,
val daily_units: DailyUnits_Dto,
val elevation: Double,
val generationtime_ms: Double,
val hourly: Hourly_Dto,
val hourly_units: HourlyUnits_Dto,
val latitude: Double,
val longitude: Double,
val timezone: String,
val timezone_abbreviation: String,
val utc_offset_seconds: Int
)
data class CurrentWeather_Dto(
val is_day: Int,
val temperature: Double,
val time: String,
val weathercode: Int,
val winddirection: Int,
val windspeed: Double
)
data class Daily_Dto(
val temperature_2m_max: List<Double>,
val temperature_2m_min: List<Double>,
val time: List<String>,
val weathercode: List<Int>
)
data class DailyUnits_Dto(
val temperature_2m_max: String,
val temperature_2m_min: String,
val time: String,
val weathercode: String
)
data class Hourly_Dto(
val precipitation: List<Double>,
val rain: List<Double>,
val snowfall: List<Double>,
val surface_pressure: List<Double>,
val temperature_2m: List<Double>,
val time: List<String>,
val uv_index: List<Double>,
val visibility: List<Double>,
val weathercode: List<Int>,
val windspeed_120m: List<Double>
)
data class HourlyUnits_Dto(
val precipitation: String,
val rain: String,
val snowfall: String,
val surface_pressure: String,
val temperature_2m: String,
val time: String,
val uv_index: String,
val visibility: String,
val weathercode: String,
val windspeed_120m: String
)
// create this in different file
interface Weatherapi {
@GET("/v1/forecast?&windspeed_unit=ms&hourly=temperature_2m,precipitation,rain,snowfall,weathercode,surface_pressure,visibility,windspeed_120m,uv_index&daily=weathercode," +
"temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=auto")
suspend fun getAllWhetherData(
@Query("latitude") lat: Double,
@Query("longitude") long:Double
):Response<AllWeather_Dto>
}
Step 2: Dependency Injection (DI) - Data Module
<application android:name=".BaseApplication" ....... </application>
- create a class
BaseApplicationfor dagger-hilt
@HiltAndroidApp
class BaseApplication: Application() {
}
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun WeatherapiService(): Weatherapi {
return Retrofit.Builder().baseUrl("https://api.open-meteo.com")
.addConverterFactory(GsonConverterFactory.create())
.build().create(
Weatherapi::class.java
)
}
}
Step 3: Location Services - DefaultLocationTracker
class DefaultLocationTracker @Inject constructor(
private val locationClient: FusedLocationProviderClient,
private val application: Application
): LocationTracker {
override suspend fun getLocation(): Location? {
val hasAccessFineLocationPermission = ContextCompat.checkSelfPermission(
application,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
application,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val locationManager = application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
if(!hasAccessCoarseLocationPermission || !hasAccessFineLocationPermission || !isGpsEnabled) {
return null
}
return suspendCancellableCoroutine { cont ->
locationClient.lastLocation.apply {
if(isComplete) {
if(isSuccessful) {
cont.resume(result)
} else {
cont.resume(null)
}
return@suspendCancellableCoroutine
}
addOnSuccessListener {
cont.resume(it)
}
addOnFailureListener {
cont.resume(null)
}
addOnCanceledListener {
cont.cancel()
}
}
}
}
}
also create
LocationTrackerinterface in domain layerinterface LocationTracker { suspend fun getLocation() : Location ? }well for injecting
DefaultLocationTrackerwe needFusedproviderclientand context ..@Module @InstallIn(SingletonComponent::class) object LocationModule { @Provides @Singleton fun provideLocationTracker(defaultLocationTracker: DefaultLocationTracker): LocationTracker { return defaultLocationTracker } @Provides @Singleton fun provideContext(@ApplicationContext context: Context): Context { return context } }
Step 4: Domain and Presentation Layers - WeatherRepoImpl(data-layer)
before implementing the repository let's create model class for showing data of current weather in presentation layer
data class CurrentWeather( val isDay: Int, val temperature: Double, val time: String, val weatherType: WeatherType, val windDirection: Int, val windSpeed: Double )// this class will be used in mapper function for converting weather // code to weather type sealed class WeatherType( val weatherDesc: String, @DrawableRes val iconRes: Int, ) { object ClearSky : WeatherType( weatherDesc = "Clear sky", iconRes = R.drawable.ic_sunny ) object MainlyClear : WeatherType( weatherDesc = "Mainly clear", iconRes = R.drawable.ic_cloudy ) object PartlyCloudy : WeatherType( weatherDesc = "Partly cloudy", iconRes = R.drawable.ic_cloudy ) object Overcast : WeatherType( weatherDesc = "Overcast", iconRes = R.drawable.ic_cloudy ) object Foggy : WeatherType( weatherDesc = "Foggy", iconRes = R.drawable.ic_verycloudy ) object DepositingRimeFog : WeatherType( weatherDesc = "Depositing rime fog", iconRes = R.drawable.ic_verycloudy ) object LightDrizzle : WeatherType( weatherDesc = "Light drizzle", iconRes = R.drawable.ic_rainshower ) object ModerateDrizzle : WeatherType( weatherDesc = "Moderate drizzle", iconRes = R.drawable.ic_rainshower ) object DenseDrizzle : WeatherType( weatherDesc = "Dense drizzle", iconRes = R.drawable.ic_rainshower ) object LightFreezingDrizzle : WeatherType( weatherDesc = "Slight freezing drizzle", iconRes = R.drawable.ic_snowyrain ) object DenseFreezingDrizzle : WeatherType( weatherDesc = "Dense freezing drizzle", iconRes = R.drawable.ic_snowyrain ) object SlightRain : WeatherType( weatherDesc = "Slight rain", iconRes = R.drawable.ic_rainy ) object ModerateRain : WeatherType( weatherDesc = "Rainy", iconRes = R.drawable.ic_rainy ) object HeavyRain : WeatherType( weatherDesc = "Heavy rain", iconRes = R.drawable.ic_rainy ) object HeavyFreezingRain: WeatherType( weatherDesc = "Heavy freezing rain", iconRes = R.drawable.ic_snowyrain ) object SlightSnowFall: WeatherType( weatherDesc = "Slight snow fall", iconRes = R.drawable.ic_snowy ) object ModerateSnowFall: WeatherType( weatherDesc = "Moderate snow fall", iconRes = R.drawable.ic_heavysnow ) object HeavySnowFall: WeatherType( weatherDesc = "Heavy snow fall", iconRes = R.drawable.ic_heavysnow ) object SnowGrains: WeatherType( weatherDesc = "Snow grains", iconRes = R.drawable.ic_heavysnow ) object SlightRainShowers: WeatherType( weatherDesc = "Slight rain showers", iconRes = R.drawable.ic_rainshower ) object ModerateRainShowers: WeatherType( weatherDesc = "Moderate rain showers", iconRes = R.drawable.ic_rainshower ) object ViolentRainShowers: WeatherType( weatherDesc = "Violent rain showers", iconRes = R.drawable.ic_rainshower ) object SlightSnowShowers: WeatherType( weatherDesc = "Light snow showers", iconRes = R.drawable.ic_snowy ) object HeavySnowShowers: WeatherType( weatherDesc = "Heavy snow showers", iconRes = R.drawable.ic_snowy ) object ModerateThunderstorm: WeatherType( weatherDesc = "Moderate thunderstorm", iconRes = R.drawable.ic_thunder ) object SlightHailThunderstorm: WeatherType( weatherDesc = "Thunderstorm with slight hail", iconRes = R.drawable.ic_rainythunder ) object HeavyHailThunderstorm: WeatherType( weatherDesc = "Thunderstorm with heavy hail", iconRes = R.drawable.ic_rainythunder ) companion object { fun fromWMO(code: Int): WeatherType { return when(code) { 0 -> ClearSky 1 -> MainlyClear 2 -> PartlyCloudy 3 -> Overcast 45 -> Foggy 48 -> DepositingRimeFog 51 -> LightDrizzle 53 -> ModerateDrizzle 55 -> DenseDrizzle 56 -> LightFreezingDrizzle 57 -> DenseFreezingDrizzle 61 -> SlightRain 63 -> ModerateRain 65 -> HeavyRain 66 -> LightFreezingDrizzle 67 -> HeavyFreezingRain 71 -> SlightSnowFall 73 -> ModerateSnowFall 75 -> HeavySnowFall 77 -> SnowGrains 80 -> SlightRainShowers 81 -> ModerateRainShowers 82 -> ViolentRainShowers 85 -> SlightSnowShowers 86 -> HeavySnowShowers 95 -> ModerateThunderstorm 96 -> SlightHailThunderstorm 99 -> HeavyHailThunderstorm else -> ClearSky } } } }also create util package and create two classes Resource and
safe api request class
sealed class Resource<T>(val data:T? = null , val message : String? = null){ class Success<T>(data:T?):Resource<T>(data= data) class Error <T>(message: String?):Resource<T>(message =message) }```kotlin abstract class SafeApiRequest {
suspend fun safeApiRequest(call: suspend () -> Response): T { val response = call.invoke() if (response.isSuccessful) { return response.body()!! } else { val responseErr = response.errorBody()?.string() val message = StringBuilder() responseErr.let { try { message.append(JSONObject(it).getString("error")) } catch (e: JSONException) { } } Log.d("TAG", "safeApiRequest: ${message.toString()}") throw Exception(message.toString()) } }
}
* In essence, this class provides a convenient way to make API requests and handle potential errors in a standardized manner. It ensures that API errors are properly logged and can be caught and handled at a higher level in your application.
* in this step we will create weather repo interface in domain section and implement that in data layer as weather repo implementation class
* before that create a mapper function which maps current weather\_dto to current weather model present in domain class
```kotlin
object Mapper {
fun CurrentWeather_Dto.toDomainModel(): CurrentWeather {
val weatherType = WeatherType.fromWMO(weathercode)
return CurrentWeather(is_day, temperature, time, weatherType, winddirection, windspeed)
}
}
// domain layer repo package
interface WeatherRepo {
suspend fun getCurrentWeather( lat : Double, lon : Double): CurrentWeather
suspend fun getHourlyWeather( lat : Double, lon : Double): List<Hourly>
suspend fun getDailyWeather( lat : Double, lon : Double): Map<DayOfWeek, List<Daily>>
}
// data layer repoIMpl class
class WeatherRepoImpl @Inject constructor(
private val weatherapiService: Weatherapi
):WeatherRepo,SafeApiRequest() {
override suspend fun getCurrentWeather(lat: Double, lon: Double): CurrentWeather {
val response_currentWeather = safeApiRequest {
Log.d("Repository", "Fetching current weather data")
weatherapiService.getAllWhetherData(lat,lon)
}
val apiCurrentWeather_Dto = response_currentWeather.current_weather
Log.d("Repository", "Current weather data received: $apiCurrentWeather_Dto")
val domainCurrentWeather = apiCurrentWeather_Dto.toDomainModel()
return domainCurrentWeather
}
}
/// used for injecting weather repository instance
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideWeatherRepository(
weatherapi: Weatherapi
): WeatherRepo {
return WeatherRepoImpl(weatherapi)
}
}
now next is to create a usecase class named
GetCurrentWeatherclass provides a clean and structured way to request and handle current weather data asynchronously. It uses Kotlin Flow to handle the data flow and provides a standardized way to handle both successful and error responses through theResourceclass.class GetCurrentWeather @Inject constructor(private val weatherRepoImpl: WeatherRepoImpl) { operator fun invoke(lat:Double, lon:Double): Flow<Resource<CurrentWeather>> = flow { try { emit(Resource.Success(weatherRepoImpl.getCurrentWeather(lat, lon))) } catch (e: Exception) { emit(Resource.Error(e.message)) } } }@Module @InstallIn(ViewModelComponent::class) object UsecaseModule { @Provides fun providegetCurrentWeather(weatherRepoImpl: WeatherRepoImpl): GetCurrentWeather { return GetCurrentWeather(weatherRepoImpl) } }Step 6: Viewmodel and composable creation(presentation layer)
create a
CurrentWeatherViewModelfor fetching current data from usecase and also create current weather state for updating the state of weather data in viewmodeldata class CurrentWeatherState( val isLoading : Boolean = false, val data : CurrentWeather? = null, val error: String? = null )```kotlin ''' @HiltViewModel class CurrentWeatherViewModel @Inject constructor( private val context : Context, private val locationTracker: LocationTracker, private val getCurrentWeather: GetCurrentWeather,
) : ViewModel() {
private val _city = mutableStateOf("") var city: State = _city var state by mutableStateOf(CurrentWeatherState()) private set @SuppressLint("SuspiciousIndentation") fun fetchCurrentWeather(latitude: Double? = null, longitude: Double? = null) { viewModelScope.launch {
state= state.copy(isLoading = true)
try { val location = if (latitude != null && longitude != null) { Pair(latitude, longitude) }else { val currentlocation = locationTracker.getLocation() if (currentlocation != null) { Log.d("ViewModel", "Location obtained: $currentlocation") Pair(currentlocation.latitude, currentlocation.longitude) } else { state = state.copy( isLoading = false, data = null, error = "Location not available" ) return@launch } } _city.value = getCityName(location.first, location.second).toString() Log.d("ViewModel", _city.value) val weatherFlow = getCurrentWeather .invoke(location.first, location.first) Log.d("ViewModel", "Weather data fetching started")
weatherFlow.collect { resource-> when (resource) { is Resource.Success -> { state = state.copy(isLoading = false, data = resource.data,error = null) Log.d("ViewModel", "Weather data fetched successfully: ${resource.data}")
} is Resource.Error -> { state = state.copy(isLoading = false, data = resource.data,error = null) Log.e("ViewModel", "Error fetching weather: ${resource.message}") }
} }
} catch (e: Exception) { state = state.copy(isLoading = false, data =null,error = "error fetching weather") Log.e("ViewModel", "Error fetching weather: ${e.message}") } } }
suspend fun getCityName(latitude: Double, longitude: Double): String? { return withContext(Dispatchers.IO) { val geocoder = Geocoder( context, Locale.getDefault()) try { val addresses = geocoder.getFromLocation(latitude, longitude, 1) if (addresses?.isNotEmpty() == true) { val address = addresses[0] val city = address.locality city
} else {
} } catch (e: Exception) {
Log.e("Cityname", "Error getting city name: ${e.message}")
} } } } '''
### **Step 7: Composables - CurrentWeathercard and CircularProgressBar& All weather composable**
* Now it's time to create composables for current weather ..
```kotlin
@Composable
fun CurrentWeathercard(
modifier: Modifier,
currentWeatherViewModel: CurrentWeatherViewModel
) {
state.data.let {
Log.d("weatherCard",it?.temperature.toString())
Card(modifier = modifier
.padding(14.dp)
.fillMaxWidth()
.height(450.dp),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.outlinedCardElevation(10.dp)
) {
Column(
horizontalAlignment = Alignment.Start
) {
Text(text = currentWeatherViewModel.city.value, fontSize = 45.sp, fontStyle = FontStyle.Normal,
fontWeight = FontWeight.ExtraBold ,
modifier = Modifier.padding(start = 13.dp) )
Text(text = "${it?.temperature}°", fontSize = 45.sp, fontStyle = FontStyle.Normal,
fontWeight = FontWeight.ExtraBold, modifier =
Modifier.padding(start = 13.dp, bottom = 13.dp))
Spacer(modifier = Modifier.height(3.dp))
Text(text = "${it?.weatherType?.weatherDesc}",
fontSize = 13.sp, fontStyle = FontStyle.Normal, fontWeight = FontWeight.Light,
modifier = Modifier.padding(start = 13.dp))
Spacer(modifier = Modifier.height(0.5.dp))
Text(text = "${it?.windSpeed} m/s", fontSize =
13.sp, fontStyle = FontStyle.Normal, fontWeight = FontWeight.Light,
modifier = Modifier.padding(start = 13.dp))
Divider(
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
.height(2.dp),
color = Color.LightGray, thickness = 0.3.dp
)
}
}
}
}
@Composable
fun AllWeatherComposable(
viewModel: CurrentWeatherViewModel = hiltViewModel()
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
val state = viewModel.state
var permissionStatus by remember { mutableStateOf(false) }
// Permission launcher setup and handling here...
if (state.isLoading) {
CircularProgressBar()
} else if (state.error != null) {
// Show an error message if there's an error
Text(
text = state.error,
color = Color.Red,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.wrapContentHeight(Alignment.CenterVertically)
)
} else {
// Include your UI content within LazyColumn
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.Transparent)
) {
item {
Spacer(modifier = Modifier.height(180.dp))
CurrentWeathercard(
state = state,
modifier = Modifier,
currentWeatherViewModel = viewModel
)
Spacer(modifier = Modifier.height(10.dp))
// Include other UI elements within the LazyColumn as needed...
}
}
}
}
@Composable
fun CircularProgressBar() {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator(
modifier = Modifier
.size(70.dp)
.padding(16.dp)
.wrapContentSize(Alignment.Center)
)
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WhetherAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AllWeatherComposable()
}
}
}
}
}
don't forget to allow permissions on Android -manifest file
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> ... </manifest>
"If you want to add more features, you can check the GitHub repository here: https://github.com/vyom198/WeatherApp_jetpackCompose."
