@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

No Answers yet.

Your answer might help someone💌