@miyajima0630

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

FlutterでのNFC実装のコードにについて

解決したいこと

flutterでNFCを実装していてAPIレベルを34から引き揚げたときにいろいろエラーが出て修正して動くようにはなったのですが、なぜかNFCを起動してキャンセルや✕ボタンを押して終了するとアプリ内の操作が何も操作できず強制終了させるしかなくなります。

該当するソースコード

nfc_reader_dialog.dart

// ignore_for_file: avoid_dynamic_calls

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:future_progress_dialog/future_progress_dialog.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:provider/provider.dart';
import 'package:rive/rive.dart' as rive;

import '../../bloc.dart';
import '../../entity.dart';
import '../../properties.dart';
import '../get_stamp.dart';
import '../input_stamps.dart';
import '../shop_web_view.dart';


class ParseNdefRecord {
  final String prefix;  // ← 明示的に宣言
  final String text;    // ← 明示的に宣言

  ParseNdefRecord(Map<String, dynamic> record)
      : prefix = _getPrefix(record),
        text = _getText(record);

  static String _getPrefix(Map<String, dynamic> record) {
    final tnf = record['typeNameFormat'];
    switch (tnf) {
      case 0: return '';
      case 1: return 'WELLKNOWN:';
      case 2: return 'MEDIA:';
      case 3: return 'URI:';
      case 4: return 'EXTERNAL:';
      case 5: return 'UNKNOWN:';
      case 6: return 'UNCHANGED:';
      default: return '';
    }
  }

  static String _getText(Map<String, dynamic> record) {
    final payload = record['payload'] as Uint8List?;
    if (payload == null) return '';
    return String.fromCharCodes(payload);
  }
}

class NFCReaderDialog extends StatefulWidget {
  const NFCReaderDialog({super.key});
  @override
  NFCReaderDialogState createState() => NFCReaderDialogState();
}

class NFCReaderDialogState extends State<NFCReaderDialog> {
  late Bloc bloc;
  Map<String, dynamic>? ndefData;
  late rive.RiveAnimationController<dynamic> _riveAnimationController;
  late rive.RiveAnimationController<dynamic> _riveScannedController;

  rive.Artboard? _riveArtboard;

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

  Future<void> _startNfc() async {
    try {
      await NfcManager.instance.startSession(
        onDiscovered: _onDiscovered,
        pollingOptions: {
          NfcPollingOption.iso,
          NfcPollingOption.iso,
        },
      );
    } catch (e, stack) {
      FirebaseCrashlytics.instance.recordError(e, stack);
      if (mounted) Navigator.of(context).pop();
      await showDialog<void>(
        context: context,
        builder: (_) =>
            AlertDialog(
              title: const Text('エラー'),
              content: const Text('NFCタグの読取に失敗しました。'),
              actions: [
                TextButton(
                  onPressed: () =>
                      Navigator.of(context).popUntil((route) => route.isFirst),
                  child: const Text('OK'),
                ),
              ],
            ),
      );
    }

    final data = await rootBundle.load('images/scan.riv');
    final file = rive.RiveFile.import(data);

    final artBoard = file.mainArtboard..addController(_riveAnimationController =
        rive.SimpleAnimation('Animation 1'))..addController(
        _riveScannedController = rive.SimpleAnimation('Animation 2'));

    setState(() {
      _riveArtboard = artBoard;
    });

    _riveScannedController.isActive = false;
    _riveAnimationController.isActiveChanged.addListener(() {
      if (!_riveAnimationController.isActive) {
        _riveScannedController.isActive = true;
      }
    });
  }

