Creating A Reusable Appointment Card View With ViewModel

by Alex Johnson 57 views

In this article, we'll walk through the process of creating a reusable appointment card component in an Android application, focusing on implementing an AppointmentCardView and mapping data using a ViewModel. This is a common UI pattern in applications that display lists of appointments or events. By creating a reusable component, we can ensure consistency across our application and reduce code duplication. This article will cover the key steps involved, from defining the ViewModel to implementing the composable UI and handling user interactions. Let's dive in and explore how to build an efficient and visually appealing appointment card!

Understanding the Requirements

Before we start coding, let's clearly understand the requirements for our AppointmentCardView. The main goal is to create a reusable component that can be used in various screens, such as a list of appointments or detail views. The card should display essential information about the appointment, including:

  • Title: The main title or subject of the appointment.
  • Subtitle: Additional information or a brief description of the appointment.
  • Date/Time: The date and time of the appointment.
  • Status Chip: A visual indicator of the appointment's status (e.g., scheduled, confirmed, canceled).

Additionally, the card should be interactive, allowing users to tap on it to open the appointment details screen. It's also important that the design of the card matches the Figma design specifications provided, ensuring a consistent look and feel throughout the application.

Key Tasks

To achieve our goal, we'll need to complete the following tasks:

  1. Implement AppointmentCardViewModel: This ViewModel will be responsible for transforming the appointment data into a UI-friendly model. It acts as an intermediary between the data layer and the UI, ensuring that the View receives only the data it needs.
  2. Implement AppointmentCardView() Composable: This is the actual UI component that will display the appointment information. It will use Jetpack Compose to create a declarative UI, making it easier to build and maintain.
  3. Handle Click to Open Appointment Detail: We need to add a click listener to the card, so when a user taps on it, the application navigates to the appointment details screen.

Acceptance Criteria

To ensure that our component meets the requirements, we'll use the following acceptance criteria:

  • The card should be used by AppointmentListScreen and potentially other screens.
  • Different statuses should be visually distinguishable using the status chip.
  • The card layout should match the Figma design.

Implementing the AppointmentCardViewModel

The first step in creating our reusable appointment card is to implement the AppointmentCardViewModel. This ViewModel will be responsible for taking the raw appointment data and transforming it into a format that is easy for the UI to display. This often involves converting dates and times into human-readable strings, determining the appropriate status chip color, and handling any other UI-specific logic. Let's delve deeper into how we can create this ViewModel effectively.

Defining the UI Model

Before we implement the ViewModel, let's define the UI model that will hold the data for our card. This model should contain all the information needed to display the appointment, such as the title, subtitle, date/time, and status. Here’s an example of what the UI model might look like:

data class AppointmentCardUiModel(
    val id: String,
    val title: String,
    val subtitle: String,
    val dateTime: String,
    val status: AppointmentStatus
)

In this model:

  • id is a unique identifier for the appointment.
  • title is the main title of the appointment.
  • subtitle provides additional details.
  • dateTime is a formatted string representing the date and time of the appointment.
  • status is an enum representing the status of the appointment (e.g., Scheduled, Confirmed, Canceled).

Creating the ViewModel

Now that we have our UI model, we can create the AppointmentCardViewModel. This ViewModel will take an Appointment object as input and map it to our AppointmentCardUiModel. Here’s how we can implement it:

import androidx.lifecycle.ViewModel
import java.time.format.DateTimeFormatter

class AppointmentCardViewModel(private val appointment: Appointment) : ViewModel() {

    fun mapToUiModel(): AppointmentCardUiModel {
        val formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy hh:mm a")
        val formattedDateTime = appointment.dateTime.format(formatter)

        return AppointmentCardUiModel(
            id = appointment.id,
            title = appointment.title,
            subtitle = appointment.description,
            dateTime = formattedDateTime,
            status = appointment.status
        )
    }
}

In this ViewModel:

  • We take an Appointment object as a constructor parameter.
  • The mapToUiModel() function is responsible for mapping the Appointment to AppointmentCardUiModel.
  • We use DateTimeFormatter to format the date and time into a human-readable string.

Using a Mapper

Alternatively, we can use a mapper class to handle the transformation logic. This can make our code more modular and easier to test. Here’s an example of how we can use a mapper:

class AppointmentCardUiModelMapper {

