While modern Android development increasingly embraces Jetpack Compose, many legacy applications and projects still rely on XML for designing user interfaces. This blog post delves into building a weather app using XML for the UI, showcasing how to fetch weather data from an API and display it effectively. Even though XML might seem outdated, it remains a valuable skill for maintaining existing apps and understanding the fundamentals of Android UI development.
Why Build a Weather App with XML?
- Maintenance of Legacy Apps: Many existing apps are built using XML, requiring ongoing maintenance and updates.
- Understanding Core Concepts: Working with XML provides a solid understanding of Android’s UI system.
- Performance Considerations: In some cases, XML layouts can be more performant than Compose for complex views, especially on older devices.
Project Setup
Step 1: Create a New Android Project
In Android Studio, create a new project with an Empty Activity template. Ensure that the language is set to Kotlin and the minimum SDK is appropriate for your target devices.
Step 2: Add Dependencies
Add the necessary dependencies to your build.gradle file. These dependencies will include libraries for networking (Retrofit), JSON parsing (Gson), and image loading (Glide).
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.github.bumptech.glide:glide:4.12.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.12.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.appcompat:appcompat:1.4.0") // Check the latest version
implementation("androidx.constraintlayout:constraintlayout:2.1.4") // Check the latest version
// Optional: Kotlin Coroutines for asynchronous operations
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}
Sync the Gradle file to download and install the dependencies.
Step 3: Request Internet Permission
Add the internet permission to your AndroidManifest.xml file:
UI Design with XML
Step 1: Design the Layout
Create the layout for your weather app in activity_main.xml. This layout will include elements to display the weather information, such as TextViews for temperature, city name, description, and an ImageView for the weather icon.
android:contentDescription="Weather Icon"/>
Step 2: Define UI Elements in the Activity
In your MainActivity.kt, declare variables for each of the UI elements defined in the XML layout.
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
class MainActivity : AppCompatActivity() {
private lateinit var textViewCity: TextView
private lateinit var imageViewWeatherIcon: ImageView
private lateinit var textViewTemperature: TextView
private lateinit var textViewDescription: TextView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI elements
textViewCity = findViewById(R.id.textViewCity)
imageViewWeatherIcon = findViewById(R.id.imageViewWeatherIcon)
textViewTemperature = findViewById(R.id.textViewTemperature)
textViewDescription = findViewById(R.id.textViewDescription)
swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
// TODO: Fetch Weather Data and update UI
}
}
Fetching Weather Data
Step 1: Define the API Interface
Create a Kotlin interface that defines the API endpoint for fetching weather data. You’ll need an API key and an endpoint URL from a weather API provider (e.g., OpenWeatherMap).
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherApi {
@GET("data/2.5/weather")
fun getWeather(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric" // Use "imperial" for Fahrenheit
): Call
}
Step 2: Create a Data Class for the Weather Response
Define Kotlin data classes that match the structure of the JSON response from the weather API. Here’s an example:
Step 3: Implement Retrofit Client
Create a Retrofit client to handle the API calls.
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ApiClient {
private const val BASE_URL = "https://api.openweathermap.org/" // Replace with correct base URL
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val weatherApi: WeatherApi = retrofit.create(WeatherApi::class.java)
}
Step 4: Fetch Weather Data in the Activity
Implement a function to fetch weather data from the API. Use Kotlin coroutines for asynchronous operations to avoid blocking the main thread.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
private val apiKey = "YOUR_API_KEY" // Replace with your actual API key
private val defaultCity = "London" // Example Default City
private fun fetchWeatherData(city: String = defaultCity) {
swipeRefreshLayout.isRefreshing = true // Start the refreshing animation
CoroutineScope(Dispatchers.IO).launch {
val call = ApiClient.weatherApi.getWeather(city, apiKey)
withContext(Dispatchers.Main) {
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
swipeRefreshLayout.isRefreshing = false // Stop the refreshing animation
if (response.isSuccessful) {
val weatherResponse = response.body()
weatherResponse?.let { updateUI(it) }
} else {
// Handle error
textViewDescription.text = "Error fetching weather data"
}
}
override fun onFailure(call: Call, t: Throwable) {
swipeRefreshLayout.isRefreshing = false // Stop the refreshing animation
textViewDescription.text = "Network error: ${t.message}"
}
})
}
}
}
Replace "YOUR_API_KEY" with your actual API key.
Step 5: Update the UI
Implement a function to update the UI with the fetched weather data.
import com.bumptech.glide.Glide
private fun updateUI(weatherResponse: WeatherResponse) {
textViewCity.text = weatherResponse.name
textViewTemperature.text = "Temperature: ${weatherResponse.main.temp}°C"
textViewDescription.text = "Description: ${weatherResponse.weather[0].description}"
val iconCode = weatherResponse.weather[0].icon
val iconUrl = "https://openweathermap.org/img/w/$iconCode.png" // Correct image URL
Glide.with(this)
.load(iconUrl)
.into(imageViewWeatherIcon)
}
Implement Swipe-to-Refresh
Use SwipeRefreshLayout to allow users to refresh weather data.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI elements
textViewCity = findViewById(R.id.textViewCity)
imageViewWeatherIcon = findViewById(R.id.imageViewWeatherIcon)
textViewTemperature = findViewById(R.id.textViewTemperature)
textViewDescription = findViewById(R.id.textViewDescription)
swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
swipeRefreshLayout.setOnRefreshListener {
fetchWeatherData() // Fetch new data on refresh
}
fetchWeatherData() // Initial data fetch
}
Final Code
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.bumptech.glide.Glide
import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
// API Data Classes
data class WeatherResponse(
val weather: List,
val main: Main,
val name: String
)
data class Weather(
val id: Int,
val main: String,
val description: String,
val icon: String
)
data class Main(
val temp: Double,
@SerializedName("temp_min") val tempMin: Double,
@SerializedName("temp_max") val tempMax: Double,
val pressure: Int,
val humidity: Int
)
// Retrofit API Interface
interface WeatherApi {
@GET("data/2.5/weather")
fun getWeather(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric" // Use "imperial" for Fahrenheit
): Call
}
// Retrofit Client
object ApiClient {
private const val BASE_URL = "https://api.openweathermap.org/"
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val weatherApi: WeatherApi = retrofit.create(WeatherApi::class.java)
}
class MainActivity : AppCompatActivity() {
private lateinit var textViewCity: TextView
private lateinit var imageViewWeatherIcon: ImageView
private lateinit var textViewTemperature: TextView
private lateinit var textViewDescription: TextView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private val apiKey = "YOUR_API_KEY" // Replace with your actual API key
private val defaultCity = "London" // Example Default City
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI elements
textViewCity = findViewById(R.id.textViewCity)
imageViewWeatherIcon = findViewById(R.id.imageViewWeatherIcon)
textViewTemperature = findViewById(R.id.textViewTemperature)
textViewDescription = findViewById(R.id.textViewDescription)
swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
swipeRefreshLayout.setOnRefreshListener {
fetchWeatherData() // Fetch new data on refresh
}
fetchWeatherData() // Initial data fetch
}
private fun fetchWeatherData(city: String = defaultCity) {
swipeRefreshLayout.isRefreshing = true // Start the refreshing animation
CoroutineScope(Dispatchers.IO).launch {
val call = ApiClient.weatherApi.getWeather(city, apiKey)
withContext(Dispatchers.Main) {
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
swipeRefreshLayout.isRefreshing = false // Stop the refreshing animation
if (response.isSuccessful) {
val weatherResponse = response.body()
weatherResponse?.let { updateUI(it) }
} else {
// Handle error
textViewDescription.text = "Error fetching weather data"
}
}
override fun onFailure(call: Call, t: Throwable) {
swipeRefreshLayout.isRefreshing = false // Stop the refreshing animation
textViewDescription.text = "Network error: ${t.message}"
}
})
}
}
}
private fun updateUI(weatherResponse: WeatherResponse) {
textViewCity.text = weatherResponse.name
textViewTemperature.text = "Temperature: ${weatherResponse.main.temp}°C"
textViewDescription.text = "Description: ${weatherResponse.weather[0].description}"
val iconCode = weatherResponse.weather[0].icon
val iconUrl = "https://openweathermap.org/img/w/$iconCode.png" // Correct image URL
Glide.with(this@MainActivity)
.load(iconUrl)
.into(imageViewWeatherIcon)
}
}
Conclusion
Building a weather app using XML for the UI in Android demonstrates fundamental UI design principles and how to integrate networking libraries. While XML might not be the cutting-edge approach compared to Jetpack Compose, it’s a valuable skill for maintaining existing applications and understanding the underlying structure of Android UI development. This guide provides a solid foundation for fetching data from an API and displaying it dynamically using XML-based layouts.