Skip to main content

Command Palette

Search for a command to run...

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

Updated
11 min read
How to create a weather app in jetpack compose ( Clean Architecture/MVVM ,dagger -hilt)
V

📱 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:

  1. Android Studio: Ensure you have Android Studio installed on your development machine.

  2. Api Used from here: https://open-meteo.com/en/docs

  3. Jetpack Compose: Make sure you're familiar with Jetpack Compose, the modern Android UI toolkit.

  4. MVVM Architecture: Understand the MVVM architectural pattern, which separates your app's logic into distinct layers.

  5. Add Plugins in the build.gradle(app -level):

  6. 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&current_weather=true&timezone=auto")
       suspend fun  getAllWhetherData(

           @Query("latitude") lat: Double,
           @Query("longitude") long:Double

       ):Response<AllWeather_Dto>



       }
  1. Step 2: Dependency Injection (DI) - Data Module

    1.    <application
                 android:name=".BaseApplication"
                  .......
         </application>
      
  • create a class BaseApplication for 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 LocationTracker interface in domain layer

  •       interface LocationTracker {
              suspend fun getLocation() : Location ?
          }
    
  • well for injecting DefaultLocationTracker we need Fusedproviderclient and 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 GetCurrentWeather class 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 the Resource class.

      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 CurrentWeatherViewModel for fetching current data from usecase and also create current weather state for updating the state of weather data in viewmodel

      data 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."