    fun map(appointment: Appointment): AppointmentCardUiModel {
        val formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy hh:mm a")
        val formattedDateTime = appointment.dateTime.format(formatter)

        return AppointmentCardUiModel(
            id = appointment.id,
            title = appointment.title,
            subtitle = appointment.description,
            dateTime = formattedDateTime,
            status = appointment.status
        )
    }
}

class AppointmentCardViewModel(
    private val appointment: Appointment,
    private val mapper: AppointmentCardUiModelMapper
) : ViewModel() {

    fun getUiModel(): AppointmentCardUiModel {
        return mapper.map(appointment)
    }
}

Using a mapper makes the ViewModel cleaner and more focused on its primary responsibility: managing the UI state. The mapping logic is encapsulated in the AppointmentCardUiModelMapper, making it easier to test and maintain.

Implementing the AppointmentCardView() Composable

Now that we have our ViewModel and UI model, we can implement the AppointmentCardView() composable. This is where we define the UI layout for our appointment card using Jetpack Compose. Compose allows us to build UI in a declarative way, making our code more readable and maintainable. Let's explore how to create this composable and incorporate the necessary elements.

Setting Up the Composable

The AppointmentCardView() composable will take an AppointmentCardUiModel as input and display the information in a card layout. We'll use Compose's built-in components like Card, Column, Text, and Chip to create the UI. Here’s a basic structure for our composable:

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.* // Correct import for MaterialTheme and other components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun AppointmentCardView(uiModel: AppointmentCardUiModel, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onClick() }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = uiModel.title, style = MaterialTheme.typography.h6)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = uiModel.subtitle, style = MaterialTheme.typography.body2)
            Spacer(modifier = Modifier.height(8.dp))
            Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
                Text(text = uiModel.dateTime, style = MaterialTheme.typography.caption)
                Chip(label = { Text(text = uiModel.status.name) })
            }
        }
    }
}

In this composable:

  • We use a Card as the main container, providing a visually distinct element.
  • We add a Modifier to the Card to make it fill the available width, add padding, and make it clickable.
  • Inside the Card, we use a Column to arrange the content vertically.
  • We use Text components to display the title, subtitle, and date/time.
  • We use a Chip to display the status of the appointment.
  • The onClick lambda allows us to handle clicks on the card.

Displaying Different Statuses

To visually distinguish different statuses, we can modify the Chip component to display different colors based on the AppointmentStatus. Here’s how we can do it:

import androidx.compose.material.contentColorFor
import androidx.compose.material.primarySurface
import androidx.compose.ui.graphics.Color

@Composable
fun AppointmentCardView(uiModel: AppointmentCardUiModel, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onClick() }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = uiModel.title, style = MaterialTheme.typography.h6)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = uiModel.subtitle, style = MaterialTheme.typography.body2)
            Spacer(modifier = Modifier.height(8.dp))
            Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
                Text(text = uiModel.dateTime, style = MaterialTheme.typography.caption)
                val statusColor = when (uiModel.status) {
                    AppointmentStatus.SCHEDULED -> Color.Yellow
                    AppointmentStatus.CONFIRMED -> Color.Green
                    AppointmentStatus.CANCELED -> Color.Red
                }
                Chip(
                  label = { Text(text = uiModel.status.name) },
                  colors = ChipDefaults.chipColors(backgroundColor = statusColor, contentColor = MaterialTheme.colors.contentColorFor(MaterialTheme.colors.primarySurface))
                )
            }
        }
    }
}

In this updated version:

  • We define a statusColor based on the AppointmentStatus.
  • We pass the statusColor to the Chip component to change its background color.

Handling Click Events

To handle click events, we need to pass an onClick lambda to the AppointmentCardView() composable. This lambda will be invoked when the user clicks on the card. Here’s how we can modify our composable:

@Composable
fun AppointmentCardView(uiModel: AppointmentCardUiModel, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
            .clickable { onClick() }
    ) { // The rest of the composable remains the same }
}

Now, when the user clicks on the card, the onClick lambda will be invoked, allowing us to navigate to the appointment details screen or perform any other action.

Using the AppointmentCardView in AppointmentListScreen

With our AppointmentCardView implemented, we can now use it in the AppointmentListScreen. This screen will display a list of appointments, each represented by an AppointmentCardView. Using the card in a list demonstrates its reusability and ensures that it meets our acceptance criteria. Let's see how we can integrate the AppointmentCardView into a list screen using Jetpack Compose.

