Jetpack Compose Flight Search UI: A Step-by-Step Guide
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!