adaptive_platform_ui
Flutter용 적응형 플랫폼 전용 위젯입니다. iOS 26 이상에서는 네이티브 iOS의 액체 유리 디자인을 자동으로 렌더링하고, 이전 iOS 버전에는 전통적인 Cupertino 위젯, 안드로이드에는 Material Design을 사용합니다.
iOS 26 이상의 네이티브 리퀴드 글래스 디자인과 iOS, Android, 웹에 대한 자동 플랫폼 감지 기능을 갖춘 적응형 Flutter 위젯.
{"sdk":"flutter"}^1.0.3{"sdk":"flutter"}^5.0.0아래는 영문 원문 스냅샷입니다. 최신 내용은 GitHub에서 확인하세요.
CI Release License: MIT Flutter
A Flutter package that provides adaptive platform-specific widgets with native iOS 26+ designs, traditional Cupertino widgets for older iOS versions, and Material Design for Android.
Upgrading from 0.1.x? Read Migrating to 1.0.0 first: the iOS deployment target is now 15.0, and on iOS 26+ the toolbar is fixed above the navigator.
Toolbar and tab bar in the trailing bar of iPhone Duo, in portrait and landscape
Fixed Liquid Glass toolbar on iPhone
One toolbar stays in place while pages slide underneath it, and only its items change, in step with the page transition and with a back swipe. On iPhone Duo the toolbar and the tab bar move to the vertical bar on the side, as native Liquid Glass capsules, in every rotation; items that do not fit move into the system overflow menu. Works with any router, with nothing to set up.
Native iOS 26 UIToolbar and UITabBar with Liquid Glass blur effects, minimize behavior, and native gesture handling.
AdaptiveApp - Unified app configuration for all platforms:
AdaptiveApp.router()iOS 26+ Native Designs - Modern iOS 26 components with:
iOS Legacy Support - Traditional Cupertino widgets for iOS 18 and below
Material Design - Full Material 3 support for Android
Automatic Platform Detection - Zero configuration required
Version-Aware Rendering - Automatically selects appropriate widget based on iOS version
⚠️ For proper localization support (automatic translations for date/time pickers, buttons, etc.), you must add localization delegates to your AdaptiveApp:
import 'package:flutter_localizations/flutter_localizations.dart';
AdaptiveApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, // Important!
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
Locale('en', ''), // English
Locale('de', ''), // German
Locale('tr', ''), // Turkish
// Add more locales as needed
],
// ... rest of your app configuration
)
Without these delegates, date/time pickers and other widgets will show English text regardless of system language.
Basic Usage:
AdaptiveScaffold(
appBar: AdaptiveAppBar(
title: 'My App',
actions: [
AdaptiveAppBarAction(
onPressed: () {},
iosSymbol: 'gear',
icon: Icons.settings,
),
],
),
bottomNavigationBar: AdaptiveBottomNavigationBar(
items: [
AdaptiveNavigationDestination(
icon: 'house.fill',
label: 'Home',
),
AdaptiveNavigationDestination(
icon: 'person.fill',
label: 'Profile',
),
],
selectedIndex: 0,
onTap: (index) {},
),
body: YourContent(),
)
iOS 26 Native Toolbar:
AdaptiveScaffold(
appBar: AdaptiveAppBar(
title: 'My App',
useNativeToolbar: true, // Enable native iOS 26 UIToolbar with Liquid Glass effects
actions: [...],
),
body: YourContent(),
)
iOS 26 Native Bottom Bar:
AdaptiveScaffold(
bottomNavigationBar: AdaptiveBottomNavigationBar(
useNativeBottomBar: true, // Enable native iOS 26 UITabBar with Liquid Glass effects (default)
items: [...],
selectedIndex: 0,
onTap: (index) {},
),
body: YourContent(),
)
No AppBar or Bottom Navigation:
// If appBar and bottomNavigationBar are null, neither will be shown
AdaptiveScaffold(
body: YourContent(),
)
Key Features:
Adaptive Bottom Navigation Bar (Destinations):
Native Toolbar
On iOS 26+ the native toolbar is not part of a page. AdaptiveApp keeps one
toolbar above the navigator, the way a UINavigationController does: pages
slide underneath it, the bar stays where it is, and only its items change.
AdaptiveApp (or AdaptiveApp.router) and give
your pages an AdaptiveAppBar(useNativeToolbar: true). Each
AdaptiveScaffold publishes its app bar when it appears and withdraws it
when it leaves.NavigatorObserver, so it works with Navigator, GoRouter (including
StatefulShellRoute), auto_route, nested navigators and tabs.label so it has a name there:AdaptiveAppBarAction(
iosSymbol: 'arrow.uturn.backward',
icon: Icons.undo,
label: 'Undo', // overflow menu, VoiceOver, Android tooltip
onPressed: undo,
)
Unlike title, a label never replaces the icon with text.
Not using AdaptiveApp? Install the host yourself, around the navigator
(see Migrating to 1.0.0):
MaterialApp(
builder: (context, child) => AdaptiveToolbarHost(child: child!),
// ...
);
Without a host every page draws its own toolbar, as before.
// Basic button with label
AdaptiveButton(
onPressed: () {},
label: 'Click Me',
)
// Button with custom child
AdaptiveButton.child(
onPressed: () {},
child: Row(
children: [
Icon(Icons.add),
Text('Add Item'),
],
),
)
// Icon button
AdaptiveButton.icon(
onPressed: () {},
icon: Icons.favorite,
)
// Basic alert dialog
AdaptiveAlertDialog.show(
context: context,
title: 'Confirm',
message: 'Are you sure?',
icon: 'checkmark.circle.fill',
actions: [
AlertAction(
title: 'Cancel',
style: AlertActionStyle.cancel,
onPressed: () {},
),
AlertAction(
title: 'Confirm',
style: AlertActionStyle.primary,
onPressed: () {
// Do something
},
),
],
);
// Alert dialog with text input
final result = await AdaptiveAlertDialog.show(
context: context,
title: 'Enter Your Name',
message: 'Please provide your name',
icon: 'person.fill',
input: AdaptiveAlertDialogInput(
placeholder: 'Your name',
initialValue: '',
keyboardType: TextInputType.text,
),
actions: [
AlertAction(
title: 'Cancel',
style: AlertActionStyle.cancel,
onPressed: () {},
),
AlertAction(
title: 'Submit',
style: AlertActionStyle.primary,
onPressed: () {},
),
],
);
// result contains the text entered by the user
if (result != null) {
print('User entered: $result');
}
AdaptiveContextMenu(
actions: [
AdaptiveContextMenuAction(
title: 'Edit',
icon: PlatformInfo.isIOS ? CupertinoIcons.pencil : Icons.edit,
onPressed: () {
print('Edit pressed');
},
),
AdaptiveContextMenuAction(
title: 'Share',
icon: PlatformInfo.isIOS ? CupertinoIcons.share : Icons.share,
onPressed: () {
print('Share pressed');
},
),
AdaptiveContextMenuAction(
title: 'Delete',
icon: PlatformInfo.isIOS ? CupertinoIcons.trash : Icons.delete,
isDestructive: true,
onPressed: () {
print('Delete pressed');
},
),
],
child: Container(
padding: EdgeInsets.all(16),
child: Text('Long press me'),
),
)
iOS: Uses CupertinoContextMenu with preview and native animations.
Android: Uses PopupMenuButton with Material Design styling.
iOS 26 Native Popup
// Text button with popup menu
AdaptivePopupMenuButton.text<String>(
label: 'Options',
items: [
AdaptivePopupMenuItem(
label: 'Edit',
icon: PlatformInfo.isIOS26OrHigher() ? 'pencil' : Icons.edit,
value: 'edit',
),
AdaptivePopupMenuItem(
label: 'Delete',
icon: PlatformInfo.isIOS26OrHigher() ? 'trash' : Icons.delete,
value: 'delete',
),
AdaptivePopupMenuDivider(),
AdaptivePopupMenuItem(
label: 'Share',
icon: PlatformInfo.isIOS26OrHigher() ? 'square.and.arrow.up' : Icons.share,
value: 'share',
),
],
onSelected: (index, item) {
print('Selected: ${item.value}');
},
)
// Icon button with popup menu
AdaptivePopupMenuButton.icon<String>(
icon: 'ellipsis.circle',
items: [...],
onSelected: (index, item) { },
buttonStyle: PopupButtonStyle.glass,
)
// Custom widget with popup menu
AdaptivePopupMenuButton.widget<String>(
items: [
AdaptivePopupMenuItem(label: 'Option 1', value: 'opt1'),
AdaptivePopupMenuItem(label: 'Option 2', value: 'opt2'),
],
onSelected: (index, item) {
print('Selected: ${item.value}');
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.menu),
SizedBox(width: 8),
Text('Custom Button'),
],
),
),
)
Segmented Control
AdaptiveSegmentedControl(
labels: ['One', 'Two', 'Three'],
selectedIndex: 0,
onValueChanged: (index) {
print('Selected: $index');
},
)
// With icons (SF Symbols on iOS)
AdaptiveSegmentedControl(
labels: [],
sfSymbols: [
'house.fill',
'person.fill',
'gear',
],
selectedIndex: 0,
onValueChanged: (index) {},
iconColor: CupertinoColors.systemBlue,
)
Adaptive Switch
AdaptiveSwitch(
value: true,
onChanged: (value) {
print('Switch: $value');
},
)
Adaptive Slider
AdaptiveSlider(
value: 0.5,
onChanged: (value) {
print('Slider: $value');
},
min: 0.0,
max: 1.0,
)
AdaptiveCheckbox(
value: true,
onChanged: (value) {
print('Checkbox: $value');
},
)
// Tristate checkbox
AdaptiveCheckbox(
value: null, // Can be true, false, or null
tristate: true,
onChanged: (value) {
print('Checkbox: $value');
},
)
enum Options { option1, option2, option3 }
Options? _selectedOption = Options.option1;
AdaptiveRadio<Options>(
value: Options.option1,
groupValue: _selectedOption,
onChanged: (Options? value) {
setState(() {
_selectedOption = value;
});
},
)
AdaptiveCard(
padding: EdgeInsets.all(16),
child: Text('Card Content'),
)
// Card with custom styling
AdaptiveCard(
padding: EdgeInsets.all(16),
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
elevation: 8, // Android only
child: Column(
children: [
Text('Custom Card'),
Text('With multiple elements'),
],
),
)
AdaptiveBadge(
count: 5,
child: Icon(Icons.notifications),
)
// Badge with text label
AdaptiveBadge(
label: 'NEW',
backgroundColor: Colors.red,
child: Icon(Icons.mail),
)
// Large badge
AdaptiveBadge(
count: 99,
isLarge: true,
child: Icon(Icons.message),
)
AdaptiveTooltip(
message: 'This is a tooltip',
child: Icon(Icons.info),
)
// Tooltip positioned above
AdaptiveTooltip(
message: 'Tooltip appears above',
preferBelow: false,
child: Icon(Icons.help),
)
// Basic snackbar
AdaptiveSnackBar.show(
context,
message: 'Operation completed successfully!',
type: AdaptiveSnackBarType.success,
)
// Snackbar with action button
AdaptiveSnackBar.show(
context,
message: 'File deleted',
type: AdaptiveSnackBarType.info,
action: 'Undo',
onActionPressed: () {
// Undo action
},
)
// Custom duration
AdaptiveSnackBar.show(
context,
message: 'This will stay longer',
duration: Duration(seconds: 8),
)
// Different types
AdaptiveSnackBar.show(context, message: 'Info', type: AdaptiveSnackBarType.info);
AdaptiveSnackBar.show(context, message: 'Success', type: AdaptiveSnackBarType.success);
AdaptiveSnackBar.show(context, message: 'Warning', type: AdaptiveSnackBarType.warning);
AdaptiveSnackBar.show(context, message: 'Error', type: AdaptiveSnackBarType.error);
iOS: Banner-style notification at the top with slide/fade animations, tap to dismiss, and icon indicators. Android: Material SnackBar at the bottom with standard Material Design appearance.
// Basic date picker
final selectedDate = await AdaptiveDatePicker.show(
context: context,
initialDate: DateTime.now(),
);
// Date picker with range
final selectedDate = await AdaptiveDatePicker.show(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2025),
);
// Date and time picker (iOS)
final selectedDateTime = await AdaptiveDatePicker.show(
context: context,
initialDate: DateTime.now(),
mode: CupertinoDatePickerMode.dateAndTime,
);
if (selectedDate != null) {
print('Selected: ${selectedDate.toString()}');
}
iOS: Uses CupertinoDatePicker in a modal bottom sheet with Cancel/Done buttons.
Android: Uses Material DatePickerDialog.
// 12-hour format
final selectedTime = await AdaptiveTimePicker.show(
context: context,
initialTime: TimeOfDay.now(),
use24HourFormat: false,
);
// 24-hour format
final selectedTime = await AdaptiveTimePicker.show(
context: context,
initialTime: TimeOfDay.now(),
use24HourFormat: true,
);
if (selectedTime != null) {
print('Selected: ${selectedTime.format(context)}');
}
iOS: Uses CupertinoDatePicker in time mode in a modal bottom sheet.
Android: Uses Material TimePickerDialog.
// Basic list tile
AdaptiveListTile(
title: Text('Profile'),
subtitle: Text('View your profile'),
hideBottomDivider: false, // Hide bottom border, useful for last item (iOS only)
onTap: () {
// Handle tap
},
)
// List tile with leading and trailing
AdaptiveListTile(
leading: Icon(Icons.person),
title: Text('Profile'),
subtitle: Text('View your profile'),
trailing: Icon(Icons.chevron_right),
onTap: () {
// Handle tap
},
)
// Selectable list tile
AdaptiveListTile(
leading: Icon(Icons.star),
title: Text('Favorite'),
selected: true,
trailing: Icon(Icons.check_circle),
onTap: () {
// Handle tap
},
)
// List tile with custom trailing widget
AdaptiveListTile(
title: Text('Enable Feature'),
subtitle: Text('Toggle to enable'),
trailing: AdaptiveSwitch(
value: switchValue,
onChanged: (value) {
// Handle change
},
),
)
iOS: Uses CupertinoListTile-like styling with bottom border separator.
Android: Uses Material ListTile.
// Basic text field
AdaptiveTextField(
placeholder: 'Enter your name',
onChanged: (value) {
print('Text: $value');
},
)
// Text field with icons
AdaptiveTextField(
placeholder: 'Search',
prefixIcon: Icon(
PlatformInfo.isIOS ? CupertinoIcons.search : Icons.search,
),
suffixIcon: IconButton(
icon: Icon(
PlatformInfo.isIOS ? CupertinoIcons.clear : Icons.clear,
),
onPressed: () {
// Clear text
},
),
)
// Password field
AdaptiveTextField(
placeholder: 'Enter password',
obscureText: true,
prefixIcon: Icon(
PlatformInfo.isIOS ? CupertinoIcons.lock : Icons.lock,
),
)
// Multiline text field
AdaptiveTextField(
placeholder: 'Enter description',
maxLines: 5,
minLines: 3,
keyboardType: TextInputType.multiline,
)
iOS: Uses CupertinoTextField with tertiarySystemBackground color and rounded corners.
Android: Uses Material TextField with outlined border.
// Form with validation
Form(
key: _formKey,
child: Column(
children: [
AdaptiveTextFormField(
placeholder: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
onSaved: (value) => _email = value,
),
AdaptiveButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// Process form
}
},
label: 'Submit',
),
],
),
)
iOS: Uses custom FormField wrapper with CupertinoTextField for proper validation with error display.
Android: Uses Material TextFormField.
// Basic floating action button
AdaptiveFloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
)
// Mini FAB
AdaptiveFloatingActionButton(
onPressed: () {},
mini: true,
child: Icon(Icons.edit),
)
// Custom colors
AdaptiveFloatingActionButton(
onPressed: () {},
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Icon(Icons.favorite),
)
iOS: Circular button with custom shadow effects.
Android: Material FloatingActionButton with elevation.
// Basic form section
AdaptiveFormSection(
header: Text('Personal Information'),
footer: Text('Please provide accurate information'),
children: [
CupertinoFormRow(
prefix: Text('Name'),
child: AdaptiveTextField(placeholder: 'Enter name'),
),
CupertinoFormRow(
prefix: Text('Email'),
child: AdaptiveTextField(placeholder: 'Enter email'),
),
],
)
// Inset grouped style
AdaptiveFormSection.insetGrouped(
header: Text('Settings'),
children: [
CupertinoFormRow(
prefix: Text('Notifications'),
child: AdaptiveSwitch(value: true, onChanged: (v) {}),
),
],
)
iOS: Uses CupertinoFormSection with native iOS styling.
Android: Uses Material Card with similar grouped layout.
// Basic expansion tile
AdaptiveExpansionTile(
title: Text('Settings'),
children: [
ListTile(title: Text('Option 1')),
ListTile(title: Text('Option 2')),
],
)
// With leading and subtitle
AdaptiveExpansionTile(
leading: Icon(Icons.settings),
title: Text('Advanced Settings'),
subtitle: Text('Configure advanced options'),
initiallyExpanded: true,
children: [
ListTile(title: Text('Option 1')),
ListTile(title: Text('Option 2')),
],
)
// With custom colors
AdaptiveExpansionTile(
title: Text('Premium Features'),
backgroundColor: Colors.amber.withValues(alpha: 0.1),
iconColor: Colors.amber,
onExpansionChanged: (expanded) {
print('Expanded: $expanded');
},
children: [
ListTile(title: Text('Feature 1')),
ListTile(title: Text('Feature 2')),
],
)
iOS: Modern custom design with rounded corners, smooth shadows, animated chevron, and gradient separator.
Android: Material ExpansionTile with InkWell effects.
Horizontal swipeable tab view with tabs at the top.
// Tab bar view at the top
AdaptiveTabBarView(
tabs: ['Latest', 'Popular', 'Trending'],
children: [
LatestPage(),
PopularPage(),
TrendingPage(),
],
onTabChanged: (index) {
print('Tab changed to: $index');
},
)
iOS: Uses CupertinoSlidingSegmentedControl for tab selection.
Android: Uses Material TabBar + TabBarView.
// Filled button (primary action)
AdaptiveButton(
onPressed: () {},
style: AdaptiveButtonStyle.filled,
label: 'Filled',
)
// Tinted button (secondary action)
AdaptiveButton(
onPressed: () {},
style: AdaptiveButtonStyle.tinted,
label: 'Tinted',
)
// Gray button (neutral action)
AdaptiveButton(
onPressed: () {},
style: AdaptiveButtonStyle.gray,
label: 'Gray',
)
// Bordered button
AdaptiveButton(
onPressed: () {},
style: AdaptiveButtonStyle.bordered,
label: 'Bordered',
)
// Plain text button
AdaptiveButton(
onPressed: () {},
style: AdaptiveButtonStyle.plain,
label: 'Plain',
)
// Small button (28pt height on iOS)
AdaptiveButton(
onPressed: () {},
size: AdaptiveButtonSize.small,
label: 'Small',
)
// Medium button (36pt height on iOS) - default
AdaptiveButton(
onPressed: () {},
size: AdaptiveButtonSize.medium,
label: 'Medium',
)
// Large button (44pt height on iOS)
AdaptiveButton(
onPressed: () {},
size: AdaptiveButtonSize.large,
label: 'Large',
)
AdaptiveButton(
onPressed: () {},
label: 'Custom Button',
color: Colors.red,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
borderRadius: BorderRadius.circular(16),
minSize: Size(200, 50),
)
AdaptiveButton(
onPressed: () {},
label: 'Disabled',
enabled: false,
)
Use the PlatformInfo utility class to check platform and iOS version:
import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
// Check platform
if (PlatformInfo.isIOS) {
print('Running on iOS');
}
if (PlatformInfo.isAndroid) {
print('Running on Android');
}
// Check iOS version
if (PlatformInfo.isIOS26OrHigher()) {
print('Using iOS 26+ features');
}
if (PlatformInfo.isIOS18OrLower()) {
print('Using legacy iOS widgets');
}
// Get iOS version number
int version = PlatformInfo.iOSVersion; // e.g., 26
// Check version range
if (PlatformInfo.isIOSVersionInRange(24, 26)) {
print('iOS version is between 24 and 26');
}
// Get platform description
String description = PlatformInfo.platformDescription; // e.g., "iOS 26"
Add this to your package's pubspec.yaml file:
dependencies:
adaptive_platform_ui: ^1.0.0
Then run:
flutter pub get
iOS needs a deployment target of 15.0 or higher. In ios/Podfile:
platform :ios, '15.0'
Most apps need two small steps, or none.
1. Raise the iOS deployment target to 15.0. Set platform :ios, '15.0' in
ios/Podfile, set the Runner target's iOS Deployment Target to 15.0 in Xcode,
then run pod install. Xcode 27 does not build below 15.0.
2. Let the fixed toolbar in. On iOS 26+ the native toolbar now lives above the navigator instead of inside each page.
AdaptiveApp or AdaptiveApp.router, there is nothing to do.MaterialApp or CupertinoApp directly, add the host once:MaterialApp(
builder: (context, child) => AdaptiveToolbarHost(child: child!),
// ...
);
Without the host every page keeps drawing its own toolbar, exactly as before 1.0.0, so nothing breaks; you just do not get the fixed toolbar, and on iPhone Duo the tab bar of a tab layout stays at the bottom instead of moving into the vertical bar.
Check these if they apply to you:
leading, titleWidget or iconWidget. They are now built above
the navigator. A widget that calls Navigator.of(context) with its own
context no longer finds the page's navigator. Use the page's context:// Before: a widget that looks the navigator up from its own context.
leading: const MyCloseButton(),
// After: capture the page's context in the callback.
leading: CupertinoButton(
onPressed: () => Navigator.of(context).pop(), // the page's context
child: const Icon(CupertinoIcons.xmark),
),
AdaptiveScaffold(
useFixedToolbar: false,
appBar: AdaptiveAppBar(title: 'Detail', useNativeToolbar: true),
body: ...,
);
Scaffolds inside a sheet, dialog or popup do this automatically.
useHeroBackButton has no effect with the fixed toolbar: the back button
already stays in place between pages.AdaptiveScaffold (a full screen image viewer,
for example) show no toolbar, because no page owns it while they are in
front.Use AdaptiveApp to automatically configure your app for each platform:
import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return AdaptiveApp(
title: 'My App',
themeMode: ThemeMode.system,
materialLightTheme: ThemeData.light(),
materialDarkTheme: ThemeData.dark(),
cupertinoLightTheme: const CupertinoThemeData(
brightness: Brightness.light,
),
cupertinoDarkTheme: const CupertinoThemeData(
brightness: Brightness.dark,
),
home: const HomePage(),
);
}
}
With Router Support (GoRouter, etc.):
AdaptiveApp.router(
routerConfig: router,
title: 'My App',
themeMode: ThemeMode.system,
materialLightTheme: ThemeData.light(),
materialDarkTheme: ThemeData.dark(),
cupertinoLightTheme: const CupertinoThemeData(
brightness: Brightness.light,
),
cupertinoDarkTheme: const CupertinoThemeData(
brightness: Brightness.dark,
),
)
Key Features:
AdaptiveApp.router()When running on iOS 26+, widgets automatically use native UIKit platform views with Liquid Glass design:
UiKitView to render actual iOS 26 UIKit componentsRun the example app to see all widgets in action:
cd example
flutter run
The example app includes:
⚠️ WARNING: This is a highly experimental feature with significant limitations. Only use for prototyping and demos.
Native iOS 26+ search tab bar with UITabBarController that transforms the tab bar into a search bar when the search tab is selected.
import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
// Enable native search tab bar
await IOS26NativeSearchTabBar.enable(
tabs: [
const NativeTabConfig(
title: 'Home',
sfSymbol: 'house.fill',
),
const NativeTabConfig(
title: 'Search',
sfSymbol: 'magnifyingglass',
isSearchTab: true, // This tab transforms into search
),
const NativeTabConfig(
title: 'Profile',
sfSymbol: 'person.fill',
),
],
selectedIndex: 0,
onTabSelected: (index) {
print('Tab selected: $index');
},
onSearchQueryChanged: (query) {
print('Search query: $query');
},
onSearchSubmitted: (query) {
print('Search submitted: $query');
},
onSearchCancelled: () {
print('Search cancelled');
},
);
// Disable when done
await IOS26NativeSearchTabBar.disable();
// Programmatically show search
await IOS26NativeSearchTabBar.showSearch();
Features:
Known Issues & Limitations:
This feature replaces Flutter's root view controller with a native UITabBarController, which creates fundamental architectural conflicts:
initState, dispose, and other lifecycle methods may not work correctlyNavigator.pop() and related methods become unreliableWhy These Issues Occur:
The feature attempts to merge two incompatible architectural philosophies:
When UITabBarController becomes root, Flutter engine still believes it owns the screen, creating a parent-child relationship neither framework was designed to handle.
Recommendation:
For production apps, use Flutter's built-in TabBar or implement search within the existing navigation structure.
See the example app's Native Search Tab demo page for detailed technical explanation.
Currently available adaptive widgets:
This package follows Apple's Human Interface Guidelines for iOS and Material Design guidelines for Android. The goal is to provide:
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
Thanks to all contributors who helped improve this package!
https://contrib.rocks/image?repo=berkaycatak/adaptive_platform_uiBerkay Çatak