Flutter Countdown Timer for Chess App ( stuck in the logic) - flutter

So from past few day I have been trying to get around this logical loop, and have tried everything I could and then finally decided to post it here (hence made my debut here). I am stuck, all the help is appreciated. Thanks !
Logic: In the app there are 2 container which displays White's and black's time, which is selected from a given five options -> 5,10,15,30,60, which then update's the time to display in those containers, used a provider package for this and everything else.
Now I have also added a Raised button named 'switch', which when pressed was supposed to:
Start's the white's countdown timer
Then if pressed again should stops white's timer and then start Black's countdown timer
Then if pressed again should stop the black's timer and then starts white's timer.
And repeat till the timer runs out and one out of the two is declared a winner.
Problem:
So with what I coded so far, when 'switch' is pressed it starts white's timer and if pressed again stops white's timer but it doesn't begins black's timer. I know its because of the way I have framed the if() conditions and also because I don't know how to stop the timer from outside. What I have done is to use a - bool checkTimerW and checkTimerB for each white and black, which I check in the if() condition to cancel the timer is based on it.
Code:
Provider -
import 'dart:async';
import 'package:flutter/foundation.dart';
class SettingsProvider extends ChangeNotifier {
int valueW = 0;
int valueB = 0;
// * with these booleans we will stop the timer.
bool checkTimerW = true;
bool checkTimerB = true;
String timeToDisplayW = ""; // for white
String timeToDisplayB = ""; // for black
bool switchT = false;
// this is called in the settings Modal Bottom Sheet
void changeValue(int valW, int valB) {
//? Changing the value in seconds
valueW = valW * 60;
valueB = valB * 60;
print(valueW);
timeToDisplayW = valueW.toString();
timeToDisplayB = valueB.toString();
notifyListeners();
}
void reset() {
started = true;
stopped = true;
checkTimerW = false;
checkTimerB = false;
notifyListeners();
}
void toggleSwitch() {
if (switchT == false) {
switchT = true;
print('true');
} else if (switchT == true) {
switchT = false;
print('false');
}
}
void switchTimer() {
if (switchT == false) {
// Starts white's timer
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueW <= 1 || checkTimerW == false) {
t.cancel();
checkTimerW = true;
// TODO : Black Won
notifyListeners();
} else {
valueW = valueW - 1;
notifyListeners();
}
timeToDisplayW = valueW.toString();
notifyListeners();
},
);
// stops black's timer
checkTimerB = false;
toggleSwitch();
notifyListeners();
} else {
// Starts black's timer
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueB <= 1 || checkTimerB == false) {
t.cancel();
checkTimerB = true;
// TODO : White won
notifyListeners();
} else {
valueB = valueB - 1;
notifyListeners();
}
timeToDisplayB = valueB.toString();
notifyListeners();
},
);
// stops white's timer
checkTimerW = false;
toggleSwitch();
notifyListeners();
}
}
}
Main.dart -
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'controller/countdown_controller.dart';
import 'widgets/blackButton.dart';
import 'widgets/bottom_sheet_design.dart';
import 'widgets/whiteButton.dart';
import 'providers/settings.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return ChangeNotifierProvider(
create: (ctx) => SettingsProvider(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.amber,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void settings(BuildContext ctx) {
showModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
context: ctx,
builder: (_) => BottomSheetDesign(),
);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[350],
body: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 1,
child: Transform.rotate(
angle: pi / 1,
child: GestureDetector(
onTap: () {
Provider.of<SettingsProvider>(context, listen: false)
.switchTimer();
},
child: Container(
width: 80.0,
height: 500,
child: Center(
child: Transform.rotate(
angle: pi / 2,
child: Text('Switch',
style: Theme.of(context).textTheme.bodyText2),
),
),
decoration: BoxDecoration(
color: Colors.blueGrey,
),
),
),
),
),
VerticalDivider(),
Expanded(
flex: 4,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
// container that displays black's timer
BlackButton(),
Expanded(
child: Transform.rotate(
angle: pi / 2,
child: RaisedButton(
onPressed: () {
settings(context);
},
color: Colors.blue[300],
child: Text('Settings'),
),
),
),
],
),
SizedBox(
height: 20,
),
Row(
children: <Widget>[
// container that displays white's timer
WhiteButton(),
Expanded(
child: Transform.rotate(
angle: pi / 2,
child: RaisedButton(
onPressed: () {
Provider.of<SettingsProvider>(context, listen: false).reset();
},
color: Colors.red[600],
child: Text('Reset'),
),
),
),
],
),
],
),
),
],
),
),
);
}
}

