├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── timer_builder_example.gif ├── example ├── .gitignore ├── lib │ └── example.dart └── pubspec.yaml ├── lib └── timer_builder.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/ 34 | 35 | # iOS/XCode related 36 | **/ios/ 37 | 38 | **/web/ -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 8 | channel: dev 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0] - -3/23/2021 2 | 3 | * Null safety 4 | 5 | ## [1.3.0] - 04/14/2019 6 | 7 | * Moved a number of static methods outside of TimerBuilder to make them reusable 8 | 9 | ## [1.2.3] - 02/18/2019 10 | 11 | * Updated README 12 | 13 | ## [1.2.2] - 02/18/2019 14 | 15 | * Updated package documentation 16 | 17 | ## [1.2.0] - 02/18/2019 18 | 19 | * Changed date alignment rules, supporting alignment by the day 20 | 21 | ## [1.1.0] - 02/18/2019 22 | 23 | * Introduced factory constructors for scheduled and periodic time events 24 | 25 | ## [1.0.0] - 02/16/2019 26 | 27 | * Initial release 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Alexander Ryzhov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimerBuilder 2 | 3 | A widget that rebuilds itself on scheduled, periodic, or 4 | dynamically generated time events. 5 | 6 | Here are some use cases for this widget: 7 | * When showing time since or until a specified event; 8 | * When the model updates frequently but you want to limit UI update frequency; 9 | * When showing current date or time; 10 | * When the representation a widget depends on a certain time event. 11 | 12 | ![animated image](https://github.com/aryzhov/flutter-timer-builder/blob/master/doc/timer_builder_example.gif?raw=true) 13 | 14 | ## Examples 15 | 16 | ### Periodic rebuild 17 | 18 | ```dart 19 | import 'package:timer_builder/timer_builder.dart'; 20 | 21 | class ClockWidget extends StatelessWidget { 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return TimerBuilder.periodic(Duration(seconds: 1), 26 | builder: (context) { 27 | return Text("${DateTime.now()}"); 28 | } 29 | ); 30 | } 31 | 32 | } 33 | ``` 34 | 35 | ### Rebuild on a schedule 36 | 37 | ```dart 38 | import 'package:timer_builder/timer_builder.dart'; 39 | 40 | class StatusIndicator extends StatelessWidget { 41 | 42 | final DateTime startTime; 43 | final DateTime endTime; 44 | 45 | StatusIndicator(this.startTime, this.endTime); 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return TimerBuilder.scheduled([startTime, endTime], 50 | builder: (context) { 51 | final now = DateTime.now(); 52 | final started = now.compareTo(startTime) >= 0; 53 | final ended = now.compareTo(endTime) >= 0; 54 | return Text(started ? ended ? "Ended": "Started": "Not Started"); 55 | } 56 | ); 57 | } 58 | 59 | } 60 | 61 | ``` -------------------------------------------------------------------------------- /doc/timer_builder_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryzhov/flutter-timer-builder/c4b667b92be943e504f58b762edea4ceb2ca4899/doc/timer_builder_example.gif -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | android/ 33 | ios/ 34 | web/ 35 | test/ -------------------------------------------------------------------------------- /example/lib/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:timer_builder/timer_builder.dart'; 3 | 4 | void main() { 5 | runApp(SampleApp()); 6 | } 7 | 8 | class SampleApp extends StatefulWidget { 9 | @override 10 | State createState() { 11 | return SampleAppState(); 12 | } 13 | } 14 | 15 | class SampleAppState extends State { 16 | late DateTime alert; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | alert = DateTime.now().add(Duration(seconds: 10)); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return MaterialApp( 27 | home: Scaffold( 28 | appBar: AppBar( 29 | title: Text('Sample App'), 30 | ), 31 | body: TimerBuilder.scheduled([alert], builder: (context) { 32 | // This function will be called once the alert time is reached 33 | var now = DateTime.now(); 34 | var reached = now.compareTo(alert) >= 0; 35 | final textStyle = Theme.of(context).textTheme.title; 36 | return Center( 37 | child: Column( 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | crossAxisAlignment: CrossAxisAlignment.center, 40 | children: [ 41 | Icon( 42 | reached ? Icons.alarm_on : Icons.alarm, 43 | color: reached ? Colors.red : Colors.green, 44 | size: 48, 45 | ), 46 | !reached 47 | ? TimerBuilder.periodic(Duration(seconds: 1), 48 | alignment: Duration.zero, builder: (context) { 49 | // This function will be called every second until the alert time 50 | var now = DateTime.now(); 51 | var remaining = alert.difference(now); 52 | return Text( 53 | formatDuration(remaining), 54 | style: textStyle, 55 | ); 56 | }) 57 | : Text("Alert", style: textStyle), 58 | RaisedButton( 59 | child: Text("Reset"), 60 | onPressed: () { 61 | setState(() { 62 | alert = DateTime.now().add(Duration(seconds: 10)); 63 | }); 64 | }, 65 | ), 66 | ], 67 | ), 68 | ); 69 | }), 70 | ), 71 | theme: ThemeData(backgroundColor: Colors.white), 72 | ); 73 | } 74 | } 75 | 76 | String formatDuration(Duration d) { 77 | String f(int n) { 78 | return n.toString().padLeft(2, '0'); 79 | } 80 | 81 | // We want to round up the remaining time to the nearest second 82 | d += Duration(microseconds: 999999); 83 | return "${f(d.inMinutes)}:${f(d.inSeconds % 60)}"; 84 | } 85 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | author: Alexander Ryzhov 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=2.12.0 < 3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | dev_dependencies: 13 | flutter_test: 14 | sdk: flutter 15 | 16 | timer_builder: 17 | path: ../ 18 | 19 | flutter: 20 | 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /lib/timer_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | /// Used by TimerBuilder to determine the next DateTime to trigger a rebuild on 5 | typedef DateTime? TimerGenerator(DateTime now); 6 | 7 | /// A widget that rebuilds on specific and / or periodic Timer events. 8 | class TimerBuilder extends StatefulWidget { 9 | final WidgetBuilder builder; 10 | final TimerGenerator generator; 11 | 12 | /// Use this constructor only if you need to provide a custom TimerGenerator. 13 | /// For general cases, prefer to use [TimerBuilder.periodic] and [TimerBuilder..scheduled] 14 | /// This constructor accepts a custom generator function that returns the next time event 15 | /// to rebuild on. 16 | TimerBuilder({ 17 | /// Returns the next time event. If the returned time is in the past, it will be ignored and 18 | /// the generator will be called again to retrieve the next time event. 19 | /// If the generator returns [null], it indicates the end of time event sequence. 20 | required this.generator, 21 | 22 | /// Builds the widget. Called for every time event or when the widget needs to be built/rebuilt. 23 | required this.builder, 24 | }); 25 | 26 | @override 27 | State createState() { 28 | return _TimerBuilderState(); 29 | } 30 | 31 | /// Rebuilds periodically 32 | TimerBuilder.periodic( 33 | Duration interval, { 34 | 35 | /// Specifies the alignment unit for the generated time events. Specify Duration.zero 36 | /// if you want no alignment. By default, the alignment unit is computed from the interval. 37 | Duration? alignment, 38 | 39 | /// Builds the widget. Called for every time event or when the widget needs to be built/rebuilt. 40 | required this.builder, 41 | }) : this.generator = periodicTimer(interval, 42 | alignment: alignment ?? getAlignmentUnit(interval)); 43 | 44 | /// Rebuilds on a schedule 45 | TimerBuilder.scheduled( 46 | Iterable schedule, { 47 | 48 | /// Builds the widget. Called for every time event or when the widget needs to be built/rebuilt. 49 | required this.builder, 50 | }) : this.generator = scheduledTimer(schedule); 51 | } 52 | 53 | class _TimerBuilderState extends State { 54 | late Stream stream; 55 | late Completer completer; 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return StreamBuilder( 60 | stream: stream, 61 | builder: (context, _) => widget.builder(context), 62 | ); 63 | } 64 | 65 | @override 66 | void didUpdateWidget(TimerBuilder oldWidget) { 67 | super.didUpdateWidget(oldWidget); 68 | _cancel(); 69 | _init(); 70 | } 71 | 72 | @override 73 | void initState() { 74 | super.initState(); 75 | _init(); 76 | } 77 | 78 | @override 79 | void dispose() { 80 | super.dispose(); 81 | _cancel(); 82 | } 83 | 84 | _init() { 85 | completer = Completer(); 86 | stream = createTimerStream(widget.generator, completer.future); 87 | } 88 | 89 | _cancel() { 90 | completer.complete(); 91 | } 92 | } 93 | 94 | TimerGenerator periodicTimer(Duration interval, 95 | {Duration alignment = Duration.zero}) { 96 | assert(interval > Duration.zero); 97 | 98 | DateTime? next; 99 | return (DateTime now) { 100 | next = alignDateTime((next ?? now).add(interval), alignment); 101 | if (now.compareTo(next!) < 0) { 102 | next = alignDateTime(now.add(interval), alignment); 103 | } 104 | return next!; 105 | }; 106 | } 107 | 108 | TimerGenerator scheduledTimer(Iterable schedule) { 109 | List sortedSpecific = schedule.toList(growable: false); 110 | sortedSpecific.sort((a, b) => a.compareTo(b)); 111 | return fromIterable(sortedSpecific); 112 | } 113 | 114 | TimerGenerator fromIterable(Iterable iterable) { 115 | final iterator = iterable.iterator; 116 | return (DateTime? now) { 117 | return iterator.moveNext() ? iterator.current : null; 118 | }; 119 | } 120 | 121 | /// Creates a stream tha produces DateTime objects at the times specified by the [generator]. 122 | /// Stops the stream when [stopSignal] is received. 123 | Stream createTimerStream( 124 | TimerGenerator generator, 125 | Future stopSignal, 126 | ) async* { 127 | for (var now = DateTime.now(), next = generator(now); 128 | next != null; 129 | now = DateTime.now(), next = generator(now)) { 130 | if (now.compareTo(next) > 0) continue; 131 | Duration waitTime = next.difference(now); 132 | try { 133 | await stopSignal.timeout(waitTime); 134 | return; 135 | } on TimeoutException catch (_) { 136 | yield next; 137 | } 138 | } 139 | } 140 | 141 | /// Returns an alignment unit can be passed to [alignDateTime] in order to align 142 | /// the date/time units. For example, if the specified interval is 15 minutes, 143 | /// the alignment unit is 1 minute. 144 | Duration getAlignmentUnit(Duration interval) { 145 | return Duration( 146 | days: interval.inDays > 0 ? 1 : 0, 147 | hours: interval.inDays == 0 && interval.inHours > 0 ? 1 : 0, 148 | minutes: interval.inHours == 0 && interval.inMinutes > 0 ? 1 : 0, 149 | seconds: interval.inMinutes == 0 && interval.inSeconds > 0 ? 1 : 0, 150 | milliseconds: 151 | interval.inSeconds == 0 && interval.inMilliseconds > 0 ? 1 : 0, 152 | microseconds: 153 | interval.inMilliseconds == 0 && interval.inMicroseconds > 0 ? 1 : 0, 154 | ); 155 | } 156 | 157 | /// Rounds down or up a [DateTime] object using a [Duration] object. 158 | /// If [roundUp] is true, the result is rounded up, otherwise it's rounded down. 159 | /// If the duration is a multiple of days, the result will be aligned at 160 | /// the day mark in the timezone of the source datetime. 161 | DateTime alignDateTime(DateTime dt, Duration alignment, 162 | [bool roundUp = false]) { 163 | assert(alignment >= Duration.zero); 164 | if (alignment == Duration.zero) return dt; 165 | final correction = Duration( 166 | days: 0, 167 | hours: alignment.inDays > 0 168 | ? dt.hour 169 | : alignment.inHours > 0 170 | ? dt.hour % alignment.inHours 171 | : 0, 172 | minutes: alignment.inHours > 0 173 | ? dt.minute 174 | : alignment.inMinutes > 0 175 | ? dt.minute % alignment.inMinutes 176 | : 0, 177 | seconds: alignment.inMinutes > 0 178 | ? dt.second 179 | : alignment.inSeconds > 0 180 | ? dt.second % alignment.inSeconds 181 | : 0, 182 | milliseconds: alignment.inSeconds > 0 183 | ? dt.millisecond 184 | : alignment.inMilliseconds > 0 185 | ? dt.millisecond % alignment.inMilliseconds 186 | : 0, 187 | microseconds: alignment.inMilliseconds > 0 ? dt.microsecond : 0); 188 | if (correction == Duration.zero) return dt; 189 | final corrected = dt.subtract(correction); 190 | final result = roundUp ? corrected.add(alignment) : corrected; 191 | return result; 192 | } 193 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: timer_builder 2 | description: A widget that rebuilds itself on scheduled, periodic, or dynamically generated time events. 3 | homepage: https://github.com/aryzhov/flutter-timer-builder 4 | version: 2.0.0 5 | 6 | environment: 7 | sdk: ">=2.12.0 < 3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | dev_dependencies: 14 | flutter_test: 15 | sdk: flutter 16 | 17 | flutter: 18 | 19 | uses-material-design: true 20 | --------------------------------------------------------------------------------