Custom WeekView with Jetpack Compose

Custom WeekView with Jetpack Compose

In this blog post, we will learn how to create a custom week view using Jetpack Compose in Android. Jetpack Compose is a modern toolkit for building native Android UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs.

Part 1: Setting up the Project

Before we start, make sure you have the latest version of Android Studio installed.

Step 1: Create a new Android project

Open Android Studio and create a new project. Choose the "Empty Compose Activity" template. Name the project "CustomWeekView".

Step 2: Add dependencies

In the build.gradle file of your app module, add the following dependencies:

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    implementation("androidx.activity:activity-compose:1.7.0")
    implementation(platform("androidx.compose:compose-bom:2023.03.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:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    implementation("com.jakewharton.threetenabp:threetenabp:1.3.1")
}

In project level build.gradle

plugins {
    id("com.android.application") version "8.1.0" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
}

Structure:

Step 3: Initialize ThreeTenABP

We will be using the ThreeTenABP library for date and time handling. Initialize it in your custom Application class:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        AndroidThreeTen.init(this)
    }
}

And declare it in your AndroidManifest.xml:

<application
    android:allowBackup="true"
    android:name=".MyApp"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.LearnAndroidCompose"
    tools:targetApi="31">
    <activity
        android:name=".MainActivity"
        android:exported="true"
        android:label="@string/app_name"
        android:theme="@style/Theme.LearnAndroidCompose">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

Part 2: Building the WeekView

In this part, we will start building our custom week view. We will create a CalendarView composable function that will display the week view.

Step 1: Create the MainActivity

In your MainActivity, set the content to our CalendarView composable. We will pass a selectedDate state to it, which will be updated when a date is clicked.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LearnAndroidComposeTheme {
                var selectedDate = remember { mutableStateOf(LocalDate.now()) }
                // A surface container using the 'background' color from the theme
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    CalendarView(selectedDate = selectedDate)
                }
            }
        }
    }
}

Step 2: Create the CalendarView Composable

In your CalendarView composable, we will display the selected date and the week view. We will use a HorizontalPager to allow the user to swipe through weeks.

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun CalendarView(selectedDate: MutableState<LocalDate>) {
    val today = LocalDate.now()
    Log.e("TAG", "CalendarView: $today")

    val weeks = getWeeksFromToday(today, 52)

    val pagerState = rememberPagerState(
        initialPage = 0,
        initialPageOffsetFraction = 0f
    )

    Column(modifier = Modifier.fillMaxWidth()) {
        // Display the selected date and week view here...
    }
}

Step 3: Display the Selected Date

Inside your CalendarView composable, add a Text composable to display the selected date. We will format the date based on whether it is today, tomorrow, or another day.

val displayText = when {
    selectedDate.value == today -> "Today"
    selectedDate.value == today.plusDays(1) -> "Tomorrow"
    else -> {
        val dateFormat = if (selectedDate.value.year == today.year) {
            DateTimeFormatter.ofPattern("d MMM", Locale.getDefault())
        } else {
            DateTimeFormatter.ofPattern("d MMM yyyy", Locale.getDefault())
        }
        selectedDate.value.format(dateFormat)
    }
}

Text(
    text = displayText,
    style = MaterialTheme.typography.headlineLarge.copy( fontWeight = FontWeight.Bold),
    color = MaterialTheme.colorScheme.primary,
    modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
)

Step 4: Display the Week View

Next, add a HorizontalPager to display the week view. Inside the HorizontalPager, create a Row for each week and a Box for each date. When a date is clicked, update the selectedDate state.

HorizontalPager(    pageCount = weeks.size,
    state = pagerState, modifier = Modifier.fillMaxWidth()) { page ->
    val weekDates = weeks[page]

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp)  // Add horizontal padding
    ) {
        weekDates.forEach { date ->
            val interactionSource = remember { MutableInteractionSource() }
            Box(
                modifier = Modifier
                    .weight(1f)
                    .height(48.dp)  // Add a fixed height
                    .clip(CircleShape)
                    .clickable(
                    ) { selectedDate.value = date },

                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = date.dayOfMonth.toString(),
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

Part 3: Enhancing the WeekView

In this part, we will enhance our week view by adding day names and handling date selection.

Step 1: Display Day Names

Before the HorizontalPager in your CalendarView composable, add a Row to display the day names.

Row(
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 8.dp)
) {
    listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat").forEach { day ->
        Text(
            text = day,
            modifier = Modifier.weight(1f),
            textAlign = TextAlign.Center
        )
    }
}
Spacer(modifier = Modifier.height(8.dp))

Step 2: Highlight Selected Date

In the Box for each date, change the background color based on whether the date is the selected date.

Box(
    modifier = Modifier
        .weight(1f)
        .height(48.dp)  // Add a fixed height
        .clip(CircleShape)
        .clickable(
        ) { selectedDate.value = date }
        .background(if (date == selectedDate.value) MaterialTheme.colorScheme.primaryContainer else Color.Transparent),

    contentAlignment = Alignment.Center
) {
    Text(
        text = date.dayOfMonth.toString(),
        textAlign = TextAlign.Center
    )
}

Step 3: Generate Weeks

Create a function getWeeksFromToday to generate a list of weeks from today. Each week is a list of dates.

fun getWeeksFromToday(today: LocalDate, weeksCount: Int): List<List<LocalDate>> {
    val weeks = mutableListOf<List<LocalDate>>()
    var currentStartOfWeek = today
    while (currentStartOfWeek.dayOfWeek != DayOfWeek.SUNDAY) {
        currentStartOfWeek = currentStartOfWeek.minusDays(1)
    }
    repeat(weeksCount) {
        val week = (0 until 7).map { currentStartOfWeek.plusDays(it.toLong()) }
        weeks.add(week)
        currentStartOfWeek = currentStartOfWeek.plusWeeks(1)
    }
    weeks.forEach { week ->
        Log.e("TAG", "Week: ${week.joinToString(", ")}")
    }
    return weeks
}

Call this function in your CalendarView composable to generate the weeks.

val weeks = getWeeksFromToday(today, 52)

Step 4: Display Week Name

Below the displayed date, add a Text composable to display the name of the week of the selected date.

val weekName = selectedDate.value.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault())
Text(
    text = weekName,
    color = MaterialTheme.colorScheme.secondary,
    style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Light),
    modifier = Modifier.padding(start = 20.dp, bottom = 16.dp)
)

