Im trying to use plugin Android Alarm Manager for timer and code execution in background, but cant really get it working right. If i set something like "print("v")" as a callback - everything works fine, but when Im trying to do something extra, it just doesn't work.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:android_alarm_manager/android_alarm_manager.dart';
import 'dart:isolate';
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
startTimer(sendport) async {
await AndroidAlarmManager.oneShot(
Duration(seconds: 60), 0, timerCallback(sendport),
wakeup: true, exact: true);
}
timerCallback(sendport) {
sendport.send("DONE");
}
class _MyAppState extends State<MyApp> {
ReceivePort receivePort = ReceivePort();
SendPort sendport;
#override
void initState() {
super.initState();
AndroidAlarmManager.initialize();
receivePort.listen((v) {
print(v);
});
}
#override
Widget build(BuildContext context) {
RaisedButton(
onPressed: startTimer(sendport),
child: Text("Start"),
);
}
}
I expect that code to send message after 1 minute, instead im getting message right after execution and get Error
"/flutter (11424): [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: 'dart:ui/plugins.dart': Failed assertion: line 62: '': 'callback' must not be null."
In the question code, startTimer() calls timerCallup (sendPort) when setting up the oneShot, and so it's happening immediately.
The timerCallback function required by the oneShot call does not take any parameters. When the timerCallback runs at the alarm time, it runs in the context of an "isolate", which means it is isolated from the rest of your application. Constants seem to be accessible but variable values are not. The timerCallback function can talk to your app via the ports to exchange information and can use limited APIs to talk the the system, but that's about it. Check the docs on isolates for details.
Isolates can find one another by registering and looking up names with IsolateNameServer
You need a name for the port which the main app and the callback isolate can both see, a global constant seems to work ok for that.
The timer callback passed to AlarmManager.oneShot doesn't have a parameter, so it needs to look up the port name to find the SendPort to use when it runs.
Register the port name for the main isolate's SendPort when setting up - you still need the ReceivePort in MyAppState, but you don't need the SendPort.
The code below registers the receive port with a name, the timerCallback looks that name up, and then sends the message to the send port belonging to the receive port. It does feel very clunky.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:android_alarm_manager/android_alarm_manager.dart';
import 'dart:isolate';
import 'dart:ui';
const String portName = "MyAppPort";
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
startTimer() async {
await AndroidAlarmManager.oneShot(
Duration(seconds: 60), 0, timerCallback,
wakeup: true, exact: true);
}
timerCallback() {
SendPort sendPort = IsolateNameServer.lookupPortByName(portName);
if (sendPort != null) {
sendport.send("DONE");
}
}
class _MyAppState extends State<MyApp> {
ReceivePort receivePort = ReceivePort();
#override
void initState() {
super.initState();
IsolateNameServer.registerPortName(receivePort.sendPort, portName)
AndroidAlarmManager.initialize();
receivePort.listen((v) {
print(v);
});
}
#override
Widget build(BuildContext context) {
RaisedButton(
onPressed: startTimer(),
child: Text("Start"),
);
}
Related
As title, In flutter,how to redirect to a specified screen(not main scrren) after clicking on received background push notification?
PS: Foreground mode is ok!
Pls see my code, after I listened background message,then...what should I do next?
The following is my main.dart code:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import './tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
#pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
print("Handling a background message: ${message.data['id']}");
print("Handling a background message: ${message.data['screen']}");
}
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(home: Tabs());
}
}
// return const MaterialApp(home: Tabs());
After clicking on background push notification,jump to a specified screen,not main screen.thanks all.
you can handle the background message after your flutter app has a context.
here the documentation: https://firebase.flutter.dev/docs/messaging/notifications/#handling-interaction
handling interaction from background message
Future<void> setupInteractedMessage() async {
// Get any messages which caused the application to open from
// a terminated state.
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
// If the message also contains a data property with a "type" of "chat",
// navigate to a chat screen
if (initialMessage != null) {
_handleMessage(initialMessage);
}
// Also handle any interaction when the app is in the background via a
// Stream listener
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
}
void _handleMessage(RemoteMessage message) {
if (message.data['type'] == 'chat') {
Navigator.pushNamed(context, '/chat',
arguments: ChatArguments(message),
);
}
}
then call the function inside the initState. if your apps has a SplasScreen put the function there. So after the apps initialize, the function will executed before navigate to the mainscreen.
but if you dont have SplashScree, you can put it on top of all function inside the initState on the MainScreen.
#override
void initState() {
super.initState();
// Run code required to handle interacted messages in an async function
// as initState() must not be async
setupInteractedMessage();
}
I am trying to wait till amplify configuration is done then load the login screen. Even though state seems to be getting updated I am still getting the loadinscreen. Why is that?
I am not sure if setState is proper method on the init : Importance of Calling SetState inside initState
As per the doc : https://docs.amplify.aws/start/getting-started/integrate/q/integration/flutter/#configure-amplify
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isAmplifyConfigured = false;
late AmplifyAuthCognito auth;
#override
void initState() {
_initializeApp();
super.initState();
}
Future<void> _initializeApp() async {
await _configureAmplify();
setState(() {
_isAmplifyConfigured = true;
});
}
Future<void> _configureAmplify() async {
auth = AmplifyAuthCognito();
try {
await Amplify.addPlugin(auth);
await Amplify.configure(amplifyconfig);
} on AmplifyAlreadyConfiguredException {
print(
'Amplify was already configured. Looks like app restarted on android.');
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: AppRoutes.onGenerateRoute,
initialRoute: _isAmplifyConfigured
? LoginScreen.routeName
: LoadingScreen.routeName,
);
}
}
I think the issue is with you trying to reassign your initialRoute. I'm not super familiar with this property, but given the name I assume this is set once and is not rebuilt, not even when the state changes. It would make sense, also, because the rest of your code sounds like it should work.
Before trying anything else, I'd recommend you move your logic to initialize Amplify to the LoginScreen, and having its body depend on the _isAmplifyConfigured boolean value. So show spinner if it's false, and show Login fields when it's true.
Even better would be to create a HomeScreen, so you can keep this Amplify initialization at the bottom of your app's stack. And then have your HomeScreen either show the Login widgets, the home screen of your app, or a loading state.
I would like to access a function defined in my top level class from another class. How can i do that ?
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp>
{
void startListeningNotifications()
{
//start listening to fcm messages
}
void initState()
{
super.initState();
startListeningNotifications();
}
}
I want to call this function startListeningNotifications() from another class. Is that possible ?
I am already calling this function in initState() but there are some cases in which i need to call it from some other class. For example, if a user isn't already registered with your Firebase-app, then after the registration process, i need to access this method in order to start listening to fcm notifications.
You can define startListeningNotifications() in a different file, import it into any page you need and call it there.
// create lib/_utils/fcm_utils.dart
void startListeningNotifications() {
// your function
}
...
// main.dart or any page you want to call your functions from
// TODO: replace yourAppName below with your app name
import 'package:yourAppName/_utils/fcm_utils.dart';
...
void initState() {
super.initState();
startListeningNotifications();
}
I am trying to get user permission at the start of the app inside main but i get and exception.
void main() async {
final permission = Permission.manageExternalStorage.request();
if (await permission.isGranted){
}
Error:
Exception has occurred.
_CastError (Null check operator used on a null value)
Check out the code below, I think it can solve your problem, I tested it and worked for me. Instead of getting the permission in main(), it uses the stateful widget MyApp, tries to get the permission in initState(), uses async function _getPermission() to await the user's response and with setState() updates the permission grant state in _permission member.
In the build method you can decide what your app should do if permission is granted or not. In this example I only output a single message depending on the permission. For example you can return your MaterialApp when granted, and a warning message if not, anything you like.
You might be aware of this, but one more thing: if the user permanently denies permission, this solution will not prompt again, so you have to handle this case, if your app cannot work without this, you can offer the user to go into settings and enable it there.
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _permission = false;
void _getPermission() async {
final grant = await Permission.camera.request().isGranted;
setState(() {
_permission = grant;
});
}
#override
void initState() {
_getPermission();
super.initState();
}
#override
Widget build(BuildContext context) {
String message =
_permission ? 'Permission granted' : 'Permission not granted';
return MaterialApp(home: Scaffold(body: Center(child: Text(message))));
}
}
I don't know which package do you use for permission handling, but if you use this, you have to add request() before isGranted:
if (await Permission.manageExternalStorage.request().isGranted) {
}
Or if you want just to get permission status without asking for it:
var status = await Permission.manageExternalStorage.status;
if (status.isDenied) {
}
I have a function called control in the StateFull Widget. I want to run this function with WorkManager every 15 minutes.
How can I call the control function from the callbackDispatcher function?
I added a Stream statically to the Statefull widget and then listened to it but it didn't work.
HomeScreen.dart file
import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';
const taskKontrol = "control";
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Container();
}
#override
void initState() {
super.initState();
setupWorkManager();
}
void control() async
{
//... my code control is here
}
}
void setupWorkManager() async {
await Workmanager.initialize(callbackDispatcher, isInDebugMode: true);
Workmanager.registerPeriodicTask(taskKontrol, taskKontrol,
frequency: Duration(seconds: 10),
existingWorkPolicy: ExistingWorkPolicy.append
);
}
void callbackDispatcher() {
Workmanager.executeTask((taskName, inputData) async {
switch(taskName)
{
case taskKontrol:
// How can I call the control function from here?
print("control from workmanager");
break;
}
return Future.value(true);
});
}
For those who still looking for an answer:
From the official docs:
The callbackDispatcher needs to be either a static function or a top level function to be accessible as a Flutter entry point.
I had this same problem and I solved it by moving the function callbackDispatcher to the file: main.dart
Also, the code that initializes callbackDispatcher must be in main() before the App() widget loads.
To call your control code, create a class with static function control()
Note: You cannot call the widget's method from callbackDispatcher!
Reason: Widgets are UI bound. As long as the screen remains active, the widget that is visible remains active. Once you close the app or move on to next screen, the widgets' memory gets recycled. But this callbackDispatcher gets executed even when your app is closed. So, it has to be isolated from UI code.
Here's the code:
main.dart:
import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
runApp(App());
}
void callbackDispatcher() {
Workmanager.executeTask((taskName, inputData) async {
switch(taskName)
{
case ScheduledTask.taskName:
ScheduledTask.control(); // calls your control code
break;
}
return Future.value(true);
});
}
class ScheduledTask {
const static String taskName = "control";
static void control() {
// add your control here
}
}
All you can do from HomeScreen widget is to call setupWorkManager() that schedules the task
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Container();
}
#override
void initState() {
super.initState();
setupWorkManager();
}
}
void setupWorkManager() async {
Workmanager.registerPeriodicTask(taskKontrol, taskKontrol,
frequency: Duration(minutes: 15),
existingWorkPolicy: ExistingWorkPolicy.append
);
}
Note: The minimum frequency for the recurring task is 15 minutes