Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/timer_button/example/lib/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';

import 'home_page.dart';
import 'theme.dart';

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Timer Button Demo',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
home: const HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
70 changes: 70 additions & 0 deletions packages/timer_button/example/lib/home_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:timer_button/timer_button.dart';

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

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return Scaffold(
appBar: AppBar(
title: const Text('Timer Button Demo'),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TimerButton(
label: 'Try Again',
timeOutInSeconds: 5,
onPressed: () {
log('Time for some action!');
},
),
TimerButton(
label: 'Outlined: Try Again',
timeOutInSeconds: 5,
onPressed: () {},
buttonType: ButtonType.outlinedButton,
disabledColor: colorScheme.error,
color: colorScheme.primary,
activeTextStyle: TextStyle(color: colorScheme.primary),
disabledTextStyle: TextStyle(color: colorScheme.error),
),
TimerButton(
label: 'Text: Try Again',
timeOutInSeconds: 5,
onPressed: () {
log('Time for some action!');
},
timeUpFlag: true,
buttonType: ButtonType.textButton,
disabledColor: colorScheme.errorContainer,
color: colorScheme.primaryContainer,
),
TimerButton.builder(
builder: (context, timeLeft) {
return Text(
'Custom: $timeLeft',
style: TextStyle(color: colorScheme.tertiary),
);
},
onPressed: () {
log('Time for some action!');
},
timeOutInSeconds: 5,
),
],
),
),
),
);
}
}
93 changes: 2 additions & 91 deletions packages/timer_button/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,94 +1,5 @@
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:timer_button/timer_button.dart';

void main() => runApp(const MyApp());

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Timer Button Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});

@override
MyHomePageState createState() {
return MyHomePageState();
}
}
import 'app.dart';

class MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Timer Button Demo'),
),
body: Material(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TimerButton(
label: "Try Again",
timeOutInSeconds: 5,
onPressed: () {
log("Time for some action!");
},
),
TimerButton(
label: "Outlined: Try Again",
timeOutInSeconds: 5,
onPressed: () {},
buttonType: ButtonType.outlinedButton,
disabledColor: Colors.deepOrange,
color: Colors.green,
activeTextStyle: const TextStyle(color: Colors.yellow),
disabledTextStyle: const TextStyle(color: Colors.pink),
),
TimerButton(
label: "Text: Try Again",
timeOutInSeconds: -5,
onPressed: () {
log("Time for some action!");
},
timeUpFlag: true,
buttonType: ButtonType.textButton,
disabledColor: Colors.deepOrange,
color: Colors.green,
),
TimerButton.builder(
builder: (context, timeLeft) {
return Text(
"Custom: $timeLeft",
style: const TextStyle(color: Colors.red),
);
},
onPressed: () {
log("Time for some action!");
},
timeOutInSeconds: 5,
),
],
),
),
),
),
);
}
}
void main() => runApp(const TimerButtonApp());
15 changes: 15 additions & 0 deletions packages/timer_button/example/lib/theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';

class AppTheme {
static ThemeData get light => ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.light,
);

static ThemeData get dark => ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.dark,
);
}
14 changes: 9 additions & 5 deletions packages/timer_button/lib/timer_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,15 @@ class _TimerButtonState extends State<TimerButton> with SafeStateMixin {
int _timeCounter = 0;
Timer? _timer;

int get _safeTimeout =>
widget.timeOutInSeconds < 0 ? 0 : widget.timeOutInSeconds;

String get _timerText => '$_timeCounter${widget.secPostFix}';

@override
void initState() {
super.initState();
_timeCounter = widget.timeOutInSeconds;
_timeCounter = _safeTimeout;
_timeUpFlag = widget.timeUpFlag;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_timeCounter <= 0) {
Expand All @@ -139,13 +142,14 @@ class _TimerButtonState extends State<TimerButton> with SafeStateMixin {
}

void _updateTime() {
if (_timeUpFlag) {
if (_timeUpFlag || _timeCounter <= 0) {
return;
}
_timer = Timer(const Duration(seconds: aSec), () async {
_timer = Timer(const Duration(seconds: aSec), () {
if (!mounted) return;
_timeCounter--;
if (_timeCounter <= 0) {
_timeCounter = 0;
_timeUpFlag = true;
}
safeSetState();
Expand All @@ -157,10 +161,10 @@ class _TimerButtonState extends State<TimerButton> with SafeStateMixin {

void _onPressed() {
widget.onPressed();
// reset the timer when the button is pressed
if (widget.resetTimerOnPressed) {
_timer?.cancel();
_timeUpFlag = false;
_timeCounter = widget.timeOutInSeconds;
_timeCounter = _safeTimeout;
safeSetState();
_updateTime();
}
Expand Down
63 changes: 63 additions & 0 deletions packages/timer_button/test/timer_button_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,69 @@ void main() {
expect(button.onPressed, isNotNull);
expect(find.text('Negative'), findsOneWidget);
});

testWidgets(
'should never display negative counter values with negative timeOutInSeconds',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TimerButton(
label: 'NegInput',
onPressed: () {},
timeOutInSeconds: -5,
),
),
),
);

// Counter should be clamped to 0, not -5
expect(find.text('NegInput | 0s'), findsOneWidget);

// After frame callback, button should be enabled immediately
await tester.pump();
final button =
tester.widget<ElevatedButton>(find.byType(ElevatedButton));
expect(button.onPressed, isNotNull);
expect(find.text('NegInput'), findsOneWidget);
});

testWidgets(
'should clamp counter to 0 and not show negative values during countdown',
(tester) async {
int lastSeenSeconds = 999;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TimerButton.builder(
builder: (context, seconds) {
lastSeenSeconds = seconds;
return Text('Time: $seconds');
},
onPressed: () {},
timeOutInSeconds: 2,
),
),
),
);

expect(lastSeenSeconds, 2);

// Tick past the timeout
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(lastSeenSeconds, 1);

await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(lastSeenSeconds, 0);

// Pump additional seconds — counter should stay at 0
await tester.pump(const Duration(seconds: 1));
await tester.pump();
expect(lastSeenSeconds, 0);
expect(lastSeenSeconds >= 0, isTrue);
});
});
});

Expand Down
Loading