I make app to read accelerometer but I have difficult to read accelerometer sensor log. Anyway, I used toggle switch. The problem is when I turned on toggle switch, the accelerometer only read once not my trial before by used button component. These the code below:
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
class WalkingApp extends StatefulWidget {
const WalkingApp({super.key});
#override
State<WalkingApp> createState() => _WalkingAppState();
}
class _WalkingAppState extends State<WalkingApp> {
bool isSwitched = false;
var _accel = 0.0;
var _currentAcc = 0.0;
var lastAccel = 0.0;
var stepCount = 0;
var x= 0.0, y=0.0, z=0.0;
late Timer timer;
void stop() {
setState(() {
isSwitched = false;
_accel = 0.0;
for (StreamSubscription<dynamic> subscription in _streamSubscriptions) {
subscription.cancel();
}
});
}
//walk
final _streamSubscriptions = <StreamSubscription<dynamic>>[];
#override
void initState() {
super.initState();
_streamSubscriptions.add(
accelerometerEvents.listen((AccelerometerEvent event) {
setState(() {
x = event.x;
y = event.y;
z = event.z;
});
})
);
}
void start() {
isSwitched = true;
setState(() {
hitungStep(x, y, z);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SwitchListTile(
title: const Text('Jalan',
style: TextStyle(color: Colors.black87, fontSize: 16.0)),
value: isSwitched,
onChanged: (value) {
setState(() {
(!isSwitched) ? start(): stop();
});
},
activeTrackColor: Colors.blueAccent,
activeColor: Colors.blue,
),
const SizedBox(height: 16),
Text('$stepCount',
style: const TextStyle(color: Colors.cyan, fontSize: 48.0)),
const SizedBox(height: 8),
const Text('Langkah',
style: TextStyle(color: Colors.black54, fontSize: 18.0)),
],
)),
);
}
void hitungStep(double x, double y, double z) {
_accel = 10;
_currentAcc = 9.8;
lastAccel = 9.8;
lastAccel = _currentAcc;
_currentAcc = sqrt((x*x)+(y*y)+(z*z));
var delta = _currentAcc - lastAccel;
_accel = delta.abs();
if(_accel > 1){
stepCount++;
}
if(kDebugMode){
print('nilai x :$x, nilai y: $y, nilai z: $z, accel: $_accel');
}
}
#override
void dispose() {
super.dispose();
for (final subscription in _streamSubscriptions) {
subscription.cancel();
}
}
}
My expectation is when turning on toggle and the phone still read accelerometer log like I pressed button. Would you like to help solve problem? what should I do?
Related
I have a modalBottomSheet. On it, I am displaying several widgets, especially an audioPlayer.
I have find out that when I press the play button, the audio file is playing, so far so good, but if I tap outside the modalBottomSheet, the modal is dismissed. I am OK to get the modal dismissed. But my problem is that when it is dismissed, the player which is running, is generating an exception.
Unhandled Exception: setState() called after dispose()
I do not want to make the Modal not dissmisible.
Please, can you advise? Many thanks.
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers/audioplayers_api.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:gtd_official_sharped_focused/Reusable%20Widget/Player_Audio/widgets/play_pause_button.dart';
class AudioPlayerWidget extends StatefulWidget {
final String url;
final bool isAsset;
final Duration currentTime;
final Duration totalTime;
final ValueChanged<Duration> onSeekBarMoved;
const AudioPlayerWidget({
Key key,
this.url,
this.isAsset = false,
this.currentTime,
this.totalTime,
this.onSeekBarMoved,
}) : super(key: key);
#override
_AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();
}
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
AudioPlayer _audioPlayer;
AudioCache _audioCache;
//variables for slider
Duration _duration = new Duration();
Duration _position = new Duration();
PlayerState _playerState = PlayerState.STOPPED;
bool get _isPlaying => _playerState == PlayerState.PLAYING;
bool get _isLocal => !widget.url.contains('https');
#override
void initState() {
_audioPlayer = AudioPlayer(mode: PlayerMode.MEDIA_PLAYER);
_audioCache = AudioCache(fixedPlayer: _audioPlayer);
_audioPlayer.onDurationChanged.listen((d) {setState(() {
_duration = d;
});});
_audioPlayer.onAudioPositionChanged.listen((p) {setState((){
_position = p;
});});
_audioPlayer.onPlayerCompletion.listen((event) {
setState(() {
_position = Duration(seconds: 0);
_playerState = PlayerState.STOPPED;
});
});
_audioPlayer.onPlayerError.listen((msg) {
print('audioPlayer error : $msg');
setState(() {
_playerState = PlayerState.STOPPED;
});
});
super.initState();
}
#override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left:18.0),
child: Text(_position.toString().split('.')[0],style:TextStyle(fontSize: 16)),
),
Padding(
padding: const EdgeInsets.only(right:18.0),
child: Text(_duration.toString().split('.')[0],style:TextStyle(fontSize: 16)),
),
],),
_buildSliderBar(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
buttonBackWard10Seconds(),
PlayPauseButton(
isPlaying: _isPlaying,
onPlay: () => _playPause()
),
buttonForward10Seconds(),
//Do not delete iconButton below => for reference
/* IconButton(
onPressed: () => _stop(),
icon: Icon(
Icons.stop,
size: 40,
color: Colors.red,
),
),*/
],
),
],
);
}
//########################################################
_playPause() async {
if (_playerState == PlayerState.PLAYING) {
final playerResult = await _audioPlayer.pause();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PAUSED;
});
}
} else if (_playerState == PlayerState.PAUSED) {
final playerResult = await _audioPlayer.resume();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PLAYING;
});
}
} else {
if (widget.isAsset) {
_audioPlayer = await _audioCache.play(widget.url);
setState(() {
_playerState = PlayerState.PLAYING;
});
} else {
final playerResult = await _audioPlayer.play(widget.url, isLocal: _isLocal);
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.PLAYING;
});
}
}
}
}
void changeToSecond(int second){
Duration newDuration = Duration(seconds:second);
_audioPlayer.seek(newDuration);
}
_stop() async {
final playerResult = await _audioPlayer.stop();
if (playerResult == 1) {
setState(() {
_playerState = PlayerState.STOPPED;
});
}
}
//###############################################################
Slider _buildSliderBar(BuildContext context) {
return Slider(
value: _position.inSeconds.toDouble(),
min: 0.0,
max: _duration.inSeconds.toDouble(), //_sliderValue,
activeColor: Colors.red,
inactiveColor: Colors.grey,
onChanged: (double value) {
setState(() {
changeToSecond(value.toInt());
value=value;
});
},
);
}
Widget buttonBackWard10Seconds(){
return IconButton( icon: Icon(CupertinoIcons.gobackward_10),
iconSize: 40,
color: Colors.black,
onPressed: (){
_position = _position - Duration(seconds:10);
if (_position < Duration(seconds:0)) {
_audioPlayer.seek(Duration(seconds: 0));
}
else {
_audioPlayer.seek(_position);
}});
}
Widget buttonForward10Seconds(){
return IconButton( icon:Icon( CupertinoIcons.goforward_10),
iconSize: 40,
color: Colors.black,
onPressed: (){
_position = _position + Duration(seconds:10);
if (_duration >_position) {
_audioPlayer.seek(_position);
}
else if (_duration <_position) {
_audioPlayer.seek(_duration);
}
}
);
}
}
import 'package:flutter/material.dart';
import 'package:gtd_official_sharped_focused/Reusable%20Widget/Player_Audio/widgets/audio_player_widget.dart';
import 'package:gtd_official_sharped_focused/Services/extract_file_Name_url/extract_file_name_url.dart';
Widget modalBottomPlayAudio (context,String urlToPlay){
showModalBottomSheet(
context: context,
//background color for modal bottom screen
backgroundColor: Colors.white,
//elevates modal bottom screen
elevation: 10,
// gives rounded corner to modal bottom screen
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
builder: (BuildContext context) {
// UDE : SizedBox instead of Container for whitespaces
return SizedBox(
height: 350,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
/*Padding(
padding: const EdgeInsets.all(28.0),
),*/
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left:18.0),
child: Text(getFileNameFromURL(urlToPlay),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
),
],
),
SizedBox(height: 60,),
AudioPlayerWidget(url:urlToPlay),
],
),
),
);
},
);
}
you could change
setState(()=>{
...
})
to
if(mounted)(
setState(()=>{
...
})
)
Which ensures setState is called only when the widget is mounted on the screen.
How to create Custom camera with Countdown Timer in Flutter.
I have tried take container under stack on camera but this Container is not showing on camera.
Here I am focus only functionality check below code may this is help full for you.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'dart:async';
import 'dart:io';
List<CameraDescription> cameras = <CameraDescription>[];
Future<void> main() async {
// Fetch the available cameras before initializing the app.
try {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras();
} on CameraException catch (e) {
print("${e.code} ${e.description}");
}
runApp(CameraApp());
}
class CameraApp extends StatefulWidget {
#override
_CameraAppState createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> {
CameraController? controller;
Timer? countdownTimer;
Duration myDuration = const Duration(seconds: 10);
#override
void initState() {
super.initState();
controller = CameraController(cameras[0], ResolutionPreset.max);
controller?.initialize().then((_) {
if (!mounted) {
return;
}
setState(() {});
});
}
#override
void dispose() {
controller?.dispose();
super.dispose();
}
void startTimer() {
countdownTimer =
Timer.periodic(const Duration(seconds: 1), (_) => setCountDown());
}
void setCountDown() {
const reduceSecondsBy = 1;
setState(() {
final seconds = myDuration.inSeconds - reduceSecondsBy;
if (seconds < 0) {
countdownTimer!.cancel();
} else {
myDuration = Duration(seconds: seconds);
}
});
}
#override
Widget build(BuildContext context) {
if (!controller!.value.isInitialized) {
return Container();
}
String strDigits(int n) => n.toString().padLeft(2, '0');
final days = strDigits(myDuration.inDays);
// Step 7
final hours = strDigits(myDuration.inHours.remainder(24));
final minutes = strDigits(myDuration.inMinutes.remainder(60));
final seconds = strDigits(myDuration.inSeconds.remainder(60));
return MaterialApp(
home: Scaffold(
body: Center(
child: Stack(
alignment: Alignment.center,
children: [
CameraPreview(controller!),
Text(
'$minutes:$seconds',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 30),
),
Padding(
padding: const EdgeInsets.only(top: 100),
child: ElevatedButton(
onPressed: startTimer, child: const Text("Start")),
)
],
),
),
),
);
}
}
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
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
I recently wrote a test program that I needed that is essentially a CRUD program. I needed to handle this differently to other similar programs that I have written, because I normally use a stateful FAB widget, and don't have to setState() to enable and disable the FAB. In this test program I didn't want to use the custom FAB, and used the standard FAB. I found that whenever I had to enable or disable the FAB because of a change to a TextField, that this required a setState(), and after the build, the cursor for the TextField that was being edited had repositioned. I don't know why that happens, because I had not recreated the Widgets. The only solution that I could come up with to handle that issue was fairly messy and required saving the Widget position in the List of TextField and also save the Selection, and then after the build resetting the Selection to the saved Selection.
What I need to achieve is for the FAB to be only enabled when data has changed. Obviously this can vary with every key entry.
I presume I'm not handling this in the optimal way. How is this handled so that the cursor position remains as it was prior to the build?
----- Have Now Added Code below ----
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
//=====================================================================================
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test Widgets',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(title: 'Test Widgets'),
);
}
}
//=====================================================================================
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
//=====================================================================================
class _HomePageState extends State<HomePage> {
bool _tfDataHasChanged = false;
bool _tfInitialized = false;
bool _tfSaveSelection = false;
int _iNdxWidgetChanged = -1;
List<String> _lsOldData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<String> _lsNewData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<TextField> _lwTextFields;
TextSelection _wTextSelection;
//-------------------------------------------------------------------------------------
#override
void dispose() {
for (int iNdxWidget = 0;
_lwTextFields != null && iNdxWidget < _lwTextFields.length;
iNdxWidget++) {
_lwTextFields[iNdxWidget].focusNode.removeListener(() {
_fnFocusChanged();
});
_lwTextFields[iNdxWidget]?.controller?.dispose();
_lwTextFields[iNdxWidget]?.focusNode?.dispose();
}
super.dispose();
}
//-------------------------------------------------------------------------------------
#override
Widget build(BuildContext context) {
_tfInitialized = false;
SchedulerBinding.instance.addPostFrameCallback((_) => _fnOnBuildComplete());
if (_lwTextFields == null) {
_fnCreateAllWidgets();
}
List<Widget> lwDisplay = _fnCreateDisplay();
return Scaffold(
appBar: AppBar(
flexibleSpace: SafeArea(
child: _fnCreateAppBarWidgets(),
)),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: lwDisplay,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _tfDataHasChanged ? _fnUpdateData : null,
tooltip: 'Update',
backgroundColor: _tfDataHasChanged ? Colors.blue : Colors.grey,
child: Icon(Icons.done),
),
);
}
//-------------------------------------------------------------------------------------
_fnOnBuildComplete() {
_tfInitialized = true;
if (_tfSaveSelection && _iNdxWidgetChanged >= 0) {
_lwTextFields[_iNdxWidgetChanged].controller.selection = _wTextSelection;
}
}
//-------------------------------------------------------------------------------------
void _fnCreateAllWidgets() {
_lwTextFields = List(_lsNewData.length);
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_fnCreateTextField(iNdxWidget);
}
}
//-------------------------------------------------------------------------------------
void _fnCreateTextField(int iNdxWidget) {
TextEditingController wController = TextEditingController();
FocusNode wFocusNode = FocusNode();
wFocusNode.addListener(() => _fnFocusChanged());
_lwTextFields[iNdxWidget] = TextField(
autofocus: false, //(iNdxWidget == 0),
autocorrect: false,
enabled: true,
keyboardType: TextInputType.text,
maxLength: 25,
controller: wController,
focusNode: wFocusNode,
textInputAction: TextInputAction.next /* TYPE OF ACTION KEY */,
onSubmitted: ((v) => _fnSetNextFocus(iNdxWidget)),
onChanged: (text) => _fnTextListener(iNdxWidget, text),
decoration: _fnCreateInputDecoration(
'Text Field Number ${iNdxWidget + 1}', 'Enter Data'),
style: _fnCreateWidgetTextStyle(Colors.blue[700]),
);
}
//-------------------------------------------------------------------------------------
_fnTextListener(int iNdxWidget, String sText) {
if (_tfInitialized) {
_lsNewData[iNdxWidget] = sText;
_fnCheckIfDataHasChanged(
iNdxWidget) /* ENABLE OR DISABLE SUBMIT BUTTON */;
}
}
//-------------------------------------------------------------------------------------
_fnSetNextFocus(int iNdxWidget) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_lwTextFields[iNdxWidget].focusNode.unfocus();
if (iNdxWidget + 1 < _lwTextFields.length) {
_lwTextFields[iNdxWidget + 1]?.focusNode?.requestFocus();
}
}
}
//-------------------------------------------------------------------------------------
InputDecoration _fnCreateInputDecoration(String sHeading, String sHint) {
return InputDecoration(
labelText: sHeading,
hintText: sHint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(20.0)),
);
}
//-------------------------------------------------------------------------------------
TextStyle _fnCreateWidgetTextStyle(Color color) {
return TextStyle(
fontSize: 14.0,
color: color,
);
}
//-------------------------------------------------------------------------------------
List<Widget> _fnCreateDisplay() {
List<Widget> lwDisplay = List((_lwTextFields.length * 2) + 2);
lwDisplay[0] = SizedBox(height: 10);
int iNdxDisplay = 1;
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
lwDisplay[iNdxDisplay++] = _lwTextFields[iNdxWidget];
lwDisplay[iNdxDisplay++] =
SizedBox(height: iNdxDisplay < lwDisplay.length - 2 ? 10 : 80);
}
lwDisplay[lwDisplay.length - 1] = Divider(color: Colors.black, height: 2);
return lwDisplay;
}
//-------------------------------------------------------------------------------------
_fnUpdateData() {
for (int iNdxWidget = 0; iNdxWidget < _lsNewData.length; iNdxWidget++) {
if (_lsNewData[iNdxWidget] != _lsOldData[iNdxWidget]) {
_lsOldData[iNdxWidget] = _lsNewData[iNdxWidget];
}
}
_fnCheckIfDataHasChanged(-1);
}
//-------------------------------------------------------------------------------------
_fnCheckIfDataHasChanged(int iNdxWidgetChanged) {
bool tfChanged = false /* INIT */;
for (int iNdxWidgetTest = 0;
!tfChanged && iNdxWidgetTest < _lsNewData.length;
iNdxWidgetTest++) {
tfChanged = _lsNewData[iNdxWidgetTest] != _lsOldData[iNdxWidgetTest];
}
if (iNdxWidgetChanged >= 0) {
_iNdxWidgetChanged = iNdxWidgetChanged;
_wTextSelection = _lwTextFields[iNdxWidgetChanged].controller.selection;
}
if (tfChanged != _tfDataHasChanged) {
setState(() => _tfDataHasChanged = tfChanged) /* WE NEED TO ENABLE FAB */;
}
}
//-------------------------------------------------------------------------------------
Row _fnCreateAppBarWidgets() {
IconData wIconData =
_tfSaveSelection ? Icons.check_box : Icons.check_box_outline_blank;
Color wColor = _tfSaveSelection ? Colors.blue[900] : Colors.grey[600];
IconButton wIconButton = IconButton(
icon: Icon(wIconData),
color: wColor,
onPressed: _fnCheckboxChanged,
iconSize: 40);
return Row(children: <Widget>[
SizedBox(width: 10),
Text('Save\nSelection', textAlign: TextAlign.center),
wIconButton,
SizedBox(width: 30),
Text('Test TextField')
]);
}
//-------------------------------------------------------------------------------------
_fnFocusChanged() {
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_iNdxWidgetChanged = iNdxWidget;
_wTextSelection = _lwTextFields[iNdxWidget].controller.selection;
return;
}
}
}
//-------------------------------------------------------------------------------------
void _fnCheckboxChanged() {
_tfSaveSelection = !_tfSaveSelection;
if (!_tfSaveSelection) {
_iNdxWidgetChanged = -1;
}
setState(() {});
}
}
-------- Have added key to TextField, but issue persists ---------
key: ValueKey<int>(iNdxWidget),
My bug - as posted by #pskink
My excuse - I normally use a stateful FAB, so I don't normally encounter this.
Answer:
so change this line:
TextEditingController wController = TextEditingController(text: _lsNewData[iNdxWidget]);
and remove this one
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
– pskink Feb 23 at 7:33
I hope these functions might help you
void updateText(String text) {
if (text != null) {
this.text = _applyMask(mask, text);
} else {
this.text = '';
}
_lastUpdatedText = this.text;
}
void updateMask(String mask, {bool moveCursorToEnd = true}) {
this.mask = mask;
updateText(text);
if (moveCursorToEnd) {
this.moveCursorToEnd();
}
}
void moveCursorToEnd() {
final String text = _lastUpdatedText;
selection =
TextSelection.fromPosition(TextPosition(offset: (text ?? '').length));
}