You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:Flutter InAppWebView调用mobile_scanner扫码并回传至网页JS

问题描述

我正在开发一款Android/iOS双端Flutter应用,通过InAppWebView加载外部URL,支持外部网页用JavaScript调用获取设备联系人、扫描二维码。目前获取联系人功能已实现,但无法在JS触发的Flutter handler中激活摄像头完成扫码,也没法把扫描结果回传给网页,当前使用mobile_scanner包实现扫码。

需要解决的核心问题:如何在JavaScript触发的Flutter handler中调用摄像头扫码,并将结果返回给网页的JavaScript?

解决方案

要实现该功能,需完成以下关键步骤:

1. 配置摄像头权限

扫码前必须获取摄像头权限,需在原生配置文件中声明权限,并动态申请。

2. 实现扫码弹窗界面

通过mobile_scanner创建扫码界面,用Flutter弹窗(如showModalBottomSheet)展示,扫码成功后关闭弹窗并返回结果。

3. 修改JavaScript Handler逻辑

在scanQRCode的handler中启动扫码流程,等待扫码结果后返回给JavaScript。


修改后的Flutter代码

首先确保添加依赖:

dependencies:
  flutter:
    sdk: flutter
  mobile_scanner: ^4.0.0
  permission_handler: ^10.2.0
  flutter_inappwebview: ^6.0.0
  flutter_contacts: ^1.1.0

核心代码修改:

void main() async{
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
    await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode);
  }
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const AppBrowser();
  }
}

class AppBrowser extends StatelessWidget {
  const AppBrowser({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SafeArea(
        child: FetchContacts(),
      ),
    );
  }
}

class FetchContacts extends StatefulWidget {
  const FetchContacts({super.key});

  @override
  State<FetchContacts> createState() => _FetchContacts();
}

class _FetchContacts extends State<FetchContacts> {
  late List contactsListDTO = [];
  InAppWebViewController? _webViewController;

  Future<List<Contact>> getPhoneContactsData() async {
    late List<Contact> contactsList;
    PermissionStatus permission = await Permission.contacts.status;
    if (permission == PermissionStatus.granted) {
      contactsList = await FlutterContacts.getContacts(
          withProperties: true, withPhoto: false);
    }
    return contactsList;
  }

  @override
  void initState() {
    super.initState();
  }

  Future<void> _askPermissions() async {
    PermissionStatus permissionStatus = await _getContactPermission();
    if (permissionStatus != PermissionStatus.granted) {
      _handleInvalidPermissions(permissionStatus);
    }
  }

  Future<PermissionStatus> _getContactPermission() async {
    PermissionStatus permission = await Permission.contacts.status;
    if (permission != PermissionStatus.granted &&
        permission != PermissionStatus.permanentlyDenied) {
      PermissionStatus permissionStatus = await Permission.contacts.request();
      return permissionStatus;
    } else {
      return permission;
    }
  }

  void _handleInvalidPermissions(PermissionStatus permissionStatus) async {
    if (permissionStatus == PermissionStatus.denied) {
      print('Access to contact data denied');
      await openAppSettings();
    } else if (permissionStatus == PermissionStatus.permanentlyDenied) {
      print('Contact data not available on device or permanently Denied');
      await openAppSettings();
    }
  }

  // 新增:扫码方法,返回扫码结果
  Future<String?> _scanQRCode() async {
    // 请求摄像头权限
    PermissionStatus cameraPermission = await Permission.camera.request();
    if (cameraPermission != PermissionStatus.granted) {
      await openAppSettings();
      return null;
    }

    // 弹出扫码界面
    return await showModalBottomSheet<String>(
      context: context,
      isScrollControlled: true,
      builder: (context) {
        return SizedBox(
          height: MediaQuery.of(context).size.height * 0.7,
          child: MobileScanner(
            controller: MobileScannerController(
              detectionSpeed: DetectionSpeed.normal,
              facing: CameraFacing.back,
            ),
            onDetect: (capture) {
              final List<Barcode> barcodes = capture.barcodes;
              if (barcodes.isNotEmpty) {
                String? code = barcodes.first.rawValue;
                if (code != null) {
                  Navigator.pop(context, code);
                }
              }
            },
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final GlobalKey webViewKey = GlobalKey();
    InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: false,
      mediaPlaybackRequiresUserGesture: false,
      allowsInlineMediaPlayback: true,
      iframeAllowFullscreen: true,
      safeBrowsingEnabled: false,
      cacheEnabled: false,
      clearCache: true,
    );

    return InAppWebView(
      key: webViewKey,
      initialSettings: settings,
      initialUrlRequest: URLRequest(
          url: WebUri("https://localhost/staticwebapp/index.html")),
      onReceivedServerTrustAuthRequest: (controller, challenge) async {
        return ServerTrustAuthResponse(
            action: ServerTrustAuthResponseAction.PROCEED);
      },
      onPermissionRequest: (InAppWebViewController controller,
          PermissionRequest permissionRequest) async {
        print('permissionRequest.resources :  ${permissionRequest.resources}');
        return PermissionResponse(
            action: PermissionResponseAction.GRANT,
            resources: permissionRequest.resources);
      },
      onWebViewCreated: (controller) {
        _webViewController = controller;

        controller.addJavaScriptHandler(
            handlerName: 'fetchDeviceContacts',
            callback: (args) async {
              print(args);
              _askPermissions();
              var contactsList = await getPhoneContactsData();
              return jsonEncode(contactsList);
            });

        // 修改扫码handler逻辑
        controller.addJavaScriptHandler(
            handlerName: 'scanQRCode',
            callback: (args) async {
              print(args);
              // 调用扫码方法,等待结果
              String? qrResult = await _scanQRCode();
              // 返回结果给JavaScript
              return qrResult;
            });

        controller.addJavaScriptHandler(
            handlerName: 'handlerFooWithArgs',
            callback: (args) {
              print(args);
            });
      },
      onConsoleMessage: (controller, consoleMessage) {
        print('onConsoleMessage $consoleMessage');
      },
    );
  }
}

原生权限配置

  • Android:在android/app/src/main/AndroidManifest.xml中添加
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
  • iOS:在ios/Runner/Info.plist中添加
<key>NSCameraUsageDescription</key>
<string>需要摄像头权限进行二维码扫描</string>

网页JavaScript代码(保持原逻辑即可)

var isFlutterInAppWebViewReady = false;
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
 isFlutterInAppWebViewReady = true;
 
 alert(isFlutterInAppWebViewReady);
 if (isFlutterInAppWebViewReady) {
 const args = [1, true, ['bar', 5], {foo: 'baz'}];
                
 window.flutter_inappwebview.callHandler('scanQRCode', ...args).
    then(function(result) {
                    console.log(JSON.stringify(result));
                    alert(JSON.stringify(result));
                });
                
}
});

内容的提问来源于stack exchange,提问作者Satya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 15:52:05