I have coded this in the past. It might help you.
class DoubleTimer extends StatefulWidget {
#override
_DoubleTimerState createState() => _DoubleTimerState();
}
class _DoubleTimerState extends State<DoubleTimer> {
int timeToGoA = 50000;
int timeToGoB = 50000;
int state = 0; //0: waiting, 1: counting A, 2: counting B
DateTime timeStamp;
_DoubleTimerState() {
print("init");
}
#override
Widget build(BuildContext context) {
print(
"${DateTime.now().compareTo(DateTime.now().add(Duration(seconds: 1)))}");
return Row(
children: <Widget>[
if (state == 1)
ToTime(timeStamp.add(Duration(milliseconds: timeToGoA))),
FlatButton(
onPressed: () {
setState(() {
switch (state) {
case 0:
state = 1;
timeStamp = DateTime.now();
print("Running A");
break;
case 1:
state = -1;
timeToGoA -=
DateTime.now().difference(timeStamp).inMilliseconds;
timeStamp = DateTime.now();
print("A: $timeToGoA\nRunning B");
break;
case -1:
state = 1;
timeToGoB -=
DateTime.now().difference(timeStamp).inMilliseconds;
timeStamp = DateTime.now();
print("B: $timeToGoB\nRunning A");
break;
}
});
},
child: Text("switch"),
),
if (state == -1)
ToTime(timeStamp.add(Duration(milliseconds: timeToGoB))),
],
);
}
}
class ToTime extends StatelessWidget {
final DateTime timeStamp;
const ToTime(this.timeStamp, {Key key}) : super(key: key);
static final Map<String, int> _times = <String, int>{
'y': -const Duration(days: 365).inMilliseconds,
'm': -const Duration(days: 30).inMilliseconds,
'w': -const Duration(days: 7).inMilliseconds,
'd': -const Duration(days: 1).inMilliseconds,
'h': -const Duration(hours: 1).inMilliseconds,
'\'': -const Duration(minutes: 1).inMilliseconds,
'"': -const Duration(seconds: 1).inMilliseconds,
"ms": -1,
};
Stream<String> get relativeStream async* {
while (true) {
int duration = DateTime.now().difference(timeStamp).inMilliseconds;
String res = '';
int level = 0;
int levelSize;
for (MapEntry<String, int> time in _times.entries) {
int timeDelta = (duration / time.value).floor();
if (timeDelta > 0) {
levelSize = time.value;
res += '$timeDelta${time.key} ';
duration -= time.value * timeDelta;
level++;
}
if (level == 2) {
break;
}
}
levelSize ??= _times.values.reduce(min);
if (level > 0 && level < 2) {
List<int> _tempList =
_times.values.where((element) => (element < levelSize)).toList();
if (_tempList.isNotEmpty) levelSize = _tempList.reduce(max);
}
if (res.isEmpty) {
yield 'now';
} else {
res.substring(0, res.length - 2);
yield res;
}
// print('levelsize $levelSize sleep ${levelSize - duration}ms');
await Future.delayed(Duration(milliseconds: levelSize - duration));
}
}
#override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: relativeStream,
builder: (context, snapshot) {
return Text(snapshot.data ?? '??');
});
}
}

