How Can I PAUSE or RESUME a async task using a button in flutter? - flutter

I'm Building An Flutter Application which requires image changes after a period of time. I thought using while loop with a sleep method inside may solve the problem. But It didn't, Image is only getting change after the loop ends. Application UI also gets froze.
So, I used the async Task which I can't control with a Button.
Desired Output: Image should be changed after every 10 seconds and the user can pause or resume method execution.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Test(
),
),
)
);
}}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int imgnumber=1;
int varToCheckButtonPress = 0;
String BtnTxt = "START";
void inc(){
while(imgnumber<10)
{
print(imgnumber);
await Future.delayed(const Duration(seconds: 10));
setState(() {
imgnumber++;
});
}
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(flex: 1,
child: Container(
child: Image.asset('images/'+imgnumber.toString()+'.png'),
height: 500,
width:500,
color: Colors.green,
),
),
FlatButton(
child: Text(BtnTxt),
onPressed: (){
if (varToCheckButtonPress == 0) {
setState(() {
inc();
BtnTxt = 'PAUSE';
varToCheckButtonPress = 1;
});
} else if (varToCheckButtonPress == 1) {
setState(() {
BtnTxt = 'RESUME';
varToCheckButtonPress = 0;
});
}
},
)
],
);
}
}
I want the user to control the UI with a single button behave as START, PAUSE and RESUME.
Can we Use normal function To implement this functionality?

You should make use of Bloc pattern to manage your states, e.g: StreamBuilder, Providers, and make a timer to push new imageUrl to the sink and let the streamBuilder receive the latest imageUrl.
As for your button, all it controls is the timer. When u hit the play button, new imageUrl will keep pushing to the sink, while you press paused, simply stop the timer, and new image Url will not be pushing new imageUrl to the sink, and of course, reset the timer when you hit the stop button.
Here is a very detail Bloc pattern tutorial you can follow: Medium

The shortcut to achieve this is :
You can probably hold a function in async loop and call setState method on tap to change it's state.
For example :
call this function in desired location
while (_isPaused) {
await Future.delayed(Duration(milliseconds: 500));
}
and then call set state method from onTap, just like this
onTap:(){
setState((){
_isPaused? _isPaused=false: _isPaused=true;
});
}

Related

Flutter - updating state and THEN navigating away