Setting Up the List Screen

First, let's create the AppointmentListScreen composable. This screen will fetch the list of appointments and display them using a LazyColumn, which is an efficient way to display large lists in Compose. Here’s a basic structure for our list screen:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun AppointmentListScreen(onAppointmentClick: (String) -> Unit) {
    val viewModel: AppointmentListViewModel = viewModel()
    val appointments by viewModel.appointments.collectAsState(initial = emptyList())

    Column {
        LazyColumn(contentPadding = PaddingValues(16.dp)) {
            items(appointments) {
                AppointmentCardView(uiModel = it) { // Modified AppointmentCardView Usage
                    onAppointmentClick(it.id)
                }
            }
        }
    }
}

In this composable:

  • We use viewModel() to get an instance of AppointmentListViewModel.
  • We collect the list of appointments from the ViewModel using collectAsState().
  • We use a LazyColumn to display the list of appointments.
  • We iterate over the list of appointments and create an AppointmentCardView for each appointment.

Creating the AppointmentListViewModel

Now, let's create the AppointmentListViewModel. This ViewModel will be responsible for fetching the list of appointments and exposing it as a StateFlow. Here’s how we can implement it:

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class AppointmentListViewModel : ViewModel() {

    private val _appointments = MutableStateFlow<List<AppointmentCardUiModel>>(emptyList())
    val appointments: StateFlow<List<AppointmentCardUiModel>> = _appointments

    init {
        // Fetch appointments from your data source and map them to UI models
        val appointments = getAppointmentsFromDataSource().map { appointment ->
            AppointmentCardViewModel(appointment).mapToUiModel()
        }
        _appointments.value = appointments
    }

    private fun getAppointmentsFromDataSource(): List<Appointment> {
        // Replace with your actual data fetching logic
        return listOf(
            Appointment("1", "Doctor's Appointment", "Checkup with Dr. Smith", LocalDateTime.now(), AppointmentStatus.SCHEDULED),
            Appointment("2", "Dentist Appointment", "Teeth cleaning", LocalDateTime.now().plusDays(7), AppointmentStatus.CONFIRMED),
            Appointment("3", "Therapy Session", "Session with therapist", LocalDateTime.now().plusDays(14), AppointmentStatus.CANCELED)
        )
    }
}

In this ViewModel:

  • We use a MutableStateFlow to hold the list of appointments.
  • We expose the StateFlow as appointments.
  • In the init block, we fetch the appointments from a data source (replace with your actual data fetching logic).
  • We map each Appointment to an AppointmentCardUiModel using the AppointmentCardViewModel.

Handling Click Events in the List Screen

To handle click events in the list screen, we need to pass a lambda to the AppointmentListScreen that will be invoked when an appointment card is clicked. This lambda will take the appointment ID as input and navigate to the appointment details screen. Here’s how we can modify our AppointmentListScreen composable:

@Composable
fun AppointmentListScreen(onAppointmentClick: (String) -> Unit) {
    // The rest of the composable remains the same
    LazyColumn(contentPadding = PaddingValues(16.dp)) {
        items(appointments) {
            AppointmentCardView(uiModel = it) { // Modified AppointmentCardView Usage
                onAppointmentClick(it.id)
            }
        }
    }
}

In this updated version:

  • We pass a lambda onAppointmentClick to the AppointmentListScreen.
  • Inside the AppointmentCardView, we invoke this lambda with the appointment ID when the card is clicked.

Conclusion

In this article, we've walked through the process of creating a reusable AppointmentCardView using Jetpack Compose and a ViewModel. We started by defining the requirements and acceptance criteria for our component. Then, we implemented the AppointmentCardViewModel to transform the appointment data into a UI-friendly model. We also created the AppointmentCardView() composable, which displays the appointment information in a card layout, and handled click events to open the appointment details screen. Finally, we integrated the AppointmentCardView into the AppointmentListScreen, demonstrating its reusability and ensuring that it meets our acceptance criteria.

By following these steps, you can create a reusable and visually appealing appointment card component that enhances the user experience in your Android application. This approach not only promotes code reuse but also ensures consistency across different parts of your application.

For further reading on related topics, consider exploring articles and documentation on Jetpack Compose and ViewModels.