I'm almost finished with my 2FA app. It generates TOTP codes for a given secret key, digits, interval and algorithm. However, I have a hard time with syncing multiple CircularProgressIndicators to repeat at the same time.
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. Any ideas how to fix it? https://streamable.com/p7uw8d
I'm using the BLoC pattern to generate TOTP codes.
Code:
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
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
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
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');
}
}
}
}
}