I'm integrate Flutter module to ios Native project, I want to set Initial Route from ios native, but it not work, it use default route.
ViewController.swift
import UIKit
import Flutter
class ViewController: UIViewController {
let flutterEngine = FlutterEngine(name: "test")
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/home")
flutterEngine.run();
}
#IBAction func handleClick(_ sender: Any) {
let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
flutterViewController.setInitialRoute("/home")
self.navigationController?.pushViewController(flutterViewController, animated: true)
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final _route = <String, WidgetBuilder>{
"/login": (context) => Login(),
"/home": (context) => Home()
};
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: _route,
title: "App", // Title ของหน้า
home: Scaffold(
// หน้าจอหลัก
appBar: AppBar(
title: Text("App Navi"),
),
body: Login(),
),
);
}
}
Login, Home file please see in image, beacause stackoverflow can't to post text "It looks like your post is mostly code; please add some more details."
This issue is now fixed and as of Flutter 1.22 it can be done using:
While initializing flutter engine:
let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
withEntrypoint: FlutterDefaultDartEntrypoint, initialRoute: "/onboarding")
and, Directly while creating the FlutterViewController,
let flutterViewController = FlutterViewController(
project: nil,
initialRoute: "/onboarding",
nibName: nil,
bundle: nil)
Related
I have looked at many similar questions on this topic, but none of the solutions have worked for me.. I am developing an App in Flutter, but want to call a specific method in my main.dart file from AppDelegate.swift in the native iOS project.
To remove all other variables I have extracted the issue into a fresh dart project. I am trying to call setChannelText() from AppDelegate.swift using methodChannel.invokeMethod(), but with no success.
Does anybody know where I am going wrong? I know I'm not acting upon the "name" parameter in methodChannel.invokeMethod(), but that's because I only want the call to invoke the method at all...
Here is my main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
MethodChannel channel =
new MethodChannel("com.example.channeltest/changetext");
String centerText;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.purple,
body: Center(
child: Text(
centerText,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 30.0,
),
),
),
),
);
}
#override
void initState() {
super.initState();
this.channel.setMethodCallHandler((call) async => await setChannelText());
this.centerText = "Hello World!";
}
Future setChannelText() async {
Future.delayed(Duration(milliseconds: 200));
setState(() => this.centerText = "Another Text.");
}
}
And here is my AppDelegate.swift file:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var methodChannel: FlutterMethodChannel!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
//This call would obviously be somewhere else in a real world example, but I'm just
//testing if I can invoke the method in my dart code at all..
methodChannel.invokeMethod("some_method_name", arguments: nil)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
In the end, I am trying to get the text to change right after launch, but it doesn't.
Screenshot of app running on iOS simulator
Thanks in advance for any help!!
Issue
The issue is your platform side (iOS in this case) is calling a method on the Flutter side before Flutter is ready. There is no way to check from the platform side, so your Flutter app must tell your platform side. You'll have the same problem on Android.
Solution
To overcome this, you have to tell the Platform side that the app is ready (by sending a platform method) and save it in a boolean, or instantiate a class, and calling a method. Then the platform side can start sending messages.
You should really read the logs, it should warn you something along the lines of: "There is nothing listening to this, or the Flutter Engine is not attached".
import 'dart:async';
import 'package:flutter/src/services/platform_channel.dart';
class StringService {
final methodChannel =
const MethodChannel("com.example.app_name.method_channel.strings");
final StreamController<String> _stringStreamController =
StreamController<String>();
Stream<String> get stringStream => _stringStreamController.stream;
StringService() {
// Set method call handler before telling platform side we are ready to receive.
methodChannel.setMethodCallHandler((call) async {
print('Just received ${call.method} from platform');
if (call.method == "new_string") {
_stringStreamController.add(call.arguments as String);
} else {
print("Method not implemented: ${call.method}");
}
});
// Tell platform side we are ready!
methodChannel.invokeMethod("isReady");
}
}
You can see a working project at reverse_platform_methods, especially AppDelegate.swift. I didn't implement it for Android, but you can do it in a similar way in MainActivity.kt.
Question
Most apps don't want code to call from the platform side first. What is your use case? I can possibly provide better advice depending on your answer. I implemented this to handle push notifications being delivered to the device, so the "event" is definitely triggered from the platform side.
Also, you should show errors and warnings if you face them, e.g. No implementation found for method $method on channel $name'.
Well, the problem is all about the initialization process. You try to call your method from swift code BEFORE the dart/flutter part is ready to handle it.
You have to do the next steps to achieve the result:
Important. Use applicationDidBecomeActive method in your AppDelegate for ios
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
var methodChannel: FlutterMethodChannel? = nil
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
print("Setup methodChannel from Swift")
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
//THIS METHOD
override func applicationDidBecomeActive(_ application: UIApplication) {
methodChannel?.invokeMethod("some_method_name", arguments: "ios string")
}
}
For Android onStart() method:
class MainActivity : FlutterActivity() {
var channel: MethodChannel? = null
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.channeltest/changetext"
)
}
override fun onStart() {
super.onStart()
channel?.invokeMethod("some_method_name", "android str")
}
}
Create your own class with MethodChannel(like from prev. answer)
class TestChannel {
static MethodChannel channel =
const MethodChannel("com.example.channeltest/changetext");
final StreamController<String> _controller =
StreamController<String>();
Stream<String> get stringStream => _controller.stream;
TestChannel() {
channel.setMethodCallHandler((call) async {
if (call.method == "some_method_name") {
_controller.add(call.arguments as String);
} else {
print("Method not implemented: ${call.method}");
}
});
}
}
Important. Create it global instance
final _changeTextChannel = TestChannel(); //<--- like this
void main() {
runApp(MyApp());
}
Handle it in UI
class TestPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<String>(
stream: _changeTextChannel.stringStream,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasError) {
return Text("Error");
}
if (!snapshot.hasData) {
return Text("Loading");
}
return Text(snapshot.data ?? "NO_DATA");
},
)),
);
}
}
Flutter side code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('samples.flutter.dev/battery');
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
}
Swift code here:
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
// Note: this method is invoked on the UI thread.
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Or refer to this link:
Platform Channels
I have created my first flutter module which is having a single web view in it.
its working fine when running the module independently or running the .ios/Runner project
But when i integrate this module with my ios app and run the app then the web view is disappeared.
dart file code :
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
theme: ThemeData.light(),
home: Home(),
);
}
}
// ignore: must_be_immutable
class Home extends StatelessWidget {
String _url = "https://www.google.com";
final _key = UniqueKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Web Viewwww"),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => SystemNavigator.pop(
animated: true) /*Navigator.pop(context, true)*/,
)),
body: Column(
children: [
Expanded(
child: WebView(
key: _key,
javascriptMode: JavascriptMode.unrestricted,
initialUrl: _url))
],
));
}
}
here is the code for app delegate i used:
lazy var flutterEngine = FlutterEngine()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
flutterEngine.run()
return true
}
Here is the code for iOS viewcontroller for navigation
let engin = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let myflutterVC = FlutterViewController(engine: engin, nibName: nil, bundle: nil)
myflutterVC.modalPresentationStyle = .fullScreen
present(myflutterVC, animated: true, completion: nil)
the first output is for module/runner app.
2nd output is for my ios app.
iOS
In order for plugin to work correctly, you need to add new key to ios/Runner/Info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
If your URL has some special character then you need to encode the URL like this,
WebView(
initialUrl: Uri.encodeFull(yourUrl),
...
)
I am trying to use PlatformViews in Flutter to show Swift code natively in my Flutter app, however my app is crashing with my current code.
This is my AppDelegate currently where I am invoking my method channel:
import Foundation
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate, TJPlacementDelegate {
var p = TJPlacement()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let channelName = "NativeView"
let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel(name: channelName, binaryMessenger: rootViewController as! FlutterBinaryMessenger)
methodChannel.setMethodCallHandler {(call: FlutterMethodCall, result: FlutterResult) -> Void in
if (call.method == "setDebugEnabled") {
let isDebug = call.arguments as! Bool
Tapjoy.setDebugEnabled(isDebug)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
This is my Dart implementation for the native code:
import 'package:flutter/material.dart';
import 'tapjoy.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
callTapjoy();
super.initState();
}
Widget build(context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Test'),
),
body: UiKitView(viewType: 'NativeView'),
),
);
}
void callTapjoy() {
Tapjoy.setDebugEnabled(true);
}
}
//My code in tapjoy.dart
class Tapjoy {
static const MethodChannel _channel = const MethodChannel('NativeView');
static void setDebugEnabled(bool isDebug) {
_channel.invokeMethod('setDebugEnabled', {"isDebug": isDebug});
}
}
My app crashes and shows me an error in the debug console:
Could not cast value of type '__NSDictionaryM' (0x7fff87a61d78) to 'NSNumber' (0x7fff87b1eb08).
2020-04-29 16:56:42.985269+0530 Runner[18484:224162] Could not cast value of type '__NSDictionaryM' (0x7fff87a61d78) to 'NSNumber' (0x7fff87b1eb08).
You are passing a Map from Dart to native: {"isDebug": isDebug}, so you need extract the parameter from the map/dictionary at the Swift end.
if let args = call.arguments as? Dictionary<String, Any>,
let isDebug = args["isDebug"] as? Bool {
// please check the "as" above - wasn't able to test
// handle the method
result(nil)
} else {
result(FlutterError.init(code: "errorSetDebug", message: "data or format error", details: nil))
}
Alternatively, just pass the boolean from the Dart end, without first putting it into a map.
_channel.invokeMethod('setDebugEnabled', isDebug);
I'm creating an app to integrate react-native with an existing Swift app.
I've looked into similar issues:
React-Native: Dismiss/Exit React-Native View back to Native
How can I go back to native view controller from react-native page?
While following different tutorials:
React Native calling class methods on native Swift
Swift in React Native the ultimate guide
React Native tutorial integrating in an existing app*
And the official docs
The problem is: all of them are outdated (but the docs). Most of them use the legacy Navigation rather than Stack Navigator. One of the tutorials (the one with an asterisk) shows how to dismiss the React Native app back to the Native app using the rootTag of the app, but again, this was done with the legacy Navigation.
If I try to do the same, I'm not able to see the props from my app.
I have a single Storyboard with a Button inside that when clicked calls this UIViewController:
ButtonController
import Foundation
import UIKit
import React
class ButtonController: UIViewController {
#IBOutlet weak var button: UIButton!
#IBAction func buttonClicked(_ sender: Any) {
let data:[String : String] = ["onNavigationStateChange": "{handleNavigationChange}",
"uriPrefix":"/app"];
let rootView = MixerReactModule.sharedInstance.viewForModule("ReactNativeApp", initialProperties: data)
let viewController = UIViewController()
viewController.view = rootView
self.present(viewController, animated: true, completion: nil)
}
}
And when I start the app I can see this:
2019-10-23 10:29:30.021 [info][tid:com.facebook.react.JavaScript] Running "ReactNativeApp" with {"rootTag":1,"initialProps":{"uriPrefix":"/app","onNavigationStateChange":"{handleNavigationChange}"}}
But when I try to access the this.props property on React Native code I get undefined.
index.js
import HomeScreen from './components/HomeScreen'
import {AppRegistry} from 'react-native';
import {createAppContainer} from 'react-navigation';
import {createStackNavigator} from 'react-navigation-stack';
const MainNavigator = createStackNavigator({
Home: {screen: HomeScreen}
}, {
initialRouteName: 'Home'
})
const NavigationApp = createAppContainer(MainNavigator);
AppRegistry.registerComponent('ReactNativeApp', () => NavigationApp)
console.log("NANANA1", this)
console.log("NANANA2", this.routeName)
console.log("NANANA3", MainNavigator)
console.log("NANANA4", MainNavigator.props)
console.log("NANANA5", NavigationApp)
console.log("NANANA6", NavigationApp.props)
export default NavigationApp
HomeScreen.js
import React from 'react';
import {Text, View, Button, NativeModules} from 'react-native';
var RNBridge = NativeModules.RNBridge;
export default class HomeScreen extends React.Component {
static navigationOptions = {
headerTitle: () => (
<Text>'Welcome'</Text>
),
headerLeft: () => (
<Button title="Dismiss" onPress={() => {
console.log("WOLOLO: ", RNBridge)
console.log("ROGAN: ", this._reactInternalFiber.tag)
RNBridge.dismissPresentedViewController(this._reactInternalFiber.tag)
// RNBridge.dismissPresentedViewController(1)
}}/>
)
};
render() {
console.log('KIKIKI', this._reactInternalFiber.tag)
console.log("MMMMMMM: ", this.props)
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
}
These are the 2 files that I use in RN to generate my View. I've tried many things to get the rootTag value, and the only one that seems to provide this value is (tag and rootTag on XCode are the same (1))
this._reactInternalFiber.tag
But I don't know how to send these values to my headerLeft method to use the same tag so that when I press the Dismiss button it calls the Swift code for dismissPresentedViewController.
How can I dismiss my VC effectively? Or at least get the rootTag being passed from Swift to my headerLeft() method?
I'm using these versions of react-native:
react-native-cli: 2.0.1
react-native: 0.61.2
If your are looking to dismiss the native swift view controller presented from RN View
self.dismiss(animated: true, completion: nil)
I present the swift view controller as below
let modelVC = ModelViewController() <-- sub class of UIViewController
DispatchQueue.main.async {
let navController = UINavigationController(rootViewController: modelVC)
navController.modalPresentationStyle = .fullScreen
let topController = UIApplication.topMostViewController()
topController?.present(navController, animated: true, completion: nil)
}
I have an extension on the UIApplication to find out what is the top most view controller which is used above
extension UIApplication {
class func topMostViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topMostViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topMostViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topMostViewController(controller: presented)
}
return controller
}
}
Flutter code to send the app to background when back button is pressed. I want to minimize the app to background when i click the back button like home button does to apps and now when i click the back button it kills the app. I am using willPopScope to get it work but no help
I found this package on pub.dev and it worked well for me and it's easy to use
https://pub.dev/packages/move_to_background
03.2020 UPDATE
As #user1717750 wrote - The dart code remains the same, so it's:
var _androidAppRetain = MethodChannel("android_app_retain");
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (Platform.isAndroid) {
if (Navigator.of(context).canPop()) {
return Future.value(true);
} else {
_androidAppRetain.invokeMethod("sendToBackground");
return Future.value(false);
}
} else {
return Future.value(true);
}
},
child: Scaffold(
...
),
);
}
The code in the MainActivity() should look like this:
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "android_app_retain").apply {
setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") {
moveTaskToBack(true)
}
}
}
}
}
From here:
https://medium.com/stuart-engineering/%EF%B8%8F-the-tricky-task-of-keeping-flutter-running-on-android-2d51bbc60882
PLAT FORM CODE:
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, "android_app_retain").apply {
setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") {
moveTaskToBack(true)
}
}
}
}
}
YOUR DART CODE:
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (Platform.isAndroid) {
if (Navigator.of(context).canPop()) {
return Future.value(true);
} else {
_androidAppRetain.invokeMethod("sendToBackground");
return Future.value(false);
}
} else {
return Future.value(true);
}
},
child: Scaffold(
drawer: MainDrawer(),
body: Stack(
children: <Widget>[
GoogleMap(),
],
),
),
);
}
CREDIT TO:
Sergi Castellsagué Millán