たろすの技術メモ

Jot Down the Tech

ソフトウェアエンジニアのメモ書き

【Flutter】qr_mobile_visionでiOSのカメラ権限を不許可にするとクラッシュするバグ

※この記事は【Flutter】qr_mobile_visionでiOSのカメラ権限を不許可にするとクラッシュするバグ - Qiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

開発環境

バグ

issue

qr_mobile_visionを導入したのですがiOSでのみ、端末のカメラ権限が不許可になるとアプリがクラッシュするバグが発生しました。

公式にissueも立っているのですがバージョンアップを待つには致命的過ぎるバグなのでpermission_handlerを使って回避することにしました。

qr_mobile_vision

QRコードやバーコードを読み取ってデータを取得できるpluginです。利用するにはFirebaseを導入する必要があります。

permission_handler

カメラや位置情報など、様々な権限の取得をサポートしてくれます。設定アプリに遷移させる機能もあり、とても使い勝手が良いです。権限関連のpluginを検索すると一番上に出てきます。今後も使う頻度が高そうです。

回避策

  1. カメラ機能を使う準備
  2. カメラの権限をStateで管理しておく
  3. permission_handlerで権限を確認する
  4. 権限有りならカメラ表示、無しなら代わりにAlert Dialogを表示(設定アプリに遷移できるボタンを設置)

(1)カメラ機能を使う準備

両OSに権限の設定をします。

iOS

// info.plist
<key>NSCameraUsageDescription</key>
<string>このアプリでは○○のために、カメラを利用します。</string>

Android

// AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA"/>

(2)カメラの権限をStateで管理しておく

Stateで管理することによって「使用許可されている時はカメラ表示」「使用許可されていない時はアラートを表示」みたいに処理を分ける事ができます。

// qr_camera_screen.dart
import 'package:permission_handler/permission_handler.dart';
import 'package:qr_mobile_vision/qr_camera.dart';

PermissionStatus _permissionStatus = PermissionStatus.undetermined;

(3)permission_handlerで権限を確認する

使用許可のアラートを出してユーザに確認してくれます。

// qr_camera_screen.dart
Future<void> _requestPermission() async {
  var status = await Permission.camera.request();
  setState(() {
    _permissionStatus = status;
  });
}

(4)権限有りならカメラ表示、無しなら代わりにAlert Dialogを表示(設定アプリに遷移できるボタン有り)

権限を確認してからQrCameraを生成することで、「権限がなくてクラッシュする」バグを回避できます。

// qr_camera_screen.dart
_checkPermissionStatus() {
  switch (_permissionStatus) {
    case PermissionStatus.undetermined:
      print('カメラの使用許可/不許可が未選択');
      _requestPermission();
      return Container();
    case PermissionStatus.permanentlyDenied:
      print('カメラの権限が手動で設定しない限り不許可');
      return _showDialog();
    case PermissionStatus.restricted:
      print('カメラの使用制限');
      return _showDialog();
    case PermissionStatus.denied:
      print('カメラの使用不許可');
      return _showDialog();
    case PermissionStatus.granted:
      print('カメラの使用許可');
      return QrCamera(
          notStartedBuilder: _notStartedBuilder,
          onError: (context, error) => Text(
            error.toString(),
            style: const TextStyle(color: Colors.red),
          ),
          qrCodeCallback: (code) {
            print('QRコード or バーコード: $code');
          });
    default:
      return _showDialog();
  }
}

_showDialog() {
  return AlertDialog(
    title: Text("カメラが許可されていません。"),
    content: Text("このアプリではカメラを使用します。"),
    actions: <Widget>[
      FlatButton(
        child: Text("Cancel"),
        onPressed: () {
          Navigator.of(context).pop();
        },
      ),
      FlatButton(
        child: Text("設定"),
        onPressed: () {
          openAppSettings(); // 設定アプリに遷移
      }),
    ],
  );
}

全体コード

// qr_camera_screen.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:qr_mobile_vision/qr_camera.dart';

class QrCameraScreen extends StatefulWidget {
  QrCameraScreen();

  @override
  State<StatefulWidget> createState() => _QrCameraScreenState();
}

class _QrCameraScreenState extends State<QrCameraScreen> {

PermissionStatus _permissionStatus = PermissionStatus.undetermined;
final WidgetBuilder _notStartedBuilder = (context) => Container();

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('QRカメラ'),
      brightness: Brightness.light,
    ),
    body: SafeArea(
      child: _checkPermissionStatus();
    ),
  );
}

/// カメラ使用権限を確認してWidgetを返す
_checkPermissionStatus() {
  switch (_permissionStatus) {
    case PermissionStatus.undetermined:
      print('カメラの使用許可/不許可が未選択');
      _requestPermission();
      return Container();
    case PermissionStatus.permanentlyDenied:
      print('カメラの権限が手動で設定しない限り不許可');
      return _showDialog();
    case PermissionStatus.restricted:
      print('カメラの使用制限');
      return _showDialog();
    case PermissionStatus.denied:
      print('カメラの使用不許可');
      return _showDialog();
    case PermissionStatus.granted:
      print('カメラの使用許可');
      return QrCamera(
          notStartedBuilder: _notStartedBuilder,
          onError: (context, error) => Text(
            error.toString(),
            style: const TextStyle(color: Colors.red),
          ),
          qrCodeCallback: (code) {
            print('QRコード: $code');
          });
    default:
      return _showDialog();
  }
}

/// カメラの使用許可状況を確認
Future<void> _requestPermission() async {
  var status = await Permission.camera.request();
  setState(() {
    _permissionStatus = status;
  });
}

/// カメラ使用不許可だった場合にアラートを出す
_showDialog() {
  return AlertDialog(
    title: Text("カメラが許可されていません。"),
    content: Text("このアプリではカメラを使用します。"),
    actions: <Widget>[
      FlatButton(
        child: Text("Cancel"),
        onPressed: () {
          Navigator.of(context).pop();
        },
      ),
      FlatButton(
          child: Text("設定"),
          onPressed: () {
            openAppSettings(); // 設定アプリに遷移
        }),
      ],
    );
  }
}

以上。