I coded it withoud provider (Using only ValueNotifier) just to show you the logic
enum Player{White, Black}
class MyTimer extends ValueNotifier<int>{
Player _turn; //White starts
int _minutes;
int _whiteTime;
int _blackTime;
MyTimer(int time) :
_minutes = time * 60,
_whiteTime = time * 60,
_blackTime = time * 60,
_turn = Player.White, //White starts
super(time * 60 * 2);
bool get _isWhiteTurn => Player.White == _turn;
String get timeLeft{
if(value != 0){
//int time = _isWhiteTurn ? _whiteTime : _blackTime; //use this instead of playerTime if you want to display the time in seconds
Duration left = Duration(seconds: _isWhiteTurn ? _whiteTime : _blackTime);
String playerTime = left.toString();
playerTime = playerTime.substring(0, playerTime.lastIndexOf('.'));
return '${describeEnum(_turn)} turn time left : $playerTime';
}
else{
return '${describeEnum(_turn)} wins!'; //We have a winner
}
}
void switchPlayer() => _turn = _isWhiteTurn ? Player.Black : Player.White;
void reset([int time]){
if(time != null) _minutes = time * 60; //if you want to start with a different value
_turn = Player.White; //White starts
_whiteTime = _minutes; //reset time
_blackTime = _minutes; //reset time
value = 2*_minutes; //reset time
//twice as long because it counts the whole time of the match (the time of the 2 players)
}
void start(){
_initilizeTimer();
}
void _initilizeTimer(){
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if(_whiteTime == 0 || _blackTime == 0){
t.cancel();
switchPlayer(); //the time of one player ends, so it switch to the winner player
value = 0; //end the game
}
else{
_isWhiteTurn ? --_whiteTime : --_blackTime;
--value;
}
},
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final MyTimer clock = MyTimer(1);
#override
void initState(){
super.initState();
clock.start();
}
#override
void dispose(){
clock.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.grey[350],
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ValueListenableBuilder<int>(
valueListenable: clock,
builder: (context, unit, _) =>
Text(clock.timeLeft ,style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500))
),
RaisedButton(
child: Text('Switch'),
onPressed: () => clock.switchPlayer(),
)
],
),
)
),
);
}
}
The idea is the same but what I want to show you is that you can use only one timer to do the whole logic and with some enum value (White and Black) change between both minutes.
The button change the turn of the player (switchPlayer method) and inside the timer you see that depending the turn of the player it reduces its time _isWhiteTurn ? --_whiteTime : --_blackTime;. As a ValueNotifier it updates only when value changes, but you can use your Provider with ChangeNotifier and update when you want (and it's better, because when changing player in my example I still have to wait for the second to end so the timer updates the text).
You can try change something like this with an enum to simplify the timer logic
bool get _isWhiteTurn => Player.White == _turn;
void startMatch() {
Timer.periodic(
Duration(seconds: 1),
(Timer t) {
if (valueW == 0 || valueB == 0) {
t.cancel();
if(valueW == 0) checkTimerB = true;
else checkTimerW = true
//it won the one whose time didn't end
} else {
_isWhiteTurn ? --valueW : --valueB;
}
timeToDisplayW = valueW.toString();
timeToDisplayB = valueB.toString();
//only one of them will change
notifyListeners();
},
);
}
void switchTimer(){
_turn = _isWhiteTurn ? Player.Black : Player.White;
notifyListeners();
}
That way you have only one timer the whole match that will cancel when one of the timer gets to 0 (or if someone loese, but thats other logic in some other Provider I guess)
UPDATE
You can change the timeLeft getter to something like this
String get timeLeft{
if(value != 0){
//int time = _isWhiteTurn ? _whiteTime : _blackTime; //use this instead of playerTime if you want to display the time in seconds
Duration white = Duration(seconds: _whiteTime);
Duration black = Duration(seconds: _blackTime);
String whiteTime = white.toString();
String blackTime = black.toString();
whiteTime = whiteTime.substring(0, whiteTime.lastIndexOf('.'));
blackTime = blackTime.substring(0, blackTime.lastIndexOf('.'));
return '''
${describeEnum(Player.White)} time left : $whiteTime
${describeEnum(Player.Black)} time left : $blackTime
''';
}
else{
return '${describeEnum(_turn)} wins!'; //We have a winner
}
}
That way it will return a String with both times and only the timer of the player in turn will change each second. But as I saig try this logic with ChangeNotifierProvider and it should work too, and you can consume it in differents parts of your widget tree

Related

Flutter - Handling Life Cycle

Thanks for reading my question.
I wonder why
Unhandled Exception: setState() called after
dispose():_PomodoroState#42b6e(lifecycle state: defunct, not mounted)
is occured when I leave that page without pausing my timer function!
and also I want to know if it's fine to leave it like this.
But I think it's not good idea so I want some advices from you guys
This is my timer code.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:cat_app/provider/counts.dart';
class Pomodoro extends StatefulWidget {
#override
_PomodoroState createState() => _PomodoroState();
}
class _PomodoroState extends State<Pomodoro> {
double coinCount = 0;
Stopwatch watch = Stopwatch();
late Timer timer;
bool startStop = true;
String elapsedTime = '';
updateTime(Timer timer) {
if (watch.isRunning) {
setState(() {
print("startstop Inside=$startStop");
elapsedTime = transformMilliSeconds(watch.elapsedMilliseconds);
});
if (coinCount == 360000) {
context.read<Counts>().add(1);
coinCount = 0;
} else {
coinCount = coinCount + 10;
}
context.read<Times>().timeAdd(100);
}
}
restartTimer() {
updateTime(timer).cancel();
setState(() {
startStop = true;
watch.stop();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(elapsedTime,
style: TextStyle(fontFamily: 'Kitto', fontSize: 25.0)),
SizedBox(height: 20.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => startOrStop(),
icon: Image.asset('assets/button.png'),
iconSize: 50,
)
],
)
],
),
),
);
}
startOrStop() {
if (startStop) {
startWatch();
} else {
stopWatch();
}
}
startWatch() {
setState(() {
startStop = false;
watch.start();
timer = Timer.periodic(Duration(milliseconds: 100), updateTime);
});
}
stopWatch() {
setState(() {
startStop = true;
watch.stop();
setTime();
});
}
setTime() {
var timeSoFar = watch.elapsedMilliseconds;
setState(() {
elapsedTime = transformMilliSeconds(timeSoFar);
});
}
transformMilliSeconds(int milliseconds) {
int hundreds = (milliseconds / 10).truncate();
int seconds = (hundreds / 100).truncate();
int minutes = (seconds / 60).truncate();
int hours = (minutes / 60).truncate();
String hoursStr = (hours % 60).toString().padLeft(2, '0');
String minutesStr = (minutes % 60).toString().padLeft(2, '0');
String secondsStr = (seconds % 60).toString().padLeft(2, '0');
return "$hoursStr:$minutesStr:$secondsStr";
}
}
Can you tell me what is wrong?? I want to handle it!
dispose() is used to execute code when the screen is disposed. Equal to onDestroy() of Android.
Example:
#override
void dispose() {
anyController?.dispose();
timer.cancel();
super.dispose();
}
Add this in your Code