I have a stateful widget called AuthenticatingScreen where I'm trying to perform the following flow...
Output message letting the user know we are logging them in
Get user oAuth token (calls to service file)
Update the message to let the user know we are loading their details
Fetch the users details and redirect them away
The problem is that at step three, I'm rebuilding the state, which is in turn causing the build method to be fired again and calling the service again, which triggers an exception.
import 'package:flutter/material.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
// step 1) our default message
String _message = 'Please wait while we log you in...';
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
Widget build(BuildContext context) {
// step 2) get our oAuth token
AuthService.handleCallback(widget.token).then((accessCode) async {
// step 3) update our message
setState(() => _message = 'We\'re just getting your details');
// step 4) retrieve our user details and redirect away
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/home',
(Route<dynamic> route) => false,
);
});
});
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
My question being: How can I work around this / extract this logic to only fire when the widget is created, while still having access to the build context for navigation?
I've tried making the widget itself stateless and extracting the message and spinner into a separate widget, but changing the input argument alone still doesn't force a rebuild.
you can do it this way, i usually use getx & controller to achieve this.
separate the UI class & service class preferably in a controller
make the UI class statefull
call the API in onInit() method,as it called only once it will trigger the
service class
in API method when you get the result 200, initiate the UI transition
Ok, so I have figured out the solution. It seems making service calls within the build() method is a bad idea.
Moving my service calls into a void function which can then be called within the initState() method seems to be the way to go.
import 'package:flutter/material.dart';
import 'package:testing/screens/home.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
/// the default message to display to the user.
String _message = 'Please wait while we log you in...';
void _authenticateUser(String token) {
AuthService.handleCallback(widget.token).then((accessCode) async {
// we've got the users token, now we need to fetch the user details
setState(() => _message = 'We\'re just getting your details');
// after fetching the user details, push them to the home screen
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
HomeScreen.name,
(Route<dynamic> route) => false,
);
});
});
}
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
void initState() {
super.initState();
_authenticateUser(widget.token);
}
#override
Widget build(BuildContext context) {
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
And this way when the build() method is called again for the rebuild, very little details have to be redrawn.

Flutter - programmatically update TextField to show scrolling info

I'd like to show some debug info in my App (e. g. user pressed Button A) and was thinking of using the TextField widget for this.
Using below code, I can record ambient sound from my phone and I'd like to add a widget at the bottom that displays timestamps of when the recordings started and stopped, including length of the recording. The idea is to use a ring buffer (package:circular_buffer) that keeps track of a pre-defined number of text lines being displayed in the TextField. Whenever something happens, the new debug info is added as an element to the ring buffer and the TextField is updated. I am very new to flutter, so I'm completely unsure how to achieve this. I was trying to use setState() but I don't know how to call it from other widgets. Is there a way to register as listener to state changes of other widgets and update the text accordingly?
import 'dart:async';
import 'dart:io';
// locale, datetime formatting
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
// local imports
import './display_timer.dart';
void main() {
runApp(myApp());
}
class myApp extends StatefulWidget {
#override
myAppState createState() => myAppState();
}
class myAppState extends State<myApp> {
final record = Record();
bool bPressed = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Record',
home: Scaffold(
appBar: AppBar(
title: const Text('Record'),
),
body: Center(
child:
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
ElevatedButton(
child: bPressed ? const Text("Stop") : const Text("Record"),
onPressed: () {
setState(() {
bPressed = !bPressed;
});
action();
},
),
if (bPressed)
ElapsedTime(timestamp: DateTime.now().toString())
else
const Text("")
]),
),
));
}
String getTimestampSec() {
DateTime now = DateTime.now();
var dateString = DateFormat('yyyyMMdd_hhmmss').format(now);
return dateString;
}
Future<void> action() async {
// Get the state of the recorder
bool isRecording = await record.isRecording();
// Check and request permission
if (await record.hasPermission()) {
if (isRecording) {
// Stop recording
await record.stop();
} else {
// get write dir
// TODO: add fnc so dir only has to be initialize once
Directory appDocDir = await getApplicationDocumentsDirectory();
String appDocPath = appDocDir.path;
String outFile =
"$appDocPath/${getTimestampSec()}_ambient_test.m4a";
print("saving audio to: $outFile");
// Start recording
await record.start(
path: outFile,
encoder: AudioEncoder.aacLc, // by default
bitRate: 128000, // by default
samplingRate: 44100, // by default
);
}
}
print('Pressed');
}
}
To use setState() every time is not good practice rather than you can use ValueNotifier. Every time you update the string value it notify to your TextField and it will update TextField data.
ValueNotifier<String> logText = ValueNotifier<String>("Welcome");
ValueListenableBuilder(
valueListenable: logText,
builder: (context, logTextUpdate,child) {
return Text(
"${logText.value}",
style: TextStyle(fontSize: 25),
);
}),
when you want to update your text, assign value to logText
logText.value = "Happy to see you!"

How to make a timer constantly display

