├── .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 | ![Screenshot](url-to-image) 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 | --------------------------------------------------------------------------------