  @override
  void dispose() {
    NfcManager.instance.stopSession()
        .catchError((dynamic e, StackTrace stack) {
      FirebaseCrashlytics.instance.recordError(e, stack);
    });
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    bloc = Provider.of<Bloc>(context, listen: false);

    if (Platform.isIOS) {
      return const SizedBox.shrink();
    }

    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        height: MediaQuery
            .of(context)
            .size
            .height * 0.45,
        width: double.infinity,
        margin: const EdgeInsets.symmetric(horizontal: 5),
        padding: const EdgeInsets.all(30),
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(30), topRight: Radius.circular(30),
          ),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                'スキャンの準備ができました',
                style: TextStyle(color: Colors.grey),
              ),
            ),
            _riveArtboard == null
                ? const SizedBox.shrink()
                : Expanded(
              child: Container(
                alignment: Alignment.center,
                child: rive.Rive(artboard: _riveArtboard!),
              ),
            ),
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('キャンセル'),
            )
          ],
        ),
      ),
    );
  }


  Future<void> _onDiscovered(NfcTag tag) async {
    try {
      if (Platform.isIOS) await NfcManager.instance.stopSession();
      if (ndefData != null) return;
      final tagData = tag.data as Map<String, dynamic>;

      // Map 形式で NDEF データ取得
      final ndef = tagData['ndef'] as Map<String, dynamic>?;
      if (ndef == null) {
        Navigator.of(context).pop();
        return;
      }

      final records = ndef['cachedMessage']['records'] as List<dynamic>?;
      if (records == null || records.isEmpty) {
        Navigator.of(context).pop();
        return;
      }

      ndefData = records.first as Map<String, dynamic>;

      // NFC ID (identifier)
      String nfcId = '';
      final techList = tagData['tech'] as List<dynamic>? ?? [];
      if (techList.contains('android.nfc.tech.NfcA')) {
        final identifier = tagData['id'] as Uint8List?;
        if (identifier != null) {
          nfcId =
              identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join();
        }
      }

      // NDEF レコード解析
      final parseData = ParseNdefRecord(ndefData!);
      final message = '${parseData.prefix}${parseData.text}';

      // 以下は従来と同じ処理
      final param = message.replaceAll(shopInfoUrl, '').replaceAll(
          newShopInfoUrl, '').split('/');
      if (param.length <= 3 || !groupList.contains(param[0])) {
        await showDialog<void>(
          context: context,
          builder: (context) =>
              AlertDialog(
                title: const Text('エラー'),
                content: const Text(
                    'このNFCタグはスタンプの対象ではありません。'),
                actions: [
                  TextButton(
                    onPressed: () {
                      ndefData = null;
                      Navigator.of(context).popUntil((route) => route.isFirst);
                    },
                    child: const Text('OK'),
                  )
                ],
              ),
        );
        return;
      }

      _riveAnimationController.isActive = false;
      await Future.delayed(const Duration(seconds: 3));

      final area = param[0];
      final rowId = int.tryParse(param[2]) ?? int.tryParse(param[3]) ?? 0;

      final result = await showDialog<ApiResult>(
        barrierDismissible: false,
        context: context,
        builder: (context) =>
            FutureProgressDialog(
              bloc.stampAuthWithNFC(area: area, rowId: rowId, nfcId: nfcId),
              message: const Text('スタンプ認証中...'),
            ),
      ) ?? const ApiResult(status: false);

      if (!result.status) {
        await _showErrorDialog(result, area, rowId, message);
        return;
      }

      final resultData = result.data as Map<String, dynamic>;
      if (resultData['multiple'] == '0') {
        final addResult = await bloc.addStamps(
          shopId: int.tryParse(resultData['shop_id'].toString())!,
          cardId: int.tryParse(resultData['card_id'].toString())!,
          stampCount: 1,
        );
        if (!addResult.status) {
          await _showGenericError(addResult.msg);
          return;
        }
        unawaited(bloc.getStampList());
        unawaited(bloc.getCoupons());
        if (!mounted) return;
        Navigator.of(context).popUntil((route) => route.isFirst);
        await Navigator.of(context).push(
          GetStamp.route(
            stampData: addResult.data,
            shopName: resultData['shop_name'].toString(),
            shopUrl: message,
            shopId: int.tryParse(resultData['shop_id'].toString())!,
          ),
        );
      } else {
        if (!mounted) return;
        Navigator.of(context).popUntil((route) => route.isFirst);
        await Navigator.of(context).push(InputStamp.route(result, message));
      }
    } catch (e, stack) {
      await FirebaseCrashlytics.instance.recordError(e, stack);
      await _showGenericError('予期しないエラーが発生しました。');
    }
  }

  Future<void> _showGenericError(String? msg) async {
    await showDialog<void>(
      context: context,
      builder: (context) =>
          AlertDialog(
            title: const Text('エラー'),
            content: Text(msg ?? 'エラーが発生しました。'),
            actions: [
              TextButton(
                onPressed: () {
                  ndefData = null;
                  Navigator.of(context).popUntil((route) => route.isFirst);
                },
                child: const Text('OK'),
              )
            ],
          ),
    );
  }

  Future<void> _showErrorDialog(ApiResult result, String area, int rowId,
      String message) async {
    await showDialog<void>(
      context: context,
      builder: (context) {
        if (result.data == 'notStamp') {
          return AlertDialog(
            content: Text('${result.msg}\n店舗情報を表示しますか?'),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).popUntil((route) => route.isFirst);
                  Navigator.of(context).push(
                    ShopWebView.route(
                      '$managerUrl/APIs/shopInfo.php',
                    ),
                  );
                },
                child: const Text('はい'),
              ),
              TextButton(
                onPressed: () =>
                    Navigator.of(context).popUntil((route) => route.isFirst),
                child: const Text('いいえ'),
              ),
            ],
          );
        }
        return AlertDialog(
          content: Text(result.msg ?? 'エラーが発生しました。'),
          actions: [
            TextButton(
              onPressed: () =>
                  Navigator.of(context).popUntil((route) => route.isFirst),
              child: const Text('OK'),
            )
          ],
        );
      },
    );
  }
}

