Jetpack Compose: Building Weather Apps

Building a weather app is a common project for Android developers, serving as a practical exercise to showcase skills in fetching data from APIs, handling asynchronous tasks, and creating a responsive UI. Jetpack Compose, Android’s modern UI toolkit, simplifies this process with its declarative syntax and powerful composables. This blog post will guide you through the steps of creating a basic weather app using Jetpack Compose.

Prerequisites

Before diving into the implementation, ensure you have the following:

  • Android Studio installed with the latest updates.
  • Basic knowledge of Kotlin and Jetpack Compose.
  • An API key from a weather data provider (e.g., OpenWeatherMap).

Step 1: Setting Up the Project

Create a new Android project in Android Studio, selecting an Empty Compose Activity template.

Add Dependencies

Include the necessary dependencies in your build.gradle file:


dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    // Retrofit for networking
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
    // Coil for image loading
    implementation("io.coil-kt:coil-compose:2.4.0")
    // Location
    implementation("com.google.android.gms:play-services-location:21.1.0")
}

Step 2: Define the Data Model

Create Kotlin data classes to represent the weather data you will receive from the API. For example:


data class WeatherResponse(
    val weather: List<Weather>,
    val main: Main,
    val name: String,
    val wind: Wind
)

data class Weather(
    val id: Int,
    val main: String,
    val description: String,
    val icon: String
)

data class Main(
    val temp: Double,
    val feels_like: Double,
    val temp_min: Double,
    val temp_max: Double,
    val pressure: Int,
    val humidity: Int
)

data class Wind(
    val speed: Double,
    val deg: Int
)

Step 3: Implement the API Service

Use Retrofit to define an interface for the weather API.

Create the API Interface


import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.Call
import retrofit2.Response

interface WeatherApiService {
    @GET("data/2.5/weather")
    suspend fun getCurrentWeather(
        @Query("q") city: String,
        @Query("appid") apiKey: String,
        @Query("units") units: String = "metric" // Metric units
    ): Response<WeatherResponse>
}

Configure Retrofit


import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitInstance {
    private const val BASE_URL = "https://api.openweathermap.org/"

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    val weatherApiService: WeatherApiService by lazy {
        retrofit.create(WeatherApiService::class.java)
    }
}

Step 4: Create a ViewModel

Use a ViewModel to fetch weather data and hold it in a lifecycle-conscious manner.


import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class WeatherViewModel : ViewModel() {
    private val _weatherData = MutableLiveData<WeatherResponse?>()
    val weatherData = _weatherData

    private val apiKey = "YOUR_API_KEY" // Replace with your API key

    fun fetchWeather(city: String) {
        viewModelScope.launch {
            try {
                val response = RetrofitInstance.weatherApiService.getCurrentWeather(city, apiKey)
                if (response.isSuccessful) {
                    _weatherData.value = response.body()
                } else {
                    // Handle error
                    println("Error fetching weather: ${response.message()}")
                    _weatherData.value = null
                }
            } catch (e: Exception) {
                // Handle exception
                println("Exception: ${e.message}")
                _weatherData.value = null
            }
        }
    }
}

Step 5: Build the UI with Compose

Create composable functions to display the weather information.

Compose Activity Content


import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WeatherApp() {
    val weatherViewModel: WeatherViewModel = viewModel()
    var cityInput by remember { mutableStateOf("London") }
    val weatherData = weatherViewModel.weatherData.value

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Weather App") })
        }
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            OutlinedTextField(
                value = cityInput,
                onValueChange = { cityInput = it },
                label = { Text("Enter City") }
            )
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = {
                weatherViewModel.fetchWeather(cityInput)
            }) {
                Text("Get Weather")
            }

            Spacer(modifier = Modifier.height(32.dp))

            if (weatherData != null) {
                WeatherDisplay(weatherData = weatherData)
            } else {
                Text("No weather data available", style = MaterialTheme.typography.bodyMedium)
            }
        }
    }
}

Weather Display Composable


import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage

@Composable
fun WeatherDisplay(weatherData: WeatherResponse) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = weatherData.name,
            style = MaterialTheme.typography.headlineSmall,
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(8.dp))
        val weather = weatherData.weather.firstOrNull()
        if (weather != null) {
            AsyncImage(
                model = "https://openweathermap.org/img/wn/${weather.icon}@2x.png",
                contentDescription = "Weather Icon",
                modifier = Modifier.size(72.dp)
            )
            Text(
                text = weather.description,
                style = MaterialTheme.typography.bodyLarge,
                textAlign = TextAlign.Center
            )
        }
        Spacer(modifier = Modifier.height(8.dp))
        Text(
            text = "Temperature: ${weatherData.main.temp}°C",
            style = MaterialTheme.typography.bodyMedium
        )
        Text(
            text = "Feels like: ${weatherData.main.feels_like}°C",
            style = MaterialTheme.typography.bodyMedium
        )
        Text(
            text = "Humidity: ${weatherData.main.humidity}%",
            style = MaterialTheme.typography.bodyMedium
        )
        Text(
            text = "Wind Speed: ${weatherData.wind.speed} m/s",
            style = MaterialTheme.typography.bodyMedium
        )
    }
}

Call WeatherApp in Main Activity


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.weatherapp.WeatherApp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WeatherApp()
        }
    }
}

Step 6: Request Internet Permission

Add the internet permission to your AndroidManifest.xml file:


<uses-permission android:name="android.permission.INTERNET"/>

Conclusion

You’ve successfully built a basic weather app using Jetpack Compose. This project demonstrates the power and simplicity of Compose in creating modern Android UIs. Expand this app by adding features like location detection, weather forecasting, and a more polished UI. Experiment with different APIs and UI components to enhance your skills in Android development with Jetpack Compose.