Flutter - Life Cycle

Thanks for reading my questions
I wonder again why the error shown below is printed when I navigate to timer page.
I want to know how can I fix it.
The following LateError was thrown while finalizing the widget tree:
LateInitializationError: Field 'timer' has not been initialized.
This is my timer code!
class Pomodoro extends StatefulWidget {
#override
_PomodoroState createState() => _PomodoroState();
}
class _PomodoroState extends State<Pomodoro> {
double coinCount = 0;
Stopwatch watch = Stopwatch();
late Timer timer;
bool startStop = true;
String elapsedTime = '';
#override
void dispose() {
timer.cancel();
super.dispose();
}
updateTime(Timer timer) {
if (watch.isRunning) {
setState(() {
elapsedTime = transformMilliSeconds(watch.elapsedMilliseconds);
});
if (coinCount == 360000) {
context.read<Counts>().add(1);
coinCount = 0;
} else {
coinCount = coinCount + 10;
}
context.read<Times>().timeAdd(100);
}
}
restartTimer() {
updateTime(timer).cancel();
setState(() {
startStop = true;
watch.stop();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Container(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(elapsedTime,
style: TextStyle(fontFamily: 'Kitto', fontSize: 25.0)),
SizedBox(height: 20.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => startOrStop(),
icon: Image.asset('assets/button.png'),
iconSize: 50,
)
],
)
],
),
),
);
}
startOrStop() {
if (startStop) {
startWatch();
} else {
stopWatch();
}
}
startWatch() {
setState(() {
startStop = false;
watch.start();
timer = Timer.periodic(Duration(milliseconds: 100), updateTime);
});
}
stopWatch() {
setState(() {
startStop = true;
watch.stop();
setTime();
});
}
setTime() {
var timeSoFar = watch.elapsedMilliseconds;
setState(() {
elapsedTime = transformMilliSeconds(timeSoFar);
});
}
transformMilliSeconds(int milliseconds) {
int hundreds = (milliseconds / 10).truncate();
int seconds = (hundreds / 100).truncate();
int minutes = (seconds / 60).truncate();
int hours = (minutes / 60).truncate();
String hoursStr = (hours % 60).toString().padLeft(2, '0');
String minutesStr = (minutes % 60).toString().padLeft(2, '0');
String secondsStr = (seconds % 60).toString().padLeft(2, '0');
return "$hoursStr:$minutesStr:$secondsStr";
}
}
If you know how to fix it, I hope you answered to my question.
I would be grateful for your answer.
And Have a nice day!
I don't think you should use late in that case. Adding late to field means that the field will be initialized when you use it for the first time.
use late when you strongly convinced that first time you use late field it will be initialized. And always remember that using late makes you code less safe and adds possibility of runtime errors.
You don't want a late variable, you want a nullable one. If you need to check if something is initialized, you should be using a nullable variable instead and your code is already set up to check for null.
Just change
late Timer timer;
to
Timer? timer;

