Building Weather Apps with XML UI

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.