Showing MaterialTimePicker: Using SupportFragmentManager in Android Compose
Introduction
Recently, I wanted to create a time picker in an Android app following Material Design. Google provides a MaterialTimePicker from their Material Components library to do this.
However, you need some extra steps to show the MaterialTimePicker. You need to get a FragmentManager and change your theme style.
Here’s each step to show the picker in a new Android Compose app:
Setup
Create a default Compose project in Android Studio.
First add the dependency to your app-level build.gradle:
implementation 'com.google.android.material:material:1.3.0'
Then put a picker in setContent in MainActivity. If you try to show it using MaterialTimePicker.Builder like below, you will get 2 errors:
package com.example.examplematerialtimepicker
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.examplematerialtimepicker.ui.theme.ExampleMaterialTimePickerTheme
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ExampleMaterialTimePickerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Build TimePicker and show
MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_12H)
.setHour(9)
.setMinute(30)
.setTitleText("Time of the meeting")
.build().apply {
addOnCancelListener { /* on cancel */ }
addOnDismissListener { /* on dismiss */ }
addOnPositiveButtonClickListener { "Selected time: $hour : $minute" }
addOnNegativeButtonClickListener { /* on negative button click */ }
}
.show(supportFragmentManager, FRAGMENT_TAG)
}
}
}
}
}
supportFragmentManager and FRAGMENT_TAG are unresolved references, because they’re not defined.
FragmentManager
The picker expects supportFragmentManager to be a FragmentManager. Why is FragmentManager required in the first place?
It turns out MaterialTimePicker extends DialogFragment.
Here’s a rough UML class diagram to illustrate that:
The show() method comes from DialogFragment:
/**
Params:
manager – The FragmentManager this fragment will be added to.
tag – The tag for this fragment, as per FragmentTransaction.add.
*/
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
The tag is just a string for the picker. You can define it outside the class:
private const val FRAGMENT_TAG = "time_picker_frag"
class MainActivity : ComponentActivity() {
// ...
}
Here we come to the next problem: By default, Android’s MainActivity extends ComponentActivity. It does not have a FragmentManager.
Solution: AppCompatActivity
One solution is to change MainActivity to extend AppCompatActivity. Then use its supportFragmentManager property.
This works because AppCompatActivity extends FragmentActivity which extends ComponentActivity. The relationship looks like this:
Extend AppCompatActivity instead. Now the app can build, but by default it will crash with a different error when you run it:
package com.example.examplematerialtimepicker
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.examplematerialtimepicker.ui.theme.ExampleMaterialTimePickerTheme
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
private const val FRAGMENT_TAG = "time_picker_frag"
// Extend AppCompatActivity instead of ComponentActivity.
// AppCompatActivity extends FragmentActivity which extends ComponentActivity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ExampleMaterialTimePickerTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_12H)
.setHour(9)
.setMinute(30)
.setTitleText("Time of the meeting")
.build().apply {
addOnCancelListener { /* on cancel */ }
addOnDismissListener { /* on dismiss */ }
addOnPositiveButtonClickListener { "Selected time: $hour : $minute" }
addOnNegativeButtonClickListener { /* on negative button click */ }
}
.show(supportFragmentManager, FRAGMENT_TAG)
}
}
}
}
}
The new error is related to the app theme:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.examplematerialtimepicker/com.example.examplematerialtimepicker.MainActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
Solution: Change Theme
The theme is defined in AndroidManifest.xml. You should have a line like this in the application tag:
android:theme="@style/ExampleMaterialTimePickerTheme"
The style comes from your app folder’s res/values/themes.xml. The default file looks something like this:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ExampleMaterialTimePickerTheme" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
The problem here is the parent theme android:Theme.Material.Light.NoActionBar. It’s a general theme that doesn’t include all the attributes required by Material Components.
The solution is you should use a specific Material Components theme, for example Theme.MaterialComponents.Light:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ExampleMaterialTimePickerTheme" parent="Theme.MaterialComponents.Light" />
</resources>
Summary
TL;DR change your Activity to AppCompatActivity, get the supportFragmentManager, and make sure your parent theme works with the Material Components library.
Here is the complete code:
MainActivity.kt
package com.example.examplematerialtimepicker
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.examplematerialtimepicker.ui.theme.ExampleMaterialTimePickerTheme
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
private const val FRAGMENT_TAG = "time_picker_frag"
// Extend AppCompatActivity instead of ComponentActivity.
// AppCompatActivity extends FragmentActivity which extends ComponentActivity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ExampleMaterialTimePickerTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_12H)
.setHour(9)
.setMinute(30)
.setTitleText("Time of the meeting")
.build().apply {
addOnCancelListener { /* on cancel */ }
addOnDismissListener { /* on dismiss */ }
addOnPositiveButtonClickListener { "Selected time: $hour : $minute" }
addOnNegativeButtonClickListener { /* on negative button click */ }
}
.show(supportFragmentManager, FRAGMENT_TAG)
}
}
}
}
}
app/res/values/themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ExampleMaterialTimePickerTheme" parent="Theme.MaterialComponents.Light" />
</resources>
Hope this was useful. Happy coding.