main.dart

void main() {
  setupLocator();
  WidgetsFlutterBinding.ensureInitialized();
  LicenseRegistry.addLicense(() async* {
    final license = await rootBundle.loadString('');
    yield LicenseEntryWithLineBreaks(['google_fonts'], license);
  });

  runZonedGuarded(() {
    SystemChrome.setPreferredOrientations(
            [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],)
        .then((_) async {
        const flavor = String.fromEnvironment('flavor');
        shopUrl = newShopInfoUrl;
        if (flavor == 'dev') {
          managerUrl = devManagerUrl;
        } else {
          managerUrl = prodNewManagerUrl;
        }
      await getBitmapDescriptorFromAssetBytes('images/mkr_default.png', 150)
          .then((value) => defaultPin = value);
      await getBitmapDescriptorFromAssetBytes('images/mkr_premium.png', 150)
          .then((value) => premiumPin = value);
      await getBitmapDescriptorFromAssetBytes('images/mkr_stamp.png', 150)
          .then((value) => stampPin = value);
      await Firebase.initializeApp();
      await createNotificationChannel(
        '',
      );
      await createNotificationChannel(
        '',
      );
      analytics = FirebaseAnalytics.instance;
      FirebaseMessaging.onBackgroundMessage(backgroundHandleMessage);
      if (kDebugMode) {
        await FirebaseCrashlytics.instance
            .setCrashlyticsCollectionEnabled(false);
      }
      runApp(const MyApp());
    });
  }, (error, stackTrace) {
    FirebaseCrashlytics.instance.recordError(error, stackTrace);
  });
}


自分で試したこと

chatGPTに聞いたりしてみた結果可能性がありそうなのはセッションをスタートしてから止められていないとのことです。
が、stopSessionをキャンセルのところに実装したりしてみましたが結果は変わらずでした。
アドバイスがあればよろしくお願いいたします。

0 likes

1Answer

