I have built a working countdown clock that displays the time remaining in the format, hours:minutes:senconds. inside a stateful widget. that uses a fullscreen inkwell to start and stop it.
What I want to do is transfer this code to a change notifier. (I already a change notifier setup ready to go called CountdownTimers) so i can see this countdown running from multiple pages on my app.
Here is the code for the working countdown clock in the stateful widget:
class ChessClock2 extends StatefulWidget {
const ChessClock2({Key? key}) : super(key: key);
#override
State<ChessClock2> createState() => _ChessClock2State();
}
class _ChessClock2State extends State<ChessClock2> {
static const countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
Duration duration = Duration();
Timer? timer;
bool beenPressed = false;
#override
void initState() {
super.initState();
Reset();
}
void Reset() {
setState(() => duration = countdownDuration);
}
void AddTime() {
final minusSeconds = -1;
setState(() {
final seconds = duration.inSeconds + minusSeconds;
if (seconds < 0) {
timer?.cancel();
} else {
duration = Duration(seconds: seconds);
}
});
}
void StartTimer() {
timer = Timer.periodic(
Duration(seconds: 1),
(_) => AddTime(),
);
}
void StopTimer() {
setState(() => timer?.cancel());
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InkWell(
onTap: () {
setState(() {
beenPressed = !beenPressed;
});
beenPressed ? StartTimer() : StopTimer();
},
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: beenPressed ? kOrange : kBlueGrey900,
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: TimeDisplay(),
),
),
),
),
);
}
Widget TimeDisplay() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(
duration.inHours.remainder(60),
);
final minutes = twoDigits(
duration.inMinutes.remainder(60),
);
final seconds = twoDigits(
duration.inSeconds.remainder(60),
);
return Text(
'$hours:$minutes:$seconds',
style: TextStyle(fontSize: 50),
);
}
}
When I transfer the code over, I'm running into trouble because I can't use setState in the change notifier and I'm unsure how to translate the code to get it to work.
so far, by moving the individual variables over as well as the widget TimDisplay, I'm able to get the timer to display correctly from the change notifier but am not sure how to get it to work from the change notifier.
here is where I am now:
type hereclass ChessClock3 extends StatefulWidget {
const ChessClock3({Key? key}) : super(key: key);
#override
State<ChessClock3> createState() => _ChessClock3State();
}
class _ChessClock3State extends State<ChessClock3> {
#override
void initState() {
super.initState();
Reset();
}
void Reset() {
setState(() => duration = countdownDuration);
}
void AddTime() {
final minusSeconds = -1;
setState(() {
final seconds = duration.inSeconds + minusSeconds;
if (seconds < 0) {
timer?.cancel();
} else {
duration = Duration(seconds: seconds);
}
});
}
void StartTimer() {
timer = Timer.periodic(
Duration(seconds: 1),
(_) => AddTime(),
);
}
void StopTimer() {
setState(() => timer?.cancel());
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InkWell(
onTap: () {
setState(() {
context.read<CountdownTimers>().BeenPressed();
});
context.read<CountdownTimers>().beenPressed
? StartTimer()
: StopTimer();
},
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: context.watch<CountdownTimers>().beenPressed
? kKillTeamOrange
: kBlueGrey900,
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: context.read<CountdownTimers>().TimeDisplay(),
),
),
),
),
);
}
}
class CountdownTimers with ChangeNotifier {
Duration _countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
Duration _duration = Duration();
Timer? timer;
bool _beenPressed = false;
Duration get countdownDuration => _countdownDuration;
Duration get duration => _duration;
bool get beenPressed => _beenPressed;
void BeenPressed() {
_beenPressed = !_beenPressed;
}
Widget TimeDisplay() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(
_duration.inHours.remainder(60),
);
final minutes = twoDigits(
_duration.inMinutes.remainder(60),
);
final seconds = twoDigits(
_duration.inSeconds.remainder(60),
);
return Text(
'$hours:$minutes:$seconds',
style: TextStyle(fontSize: 50),
);
}
}
If anyone can show me how to translate the code over It would be very much appreciated.
thanks so much!
I would use Flutter Riverpod instead like the code below. In Riverpod it's not recommended to use Change Notifier. Even in complexe application. To change the state of Change Notifier you must call notifyListeners.
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countDownControllerProvider = StateNotifierProvider.family
.autoDispose<CountdownController, Duration, Duration>((ref, initialDuration) {
return CountdownController(initialDuration);
});
class CountdownController extends StateNotifier<Duration> {
Timer? timer;
final Duration initialDuration;
CountdownController(this.initialDuration) : super(initialDuration) {
startTimer();
}
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (state == Duration.zero) {
timer.cancel();
} else {
if (mounted) {
state = state - const Duration(seconds: 1);
} else {
timer.cancel();
}
}
});
}
void stopTimer() {
timer?.cancel();
}
void resetTimer({required Duration initialDuration}) {
stopTimer();
state = initialDuration;
startTimer();
}
void addTime({required Duration duration}) {
state = state + duration;
}
void subtractTime({required Duration duration}) {
state = state - duration;
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
}
Then in the widget
Consumer(builder: (context, ref, _) {
return Text(
ref
.watch(countDownControllerProvider(
const Duration(
days: 25,
hours: 15,
minutes: 36,
seconds: 45)))
.formatDuration(),
style: context.theme.textTheme.bodyText1!
.copyWith(color: Colors.white),
);
})
And finally, don't hesitate to put your conversion logic Duration to String, into a extension
extension DurationExtensions on Duration {
String formatDuration() {
int seconds = inSeconds;
final days = seconds~/Duration.secondsPerDay;
seconds -= days*Duration.secondsPerDay;
final hours = seconds~/Duration.secondsPerHour;
seconds -= hours*Duration.secondsPerHour;
final minutes = seconds~/Duration.secondsPerMinute;
seconds -= minutes*Duration.secondsPerMinute;
final List<String> tokens = [];
if (days != 0) {
tokens.add('${days}d');
}
if (tokens.isNotEmpty || hours != 0){
tokens.add('${hours}h');
}
if (tokens.isNotEmpty || minutes != 0) {
tokens.add('${minutes}min');
}
if(tokens.isNotEmpty || seconds != 0) {
tokens.add('${seconds}s');
}
return tokens.join('');
}
}
The trick you are looking for is the function notifyListeners() of a ChangeNotifier. If you used context.watch() inside your widget to watch the ChangeNotifier, you will be updated and the widget is rebuild if necessary.
A short example might look like this
ConsumingWidget.dart
#override
Widget build(BuildContext context) {
var timeProvider = context.watch<TimeProvider>();
int counter = timeProvider.counter;
return Text("${counter} seconds have passed");
}
In this case, you would need to provide an instance of TimeProvider above your main widget in the widget tree via ChangeNotifierProvider.
You can then let the widget rebuild (if .counter changed) via notifyListeners()
ParentWidget.dart
#override
Widget build(BuildContext context) {
ChangeNotifierProvider(
create: (_) => TimeProvider(),
child: ConsumingWidget()
);
}
Provider.dart
class TimeProvider extends ChangeNotifier {
Timer? timer;
final Duration initialDuration;
int counter = 0;
CountdownController(this.initialDuration) : super(initialDuration) {
startTimer();
}
void startTimer() {
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
counter += 1; // Change some data based on the timer. Just an example here
notifyListeners(); // Then update listeners!
});
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
This is the built-in solution of the Provider package. Starting from this basic pattern, you can then look into more sophisticated state management solutions like Riverpod, Bloc etc.
I have to make a countdown timer for which i have written this code. but it is not updating it's UI even after using setState.
I want to refresh remaining time every second.
class CustomTimer extends StatefulWidget {
final DateTime endtime;
const CustomTimer({Key? key, required this.endtime}) : super(key: key);
#override
State<CustomTimer> createState() => _CustomTimerState();
}
class _CustomTimerState extends State<CustomTimer> {
Timer? timer;
Duration remainTime = Duration();
newTime() {
// print('hi');
setState(() {
final seconds = remainTime.inSeconds - 1;
remainTime = Duration(seconds: seconds);
});
}
#override
void initState() {
remainTime = widget.endtime.difference(DateTime.now());
timer = Timer.periodic(const Duration(seconds: 1), (_) => newTime());
super.initState();
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text(remainTime.toString().split('.')[0].split('')[0] == '-'
? "00:00:00"
: remainTime.toString().split('.')[0]),
);
}
}
I have created a counter app in flutter. I pressed the increment button for 10 times. It was showing the counter value was 10 on the screen. But whenever i exit the app and reopen it shows the counter value is 0. Why is this happening? I want to save the counter value each time I change it. Can somebody solve my problem???
You can save the on shared_preferences
class ParentWidget extends StatefulWidget {
const ParentWidget({Key? key}) : super(key: key);
#override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _isMuted = false;
int counter = 0;
SharedPreferences? preferences;
Future<void> initStorage() async {
preferences = await SharedPreferences.getInstance();
// init 1st time 0
int? savedData = preferences?.getInt("counter");
if (savedData == null) {
await preferences!.setInt("counter", counter);
} else {
counter = savedData;
}
setState(() {});
}
#override
void initState() {
super.initState();
initStorage();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () async {
preferences?.setInt("counter", counter += 1);
setState(() {});
},
),
body: Column(
children: [
Text("${preferences?.getInt("counter")}"),
More about shared_preferences. Also you can check Hive and others local data storage.
I am building a chess clock in flutter. when i change clocks time duration in settings page i want to ask the user for confirmation using alert dialog. I want the alert dialog to only appear in homepage right now it appears immediately after I change duration in settings page.
I am using provider to manage state when I change duration I change a bool value to true. then in the home page I check if the bool value is true then run the alert dialog code. I tried putting dialog code in future delay.Zero but it did not work.
right now home page rebuilds immediatly after i change settings and alert dialog pops up.
my code:
// home page code
class HomePage extends StatefulWidget {
const HomePage({
Key? key,
}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with TickerProviderStateMixin, WidgetsBindingObserver {
// code for initstate and logic
late AnimationController _whiteControl;
late AnimationController _blackControl;
int whiteTime = 1;
int blackTime = 1;
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
_whiteControl = AnimationController(
vsync: this,
duration: Duration(seconds: whiteTime * 60),
);
_blackControl = AnimationController(
vsync: this,
duration: Duration(seconds: blackTime * 60),
);
#override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
_whiteControl.dispose();
_blackControl.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
var gameState = Provider.of<GameState>(context, listen: false);
final ChoiceDialog resetChoice = ChoiceDialog(
buttonOkOnPressed: () {
_blackControl.reset();
_whiteControl.reset();
gameState.isgameChanged = false;
Navigator.pop(context);
},
buttonCancelOnPressed: () {
gameState.isgameChanged = false;
Navigator.pop(context);
},
);
if (gameState.isGameStarted && gameState.isgameChanged) {
Future.delayed(Duration.zero, () {
print('👉 Game Changed ');
resetChoice.show(context);
});
}
print('🏠 Rebuilding Home Page ---');
return Scaffold();
//reset of homepage code
Settings Page Code:
class SettingsPage extends StatelessWidget {
const SettingsPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: gamesList.length,
itemBuilder: (context, index) {
return Consumer<GameState>(
builder: (context, _gameState, child) {
return GestureDetector(
onTap: () {
_gameState.changeGame(gamesList[index]);
},
child: Container(),
);
},
);
}),
)
],
),
);
}
}
Provider State Logic:
class Game {
final String name;
final int whiteTime;
final int blackTime;
Game({
required this.name,
required this.whiteTime,
required this.blackTime,
});
}
class GameState with ChangeNotifier {
bool turnWhite = false;
bool turnBlack = false;
bool isgameChanged = false;
bool isClocksFinished = false;
bool isPaused = false;
bool isGameStarted = false;
bool resetGame = false;
int white = 0;
int black = 0;
Game _currentGame = gamesList[0];
Game get currentGame => _currentGame;
bool get isWhiteTurn => turnWhite;
void resetGameClocks() {
resetGame = true;
notifyListeners();
}
void setTurnWhite() {
turnWhite = true;
turnBlack = false;
isGameStarted = true;
notifyListeners();
}
void setTurnBlack() {
turnWhite = false;
turnBlack = true;
isGameStarted = true;
notifyListeners();
}
void changeGame(Game game) {
_currentGame = game;
isgameChanged = true;
white = game.whiteTime;
black = game.blackTime;
notifyListeners();
}
}
project repository: https://github.dev/tonydavidx/chess-clock-app/tree/dev
I have checked your code you did a blunder in a very initial point/starting point of the Homepage screen kindly avoid this
never ever initialize or put your code before returning in build function of widgets if you really need some logic or code to be executed in the beginning then put it inside an initstate() or inside your provider constructor and handle it properly.
this is the root cause of the problem which you are facing.
this my code
Container(
child: Column(
children: jobProvider.jobs
.map(
(job) => JobTile(job),
)
.toList(),
),
)
how to load this data delay for 3 seconds ? here I display the listview data, before displaying it I want to display, the
return Container(
child: ProfileShimmer(),
);
Should be as easy as this:
import 'package:flutter/material.dart';
class PageWithDelayedView extends StatefulWidget {
const PageWithDelayedView({Key? key}) : super(key: key);
#override
_PageWithDelayedViewState createState() => _PageWithDelayedViewState();
}
class _PageWithDelayedViewState extends State<PageWithDelayedView> {
bool _initialized = false;
#override
void initState() {
super.initState();
// Schedule function call after the widget is ready to display
WidgetsBinding.instance?.addPostFrameCallback((_) {
_initialize();
});
}
void _initialize() {
Future<void>.delayed(const Duration(seconds: 3), () {
if (mounted) { // Check that the widget is still mounted
setState(() {
_initialized = true;
});
}
});
}
#override
Widget build(BuildContext context) {
if (!_initialized) {
return Text('Hold on a bit');
}
return Text('Yay, I\'m ready!');
}
}