如何使用 GetX 和顶级回调处理程序在 Flutter 中路由到不同的屏幕?

How do I route to a different screen, in Flutter, by using GetX and a top level Callback Handler?

简介

为了在Flutter中路由到不同的屏幕,使用了GetX包来简化contextless路由。需要无上下文路由,因为切换到不同屏幕需要能够在未在 Widget 中使用的顶级回调处理程序中发生,因此没有 BuildContext。我使用的回调处理程序源自 caller: ^0.0.4 包,如果 phone 调用结束,主页需要路由到不同的屏幕。我在 Android Studio Arctic Fox 中使用 Android 模拟器 | 2020.3.1 补丁 3,在 Windows 10 桌面上。

程序流程和错误

启动时,出现主页。然后用户在应用程序中发出调用,并由 flutter_phone_direct_caller: ^2.1.0contacts_service: ^0.6.3 包在单独的文件中处理到 main.dart。当 phone 通话结束时,我希望向控制台打印一条语句,其中包括联系人号码和通话持续时间,以及完成到不同屏幕的路线。打印语句正常运行(因此回调处理程序初始化工作),但是,路由没有发生并且出现以下错误消息:

E/flutter (20421): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: You are trying to use contextless navigation without
E/flutter (20421):       a GetMaterialApp or Get.key.
E/flutter (20421):       If you are testing your app, you can use:
E/flutter (20421):       [Get.testMode = true], or if you are running your app on
E/flutter (20421):       a physical device or emulator, you must exchange your [MaterialApp]
E/flutter (20421):       for a [GetMaterialApp].
E/flutter (20421):       
E/flutter (20421): #0      GetNavigation.global (package:get/get_navigation/src/extension_navigation.dart:1094:7)
E/flutter (20421): #1      GetNavigation.toNamed (package:get/get_navigation/src/extension_navigation.dart:592:12)
E/flutter (20421): #2      callerCallbackHandler (package:phone_app/main.dart:22:11)
E/flutter (20421): #3      _callbackDispatcher.<anonymous closure> (package:caller/caller.dart:129:19)
E/flutter (20421): #4      _callbackDispatcher.<anonymous closure> (package:caller/caller.dart:99:43)
E/flutter (20421): #5      MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:386:55)
E/flutter (20421): #6      MethodChannel.setMethodCallHandler.<anonymous closure> (package:flutter/src/services/platform_channel.dart:379:34)
E/flutter (20421): #7      _DefaultBinaryMessenger.setMessageHandler.<anonymous closure> (package:flutter/src/services/binding.dart:379:35)
E/flutter (20421): #8      _DefaultBinaryMessenger.setMessageHandler.<anonymous closure> (package:flutter/src/services/binding.dart:376:46)
E/flutter (20421): #9      _invoke2.<anonymous closure> (dart:ui/hooks.dart:205:15)
E/flutter (20421): #10     _rootRun (dart:async/zone.dart:1428:13)
E/flutter (20421): #11     _CustomZone.run (dart:async/zone.dart:1328:19)
E/flutter (20421): #12     _CustomZone.runGuarded (dart:async/zone.dart:1236:7)
E/flutter (20421): #13     _invoke2 (dart:ui/hooks.dart:204:10)
E/flutter (20421): #14     _ChannelCallbackRecord.invoke (dart:ui/channel_buffers.dart:42:5)
E/flutter (20421): #15     _Channel.push (dart:ui/channel_buffers.dart:132:31)
E/flutter (20421): #16     ChannelBuffers.push (dart:ui/channel_buffers.dart:329:17)
E/flutter (20421): #17     PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:544:22)
E/flutter (20421): #18     _dispatchPlatformMessage (dart:ui/hooks.dart:92:31)

尝试的解决方案

am 使用 GetMaterialApp 进行无上下文导航,所以我怀疑问题可能是回调处理程序在返回 GetMaterialApp 之前被调用,这导致 Navigator 不可用尚未初始化。出于这个原因,我尝试仅在 GetMaterialApp Widget 完成构建后调用路由:WidgetsBinding.instance?.addPostFrameCallback((timeStamp) => Get.toNamed('/testScreen'));。我已确保通过以下语句创建了 WidgetsBinding:WidgetsFlutterBinding.ensureInitialized();。在前一种情况下:路由永远不会发生,我认为这是由于 WidgetsBinding 的实例为空,并且没有出现错误消息。我不知道如何让回调处理程序仅在构建完成后才执行代码,假设这是导致上述错误的问题。我还没有看到在顶级回调处理程序中实现 GetX 路由的其他代码案例。

示例代码

我的 main.dart 文件的内容如下所示,其中包括主要功能、MyApp class 及其构建的 Widget 和命名路由、回调处理程序的初始化和回调处理程序本身:

import 'package:caller/caller.dart';
import 'package:flutter/material.dart';
import 'package:phone_app/screens/home.dart';
import 'package:phone_app/screens/test_screen.dart';
import 'package:get/get.dart';

/// Defines a callback that will handle all background incoming events
///
/// The duration will only have a value if the current event is `CallerEvent.callEnded`
Future<void> callerCallbackHandler(
    CallerEvent event,
    String number,
    int? duration,
    ) async {
  print("New event received from native $event");
  switch (event) {
    case CallerEvent.callEnded:
      print('[ Caller ] Ended a call with number $number and duration $duration');
      Get.toNamed('/testScreen');
      break;
    case CallerEvent.onMissedCall:
      print('[ Caller ] Missed a call from number $number');
      break;
    case CallerEvent.onIncomingCallAnswered:
      print('[ Caller ] Accepted call from number $number');
      break;
    case CallerEvent.onIncomingCallReceived:
      print('[ Caller ] Phone is ringing with number $number');
      break;
  }
}

Future<void> initialize() async {
  /// Check if the user has granted permissions
  final permission = await Caller.checkPermission();
  print('Caller permission $permission');

  /// If not, then request user permission to access the Call State
  if (!permission) {
    Caller.requestPermissions();
  } else {
    Caller.initialize(callerCallbackHandler);
  }
}

void main() {
  WidgetsFlutterBinding.ensureInitialized(); //used when Flutter needs to call native code before calling runApp (sets up internal state of MethodChannels)

  initialize(); //checks for and requests call prompt permissions

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp( //MaterialApp is a child of GetMaterial app (which is used to display dialogs, routes, snackbars, etc. without passing a context)
      debugShowCheckedModeBanner: false,
      title: 'Phone calls',
      theme: ThemeData(
        primarySwatch: Colors.purple,
      ),
      initialRoute: '/',
      getPages: [
        GetPage(
            name: '/',
            page: () => const MyHomePage(title: 'Phone Calls')
        ),
        GetPage(
            name: '/testScreen',
            page: () => const TestScreen()
        ),
      ],
    );
  }
}

经过仔细检查,caller: ^0.0.4 包使用了一个与主隔离区分开的 Dart 隔离区,因此 callerCallbackHandler 方法在应用程序的上下文之外运行。这使得无法更新应用程序状态或执行 UI 影响逻辑。作为一种解决方法,我决定实现一个原生的 Android BroadcastReceiver 来获取不断变化的 phone 状态并路由到指定的屏幕。如果 Flutter 应用程序当前关闭,BroadcastReceiver 也会以编程方式启动它。