Creating Reusable and Adaptive UI Components in Android Using Jetpack Compose
Course: Android App Development
Topic: Creating Reusable and Adaptive UI Components
Course Link: Android App Development Course
Demo Registration: Register for a Free Course Demo
JustAcademy Homepage: Visit JustAcademy
1. Introduction
Modern Android applications need user interfaces that are consistent, maintainable, and capable of working across different screen sizes and device types. Jetpack Compose makes this easier by allowing developers to build interfaces using small, reusable composable functions rather than repeating the same UI code throughout an application.
A reusable UI component is a composable function that can be used in multiple places with different data or configuration. An adaptive UI component adjusts its layout or behavior according to available space, screen size, orientation, or other relevant conditions.
For example, an application might use the same product card on a home screen, a search results screen, and a favorites screen. The card can display different products while retaining a consistent appearance. On a phone, products may appear in a single column; on a tablet, they may appear in multiple columns.
2. Learning Objectives
- Understand reusable UI components in Jetpack Compose.
- Create composable functions with parameters.
- Pass data and callback functions to components.
- Reduce duplicated UI code and improve maintainability.
- Build adaptive layouts for phones, tablets, and larger screens.
- Use layout constraints and window size information appropriately.
- Apply accessibility, previews, and testing practices to reusable components.
3. What Are Reusable UI Components?
Reusable UI components are small UI building blocks designed to perform a specific presentation or interaction task. Instead of writing the same button, card, profile section, or input field multiple times, developers define it once and configure it through parameters.
Examples of reusable components
| Component |
Purpose |
Example Usage |
| CustomButton |
Provides a consistent button design. |
Save, Submit, Continue |
| ProfileCard |
Displays a person's image and details. |
Team or user profiles |
| ProductCard |
Shows product information. |
Shopping application |
| SectionTitle |
Displays a consistent section heading. |
Dashboard sections |
| EmptyState |
Explains when a list has no content. |
No search results |
| LoadingIndicator |
Displays a loading state. |
Network data loading |
4. Creating a Basic Reusable Composable
In Jetpack Compose, the @Composable annotation identifies a function that describes part of the user interface. A composable can contain text, images, layouts, buttons, and other composables.
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun WelcomeMessage() {
Text(text = "Welcome to Android Development!")
}
The WelcomeMessage() component can be called from multiple screens without repeating its internal UI implementation.
@Composable
fun HomeScreen() {
WelcomeMessage()
}
@Composable
fun ProfileScreen() {
WelcomeMessage()
}
This example demonstrates reuse. However, a component becomes more flexible when its content can be configured through parameters.
5. Using Parameters to Make Components Flexible
Parameters allow a component to display different content while retaining its shared structure. For example, a greeting component can accept a name instead of using a fixed message.
@Composable
fun WelcomeMessage(name: String) {
Text(text = "Welcome, $name!")
}
Usage:
@Composable
fun HomeScreen() {
WelcomeMessage(name = "Aarav")
WelcomeMessage(name = "Priya")
}
Both calls use the same component, but each displays a different name. This approach separates UI structure from the data displayed by that structure.
Good parameter design
- Use descriptive parameter names.
- Pass variable content through parameters rather than hardcoding it.
- Provide sensible default values when appropriate.
- Avoid giving a component too many unrelated responsibilities.
- Use data classes when a component needs several related fields.
6. Building a Reusable Card Component
Cards are commonly used to group related information. A reusable card can display a title, description, and action without requiring each screen to implement the same layout separately.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun InformationCard(
title: String,
description: String,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = title)
Text(text = description)
}
}
}
Usage:
@Composable
fun DashboardScreen() {
Column {
InformationCard(
title = "My Courses",
description = "View your enrolled courses."
)
InformationCard(
title = "Assignments",
description = "Review your pending assignments."
)
}
}
The same component is used for different pieces of information. Changes to its shared layout can be made in one place, improving consistency and reducing maintenance effort.
7. Passing Event Callbacks to Components
Reusable components often need to respond to user actions. Instead of embedding application-specific navigation or business logic inside a general-purpose component, pass a callback function from the parent.
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun ActionButton(
label: String,
onClick: () -> Unit
) {
Button(onClick = onClick) {
Text(text = label)
}
}
Usage:
@Composable
fun AccountScreen() {
ActionButton(
label = "Save Profile",
onClick = {
// Trigger the profile-saving action.
}
)
}
The component controls how the button looks, while the parent decides what happens when the user taps it. This separation makes the component reusable and easier to test.
8. Using Data Classes for Reusable Components
When a component displays multiple related values, a data class can make the interface easier to understand and maintain.
data class Course(
val title: String,
val instructor: String,
val duration: String
)
@Composable
fun CourseCard(course: Course) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = course.title)
Text(text = "Instructor: ${course.instructor}")
Text(text = "Duration: ${course.duration}")
}
}
}
Example usage:
val course = Course(
title = "Android Development",
instructor = "Course Trainer",
duration = "8 Weeks"
)
CourseCard(course = course)
For a production application, keep UI components focused on presentation. Data retrieval, repository calls, and complex business rules usually belong in a ViewModel or another appropriate application layer.
9. Understanding Adaptive UI Components
An adaptive UI changes its layout or presentation according to the space available to the application. Adaptive design is especially important because Android applications can run on compact phones, tablets, foldable devices, Chromebooks, and resizable windows. Google's Android documentation recommends making layout decisions based on the app window's available space rather than relying only on the physical device type. Read the official adaptive UI guide.
Reusable vs. adaptive components
| Feature |
Reusable Component |
Adaptive Component |
| Main purpose |
Shares UI structure across different parts of an app. |
Adjusts presentation to available space or device conditions. |
| Primary technique |
Composable functions, parameters, and callbacks. |
Window size classes, constraints, and responsive layouts. |
| Example |
A reusable product card. |
A product grid that changes its column count. |
| Can they be combined? |
Yes. A reusable component can also adapt its layout when necessary. |
10. Designing for Different Screen Sizes
Do not assume that every Android screen has the same width. A layout that works on a small phone may appear stretched on a tablet, while a two-column layout may become cramped in a narrow split-screen window.
Common design strategies include:
- Compact layouts: Display content in a single column and use space-efficient navigation.
- Medium layouts: Consider additional columns, a navigation rail, or more generous spacing.
- Expanded layouts: Use additional screen space for multi-column layouts, navigation drawers, or list-detail screens.
- Resizable windows: Recalculate the layout when the available application window changes.
- Foldable devices: Account for changing window dimensions and the device's posture.
These categories are design guidance rather than rules that require every app to use identical layouts. Select the layout that suits the content and user task.
11. Creating an Adaptive Grid with LazyVerticalGrid
A grid is useful for displaying repeated items such as products, courses, photos, or dashboard cards. Jetpack Compose provides LazyVerticalGrid for displaying a vertically scrolling grid efficiently.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AdaptiveCourseGrid(courses: List<Course>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 160.dp),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(courses) { course ->
Card {
Text(
text = course.title,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
GridCells.Adaptive(minSize = 160.dp) allows Compose to fit as many columns as the available width can accommodate while keeping each cell at least the specified minimum size, subject to the grid's available space and spacing.
This approach is often more flexible than hardcoding a fixed number of columns. Use a stable item key when list items have unique identifiers and preserving item identity is important.
12. Using Window Size Classes for Adaptive Layouts
For more significant layout changes, an application can use window size classes. These classify the available app window into categories such as compact, medium, and expanded. The classification can change when the device rotates, a foldable opens, or an app window is resized.
The following example demonstrates the concept using a width value passed into a screen component. This keeps the layout component easy to preview and test.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@Composable
fun AdaptiveDashboard(
availableWidth: Dp
) {
if (availableWidth >= 840.dp) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
DashboardSummary(
title = "Courses",
value = "12",
modifier = Modifier.weight(1f)
)
DashboardSummary(
title = "Assignments",
value = "5",
modifier = Modifier.weight(1f)
)
}
} else {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
DashboardSummary(
title = "Courses",
value = "12"
)
DashboardSummary(
title = "Assignments",
value = "5"
)
}
}
}
@Composable
fun DashboardSummary(
title: String,
value: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(text = title)
Text(text = value)
}
}
Important: This is a simplified illustration, not a complete window-size-class implementation. The parent must pass the actual available width, and the example assumes that the chosen breakpoint is suitable for the application's design. In a production app, prefer official window size class APIs when making app-wide adaptive decisions instead of assuming a device category or using a hardcoded screen-width guess.
Using official adaptive APIs
For projects using the Compose Material 3 Adaptive library, the current adaptive information can be obtained with APIs such as currentWindowAdaptiveInfo(). Exact dependencies and API availability depend on the versions used by the project.
// Conceptual example for a project configured with
// the appropriate Compose Material 3 Adaptive dependency.
val adaptiveInfo = currentWindowAdaptiveInfo()
val windowSizeClass = adaptiveInfo.windowSizeClass
Consult the official documentation for the correct dependency setup, supported breakpoints, and API version: Get started with adaptive Android apps.
13. Creating an Adaptive List and Detail Layout
A list-detail interface displays a collection of items and the details of a selected item. On a narrow window, the app may show the list or the detail screen separately. On a wider window, it can show both panes side by side.
Example: Course browser
- Compact window: Show a list of courses; selecting a course opens its details.
- Expanded window: Display the course list in one pane and the selected course information in another.
- Resizable window: Change the arrangement when sufficient space becomes available or disappears.
For a production implementation, consider the Compose Material 3 Adaptive APIs, including ListDetailPaneScaffold and its navigation-enabled variants where appropriate. These APIs help implement multi-pane behavior without manually maintaining separate UI implementations for every screen size.
// Illustrative structure only; scaffold APIs require
// their matching adaptive dependencies and API version.
@Composable
fun CourseBrowserContent(
isWideLayout: Boolean,
courseList: @Composable () -> Unit,
courseDetails: @Composable () -> Unit
) {
if (isWideLayout) {
Row {
Column(modifier = Modifier.weight(1f)) {
courseList()
}
Column(modifier = Modifier.weight(2f)) {
courseDetails()
}
}
} else {
// In a real app, navigation or selection state
// determines whether the list or details are shown.
courseList()
}
}
The example demonstrates the layout idea, not a complete navigation system. A real application must manage selected-course state, back navigation, empty selection, and continuity when the window changes size.
14. Separating UI Components into Files
As an application grows, reusable composables should be organized so that developers can find and maintain them easily. A simple structure might look like this:
com.example.learningapp/
├── data/
│ └── Course.kt
├── ui/
│ ├── components/
│ │ ├── ActionButton.kt
│ │ ├── InformationCard.kt
│ │ ├── CourseCard.kt
│ │ └── EmptyState.kt
│ ├── screens/
│ │ ├── HomeScreen.kt
│ │ └── CourseDetailsScreen.kt
│ └── theme/
│ ├── Color.kt
│ ├── Theme.kt
│ └── Type.kt
└── MainActivity.kt
This is an example structure, not a mandatory directory layout. Small projects may keep related composables together, while larger projects can organize files by feature. Choose a structure that makes shared components easy to locate and avoids unnecessary complexity.
15. State Management in Reusable Components
Reusable components should have a clear approach to state. State determines what the UI displays and how it responds to user interaction.
Stateful component
A stateful component manages its own internal state. This is suitable when the state is local and does not need to be controlled by its parent.
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun LikeButton() {
var liked by remember { mutableStateOf(false) }
Column {
Button(onClick = { liked = !liked }) {
Text(if (liked) "Liked" else "Like")
}
Text(
if (liked) "You liked this item."
else "You have not liked this item yet."
)
}
}
Stateless component with state hoisting
State hoisting means moving state to a parent and passing the current value and event callback into a child. This allows the parent to coordinate multiple components and control their behavior.
@Composable
fun LikeButton(
liked: Boolean,
onLikeChanged: (Boolean) -> Unit
) {
Button(
onClick = { onLikeChanged(!liked) }
) {
Text(if (liked) "Liked" else "Like")
}
}
The parent owns the state and supplies the value. This pattern is often preferable when the same state affects several parts of the screen, needs to survive configuration changes through a suitable state holder, or must be tested independently.
16. Customization Without Duplicating Components
A reusable component should support meaningful customization without forcing developers to create a separate component for every small variation.
Using optional parameters
@Composable
fun SectionTitle(
title: String,
subtitle: String? = null
) {
Column {
Text(text = title)
if (subtitle != null) {
Text(text = subtitle)
}
}
}
Using a content slot
Composable content parameters, also called content slots, allow the caller to provide custom UI inside a reusable component.
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
@Composable
fun ContentCard(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Card(modifier = modifier) {
Column(modifier = Modifier.padding(16.dp)) {
content()
}
}
}
Usage:
ContentCard {
Text(text = "Course Progress")
Text(text = "You have completed 65% of this course.")
}
Content slots let the caller control the inner content while the component maintains shared visual structure. They are particularly useful for cards, dialogs, list items, and section containers.
17. Accessibility in Reusable and Adaptive Components
A component is not complete simply because it looks correct on multiple screens. It must also be understandable and usable with accessibility services, larger text settings, keyboards, and other input methods.
- Provide meaningful labels for interactive controls.
- Use sufficient color contrast and do not rely on color alone to communicate meaning.
- Keep touch targets large enough for comfortable interaction.
- Support text scaling without clipping important content.
- Ensure focus order makes sense for keyboard and accessibility navigation.
- Use descriptive content descriptions for meaningful images; avoid redundant descriptions for decorative images.
- Test large-screen layouts with mouse, keyboard, and other relevant input devices.
- Check that adaptive layout changes do not hide essential actions or information.
18. Previewing and Testing Reusable Components
Jetpack Compose previews allow developers to inspect a composable in Android Studio without repeatedly running the entire application. Previewing components with different data helps identify spacing, text, and layout problems early.
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Preview(showBackground = true)
@Composable
fun InformationCardPreview() {
InformationCard(
title = "Android Development",
description = "Learn to build modern Android applications."
)
}
Also test the component in the context of the full screen. A component may work correctly in isolation but overflow when placed in a narrow column or a long scrolling list.
Testing checklist
- Test compact, medium, and expanded window widths.
- Test portrait and landscape orientations.
- Resize the application window where supported.
- Test long titles, long descriptions, and missing optional values.
- Test empty lists, large lists, and loading states.
- Verify callbacks and user interactions.
- Check accessibility labels and larger font settings.
- Verify that UI state behaves correctly when the layout changes.
19. Common Mistakes and Troubleshooting
| Problem |
Possible Cause |
Recommended Solution |
| Repeated UI code |
Components are defined separately for similar screens. |
Extract shared structure into composables with parameters. |
| Component is difficult to customize |
Content and behavior are hardcoded. |
Pass values and callbacks; use content slots when appropriate. |
| Layout looks stretched on tablets |
Components fill all available width without considering content needs. |
Use appropriate maximum widths, columns, panes, or adaptive layout patterns. |
| Grid items are too narrow |
Too many columns fit the chosen minimum cell size. |
Review minimum cell size and available spacing. |
| State resets unexpectedly |
State is stored in a short-lived composable or not managed at the correct level. |
Review state ownership, state hoisting, and appropriate state holders. |
| Component performs unrelated tasks |
Presentation, networking, and business logic are mixed together. |
Separate UI responsibilities from data and domain logic. |
| Text is clipped |
Fixed dimensions or assumptions about text length. |
Allow flexible sizing and test with larger fonts and longer text. |
| Layout does not update when resized |
Layout decisions use stale or incorrect size information. |
Use appropriate adaptive APIs and ensure decisions reflect the current app window. |
| Reusable button triggers the wrong action |
Callback logic is embedded in the component. |
Pass the intended callback from the parent. |
20. Best Practices
- Follow single responsibility: Give each composable a clear purpose.
- Prefer parameterized components: Make titles, descriptions, values, and callbacks configurable.
- Keep reusable UI predictable: Avoid hidden side effects inside presentation components.
- Use state hoisting when needed: Keep shared state at the appropriate parent or state-holder level.
- Adapt based on available space: Do not assume that every tablet or phone has a fixed layout.
- Choose meaningful breakpoints: Make layout changes when the content needs more or less room.
- Use lazy layouts for large collections: Consider lazy lists and grids when displaying many items.
- Reuse Material components where suitable: Prefer established components over recreating standard interaction patterns without a reason.
- Design for accessibility: Support text scaling, screen readers, and relevant input methods.
- Preview and test variants: Validate different content lengths and window sizes.
21. Practical Exercise
Task: Build a reusable course dashboard that adapts to different screen widths.
Requirements
- Create a
Course data class with title, instructor, and duration.
- Create a reusable
CourseCard composable.
- Display a list of at least six courses using a lazy list or grid.
- Use an adaptive grid strategy so the number of columns responds to available space.
- Add a reusable action button with an
onClick callback.
- Display an empty-state component when no courses are available.
- Preview the card and test the dashboard on narrow and wide windows.
- Check the layout with long course titles and larger text settings.
Expected outcome
The finished application should reuse the same course card for each item, avoid duplicated UI code, and present the course collection clearly across different available window sizes.
22. Interview Questions and Answers
Q1. What is a reusable UI component in Jetpack Compose?
Answer: It is a composable function that encapsulates a piece of UI so it can be used in multiple places with different parameters or content.
Q2. Why should developers create reusable composables?
Answer: Reusable composables reduce code duplication, improve consistency, simplify maintenance, and make testing easier.
Q3. What is an adaptive UI?
Answer: An adaptive UI changes its presentation or layout to suit the space available to the application and the user's device or window configuration.
Q4. What is the difference between reusable and adaptive components?
Answer: Reusability means using the same component in different places. Adaptability means adjusting the component or screen to different layout conditions. A component can provide both.
Q5. How can a composable receive different data?
Answer: Pass parameters such as strings, numbers, data objects, and configuration values to the composable function.
Q6. Why pass callbacks to reusable buttons?
Answer: Callbacks allow the parent to define the action, keeping the reusable button independent of specific business logic or navigation behavior.
Q7. What is state hoisting?
Answer: State hoisting moves state to a parent and passes the state value and event callbacks to child composables.
Q8. What is the purpose of LazyVerticalGrid?
Answer: It displays items in a vertically scrolling grid and composes items as needed, making it suitable for large collections.
Q9. What are window size classes?
Answer: They categorize the available app window dimensions into size categories that help developers choose suitable layouts.
Q10. How should adaptive layouts be tested?
Answer: Test different window widths, orientations, resizable windows, content lengths, text sizes, accessibility settings, and user interactions.
23. Quick Revision
@Composable is used to define composable UI functions.
- Parameters allow a component to display different data.
- Callbacks separate component presentation from actions.
- Data classes group related information for display.
- Content slots allow callers to supply custom composable content.
- State hoisting makes shared state easier to manage.
LazyVerticalGrid supports adaptive grid layouts.
- Window size classes help make decisions based on available app window space.
- Adaptive design should account for resizing, orientation, foldables, and accessibility.
- Previews and device testing help catch layout issues before release.
24. Conclusion
Creating reusable and adaptive UI components is an important skill in modern Android development with Jetpack Compose. Reusable composables help developers build consistent interfaces from smaller building blocks, while adaptive layouts help those interfaces work effectively across phones, tablets, foldables, and resizable windows.
By combining parameterized composables, callbacks, state hoisting, lazy layouts, accessibility practices, and window-aware layout decisions, developers can create Android applications that are easier to maintain and provide a better user experience on different devices.
Continue learning: Explore the Android App Development Course | Register for a Course Demo | Visit JustAcademy