SeekBar in Android: Kotlin, XML and Compose Slider Examples

A SeekBar lets the user pick a value from a range by dragging a thumb along a track — think volume, brightness or text size. Here is the short version of this whole guide: in classic View-based layouts you add a <SeekBar> in XML and read values with setOnSeekBarChangeListener; in a Material 3 app you should usually reach for com.google.android.material.slider.Slider instead, which supports float values, a real minimum and value labels out of the box; and in Jetpack Compose the equivalent is the Slider composable driven by state. All three, with complete Kotlin code, are below.
In a hurry? Jump to: classic SeekBar in XML + Kotlin · the listener callbacks explained · Material 3 Slider (recommended) · Jetpack Compose Slider · which one to use
What Is a SeekBar?
android.widget.SeekBar extends ProgressBar and adds user interaction: the user drags the thumb, your app reacts to the changing progress value. It has been in Android since API 1, it still works fine, and it is what the queries "seekbar android example" are really about — so we start there. But it is worth knowing up front that Google's Material Components library ships a more capable replacement, Slider, and that Compose apps do not use SeekBar at all. This tutorial covers all three so you can pick the right one for your project.
Classic SeekBar: XML Layout + Kotlin
The example app below changes the size of a TextView as you drag the SeekBar. Create an Empty Views Activity project in Android Studio with Kotlin selected as the language.
Step 1: The layout
Open activity_main.xml and add a TextView and a SeekBar:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello SeekBar!"
android:textSize="20sp"
android:layout_marginTop="50dp"
android:layout_marginStart="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:min="12"
android:max="48"
android:progress="20"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Key attributes: android:max sets the upper bound, android:progress the starting value, and android:min sets a lower bound — note that android:min only works on API 26 (Android 8.0) and higher; on older versions the range always starts at 0 and you offset the value in code.
Step 2: The Kotlin code
In MainActivity.kt, attach an OnSeekBarChangeListener:
package com.example.seekbarexample
import android.os.Bundle
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val seekBar = findViewById<SeekBar>(R.id.seekBar)
val textView = findViewById<TextView>(R.id.textView)
// Apply the initial value
textView.textSize = seekBar.progress.toFloat()
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
// Fires continuously while the thumb moves
textView.textSize = progress.toFloat()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
// User grabbed the thumb
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
// User let go — good place for one-shot work
Toast.makeText(this@MainActivity, "Text size: ${seekBar.progress}sp", Toast.LENGTH_SHORT).show()
}
})
}
}
Run the app on an emulator or device and drag the thumb — the text resizes live, and a Toast shows the final value when you release. If your project uses View Binding, swap the findViewById calls for binding.seekBar and binding.textView; the listener code is identical. Prefer Java? The same three callbacks apply — only the anonymous-class syntax differs.
The Three Listener Callbacks, Explained
- onProgressChanged(seekBar, progress, fromUser) — fires for every progress change.
fromUseristrueonly when the user moved the thumb; it isfalsewhen your own code callssetProgress(). Check it to avoid feedback loops. - onStartTrackingTouch(seekBar) — fires once when the user touches the thumb. Useful for pausing playback while the user scrubs a media position bar.
- onStopTrackingTouch(seekBar) — fires once when the user releases. Do expensive one-shot work here (saving a preference, seeking a video) instead of in
onProgressChanged, which can fire dozens of times per second.
Styling the Classic SeekBar
Two attributes cover most styling needs without a custom drawable:
android:thumbTint="@color/purple_500"
android:progressTint="@color/purple_500"
android:progressBackgroundTint="@color/gray_300"
For a stepped (discrete) SeekBar in a Material theme, use the style Widget.AppCompat.SeekBar.Discrete and a small max value such as 5. For anything fancier — value labels, ranges, floats — use the Material Slider below instead of fighting SeekBar with custom drawables.
Material 3 Slider: The Modern View-Based Replacement
If your app already depends on Material Components (com.google.android.material:material), prefer Slider over SeekBar for new screens. It follows the Material 3 spec, works with float values, supports a true minimum on every API level, shows a value label while dragging, and has a two-thumb sibling, RangeSlider, that SeekBar simply cannot do.
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="12.0"
android:valueTo="48.0"
android:stepSize="1.0"
android:value="20.0" />
And the Kotlin listener — one lambda instead of a three-method interface:
val slider = findViewById<com.google.android.material.slider.Slider>(R.id.slider)
slider.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
textView.textSize = value
}
}
// Equivalent of start/stop tracking:
slider.addOnSliderTouchListener(object : com.google.android.material.slider.Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: com.google.android.material.slider.Slider) { }
override fun onStopTrackingTouch(slider: com.google.android.material.slider.Slider) {
// Save the final value here
}
})
Set android:stepSize="0.0" (or omit it) for a continuous slider, or a positive value for discrete steps with tick marks. Your activity's theme must inherit from a Theme.Material3 (or Theme.MaterialComponents) theme, or the Slider will crash on inflation — that is the most common first-run error.
Jetpack Compose Slider Example
In Compose there is no SeekBar; the Material 3 Slider composable fills the role, and like everything in Compose it is state-driven: you hold the value in state, the slider reports changes through onValueChange, and recomposition redraws it.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun TextSizeSlider() {
var textSize by remember { mutableFloatStateOf(20f) }
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = "Hello Slider!",
fontSize = textSize.sp
)
Slider(
value = textSize,
onValueChange = { textSize = it },
valueRange = 12f..48f,
steps = 0, // 0 = continuous; e.g. 35 would give discrete stops
onValueChangeFinished = {
// Equivalent of onStopTrackingTouch — persist the value here
}
)
}
}
Notes worth remembering: steps counts the stops between the endpoints, not including them, so valueRange = 0f..10f with steps = 9 gives whole-number stops. Use onValueChangeFinished for expensive work, exactly like onStopTrackingTouch in the View world. Compose Material 3 also provides RangeSlider for two-thumb selection and lets you pass custom thumb and track composables for full visual control. If your screens navigate between Views and Compose, our guide on moving between activities in Kotlin pairs well with this one.
SeekBar vs Material Slider vs Compose Slider: Which One?
| Use this | When |
|---|---|
Compose Slider | Any Jetpack Compose UI — there is no SeekBar in Compose |
Material Slider | New View-based screens in an app with a Material 3 theme; you need floats, a min below API 26, value labels, or a RangeSlider |
Classic SeekBar | Maintaining older code, or a project that cannot take the Material Components dependency |
Practical Tips
- Check
fromUserbefore reacting, so programmaticsetProgress()/value updates do not trigger loops. - Debounce expensive work into
onStopTrackingTouch/onValueChangeFinished, not the per-frame change callback. - Accessibility: give the control a
contentDescription(orstateDescription) so TalkBack announces what the value means, and keep the touch target at least 48dp tall. - Persist the value in a ViewModel or
rememberSaveableso rotation does not reset it. - Building richer controls? See our tutorial on making an Android home screen widget for taking values like these outside the app.
Bottom line: the classic SeekBar plus OnSeekBarChangeListener still works everywhere and takes ten lines of Kotlin, but for new code use Material 3's Slider in View-based screens and the Slider composable in Compose — you get floats, real minimums, value labels and range selection for free. Official references: SeekBar, Material 3 Sliders, and the Compose Slider guide.
Frequently Asked Questions
What is a SeekBar in Android?
A SeekBar is a widget that lets the user pick a value from a range by dragging a thumb along a track. It extends ProgressBar and adds user interaction, which is why it is the classic control for settings like volume, brightness and text size in View-based layouts.
How do I use a SeekBar in Kotlin?
Add a <SeekBar> to your XML layout, get a reference with findViewById or View Binding, then call setOnSeekBarChangeListener with an object implementing SeekBar.OnSeekBarChangeListener. Read the current value from the progress parameter in onProgressChanged, and do one-shot work like saving the value in onStopTrackingTouch.
Should I use SeekBar or the Material Slider?
For new View-based screens in an app with a Material 3 theme, use com.google.android.material.slider.Slider. It supports float values, a true minimum on every API level, value labels while dragging, discrete steps via stepSize, and a two-thumb RangeSlider. Keep SeekBar for legacy code or projects without the Material Components dependency.
Is there a SeekBar in Jetpack Compose?
No. Compose uses the Material 3 Slider composable instead. You hold the value in state, pass it as value, update it in onValueChange, and use valueRange and steps to control the range. onValueChangeFinished is the Compose equivalent of onStopTrackingTouch.
How do I set a minimum value on a SeekBar?
android:min works on API 26 (Android 8.0) and higher. On older versions SeekBar always starts at 0, so keep the widget at 0..n and add an offset in code — for example add 12 to represent a 12 to 48 range. The Material Slider avoids this entirely with valueFrom on every supported API level.
Why does onProgressChanged fire when I set the value in code?
The callback fires for any progress change, not only user gestures. Check the boolean fromUser parameter and ignore changes where it is false to avoid feedback loops when your own code calls setProgress(). The Material Slider's addOnChangeListener passes the same fromUser flag.
How do I make a SeekBar with discrete steps?
With the classic SeekBar, apply the Widget.AppCompat.SeekBar.Discrete style and use a small max value. With the Material Slider, set android:stepSize to a positive value to get tick marks. In Compose, set the steps parameter — it counts the stops between the endpoints, so 0f..10f with steps = 9 gives whole-number stops.
Why does the Material Slider crash when my layout inflates?
Almost always a theming problem: the Material Slider requires your activity theme to inherit from Theme.Material3 or Theme.MaterialComponents. If your app still uses a plain AppCompat theme, either migrate the theme or wrap the layout in a ThemeOverlay via android:theme on the Slider.