Complete Code:

MainActivity.class:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LearnAndroidComposeTheme {
                var selectedDate = remember { mutableStateOf(LocalDate.now()) }
                // A surface container using the 'background' color from the theme
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    CalendarView(selectedDate = selectedDate)

                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun CalendarView(selectedDate: MutableState<LocalDate>) {
    val today = LocalDate.now()
    Log.e("TAG", "CalendarView: $today")

    val weeks = getWeeksFromToday(today, 52)

    val pagerState = rememberPagerState(
        initialPage = 0,
        initialPageOffsetFraction = 0f
    )

    Column(modifier = Modifier.fillMaxWidth()) {
        val displayText = when {
            selectedDate.value == today -> "Today"
            selectedDate.value == today.plusDays(1) -> "Tomorrow"
            else -> {
                val dateFormat = if (selectedDate.value.year == today.year) {
                    DateTimeFormatter.ofPattern("d MMM", Locale.getDefault())
                } else {
                    DateTimeFormatter.ofPattern("d MMM yyyy", Locale.getDefault())
                }
                selectedDate.value.format(dateFormat)
            }
        }

        Text(
            text = displayText,
            style = MaterialTheme.typography.headlineLarge.copy( fontWeight = FontWeight.Bold),
            color = MaterialTheme.colorScheme.primary,
            modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
        )

        val weekName = selectedDate.value.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault())
        Text(
            text = weekName,
            color = MaterialTheme.colorScheme.secondary,
            style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Light),
            modifier = Modifier.padding(start = 20.dp, bottom = 16.dp)
        )

        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 8.dp)
        ) {
            listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat").forEach { day ->
                Text(
                    text = day,
                    modifier = Modifier.weight(1f),
                    textAlign = TextAlign.Center
                )
            }
        }
        Spacer(modifier = Modifier.height(8.dp))

        HorizontalPager(    pageCount = weeks.size,
            state = pagerState, modifier = Modifier.fillMaxWidth()) { page ->
            val weekDates = weeks[page]

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 8.dp)  // Add horizontal padding
            ) {
                weekDates.forEach { date ->
                    val interactionSource = remember { MutableInteractionSource() }
                    Box(
                        modifier = Modifier
                            .weight(1f)
                            .height(48.dp)  // Add a fixed height
                            .clip(CircleShape)
                            .clickable(
                            ) { selectedDate.value = date }
                            .background(if (date == selectedDate.value) MaterialTheme.colorScheme.primaryContainer else Color.Transparent),

                        contentAlignment = Alignment.Center
                    ) {
                        Text(
                            text = date.dayOfMonth.toString(),
                            textAlign = TextAlign.Center
                        )
                    }
                }
            }
        }
    }
}

fun getWeeksFromToday(today: LocalDate, weeksCount: Int): List<List<LocalDate>> {
    val weeks = mutableListOf<List<LocalDate>>()
    var currentStartOfWeek = today
    while (currentStartOfWeek.dayOfWeek != DayOfWeek.SUNDAY) {
        currentStartOfWeek = currentStartOfWeek.minusDays(1)
    }
    repeat(weeksCount) {
        val week = (0 until 7).map { currentStartOfWeek.plusDays(it.toLong()) }
        weeks.add(week)
        currentStartOfWeek = currentStartOfWeek.plusWeeks(1)
    }
    weeks.forEach { week ->
        Log.e("TAG", "Week: ${week.joinToString(", ")}")
    }
    return weeks
}

Step 4: Testing the WeekView

Now, run your app and test the week view. You should be able to swipe through weeks and select a date. The displayed date and week name should update accordingly.

That's it! You have created a custom week view using Jetpack Compose. This is a basic implementation and there are many ways you can enhance it. For example, you can add animations when a date is selected, or you can add events to the dates.

I hope you found this tutorial helpful. Happy coding!

Did you find this article valuable?

Support saurabh jadhav by becoming a sponsor. Any amount is appreciated!