NFC付きの android 端末でロジックを簡易化(_startNfc(), キャンセル時のNavigator.of(context).pop() を模擬)
して確認したところ、おっしゃられているアプリの操作が何もできなくなるような現象は確認できませんでした。
stopSession が呼ばれているのかDebugPrintなどを用いて確認してみるのが良いのかもしれません。

以下、動作確認したコードになります。

main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'mock_bloc.dart';
import 'home.dart';

void main() {
  runApp(const MyApp());
}

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return
      Provider<Bloc>(
      create: (_) => Bloc(),
      child: const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: HomePage(),
      ),
    );
  }
}

home.dart
import 'package:flutter/material.dart';
import 'nfc_reader_dialog.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ホーム')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => NFCReaderDialog()),
            );
          },
          child: const Text('次の画面へ'),
        ),
      ),
    );
  }
}
nfc_reader_dialog.dart
// ignore_for_file: avoid_dynamic_calls
import 'dart:async';
import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:provider/provider.dart';
import 'package:rive/rive.dart' as rive;

import 'mock_bloc.dart';
import 'mock_entity.dart';
import 'mock_properties.dart';

class ParseNdefRecord {
  final String prefix;
  final String text;

  ParseNdefRecord(Map<String, dynamic> record)
      : prefix = 'WELLKNOWN:',
        text = utf8.decode(record['payload'] ?? []);
}

class NFCReaderDialog extends StatefulWidget {
  const NFCReaderDialog({super.key});
  @override
  NFCReaderDialogState createState() => NFCReaderDialogState();
}

class NFCReaderDialogState extends State<NFCReaderDialog> {
  Map<String, dynamic>? ndefData;
  rive.Artboard? _riveArtboard;
  late rive.SimpleAnimation _riveAnimation;
  late rive.SimpleAnimation _riveScanned;

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

  @override
  void dispose() {
    NfcManager.instance.stopSession()
        .catchError((dynamic e, StackTrace stack) {
    });
    super.dispose();
    debugPrint('⚠️ Disposed');
  }

  Future<void> _startNfc() async {
    try {
      await NfcManager.instance.startSession(
          onDiscovered: _onDiscovered
      );
      debugPrint('⚠️ NFC starts');
    } catch (e, stack) {
      if (mounted) Navigator.of(context).pop();
      await showDialog<void>(
        context: context,
        builder: (_) =>
            AlertDialog(
              title: const Text('エラー'),
              content: const Text('NFCタグの読取に失敗しました。'),
              actions: [
                TextButton(
                  onPressed: () =>
                      Navigator.of(context).popUntil((route) => route.isFirst),
                  child: const Text('OK'),
                ),
              ],
            ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    final bloc = Provider.of<Bloc>(context, listen: false);

    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(30),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text('スキャンの準備ができました',
                  style: TextStyle(color: Colors.grey, fontSize: 18)),
              Expanded(
                child: Center(
                  child: _riveArtboard == null
                      ? const CircularProgressIndicator()
                      : rive.Rive(artboard: _riveArtboard!),
                ),
              ),
              ElevatedButton(
                onPressed: () async {
                  final result = await bloc.stampAuthWithNFC(
                    area: 'tokyo',
                    rowId: 1,
                    nfcId: 'ABC123',
                  );
                  if (result.status && mounted) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('NFCスキャン成功!')),
                    );
                  }
                },
                child: const Text('NFCモックスキャン実行'),
              ),
              ElevatedButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: const Text('キャンセル'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Future<void> _onDiscovered(NfcTag tag) async {
  debugPrint('⚠️ NFC読み取り成功: ');
  return;
}

pubspec.yaml
environment:
  sdk: ^3.5.0

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  nfc_manager: ^3.5.0
  rive: ^0.13.5
  provider: ^6.1.2

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^4.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  assets:
    - assets/images/scan.riv
  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/to/resolution-aware-images

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/to/asset-from-package

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/to/font-from-package

0Like

Your answer might help someone💌