Jetpack Compose Flight Search UI: A Step-by-Step Guide

by RICHARD 55 views

Hey there, fellow developers! Are you ready to dive into the exciting world of Jetpack Compose and build a killer UI for a flight search application? In this guide, we'll walk through the process of creating a solid foundation for your UI, using Jetpack Compose, ViewModels, and some handy local test data. Let's get started!

Setting the Stage: Project Setup and Dependencies

First things first, let's set up our project and get those dependencies in place. Make sure you have Android Studio installed and that you're familiar with creating a new Android project. Once you've got your project structure ready, you'll need to add the necessary dependencies in your build.gradle file (Module: app).

implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
implementation "androidx.activity:activity-compose:1.8.2"
implementation platform("androidx.compose:compose-bom:2024.04.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:2024.04.00")
androidTestImplementation "androidx.compose.ui:ui-test-junit4"
debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
// Optional - Integration with activities
implementation "androidx.activity:activity-compose:1.8.2"

Make sure to sync your project after adding these dependencies. Now, let's create the basic structure for our UI. We'll start by defining a few composables to represent the main components of our flight search screen.

This setup includes the core Jetpack Compose libraries, along with dependencies for the ViewModel, which will be crucial for managing the UI state. Now that we have the project structured and dependencies set up, it is time to create the skeleton of the UI using Jetpack Compose.

Building the UI Skeleton with Jetpack Compose

Alright, time to get our hands dirty with some Jetpack Compose code! We'll break down our UI into several composable functions, each responsible for a specific part of the flight search screen. This modular approach will make our code more organized and easier to maintain. Here’s a basic structure we can follow.

1. FlightSearchScreen

This will be our main composable, acting as the entry point for the entire flight search UI. It will orchestrate the other composables and manage the overall layout.

@Composable
fun FlightSearchScreen(viewModel: FlightSearchViewModel = viewModel()) {
    // Use a Column to arrange elements vertically
    Column(modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Add Search Input Fields (e.g., origin, destination, dates)
        SearchInputFields(viewModel)
        Spacer(modifier = Modifier.height(16.dp))
        // Display Flight Results (if any)
        FlightResults(viewModel)
    }
}

2. SearchInputFields

This composable will hold the input fields for the user to enter their search criteria. We’ll include fields for origin, destination, departure date, and return date (if applicable). These input fields could be simple TextField composables. It's worth noting that the data we get from the user are stored in the FlightSearchViewModel as state.

