├── .gitattributes
├── example
├── ios
│ ├── Runner
│ │ ├── Runner-Bridging-Header.h
│ │ ├── Assets.xcassets
│ │ │ ├── LaunchImage.imageset
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ ├── README.md
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ └── Info.plist
│ ├── build
│ │ └── XCBuildData
│ │ │ └── eca9beaf18847b10b71235e7f1569c2f.xcbuilddata
│ │ │ ├── target-graph.txt
│ │ │ ├── description.msgpack
│ │ │ ├── task-store.msgpack
│ │ │ ├── manifest.json
│ │ │ └── build-request.json
│ ├── Flutter
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── AppFrameworkInfo.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── RunnerTests
│ │ └── RunnerTests.swift
│ ├── Podfile.lock
│ ├── .gitignore
│ └── Podfile
├── android
│ ├── app
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── drawable
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── drawable-v21
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ ├── values
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── values-night
│ │ │ │ │ │ └── styles.xml
│ │ │ │ ├── kotlin
│ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── adaptive_platform_ui_example
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── debug
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── profile
│ │ │ │ └── AndroidManifest.xml
│ │ └── build.gradle.kts
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── settings.gradle.kts
├── lib
│ ├── utils
│ │ ├── global_variables.dart
│ │ ├── extensions
│ │ │ └── extensions.dart
│ │ └── constants
│ │ │ └── route_constants.dart
│ ├── pages
│ │ ├── search
│ │ │ └── search_page.dart
│ │ └── demos
│ │ │ ├── tab_view_demo_page.dart
│ │ │ ├── slider_demo_page.dart
│ │ │ └── demo_tabbar_page.dart
│ ├── main.dart
│ ├── test_tab_colors.dart
│ └── main
│ │ └── main_page.dart
├── README.md
├── pubspec.yaml
├── .vscode
│ └── launch.json
├── test
│ └── widget_test.dart
├── .gitignore
├── .metadata
└── analysis_options.yaml
├── img
├── alert.png
├── alert_p.png
├── appbar.gif
├── button.png
├── popup_p.png
├── slider.gif
├── switch.gif
├── switch.png
├── bottombar.gif
├── buttons_p.png
├── toolbar_p.png
├── bottom_nav_p.png
├── pop-up-menu.png
├── toolbar2_p.png
├── bottom_nav2_p.png
├── highlight-img.png
├── pop-up-menu_p.png
├── liquid-glass-demo.png
└── segmented_control.gif
├── android
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── com
│ │ └── berkaycatak
│ │ └── adaptive_platform_ui
│ │ └── AdaptivePlatformUiPlugin.kt
└── build.gradle
├── analysis_options.yaml
├── .metadata
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── bug_report.md
│ └── feature_request.md
├── DISCUSSION_TEMPLATE
│ ├── questions.yml
│ ├── ideas.yml
│ └── show-and-tell.yml
├── workflows
│ ├── ci.yml
│ └── release.yml
├── PULL_REQUEST_TEMPLATE.md
└── RELEASING.md
├── .gitignore
├── lib
├── src
│ ├── style
│ │ └── sf_symbol.dart
│ ├── platform
│ │ └── platform_info.dart
│ └── widgets
│ │ ├── adaptive_app_bar_action.dart
│ │ ├── adaptive_app_bar.dart
│ │ ├── adaptive_switch.dart
│ │ ├── adaptive_slider.dart
│ │ ├── adaptive_floating_action_button.dart
│ │ ├── ios26
│ │ ├── ios26_tab_bar.dart
│ │ ├── ios26_native_search_tab_bar.dart
│ │ └── ios26_switch.dart
│ │ ├── adaptive_bottom_navigation_bar.dart
│ │ ├── adaptive_context_menu.dart
│ │ ├── adaptive_list_tile.dart
│ │ ├── adaptive_radio.dart
│ │ ├── adaptive_time_picker.dart
│ │ └── adaptive_segmented_control.dart
└── adaptive_platform_ui.dart
├── codecov.yml
├── LICENSE
├── ios
├── adaptive_platform_ui.podspec
└── Classes
│ ├── AdaptivePlatformUiPlugin.swift
│ ├── iOS26ScaffoldManager.swift
│ ├── iOS26BlurViewPlatformView.swift
│ └── iOS26SwitchView.swift
├── pubspec.yaml
└── test
├── platform_info_test.dart
├── adaptive_tab_view_test.dart
└── adaptive_floating_action_button_test.dart
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/example/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/img/alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/alert.png
--------------------------------------------------------------------------------
/img/alert_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/alert_p.png
--------------------------------------------------------------------------------
/img/appbar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/appbar.gif
--------------------------------------------------------------------------------
/img/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/button.png
--------------------------------------------------------------------------------
/img/popup_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/popup_p.png
--------------------------------------------------------------------------------
/img/slider.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/slider.gif
--------------------------------------------------------------------------------
/img/switch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/switch.gif
--------------------------------------------------------------------------------
/img/switch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/switch.png
--------------------------------------------------------------------------------
/img/bottombar.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/bottombar.gif
--------------------------------------------------------------------------------
/img/buttons_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/buttons_p.png
--------------------------------------------------------------------------------
/img/toolbar_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/toolbar_p.png
--------------------------------------------------------------------------------
/img/bottom_nav_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/bottom_nav_p.png
--------------------------------------------------------------------------------
/img/pop-up-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/pop-up-menu.png
--------------------------------------------------------------------------------
/img/toolbar2_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/toolbar2_p.png
--------------------------------------------------------------------------------
/img/bottom_nav2_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/bottom_nav2_p.png
--------------------------------------------------------------------------------
/img/highlight-img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/highlight-img.png
--------------------------------------------------------------------------------
/img/pop-up-menu_p.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/pop-up-menu_p.png
--------------------------------------------------------------------------------
/img/liquid-glass-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/liquid-glass-demo.png
--------------------------------------------------------------------------------
/img/segmented_control.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/img/segmented_control.gif
--------------------------------------------------------------------------------
/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/target-graph.txt:
--------------------------------------------------------------------------------
1 | Target dependency graph (0 target)
--------------------------------------------------------------------------------
/example/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | # Additional information about this file can be found at
4 | # https://dart.dev/guides/language/analysis-options
5 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/app/src/main/kotlin/com/example/adaptive_platform_ui_example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.adaptive_platform_ui_example
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity : FlutterActivity()
6 |
--------------------------------------------------------------------------------
/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/description.msgpack:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/description.msgpack
--------------------------------------------------------------------------------
/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/task-store.msgpack:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/berkaycatak/adaptive_platform_ui/HEAD/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/task-store.msgpack
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
6 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 | .cxx/
9 |
10 | # Remember to never publicly share your keystore.
11 | # See https://flutter.dev/to/reference-keystore
12 | key.properties
13 | **/*.keystore
14 | **/*.jks
15 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
8 | channel: "stable"
9 |
10 | project_type: package
11 |
--------------------------------------------------------------------------------
/example/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Question or Discussion
4 | url: https://github.com/berkaycatak/adaptive_platform_ui/discussions
5 | about: Ask questions or discuss ideas in GitHub Discussions
6 | - name: Documentation
7 | url: https://github.com/berkaycatak/adaptive_platform_ui#readme
8 | about: Check the README for documentation and examples
9 |
--------------------------------------------------------------------------------
/example/lib/utils/global_variables.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | final GlobalKey navigatorKey = GlobalKey();
4 | BuildContext? get currentContext => navigatorKey.currentContext;
5 |
6 | ScrollController homeScrollController = ScrollController();
7 | ScrollController infoScrollController = ScrollController();
8 | ScrollController searchScrollController = ScrollController();
9 |
--------------------------------------------------------------------------------
/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/manifest.json:
--------------------------------------------------------------------------------
1 | {"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}}
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | @main
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/android/src/main/kotlin/com/berkaycatak/adaptive_platform_ui/AdaptivePlatformUiPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.berkaycatak.adaptive_platform_ui
2 |
3 | import io.flutter.embedding.engine.plugins.FlutterPlugin
4 |
5 | /** AdaptivePlatformUiPlugin */
6 | class AdaptivePlatformUiPlugin: FlutterPlugin {
7 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
8 | // Android uses Material Design widgets directly in Dart
9 | // No platform views needed for Android
10 | }
11 |
12 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # adaptive_platform_ui_example
2 |
3 | A new Flutter project.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
13 |
14 | For help getting started with Flutter development, view the
15 | [online documentation](https://docs.flutter.dev/), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/example/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - adaptive_platform_ui (0.1.0):
3 | - Flutter
4 | - Flutter (1.0.0)
5 |
6 | DEPENDENCIES:
7 | - adaptive_platform_ui (from `.symlinks/plugins/adaptive_platform_ui/ios`)
8 | - Flutter (from `Flutter`)
9 |
10 | EXTERNAL SOURCES:
11 | adaptive_platform_ui:
12 | :path: ".symlinks/plugins/adaptive_platform_ui/ios"
13 | Flutter:
14 | :path: Flutter
15 |
16 | SPEC CHECKSUMS:
17 | adaptive_platform_ui: dcd588cb59eb4c5bd1a430158b00b98b0493b3f0
18 | Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
19 |
20 | PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
21 |
22 | COCOAPODS: 1.16.2
23 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: adaptive_platform_ui_example
2 | description: Example app demonstrating adaptive_platform_ui package
3 | version: 0.1.0
4 | publish_to: 'none'
5 |
6 | environment:
7 | sdk: ^3.9.2
8 | flutter: ">=1.17.0"
9 |
10 |
11 | dependencies:
12 | go_router: ^15.1.2
13 | cupertino_icons: ^1.0.8
14 | intl: ^0.20.2
15 | adaptive_platform_ui:
16 | path: ../
17 | flutter:
18 | sdk: flutter
19 | # cupertino: ^0.0.1
20 | flutter_localizations:
21 | sdk: flutter
22 |
23 | dev_dependencies:
24 | flutter_test:
25 | sdk: flutter
26 | flutter_lints: ^5.0.0
27 |
28 | flutter:
29 | uses-material-design: true
30 |
--------------------------------------------------------------------------------
/example/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | val newBuildDir: Directory =
9 | rootProject.layout.buildDirectory
10 | .dir("../../build")
11 | .get()
12 | rootProject.layout.buildDirectory.value(newBuildDir)
13 |
14 | subprojects {
15 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
16 | project.layout.buildDirectory.value(newSubprojectBuildDir)
17 | }
18 | subprojects {
19 | project.evaluationDependsOn(":app")
20 | }
21 |
22 | tasks.register("clean") {
23 | delete(rootProject.layout.buildDirectory)
24 | }
25 |
--------------------------------------------------------------------------------
/example/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26 | /pubspec.lock
27 | **/doc/api/
28 | .dart_tool/
29 | .flutter-plugins-dependencies
30 | /build/
31 | /coverage/
32 | .github/DISCUSSIONS_SETUP.md
33 |
--------------------------------------------------------------------------------
/example/ios/build/XCBuildData/eca9beaf18847b10b71235e7f1569c2f.xcbuilddata/build-request.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand" : {
3 | "command" : "build",
4 | "skipDependencies" : false,
5 | "style" : "buildOnly"
6 | },
7 | "configuredTargets" : [
8 |
9 | ],
10 | "continueBuildingAfterErrors" : false,
11 | "dependencyScope" : "workspace",
12 | "enableIndexBuildArena" : false,
13 | "hideShellScriptEnvironment" : false,
14 | "parameters" : {
15 | "action" : "build",
16 | "overrides" : {
17 |
18 | }
19 | },
20 | "qos" : "utility",
21 | "schemeCommand" : "launch",
22 | "showNonLoggedProgress" : true,
23 | "useDryRun" : false,
24 | "useImplicitDependencies" : false,
25 | "useLegacyBuildLocations" : false,
26 | "useParallelTargets" : true
27 | }
--------------------------------------------------------------------------------
/example/lib/utils/extensions/extensions.dart:
--------------------------------------------------------------------------------
1 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
2 | import 'package:flutter/cupertino.dart';
3 | import 'package:flutter/material.dart';
4 |
5 | extension CheckThemeMode on BuildContext {
6 | bool isDarkMode() => PlatformInfo.isIOS
7 | ? CupertinoTheme.brightnessOf(this) == Brightness.dark
8 | : Theme.of(this).brightness == Brightness.dark;
9 | bool isLightMode() => PlatformInfo.isIOS
10 | ? CupertinoTheme.brightnessOf(this) == Brightness.light
11 | : Theme.of(this).brightness == Brightness.dark;
12 | }
13 |
14 | extension ColorOpacity on Color {
15 | // e.g 0.5 for 50% opacity
16 | Color withOpacityValue(double opacity) {
17 | return withAlpha((opacity * 255).round());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/example/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "example",
9 | "request": "launch",
10 | "type": "dart"
11 | },
12 | {
13 | "name": "example (profile mode)",
14 | "request": "launch",
15 | "type": "dart",
16 | "flutterMode": "profile"
17 | },
18 | {
19 | "name": "example (release mode)",
20 | "request": "launch",
21 | "type": "dart",
22 | "flutterMode": "release"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/example/lib/pages/search/search_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
2 | import 'package:flutter/cupertino.dart';
3 | import 'package:flutter/foundation.dart';
4 |
5 | class SearchPage extends StatefulWidget {
6 | const SearchPage({super.key});
7 |
8 | @override
9 | State createState() => _SearchPageState();
10 | }
11 |
12 | class _SearchPageState extends State {
13 | @override
14 | void initState() {
15 | if (kDebugMode) {
16 | print("search initState");
17 | }
18 | super.initState();
19 | }
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return AdaptiveScaffold(
24 | appBar: AdaptiveAppBar(title: 'Search'),
25 |
26 | body: Center(child: Text("Search Page")),
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/src/style/sf_symbol.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 |
3 | /// Describes an SF Symbol for native iOS 26 rendering
4 | ///
5 | /// SF Symbols are Apple's system icons that can be used in iOS apps.
6 | /// For a full list of available symbols, see: https://developer.apple.com/sf-symbols/
7 | ///
8 | /// Example:
9 | /// ```dart
10 | /// SFSymbol('star.fill', size: 24, color: Colors.blue)
11 | /// ```
12 | class SFSymbol {
13 | /// The SF Symbol name (e.g., 'star.fill', 'heart', 'plus.circle')
14 | final String name;
15 |
16 | /// The size of the symbol in points
17 | final double size;
18 |
19 | /// The color of the symbol
20 | final Color? color;
21 |
22 | /// Creates an SF Symbol descriptor for native iOS rendering
23 | const SFSymbol(this.name, {this.size = 24.0, this.color});
24 | }
25 |
--------------------------------------------------------------------------------
/example/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility in the flutter_test package. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter_test/flutter_test.dart';
9 |
10 | import 'package:adaptive_platform_ui_example/main.dart';
11 |
12 | void main() {
13 | testWidgets('App launches smoke test', (WidgetTester tester) async {
14 | // Build our app and trigger a frame.
15 | await tester.pumpWidget(const AdaptivePlatformUIDemo());
16 |
17 | // Verify that the app title is displayed
18 | expect(find.text('Adaptive Platform UI'), findsWidgets);
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/example/android/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | val flutterSdkPath =
3 | run {
4 | val properties = java.util.Properties()
5 | file("local.properties").inputStream().use { properties.load(it) }
6 | val flutterSdkPath = properties.getProperty("flutter.sdk")
7 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
8 | flutterSdkPath
9 | }
10 |
11 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
12 |
13 | repositories {
14 | google()
15 | mavenCentral()
16 | gradlePluginPortal()
17 | }
18 | }
19 |
20 | plugins {
21 | id("dev.flutter.flutter-plugin-loader") version "1.0.0"
22 | id("com.android.application") version "8.9.1" apply false
23 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false
24 | }
25 |
26 | include(":app")
27 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
13 | migrate_working_dir/
14 |
15 | # IntelliJ related
16 | *.iml
17 | *.ipr
18 | *.iws
19 | .idea/
20 |
21 | # The .vscode folder contains launch configuration and tasks you configure in
22 | # VS Code which you may wish to be included in version control, so this line
23 | # is commented out by default.
24 | #.vscode/
25 |
26 | # Flutter/Dart/Pub related
27 | **/doc/api/
28 | **/ios/Flutter/.last_build_id
29 | .dart_tool/
30 | .flutter-plugins-dependencies
31 | .pub-cache/
32 | .pub/
33 | /build/
34 | /coverage/
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/example/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 13.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | # Codecov Configuration
2 | # https://docs.codecov.com/docs/codecov-yaml
3 |
4 | coverage:
5 | precision: 2
6 | round: down
7 | range: "70...100"
8 |
9 | status:
10 | project:
11 | default:
12 | target: 80%
13 | threshold: 5%
14 | patch:
15 | default:
16 | target: 80%
17 |
18 | ignore:
19 | - "lib/src/widgets/ios26/**" # Native iOS 26 implementations
20 | - "**/*.g.dart" # Generated files
21 | - "**/*.freezed.dart" # Generated files
22 | - "example/**" # Example app
23 |
24 | comment:
25 | layout: "reach,diff,flags,tree,betaprofiling"
26 | behavior: default
27 | require_changes: false
28 |
29 | # Note: iOS 26 native code cannot be tested without a real iOS device
30 | # These implementations use Platform Views (UiKitView) which require
31 | # native iOS runtime and cannot be unit tested in Dart test environment.
32 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Berkay Catak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug or issue with adaptive_platform_ui
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 | ## Bug Description
10 |
11 |
12 | ## Steps to Reproduce
13 | 1.
14 | 2.
15 | 3.
16 |
17 | ## Expected Behavior
18 |
19 |
20 | ## Actual Behavior
21 |
22 |
23 | ## Code Sample
24 | ```dart
25 | // Minimal reproducible code sample
26 | ```
27 |
28 | ## Screenshots
29 |
30 |
31 | ## Environment
32 | - **Package Version**:
33 | - **Flutter Version**:
34 | - **Dart Version**:
35 | - **Platform**:
36 | - **Device/Simulator**:
37 | - **OS Version**:
38 |
39 | ## Additional Context
40 |
41 |
42 | ## Logs/Error Messages
43 | ```
44 | // Paste relevant logs or error messages here
45 | ```
46 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'com.berkaycatak.adaptive_platform_ui'
2 | version '1.0'
3 |
4 | buildscript {
5 | ext.kotlin_version = '1.7.10'
6 | repositories {
7 | google()
8 | mavenCentral()
9 | }
10 |
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:7.3.0'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | }
15 | }
16 |
17 | rootProject.allprojects {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | }
23 |
24 | apply plugin: 'com.android.library'
25 | apply plugin: 'kotlin-android'
26 |
27 | android {
28 | namespace 'com.berkaycatak.adaptive_platform_ui'
29 | compileSdk 34
30 |
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 |
36 | kotlinOptions {
37 | jvmTarget = '1.8'
38 | }
39 |
40 | sourceSets {
41 | main.java.srcDirs += 'src/main/kotlin'
42 | }
43 |
44 | defaultConfig {
45 | minSdk 21
46 | }
47 |
48 | dependencies {
49 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/ios/adaptive_platform_ui.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3 | # Run `pod lib lint adaptive_platform_ui.podspec` to validate before publishing.
4 | #
5 | Pod::Spec.new do |s|
6 | s.name = 'adaptive_platform_ui'
7 | s.version = '0.1.0'
8 | s.summary = 'Adaptive platform-specific widgets for Flutter with iOS 26 native support.'
9 | s.description = <<-DESC
10 | A Flutter package that provides adaptive platform-specific widgets with native iOS 26+ designs,
11 | traditional Cupertino widgets for older iOS versions, and Material Design for Android.
12 | DESC
13 | s.homepage = 'https://github.com/berkaycatak/adaptive_platform_ui'
14 | s.license = { :file => '../LICENSE' }
15 | s.author = { 'Berkay Catak' => 'berkaycatak@example.com' }
16 | s.source = { :path => '.' }
17 | s.source_files = 'Classes/**/*'
18 | s.dependency 'Flutter'
19 | s.platform = :ios, '12.0'
20 |
21 | # Flutter.framework does not contain a i386 slice.
22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
23 | s.swift_version = '5.0'
24 | end
25 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
17 | base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
18 | - platform: android
19 | create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
20 | base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
21 | - platform: ios
22 | create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
23 | base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
24 |
25 | # User provided section
26 |
27 | # List of Local paths (relative to this file) that should be
28 | # ignored by the migrate tool.
29 | #
30 | # Files that are not part of the templates will be ignored by default.
31 | unmanaged_files:
32 | - 'lib/main.dart'
33 | - 'ios/Runner.xcodeproj/project.pbxproj'
34 |
--------------------------------------------------------------------------------
/example/lib/utils/constants/route_constants.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: non_constant_identifier_names
2 |
3 | class RouteConstants {
4 | // Main tabs
5 | String home = '/home';
6 | String info = '/info';
7 | String search = '/search';
8 |
9 | // Demo pages
10 | String demoTabbar = 'demo-tabbar';
11 | String button = 'button';
12 | String alertDialog = 'alert-dialog';
13 | String popupMenu = 'popup-menu';
14 | String contextMenu = 'context-menu';
15 | String slider = 'slider';
16 | String switchDemo = 'switch';
17 | String checkbox = 'checkbox';
18 | String radio = 'radio';
19 | String card = 'card';
20 | String badge = 'badge';
21 | String badgeNavigation = 'badge-navigation';
22 | String tooltip = 'tooltip';
23 | String segmentedControl = 'segmented-control';
24 | String nativeSearchTab = 'native-search-tab';
25 | String snackbar = 'snackbar';
26 | String datePicker = 'date-picker';
27 | String timePicker = 'time-picker';
28 | String listTile = 'list-tile';
29 | String textField = 'text-field';
30 | String tabView = 'tab-view';
31 | String floatingActionButton = 'floating-action-button';
32 | String formSection = 'form-section';
33 | String expansionTile = 'expansion-tile';
34 | String blurView = 'blur-view';
35 | }
36 |
--------------------------------------------------------------------------------
/example/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest a new feature or improvement for adaptive_platform_ui
4 | title: '[FEATURE] '
5 | labels: enhancement
6 | assignees: ''
7 | ---
8 |
9 | ## Feature Description
10 |
11 |
12 | ## Problem Statement
13 |
14 |
15 | ## Proposed Solution
16 |
17 |
18 | ## Platform Considerations
19 |
20 | - **iOS 26+**:
21 | - **iOS <26**:
22 | - **Android**:
23 |
24 | ## Code Example
25 |
26 | ```dart
27 | // Example usage
28 | AdaptiveWidget(
29 | // Your proposed API
30 | )
31 | ```
32 |
33 | ## Alternatives Considered
34 |
35 |
36 | ## Additional Context
37 |
38 |
39 | ## Design References
40 |
41 | - Apple HIG:
42 | - Material Design:
43 | - Other references:
44 |
45 | ## Priority
46 |
47 | - [ ] Low - Nice to have
48 | - [ ] Medium - Would significantly improve my use case
49 | - [ ] High - Blocking or critical for my project
50 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '13.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 |
33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
34 | target 'RunnerTests' do
35 | inherit! :search_paths
36 | end
37 | end
38 |
39 | post_install do |installer|
40 | installer.pods_project.targets.each do |target|
41 | flutter_additional_ios_build_settings(target)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id("dev.flutter.flutter-gradle-plugin")
6 | }
7 |
8 | android {
9 | namespace = "com.example.adaptive_platform_ui_example"
10 | compileSdk = flutter.compileSdkVersion
11 | ndkVersion = flutter.ndkVersion
12 |
13 | compileOptions {
14 | sourceCompatibility = JavaVersion.VERSION_11
15 | targetCompatibility = JavaVersion.VERSION_11
16 | }
17 |
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_11.toString()
20 | }
21 |
22 | defaultConfig {
23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24 | applicationId = "com.example.adaptive_platform_ui_example"
25 | // You can update the following values to match your application needs.
26 | // For more information, see: https://flutter.dev/to/review-gradle-config.
27 | minSdk = flutter.minSdkVersion
28 | targetSdk = flutter.targetSdkVersion
29 | versionCode = flutter.versionCode
30 | versionName = flutter.versionName
31 | }
32 |
33 | buildTypes {
34 | release {
35 | // TODO: Add your own signing config for the release build.
36 | // Signing with the debug keys for now, so `flutter run --release` works.
37 | signingConfig = signingConfigs.getByName("debug")
38 | }
39 | }
40 | }
41 |
42 | flutter {
43 | source = "../.."
44 | }
45 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/DISCUSSION_TEMPLATE/questions.yml:
--------------------------------------------------------------------------------
1 | labels: [question]
2 | body:
3 | - type: markdown
4 | attributes:
5 | value: |
6 | ## Ask a Question
7 |
8 | Ask questions about using Adaptive Platform UI. Before posting:
9 | - Check existing discussions and documentation
10 | - Provide code examples when relevant
11 | - Be specific about your use case
12 |
13 | - type: textarea
14 | id: question
15 | attributes:
16 | label: Question
17 | description: What would you like to know?
18 | placeholder: How do I...?
19 | validations:
20 | required: true
21 |
22 | - type: textarea
23 | id: context
24 | attributes:
25 | label: Context
26 | description: Provide context about what you're trying to achieve
27 | placeholder: I'm building an app that needs to...
28 | validations:
29 | required: false
30 |
31 | - type: textarea
32 | id: code
33 | attributes:
34 | label: Code Sample
35 | description: If applicable, provide a code sample
36 | placeholder: |
37 | ```dart
38 | // Your code here
39 | ```
40 | render: dart
41 | validations:
42 | required: false
43 |
44 | - type: dropdown
45 | id: platform
46 | attributes:
47 | label: Platform
48 | description: Which platform(s) are you working with?
49 | multiple: true
50 | options:
51 | - iOS
52 | - Android
53 | - Both
54 | - Web
55 | validations:
56 | required: false
57 |
58 | - type: input
59 | id: version
60 | attributes:
61 | label: Package Version
62 | description: Which version of adaptive_platform_ui are you using?
63 | placeholder: e.g., 0.1.94
64 | validations:
65 | required: false
66 |
--------------------------------------------------------------------------------
/example/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Adaptive Platform Ui Example
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | adaptive_platform_ui_example
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | CADisableMinimumFrameDurationOnPhone
45 |
46 | UIApplicationSupportsIndirectInputEvents
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/DISCUSSION_TEMPLATE/ideas.yml:
--------------------------------------------------------------------------------
1 | labels: [idea]
2 | body:
3 | - type: markdown
4 | attributes:
5 | value: |
6 | ## Share Your Ideas
7 |
8 | Share ideas for improving Adaptive Platform UI or discuss new features.
9 |
10 | **Note:** For formal feature requests that you want tracked, please use the [Feature Request issue template](https://github.com/berkaycatak/adaptive_platform_ui/issues/new?template=feature_request.md) instead.
11 |
12 | - type: textarea
13 | id: idea
14 | attributes:
15 | label: Your Idea
16 | description: Describe your idea for improvement or new feature
17 | placeholder: What if we could...
18 | validations:
19 | required: true
20 |
21 | - type: textarea
22 | id: problem
23 | attributes:
24 | label: Problem or Use Case
25 | description: What problem does this solve? What use case does it enable?
26 | placeholder: This would help when...
27 | validations:
28 | required: false
29 |
30 | - type: textarea
31 | id: implementation
32 | attributes:
33 | label: Implementation Ideas
34 | description: Any thoughts on how this could be implemented?
35 | placeholder: This could work by...
36 | validations:
37 | required: false
38 |
39 | - type: textarea
40 | id: example
41 | attributes:
42 | label: Example Usage
43 | description: How would you like to use this feature?
44 | placeholder: |
45 | ```dart
46 | AdaptiveWidget(
47 | // Example usage
48 | )
49 | ```
50 | render: dart
51 | validations:
52 | required: false
53 |
54 | - type: dropdown
55 | id: platforms
56 | attributes:
57 | label: Relevant Platforms
58 | description: Which platforms would this affect?
59 | multiple: true
60 | options:
61 | - iOS 26+
62 | - iOS <26
63 | - Android
64 | - All platforms
65 | - Platform agnostic
66 | validations:
67 | required: false
68 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main, dev ]
6 | pull_request:
7 | branches: [ main, dev ]
8 |
9 | jobs:
10 | analyze:
11 | name: Analyze
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup Flutter
19 | uses: subosito/flutter-action@v2
20 | with:
21 | flutter-version: '3.35.6'
22 | channel: 'stable'
23 | cache: true
24 |
25 | - name: Get dependencies
26 | run: flutter pub get
27 |
28 | - name: Analyze project source
29 | run: flutter analyze --fatal-infos
30 |
31 | test:
32 | name: Test
33 | runs-on: ubuntu-latest
34 | needs: analyze
35 |
36 | steps:
37 | - name: Checkout code
38 | uses: actions/checkout@v4
39 |
40 | - name: Setup Flutter
41 | uses: subosito/flutter-action@v2
42 | with:
43 | flutter-version: '3.35.6'
44 | channel: 'stable'
45 | cache: true
46 |
47 | - name: Get dependencies
48 | run: flutter pub get
49 |
50 | - name: Run tests
51 | run: flutter test --coverage --reporter expanded
52 |
53 | - name: Upload coverage to Codecov
54 | uses: codecov/codecov-action@v4
55 | with:
56 | file: ./coverage/lcov.info
57 | fail_ci_if_error: false
58 | token: ${{ secrets.CODECOV_TOKEN }}
59 |
60 | build-example:
61 | name: Build Android Example
62 | runs-on: ubuntu-latest
63 | needs: test
64 |
65 | steps:
66 | - name: Checkout code
67 | uses: actions/checkout@v4
68 |
69 | - name: Setup Flutter
70 | uses: subosito/flutter-action@v2
71 | with:
72 | flutter-version: '3.35.6'
73 | channel: 'stable'
74 | cache: true
75 |
76 | - name: Get dependencies
77 | run: flutter pub get
78 |
79 | - name: Build example app (Android)
80 | run: |
81 | cd example
82 | flutter pub get
83 | flutter build apk --release
84 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:adaptive_platform_ui_example/service/router/router_service.dart';
2 | import 'package:flutter/cupertino.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
5 | import 'package:flutter_localizations/flutter_localizations.dart';
6 |
7 | void main() {
8 | runApp(const AdaptivePlatformUIDemo());
9 | }
10 |
11 | class AdaptivePlatformUIDemo extends StatefulWidget {
12 | const AdaptivePlatformUIDemo({super.key});
13 |
14 | @override
15 | State createState() => _AdaptivePlatformUIDemoState();
16 | }
17 |
18 | class _AdaptivePlatformUIDemoState extends State {
19 | RouterService routerService = RouterService();
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return AdaptiveApp.router(
24 | themeMode: ThemeMode.system,
25 | title: 'Adaptive Platform UI',
26 | cupertinoLightTheme: CupertinoThemeData(brightness: Brightness.light),
27 | cupertinoDarkTheme: CupertinoThemeData(brightness: Brightness.dark),
28 | materialLightTheme: ThemeData(
29 | colorScheme: ColorScheme.fromSeed(
30 | seedColor: Colors.blue,
31 | brightness: Brightness.light,
32 | ),
33 | useMaterial3: true,
34 | brightness: Brightness.light,
35 | ),
36 | materialDarkTheme: ThemeData(
37 | colorScheme: ColorScheme.fromSeed(
38 | seedColor: Colors.blue,
39 | brightness: Brightness.dark,
40 | ),
41 | useMaterial3: true,
42 | brightness: Brightness.dark,
43 | ),
44 | localizationsDelegates: [
45 | GlobalMaterialLocalizations.delegate,
46 | GlobalCupertinoLocalizations.delegate, // Important!
47 | DefaultWidgetsLocalizations.delegate,
48 | ],
49 | locale: const Locale('en'),
50 | supportedLocales: [
51 | const Locale('en'), // English
52 | const Locale('tr'), // Turkish
53 | // ... other locales the app supports
54 | ],
55 | routerConfig: routerService.router,
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/example/lib/test_tab_colors.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
3 |
4 | void main() {
5 | runApp(const MyApp());
6 | }
7 |
8 | class MyApp extends StatelessWidget {
9 | const MyApp({super.key});
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return AdaptiveApp(
14 | title: 'Tab Bar Color Test',
15 | home: const TestTabColors(),
16 | );
17 | }
18 | }
19 |
20 | class TestTabColors extends StatefulWidget {
21 | const TestTabColors({super.key});
22 |
23 | @override
24 | State createState() => _TestTabColorsState();
25 | }
26 |
27 | class _TestTabColorsState extends State {
28 | int _selectedIndex = 0;
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return AdaptiveScaffold(
33 | appBar: AdaptiveAppBar(title: 'Tab Bar Color Test'),
34 | body: Center(
35 | child: Column(
36 | mainAxisAlignment: MainAxisAlignment.center,
37 | children: [
38 | Text(
39 | 'Selected Tab: $_selectedIndex',
40 | style: const TextStyle(fontSize: 24),
41 | ),
42 | const SizedBox(height: 20),
43 | const Text('Testing selectedItemColor: Red'),
44 | const Text('Testing unselectedItemColor: Green'),
45 | ],
46 | ),
47 | ),
48 | bottomNavigationBar: AdaptiveBottomNavigationBar(
49 | items: [
50 | AdaptiveNavigationDestination(icon: 'house', label: 'Home'),
51 | AdaptiveNavigationDestination(
52 | icon: 'magnifyingglass',
53 | label: 'Search',
54 | isSearch: true,
55 | ),
56 | AdaptiveNavigationDestination(icon: 'person', label: 'Profile'),
57 | AdaptiveNavigationDestination(icon: 'gearshape', label: 'Settings'),
58 | ],
59 | selectedIndex: _selectedIndex,
60 | onTap: (index) {
61 | setState(() {
62 | _selectedIndex = index;
63 | });
64 | },
65 | selectedItemColor: CupertinoColors.systemRed,
66 | unselectedItemColor: CupertinoColors.systemGreen,
67 | useNativeBottomBar: true, // Test with native iOS 26 bar
68 | ),
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: adaptive_platform_ui
2 | description: "Adaptive platform-specific widgets for Flutter. Auto renders native iOS 26+ liquid glass designs, traditional Cupertino widgets for older iOS versions, Material Design for Android."
3 | version: 0.1.100
4 | homepage: https://github.com/berkaycatak/adaptive_platform_ui
5 | screenshots:
6 | - description: "iOS 26+ liquid glass designs"
7 | path: img/liquid-glass-demo.png
8 |
9 | environment:
10 | sdk: ^3.9.2
11 | flutter: ">=1.17.0"
12 |
13 | dependencies:
14 | flutter:
15 | sdk: flutter
16 |
17 | dev_dependencies:
18 | flutter_test:
19 | sdk: flutter
20 | flutter_lints: ^5.0.0
21 |
22 | # For information on the generic Dart part of this file, see the
23 | # following page: https://dart.dev/tools/pub/pubspec
24 |
25 | # The following section is specific to Flutter packages.
26 | flutter:
27 | # This package is a Flutter plugin
28 | plugin:
29 | platforms:
30 | ios:
31 | pluginClass: AdaptivePlatformUiPlugin
32 | android:
33 | package: com.berkaycatak.adaptive_platform_ui
34 | pluginClass: AdaptivePlatformUiPlugin
35 |
36 | # To add assets to your package, add an assets section, like this:
37 | # assets:
38 | # - images/a_dot_burr.jpeg
39 | # - images/a_dot_ham.jpeg
40 | #
41 | # For details regarding assets in packages, see
42 | # https://flutter.dev/to/asset-from-package
43 | #
44 | # An image asset can refer to one or more resolution-specific "variants", see
45 | # https://flutter.dev/to/resolution-aware-images
46 |
47 | # To add custom fonts to your package, add a fonts section here,
48 | # in this "flutter" section. Each entry in this list should have a
49 | # "family" key with the font family name, and a "fonts" key with a
50 | # list giving the asset and other descriptors for the font. For
51 | # example:
52 | # fonts:
53 | # - family: Schyler
54 | # fonts:
55 | # - asset: fonts/Schyler-Regular.ttf
56 | # - asset: fonts/Schyler-Italic.ttf
57 | # style: italic
58 | # - family: Trajan Pro
59 | # fonts:
60 | # - asset: fonts/TrajanPro.ttf
61 | # - asset: fonts/TrajanPro_Bold.ttf
62 | # weight: 700
63 | #
64 | # For details regarding fonts in packages, see
65 | # https://flutter.dev/to/font-from-package
66 |
--------------------------------------------------------------------------------
/.github/DISCUSSION_TEMPLATE/show-and-tell.yml:
--------------------------------------------------------------------------------
1 | labels: [showcase]
2 | body:
3 | - type: markdown
4 | attributes:
5 | value: |
6 | ## Show and Tell
7 |
8 | Share what you've built with Adaptive Platform UI! We'd love to see your projects, experiments, and implementations.
9 |
10 | - type: input
11 | id: project-name
12 | attributes:
13 | label: Project Name
14 | description: What's your project called?
15 | placeholder: My Awesome App
16 | validations:
17 | required: true
18 |
19 | - type: textarea
20 | id: description
21 | attributes:
22 | label: Description
23 | description: Tell us about your project
24 | placeholder: This app does...
25 | validations:
26 | required: true
27 |
28 | - type: textarea
29 | id: widgets-used
30 | attributes:
31 | label: Adaptive Widgets Used
32 | description: Which Adaptive Platform UI widgets did you use?
33 | placeholder: |
34 | - AdaptiveScaffold
35 | - AdaptiveButton
36 | - AdaptiveTextField
37 | validations:
38 | required: false
39 |
40 | - type: textarea
41 | id: screenshots
42 | attributes:
43 | label: Screenshots/Demo
44 | description: Share screenshots, GIFs, or video links
45 | placeholder: |
46 | 
47 | [Demo Video](url-to-video)
48 | validations:
49 | required: false
50 |
51 | - type: textarea
52 | id: experience
53 | attributes:
54 | label: Your Experience
55 | description: How was your experience using the package? Any challenges or wins?
56 | placeholder: Building with adaptive_platform_ui was...
57 | validations:
58 | required: false
59 |
60 | - type: input
61 | id: links
62 | attributes:
63 | label: Links
64 | description: App Store, Google Play, website, or repository link (if public)
65 | placeholder: https://...
66 | validations:
67 | required: false
68 |
69 | - type: dropdown
70 | id: platforms
71 | attributes:
72 | label: Platforms
73 | description: Which platforms does your app support?
74 | multiple: true
75 | options:
76 | - iOS
77 | - Android
78 | - Web
79 | - Desktop
80 | validations:
81 | required: false
82 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build-and-release:
10 | name: Build and Release
11 | runs-on: ubuntu-latest
12 |
13 | permissions:
14 | contents: write # Required to create releases
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Setup Flutter
23 | uses: subosito/flutter-action@v2
24 | with:
25 | flutter-version: '3.35.6'
26 | channel: 'stable'
27 | cache: true
28 |
29 | - name: Get package dependencies
30 | run: flutter pub get
31 |
32 | - name: Get example dependencies
33 | run: |
34 | cd example
35 | flutter pub get
36 |
37 | - name: Extract version from tag
38 | id: version
39 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
40 |
41 | - name: Build Android APK
42 | run: |
43 | cd example
44 | flutter build apk --release
45 |
46 | - name: Rename APK with version
47 | run: |
48 | cd example/build/app/outputs/flutter-apk
49 | mv app-release.apk adaptive_platform_ui_example_v${{ steps.version.outputs.VERSION }}.apk
50 |
51 | - name: Extract changelog for this version
52 | id: changelog
53 | run: |
54 | VERSION=${{ steps.version.outputs.VERSION }}
55 | # Extract changelog section for this version
56 | sed -n "/## \[$VERSION\]/,/## \[/p" CHANGELOG.md | sed '$d' > release_notes.md
57 | # If empty, use a default message
58 | if [ ! -s release_notes.md ]; then
59 | echo "Release $VERSION" > release_notes.md
60 | echo "" >> release_notes.md
61 | echo "See [CHANGELOG.md](https://github.com/berkaycatak/adaptive_platform_ui/blob/main/CHANGELOG.md) for details." >> release_notes.md
62 | fi
63 |
64 | - name: Create GitHub Release
65 | uses: softprops/action-gh-release@v1
66 | with:
67 | name: Release v${{ steps.version.outputs.VERSION }}
68 | body_path: release_notes.md
69 | files: |
70 | example/build/app/outputs/flutter-apk/adaptive_platform_ui_example_v${{ steps.version.outputs.VERSION }}.apk
71 | draft: false
72 | prerelease: false
73 | env:
74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75 |
76 | - name: Upload build artifacts
77 | uses: actions/upload-artifact@v4
78 | with:
79 | name: release-apk
80 | path: example/build/app/outputs/flutter-apk/*.apk
81 | retention-days: 30
82 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 | ## Type of Change
5 |
6 | - [ ] Bug fix (non-breaking change which fixes an issue)
7 | - [ ] New feature (non-breaking change which adds functionality)
8 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
9 | - [ ] Documentation update
10 | - [ ] Code refactoring
11 | - [ ] Performance improvement
12 |
13 | ## Related Issues
14 |
15 | Closes #
16 |
17 | ## Changes Made
18 |
19 | -
20 | -
21 | -
22 |
23 | ## Testing
24 |
25 | ### Automated Tests ⚠️ **REQUIRED**
26 |
27 | - [ ] I have added unit/widget tests for my changes
28 | - [ ] All new and existing tests pass locally (`flutter test`)
29 | - [ ] Code analysis passes with no errors (`flutter analyze`)
30 | - [ ] Code formatting is correct (`dart format`)
31 | - [ ] Test coverage is adequate (>80% for new code)
32 |
33 | ### Manual Testing
34 |
35 | - [ ] iOS 26+ tested
36 | - [ ] iOS <26 tested
37 | - [ ] Android tested
38 | - [ ] Web tested (if applicable)
39 | - [ ] Tested in both light and dark mode
40 | - [ ] Tested with different screen sizes
41 | - [ ] Tested with accessibility features (large fonts, screen readers, etc.)
42 |
43 | ## Screenshots/Videos
44 |
45 |
46 | ### Before
47 |
48 |
49 | ### After
50 |
51 |
52 | ## Checklist
53 |
54 | - [ ] My code follows the style guidelines of this project
55 | - [ ] I have performed a self-review of my own code
56 | - [ ] I have commented my code, particularly in hard-to-understand areas
57 | - [ ] I have made corresponding changes to the documentation
58 | - [ ] My changes generate no new warnings
59 | - [ ] I have checked that my code does not introduce any accessibility issues
60 | - [ ] I have updated the CHANGELOG.md file (if applicable)
61 | - [ ] I have updated version number in pubspec.yaml (if applicable)
62 | - [ ] I have added examples to the example app (if adding new widgets)
63 |
64 | ## Breaking Changes
65 |
66 |
67 | ## Additional Notes
68 |
69 |
70 | ## Demo Code
71 |
72 | ```dart
73 | // Example usage
74 | ```
75 |
--------------------------------------------------------------------------------
/test/platform_info_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
3 |
4 | void main() {
5 | group('PlatformInfo', () {
6 | test('returns valid platform type', () {
7 | // At least one platform should be detected
8 | final hasValidPlatform =
9 | PlatformInfo.isIOS ||
10 | PlatformInfo.isAndroid ||
11 | PlatformInfo.isMacOS ||
12 | PlatformInfo.isWindows ||
13 | PlatformInfo.isLinux ||
14 | PlatformInfo.isFuchsia ||
15 | PlatformInfo.isWeb;
16 |
17 | expect(hasValidPlatform, isTrue);
18 | });
19 |
20 | test('iOS version methods work correctly', () {
21 | if (PlatformInfo.isIOS) {
22 | expect(PlatformInfo.iOSVersion, greaterThanOrEqualTo(0));
23 |
24 | // Version checks should be consistent
25 | final version = PlatformInfo.iOSVersion;
26 | if (version >= 26) {
27 | expect(PlatformInfo.isIOS26OrHigher(), isTrue);
28 | expect(PlatformInfo.isIOS18OrLower(), isFalse);
29 | } else if (version > 0 && version < 26) {
30 | expect(PlatformInfo.isIOS26OrHigher(), isFalse);
31 | expect(PlatformInfo.isIOS18OrLower(), isTrue);
32 | }
33 | } else {
34 | // Non-iOS platforms should return 0 for iOS version
35 | expect(PlatformInfo.iOSVersion, equals(0));
36 | expect(PlatformInfo.isIOS26OrHigher(), isFalse);
37 | expect(PlatformInfo.isIOS18OrLower(), isFalse);
38 | }
39 | });
40 |
41 | test('isIOSVersionInRange works correctly', () {
42 | if (PlatformInfo.isIOS) {
43 | final version = PlatformInfo.iOSVersion;
44 | if (version > 0) {
45 | expect(PlatformInfo.isIOSVersionInRange(version, version), isTrue);
46 | expect(
47 | PlatformInfo.isIOSVersionInRange(version - 1, version + 1),
48 | isTrue,
49 | );
50 | expect(
51 | PlatformInfo.isIOSVersionInRange(version + 1, version + 2),
52 | isFalse,
53 | );
54 | }
55 | } else {
56 | expect(PlatformInfo.isIOSVersionInRange(1, 100), isFalse);
57 | }
58 | });
59 |
60 | test('platformDescription returns non-empty string', () {
61 | expect(PlatformInfo.platformDescription.isNotEmpty, isTrue);
62 | });
63 |
64 | test('only one primary platform is detected', () {
65 | // On native platforms, only one platform should be true
66 | if (!PlatformInfo.isWeb) {
67 | final platformCount = [
68 | PlatformInfo.isIOS,
69 | PlatformInfo.isAndroid,
70 | PlatformInfo.isMacOS,
71 | PlatformInfo.isWindows,
72 | PlatformInfo.isLinux,
73 | PlatformInfo.isFuchsia,
74 | ].where((p) => p).length;
75 |
76 | expect(platformCount, equals(1));
77 | }
78 | });
79 | });
80 | }
81 |
--------------------------------------------------------------------------------
/lib/adaptive_platform_ui.dart:
--------------------------------------------------------------------------------
1 | /// A Flutter package that provides adaptive platform-specific widgets
2 | ///
3 | /// This package automatically renders native-looking widgets based on the platform:
4 | /// - iOS 26+: Modern iOS 26 native designs with latest visual styles
5 | /// - iOS <26 (iOS 18 and below): Traditional Cupertino widgets
6 | /// - Android: Material Design widgets
7 | ///
8 | /// ## Features
9 | ///
10 | /// - Automatic platform detection
11 | /// - iOS version-specific widget rendering
12 | /// - Native iOS 26 designs following Apple's Human Interface Guidelines
13 | /// - Seamless fallback to appropriate widgets for older iOS versions
14 | /// - Material Design for Android
15 | ///
16 | /// ## Usage
17 | ///
18 | /// ```dart
19 | /// import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
20 | ///
21 | /// AdaptiveButton(
22 | /// onPressed: () {
23 | /// print('Button pressed');
24 | /// },
25 | /// child: Text('Click Me'),
26 | /// )
27 | /// ```
28 | library;
29 |
30 | // Platform utilities
31 | export 'src/platform/platform_info.dart';
32 |
33 | // Styles
34 | export 'src/style/sf_symbol.dart';
35 |
36 | // Widgets
37 | export 'src/widgets/adaptive_app.dart';
38 | export 'src/widgets/adaptive_app_bar.dart';
39 | export 'src/widgets/adaptive_bottom_navigation_bar.dart';
40 | export 'src/widgets/adaptive_button.dart';
41 | export 'src/widgets/adaptive_switch.dart';
42 | export 'src/widgets/adaptive_checkbox.dart';
43 | export 'src/widgets/adaptive_radio.dart';
44 | export 'src/widgets/adaptive_card.dart';
45 | export 'src/widgets/adaptive_badge.dart';
46 | export 'src/widgets/adaptive_tooltip.dart';
47 | export 'src/widgets/adaptive_slider.dart';
48 | export 'src/widgets/adaptive_segmented_control.dart';
49 | export 'src/widgets/adaptive_alert_dialog.dart';
50 | export 'src/widgets/adaptive_popup_menu_button.dart';
51 | export 'src/widgets/adaptive_context_menu.dart';
52 | export 'src/widgets/adaptive_scaffold.dart';
53 | export 'src/widgets/adaptive_app_bar_action.dart';
54 | export 'src/widgets/adaptive_snackbar.dart';
55 | export 'src/widgets/adaptive_date_picker.dart';
56 | export 'src/widgets/adaptive_time_picker.dart';
57 | export 'src/widgets/adaptive_list_tile.dart';
58 | export 'src/widgets/adaptive_text_field.dart';
59 | export 'src/widgets/adaptive_text_form_field.dart';
60 | export 'src/widgets/adaptive_tab_view.dart';
61 | export 'src/widgets/adaptive_floating_action_button.dart';
62 | export 'src/widgets/adaptive_form_section.dart';
63 | export 'src/widgets/adaptive_expansion_tile.dart';
64 | export 'src/widgets/adaptive_blur_view.dart';
65 |
66 | // iOS 26 specific widgets (for advanced usage)
67 | export 'src/widgets/ios26/ios26_button.dart';
68 | export 'src/widgets/ios26/ios26_switch.dart';
69 | export 'src/widgets/ios26/ios26_slider.dart';
70 | export 'src/widgets/ios26/ios26_segmented_control.dart';
71 | export 'src/widgets/ios26/ios26_alert_dialog.dart';
72 | export 'src/widgets/ios26/ios26_native_search_tab_bar.dart';
73 | export 'src/widgets/ios26/ios26_native_tab_bar.dart';
74 | export 'src/widgets/ios26/ios26_scaffold_legacy.dart';
75 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/src/platform/platform_info.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'package:flutter/foundation.dart';
3 |
4 | /// Provides platform detection and iOS version information
5 | ///
6 | /// This class helps determine the current platform and iOS version
7 | /// to enable adaptive widget rendering based on platform capabilities.
8 | class PlatformInfo {
9 | /// Returns true if the current platform is iOS
10 | static bool get isIOS => !kIsWeb && Platform.isIOS;
11 |
12 | /// Returns true if the current platform is Android
13 | static bool get isAndroid => !kIsWeb && Platform.isAndroid;
14 |
15 | /// Returns true if the current platform is macOS
16 | static bool get isMacOS => !kIsWeb && Platform.isMacOS;
17 |
18 | /// Returns true if the current platform is Windows
19 | static bool get isWindows => !kIsWeb && Platform.isWindows;
20 |
21 | /// Returns true if the current platform is Linux
22 | static bool get isLinux => !kIsWeb && Platform.isLinux;
23 |
24 | /// Returns true if the current platform is Fuchsia
25 | static bool get isFuchsia => !kIsWeb && Platform.isFuchsia;
26 |
27 | /// Returns true if running on web
28 | static bool get isWeb => kIsWeb;
29 |
30 | /// Returns the iOS major version number
31 | ///
32 | /// Returns 0 if not running on iOS or if version cannot be determined.
33 | /// Example: For iOS 26.1.2, returns 26
34 | static int get iOSVersion {
35 | if (!isIOS) return 0;
36 |
37 | try {
38 | final version = Platform.operatingSystemVersion;
39 | // Extract major version from string like "Version 26.1.2 (Build 20A123)"
40 | final match = RegExp(r'Version (\d+)').firstMatch(version);
41 | if (match != null) {
42 | return int.parse(match.group(1)!);
43 | }
44 |
45 | // Fallback: try to parse the first number in the version string
46 | final fallbackMatch = RegExp(r'(\d+)').firstMatch(version);
47 | if (fallbackMatch != null) {
48 | return int.parse(fallbackMatch.group(1)!);
49 | }
50 | } catch (e) {
51 | debugPrint('Error parsing iOS version: $e');
52 | }
53 |
54 | return 0;
55 | }
56 |
57 | /// Returns true if iOS version is 26 or higher
58 | ///
59 | /// This is used to determine if iOS 26+ specific widgets should be used.
60 | static bool isIOS26OrHigher() {
61 | return isIOS && iOSVersion >= 26;
62 | }
63 |
64 | /// Returns true if iOS version is 18 or lower (pre-iOS 26)
65 | ///
66 | /// This is used to determine if legacy Cupertino widgets should be used.
67 | static bool isIOS18OrLower() {
68 | return isIOS && iOSVersion > 0 && iOSVersion < 26;
69 | }
70 |
71 | /// Returns true if iOS version is in a specific range
72 | ///
73 | /// [min] - Minimum iOS version (inclusive)
74 | /// [max] - Maximum iOS version (inclusive)
75 | static bool isIOSVersionInRange(int min, int max) {
76 | return isIOS && iOSVersion >= min && iOSVersion <= max;
77 | }
78 |
79 | /// Returns a human-readable platform description
80 | static String get platformDescription {
81 | if (isIOS) return 'iOS $iOSVersion';
82 | if (isAndroid) return 'Android';
83 | if (isMacOS) return 'macOS';
84 | if (isWindows) return 'Windows';
85 | if (isLinux) return 'Linux';
86 | if (isFuchsia) return 'Fuchsia';
87 | if (isWeb) return 'Web';
88 | return 'Unknown';
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_app_bar_action.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | /// Spacer type for toolbar items (iOS 26+ only)
5 | enum ToolbarSpacerType {
6 | /// No spacer
7 | none,
8 |
9 | /// Fixed 12pt space - groups items within same section
10 | fixed,
11 |
12 | /// Flexible space - separates item groups (pushes next items to opposite side)
13 | flexible,
14 | }
15 |
16 | /// An app bar action that can be displayed in AdaptiveScaffold
17 | ///
18 | /// - On iOS 26+: Uses iosSymbol (SF Symbol) in native UIToolbar
19 | /// - On iOS < 26: Uses icon (IconData) in CupertinoNavigationBar
20 | /// - On Android: Uses icon (IconData) in Material AppBar
21 | class AdaptiveAppBarAction {
22 | const AdaptiveAppBarAction({
23 | this.iosSymbol,
24 | this.icon,
25 | this.title,
26 | required this.onPressed,
27 | this.spacerAfter = ToolbarSpacerType.none,
28 | }) : assert(
29 | iosSymbol != null || icon != null || title != null,
30 | 'At least one of iosSymbol, icon, or title must be provided',
31 | );
32 |
33 | /// SF Symbol name for iOS 26+ ONLY (e.g., 'info.circle', 'plus.circle')
34 | /// - iOS 26+: Uses UIImage(systemName:) in native UIBarButtonItem
35 | /// - iOS <26: NOT used, use icon parameter instead
36 | /// - Android: NOT used, use icon parameter instead
37 | final String? iosSymbol;
38 |
39 | /// Icon for iOS <26 and Android (e.g., Icons.info, CupertinoIcons.info)
40 | /// - iOS 26+: NOT used (iosSymbol takes priority)
41 | /// - iOS <26: Used for CupertinoButton
42 | /// - Android: Used for IconButton
43 | final IconData? icon;
44 |
45 | /// Text title for the action (optional)
46 | /// If provided along with icons, title takes precedence
47 | final String? title;
48 |
49 | /// Callback when the action is tapped
50 | final VoidCallback onPressed;
51 |
52 | /// Add spacer after this action in iOS 26+ toolbar
53 | /// - `none`: No spacer (default)
54 | /// - `fixed`: 12pt fixed space - groups items within same section
55 | /// - `flexible`: Flexible space - separates item groups (e.g., left vs right groups)
56 | ///
57 | /// Example: For Undo/Redo on left and Markup/More on right:
58 | /// ```dart
59 | /// actions: [
60 | /// AdaptiveAppBarAction(iosSymbol: 'arrow.uturn.backward', ...),
61 | /// AdaptiveAppBarAction(iosSymbol: 'arrow.uturn.forward', ..., spacerAfter: ToolbarSpacerType.flexible),
62 | /// AdaptiveAppBarAction(iosSymbol: 'pencil', ...),
63 | /// AdaptiveAppBarAction(iosSymbol: 'ellipsis', ...),
64 | /// ]
65 | /// ```
66 | final ToolbarSpacerType spacerAfter;
67 |
68 | @override
69 | bool operator ==(Object other) {
70 | if (identical(this, other)) return true;
71 | return other is AdaptiveAppBarAction &&
72 | other.iosSymbol == iosSymbol &&
73 | other.icon == icon &&
74 | other.title == title;
75 | }
76 |
77 | @override
78 | int get hashCode => Object.hash(iosSymbol, icon, title);
79 |
80 | /// Convert action to map for native platform channel (iOS 26+ only)
81 | Map toNativeMap() {
82 | return {
83 | if (iosSymbol != null) 'icon': iosSymbol!,
84 | if (title != null) 'title': title!,
85 | 'spacerAfter': spacerAfter.index, // 0=none, 1=fixed, 2=flexible
86 | };
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ios/Classes/AdaptivePlatformUiPlugin.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | /// Main plugin class for Adaptive Platform UI
5 | /// Registers platform views and handles plugin lifecycle
6 | public class AdaptivePlatformUiPlugin: NSObject, FlutterPlugin {
7 |
8 | public static func register(with registrar: FlutterPluginRegistrar) {
9 | // Initialize iOS 26+ Native Tab Bar Manager
10 | if #available(iOS 26.0, *) {
11 | iOS26NativeTabBarManager.shared.setup(messenger: registrar.messenger())
12 | }
13 |
14 | // Register iOS 26 Button platform view factory
15 | let ios26ButtonFactory = iOS26ButtonViewFactory(messenger: registrar.messenger())
16 | registrar.register(
17 | ios26ButtonFactory,
18 | withId: "adaptive_platform_ui/ios26_button"
19 | )
20 |
21 | // Register iOS 26 Switch platform view factory
22 | let ios26SwitchFactory = iOS26SwitchViewFactory(messenger: registrar.messenger())
23 | registrar.register(
24 | ios26SwitchFactory,
25 | withId: "adaptive_platform_ui/ios26_switch"
26 | )
27 |
28 | // Register iOS 26 Slider platform view factory
29 | let ios26SliderFactory = iOS26SliderViewFactory(messenger: registrar.messenger())
30 | registrar.register(
31 | ios26SliderFactory,
32 | withId: "adaptive_platform_ui/ios26_slider"
33 | )
34 |
35 | // Register iOS 26 SegmentedControl platform view factory
36 | let ios26SegmentedControlFactory = iOS26SegmentedControlViewFactory(messenger: registrar.messenger())
37 | registrar.register(
38 | ios26SegmentedControlFactory,
39 | withId: "adaptive_platform_ui/ios26_segmented_control"
40 | )
41 |
42 | // Register iOS 26 AlertDialog platform view factory
43 | let ios26AlertDialogFactory = iOS26AlertDialogViewFactory(messenger: registrar.messenger())
44 | registrar.register(
45 | ios26AlertDialogFactory,
46 | withId: "adaptive_platform_ui/ios26_alert_dialog"
47 | )
48 |
49 | // Register iOS 26 PopupMenuButton platform view factory
50 | let ios26PopupMenuButtonFactory = iOS26PopupMenuButtonViewFactory(messenger: registrar.messenger())
51 | registrar.register(
52 | ios26PopupMenuButtonFactory,
53 | withId: "adaptive_platform_ui/ios26_popup_menu_button"
54 | )
55 |
56 | // Register iOS 26 TabBar platform view factory
57 | let ios26TabBarFactory = iOS26TabBarViewFactory(messenger: registrar.messenger())
58 | registrar.register(
59 | ios26TabBarFactory,
60 | withId: "adaptive_platform_ui/ios26_tab_bar"
61 | )
62 |
63 | // Register iOS 26 Toolbar platform view factory
64 | let ios26ToolbarFactory = iOS26ToolbarFactory(messenger: registrar.messenger())
65 | registrar.register(
66 | ios26ToolbarFactory,
67 | withId: "adaptive_platform_ui/ios26_toolbar"
68 | )
69 |
70 | // Register iOS 26 Blur View platform view factory
71 | let ios26BlurViewFactory = iOS26BlurViewFactory(messenger: registrar.messenger())
72 | registrar.register(
73 | ios26BlurViewFactory,
74 | withId: "adaptive_platform_ui/ios26_blur_view"
75 | )
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/example/lib/pages/demos/tab_view_demo_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
3 |
4 | class TabViewDemoPage extends StatelessWidget {
5 | const TabViewDemoPage({super.key});
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | return AdaptiveScaffold(
10 | appBar: AdaptiveAppBar(title: 'Tab Bar View Demo'),
11 | body: SafeArea(
12 | bottom: false,
13 | child: Padding(
14 | padding: EdgeInsets.only(
15 | top: PlatformInfo.isIOS26OrHigher() ? 48.0 : 0,
16 | ),
17 | child: AdaptiveTabBarView(
18 | tabs: const ['Latest', 'Popular', 'Trending', 'Featured'],
19 | selectedColor: Colors.white,
20 | unselectedColor: Colors.white.withValues(alpha: 0.6),
21 | onTabChanged: (index) {
22 | // Tab changed callback
23 | },
24 | children: [
25 | _buildContent(
26 | context,
27 | title: 'Latest',
28 | color: Colors.blue,
29 | description: 'Most recent content',
30 | ),
31 | _buildContent(
32 | context,
33 | title: 'Popular',
34 | color: Colors.green,
35 | description: 'Most viewed content',
36 | ),
37 | _buildContent(
38 | context,
39 | title: 'Trending',
40 | color: Colors.orange,
41 | description: 'Trending now',
42 | ),
43 | _buildContent(
44 | context,
45 | title: 'Featured',
46 | color: Colors.purple,
47 | description: 'Featured content',
48 | ),
49 | ],
50 | ),
51 | ),
52 | ),
53 | );
54 | }
55 |
56 | Widget _buildContent(
57 | BuildContext context, {
58 | required String title,
59 | required Color color,
60 | required String description,
61 | }) {
62 | return Container(
63 | color: color.withValues(alpha: 0.1),
64 | child: Center(
65 | child: Padding(
66 | padding: const EdgeInsets.all(24.0),
67 | child: Column(
68 | mainAxisAlignment: MainAxisAlignment.center,
69 | children: [
70 | Icon(Icons.article, size: 80, color: color),
71 | const SizedBox(height: 24),
72 | Text(
73 | title,
74 | style: Theme.of(context).textTheme.headlineMedium?.copyWith(
75 | color: color,
76 | fontWeight: FontWeight.bold,
77 | ),
78 | ),
79 | const SizedBox(height: 16),
80 | Text(
81 | description,
82 | style: Theme.of(context).textTheme.bodyLarge,
83 | textAlign: TextAlign.center,
84 | ),
85 | const SizedBox(height: 32),
86 | Text(
87 | 'Swipe left or right to switch tabs',
88 | style: Theme.of(context).textTheme.bodyMedium?.copyWith(
89 | color: Colors.grey,
90 | fontStyle: FontStyle.italic,
91 | ),
92 | textAlign: TextAlign.center,
93 | ),
94 | ],
95 | ),
96 | ),
97 | ),
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/test/adaptive_tab_view_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
4 |
5 | void main() {
6 | group('AdaptiveTabBarView', () {
7 | testWidgets('creates tab bar view with tabs', (WidgetTester tester) async {
8 | await tester.pumpWidget(
9 | MaterialApp(
10 | home: Scaffold(
11 | body: AdaptiveTabBarView(
12 | tabs: const ['Tab 1', 'Tab 2', 'Tab 3'],
13 | children: const [
14 | Center(child: Text('Page 1')),
15 | Center(child: Text('Page 2')),
16 | Center(child: Text('Page 3')),
17 | ],
18 | ),
19 | ),
20 | ),
21 | );
22 |
23 | expect(find.text('Tab 1'), findsOneWidget);
24 | expect(find.text('Tab 2'), findsOneWidget);
25 | expect(find.text('Tab 3'), findsOneWidget);
26 | });
27 |
28 | testWidgets('switches between tab pages', (WidgetTester tester) async {
29 | await tester.pumpWidget(
30 | MaterialApp(
31 | home: Scaffold(
32 | body: AdaptiveTabBarView(
33 | tabs: const ['Tab 1', 'Tab 2'],
34 | children: const [
35 | Center(child: Text('Page 1')),
36 | Center(child: Text('Page 2')),
37 | ],
38 | ),
39 | ),
40 | ),
41 | );
42 |
43 | // Initial page should be Page 1
44 | expect(find.text('Page 1'), findsOneWidget);
45 |
46 | // Tap on Tab 2
47 | await tester.tap(find.text('Tab 2'));
48 | await tester.pumpAndSettle();
49 |
50 | // Page 2 should be visible
51 | expect(find.text('Page 2'), findsOneWidget);
52 | });
53 |
54 | testWidgets('supports swipe gesture', (WidgetTester tester) async {
55 | await tester.pumpWidget(
56 | MaterialApp(
57 | home: Scaffold(
58 | body: AdaptiveTabBarView(
59 | tabs: const ['Tab 1', 'Tab 2'],
60 | children: const [
61 | Center(child: Text('Page 1')),
62 | Center(child: Text('Page 2')),
63 | ],
64 | ),
65 | ),
66 | ),
67 | );
68 |
69 | // Find the PageView widget (it contains the swipeable content)
70 | final pageView = find.byType(PageView);
71 |
72 | // Swipe left to go to next page
73 | await tester.drag(pageView, const Offset(-400, 0));
74 | await tester.pumpAndSettle();
75 |
76 | // Page 2 should be visible after swipe
77 | expect(find.text('Page 2'), findsOneWidget);
78 | });
79 |
80 | testWidgets('calls onTabChanged callback', (WidgetTester tester) async {
81 | int? selectedIndex;
82 |
83 | await tester.pumpWidget(
84 | MaterialApp(
85 | home: Scaffold(
86 | body: AdaptiveTabBarView(
87 | tabs: const ['Tab 1', 'Tab 2'],
88 | children: const [
89 | Center(child: Text('Page 1')),
90 | Center(child: Text('Page 2')),
91 | ],
92 | onTabChanged: (index) {
93 | selectedIndex = index;
94 | },
95 | ),
96 | ),
97 | ),
98 | );
99 |
100 | await tester.tap(find.text('Tab 2'));
101 | await tester.pumpAndSettle();
102 |
103 | expect(selectedIndex, 1);
104 | });
105 | });
106 | }
107 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_app_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'adaptive_app_bar_action.dart';
3 |
4 | /// Configuration for an adaptive app bar
5 | ///
6 | /// This class holds the configuration for the app bar in [AdaptiveScaffold].
7 | /// The actual rendering is platform-specific:
8 | /// - iOS 26+ with useNativeToolbar: Native UIToolbar with Liquid Glass effects
9 | /// - iOS 26+ without useNativeToolbar: CupertinoNavigationBar with custom back button
10 | /// - iOS <26: CupertinoNavigationBar
11 | /// - Android: Material AppBar
12 | ///
13 | /// You can provide custom navigation bars using [cupertinoNavigationBar] or [appBar]:
14 | /// - If [cupertinoNavigationBar] is provided and [useNativeToolbar] is false: Uses custom CupertinoNavigationBar on iOS
15 | /// - If [appBar] is provided: Uses custom AppBar on Android
16 | /// - Otherwise: Builds navigation bar from [title], [actions], and [leading]
17 | class AdaptiveAppBar {
18 | /// Creates an adaptive app bar configuration
19 | const AdaptiveAppBar({
20 | this.title,
21 | this.actions,
22 | this.leading,
23 | this.useNativeToolbar = true,
24 | this.cupertinoNavigationBar,
25 | this.appBar,
26 | });
27 |
28 | /// Title for the app bar
29 | final String? title;
30 |
31 | /// Action buttons in the app bar
32 | /// - iOS 26+ with native toolbar: Rendered as native UIBarButtonItem in UIToolbar
33 | /// - iOS < 26: Rendered as buttons in CupertinoNavigationBar
34 | /// - Android: Rendered as IconButtons in Material AppBar
35 | final List? actions;
36 |
37 | /// Leading widget in the app bar (e.g., back button, menu button)
38 | /// If null and navigation is possible, an automatic back button will be shown
39 | final Widget? leading;
40 |
41 | /// Use native iOS 26 toolbar (iOS 26+ only)
42 | /// - When false (default): Uses CupertinoNavigationBar for better compatibility with routers
43 | /// - When true: Uses native iOS 26 UIToolbar with Liquid Glass effect
44 | ///
45 | /// Note: Setting this to true may cause compatibility issues with GoRouter and other
46 | /// router packages. Use with caution.
47 | ///
48 | /// If true, [cupertinoNavigationBar] will be ignored and native toolbar will be shown.
49 | final bool useNativeToolbar;
50 |
51 | /// Custom CupertinoNavigationBar for iOS
52 | ///
53 | /// When provided and [useNativeToolbar] is false, this custom navigation bar will be used
54 | /// instead of building one from [title], [actions], and [leading].
55 | ///
56 | /// Ignored when [useNativeToolbar] is true or on non-iOS platforms.
57 | final PreferredSizeWidget? cupertinoNavigationBar;
58 |
59 | /// Custom AppBar for Android
60 | ///
61 | /// When provided, this custom app bar will be used instead of building one
62 | /// from [title], [actions], and [leading].
63 | ///
64 | /// Ignored on iOS platforms.
65 | final PreferredSizeWidget? appBar;
66 |
67 | /// Creates a copy of this app bar with the given fields replaced
68 | AdaptiveAppBar copyWith({
69 | String? title,
70 | List? actions,
71 | Widget? leading,
72 | bool? useNativeToolbar,
73 | PreferredSizeWidget? cupertinoNavigationBar,
74 | PreferredSizeWidget? appBar,
75 | }) {
76 | return AdaptiveAppBar(
77 | title: title ?? this.title,
78 | actions: actions ?? this.actions,
79 | leading: leading ?? this.leading,
80 | useNativeToolbar: useNativeToolbar ?? this.useNativeToolbar,
81 | cupertinoNavigationBar:
82 | cupertinoNavigationBar ?? this.cupertinoNavigationBar,
83 | appBar: appBar ?? this.appBar,
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/adaptive_floating_action_button_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
4 |
5 | void main() {
6 | group('AdaptiveFloatingActionButton', () {
7 | testWidgets('creates FAB with icon', (WidgetTester tester) async {
8 | await tester.pumpWidget(
9 | MaterialApp(
10 | home: Scaffold(
11 | floatingActionButton: AdaptiveFloatingActionButton(
12 | onPressed: () {},
13 | child: const Icon(Icons.add),
14 | ),
15 | ),
16 | ),
17 | );
18 |
19 | expect(find.byType(AdaptiveFloatingActionButton), findsOneWidget);
20 | expect(find.byIcon(Icons.add), findsOneWidget);
21 | });
22 |
23 | testWidgets('calls onPressed when tapped', (WidgetTester tester) async {
24 | var pressed = false;
25 |
26 | await tester.pumpWidget(
27 | MaterialApp(
28 | home: Scaffold(
29 | floatingActionButton: AdaptiveFloatingActionButton(
30 | onPressed: () {
31 | pressed = true;
32 | },
33 | child: const Icon(Icons.add),
34 | ),
35 | ),
36 | ),
37 | );
38 |
39 | await tester.tap(find.byType(AdaptiveFloatingActionButton));
40 | await tester.pumpAndSettle();
41 |
42 | expect(pressed, true);
43 | });
44 |
45 | testWidgets('creates mini FAB when mini is true', (
46 | WidgetTester tester,
47 | ) async {
48 | await tester.pumpWidget(
49 | MaterialApp(
50 | home: Scaffold(
51 | floatingActionButton: AdaptiveFloatingActionButton(
52 | onPressed: () {},
53 | mini: true,
54 | child: const Icon(Icons.add),
55 | ),
56 | ),
57 | ),
58 | );
59 |
60 | expect(find.byType(AdaptiveFloatingActionButton), findsOneWidget);
61 | });
62 |
63 | testWidgets('respects custom colors', (WidgetTester tester) async {
64 | await tester.pumpWidget(
65 | MaterialApp(
66 | home: Scaffold(
67 | floatingActionButton: AdaptiveFloatingActionButton(
68 | onPressed: () {},
69 | backgroundColor: Colors.red,
70 | foregroundColor: Colors.white,
71 | child: const Icon(Icons.add),
72 | ),
73 | ),
74 | ),
75 | );
76 |
77 | expect(find.byType(AdaptiveFloatingActionButton), findsOneWidget);
78 | });
79 |
80 | testWidgets('handles null onPressed (disabled state)', (
81 | WidgetTester tester,
82 | ) async {
83 | await tester.pumpWidget(
84 | MaterialApp(
85 | home: Scaffold(
86 | floatingActionButton: AdaptiveFloatingActionButton(
87 | onPressed: null,
88 | child: const Icon(Icons.add),
89 | ),
90 | ),
91 | ),
92 | );
93 |
94 | expect(find.byType(AdaptiveFloatingActionButton), findsOneWidget);
95 | });
96 |
97 | testWidgets('supports hero tag for transitions', (
98 | WidgetTester tester,
99 | ) async {
100 | await tester.pumpWidget(
101 | MaterialApp(
102 | home: Scaffold(
103 | floatingActionButton: AdaptiveFloatingActionButton(
104 | onPressed: () {},
105 | heroTag: 'fab_hero',
106 | child: const Icon(Icons.add),
107 | ),
108 | ),
109 | ),
110 | );
111 |
112 | expect(find.byType(AdaptiveFloatingActionButton), findsOneWidget);
113 | expect(find.byType(Hero), findsOneWidget);
114 | });
115 | });
116 | }
117 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_switch.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 | import 'ios26/ios26_switch.dart';
5 |
6 | /// An adaptive switch that renders platform-specific switch styles
7 | ///
8 | /// On iOS 26+: Uses native iOS 26 UISwitch with native animations
9 | /// On iOS <26 (iOS 18 and below): Uses CupertinoSwitch with traditional iOS styling
10 | /// On Android: Uses Material Design Switch
11 | ///
12 | /// Example:
13 | /// ```dart
14 | /// bool _value = false;
15 | ///
16 | /// AdaptiveSwitch(
17 | /// value: _value,
18 | /// onChanged: (bool newValue) {
19 | /// setState(() {
20 | /// _value = newValue;
21 | /// });
22 | /// },
23 | /// )
24 | /// ```
25 | class AdaptiveSwitch extends StatelessWidget {
26 | /// Creates an adaptive switch
27 | const AdaptiveSwitch({
28 | super.key,
29 | required this.value,
30 | required this.onChanged,
31 | this.activeColor,
32 | this.thumbColor,
33 | });
34 |
35 | /// Whether this switch is on or off
36 | final bool value;
37 |
38 | /// Called when the user toggles the switch on or off
39 | ///
40 | /// The switch passes the new value to the callback but does not actually
41 | /// change state until the parent widget rebuilds the switch with the new
42 | /// value.
43 | ///
44 | /// If null, the switch will be displayed as disabled.
45 | final ValueChanged? onChanged;
46 |
47 | /// The color to use when this switch is on
48 | ///
49 | /// On iOS: Uses the color for the track when on
50 | /// On Android: Uses the color for the track
51 | final Color? activeColor;
52 |
53 | /// The color of the thumb (handle)
54 | ///
55 | /// On iOS: The color of the circular knob
56 | /// On Android: The color of the circular knob
57 | final Color? thumbColor;
58 |
59 | @override
60 | Widget build(BuildContext context) {
61 | // iOS 26+ - Use native iOS 26 switch
62 | if (PlatformInfo.isIOS26OrHigher()) {
63 | return IOS26Switch(
64 | value: value,
65 | onChanged: onChanged,
66 | activeColor: activeColor,
67 | thumbColor: thumbColor,
68 | );
69 | }
70 |
71 | // iOS 18 and below - Use traditional CupertinoSwitch
72 | if (PlatformInfo.isIOS) {
73 | return CupertinoSwitch(
74 | value: value,
75 | onChanged: onChanged,
76 | activeTrackColor:
77 | activeColor ?? CupertinoTheme.of(context).primaryColor,
78 | thumbColor: thumbColor,
79 | );
80 | }
81 |
82 | // Android - Use Material Design Switch
83 | if (PlatformInfo.isAndroid) {
84 | return Switch(
85 | value: value,
86 | onChanged: onChanged,
87 | thumbColor: thumbColor != null
88 | ? WidgetStateProperty.all(thumbColor)
89 | : null,
90 | trackColor: activeColor != null
91 | ? WidgetStateProperty.resolveWith((states) {
92 | if (states.contains(WidgetState.selected)) {
93 | return activeColor;
94 | }
95 | return null;
96 | })
97 | : null,
98 | );
99 | }
100 |
101 | // Fallback for other platforms (web, desktop, etc.)
102 | return Switch(
103 | value: value,
104 | onChanged: onChanged,
105 | thumbColor: thumbColor != null
106 | ? WidgetStateProperty.all(thumbColor)
107 | : null,
108 | trackColor: activeColor != null
109 | ? WidgetStateProperty.resolveWith((states) {
110 | if (states.contains(WidgetState.selected)) {
111 | return activeColor;
112 | }
113 | return null;
114 | })
115 | : null,
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/ios/Classes/iOS26ScaffoldManager.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | /// Manager for iOS 26 adaptive scaffold with native tab bar
5 | @available(iOS 13.0, *)
6 | class iOS26ScaffoldManager: NSObject {
7 | private let channel: FlutterMethodChannel
8 | private weak var viewController: UIViewController?
9 | private var tabBarController: UITabBarController?
10 |
11 | init(channel: FlutterMethodChannel, viewController: UIViewController?) {
12 | self.channel = channel
13 | self.viewController = viewController
14 | super.init()
15 |
16 | channel.setMethodCallHandler { [weak self] (call, result) in
17 | self?.handle(call, result: result)
18 | }
19 | }
20 |
21 | private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
22 | switch call.method {
23 | case "setupTabBar":
24 | setupTabBar(call, result: result)
25 | case "setSelectedIndex":
26 | setSelectedIndex(call, result: result)
27 | default:
28 | result(FlutterMethodNotImplemented)
29 | }
30 | }
31 |
32 | private func setupTabBar(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
33 | guard let args = call.arguments as? [String: Any],
34 | let tabsData = args["tabs"] as? [[String: Any]],
35 | let selectedIndex = args["selectedIndex"] as? Int else {
36 | result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments", details: nil))
37 | return
38 | }
39 |
40 | // Get or create tab bar controller
41 | if tabBarController == nil {
42 | tabBarController = UITabBarController()
43 |
44 | // Apply iOS 26 styling if available
45 | if #available(iOS 26.0, *) {
46 | // Configure tab bar appearance for iOS 26 Liquid Glass
47 | let appearance = UITabBarAppearance()
48 | appearance.configureWithDefaultBackground()
49 |
50 | // Apply Liquid Glass effect
51 | if let tabBar = tabBarController?.tabBar {
52 | tabBar.standardAppearance = appearance
53 | tabBar.scrollEdgeAppearance = appearance
54 | }
55 | }
56 | }
57 |
58 | // Create tab bar items
59 | var items: [UITabBarItem] = []
60 | for (index, tabData) in tabsData.enumerated() {
61 | let label = tabData["label"] as? String ?? "Tab \(index + 1)"
62 | let iconName = tabData["icon"] as? String ?? "circle"
63 | let selectedIconName = tabData["selectedIcon"] as? String ?? iconName
64 |
65 | let item = UITabBarItem(
66 | title: label,
67 | image: UIImage(systemName: iconName),
68 | selectedImage: UIImage(systemName: selectedIconName)
69 | )
70 |
71 | items.append(item)
72 | }
73 |
74 | // Update tab bar items
75 | tabBarController?.tabBar.items = items
76 | tabBarController?.selectedIndex = selectedIndex
77 |
78 | // Set up tab bar item selection callback
79 | setupTabBarDelegate()
80 |
81 | result(nil)
82 | }
83 |
84 | private func setupTabBarDelegate() {
85 | // Create a custom delegate to handle tab selection
86 | tabBarController?.delegate = self
87 | }
88 |
89 | private func setSelectedIndex(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
90 | guard let index = call.arguments as? Int else {
91 | result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid index", details: nil))
92 | return
93 | }
94 |
95 | tabBarController?.selectedIndex = index
96 | result(nil)
97 | }
98 | }
99 |
100 | // MARK: - UITabBarControllerDelegate
101 | @available(iOS 13.0, *)
102 | extension iOS26ScaffoldManager: UITabBarControllerDelegate {
103 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
104 | let index = tabBarController.selectedIndex
105 | channel.invokeMethod("onTabSelected", arguments: index)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
44 |
50 |
51 |
52 |
53 |
54 |
66 |
68 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_slider.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 | import 'ios26/ios26_slider.dart';
5 |
6 | /// An adaptive slider that renders platform-specific slider styles
7 | ///
8 | /// On iOS 26+: Uses native iOS 26 UISlider with native animations
9 | /// On iOS <26 (iOS 18 and below): Uses CupertinoSlider with traditional iOS styling
10 | /// On Android: Uses Material Design Slider
11 | ///
12 | /// Example:
13 | /// ```dart
14 | /// double _value = 0.5;
15 | ///
16 | /// AdaptiveSlider(
17 | /// value: _value,
18 | /// onChanged: (double newValue) {
19 | /// setState(() {
20 | /// _value = newValue;
21 | /// });
22 | /// },
23 | /// )
24 | /// ```
25 | class AdaptiveSlider extends StatelessWidget {
26 | /// Creates an adaptive slider
27 | const AdaptiveSlider({
28 | super.key,
29 | required this.value,
30 | required this.onChanged,
31 | this.onChangeStart,
32 | this.onChangeEnd,
33 | this.min = 0.0,
34 | this.max = 1.0,
35 | this.divisions,
36 | this.label,
37 | this.activeColor,
38 | this.thumbColor,
39 | });
40 |
41 | /// The currently selected value for this slider
42 | ///
43 | /// The slider's thumb is drawn at a position that corresponds to this value.
44 | final double value;
45 |
46 | /// Called when the user is selecting a new value for the slider by dragging
47 | ///
48 | /// The slider passes the new value to the callback but does not actually
49 | /// change state until the parent widget rebuilds the slider with the new
50 | /// value.
51 | ///
52 | /// If null, the slider will be displayed as disabled.
53 | final ValueChanged? onChanged;
54 |
55 | /// Called when the user starts selecting a new value for the slider
56 | final ValueChanged? onChangeStart;
57 |
58 | /// Called when the user is done selecting a new value for the slider
59 | final ValueChanged? onChangeEnd;
60 |
61 | /// The minimum value the user can select
62 | final double min;
63 |
64 | /// The maximum value the user can select
65 | final double max;
66 |
67 | /// The number of discrete divisions
68 | ///
69 | /// On iOS: Ignored (native sliders are always continuous)
70 | /// On Android: Used to create discrete steps
71 | final int? divisions;
72 |
73 | /// A label to show above the slider when active
74 | ///
75 | /// On iOS: Ignored (native sliders don't show labels)
76 | /// On Android: Shown as a tooltip
77 | final String? label;
78 |
79 | /// The color of the track when the slider is active
80 | final Color? activeColor;
81 |
82 | /// The color of the thumb
83 | final Color? thumbColor;
84 |
85 | @override
86 | Widget build(BuildContext context) {
87 | // iOS 26+ - Use native iOS 26 slider
88 | if (PlatformInfo.isIOS26OrHigher()) {
89 | return IOS26Slider(
90 | value: value,
91 | onChanged: onChanged,
92 | onChangeStart: onChangeStart,
93 | onChangeEnd: onChangeEnd,
94 | min: min,
95 | max: max,
96 | activeColor: activeColor,
97 | thumbColor: thumbColor,
98 | );
99 | }
100 |
101 | // iOS 18 and below - Use traditional CupertinoSlider
102 | if (PlatformInfo.isIOS) {
103 | return CupertinoSlider(
104 | value: value,
105 | onChanged: onChanged,
106 | onChangeStart: onChangeStart,
107 | onChangeEnd: onChangeEnd,
108 | min: min,
109 | max: max,
110 | activeColor: activeColor,
111 | thumbColor: thumbColor ?? CupertinoColors.white,
112 | );
113 | }
114 |
115 | // Android - Use Material Design Slider
116 | if (PlatformInfo.isAndroid) {
117 | return Slider(
118 | value: value,
119 | onChanged: onChanged,
120 | onChangeStart: onChangeStart,
121 | onChangeEnd: onChangeEnd,
122 | min: min,
123 | max: max,
124 | divisions: divisions,
125 | label: label,
126 | activeColor: activeColor,
127 | thumbColor: thumbColor,
128 | );
129 | }
130 |
131 | // Fallback for other platforms (web, desktop, etc.)
132 | return Slider(
133 | value: value,
134 | onChanged: onChanged,
135 | onChangeStart: onChangeStart,
136 | onChangeEnd: onChangeEnd,
137 | min: min,
138 | max: max,
139 | divisions: divisions,
140 | label: label,
141 | activeColor: activeColor,
142 | thumbColor: thumbColor,
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_floating_action_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 |
5 | /// An adaptive floating action button that renders platform-specific styles
6 | ///
7 | /// On iOS 26+: Uses circular button with native iOS 26 design and shadow
8 | /// On iOS <26: Uses CupertinoButton with circular shape and shadow
9 | /// On Android: Uses Material FloatingActionButton
10 | ///
11 | /// Example:
12 | /// ```dart
13 | /// AdaptiveFloatingActionButton(
14 | /// onPressed: () {
15 | /// print('FAB pressed');
16 | /// },
17 | /// child: Icon(Icons.add),
18 | /// )
19 | /// ```
20 | class AdaptiveFloatingActionButton extends StatelessWidget {
21 | /// Creates an adaptive floating action button
22 | const AdaptiveFloatingActionButton({
23 | super.key,
24 | required this.onPressed,
25 | required this.child,
26 | this.backgroundColor,
27 | this.foregroundColor,
28 | this.elevation,
29 | this.mini = false,
30 | this.tooltip,
31 | this.heroTag,
32 | });
33 |
34 | /// The callback that is called when the button is tapped
35 | final VoidCallback? onPressed;
36 |
37 | /// The widget below this widget in the tree (typically an Icon)
38 | final Widget child;
39 |
40 | /// The background color of the button
41 | ///
42 | /// On iOS: Background color of the circular button
43 | /// On Android: Background color of the FAB
44 | final Color? backgroundColor;
45 |
46 | /// The foreground color of the button (typically icon color)
47 | ///
48 | /// On iOS: Icon color
49 | /// On Android: Icon color
50 | final Color? foregroundColor;
51 |
52 | /// The elevation of the button
53 | ///
54 | /// On iOS: Controls shadow intensity
55 | /// On Android: Material elevation
56 | final double? elevation;
57 |
58 | /// Whether to use a mini (smaller) floating action button
59 | final bool mini;
60 |
61 | /// Tooltip text for the button
62 | final String? tooltip;
63 |
64 | /// Hero tag for page transitions
65 | final Object? heroTag;
66 |
67 | @override
68 | Widget build(BuildContext context) {
69 | // iOS implementation
70 | if (PlatformInfo.isIOS) {
71 | return _buildIOSButton(context);
72 | }
73 |
74 | // Android - Use Material FloatingActionButton
75 | if (PlatformInfo.isAndroid) {
76 | return _buildMaterialFAB(context);
77 | }
78 |
79 | // Fallback to Material
80 | return _buildMaterialFAB(context);
81 | }
82 |
83 | Widget _buildIOSButton(BuildContext context) {
84 | final defaultBackgroundColor =
85 | backgroundColor ?? CupertinoTheme.of(context).primaryColor;
86 | final defaultForegroundColor = foregroundColor ?? CupertinoColors.white;
87 | final buttonSize = mini ? 40.0 : 56.0;
88 | final iconSize = mini ? 20.0 : 24.0;
89 | final shadowElevation = elevation ?? 6.0;
90 |
91 | Widget button = Container(
92 | width: buttonSize,
93 | height: buttonSize,
94 | decoration: BoxDecoration(
95 | color: defaultBackgroundColor,
96 | shape: BoxShape.circle,
97 | boxShadow: [
98 | BoxShadow(
99 | color: Colors.black.withValues(alpha: 0.1),
100 | blurRadius: shadowElevation * 1.5,
101 | offset: Offset(0, shadowElevation / 2),
102 | ),
103 | ],
104 | ),
105 | child: CupertinoButton(
106 | padding: EdgeInsets.zero,
107 | onPressed: onPressed,
108 | child: IconTheme(
109 | data: IconThemeData(color: defaultForegroundColor, size: iconSize),
110 | child: child,
111 | ),
112 | ),
113 | );
114 |
115 | // Wrap with hero if tag is provided
116 | if (heroTag != null) {
117 | button = Hero(tag: heroTag!, child: button);
118 | }
119 |
120 | return button;
121 | }
122 |
123 | Widget _buildMaterialFAB(BuildContext context) {
124 | Widget fab;
125 |
126 | if (mini) {
127 | fab = FloatingActionButton.small(
128 | onPressed: onPressed,
129 | backgroundColor: backgroundColor,
130 | foregroundColor: foregroundColor,
131 | elevation: elevation,
132 | tooltip: tooltip,
133 | heroTag: heroTag,
134 | child: child,
135 | );
136 | } else {
137 | fab = FloatingActionButton(
138 | onPressed: onPressed,
139 | backgroundColor: backgroundColor,
140 | foregroundColor: foregroundColor,
141 | elevation: elevation,
142 | tooltip: tooltip,
143 | heroTag: heroTag,
144 | child: child,
145 | );
146 | }
147 |
148 | return fab;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/lib/src/widgets/ios26/ios26_tab_bar.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 | import 'package:flutter/cupertino.dart';
3 | import '../adaptive_scaffold.dart';
4 |
5 | /// iOS 26 styled tab bar with Liquid Glass effect
6 | class IOS26TabBar extends StatelessWidget implements PreferredSizeWidget {
7 | const IOS26TabBar({
8 | super.key,
9 | required this.destinations,
10 | required this.selectedIndex,
11 | required this.onTap,
12 | });
13 |
14 | final List destinations;
15 | final int selectedIndex;
16 | final ValueChanged onTap;
17 |
18 | @override
19 | Size get preferredSize => const Size.fromHeight(50);
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | final brightness = MediaQuery.platformBrightnessOf(context);
24 | final isDark = brightness == Brightness.dark;
25 |
26 | return Container(
27 | decoration: BoxDecoration(
28 | color: isDark
29 | ? CupertinoColors.black.withValues(alpha: 0.8)
30 | : CupertinoColors.white.withValues(alpha: 0.8),
31 | border: Border(
32 | top: BorderSide(
33 | color: isDark
34 | ? CupertinoColors.white.withValues(alpha: 0.1)
35 | : CupertinoColors.black.withValues(alpha: 0.1),
36 | width: 0.5,
37 | ),
38 | ),
39 | ),
40 | child: ClipRect(
41 | child: BackdropFilter(
42 | filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
43 | child: Container(
44 | height: 50,
45 | padding: const EdgeInsets.only(bottom: 0),
46 | child: Row(
47 | mainAxisAlignment: MainAxisAlignment.spaceAround,
48 | children: List.generate(
49 | destinations.length,
50 | (index) => Expanded(
51 | child: _TabBarItem(
52 | destination: destinations[index],
53 | isSelected: index == selectedIndex,
54 | onTap: () => onTap(index),
55 | ),
56 | ),
57 | ),
58 | ),
59 | ),
60 | ),
61 | ),
62 | );
63 | }
64 | }
65 |
66 | class _TabBarItem extends StatelessWidget {
67 | const _TabBarItem({
68 | required this.destination,
69 | required this.isSelected,
70 | required this.onTap,
71 | });
72 |
73 | final AdaptiveNavigationDestination destination;
74 | final bool isSelected;
75 | final VoidCallback onTap;
76 |
77 | @override
78 | Widget build(BuildContext context) {
79 | final brightness = MediaQuery.platformBrightnessOf(context);
80 | final isDark = brightness == Brightness.dark;
81 |
82 | final iconColor = isSelected
83 | ? CupertinoColors.activeBlue
84 | : (isDark ? CupertinoColors.systemGrey : CupertinoColors.systemGrey2);
85 |
86 | final textColor = isSelected
87 | ? CupertinoColors.activeBlue
88 | : (isDark ? CupertinoColors.systemGrey : CupertinoColors.systemGrey2);
89 |
90 | return CupertinoButton(
91 | padding: EdgeInsets.zero,
92 | onPressed: onTap,
93 | child: Column(
94 | mainAxisAlignment: MainAxisAlignment.center,
95 | children: [
96 | Icon(_getIcon(), color: iconColor, size: 24),
97 | const SizedBox(height: 2),
98 | Text(
99 | destination.label,
100 | style: TextStyle(
101 | fontSize: 10,
102 | color: textColor,
103 | fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
104 | ),
105 | ),
106 | ],
107 | ),
108 | );
109 | }
110 |
111 | IconData _getIcon() {
112 | final icon = isSelected && destination.selectedIcon != null
113 | ? destination.selectedIcon
114 | : destination.icon;
115 |
116 | if (icon is IconData) {
117 | return icon;
118 | } else if (icon is String) {
119 | return _sfSymbolToCupertinoIcon(icon);
120 | }
121 | return CupertinoIcons.circle;
122 | }
123 |
124 | IconData _sfSymbolToCupertinoIcon(String sfSymbol) {
125 | const iconMap = {
126 | 'house': CupertinoIcons.house,
127 | 'house.fill': CupertinoIcons.house_fill,
128 | 'magnifyingglass': CupertinoIcons.search,
129 | 'heart': CupertinoIcons.heart,
130 | 'heart.fill': CupertinoIcons.heart_fill,
131 | 'person': CupertinoIcons.person,
132 | 'person.fill': CupertinoIcons.person_fill,
133 | 'gear': CupertinoIcons.settings,
134 | 'star': CupertinoIcons.star,
135 | 'star.fill': CupertinoIcons.star_fill,
136 | };
137 | return iconMap[sfSymbol] ?? CupertinoIcons.circle;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/lib/src/widgets/ios26/ios26_native_search_tab_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/services.dart';
2 |
3 | /// iOS 26+ Native Tab Bar with Search Support
4 | ///
5 | /// This widget enables the native iOS 26 tab bar with search functionality.
6 | /// When enabled, it replaces the Flutter app's root with a native UITabBarController.
7 | ///
8 | /// **Important**: This is an experimental API and may significantly impact your app's
9 | /// navigation structure. Use with caution.
10 | ///
11 | /// Example:
12 | /// ```dart
13 | /// @override
14 | /// void initState() {
15 | /// super.initState();
16 | /// IOS26NativeSearchTabBar.enable(
17 | /// tabs: [
18 | /// NativeTabConfig(title: 'Home', sfSymbol: 'house.fill'),
19 | /// NativeTabConfig(title: 'Search', sfSymbol: 'magnifyingglass', isSearchTab: true),
20 | /// NativeTabConfig(title: 'Profile', sfSymbol: 'person.fill'),
21 | /// ],
22 | /// onTabSelected: (index) {
23 | /// print('Tab selected: $index');
24 | /// },
25 | /// onSearchQueryChanged: (query) {
26 | /// print('Search query: $query');
27 | /// },
28 | /// );
29 | /// }
30 | /// ```
31 | class IOS26NativeSearchTabBar {
32 | static const MethodChannel _channel = MethodChannel(
33 | 'adaptive_platform_ui/native_tab_bar',
34 | );
35 |
36 | static bool _isEnabled = false;
37 |
38 | /// Enable native tab bar mode
39 | ///
40 | /// This will replace your app's root view controller with a native
41 | /// UITabBarController. Your Flutter content will be displayed within
42 | /// the selected tab.
43 | static Future enable({
44 | required List tabs,
45 | int selectedIndex = 0,
46 | void Function(int index)? onTabSelected,
47 | void Function(String query)? onSearchQueryChanged,
48 | void Function(String query)? onSearchSubmitted,
49 | VoidCallback? onSearchCancelled,
50 | }) async {
51 | if (_isEnabled) {
52 | return;
53 | }
54 |
55 | // Setup method call handler for callbacks
56 | _channel.setMethodCallHandler((call) async {
57 | switch (call.method) {
58 | case 'onTabSelected':
59 | final index = call.arguments['index'] as int;
60 | onTabSelected?.call(index);
61 | break;
62 | case 'onSearchQueryChanged':
63 | final query = call.arguments['query'] as String;
64 | onSearchQueryChanged?.call(query);
65 | break;
66 | case 'onSearchSubmitted':
67 | final query = call.arguments['query'] as String;
68 | onSearchSubmitted?.call(query);
69 | break;
70 | case 'onSearchCancelled':
71 | onSearchCancelled?.call();
72 | break;
73 | }
74 | });
75 |
76 | // Enable native tab bar
77 | await _channel.invokeMethod('enableNativeTabBar', {
78 | 'tabs': tabs
79 | .map(
80 | (tab) => {
81 | 'title': tab.title,
82 | 'sfSymbol': tab.sfSymbol,
83 | 'isSearch': tab.isSearchTab,
84 | },
85 | )
86 | .toList(),
87 | 'selectedIndex': selectedIndex,
88 | });
89 |
90 | _isEnabled = true;
91 | }
92 |
93 | /// Disable native tab bar and return to Flutter-only mode
94 | static Future disable() async {
95 | if (!_isEnabled) {
96 | return;
97 | }
98 |
99 | await _channel.invokeMethod('disableNativeTabBar');
100 | _isEnabled = false;
101 | }
102 |
103 | /// Set the selected tab index
104 | static Future setSelectedIndex(int index) async {
105 | await _channel.invokeMethod('setSelectedIndex', {'index': index});
106 | }
107 |
108 | /// Show the search bar (activates the search controller)
109 | static Future showSearch() async {
110 | await _channel.invokeMethod('showSearch');
111 | }
112 |
113 | /// Hide the search bar
114 | static Future hideSearch() async {
115 | await _channel.invokeMethod('hideSearch');
116 | }
117 |
118 | /// Check if native tab bar is currently enabled
119 | static Future isEnabled() async {
120 | try {
121 | final result = await _channel.invokeMethod('isEnabled');
122 | return result ?? false;
123 | } catch (e) {
124 | return false;
125 | }
126 | }
127 | }
128 |
129 | /// Configuration for a native tab
130 | class NativeTabConfig {
131 | /// The title of the tab
132 | final String title;
133 |
134 | /// SF Symbol name for the tab icon (iOS only)
135 | final String? sfSymbol;
136 |
137 | /// Whether this tab is a search tab
138 | ///
139 | /// Only one tab should be marked as a search tab.
140 | /// When selected, the tab bar will transform into a search bar on iOS 26+.
141 | final bool isSearchTab;
142 |
143 | const NativeTabConfig({
144 | required this.title,
145 | this.sfSymbol,
146 | this.isSearchTab = false,
147 | });
148 | }
149 |
--------------------------------------------------------------------------------
/example/lib/pages/demos/slider_demo_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
4 |
5 | class SliderDemoPage extends StatefulWidget {
6 | const SliderDemoPage({super.key});
7 |
8 | @override
9 | State createState() => _SliderDemoPageState();
10 | }
11 |
12 | class _SliderDemoPageState extends State {
13 | double _basicValue = 0.5;
14 | double _blueValue = 0.3;
15 | double _redValue = 0.7;
16 | double _greenValue = 0.6;
17 | double _rangeValue = 50.0;
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | return AdaptiveScaffold(
22 | appBar: AdaptiveAppBar(title: 'AdaptiveSlider Demo'),
23 | body: _buildContent(),
24 | );
25 | }
26 |
27 | Widget _buildContent() {
28 | final isDark = Theme.of(context).brightness == Brightness.dark;
29 |
30 | return ListView(
31 | padding: const EdgeInsets.all(16.0),
32 | children: [
33 | SizedBox(height: 120),
34 | _buildSection(
35 | 'Basic Slider',
36 | Column(
37 | children: [
38 | AdaptiveSlider(
39 | value: _basicValue,
40 | onChanged: (value) => setState(() => _basicValue = value),
41 | ),
42 | const SizedBox(height: 8),
43 | Text(
44 | 'Value: ${_basicValue.toStringAsFixed(2)}',
45 | style: TextStyle(
46 | color: PlatformInfo.isIOS
47 | ? (MediaQuery.platformBrightnessOf(context) ==
48 | Brightness.dark
49 | ? CupertinoColors.systemGrey
50 | : CupertinoColors.systemGrey2)
51 | : (isDark ? Colors.grey[400] : Colors.grey[700]),
52 | ),
53 | ),
54 | ],
55 | ),
56 | ),
57 | const SizedBox(height: 32),
58 | _buildSection(
59 | 'Custom Colors',
60 | Column(
61 | children: [
62 | AdaptiveSlider(
63 | value: _blueValue,
64 | onChanged: (value) => setState(() => _blueValue = value),
65 | activeColor: Colors.blue,
66 | ),
67 | const SizedBox(height: 16),
68 | AdaptiveSlider(
69 | value: _redValue,
70 | onChanged: (value) => setState(() => _redValue = value),
71 | activeColor: Colors.red,
72 | ),
73 | const SizedBox(height: 16),
74 | AdaptiveSlider(
75 | value: _greenValue,
76 | onChanged: (value) => setState(() => _greenValue = value),
77 | activeColor: Colors.green,
78 | ),
79 | ],
80 | ),
81 | ),
82 | const SizedBox(height: 32),
83 | _buildSection(
84 | 'Custom Range (0-100)',
85 | Column(
86 | children: [
87 | AdaptiveSlider(
88 | value: _rangeValue,
89 | min: 0,
90 | max: 100,
91 | onChanged: (value) => setState(() => _rangeValue = value),
92 | activeColor: Colors.purple,
93 | ),
94 | const SizedBox(height: 8),
95 | Text(
96 | 'Value: ${_rangeValue.toStringAsFixed(0)}',
97 | style: TextStyle(
98 | color: PlatformInfo.isIOS
99 | ? (MediaQuery.platformBrightnessOf(context) ==
100 | Brightness.dark
101 | ? CupertinoColors.systemGrey
102 | : CupertinoColors.systemGrey2)
103 | : (isDark ? Colors.grey[400] : Colors.grey[700]),
104 | ),
105 | ),
106 | ],
107 | ),
108 | ),
109 | ],
110 | );
111 | }
112 |
113 | Widget _buildSection(String title, Widget content) {
114 | final isDark = Theme.of(context).brightness == Brightness.dark;
115 |
116 | return Column(
117 | crossAxisAlignment: CrossAxisAlignment.start,
118 | children: [
119 | Text(
120 | title,
121 | style: TextStyle(
122 | fontSize: 18,
123 | fontWeight: FontWeight.bold,
124 | color: PlatformInfo.isIOS
125 | ? (MediaQuery.platformBrightnessOf(context) == Brightness.dark
126 | ? CupertinoColors.white
127 | : CupertinoColors.black)
128 | : (isDark ? Colors.white : Colors.black87),
129 | ),
130 | ),
131 | const SizedBox(height: 12),
132 | content,
133 | ],
134 | );
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/ios/Classes/iOS26BlurViewPlatformView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | /// Factory for creating iOS 26 native blur view platform views
5 | class iOS26BlurViewFactory: NSObject, FlutterPlatformViewFactory {
6 | private var messenger: FlutterBinaryMessenger
7 |
8 | init(messenger: FlutterBinaryMessenger) {
9 | self.messenger = messenger
10 | super.init()
11 | }
12 |
13 | func create(
14 | withFrame frame: CGRect,
15 | viewIdentifier viewId: Int64,
16 | arguments args: Any?
17 | ) -> FlutterPlatformView {
18 | return iOS26BlurViewPlatformView(
19 | frame: frame,
20 | viewIdentifier: viewId,
21 | arguments: args,
22 | binaryMessenger: messenger
23 | )
24 | }
25 |
26 | func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
27 | return FlutterStandardMessageCodec.sharedInstance()
28 | }
29 | }
30 |
31 | /// Native iOS 26 blur view using UIVisualEffectView
32 | class iOS26BlurViewPlatformView: NSObject, FlutterPlatformView {
33 | private var _blurView: UIVisualEffectView
34 | private var _channel: FlutterMethodChannel
35 | private var _viewId: Int64
36 |
37 | init(
38 | frame: CGRect,
39 | viewIdentifier viewId: Int64,
40 | arguments args: Any?,
41 | binaryMessenger messenger: FlutterBinaryMessenger
42 | ) {
43 | _viewId = viewId
44 |
45 | // Parse blur style from arguments
46 | var blurStyle: UIBlurEffect.Style = .systemUltraThinMaterial
47 | if let params = args as? [String: Any],
48 | let styleString = params["blurStyle"] as? String {
49 | blurStyle = iOS26BlurViewPlatformView.parseBlurStyle(styleString)
50 | }
51 |
52 | // Create blur effect and view
53 | let blurEffect = UIBlurEffect(style: blurStyle)
54 | _blurView = UIVisualEffectView(effect: blurEffect)
55 | _blurView.frame = frame
56 | _blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
57 |
58 | // Setup method channel
59 | _channel = FlutterMethodChannel(
60 | name: "adaptive_platform_ui/ios26_blur_view_\(viewId)",
61 | binaryMessenger: messenger
62 | )
63 |
64 | super.init()
65 |
66 | // Setup method channel handler
67 | _channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
68 | self?.handleMethodCall(call, result: result)
69 | }
70 | }
71 |
72 | func view() -> UIView {
73 | return _blurView
74 | }
75 |
76 | private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
77 | switch call.method {
78 | case "updateBlurStyle":
79 | if let args = call.arguments as? [String: Any],
80 | let styleString = args["blurStyle"] as? String {
81 | let blurStyle = iOS26BlurViewPlatformView.parseBlurStyle(styleString)
82 | let blurEffect = UIBlurEffect(style: blurStyle)
83 | _blurView.effect = blurEffect
84 | result(nil)
85 | } else {
86 | result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil))
87 | }
88 | default:
89 | result(FlutterMethodNotImplemented)
90 | }
91 | }
92 |
93 | /// Parse blur style string to UIBlurEffect.Style
94 | private static func parseBlurStyle(_ styleString: String) -> UIBlurEffect.Style {
95 | switch styleString {
96 | case "systemUltraThinMaterial":
97 | if #available(iOS 13.0, *) {
98 | return .systemUltraThinMaterial
99 | } else {
100 | return .light
101 | }
102 | case "systemThinMaterial":
103 | if #available(iOS 13.0, *) {
104 | return .systemThinMaterial
105 | } else {
106 | return .light
107 | }
108 | case "systemMaterial":
109 | if #available(iOS 13.0, *) {
110 | return .systemMaterial
111 | } else {
112 | return .light
113 | }
114 | case "systemThickMaterial":
115 | if #available(iOS 13.0, *) {
116 | return .systemThickMaterial
117 | } else {
118 | return .dark
119 | }
120 | case "systemChromeMaterial":
121 | if #available(iOS 13.0, *) {
122 | return .systemChromeMaterial
123 | } else {
124 | return .dark
125 | }
126 | default:
127 | if #available(iOS 13.0, *) {
128 | return .systemUltraThinMaterial
129 | } else {
130 | return .light
131 | }
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_bottom_navigation_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'adaptive_scaffold.dart';
3 |
4 | /// Configuration for an adaptive bottom navigation bar
5 | ///
6 | /// This class holds the configuration for the bottom navigation bar in [AdaptiveScaffold].
7 | /// The actual rendering is platform-specific:
8 | /// - iOS 26+ with useNativeBottomBar: Native UITabBar with Liquid Glass effects
9 | /// - iOS 26+ without useNativeBottomBar: CupertinoTabBar (custom or auto-generated)
10 | /// - iOS <26: CupertinoTabBar (custom or auto-generated)
11 | /// - Android: NavigationBar (custom or auto-generated)
12 | ///
13 | /// You can provide custom bottom navigation bars using [cupertinoTabBar] or [bottomNavigationBar]:
14 | /// - If [cupertinoTabBar] is provided: Uses custom CupertinoTabBar on iOS (when useNativeBottomBar is false or iOS <26)
15 | /// - If [bottomNavigationBar] is provided: Uses custom NavigationBar/BottomNavigationBar on Android
16 | /// - Otherwise: Builds bottom navigation bar from [items]
17 | class AdaptiveBottomNavigationBar {
18 | /// Creates an adaptive bottom navigation bar configuration
19 | const AdaptiveBottomNavigationBar({
20 | this.items,
21 | this.selectedIndex,
22 | this.onTap,
23 | this.useNativeBottomBar = true,
24 | this.cupertinoTabBar,
25 | this.bottomNavigationBar,
26 | this.selectedItemColor,
27 | this.unselectedItemColor,
28 | });
29 |
30 | /// Navigation items for bottom navigation bar
31 | /// These will be used to build the platform-specific navigation items if custom
32 | /// bars are not provided.
33 | final List? items;
34 |
35 | /// Currently selected item index
36 | /// If null, no item will be selected
37 | final int? selectedIndex;
38 |
39 | /// Called when a navigation item is tapped
40 | /// If null, navigation will not be interactive
41 | final ValueChanged? onTap;
42 |
43 | /// Use native iOS 26 bottom bar (iOS 26+ only)
44 | /// - When true (default): Uses native iOS 26 UITabBar with Liquid Glass effect
45 | /// - When false: Uses CupertinoTabBar (custom if provided, otherwise auto-generated)
46 | ///
47 | /// For iOS <26, this parameter is ignored and CupertinoTabBar is always used.
48 | ///
49 | /// If true, [cupertinoTabBar] will be ignored on iOS 26+ and native tab bar will be shown.
50 | /// For iOS <26, if [cupertinoTabBar] is provided, it will be used regardless of this setting.
51 | final bool useNativeBottomBar;
52 |
53 | /// Custom CupertinoTabBar for iOS
54 | ///
55 | /// When provided:
56 | /// - iOS 26+ with useNativeBottomBar=false: Uses this custom tab bar
57 | /// - iOS 26+ with useNativeBottomBar=true: Ignored, native tab bar is shown
58 | /// - iOS <26: Always uses this custom tab bar
59 | ///
60 | /// If not provided, a tab bar will be auto-generated from [items].
61 | ///
62 | /// Ignored on Android platforms.
63 | final CupertinoTabBar? cupertinoTabBar;
64 |
65 | /// Custom NavigationBar or BottomNavigationBar for Android
66 | ///
67 | /// When provided, this custom navigation bar will be used instead of building one
68 | /// from [items].
69 | ///
70 | /// Ignored on iOS platforms.
71 | final Widget? bottomNavigationBar;
72 |
73 | /// Color for the selected navigation item
74 | ///
75 | /// When provided:
76 | /// - iOS (native/CupertinoTabBar): Sets activeColor
77 | /// - Android (NavigationBar): Sets indicatorColor
78 | ///
79 | /// If null, uses platform defaults.
80 | final Color? selectedItemColor;
81 |
82 | /// Color for unselected navigation items
83 | ///
84 | /// When provided:
85 | /// - iOS (native/CupertinoTabBar): Sets inactiveColor
86 | /// - Android (NavigationBar): Not directly supported, but can affect icon colors
87 | ///
88 | /// If null, uses platform defaults.
89 | final Color? unselectedItemColor;
90 |
91 | /// Creates a copy of this bottom navigation bar with the given fields replaced
92 | AdaptiveBottomNavigationBar copyWith({
93 | List? items,
94 | int? selectedIndex,
95 | ValueChanged? onTap,
96 | bool? useNativeBottomBar,
97 | CupertinoTabBar? cupertinoTabBar,
98 | Widget? bottomNavigationBar,
99 | Color? selectedItemColor,
100 | Color? unselectedItemColor,
101 | }) {
102 | return AdaptiveBottomNavigationBar(
103 | items: items ?? this.items,
104 | selectedIndex: selectedIndex ?? this.selectedIndex,
105 | onTap: onTap ?? this.onTap,
106 | useNativeBottomBar: useNativeBottomBar ?? this.useNativeBottomBar,
107 | cupertinoTabBar: cupertinoTabBar ?? this.cupertinoTabBar,
108 | bottomNavigationBar: bottomNavigationBar ?? this.bottomNavigationBar,
109 | selectedItemColor: selectedItemColor ?? this.selectedItemColor,
110 | unselectedItemColor: unselectedItemColor ?? this.unselectedItemColor,
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/src/widgets/ios26/ios26_switch.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'package:flutter/cupertino.dart';
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/gestures.dart';
5 | import 'package:flutter/services.dart';
6 |
7 | /// Native iOS 26 switch implementation using platform views
8 | ///
9 | /// This switch uses UIKit platform views to render native iOS 26 UISwitch
10 | /// designs. It communicates with the native iOS side via platform channels.
11 | ///
12 | /// Features:
13 | /// - Native iOS 26 switch animations
14 | /// - Haptic feedback
15 | /// - Native gesture handling
16 | /// - Automatic light/dark mode support
17 | class IOS26Switch extends StatefulWidget {
18 | /// Creates an iOS 26 style switch
19 | const IOS26Switch({
20 | super.key,
21 | required this.value,
22 | required this.onChanged,
23 | this.activeColor,
24 | this.thumbColor,
25 | });
26 |
27 | /// Whether this switch is on or off
28 | final bool value;
29 |
30 | /// Called when the user toggles the switch on or off
31 | final ValueChanged? onChanged;
32 |
33 | /// The color to use when this switch is on
34 | final Color? activeColor;
35 |
36 | /// The color of the thumb (handle)
37 | final Color? thumbColor;
38 |
39 | @override
40 | State createState() => _IOS26SwitchState();
41 | }
42 |
43 | class _IOS26SwitchState extends State {
44 | static int _nextId = 0;
45 | late final int _id;
46 | late final MethodChannel _channel;
47 |
48 | @override
49 | void initState() {
50 | super.initState();
51 | _id = _nextId++;
52 | _channel = MethodChannel('adaptive_platform_ui/ios26_switch_$_id');
53 | _channel.setMethodCallHandler(_handleMethod);
54 | }
55 |
56 | @override
57 | void dispose() {
58 | _channel.setMethodCallHandler(null);
59 | super.dispose();
60 | }
61 |
62 | Future _handleMethod(MethodCall call) async {
63 | switch (call.method) {
64 | case 'valueChanged':
65 | final value = call.arguments['value'] as bool;
66 | if (widget.onChanged != null) {
67 | widget.onChanged!(value);
68 | }
69 | break;
70 | }
71 | }
72 |
73 | @override
74 | void didUpdateWidget(IOS26Switch oldWidget) {
75 | super.didUpdateWidget(oldWidget);
76 |
77 | // Update native side if properties changed
78 | if (oldWidget.value != widget.value) {
79 | _channel.invokeMethod('setValue', {'value': widget.value});
80 | }
81 |
82 | if (oldWidget.activeColor != widget.activeColor &&
83 | widget.activeColor != null) {
84 | _channel.invokeMethod('setActiveColor', {
85 | 'color': _colorToARGB(widget.activeColor!),
86 | });
87 | }
88 |
89 | if (oldWidget.thumbColor != widget.thumbColor &&
90 | widget.thumbColor != null) {
91 | _channel.invokeMethod('setThumbColor', {
92 | 'color': _colorToARGB(widget.thumbColor!),
93 | });
94 | }
95 |
96 | // Update enabled state
97 | if ((oldWidget.onChanged == null) != (widget.onChanged == null)) {
98 | _channel.invokeMethod('setEnabled', {
99 | 'enabled': widget.onChanged != null,
100 | });
101 | }
102 | }
103 |
104 | Map _buildCreationParams() {
105 | return {
106 | 'id': _id,
107 | 'value': widget.value,
108 | 'enabled': widget.onChanged != null,
109 | if (widget.activeColor != null)
110 | 'activeColor': _colorToARGB(widget.activeColor!),
111 | if (widget.thumbColor != null)
112 | 'thumbColor': _colorToARGB(widget.thumbColor!),
113 | 'isDark': MediaQuery.platformBrightnessOf(context) == Brightness.dark,
114 | };
115 | }
116 |
117 | int _colorToARGB(Color color) {
118 | return (((color.a * 255.0).round() & 0xFF) << 24) |
119 | (((color.r * 255.0).round() & 0xFF) << 16) |
120 | (((color.g * 255.0).round() & 0xFF) << 8) |
121 | ((color.b * 255.0).round() & 0xFF);
122 | }
123 |
124 | @override
125 | Widget build(BuildContext context) {
126 | // Only use native implementation on iOS
127 | if (!kIsWeb && Platform.isIOS) {
128 | final platformView = UiKitView(
129 | viewType: 'adaptive_platform_ui/ios26_switch',
130 | creationParams: _buildCreationParams(),
131 | creationParamsCodec: const StandardMessageCodec(),
132 | gestureRecognizers: >{
133 | Factory(
134 | () => HorizontalDragGestureRecognizer(),
135 | ),
136 | Factory(() => TapGestureRecognizer()),
137 | },
138 | );
139 |
140 | return SizedBox(
141 | width: 63, // Standard iOS switch width
142 | height: 29, // Standard iOS switch height
143 | child: platformView,
144 | );
145 | }
146 |
147 | // Fallback to CupertinoSwitch on other platforms
148 | return CupertinoSwitch(
149 | value: widget.value,
150 | onChanged: widget.onChanged,
151 | activeTrackColor: widget.activeColor,
152 | thumbColor: widget.thumbColor ?? CupertinoColors.white,
153 | );
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_context_menu.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 |
5 | /// A context menu action item
6 | class AdaptiveContextMenuAction {
7 | /// Creates a context menu action
8 | const AdaptiveContextMenuAction({
9 | required this.title,
10 | required this.onPressed,
11 | this.icon,
12 | this.isDestructive = false,
13 | this.isDisabled = false,
14 | });
15 |
16 | /// The title of the action
17 | final String title;
18 |
19 | /// Callback when the action is pressed
20 | final VoidCallback onPressed;
21 |
22 | /// Icon for the action (iOS 26+: SF Symbol string, iOS <26/Android: IconData)
23 | final dynamic icon;
24 |
25 | /// Whether this is a destructive action (shown in red)
26 | final bool isDestructive;
27 |
28 | /// Whether this action is disabled
29 | final bool isDisabled;
30 | }
31 |
32 | /// An adaptive context menu that renders platform-specific styles
33 | ///
34 | /// On iOS 26+: Uses native UIContextMenu with Liquid Glass effects
35 | /// On iOS <26: Uses CupertinoContextMenu
36 | /// On Android: Uses PopupMenuButton with Material Design
37 | class AdaptiveContextMenu extends StatelessWidget {
38 | /// Creates an adaptive context menu
39 | const AdaptiveContextMenu({
40 | super.key,
41 | required this.child,
42 | required this.actions,
43 | this.previewBuilder,
44 | });
45 |
46 | /// The widget to wrap with context menu
47 | final Widget child;
48 |
49 | /// List of actions to show in the context menu
50 | final List actions;
51 |
52 | /// Optional preview builder for iOS (shows preview when long pressing)
53 | final Widget Function(BuildContext)? previewBuilder;
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | // iOS 26+ - Use CupertinoContextMenu
58 | // Note: Native iOS 26 UIContextMenu could be implemented with platform view for enhanced visuals
59 | if (PlatformInfo.isIOS26OrHigher()) {
60 | return _buildCupertinoContextMenu(context);
61 | }
62 |
63 | // iOS <26 - Use CupertinoContextMenu
64 | if (PlatformInfo.isIOS) {
65 | return _buildCupertinoContextMenu(context);
66 | }
67 |
68 | // Android - Use PopupMenuButton
69 | return _buildAndroidContextMenu(context);
70 | }
71 |
72 | Widget _buildCupertinoContextMenu(BuildContext context) {
73 | return CupertinoContextMenu.builder(
74 | actions: actions.map((action) {
75 | return CupertinoContextMenuAction(
76 | onPressed: () {
77 | Navigator.of(context, rootNavigator: true).pop();
78 | Future.microtask(() => action.onPressed());
79 | },
80 | isDestructiveAction: action.isDestructive,
81 | trailingIcon: action.icon is IconData
82 | ? action.icon as IconData
83 | : null,
84 | child: Text(action.title),
85 | );
86 | }).toList(),
87 | builder: (context, animation) {
88 | return child;
89 | },
90 | );
91 | }
92 |
93 | Widget _buildAndroidContextMenu(BuildContext context) {
94 | return GestureDetector(
95 | onLongPress: () {
96 | _showAndroidMenu(context);
97 | },
98 | child: child,
99 | );
100 | }
101 |
102 | void _showAndroidMenu(BuildContext context) {
103 | final RenderBox renderBox = context.findRenderObject() as RenderBox;
104 | final Offset offset = renderBox.localToGlobal(Offset.zero);
105 | final Size size = renderBox.size;
106 |
107 | showMenu(
108 | context: context,
109 | position: RelativeRect.fromLTRB(
110 | offset.dx,
111 | offset.dy + size.height,
112 | offset.dx + size.width,
113 | offset.dy,
114 | ),
115 | items: actions.asMap().entries.map((entry) {
116 | final index = entry.key;
117 | final action = entry.value;
118 |
119 | return PopupMenuItem(
120 | value: index,
121 | enabled: !action.isDisabled,
122 | child: Row(
123 | children: [
124 | if (action.icon != null && action.icon is IconData) ...[
125 | Icon(
126 | action.icon as IconData,
127 | size: 20,
128 | color: action.isDestructive
129 | ? Colors.red
130 | : (action.isDisabled ? Colors.grey : null),
131 | ),
132 | const SizedBox(width: 12),
133 | ],
134 | Expanded(
135 | child: Text(
136 | action.title,
137 | style: TextStyle(
138 | color: action.isDestructive
139 | ? Colors.red
140 | : (action.isDisabled ? Colors.grey : null),
141 | ),
142 | ),
143 | ),
144 | ],
145 | ),
146 | );
147 | }).toList(),
148 | ).then((selectedIndex) {
149 | if (selectedIndex != null) {
150 | actions[selectedIndex].onPressed();
151 | }
152 | });
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/example/lib/main/main_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
2 | import 'package:adaptive_platform_ui_example/service/router/router_service.dart';
3 | import 'package:adaptive_platform_ui_example/utils/constants/route_constants.dart';
4 | import 'package:adaptive_platform_ui_example/utils/global_variables.dart';
5 | import 'package:flutter/cupertino.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:go_router/go_router.dart';
8 |
9 | class MainPage extends StatefulWidget {
10 | const MainPage({required this.navigationShell, super.key});
11 |
12 | final StatefulNavigationShell navigationShell;
13 |
14 | @override
15 | State createState() => _MainPageState();
16 | }
17 |
18 | class _MainPageState extends State {
19 | @override
20 | Widget build(BuildContext context) {
21 | return Stack(
22 | children: [
23 | AdaptiveScaffold(
24 | minimizeBehavior: TabBarMinimizeBehavior.automatic,
25 | body: widget.navigationShell,
26 | bottomNavigationBar:
27 | getMatchedLocation(
28 | context,
29 | ).contains(RouteConstants().badgeNavigation)
30 | ? null
31 | : AdaptiveBottomNavigationBar(
32 | selectedIndex: widget.navigationShell.currentIndex,
33 | onTap: (index) => onDestinationSelected(index, context),
34 | items: [
35 | AdaptiveNavigationDestination(
36 | icon: PlatformInfo.isIOS26OrHigher()
37 | ? "house.fill"
38 | : PlatformInfo.isIOS
39 | ? CupertinoIcons.home
40 | : Icons.home_outlined,
41 |
42 | selectedIcon: PlatformInfo.isIOS
43 | ? CupertinoIcons.home
44 | : Icons.home,
45 | label: 'Home',
46 | badgeCount: 1,
47 | ),
48 | AdaptiveNavigationDestination(
49 | icon: PlatformInfo.isIOS26OrHigher()
50 | ? "info.circle"
51 | : PlatformInfo.isIOS
52 | ? CupertinoIcons.info
53 | : Icons.info_outline,
54 | selectedIcon: PlatformInfo.isIOS
55 | ? CupertinoIcons.info
56 | : Icons.info,
57 | label: 'Info',
58 | ),
59 | AdaptiveNavigationDestination(
60 | icon: PlatformInfo.isIOS26OrHigher()
61 | ? "magnifyingglass"
62 | : PlatformInfo.isIOS
63 | ? CupertinoIcons.search
64 | : Icons.search,
65 | label: 'Search',
66 | isSearch: true,
67 | ),
68 | ],
69 | ),
70 | ),
71 | ],
72 | );
73 | }
74 |
75 | void onDestinationSelected(tappedIndex, BuildContext context) {
76 | // scroll to top if the user taps the current tab
77 | var matchedLocation = getMatchedLocation(context);
78 |
79 | if (widget.navigationShell.currentIndex == tappedIndex) {
80 | bool shouldNavigateToRoot = false;
81 |
82 | switch (tappedIndex) {
83 | case 0:
84 | if (matchedLocation != RouterService.routes.home) {
85 | shouldNavigateToRoot = true;
86 | } else {
87 | homeScrollController.animateTo(
88 | 0,
89 | duration: const Duration(milliseconds: 500),
90 | curve: Curves.easeInOut,
91 | );
92 | }
93 | break;
94 | case 1:
95 | if (matchedLocation != RouterService.routes.info) {
96 | shouldNavigateToRoot = true;
97 | } else {
98 | infoScrollController.animateTo(
99 | 0,
100 | duration: const Duration(milliseconds: 500),
101 | curve: Curves.easeInOut,
102 | );
103 | }
104 | break;
105 | case 2:
106 | if (matchedLocation != RouterService.routes.search) {
107 | shouldNavigateToRoot = true;
108 | } else {
109 | // searchScrollController.animateTo(
110 | // 0,
111 | // duration: const Duration(milliseconds: 500),
112 | // curve: Curves.easeInOut,
113 | // );
114 | }
115 | break;
116 | }
117 |
118 | if (shouldNavigateToRoot) {
119 | // Pop until we reach the root of the current branch
120 | widget.navigationShell.goBranch(tappedIndex, initialLocation: true);
121 | return;
122 | }
123 | return;
124 | }
125 |
126 | widget.navigationShell.goBranch(tappedIndex);
127 | }
128 |
129 | String getMatchedLocation(BuildContext context) {
130 | return GoRouter.of(
131 | navigatorKey.currentContext!,
132 | ).routerDelegate.currentConfiguration.last.matchedLocation;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_list_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 |
5 | /// An adaptive list tile that renders platform-specific styles
6 | ///
7 | /// On iOS: Uses CupertinoListTile-like styling
8 | /// On Android: Uses Material ListTile
9 | class AdaptiveListTile extends StatelessWidget {
10 | /// Creates an adaptive list tile
11 | const AdaptiveListTile({
12 | super.key,
13 | this.leading,
14 | this.title,
15 | this.subtitle,
16 | this.trailing,
17 | this.onTap,
18 | this.onLongPress,
19 | this.enabled = true,
20 | this.selected = false,
21 | this.backgroundColor,
22 | this.padding,
23 | });
24 |
25 | /// A widget to display before the title.
26 | final Widget? leading;
27 |
28 | /// The primary content of the list tile.
29 | final Widget? title;
30 |
31 | /// Additional content displayed below the title.
32 | final Widget? subtitle;
33 |
34 | /// A widget to display after the title.
35 | final Widget? trailing;
36 |
37 | /// Called when the user taps this list tile.
38 | final VoidCallback? onTap;
39 |
40 | /// Called when the user long-presses on this list tile.
41 | final VoidCallback? onLongPress;
42 |
43 | /// Whether this list tile is interactive.
44 | final bool enabled;
45 |
46 | /// Whether this list tile is selected.
47 | final bool selected;
48 |
49 | /// The background color of the tile.
50 | final Color? backgroundColor;
51 |
52 | /// The tile's internal padding.
53 | final EdgeInsetsGeometry? padding;
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | if (PlatformInfo.isIOS) {
58 | return _buildCupertinoListTile(context);
59 | }
60 |
61 | // Android - Use Material ListTile
62 | return _buildMaterialListTile(context);
63 | }
64 |
65 | Widget _buildCupertinoListTile(BuildContext context) {
66 | final isDark = MediaQuery.platformBrightnessOf(context) == Brightness.dark;
67 |
68 | Widget child = Container(
69 | padding:
70 | padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
71 | decoration: BoxDecoration(
72 | color:
73 | backgroundColor ??
74 | (selected
75 | ? (isDark
76 | ? CupertinoColors.systemGrey5.darkColor
77 | : CupertinoColors.systemGrey6.color)
78 | : (isDark
79 | ? CupertinoColors.darkBackgroundGray
80 | : CupertinoColors.white)),
81 | border: Border(
82 | bottom: BorderSide(
83 | color: isDark
84 | ? CupertinoColors.systemGrey4
85 | : CupertinoColors.separator,
86 | width: 0.5,
87 | ),
88 | ),
89 | ),
90 | child: Row(
91 | children: [
92 | if (leading != null) ...[leading!, const SizedBox(width: 12)],
93 | Expanded(
94 | child: Column(
95 | crossAxisAlignment: CrossAxisAlignment.start,
96 | mainAxisSize: MainAxisSize.min,
97 | children: [
98 | if (title != null)
99 | DefaultTextStyle(
100 | style: TextStyle(
101 | fontSize: 17,
102 | fontWeight: FontWeight.w400,
103 | color: enabled
104 | ? (isDark
105 | ? CupertinoColors.white
106 | : CupertinoColors.black)
107 | : (isDark
108 | ? CupertinoColors.systemGrey
109 | : CupertinoColors.systemGrey2),
110 | ),
111 | child: title!,
112 | ),
113 | if (subtitle != null) ...[
114 | const SizedBox(height: 2),
115 | DefaultTextStyle(
116 | style: TextStyle(
117 | fontSize: 14,
118 | color: isDark
119 | ? CupertinoColors.systemGrey
120 | : CupertinoColors.systemGrey2,
121 | ),
122 | child: subtitle!,
123 | ),
124 | ],
125 | ],
126 | ),
127 | ),
128 | if (trailing != null) ...[const SizedBox(width: 12), trailing!],
129 | ],
130 | ),
131 | );
132 |
133 | if (enabled && (onTap != null || onLongPress != null)) {
134 | return GestureDetector(
135 | onTap: onTap,
136 | onLongPress: onLongPress,
137 | behavior: HitTestBehavior.opaque,
138 | child: child,
139 | );
140 | }
141 |
142 | return child;
143 | }
144 |
145 | Widget _buildMaterialListTile(BuildContext context) {
146 | return ListTile(
147 | leading: leading,
148 | title: title,
149 | subtitle: subtitle,
150 | trailing: trailing,
151 | onTap: enabled ? onTap : null,
152 | onLongPress: enabled ? onLongPress : null,
153 | enabled: enabled,
154 | selected: selected,
155 | tileColor: backgroundColor,
156 | contentPadding:
157 | padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
158 | );
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_radio.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 |
5 | /// An adaptive radio button that renders platform-specific radio styles
6 | ///
7 | /// On iOS: Uses custom iOS-style radio with Cupertino design
8 | /// On Android: Uses Material Design Radio
9 | ///
10 | /// Example:
11 | /// ```dart
12 | /// enum Options { option1, option2, option3 }
13 | /// Options? _selectedOption = Options.option1;
14 | ///
15 | /// AdaptiveRadio(
16 | /// value: Options.option1,
17 | /// groupValue: _selectedOption,
18 | /// onChanged: (Options? value) {
19 | /// setState(() {
20 | /// _selectedOption = value;
21 | /// });
22 | /// },
23 | /// )
24 | /// ```
25 | class AdaptiveRadio extends StatelessWidget {
26 | /// Creates an adaptive radio button
27 | const AdaptiveRadio({
28 | super.key,
29 | required this.value,
30 | required this.groupValue,
31 | required this.onChanged,
32 | this.activeColor,
33 | this.focusColor,
34 | this.hoverColor,
35 | this.toggleable = false,
36 | });
37 |
38 | /// The value represented by this radio button
39 | final T value;
40 |
41 | /// The currently selected value for a group of radio buttons
42 | ///
43 | /// This radio button is considered selected if its [value] matches the [groupValue]
44 | final T? groupValue;
45 |
46 | /// Called when the user selects this radio button
47 | ///
48 | /// The radio button passes [value] as a parameter to this callback
49 | /// If [toggleable] is true, this will be called with null when tapping on a selected radio
50 | final ValueChanged? onChanged;
51 |
52 | /// The color to use when this radio button is selected
53 | ///
54 | /// On iOS: Uses the color for the radio button when selected
55 | /// On Android: Uses the color for the radio button
56 | final Color? activeColor;
57 |
58 | /// The color for the radio's Material when it has the input focus
59 | final Color? focusColor;
60 |
61 | /// The color for the radio's Material when a pointer is hovering over it
62 | final Color? hoverColor;
63 |
64 | /// Set to true if this radio button is allowed to be returned to an indeterminate state by selecting it again when selected
65 | ///
66 | /// To indicate returning to an indeterminate state, [onChanged] will be called with null
67 | final bool toggleable;
68 |
69 | @override
70 | Widget build(BuildContext context) {
71 | // iOS - Use custom iOS-style radio
72 | if (PlatformInfo.isIOS) {
73 | return _IOSRadio(
74 | value: value,
75 | groupValue: groupValue,
76 | onChanged: onChanged,
77 | activeColor: activeColor ?? CupertinoTheme.of(context).primaryColor,
78 | toggleable: toggleable,
79 | );
80 | }
81 |
82 | // Android - Use Material Design Radio
83 | if (PlatformInfo.isAndroid) {
84 | // ignore: deprecated_member_use
85 | return Radio(
86 | value: value,
87 | // ignore: deprecated_member_use
88 | groupValue: groupValue,
89 | // ignore: deprecated_member_use
90 | onChanged: onChanged,
91 | activeColor: activeColor,
92 | focusColor: focusColor,
93 | hoverColor: hoverColor,
94 | toggleable: toggleable,
95 | );
96 | }
97 |
98 | // Fallback for other platforms (web, desktop, etc.)
99 | // ignore: deprecated_member_use
100 | return Radio(
101 | value: value,
102 | // ignore: deprecated_member_use
103 | groupValue: groupValue,
104 | // ignore: deprecated_member_use
105 | onChanged: onChanged,
106 | activeColor: activeColor,
107 | focusColor: focusColor,
108 | hoverColor: hoverColor,
109 | toggleable: toggleable,
110 | );
111 | }
112 | }
113 |
114 | /// iOS-style radio widget
115 | class _IOSRadio extends StatelessWidget {
116 | const _IOSRadio({
117 | required this.value,
118 | required this.groupValue,
119 | required this.onChanged,
120 | required this.activeColor,
121 | required this.toggleable,
122 | });
123 |
124 | final T value;
125 | final T? groupValue;
126 | final ValueChanged? onChanged;
127 | final Color activeColor;
128 | final bool toggleable;
129 |
130 | bool get _selected => value == groupValue;
131 |
132 | @override
133 | Widget build(BuildContext context) {
134 | final brightness = MediaQuery.platformBrightnessOf(context);
135 | final isDark = brightness == Brightness.dark;
136 |
137 | return GestureDetector(
138 | onTap: onChanged == null
139 | ? null
140 | : () {
141 | if (toggleable && _selected) {
142 | onChanged!(null);
143 | } else if (!_selected) {
144 | onChanged!(value);
145 | }
146 | },
147 | child: Container(
148 | width: 22,
149 | height: 22,
150 | decoration: BoxDecoration(
151 | shape: BoxShape.circle,
152 | color: _selected
153 | ? activeColor
154 | : (isDark
155 | ? CupertinoColors.systemGrey5.darkColor
156 | : CupertinoColors.systemBackground.color),
157 | border: Border.all(
158 | color: _selected
159 | ? activeColor
160 | : (isDark
161 | ? CupertinoColors.systemGrey3.darkColor
162 | : CupertinoColors.systemGrey4.color),
163 | width: _selected ? 6 : 1.5,
164 | ),
165 | ),
166 | ),
167 | );
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_time_picker.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 |
5 | /// An adaptive time picker that renders platform-specific styles
6 | ///
7 | /// On iOS: Shows CupertinoTimerPicker in a modal bottom sheet
8 | /// On Android: Shows Material TimePickerDialog
9 | class AdaptiveTimePicker {
10 | AdaptiveTimePicker._();
11 |
12 | /// Shows a platform-adaptive time picker
13 | ///
14 | /// Returns the selected [TimeOfDay] or null if cancelled
15 | static Future show({
16 | required BuildContext context,
17 | required TimeOfDay initialTime,
18 | bool use24HourFormat = false,
19 | }) async {
20 | if (PlatformInfo.isIOS) {
21 | return _showCupertinoTimePicker(
22 | context: context,
23 | initialTime: initialTime,
24 | use24HourFormat: use24HourFormat,
25 | );
26 | }
27 |
28 | // Android - Use Material TimePicker
29 | return _showMaterialTimePicker(context: context, initialTime: initialTime);
30 | }
31 |
32 | static Future _showCupertinoTimePicker({
33 | required BuildContext context,
34 | required TimeOfDay initialTime,
35 | required bool use24HourFormat,
36 | }) async {
37 | // Convert TimeOfDay to DateTime for CupertinoDatePicker
38 | final now = DateTime.now();
39 | DateTime selectedDateTime = DateTime(
40 | now.year,
41 | now.month,
42 | now.day,
43 | initialTime.hour,
44 | initialTime.minute,
45 | );
46 |
47 | final result = await showCupertinoModalPopup(
48 | context: context,
49 | builder: (BuildContext context) {
50 | return _CupertinoTimePickerContent(
51 | initialDateTime: selectedDateTime,
52 | use24HourFormat: use24HourFormat,
53 | onTimeSelected: (dateTime) => selectedDateTime = dateTime,
54 | );
55 | },
56 | );
57 |
58 | if (result != null) {
59 | return TimeOfDay(
60 | hour: selectedDateTime.hour,
61 | minute: selectedDateTime.minute,
62 | );
63 | }
64 | return null;
65 | }
66 |
67 | static Future _showMaterialTimePicker({
68 | required BuildContext context,
69 | required TimeOfDay initialTime,
70 | }) async {
71 | return showTimePicker(context: context, initialTime: initialTime);
72 | }
73 | }
74 |
75 | /// Internal widget that properly updates when theme changes
76 | class _CupertinoTimePickerContent extends StatefulWidget {
77 | const _CupertinoTimePickerContent({
78 | required this.initialDateTime,
79 | required this.use24HourFormat,
80 | required this.onTimeSelected,
81 | });
82 |
83 | final DateTime initialDateTime;
84 | final bool use24HourFormat;
85 | final ValueChanged onTimeSelected;
86 |
87 | @override
88 | State<_CupertinoTimePickerContent> createState() =>
89 | _CupertinoTimePickerContentState();
90 | }
91 |
92 | class _CupertinoTimePickerContentState
93 | extends State<_CupertinoTimePickerContent> {
94 | late DateTime selectedDateTime;
95 |
96 | @override
97 | void initState() {
98 | super.initState();
99 | selectedDateTime = widget.initialDateTime;
100 | }
101 |
102 | @override
103 | Widget build(BuildContext context) {
104 | // Use CupertinoTheme to get dynamic colors that update with theme changes
105 | final backgroundColor = CupertinoTheme.of(context).scaffoldBackgroundColor;
106 | final separatorColor = CupertinoDynamicColor.resolve(
107 | CupertinoColors.separator,
108 | context,
109 | );
110 |
111 | return Container(
112 | height: 280,
113 | color: backgroundColor,
114 | child: Column(
115 | children: [
116 | // Header with Done button
117 | Container(
118 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
119 | decoration: BoxDecoration(
120 | border: Border(
121 | bottom: BorderSide(color: separatorColor, width: 0.5),
122 | ),
123 | ),
124 | child: Row(
125 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
126 | children: [
127 | CupertinoButton(
128 | padding: EdgeInsets.zero,
129 | onPressed: () => Navigator.of(context).pop(),
130 | child: Text(
131 | PlatformInfo.isIOS
132 | ? CupertinoLocalizations.of(context).cancelButtonLabel
133 | : MaterialLocalizations.of(context).cancelButtonLabel,
134 | ),
135 | ),
136 | CupertinoButton(
137 | padding: EdgeInsets.zero,
138 | onPressed: () => Navigator.of(context).pop(selectedDateTime),
139 | child: Text(
140 | MaterialLocalizations.of(context).okButtonLabel,
141 | style: const TextStyle(fontWeight: FontWeight.w600),
142 | ),
143 | ),
144 | ],
145 | ),
146 | ),
147 | // Time picker
148 | Expanded(
149 | child: CupertinoDatePicker(
150 | mode: CupertinoDatePickerMode.time,
151 | use24hFormat: widget.use24HourFormat,
152 | initialDateTime: widget.initialDateTime,
153 | onDateTimeChanged: (DateTime newDateTime) {
154 | setState(() {
155 | selectedDateTime = newDateTime;
156 | });
157 | widget.onTimeSelected(newDateTime);
158 | },
159 | ),
160 | ),
161 | ],
162 | ),
163 | );
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/.github/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Release Process
2 |
3 | This document describes how to create a new release of Adaptive Platform UI.
4 |
5 | ## Prerequisites
6 |
7 | - Write access to the repository
8 | - All changes merged to `main` branch
9 | - All tests passing on CI
10 | - CHANGELOG.md updated with release notes
11 |
12 | ## Release Steps
13 |
14 | ### 1. Update Version Numbers
15 |
16 | Update the version in `pubspec.yaml`:
17 |
18 | ```yaml
19 | version: 0.1.95 # Increment according to semver
20 | ```
21 |
22 | ### 2. Update CHANGELOG.md
23 |
24 | Add a new section at the top of CHANGELOG.md:
25 |
26 | ```markdown
27 | ## [0.1.95]
28 | * **NEW**: Description of new features
29 | * **FIX**: Description of bug fixes
30 | * **IMPROVEMENT**: Description of improvements
31 | ```
32 |
33 | **Important**: The version number in brackets `[0.1.95]` must match the tag you'll create.
34 |
35 | ### 3. Commit Changes
36 |
37 | ```bash
38 | git add pubspec.yaml CHANGELOG.md
39 | git commit -m "chore: Release v0.1.95"
40 | git push origin main
41 | ```
42 |
43 | ### 4. Create and Push Tag
44 |
45 | ```bash
46 | # Create annotated tag
47 | git tag -a v0.1.95 -m "Release v0.1.95"
48 |
49 | # Push tag to GitHub
50 | git push origin v0.1.95
51 | ```
52 |
53 | ### 5. Automated Release
54 |
55 | Once the tag is pushed, GitHub Actions will automatically:
56 |
57 | 1. ✅ Build the example app APK
58 | 2. ✅ Extract release notes from CHANGELOG.md
59 | 3. ✅ Create a GitHub Release
60 | 4. ✅ Upload the APK to the release
61 |
62 | You can monitor the progress in the **Actions** tab on GitHub.
63 |
64 | ### 6. Verify Release
65 |
66 | After the workflow completes (usually 5-10 minutes):
67 |
68 | 1. Go to the [Releases page](https://github.com/berkaycatak/adaptive_platform_ui/releases)
69 | 2. Verify the new release is published
70 | 3. Download and test the APK
71 | 4. Check that release notes are correct
72 |
73 | ## Release Naming Convention
74 |
75 | Follow [Semantic Versioning](https://semver.org/):
76 |
77 | - **MAJOR** version (1.0.0): Breaking changes
78 | - **MINOR** version (0.2.0): New features, backward compatible
79 | - **PATCH** version (0.1.1): Bug fixes, backward compatible
80 |
81 | Examples:
82 | - `0.1.95` → `0.1.96` (bug fix)
83 | - `0.1.95` → `0.2.0` (new features)
84 | - `0.1.95` → `1.0.0` (breaking changes)
85 |
86 | ## Tag Naming Convention
87 |
88 | Always prefix with `v`:
89 | - ✅ `v0.1.95`
90 | - ✅ `v1.0.0`
91 | - ❌ `0.1.95`
92 | - ❌ `1.0.0`
93 |
94 | ## Hotfix Releases
95 |
96 | For urgent bug fixes:
97 |
98 | ```bash
99 | # Create hotfix branch from main
100 | git checkout -b hotfix/0.1.96 main
101 |
102 | # Make fixes and commit
103 | git add .
104 | git commit -m "fix: Critical bug fix"
105 |
106 | # Merge back to main
107 | git checkout main
108 | git merge hotfix/0.1.96
109 |
110 | # Update version and changelog
111 | # ... (follow steps 1-4)
112 |
113 | # Delete hotfix branch
114 | git branch -d hotfix/0.1.96
115 | ```
116 |
117 | ## Pre-release / Beta Releases
118 |
119 | For testing releases before stable:
120 |
121 | ```bash
122 | # Use pre-release suffix
123 | git tag -a v0.2.0-beta.1 -m "Beta release v0.2.0-beta.1"
124 | git push origin v0.2.0-beta.1
125 | ```
126 |
127 | The release will be marked as "Pre-release" on GitHub.
128 |
129 | ## Rollback a Release
130 |
131 | If a release has critical issues:
132 |
133 | ### Option 1: Delete Release and Tag
134 |
135 | ```bash
136 | # Delete the tag locally
137 | git tag -d v0.1.95
138 |
139 | # Delete the tag on GitHub
140 | git push origin :refs/tags/v0.1.95
141 | ```
142 |
143 | Then manually delete the Release on GitHub.
144 |
145 | ### Option 2: Create Hotfix Release
146 |
147 | Create a new patch version with the fix:
148 |
149 | ```bash
150 | # Fix the issue
151 | git commit -m "fix: Critical issue from v0.1.95"
152 |
153 | # Create new patch release
154 | git tag -a v0.1.96 -m "Hotfix release v0.1.96"
155 | git push origin v0.1.96
156 | ```
157 |
158 | ## Troubleshooting
159 |
160 | ### Release workflow failed
161 |
162 | 1. Check the Actions tab for error logs
163 | 2. Common issues:
164 | - CHANGELOG.md format incorrect
165 | - Build errors in example app
166 | - GitHub token permissions
167 |
168 | ### Release created but APK missing
169 |
170 | 1. Check workflow logs for build failures
171 | 2. Verify example app builds locally:
172 | ```bash
173 | cd example
174 | flutter build apk --release
175 | ```
176 |
177 | ### Release notes not showing correctly
178 |
179 | 1. Verify CHANGELOG.md format:
180 | ```markdown
181 | ## [0.1.95]
182 | * Changes here
183 |
184 | ## [0.1.94]
185 | * Previous changes
186 | ```
187 | 2. Ensure version in brackets matches tag
188 |
189 | ### Cannot push tag
190 |
191 | ```bash
192 | # Fetch latest tags
193 | git fetch --tags
194 |
195 | # Check if tag already exists
196 | git tag -l | grep v0.1.95
197 |
198 | # If exists, delete and recreate
199 | git tag -d v0.1.95
200 | git tag -a v0.1.95 -m "Release v0.1.95"
201 | git push origin v0.1.95 --force
202 | ```
203 |
204 | ## Publishing to pub.dev
205 |
206 | After verifying the release on GitHub:
207 |
208 | ```bash
209 | # Dry run first
210 | flutter pub publish --dry-run
211 |
212 | # Publish to pub.dev
213 | flutter pub publish
214 | ```
215 |
216 | Follow the prompts to complete the publishing process.
217 |
218 | ## Checklist
219 |
220 | Before creating a release:
221 |
222 | - [ ] All PRs merged to main
223 | - [ ] CI passing on main branch
224 | - [ ] Version updated in pubspec.yaml
225 | - [ ] CHANGELOG.md updated with release notes
226 | - [ ] Commits pushed to main
227 | - [ ] Tag created and pushed
228 | - [ ] Release verified on GitHub
229 | - [ ] APK tested
230 | - [ ] Package published to pub.dev (if applicable)
231 |
232 | ## Questions?
233 |
234 | If you have questions about the release process, please:
235 | - Open a [Discussion](https://github.com/berkaycatak/adaptive_platform_ui/discussions)
236 | - Contact the maintainers
237 |
--------------------------------------------------------------------------------
/ios/Classes/iOS26SwitchView.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | /// Factory for creating iOS 26 native switch platform views
5 | class iOS26SwitchViewFactory: NSObject, FlutterPlatformViewFactory {
6 | private var messenger: FlutterBinaryMessenger
7 |
8 | init(messenger: FlutterBinaryMessenger) {
9 | self.messenger = messenger
10 | super.init()
11 | }
12 |
13 | func create(
14 | withFrame frame: CGRect,
15 | viewIdentifier viewId: Int64,
16 | arguments args: Any?
17 | ) -> FlutterPlatformView {
18 | return iOS26SwitchView(
19 | frame: frame,
20 | viewIdentifier: viewId,
21 | arguments: args,
22 | binaryMessenger: messenger
23 | )
24 | }
25 |
26 | func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
27 | return FlutterStandardMessageCodec.sharedInstance()
28 | }
29 | }
30 |
31 | /// Native iOS 26 switch implementation with native UISwitch
32 | class iOS26SwitchView: NSObject, FlutterPlatformView {
33 | private var _view: UIView
34 | private var switchControl: UISwitch!
35 | private var channel: FlutterMethodChannel
36 | private var switchId: Int
37 |
38 | // Configuration
39 | private var isEnabled: Bool = true
40 | private var isDark: Bool = false
41 |
42 | init(
43 | frame: CGRect,
44 | viewIdentifier viewId: Int64,
45 | arguments args: Any?,
46 | binaryMessenger messenger: FlutterBinaryMessenger
47 | ) {
48 | _view = UIView(frame: frame)
49 |
50 | // Extract configuration from arguments
51 | if let config = args as? [String: Any] {
52 | switchId = config["id"] as? Int ?? 0
53 | isEnabled = config["enabled"] as? Bool ?? true
54 | isDark = config["isDark"] as? Bool ?? false
55 | } else {
56 | switchId = 0
57 | }
58 |
59 | // Setup method channel for communication
60 | channel = FlutterMethodChannel(
61 | name: "adaptive_platform_ui/ios26_switch_\(switchId)",
62 | binaryMessenger: messenger
63 | )
64 |
65 | super.init()
66 |
67 | // Create the native switch
68 | createNativeSwitch(with: args)
69 |
70 | // Setup method call handler
71 | channel.setMethodCallHandler { [weak self] (call, result) in
72 | self?.handleMethodCall(call, result: result)
73 | }
74 | }
75 |
76 | func view() -> UIView {
77 | return _view
78 | }
79 |
80 | private func createNativeSwitch(with args: Any?) {
81 | // Create iOS UISwitch
82 | switchControl = UISwitch()
83 | switchControl.translatesAutoresizingMaskIntoConstraints = false
84 |
85 | // Enable user interaction
86 | switchControl.isUserInteractionEnabled = true
87 | _view.isUserInteractionEnabled = true
88 |
89 | // Extract initial configuration
90 | if let config = args as? [String: Any] {
91 | // Set initial value
92 | if let value = config["value"] as? Bool {
93 | switchControl.isOn = value
94 | }
95 |
96 | // Set active (on) color
97 | if let argb = config["activeColor"] as? Int {
98 | switchControl.onTintColor = UIColor(argb: argb)
99 | }
100 |
101 | // Set thumb color
102 | if let argb = config["thumbColor"] as? Int {
103 | switchControl.thumbTintColor = UIColor(argb: argb)
104 | }
105 | }
106 |
107 | // Setup constraints
108 | _view.addSubview(switchControl)
109 | NSLayoutConstraint.activate([
110 | switchControl.leadingAnchor.constraint(equalTo: _view.leadingAnchor),
111 | switchControl.trailingAnchor.constraint(equalTo: _view.trailingAnchor),
112 | switchControl.topAnchor.constraint(equalTo: _view.topAnchor),
113 | switchControl.bottomAnchor.constraint(equalTo: _view.bottomAnchor),
114 | ])
115 |
116 | // Add value changed action
117 | switchControl.addTarget(self, action: #selector(switchValueChanged), for: .valueChanged)
118 |
119 | // Apply enabled state
120 | switchControl.isEnabled = isEnabled
121 | }
122 |
123 | @objc private func switchValueChanged() {
124 | // Notify Flutter side about value change
125 | channel.invokeMethod("valueChanged", arguments: ["value": switchControl.isOn])
126 |
127 | // Add haptic feedback
128 | let impact = UIImpactFeedbackGenerator(style: .light)
129 | impact.impactOccurred()
130 | }
131 |
132 | private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
133 | switch call.method {
134 | case "setValue":
135 | if let args = call.arguments as? [String: Any],
136 | let value = args["value"] as? Bool {
137 | switchControl.setOn(value, animated: true)
138 | }
139 | result(nil)
140 |
141 | case "setEnabled":
142 | if let args = call.arguments as? [String: Any],
143 | let enabled = args["enabled"] as? Bool {
144 | isEnabled = enabled
145 | switchControl.isEnabled = enabled
146 | switchControl.alpha = enabled ? 1.0 : 0.5
147 | }
148 | result(nil)
149 |
150 | case "setActiveColor":
151 | if let args = call.arguments as? [String: Any],
152 | let argb = args["color"] as? Int {
153 | switchControl.onTintColor = UIColor(argb: argb)
154 | }
155 | result(nil)
156 |
157 | case "setThumbColor":
158 | if let args = call.arguments as? [String: Any],
159 | let argb = args["color"] as? Int {
160 | switchControl.thumbTintColor = UIColor(argb: argb)
161 | }
162 | result(nil)
163 |
164 | default:
165 | result(FlutterMethodNotImplemented)
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/example/lib/pages/demos/demo_tabbar_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/foundation.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:adaptive_platform_ui/adaptive_platform_ui.dart';
5 |
6 | /// Demo page showcasing AdaptiveButton features
7 | class DemoTabbarPage extends StatefulWidget {
8 | const DemoTabbarPage({super.key});
9 |
10 | @override
11 | State createState() => _DemoTabbarPageState();
12 | }
13 |
14 | class _DemoTabbarPageState extends State {
15 | int _selectedIndex = 0;
16 |
17 | Widget _buildCurrentScreen() {
18 | switch (_selectedIndex) {
19 | case 0:
20 | return const HomeScreen();
21 | case 1:
22 | return const ProfileScreen();
23 | case 2:
24 | return const SearchScreen();
25 | default:
26 | return const HomeScreen();
27 | }
28 | }
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | if (PlatformInfo.isAndroid) {
33 | return Scaffold(
34 | appBar: AppBar(title: const Text('Tabbar Demos')),
35 | bottomNavigationBar: BottomNavigationBar(
36 | items: const [
37 | BottomNavigationBarItem(
38 | icon: Icon(Icons.home_outlined),
39 | activeIcon: Icon(Icons.home),
40 | label: 'Home',
41 | ),
42 | BottomNavigationBarItem(
43 | icon: Icon(Icons.person_outline),
44 | activeIcon: Icon(Icons.person),
45 | label: 'Profile',
46 | ),
47 | BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
48 | ],
49 | currentIndex: _selectedIndex,
50 | onTap: (index) {
51 | setState(() {
52 | if (kDebugMode) {
53 | print('Index selected: $index');
54 | }
55 | _selectedIndex = index;
56 | });
57 | },
58 | ),
59 | body: _buildCurrentScreen(),
60 | );
61 | }
62 |
63 | return AdaptiveScaffold(
64 | appBar: AdaptiveAppBar(
65 | title: 'Tabbar Demos',
66 | actions: [
67 | AdaptiveAppBarAction(onPressed: () {}, title: "Title"),
68 | AdaptiveAppBarAction(
69 | onPressed: () {},
70 | icon: Icons.info,
71 | iosSymbol: "info.circle",
72 | ),
73 | ],
74 | ),
75 | bottomNavigationBar: AdaptiveBottomNavigationBar(
76 | selectedIndex: _selectedIndex,
77 | onTap: (index) {
78 | setState(() {
79 | if (kDebugMode) {
80 | print('Index selected: $index');
81 | }
82 | _selectedIndex = index;
83 | });
84 | },
85 |
86 | items: [
87 | AdaptiveNavigationDestination(
88 | icon: PlatformInfo.isIOS26OrHigher()
89 | ? "house.fill"
90 | : PlatformInfo.isIOS
91 | ? CupertinoIcons.home
92 | : Icons.home_outlined,
93 | selectedIcon: PlatformInfo.isIOS26OrHigher()
94 | ? "house.fill"
95 | : PlatformInfo.isIOS
96 | ? CupertinoIcons.home
97 | : Icons.home,
98 | label: 'Home',
99 | ),
100 | AdaptiveNavigationDestination(
101 | icon: PlatformInfo.isIOS26OrHigher()
102 | ? "person.fill"
103 | : PlatformInfo.isIOS
104 | ? CupertinoIcons.person
105 | : Icons.person_outline,
106 | selectedIcon: PlatformInfo.isIOS26OrHigher()
107 | ? "person.fill"
108 | : PlatformInfo.isIOS
109 | ? CupertinoIcons.person_fill
110 | : Icons.person,
111 | label: 'Profile',
112 | ),
113 | AdaptiveNavigationDestination(
114 | icon: PlatformInfo.isIOS26OrHigher()
115 | ? "magnifyingglass"
116 | : PlatformInfo.isIOS
117 | ? CupertinoIcons.search
118 | : Icons.search,
119 | label: 'Search',
120 | isSearch: true,
121 | ),
122 | ],
123 | ),
124 |
125 | // body is automatically wrapped into a single-item children list for iOS26Scaffold
126 | // The scaffold handles showing the content based on selectedIndex
127 | body: _buildCurrentScreen(),
128 | );
129 | }
130 | }
131 |
132 | class HomeScreen extends StatefulWidget {
133 | const HomeScreen({super.key});
134 |
135 | @override
136 | State createState() => _HomeScreenState();
137 | }
138 |
139 | class _HomeScreenState extends State {
140 | @override
141 | void initState() {
142 | if (kDebugMode) {
143 | print("Home Screen initState called");
144 | }
145 | super.initState();
146 | }
147 |
148 | @override
149 | Widget build(BuildContext context) {
150 | return const Text("Home Screen");
151 | }
152 | }
153 |
154 | class ProfileScreen extends StatefulWidget {
155 | const ProfileScreen({super.key});
156 |
157 | @override
158 | State createState() => _ProfileScreenState();
159 | }
160 |
161 | class _ProfileScreenState extends State {
162 | @override
163 | void initState() {
164 | if (kDebugMode) {
165 | print("Profile Screen initState called");
166 | }
167 | super.initState();
168 | }
169 |
170 | @override
171 | Widget build(BuildContext context) {
172 | return const Text("Profile Screen");
173 | }
174 | }
175 |
176 | class SearchScreen extends StatefulWidget {
177 | const SearchScreen({super.key});
178 |
179 | @override
180 | State createState() => _SearchScreenState();
181 | }
182 |
183 | class _SearchScreenState extends State {
184 | @override
185 | void initState() {
186 | if (kDebugMode) {
187 | print("Search Screen initState called");
188 | }
189 | super.initState();
190 | }
191 |
192 | @override
193 | Widget build(BuildContext context) {
194 | return const Placeholder();
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/lib/src/widgets/adaptive_segmented_control.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import '../platform/platform_info.dart';
4 | import 'ios26/ios26_segmented_control.dart';
5 |
6 | /// An adaptive segmented control that renders platform-specific styles
7 | ///
8 | /// On iOS 26+: Uses native iOS 26 UISegmentedControl with Liquid Glass
9 | /// On iOS <26 (iOS 18 and below): Uses CupertinoSlidingSegmentedControl
10 | /// On Android: Uses Material SegmentedButton
11 | class AdaptiveSegmentedControl extends StatelessWidget {
12 | /// Creates an adaptive segmented control
13 | const AdaptiveSegmentedControl({
14 | super.key,
15 | required this.labels,
16 | required this.selectedIndex,
17 | required this.onValueChanged,
18 | this.enabled = true,
19 | this.color,
20 | this.height = 36.0,
21 | this.shrinkWrap = false,
22 | this.sfSymbols,
23 | this.iconSize,
24 | this.iconColor,
25 | });
26 |
27 | /// Segment labels to display, in order
28 | final List labels;
29 |
30 | /// The index of the selected segment
31 | final int selectedIndex;
32 |
33 | /// Called when the user selects a segment
34 | final ValueChanged onValueChanged;
35 |
36 | /// Whether the control is interactive
37 | final bool enabled;
38 |
39 | /// Tint color for the selected segment
40 | final Color? color;
41 |
42 | /// Height of the control
43 | final double height;
44 |
45 | /// Whether the control should shrink to fit content
46 | final bool shrinkWrap;
47 |
48 | /// Optional SF Symbol names or IconData
49 | final List? sfSymbols;
50 |
51 | /// Icon size
52 | final double? iconSize;
53 |
54 | /// Icon color
55 | final Color? iconColor;
56 |
57 | @override
58 | Widget build(BuildContext context) {
59 | // iOS 26+ - Use native iOS 26 segmented control
60 | if (PlatformInfo.isIOS26OrHigher()) {
61 | return IOS26SegmentedControl(
62 | labels: labels,
63 | selectedIndex: selectedIndex,
64 | onValueChanged: onValueChanged,
65 | enabled: enabled,
66 | color: color,
67 | height: height,
68 | shrinkWrap: shrinkWrap,
69 | icons: sfSymbols,
70 | iconSize: iconSize,
71 | iconColor: iconColor,
72 | );
73 | }
74 |
75 | // iOS <26 (iOS 18 and below) - Use CupertinoSlidingSegmentedControl
76 | if (PlatformInfo.isIOS) {
77 | return _buildCupertinoSegmentedControl(context);
78 | }
79 |
80 | // Android - Use Material SegmentedButton
81 | if (PlatformInfo.isAndroid) {
82 | return _buildMaterialSegmentedButton(context);
83 | }
84 |
85 | // Fallback
86 | return _buildCupertinoSegmentedControl(context);
87 | }
88 |
89 | Widget _buildCupertinoSegmentedControl(BuildContext context) {
90 | // Build children map from labels or icons
91 | final Map children = {};
92 |
93 | // Check if using icons
94 | final useIcons = sfSymbols != null && sfSymbols!.isNotEmpty;
95 | final itemCount = useIcons ? sfSymbols!.length : labels.length;
96 |
97 | for (int i = 0; i < itemCount; i++) {
98 | if (useIcons) {
99 | // Icon mode
100 | final dynamic icon = sfSymbols![i];
101 | children[i] = Padding(
102 | padding: const EdgeInsets.all(8),
103 | child: icon is IconData
104 | ? Icon(icon, size: iconSize ?? 20, color: iconColor)
105 | : Text(icon.toString()),
106 | );
107 | } else {
108 | // Text mode
109 | children[i] = Padding(
110 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
111 | child: Text(
112 | labels[i],
113 | style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
114 | ),
115 | );
116 | }
117 | }
118 |
119 | Widget control = CupertinoSlidingSegmentedControl(
120 | children: children,
121 | groupValue: selectedIndex,
122 | onValueChanged: (int? value) {
123 | if (enabled && value != null) {
124 | onValueChanged(value);
125 | }
126 | },
127 | );
128 |
129 | if (shrinkWrap) {
130 | control = Center(child: IntrinsicWidth(child: control));
131 | }
132 |
133 | return SizedBox(height: height, child: control);
134 | }
135 |
136 | Widget _buildMaterialSegmentedButton(BuildContext context) {
137 | final segments = >[];
138 |
139 | // Check if using icons
140 | final useIcons = sfSymbols != null && sfSymbols!.isNotEmpty;
141 | final itemCount = useIcons ? sfSymbols!.length : labels.length;
142 |
143 | for (int i = 0; i < itemCount; i++) {
144 | if (useIcons) {
145 | // Icon mode
146 | final dynamic icon = sfSymbols![i];
147 | segments.add(
148 | ButtonSegment(
149 | value: i,
150 | icon: icon is IconData
151 | ? Icon(icon, size: iconSize ?? 20, color: iconColor)
152 | : Icon(Icons.circle, size: iconSize ?? 20, color: iconColor),
153 | ),
154 | );
155 | } else {
156 | // Text mode
157 | segments.add(
158 | ButtonSegment(
159 | value: i,
160 | label: Text(
161 | labels[i],
162 | style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
163 | ),
164 | ),
165 | );
166 | }
167 | }
168 |
169 | Widget control = SegmentedButton(
170 | segments: segments,
171 | selected: {selectedIndex},
172 | onSelectionChanged: enabled
173 | ? (Set newSelection) {
174 | if (newSelection.isNotEmpty) {
175 | onValueChanged(newSelection.first);
176 | }
177 | }
178 | : null,
179 | style: SegmentedButton.styleFrom(
180 | minimumSize: Size.fromHeight(height),
181 | padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
182 | ),
183 | );
184 |
185 | if (shrinkWrap) {
186 | control = Center(child: IntrinsicWidth(child: control));
187 | }
188 |
189 | return control;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------