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.