I have made a chess clock but the time only updates when you click one of the buttons and I want it so the time is constantly updating. I have the setState() function after the button press but no ware else. How could this be mad to updating every 10th of a second or so. Thanks. Here is my code:
import 'package:flutter/material.dart';
...
Widget build(context) {
return MaterialApp(
home: Scaffold(// this is what creates the page, everything should be inside this cuz everything is inside the page
appBar: AppBar(//what is displayed at the top WOW
title: Text('chess clock'),
),
body: Column(
children: [
Container(
margin: EdgeInsets.all(150.0),
child: RaisedButton(
onPressed: () {
setState((){
if (button1==1){
button1=2;
minus=minus2-minus;
fminus=fminus+minus;
print(minus);
divid=DateTime.now().microsecondsSinceEpoch/1000000-start/1000000;
divid.round();
divid=divid-fminus;
minus=DateTime.now().microsecondsSinceEpoch/1000000;
divid=60.0-divid;
}
});
},
child: Text(divid.round().toString()),
)
),
Container(
margin: EdgeInsets.all(150.0),
child: RaisedButton(
onPressed: () {
setState((){
if (button1==2){
button1=1;
minus2=minus-minus2;
fminus2=fminus2+minus2;
divid2=DateTime.now().microsecondsSinceEpoch/1000000-start/1000000;
divid2.round();
divid2=divid2-fminus2;
minus2=DateTime.now().microsecondsSinceEpoch/1000000;
divid2=60.0-divid2;
}
});
},
child: Text(divid2.round().toString()),
...
For a complete solution, you should put Kauli's code in your initState() method and tear it down in dispose(). It would look something like this.
I've made some small changes to fit the needs of the solution.
Edit: I added the surrounding StatefulWidget class and removed some cut and paste which was confusing. You can wrap the build method with your original Scaffold and other widgets. I left that out to make the answer a little clearer.
class ChessClock extends StatefulWidget {
#override
_ChessClockState createState() => _ChessClockState();
}
class _ChessClockState extends State<ChessClock> {
// Important to capture the timer object reference
Timer chessClockTimer;
Timer periodicSec() {
// How could this be made to updating every 10th of a second or so.
// Per op question
return Timer.periodic(Duration(milliseconds: 10), (_){
setState((){
if (button1==2){
button1=1;
minus2=minus-minus2;
fminus2=fminus2+minus2;
divid2=DateTime.now().microsecondsSinceEpoch/1000000-start/1000000;
divid2.round();
divid2=divid2-fminus2;
minus2=DateTime.now().microsecondsSinceEpoch/1000000;
divid2=60.0-divid2;
}
});
});
}
#override
void initState() {
super.initState();
chessClockTimer = periodicSec(); // Kauli's code
}
#override
void dispose() {
// Stop the timer
chessClockTimer?.cancel();
// Clean up the timer
chessClockTimer?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
// Your build method code
}
}
You can use the Timer.periodic to do this
void periodicSec() {
Timer.periodic(Duration(seconds: 1), (_){
setState((){
if (button1==2){
button1=1;
minus2=minus-minus2;
fminus2=fminus2+minus2;
divid2=DateTime.now().microsecondsSinceEpoch/1000000-start/1000000;
divid2.round();
divid2=divid2-fminus2;
minus2=DateTime.now().microsecondsSinceEpoch/1000000;
divid2=60.0-divid2;
}
});
});
}

Flutter - rebuild GestureDetector widget without user interaction

I am trying to display 3 images one after another by using GestureDetector and its onTap method but one image should be displayed without tapping. Supposing I have three images 1.png, 2.png, 3.png I would like to display the first image 1.png at the first place. Clicking on it will result in displaying the second image 2.png.
The second image 2.png should now be replaced after x seconds by displaying the image 3.png.
I am using a stateful widget that stores which image is currently shown. For the first image change the onTap will execute setState( () {screen = "2.png"} and the widget is rebuilt. However, I am pretty stuck now when the third image should be displayed after x seconds delay because there should not be any user interaction and I am not sure where to change it. I tried a lot of stuff especially editting the state from outside the widget but I did not succeed til now.
This is the code I am using:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Navigation Basics',
home: MyScreen(),
));
}
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
String currentScreen = 'assets/images/1.png';
#override
Widget build(BuildContext context) {
return GestureDetector(
child: FittedBox(
child: Container(
child: Image.asset(currentScreen),
),
fit: BoxFit.fill,
),
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
}
},
);
}
}
Displaying the second image worked without any issues but I have no idea what/where to code in order to display the third image.
Thanks for any help!
I'm assuming you don't want the second and third picture to respond to the gesture detection. Try this, please.
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
Future.delayed(const Duration(milliseconds: 3000), () {
setState(() {
currentScreen = 'assets/images/3.png';
});
});
} else {
return;
}
},

Flutter: Dipose HTTP request on close controller