How to keep a widget expanded based on user's taps?

I want to create a custom widget in which when I click a button,
it expands to a widget something like this [ - 0 + ]
if the user taps '-' or '+', the button should remain expanded and should decrement or increment the value for the respective taps. The current value should be reflected.
if the user does not tap anything after expansion, the widget should collapse to its previous state after some x duration (say 2 seconds) from the previous tap.
Here's is my code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
bool isExpanded = false;
int quantity = 0;
int duration = 3;
#override
Widget build(BuildContext context) {
return !isExpanded
? ElevatedButton(
child: Text('Expand (current: $quantity)'),
onPressed: expansionHandler)
: Card(
elevation: 5.0,
child: Container(
color: Colors.blue,
width: 200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: decrementValue),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add),
onPressed: incrementValue),
])));
}
void incrementValue() {
setState(() {
duration += 3;
quantity++;
});
}
void decrementValue() {
setState(() {
duration += 3;
quantity--;
quantity = quantity < 0 ? 0 : quantity;
});
}
void expansionHandler() async {
setState(() {
isExpanded = true;
});
await Future.delayed(Duration(seconds: duration+3)/*not sure if this can work or not*/, () {
setState(() {
isExpanded = false;
duration = 2;
});
});
}
}
DartPad Link: https://dartpad.dev/?id=c22ea14c649cc2858f6d09270a62a0d5&null_safety=true
Test this Widget. I comment down the duration changes for fast testing.
I think the problem with expansionHandler with Future that it is activated when you click the 1st time. On second and next click, it generates a new instance instead of replacing the current one.
According to you question, we to check the current state of Timer, where 'Future.delay' can't handle in this case.
Hope this is what you need.
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
bool isExpanded = false;
int quantity = 0;
int duration = 3;
Timer? timer;
setUpTimer() async {
print("startTimer");
setState(() {
isExpanded = true;
});
timer = await Timer.periodic(Duration(seconds: duration), (timer) {
setState(() {
isExpanded = false;
duration = 2;
});
timer.cancel();
print("done with time $duration");
});
}
_reset() {
if (timer != null && timer!.isActive) timer!.cancel();
setUpTimer();
}
#override
void dispose() {
if (timer != null && timer!.isActive) timer!.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return !isExpanded
? ElevatedButton(
child: Text('Expand (current: $quantity)'),
onPressed: _reset,
)
: Card(
elevation: 5.0,
child: Container(
color: Colors.blue,
width: 200,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: decrementValue),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add),
onPressed: incrementValue,
),
],
),
),
);
}
void incrementValue() {
setState(() {
// duration += 3;
///for fast testing
quantity++;
});
_reset();
}
void decrementValue() {
setState(() {
// duration += 3;
quantity--;
quantity = quantity < 0 ? 0 : quantity;
});
}
}

Flutter: Continuously running stopwatch when switching screens

