Sending your application to background when back button is pressed in flutter - dart

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

Related

How do I use a Flutter MethodChannel to invoke a method in dart code from the native swift code?

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

flutter web view is not working with ios app

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),
...
)

How to call arguments from Flutter in Swift native code?

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);

How to set Initial Route from native iOS

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)

Swift #functionBuilder doesn't recognize variadic elements

I'm using a function builder in my project and am having problems implementing buildIf. Here's the builder:
#_functionBuilder
class TableSectionBuilder {
static func buildBlock(_ children: TableGroup...) -> [TableGroup] {
children
}
static func buildBlock(_ children: [TableGroup]...) -> [TableGroup] {
children.flatMap { $0 }
}
static func buildExpression(_ child: TableGroup) -> [TableGroup] {
[child]
}
static func buildIf(_ children: [TableGroup]?) -> [TableGroup] {
return children ?? []
}
}
Here's an example of how I'd like to use it (note: Text is a custom object, similar to SwiftUI's Text)
func test() {
Self.section(identifier: "") {
Text("")
Text("")
if true {
Text("")
}
}
}
Unfortunately, this doesn't compile, though this compiles:
func test() {
Self.section(identifier: "") {
[Text(""),
Text("")]
if true {
Text("")
}
}
}
It looks to me that the function builder cannot properly map from an array to a variadic list of items. Also, removing the if in the first example makes it compile.
Ideas?
This is a limit of Swift today. It is inherently why SwiftUI views are limited to 10 right now, and you see methods like combineLatest3 and combineLatest4 in the Combine framework.
According to the swift forums, variadic function builders is something being worked on and may be included in Swift 6.

Resources