iOS Native混编Flutter 交互实践
hellobike的thrio
https://github.com/hellobike/flutter_thrio
https://juejin.im/post/5eb281e86fb9a0433c50a182
alibaba的 flutter_boost
https://github.com/alibaba/flutter_boost
完成的demo
https://gitee.com/youhui_xm/flutter_boost_sample/
Jony-iMac:~ Jony$ cd /Users/Jony/Documents/DEMO/flutter
Jony-iMac:flutter Jony$ flutter create my_flutter
Creating project my_flutter... androidx: true
my_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata (created)
my_flutter/ios/Runner/Info.plist (created)
my_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (created)
my_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
(created)
my_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x
.png (created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x
.png (created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
(created)
my_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
(created)
my_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard (created)
my_flutter/ios/Runner/Base.lproj/Main.storyboard (created)
my_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
(created)
my_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
(created)
my_flutter/ios/Flutter/Debug.xcconfig (created)
my_flutter/ios/Flutter/Release.xcconfig (created)
my_flutter/ios/Flutter/AppFrameworkInfo.plist (created)
my_flutter/ios/.gitignore (created)
my_flutter/test/widget_test.dart (created)
my_flutter/my_flutter.iml (created)
my_flutter/.gitignore (created)
my_flutter/web/index.html (created)
my_flutter/.metadata (created)
my_flutter/android/app/src/profile/AndroidManifest.xml (created)
my_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (created)
my_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (created)
my_flutter/android/app/src/main/res/drawable/launch_background.xml (created)
my_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (created)
my_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (created)
my_flutter/android/app/src/main/res/values/styles.xml (created)
my_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (created)
my_flutter/android/app/src/main/AndroidManifest.xml (created)
my_flutter/android/app/src/debug/AndroidManifest.xml (created)
my_flutter/android/gradle/wrapper/gradle-wrapper.properties (created)
my_flutter/android/gradle.properties (created)
my_flutter/android/.gitignore (created)
my_flutter/android/settings.gradle (created)
my_flutter/android/app/build.gradle (created)
my_flutter/android/app/src/main/kotlin/com/example/my_flutter/MainActivity.kt
(created)
my_flutter/android/build.gradle (created)
my_flutter/android/my_flutter_android.iml (created)
my_flutter/pubspec.yaml (created)
my_flutter/README.md (created)
my_flutter/ios/Runner/Runner-Bridging-Header.h (created)
my_flutter/ios/Runner/AppDelegate.swift (created)
my_flutter/ios/Runner.xcodeproj/project.pbxproj (created)
my_flutter/lib/main.dart (created)
my_flutter/.idea/runConfigurations/main_dart.xml (created)
my_flutter/.idea/libraries/Flutter_for_Android.xml (created)
my_flutter/.idea/libraries/Dart_SDK.xml (created)
my_flutter/.idea/libraries/KotlinJavaRuntime.xml (created)
my_flutter/.idea/modules.xml (created)
my_flutter/.idea/workspace.xml (created)
Running "flutter pub get" in my_flutter... 1.0s
Wrote 69 files.
All done!
[✓] Flutter: is fully installed. (Channel master, v1.13.9-pre.36, on Mac OS X
10.15.3 19D76, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices: is fully installed.
(Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS: is fully installed. (Xcode 11.3.1)
[✓] Chrome - develop for the web: is fully installed.
[✓] Android Studio: is fully installed. (version 3.6)
[✓] IntelliJ IDEA Ultimate Edition: is fully installed. (version 2019.2.3)
[✓] VS Code: is fully installed. (version 1.43.0)
[✓] Connected device: is fully installed. (3 available)
In order to run your application, type:
$ cd my_flutter
$ flutter run
Your application code is in my_flutter/lib/main.dart.
Jony-iMac:flutter Jony$ cd my_flutter/
android ios my_flutter.iml pubspec.yaml web
Jony-iMac:my_flutter Jony$ cd ios
Jony-iMac:ios Jony$ pod install
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing flutter_boost (0.0.2)
Generating Pods project
Integrating client project
Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed.
[!] Automatically assigning platform `iOS` with version `8.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.
[!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target `Runner` to `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` or include the `Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig` in your build configuration (`Flutter/Release.xcconfig`).
Jony-iMac:ios Jony$ cd ../
Jony-iMac:my_flutter Jony$ flutter build ios --debug --no-codesign
Warning: Building for device with codesigning disabled. You will have to
manually codesign before deploying to device.
Building com.example.myFlutter for device (ios)...
Running pod install... 1.4s
Running Xcode build...
├─Assembling Flutter resources... 9.7s
└─Compiling, linking and signing... 18.8s
Xcode build done. 34.0s
Built
/Users/Jony/Documents/DEMO/flutter/my_flutter/build/ios/iphoneos/Runner.app.
Jony-iMac:my_flutter Jony$ flutter run
Launching lib/main.dart on iPhone 11 Pro Max in debug mode...
Running pod install... 1.5s
Running Xcode build...
├─Assembling Flutter resources... 10.8s
└─Compiling, linking and signing... 18.9s
Xcode build done. 33.6s
flutter: FlutterBoost#onShownContainerChanged old:null now:default
flutter: FlutterBoost#onMetohdCall didInitPageContainer
flutter: FlutterBoost#BoostContainerLifeCycleObserver container:second lifeCycle:ContainerLifeCycle.Init
flutter: FlutterBoost#onMetohdCall willShowPageContainer
flutter: FlutterBoost#ContainerObserver#2 didPush
flutter: flutterboost#didPush
flutter: FlutterBoost#build widget:SecondRouteWidget for page:second(0)
flutter: FlutterBoost#onShownContainerChanged old:default now:0
Syncing files to device iPhone 11 Pro Max...
flutter: FlutterBoost#onMetohdCall didShowPageContainer
flutter: FlutterBoost#onShownContainerChanged old:null now:0 flutter: FlutterBoost#BoostContainerLifeCycleObserver container:second lifeCycle:ContainerLifeCycle.Appear
flutter: FlutterBoost#native containner did show, manager dump: onstage#: {uniqueId=0,name=second} offstage#: {uniqueId=default,name=default} flutter: FlutterBoost#onMetohdCall willDisappearPageContainer flutter: FlutterBoost#BoostContainerLifeCycleObserver container:second lifeCycle:ContainerLifeCycle.WillDisappear
flutter: FlutterBoost#onMetohdCall didDisappearPageContainer flutter: FlutterBoost#BoostContainerLifeCycleObserver container:second lifeCycle:ContainerLifeCycle.Disappear
Syncing files to device iPhone 11 Pro Max... 5,653ms (!)
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
q Quit (terminate the application on the device).
An Observatory debugger and profiler on iPhone 11 Pro Max is available at:
http://127.0.0.1:63692/zgHSnkcPeN8=/
Application finished.
Jony-iMac:my_flutter Jony$
1、pubspec.yaml 加入
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '1.12.13'
2、lib目录下 加入如下代码
main.dart
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | import 'package:flutter/material.dart' ; import 'package:flutter_boost/flutter_boost.dart' ; import 'simple_page_widgets.dart' ; void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @ override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @ override void initState() { super.initState(); FlutterBoost.singleton.registerPageBuilders({ 'embeded' : (pageName, params , _)=>EmbededFirstRouteWidget(), 'first' : (pageName, params , _) => FirstRouteWidget(), 'second' : (pageName, params , _) => SecondRouteWidget(), 'tab' : (pageName, params , _) => TabRouteWidget(), 'platformView' : (pageName, params , _) => PlatformRouteWidget(), 'flutterFragment' : (pageName, params , _) => FragmentRouteWidget( params ), ///可以在native层通过 getContainerParams 来传递参数 'flutterPage' : (pageName, params , _) { print( "flutterPage params:$params" ); return FlutterRouteWidget( params : params ); }, }); FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver()); } @ override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Boost example' , builder: FlutterBoost.init(postPush: _onRoutePushed), home: Container( color:Colors.white )); } void _onRoutePushed( String pageName, String uniqueId, Map params , Route route, Future _) { } } class TestBoostNavigatorObserver extends NavigatorObserver{ void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { print( "flutterboost#didPush" ); } void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { print( "flutterboost#didPop" ); } void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { print( "flutterboost#didRemove" ); } void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) { print( "flutterboost#didReplace" ); } } |
platform_view.dart
import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; typedef void TextViewCreatedCallback(TextViewController controller); class TextView extends StatefulWidget { const TextView({ Key key, this.onTextViewCreated, }) : super(key: key); final TextViewCreatedCallback onTextViewCreated; @override State<StatefulWidget> createState() => _TextViewState(); } class _TextViewState extends State<TextView> { @override Widget build(BuildContext context) { if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.test/view', onPlatformViewCreated: _onPlatformViewCreated, ); } return Text( '$defaultTargetPlatform is not yet supported by the text_view plugin'); } void _onPlatformViewCreated(int id) { if (widget.onTextViewCreated == null) { return; } widget.onTextViewCreated(new TextViewController._(id)); } } class TextViewController { TextViewController._(int id) : _channel = new MethodChannel('plugins.felix.angelov/textview_$id'); final MethodChannel _channel; Future<void> setText(String text) async { assert(text != null); return _channel.invokeMethod('setText', text); } }
simple_page_widgets.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_boost/flutter_boost.dart'; import 'package:my_flutter/platform_view.dart'; class FirstRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('First Route'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( child: Text('Open native page'), onPressed: () { print("open natve page!"); FlutterBoost.singleton.open("native").then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }); }, ), RaisedButton( child: Text('Open second route'), onPressed: () { print("open second page!"); FlutterBoost.singleton.open("second").then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }); }, ), RaisedButton( child: Text('Present second route'), onPressed: () { print("Present second page!"); FlutterBoost.singleton.open("second",urlParams:<dynamic,dynamic>{"present":true}).then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }); }, ), ], ), ), ); } } class EmbededFirstRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: RaisedButton( child: Text('Open second route'), onPressed: () { print("open second page!"); FlutterBoost.singleton.open("second").then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }); }, ), ), ); } } class SecondRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Second Route"), ), body: Center( child: RaisedButton( onPressed: () { // Navigate back to first route when tapped. BoostContainerSettings settings = BoostContainer.of(context).settings; FlutterBoost.singleton.close(settings.uniqueId, result: <dynamic,dynamic>{"result": "data from second"}); }, child: Text('Go back with result!'), ), ), ); } } class TabRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Tab Route"), ), body: Center( child: RaisedButton( onPressed: () { FlutterBoost.singleton.open("second"); }, child: Text('Open second route'), ), ), ); } } class PlatformRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title:Text("Platform Route"), ), body: Center( child: RaisedButton( child: TextView(), onPressed: () { print("open second page!"); FlutterBoost.singleton.open("second").then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }); }, ), ), ); } } class FlutterRouteWidget extends StatefulWidget { FlutterRouteWidget({this.params,this.message}); final Map params; final String message; @override _FlutterRouteWidgetState createState() => _FlutterRouteWidgetState(); } class _FlutterRouteWidgetState extends State<FlutterRouteWidget> { final TextEditingController _usernameController = TextEditingController(); @override Widget build(BuildContext context) { final String message=widget.message; return Scaffold( appBar: AppBar( brightness:Brightness.light, backgroundColor: Colors.white, textTheme:new TextTheme(title: TextStyle(color: Colors.black)) , title: Text('flutter_boost_example'), ), body: SingleChildScrollView( child:Container( margin: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 10.0,bottom: 20.0), child: Text( message ?? "This is a flutter activity \n params:${widget.params}", style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), // Expanded(child: Container()), const CupertinoTextField( prefix: Icon( CupertinoIcons.person_solid, color: CupertinoColors.lightBackgroundGray, size: 28.0, ), padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), clearButtonMode: OverlayVisibilityMode.editing, textCapitalization: TextCapitalization.words, autocorrect: false, decoration: BoxDecoration( border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), ), placeholder: 'Name', ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open native page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton .open("sample://nativePage", urlParams: <dynamic,dynamic>{ "query": {"aaa": "bbb"} }), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open first', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton .open("first", urlParams: <dynamic,dynamic>{ "query": {"aaa": "bbb"} }), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open second', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton .open("second", urlParams:<dynamic,dynamic> { "query": {"aaa": "bbb"} }), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open tab', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton .open("tab", urlParams:<dynamic,dynamic> { "query": {"aaa": "bbb"} }), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton .open("sample://flutterPage", urlParams:<dynamic,dynamic> { "query": {"aaa": "bbb"} }), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'push flutter widget', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () { Navigator.push<dynamic>(context, MaterialPageRoute<dynamic>(builder: (_) => PushWidget())); }, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'push Platform demo', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () { Navigator.push<dynamic>(context, MaterialPageRoute<dynamic>(builder: (_) => PlatformRouteWidget())); }, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open flutter fragment page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => FlutterBoost.singleton .open("sample://flutterFragmentPage"), ), ], ), ), ), ); } } class FragmentRouteWidget extends StatelessWidget { final Map params; FragmentRouteWidget(this.params); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('flutter_boost_example'), ), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 80.0), child: Text( "This is a flutter fragment", style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), Container( margin: const EdgeInsets.only(top: 32.0), child: Text( params['tag'] ?? '', style: TextStyle(fontSize: 28.0, color: Colors.red), ), alignment: AlignmentDirectional.center, ), Expanded(child: Container()), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open native page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => FlutterBoost.singleton.open("sample://nativePage"), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => FlutterBoost.singleton.open("sample://flutterPage"), ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 80.0), color: Colors.yellow, child: Text( 'open flutter fragment page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => FlutterBoost.singleton.open("sample://flutterFragmentPage"), ) ], ), ); } } class PushWidget extends StatefulWidget { @override _PushWidgetState createState() => _PushWidgetState(); } class _PushWidgetState extends State<PushWidget> { VoidCallback _backPressedListenerUnsub; @override void initState() { // TODO: implement initState super.initState(); } @override void didChangeDependencies() { // TODO: implement didChangeDependencies super.didChangeDependencies(); // if (_backPressedListenerUnsub == null) { // _backPressedListenerUnsub = // BoostContainer.of(context).addBackPressedListener(() { // if (BoostContainer.of(context).onstage && // ModalRoute.of(context).isCurrent) { // Navigator.pop(context); // } // }); // } } @override void dispose() { // TODO: implement dispose super.dispose(); _backPressedListenerUnsub?.call(); } @override Widget build(BuildContext context) { return FlutterRouteWidget(message: "Pushed Widget"); } }
test_input.dart
import 'package:flutter/material.dart'; class TestPage extends StatefulWidget { TestPage({Key key, this.title = "Input Test"}) : super(key: key); final String title; @override _TestPageState createState() => _TestPageState(); } class _TestPageState extends State<TestPage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override void initState() { // TODO: implement initState super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: SafeArea( bottom: false, child: ListView( children: <Widget>[ Container( child: Text( 'You have pushed the button this many times:', ), margin: const EdgeInsets.all(8.0), alignment: Alignment.center, ), Container( child: Text( '$_counter', style: Theme.of(context).textTheme.display1, ), margin: const EdgeInsets.all(8.0), alignment: Alignment.center, ), Container( child: TextField( minLines: 2, maxLines: 10, ), padding: const EdgeInsets.all(8.0), ), TestTextField(), Container( child: Container( color: Colors.red, width: double.infinity, height: 128.0, ), padding: const EdgeInsets.all(8.0), ), Container( child: Container( color: Colors.orange, width: double.infinity, height: 128.0, ), padding: const EdgeInsets.all(8.0), ), Container( child: Container( color: Colors.green, width: double.infinity, height: 128.0, ), padding: const EdgeInsets.all(8.0), ), Container( child: Container( color: Colors.blue, width: double.infinity, height: 128.0, ), padding: const EdgeInsets.all(8.0), ), Container( child: Container( color: Colors.yellow, width: double.infinity, height: 128.0, ), padding: const EdgeInsets.all(8.0), ), Container( child: TextField( minLines: 2, maxLines: 10, ), padding: const EdgeInsets.all(8.0), ), TestTextField(), ], )), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } class TestTextField extends StatefulWidget { @override _TestTextFieldState createState() => _TestTextFieldState(); } class _TestTextFieldState extends State<TestTextField> { FocusNode _node; PersistentBottomSheetController _controller; @override void initState() { // TODO: implement initState super.initState(); _node = FocusNode(); _node.addListener(() { if (_node.hasFocus) { print('showBottomSheet'); _controller = Scaffold.of(context) .showBottomSheet<dynamic>((BuildContext ctx) => Container( width: double.infinity, height: 36.0, color: Colors.deepPurple, )); } else { if (_controller != null) { //Navigator.of(context).pop(); print('closeBottomSheet'); _controller.close(); } _controller = null; } }); } @override Widget build(BuildContext context) { return Container( child: TextField( minLines: 2, maxLines: 10, focusNode: _node, ), padding: const EdgeInsets.all(8.0), ); } }
iOS podfile 加入如下代码
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | # Uncomment this line to define a global platform for your project # platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV[ 'COCOAPODS_DISABLE_STATS' ] = 'true' project 'Runner' , { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def parse_KV_file( file , separator= '=' ) file_abs_path = File.expand_path( file ) if !File.exists? file_abs_path return []; end generated_key_values = {} skip_line_start_symbols = [ "#" , "/" ] File.foreach(file_abs_path) do |line| next if skip_line_start_symbols.any? { |symbol| line =~ /^\s* #{symbol}/ } plugin = line. split (pattern=separator) if plugin.length == 2 podname = plugin[0].strip() path = plugin[1].strip() podpath = File.expand_path( "#{path}" , file_abs_path) generated_key_values[podname] = podpath else puts "Invalid plugin specification: #{line}" end end generated_key_values end target 'Runner' do use_frameworks! use_modular_headers! # Flutter Pod copied_flutter_dir = File. join (__dir__, 'Flutter' ) copied_framework_path = File. join (copied_flutter_dir, 'Flutter.framework' ) copied_podspec_path = File. join (copied_flutter_dir, 'Flutter.podspec' ) unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. generated_xcode_build_settings_path = File. join (copied_flutter_dir, 'Generated.xcconfig' ) unless File.exist?(generated_xcode_build_settings_path) raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" end generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) cached_framework_dir = generated_xcode_build_settings[ 'FLUTTER_FRAMEWORK_DIR' ]; unless File.exist?(copied_framework_path) FileUtils.cp_r(File. join (cached_framework_dir, 'Flutter.framework' ), copied_flutter_dir) end unless File.exist?(copied_podspec_path) FileUtils. cp (File. join (cached_framework_dir, 'Flutter.podspec' ), copied_flutter_dir) end end # Keep pod path relative so it can be checked into Podfile.lock. pod 'Flutter' , :path => 'Flutter' # Plugin Pods # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock # referring to absolute paths on developers' machines. system( 'rm -rf .symlinks' ) system( 'mkdir -p .symlinks/plugins' ) plugin_pods = parse_KV_file( '../.flutter-plugins' ) plugin_pods.each do |name, path| symlink = File. join ( '.symlinks' , 'plugins' , name) File. symlink (path, symlink ) pod name, :path => File. join ( symlink , 'ios' ) end end # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. install ! 'cocoapods' , :disable_input_output_paths => true post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings[ 'ENABLE_BITCODE' ] = 'NO' end end end |
iOS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import UIKit import Flutter @ UIApplicationMain @objc class AppDelegate : FlutterAppDelegate { override func application ( _ application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ UIApplication . LaunchOptionsKey : Any ]? ) - > Bool { let router = PlatformRouterImp (); FlutterBoostPlugin . sharedInstance (). startFlutter ( with : router , onStart : { ( engine ) in }); self . window = UIWindow ( frame : UIScreen . main . bounds ) let viewController = ViewController () let navi = UINavigationController ( rootViewController : viewController ) self . window . rootViewController = navi self . window . makeKeyAndVisible () return true ; //super.application(application, didFinishLaunchingWithOptions: launchOptions) } } |
PlatformRouterImp.swift
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 39 40 41 42 43 44 45 46 47 48 | import Foundation class PlatformRouterImp : NSObject , FLBPlatform { func open ( _ url : String , urlParams : [ AnyHashable : Any ], exts : [ AnyHashable : Any ], completion : @ escaping ( Bool ) - > Void ) { var animated = false ; if exts [ "animated" ] != nil { animated = exts [ "animated" ] as ! Bool ; } let vc = FLBFlutterViewContainer . init (); vc . setName ( url , params : urlParams ); self . navigationController (). pushViewController ( vc , animated : animated ); completion ( true ); } func present ( _ url : String , urlParams : [ AnyHashable : Any ], exts : [ AnyHashable : Any ], completion : @ escaping ( Bool ) - > Void ) { var animated = false ; if exts [ "animated" ] != nil { animated = exts [ "animated" ] as ! Bool ; } let vc = FLBFlutterViewContainer . init (); vc . setName ( url , params : urlParams ); navigationController (). present ( vc , animated : animated ) { completion ( true ); }; } func close ( _ uid : String , result : [ AnyHashable : Any ], exts : [ AnyHashable : Any ], completion : @ escaping ( Bool ) - > Void ) { var animated = false ; if exts [ "animated" ] != nil { animated = exts [ "animated" ] as ! Bool ; } let presentedVC = self . navigationController (). presentedViewController ; let vc = presentedVC as ? FLBFlutterViewContainer ; if vc ?. uniqueIDString () == uid { vc ?. dismiss ( animated : animated , completion : { completion ( true ); }); } else { self . navigationController (). popViewController ( animated : animated ); } } func navigationController () - > UINavigationController { let delegate = UIApplication . shared . delegate as ! AppDelegate let navigationController = delegate . window ?. rootViewController as ! UINavigationController return navigationController ; } } |
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 | // // ViewController.swift import UIKit class ViewController : UIViewController { override func viewDidLoad () { super . viewDidLoad () // Do any additional setup after loading the view. } @IBAction func onClickPushFlutterPage ( _ sender : UIButton , forEvent event : UIEvent ){ FlutterBoostPlugin . open ( "first" , urlParams :[ kPageCallBackId : "MycallbackId#1" ], exts : [ "animated" : true ], onPageFinished : { ( _ result : Any ?) in print ( String ( format : "call me when page finished, and your result is:%@" , result as ! CVarArg )); }) { ( f : Bool ) in print ( String ( format : "page is opened" )); } } @IBAction func onClickPresentFlutterPage ( _ sender : UIButton , forEvent event : UIEvent ){ FlutterBoostPlugin . present ( "second" , urlParams :[ kPageCallBackId : "MycallbackId#2" ], exts : [ "animated" : true ], onPageFinished : { ( _ result : Any ?) in print ( String ( format : "call me when page finished, and your result is:%@" , result as ! CVarArg )); }) { ( f : Bool ) in print ( String ( format : "page is presented" )); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下