I have a stopwatch widget which is called from the Detailed Activity page on one of my screens that starts when the page loads. How am I able to keep it continuously running in the background so that when I navigate from Detailed Activity to the Home page, I can display that same stopwatch's time?
At the moment, the Home page displays the last saved time (into the variable). Instead I would like this to continue incrementing each second and vice versa switching between the two screens. I've tried adding the startWatch() and initState() method in my Home page, but don't know how to pass the stopwatch value while navigating to the Detailed Activity page
To clarify this more, my thinking was on the Detailed Activity page it is at e.g. 00:15:00 -> pass it into Home page and call initState() to automatically start the timer again. Would this work?
Timer.dart
class NewStopWatch extends StatefulWidget {
#override
_NewStopWatchState createState() => new _NewStopWatchState();
}
class _NewStopWatchState extends State<NewStopWatch> {
static _NewStopWatchState stopwatch;
Stopwatch watch = new Stopwatch();
Timer timer;
bool startStop = true;
static String elapsedTime = '';
String duration;
updateTime(Timer timer) {
if (watch.isRunning) {
setState(() {
elapsedTime = transformMilliSeconds(watch.elapsedMilliseconds);
User.getCurrentUser().getCurrentActivity().setElapsedTime(elapsedTime);
});
}
}
#override
Widget build(BuildContext context) {
return new Container(
padding: EdgeInsets.all(20.0),
child: new Column(
children: <Widget>[
new Text(elapsedTime, style: new TextStyle(fontSize: 25.0)),
SizedBox(height: 20.0),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
heroTag: "btn1",
backgroundColor: Colors.red,
onPressed: () => startOrStop(),
child: Icon(Icons.pause)),
SizedBox(width: 20.0),
FloatingActionButton(
heroTag: "btn2",
backgroundColor: Colors.green,
onPressed: () => completeActivity(),
child: Icon(Icons.check)),
],
)
],
));
}
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => startWatch());
}
startOrStop() {
if(startStop) {
startWatch();
} else {
stopWatch();
}
}
startWatch() {
setState(() {
startStop = false;
watch.start();
timer = Timer.periodic(Duration(milliseconds: 100), updateTime);
});
}
stopWatch() {
setState(() {
startStop = true;
watch.stop();
setTime();
});
}
resetWatch() {
watch.reset();
setTime();
}
setTime() {
var timeSoFar = watch.elapsedMilliseconds;
setState(() {
elapsedTime = transformMilliSeconds(timeSoFar);
});
}
completeActivity() { //do I call activity.stopActivity()?
return showDialog(
context: context,
builder: (context) => new AlertDialog(
title: new Text('Complete Activity?',
style: new TextStyle(color: Colors.black, fontSize: 20.0)),
actions: <Widget>[
new FlatButton(
onPressed: () {
duration = elapsedTime;
print("Current activitiy: ${User.getCurrentUser().getCurrentActivity()}");
// User.getCurrentUser().completeActivity();
User.getCurrentUser().addPastActivity(User.getCurrentUser().getCurrentActivity());
User.getCurrentUser().getCurrentActivity().setStatus(ActivityStatus.completed);
User.getCurrentUser().setCurrentActivity(null);
Navigator.push(context, MaterialPageRoute(builder: (context) => FrontPage()));
// Navigator.popUntil(context, ModalRoute.withName("/"),);
},
child:
new Text('Yes', style: new TextStyle(fontSize: 18.0)),
),
new FlatButton(
onPressed: () => Navigator.pop(context), // this line dismisses the dialog
child: new Text('No', style: new TextStyle(fontSize: 18.0)),
)
],
),
) ??
false;
}
transformMilliSeconds(int milliseconds) {
int hundreds = (milliseconds / 10).truncate();
int seconds = (hundreds / 100).truncate();
int minutes = (seconds / 60).truncate();
int hours = (minutes / 60).truncate();
String hoursStr = (hours % 60).toString().padLeft(2, '0');
String minutesStr = (minutes % 60).toString().padLeft(2, '0');
String secondsStr = (seconds % 60).toString().padLeft(2, '0');
return "$hoursStr:$minutesStr:$secondsStr";
}
static String getElapsedTime() {
return elapsedTime;
}
}
It is not 100% clear for me if this solution will work since you posted a lot of code but it is missing the part where you actually display the NewStopWatch but I hope this solution helps:
Pass the Stopwatch and the timer as arguments in the init method of your NewStopWath class and have them passed from your main page - the page where you want them displayed as well... i suppose it is the FrontPage.
Like so:
class FrontPage extends StatelessWidget {
Stopwatch watch = Stopwatch();
Timer timer = Timer.periodic(Duration(milliseconds: 100), updateTime);
//... code
//.. somewhere a NewStopWatch is created
return NewStopWatch(watch, timer);
}
//... code
class NewStopWatch extends StatefulWidget {
Stopwatch watch;
Timer timer;
NewStopWatch(this.watch, this.timer);
#override
_NewStopWatchState createState() => new _NewStopWatchState();
}
class _NewStopWatchState extends State<NewStopWatch> {
static _NewStopWatchState stopwatch;
bool startStop = true;
static String elapsedTime = '';
String duration;
Stopwatch get watch => widget.watch;
Timer get timer => widget.timer;
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => startWatch());
}
Be sure to check if you should remove timer = Timer.periodic(Duration(milliseconds: 100), updateTime); from startWatch()
maybe you don't need to have it passed as an argument