Original Answer
I'm using the Getx State Management on Flutter.
Simplifying as much as possible:
I build a GetxController to control my Page, and in this controller i have a StatefulWidget instance that evoque http requests.
class MyController extends GetxController {
Player player;
}
class Player extends StatefulWidget {
PlayerState state;
#override
PlayerState createState() {
state = PlayerState();
return state;
}
}
class PlayerState extends State<Player> {
void methodName async() {
futureRequest().then((data) {
// when the error ocurrs
setState(() {});
});
}
}
The problem occurs when the user closes the mobile page, triggering the controller's close method, before the end of the request.
That way, when setState is triggered, there is no more page instance and the error occurs.
I believe that the solution would be to interrupt all requests related to this GetxController and "delete" this instance of StatefulWidget at the moment the controller close method was called.
I don't know if this would be right, and if it's how to do it ..
==================================================================
Updated Answer
The main problem was that the async request in getDetails() method, return a response even after the controller is disposed, even using GetBuilder, and this response carried a url from a video that is started by the videoPlayerController (a video_player plugin instance).
So, the user is in another screen but keep listen to the video that is playing on background.
As a workaround and thinking in apply good practices to the code, i make a refactor to use only stateless widgets, following the GetX rules. I solved the problem, but i had to convert the Future's to Stream's
The binding is being created with Get.lazyPut() to perform dependencies injection:
class Binding implements Bindings {
Get.lazyPut<PlayerController>(() {
return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
});
}
This binding is linked to the page router, based on GetX documentation.
class AppPages {
static final routes = [
GetPage(name: Routes.MyRoute, page: () => MyPage(), binding: MyBinding()),
];
}
To prevent the controller to make actions even before it is disposed, i have to created a Stream and cancel it on controller dispose.
class MyController extends GetxController {
MyController({#required this.repository}) : assert(repository != null);
StreamSubscription<bool> stream;
// Instance of plugin video_player
VideoPlayerController videoPlayerController;
#override
void onClose() {
if (streamGetVideo != null) streamGetVideo.cancel();
super.onClose();
if (videoPlayerController != null) videoPlayerController?.dispose();
}
// This is the method called by the user on screen
void loadVideo() {
stream = getDetails().asStream().listen((bool response) {
// This code is canceled on onClose() method by the stream
if (response) update();
});
}
Future<bool> getDetails() async {
return await repository.getDetails().then((data) async {
videoPlayerController = VideoPlayerController.network(data);
initFuture = videoPlayerController.initialize();
await initFuture.whenComplete(() { return true; });
});
}
}
I think that Flutter/GetX should have a better way to do this, without these workarounds that i made. If anyone has a better approach or a hint, i'm open to suggestions.
One solution could be to wrap your setState with
if(mounted){
setState(() {});
}
GetBuilder + update()
In GetX using a GetBuilder with update() takes care of that lifecycle checking / handling so you don't have to do it.
Below is an example of a screen/route being closed prior to an HTTP call finishing & calling setState(), without an exception thrown.
(On the 2nd screen, click the Go Back! button fast to simulate an already disposed StatefulWidget.)
Below, an update() call is used to update the screen, instead of setState(), but they are the same in a GetBuilder. GetBuilder is (extends) a StatefulWidget.
GetBuilder adds listeners to the Controller you pass it, either through init: constructor arg or via the GetBuilder<Type> parameter if the Controller was initialized elsewhere/earlier.
That listener will be disposed if the StatefulWidget (i.e. GetBuilder) is disposed.
(See GetBuilder's dispose() function for some wizardry. While adding a listener, the returned value from adding that listener, is a function to dispose/unsubscribe from that listen. Pretty clever.)
So the GetBuilder/StatefulWidget will never have its update() / setState() called if that widget has been disposed because the listener for those calls has been disposed. So a slow returning HTTP call won't attempt to update/setState a widget that no longer exists in the widget tree.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HttpX extends GetxController {
String slowValue = 'loading...';
#override
void onInit() {
slowCall();
}
/// Simulate a slow, long running HTTP call
Future<void> slowCall() async {
slowValue = 'Slow call STARTED!';
print(slowValue);
update(); // update the screen to show started message
await Future.delayed(Duration(seconds: 5), () {
slowValue = 'Slow call FINISHED!';
print(slowValue);
update(); // won't call setState() if GetBuilder is disposed
});
}
}
class GetXDisposePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('awaiting http call to finish'),
RaisedButton(
child: Text('Go Call Page'),
onPressed: () => Get.to(SlowCallPage()),
// using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
)
],
),
),
);
}
}
class SlowCallPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose - Go Back!'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<HttpX>(
init: HttpX(), // fake slow http call starts on init
builder: (hx) => Text(hx.slowValue),
),
RaisedButton(
child: Text('Go Back!'),
onPressed: () => Get.back(),
),
],
),
),
);
}
}