@Composable
fun SearchInputFields(viewModel: FlightSearchViewModel) {
    Column {
        // Origin Input Field
        OutlinedTextField(
            value = viewModel.origin, // Assuming origin is a state in ViewModel
            onValueChange = { viewModel.origin = it },
            label = { Text("Origin") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        )

        // Destination Input Field
        OutlinedTextField(
            value = viewModel.destination, // Destination is a state in ViewModel
            onValueChange = { viewModel.destination = it },
            label = { Text("Destination") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        )

        // Departure Date Input Field (You might use a date picker here)
        OutlinedTextField(
            value = viewModel.departureDate, // DepartureDate is a state in ViewModel
            onValueChange = { viewModel.departureDate = it },
            label = { Text("Departure Date") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        )

        // Optional: Return Date Input Field
        OutlinedTextField(
            value = viewModel.returnDate, // ReturnDate is a state in ViewModel
            onValueChange = { viewModel.returnDate = it },
            label = { Text("Return Date") },
            modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        )

        // Search Button
        Button(onClick = { viewModel.searchFlights() },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Search Flights")
        }
    }
}

3. FlightResults

This composable will display the flight search results. Initially, it might show a loading indicator while the search is in progress. Once the results are available, it will display a list of flights. We can display it by using the LazyColumn composable.

@Composable
fun FlightResults(viewModel: FlightSearchViewModel) {
    if (viewModel.isLoading) {
        // Show a loading indicator
        CircularProgressIndicator()
    } else if (viewModel.flights.isNotEmpty()) {
        // Display the flight results in a LazyColumn
        LazyColumn {
            items(viewModel.flights) {
                FlightItem(flight = it)
            }
        }
    } else if (viewModel.searchPerformed) {
        // Show a message indicating no flights found
        Text("No flights found.")
    }
}

4. FlightItem

This composable will represent a single flight in the search results. It will display relevant information such as the airline, departure and arrival times, origin, and destination airports, and price.

@Composable
fun FlightItem(flight: Flight) {
    Card(modifier = Modifier
        .fillMaxWidth()
        .padding(8.dp)) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Airline: ${flight.airline}")
            Text(text = "From: ${flight.origin} to ${flight.destination}")
            Text(text = "Departure: ${flight.departureTime}")
            Text(text = "Arrival: ${flight.arrivalTime}")
            Text(text = "Price: ${flight.price}")
        }
    }
}

Leveraging ViewModels for State Management

ViewModels are key in Android development, especially when using Jetpack Compose. They are responsible for holding and managing the UI-related data. This allows us to survive configuration changes (like screen rotations) without losing the user's input or the application's state. For our flight search application, we'll create a FlightSearchViewModel to handle the data.

Here's the structure of our FlightSearchViewModel.

import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel

data class Flight(val airline: String, val origin: String, val destination: String, val departureTime: String, val arrivalTime: String, val price: String)

class FlightSearchViewModel : ViewModel() {
    // State for the input fields
    var origin = mutableStateOf("")
    var destination = mutableStateOf("")
    var departureDate = mutableStateOf("")
    var returnDate = mutableStateOf("")

    // State to manage the loading state
    var isLoading = mutableStateOf(false)

    // State to hold the flight search results
    var flights = mutableStateListOf<Flight>()

    // State to track if a search has been performed
    var searchPerformed = mutableStateOf(false)

    fun searchFlights() {
        // Reset the flights list and set isLoading to true
        flights.clear()
        isLoading.value = true
        searchPerformed.value = true

        // Simulate a network request (replace with your actual API call)
        viewModelScope.launch {
            delay(2000) // Simulate a 2-second delay for the search

            // Replace this with your actual flight search logic
            val testFlights = listOf(
                Flight("United", "JFK", "LAX", "10:00", "13:00", "$300"),
                Flight("Delta", "JFK", "SFO", "11:00", "14:00", "$350")
            )
            flights.addAll(testFlights)
            isLoading.value = false
        }
    }
}

In this example, we are using mutableStateOf to hold the state of the input fields, loading indicators, and the list of flights. We also use the viewModelScope to launch a coroutine to simulate a network request. This is where you'd normally call your API to fetch the flight data.

The FlightSearchViewModel is responsible for managing the state of our UI components. It will hold the user's input (origin, destination, dates), the loading state, and the flight search results. This separation of concerns makes our code cleaner and easier to test. The ViewModel will also contain the logic for initiating the flight search, handling the API calls, and updating the UI with the results.

Adding Local Test Data to the Mix

To ensure our UI works properly, we'll use some local test data. This will allow us to develop and test our UI without relying on a live API. Here’s how we can implement it, by injecting some dummy flight data into the FlightSearchViewModel.

Inside the FlightSearchViewModel, we'll add some mock flight data to display in the results. When the user clicks the search button, we'll make our UI display our dummy data.

This approach allows for rapid prototyping and UI testing. We can easily simulate different scenarios and data sets to ensure that our UI components are rendering correctly. Remember to replace this with actual API calls later.

Putting it all Together: The Main Activity

Finally, let's connect all of these composables in our MainActivity. We'll use the setContent block to set up our UI and make sure our FlightSearchScreen is displayed. This is the entry point where the app starts and it's where the UI is initialized.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.tooling.preview.Preview

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                // A surface container using the 'background' color from the theme
                Surface {
                    FlightSearchScreen()
                }
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MaterialTheme {
        FlightSearchScreen()
    }
}

This is the point where the magic happens! In the onCreate function, we call setContent to set the content of our activity. Inside the setContent block, we use our FlightSearchScreen composable. The preview allows you to see what the UI looks like without having to run the app on a device or emulator.

Enhancements and Next Steps

Here are some ideas to take your flight search application to the next level:

  • Add input validation: Ensure that the user enters valid data in the input fields.
  • Implement a date picker: Allow users to select departure and return dates easily.
  • Integrate with a real API: Connect your app to a flight search API to get real-time flight data.
  • Add error handling: Display error messages to the user if something goes wrong.
  • Implement navigation: Allow users to navigate to a flight details screen when they select a flight.
  • Improve the UI: Add more features, better styling, animations, and overall design.

Conclusion

And there you have it! We've successfully created a basic UI skeleton for a flight search application using Jetpack Compose, ViewModels, and local test data. From here, the possibilities are endless. So, go ahead, experiment, and make your app shine. Happy coding, and feel free to ask questions and share your progress!