How to play videos sequentialy on video_player without delay?

I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyHomePage(),
));
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<VideoPlayerController> _controllers = [];
VoidCallback listener;
bool _isPlaying = false;
int _current = 0;
#override
void initState() {
super.initState();
// Add some sample videos
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
this.tick();
// Try refreshing by brute force (this isn't going too well)
new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
int delta = 99999999;
if(_controllers[_current].value != null) {
delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
}
print("Tick " + delta.toString());
if(delta < 500) {
_current += 1;
this.tick();
}
});
}
void tick() async {
print("Current: " + _current.toString());
await _controllers[_current].initialize();
await _controllers[_current].play();
print("Ready");
setState((){
_current = _current;
});
}
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: _controllers[_current].value.aspectRatio,
// Use the VideoPlayer widget to display the video
child: VideoPlayer(_controllers[_current]),
);
}
}
What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.
Initializing a network VideoPlayerController may take some time to finish. You can initialize the controller of the next video while playing the current. This will take more memory but I don't think it will create huge problems if you prebuffer only one or two videos. Then when the next or previous buttons get pressed, video will be ready to play.
Here is my workaround. It's functional, it prebuffers previous and next videos, skips to the next video when finishes, shows the current position and buffer, pauses and plays on long press.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
main() {
runApp(MaterialApp(
home: VideoPlayerDemo(),
));
}
class VideoPlayerDemo extends StatefulWidget {
#override
_VideoPlayerDemoState createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool _lock = true;
Map<String, VideoPlayerController> _controllers = {};
Map<int, VoidCallback> _listeners = {};
Set<String> _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
};
#override
void initState() {
super.initState();
if (_urls.length > 0) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => _lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)];
}
Future<void> _initController(int index) async {
var controller = VideoPlayerController.network(_urls.elementAt(index));
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]);
_controller(index).pause();
_controller(index).seekTo(Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (_lock || index == 0) {
return;
}
_lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
_lock = false;
} else {
_initController(index - 1).whenComplete(() => _lock = false);
}
}
void _nextVideo() async {
if (_lock || index == _urls.length - 1) {
return;
}
_lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
_lock = false;
} else {
_initController(index + 1).whenComplete(() => _lock = false);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Playing ${index + 1} of ${_urls.length}"),
),
body: Stack(
children: <Widget>[
GestureDetector(
onLongPressStart: (_) => _controller(index).pause(),
onLongPressEnd: (_) => _controller(index).play(),
child: Center(
child: AspectRatio(
aspectRatio: _controller(index).value.aspectRatio,
child: Center(child: VideoPlayer(_controller(index))),
),
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _buffer,
color: Colors.grey,
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _position,
color: Colors.greenAccent,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(onPressed: _previousVideo, child: Icon(Icons.arrow_back)),
SizedBox(width: 24),
FloatingActionButton(onPressed: _nextVideo, child: Icon(Icons.arrow_forward)),
],
),
);
}
}
All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.