简体   繁体   English

如何同时同步两个 CircularProgressIndicators?

[英]How to sync two CircularProgressIndicators at the same time?

I'm almost finished with my 2FA app.我几乎完成了我的 2FA 应用程序。 It generates TOTP codes for a given secret key, digits, interval and algorithm.它为给定的密钥、数字、间隔和算法生成 TOTP 代码。 However, I have a hard time with syncing multiple CircularProgressIndicators to repeat at the same time.但是,我很难同步多个 CircularProgressIndicators 以同时重复。

Have a look at my video.看看我的视频。 It shows two progress indicators, however, when they start over, they're not synced and there is almost 1 second difference.它显示了两个进度指示器,但是,当它们重新开始时,它们没有同步并且几乎有 1 秒的差异。 Any ideas how to fix it?任何想法如何解决它? https://streamable.com/p7uw8d https://streamable.com/p7uw8d

I'm using the BLoC pattern to generate TOTP codes.我正在使用 BLoC 模式生成 TOTP 代码。

Code:代码:

custom_progress_indicator.dart custom_progress_indicator.dart

import 'package:flutter/material.dart';

class CustomProgressIndicator extends StatefulWidget {
  final int duration;
  final VoidCallback onComplete;
  final double initialDuration;

  CustomProgressIndicator({
    @required this.duration,
    @required this.onComplete,
    @required this.initialDuration,
  });

  @override
  _CustomProgressIndicatorState createState() =>
      _CustomProgressIndicatorState();
}

class _CustomProgressIndicatorState extends State<CustomProgressIndicator>
    with TickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    _controller = AnimationController(
      duration: Duration(seconds: widget.duration),
      lowerBound: 0.0,
      upperBound: 1.0,
      vsync: this,
      animationBehavior: AnimationBehavior.preserve,
    );
    _controller.forward(from: widget.initialDuration);
    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
      ..addStatusListener((AnimationStatus status) {
        if (status == AnimationStatus.completed) {
          widget.onComplete();

          _controller.repeat();
        }
      });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
        return CircularProgressIndicator(
          value: _animation.value,
        );
      },
    );
  }
}

otp_list_tile.dart otp_list_tile.dart

import 'package:duckie/screens/widgets/custom_progress_indicator.dart';
import 'package:flutter/material.dart';

class OtpListTile extends StatelessWidget {
  final int duration;
  final VoidCallback onComplete;
  final double initialDuration;
  final String accountName;
  final String otp;

  OtpListTile({
    @required this.duration,
    @required this.onComplete,
    @required this.initialDuration,
    @required this.accountName,
    @required this.otp,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CustomProgressIndicator(
        duration: duration,
        onComplete: onComplete,
        initialDuration: initialDuration,
      ),
      title: Text(accountName),
      subtitle: Text(otp),
    );
  }
}

home_screen.dart home_screen.dart

import 'dart:io';

import 'package:duckie/blocs/manual_input/manual_input_bloc.dart';
import 'package:duckie/blocs/qr_code_scanner/qr_code_scanner_bloc.dart';
import 'package:duckie/blocs/totp_generator/totp_generator_bloc.dart';
import 'package:duckie/screens/widgets/custom_alert_dialog.dart';
import 'package:duckie/screens/widgets/otp_list_tile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:duckie/shared/text_styles.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List _totpItems;
  TotpGeneratorBloc _totpGeneratorBloc;

  @override
  void initState() {
    BlocProvider.of<TotpGeneratorBloc>(context)..add(GetTotpItemsEvent());
    super.initState();
  }

  @override
  void dispose() {
    _totpGeneratorBloc.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'title',
          style: TextStyles.appBarText,
        ).tr(),
        centerTitle: false,
        elevation: 0.0,
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              Platform.isAndroid
                  ? showAndroidModalBottomSheet(context)
                  : showIosActionSheet(context);
            },
          ),
        ],
      ),
      body: MultiBlocListener(
        listeners: [
          BlocListener<QrCodeScannerBloc, QrCodeScannerState>(
            listener: (context, state) {
              if (state is QrCodeScannerError) {
                Platform.isAndroid
                    ? CustomAlertDialog.showAndroidAlertDialog(
                        context,
                        state.alertDialogErrorTitle,
                        state.alertDialogErrorContent)
                    : CustomAlertDialog.showIosAlertDialog(
                        context,
                        state.alertDialogErrorTitle,
                        state.alertDialogErrorContent);
              }
              if (state is QrCodeScannerFinal) {
                BlocProvider.of<TotpGeneratorBloc>(context)
                    .add(GetTotpItemsEvent());
              }
            },
          ),
          BlocListener<ManualInputBloc, ManualInputState>(
            listener: (context, state) {
              if (state is ManualInputFinal) {
                BlocProvider.of<TotpGeneratorBloc>(context)
                    .add(GetTotpItemsEvent());
              }
            },
          ),
        ],
        child: BlocBuilder<TotpGeneratorBloc, TotpGeneratorState>(
          builder: (context, state) {
            if (state is TotpGeneratorFinal) {
              _totpItems = state.totpItems;

              return ListView.builder(
                itemCount: _totpItems.length,
                itemBuilder: (context, index) {
                  return OtpListTile(
                    duration: _totpItems[index].duration,
                    initialDuration: _totpItems[index].initialDuration,
                    onComplete: () {
                      BlocProvider.of<TotpGeneratorBloc>(context)
                          .add(GetTotpItemsEvent());
                    },
                    accountName: _totpItems[index].accountName,
                    otp: _totpItems[index].otp,
                  );
                },
              );
            }
            return Container();
          },
        ),
      ),
    );
  }
}

void showAndroidModalBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    builder: (BuildContext context) {
      return ListView(
        shrinkWrap: true,
        children: [
          ListTile(
            onTap: () async {
              final qrCodeScannerBloc =
                  BlocProvider.of<QrCodeScannerBloc>(context);

              Navigator.of(context).pop();

              final String qrCodeResponse =
                  await FlutterBarcodeScanner.scanBarcode(
                      '#FF6666', 'cancel'.tr(), false, ScanMode.QR);

              qrCodeScannerBloc.add(GetQrCodeResponseEvent(qrCodeResponse));
            },
            leading: Icon(Icons.qr_code),
            title: Text('scan-qr-code').tr(),
          ),
          ListTile(
            onTap: () {
              Navigator.of(context).pop();
              Navigator.of(context).pushNamed('/manual-input');
            },
            leading: Icon(Icons.keyboard),
            title: Text('manual-input').tr(),
          ),
          ListTile(
            onTap: () {
              Navigator.of(context).pop();
            },
            leading: Icon(Icons.cancel),
            title: Text('cancel').tr(),
          )
        ],
      );
    },
  );
}

void showIosActionSheet(BuildContext context) {
  showCupertinoModalPopup(
    context: context,
    builder: (BuildContext context) {
      return CupertinoActionSheet(
        title: Text('action-sheet-title').tr(),
        message: Text('action-sheet-message').tr(),
        actions: [
          CupertinoActionSheetAction(
            onPressed: () async {
              final qrCodeScannerBloc =
                  BlocProvider.of<QrCodeScannerBloc>(context);

              Navigator.pop(context);

              final String qrCodeResponse =
                  await FlutterBarcodeScanner.scanBarcode(
                      '#FF6666', 'cancel'.tr(), false, ScanMode.QR);

              qrCodeScannerBloc.add(GetQrCodeResponseEvent(qrCodeResponse));
            },
            child: Text('scan-qr-code').tr(),
          ),
          CupertinoActionSheetAction(
            onPressed: () {
              Navigator.of(context).pop();
              Navigator.of(context).pushNamed('/manual-input');
            },
            child: Text('manual-input').tr(),
          ),
          CupertinoActionSheetAction(
            isDestructiveAction: true,
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('cancel').tr(),
          )
        ],
      );
    },
  );
}

totp_generator_bloc.dart totp_generator_bloc.dart

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:dart_otp/dart_otp.dart';
import 'package:duckie/models/code/code_model.dart';
import 'package:hive/hive.dart';
import 'package:meta/meta.dart';

part 'totp_generator_event.dart';
part 'totp_generator_state.dart';

class TotpGeneratorBloc extends Bloc<TotpGeneratorEvent, TotpGeneratorState> {
  TotpGeneratorBloc() : super(TotpGeneratorInitial());

  @override
  Stream<TotpGeneratorState> mapEventToState(
    TotpGeneratorEvent event,
  ) async* {
    if (event is GetTotpItemsEvent) {
      Box box = Hive.box('totpmodel');
      List totpItems = [];

      for (var totpItem in box.values) {
        final String secret = totpItem.secret;
        final int digits = int.parse(totpItem.digits);
        final int interval = int.parse(totpItem.interval);
        final String algorithmString = totpItem.algorithm;
        final String accountName = totpItem.accountName;
        final int secondsSinceEpoch = totpItem.secondsSinceEpoch;
        final double initialDuration =
            (secondsSinceEpoch % interval) / interval;

        OTPAlgorithm algorithm = OTPAlgorithm.SHA1;

        switch (algorithmString) {
          case 'sha1':
            algorithm = OTPAlgorithm.SHA1;
            break;
          case 'sha256':
            algorithm = OTPAlgorithm.SHA256;
            break;
          case 'sha384':
            algorithm = OTPAlgorithm.SHA384;
            break;
          case 'sha512':
            algorithm = OTPAlgorithm.SHA512;
            break;
        }

        final TOTP totp = TOTP(
          secret: secret,
          digits: digits,
          interval: interval,
          algorithm: algorithm,
        );

        try {
          final String otp = totp.now();

          totpItems.add(CodeModel(otp, interval, initialDuration, accountName));

          yield TotpGeneratorFinal(totpItems);
        } catch (error) {
          yield TotpGeneratorError('totp-fail-title', 'totp-fail-content');
        }
      }
    }
  }
}

you can use value parameter or the widget with any state management or you can use any listener on DateTime.now() and get the valued based on fixed formula您可以将值参数或小部件与任何 state 管理一起使用,或者您可以在 DateTime.now() 上使用任何侦听器并根据固定公式获取值

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM