Flutter Riverpod, how to set minimut loading state time? - flutter

Building an app with Flutter and Riverpod, using a lot of:
ref.watch(someProvider).when(data: (someData){
// render layout with data
}, error: (err, stack) {
// do stuff with error
}, loading: (){
return LoadingScreen(); <----
})
The problem is that in most cases the loading screen only renders for a split second, causing a bad experience where the app feels a little "jumpy". I would like to be able to set a minimum of say 2 seconds for the loading state, is it possible to force a widget to stay rendered for a minimum amount of time some how?

I have 2 suggestions for you :
Option A - You can put a delay of say 2 seconds in your provider, somewhere between loading and the next state. This way you make the loading screen visible at least 2 seconds.
Option B - You can also make a loadingScreen as an overlay Widget, which dismisses itself after say 2 seconds. This way you make the loading screen visible exactly 2 seconds.
I believe either way is fine in your case, since you say it flashes, I assume no heavy/long tasks are done in between states. In case you do need heavy/long tasks, option A guarantees the tasks are finished before loading screen disappears.

You can create Stopwatch to track (approximate) operation duration. This example will give you some idea.
final someProvider = FutureProvider<int>((ref) async {
final operationDuration = Duration(seconds: 1); //random, you can give it random to test
final stopwatch = Stopwatch()..start();
final data = await Future.delayed(operationDuration);
final minOperationTime = const Duration(seconds: 2);
final extraWaiting = minOperationTime.inMilliseconds - stopwatch.elapsed.inMilliseconds;
if (stopwatch.elapsed.inSeconds < 2) {
await Future.delayed(minOperationTime - stopwatch.elapsed);
}
return extraWaiting;
});
And widget
class LoadingTestOnFutureBuilder extends ConsumerWidget {
const LoadingTestOnFutureBuilder({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(someProvider).when(data: (someData) {
return Text("got data ${someData}");
}, error: (err, stack) {
return Text("Err");
}, loading: () {
return CircularProgressIndicator();
});
}
}

Related

Navigator not removing old widget from tree after pushReplacament()

I've got a stateful widget GameScreen that calls Navigator.pushReplacement to recursively replace itself with another GameScreen instance laid out slightly differently. The pushReplacement causes hero animations to fly elements from one spot to another during the (otherwise invisible) route transition.
I recurse this about 50 times, and all the elements dancing all over the screen look great in theory.
The code is like this (much simplified) example:
final danceProvider = ChangeNotifierProvider<>((ref) => DanceManager(steps: 50));
class GameScreen extends ConsumerStatefulWidget {...}
class GameScreenState extends ConsumerState {
build(context) {
final danceManager = ref.watch(danceProvider);
if (gameIsOver() && danceManager.stepsLeft > 0 && danceManager.ready) {
SchedulerBinding.instance.addPostFrameCallback(
() => _replaceSelfWithNewInstance(danceManager));
}
return ... // structure with layout based on danceProvider.stepsLeft
}
_replaceSelfWithNewInstance(danceManager) {
danceManager.ready = false; // Recurse once per screen
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, _) {
animation.addStatusListener(() {
if (animation.status == "complete")
danceManager.ready = true; // Starts next recursion...
});
danceManager.stepsLeft--;
return GameScreen(); // ...in this instance <--
},
),
transitionDuration: Duration(seconds: 1),
);
}
}
It seems the old states aren't being removed from the widget tree until I finally stop calling pushReplacement: at that point they are ALL deactivated and disposed at once. It seems like Navigator is holding onto all the previous screens but not rendering them?
My intent would be that after each screen called pushReplacement to display the next screen in the recursion, it would be out of the tree and would not be rebuilt at all, much less N useless times.
Is there something I might be doing wrong to force Navigator to keep my pushReplaced widgets in the tree, or is this a known thing Navigator does, where we can't be sure when a replaced route will finally be unmounted?

Api data null whe nthe pages load even i use initState

im using post method to get api data/response.body in initState and i get all the response/data then i put it in my variables surveyData, but when i load the pages with widget looped by the response.body length it goes error and say response.body.length is null but when i save the text editor/ hot reload, all the data entered and it's not null anymore and the widget appear.
fyi: the http.post method is my own function not from import 'package:http/http.dart' as http; so don't mind it
Variables that contain the response
dynamic surveyData;
initState Code
#override
void initState() {
super.initState();
// GET PAGES
surveyPages = widget.surveyPages;
// GET FORM DETAIL
http.post(
'survey-pre-mitsu/form-detail',
body: {
"survey_form_header_id": 1,
},
).then(
(res) {
surveyData = res['data'];
},
);
}
Widget that looped by surveyData.length
for (var i = 0; i < surveyData.length; i++)
AdditionalForm(questionLabel: surveyData[i]['label'],
questionType: surveyData[i]['type'],),
This is what the error
And this is what it looks like when i do hot reload
First, I suggest you to use future builder to resolve this problem.
First, I suggest performing post(even though its your own code) as a asynchronous operation, hence not inside initState(). Preferably write the logic in a separate class/file with packages like a provider package. Try this first and then post the error after that.
You need to add Some delay , In this Delay you can show a loading icon . for delay you can use :
Future.delayed(const Duration(milliseconds: 500), () {
// Here you can write your code
setState(() {
// Here you can write your code for open new view
});
});
This will solve your problem

Flutter Rate Limit / Debouncer Design Implementation

I'm working on a small application for a friend who is trying to buy shoes that sell out frequently, and this application uses a web scraper to load a website and check a portion of its content as a status check every once in a while (don't want to hit this very frequently).
The issue I'm noticing, is that all of this action is being done during the build phase of the widget. As you can probably see, this isn't a good option. When I am re-sizing the application on desktop, the build method is called rapidly many times, which causes many many calls to be made to scrape the website which is undesired.
Here's a look at the code as it stands:
//should be moved into a controller I think
//Or at least somewhere to 'debounce' the web scraping during each rebuild
Stream<bool?> productChecker(Duration minInterval, Duration maxInterval) async* {
assert(maxInterval > minInterval);
//begin checking continuously
do {
//signal that we're checking again
yield null;
//construct duration to wait between calls
Duration waitTime = minInterval + Duration(milliseconds: math.Random().nextInt((maxInterval - minInterval).inMilliseconds));
bool loaded = await scraper.loadWebPage(unencodedPath);
bool isInStock = false;
if(loaded){
final inStockElements = scraper.getElement(cartButtonId, []);
assert(inStockElements.isNotEmpty);
final inStockElement = inStockElements[0];
isInStock = inStockElement['title'] != cartButtonInStockContent;
print('In Stock: $isInStock');
}
yield loaded ? isInStock : false;
print('${DateTime.now().toIso8601String()}: Waiting for ${waitTime.inSeconds} seconds');
await Future.delayed(waitTime);
}while(true);
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productChecker(5.seconds, 30.seconds),
builder: (context, AsyncSnapshot<bool?> snapshot) {
if(snapshot.hasData){
return snapshot.data != null ?
snapshot.data! ? ElevatedButton(onPressed: () => launch(baseUrl+unencodedPath), child: 'In Stock'.text.make()) :
'Out of Stock'.text.make() :
CircularProgressIndicator();
}else{
return CircularProgressIndicator();
}
},
);
}
So my question is this:
How should I implement a controller that will keep track of the last time the website was scraped, and a cool down duration that will ignore scrape requests during the cool down?
This is more of a broad answer but for reactive frameworks like flutter you really want to separate the business logic from the view logic. Your issue is that your business logic is in the build function which is purely for view logic. I am not to familiar with StreamBuilder but I think you could utilize a plain build function inside a stateful widget to achieve what you want.
A widget would have your scraper in the initState()/dispose() functions. Set some values up that will contain the info needed for your view then your build will contain 3 displays based on the state of isInStock.
*Remember when you change isInStock to use the function
setState(() { isInStock = true; });
setState ensures that after updating the state that the view rebuilds.
if null then that means the scraper is still awaiting data, show
loading view
if true then show your launch button
if false then show out of stock text
Rebuilds will not trigger business logic because it is outside of the build so now all you need to do is worry about the business logic and the view will then reflect what the outcome of it is.

Flutter HTTP Request Called Multiple Times On App Load [duplicate]

This question already has an answer here:
Flutter: Why setState(( ) { }) set data again and again
(1 answer)
Closed 2 years ago.
I have a flutter project that makes an http request to gather json of inventory items then render it on screen in a list view. When a user scrolls to the bottom it then loads more inventory by triggering another http call. However my initial HTTP call is being called multiple times. And I am not sure how to handle that.
The result I get is my finish print statement just triggers continuously. At first I thought I had it working because it does load the inventory to my list view. But then I noticed It just keeps loading the same data into my list view non stop. When I added that finish print statement it became clear that it is actually continuously making that http call non stop.
Id like to have it only make the call once, then make a new call again when the user scrolls to bottom.
Any tips will help thank you.
Here is my code.
Network Code
Future <List <dynamic>> fetchInventory() async{
dynamic response = await http.get('https:somelink.com',
headers: {'x-api-key': 'mykey'},);
var inventory = List<dynamic>();
//if 200 response is given then set inventory var to inventoryJson value from the http call
if (response.statusCode == 200){
dynamic inventoryJson = jsonDecode(response.body);
inventory = inventoryJson['page'];
print('finish');
}
//Inventory is returned
return inventory;
}
}
Here is my how I am using that code in my main file
class _WelcomeScreenState extends State<WelcomeScreen> {
//Create Network Helper Obj
NetworkHelper networkHelper = NetworkHelper();
//invenrtory List is set to empty
var inventory = [];
var _controller = ScrollController();
#override
void initState() {
super.initState();
// set up listener here
_controller.addListener(() {
if (_controller.position.pixels == _controller.position.maxScrollExtent) {
// Perform your task
//This method fetches the data from the fetchInventory method
print('at bottom');
networkHelper.fetchInventory().then((value) {
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory = value;
});
});
}
});
}
#override
Widget build(BuildContext context) {
//Used to format currency shown in view
var currencyFormat = NumberFormat('##,###.00', 'en_US');
//This method fetches the data from the fetchInventory method
networkHelper.fetchInventory().then((value){
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory.addAll(value);
});
});
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0.0,
title: AppBarTitle(),
),
body: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 5.0, bottom: 25.0),
child: ListView.builder(
controller: _controller,
itemCount: inventory.length,
itemBuilder: (context, index){
enter code here
enter code here
The responsibility of the build method is to construct a widget tree, and this method may be called repeatedly by the framework whenever it thinks that the screen might have changed and need to be re-rendered. As a result, its important that as little work as possible is done here. Remote calls over the network should be avoided, and implementers should try not to call setState as this will cause the UI rendering to loop.
Listening to the scroll controller as demonstrated is a good way of 'Loading more', what is missing is an initial load of the inventory in the initState() method.
So, you should move the call to networkHelper.fetchInventory() from the build method to the scrollcontroller callback (as it adds to the inventory) and move the existing call to networkHelper.fetchInventory() that initialises the inventory from the scrollcontroller callback to the start of the initState method.

Flutter setState() doesn't always call my build method

I'm trying out Flutter, but I'm having trouble getting the UI to update consistently. I'd like to show a status message while a long-running async method is called, but the setState() call I make just before calling the long-running method doesn't seem to cause my build() method to get invoked.
I've created a simple example that calculates the Fibonacci number for a randomly selected number between 25 and 30. In my sample code/app, hitting the "calc" button calls _calc(). _calc() picks a random number, sets a status message "Calculating Fib of $num..." tied to a text widget (_status) and updates it with setState(); then calls the async _fib() routine to calculate the number; then updates _status with the result using setState(). Additionally, the build() method prints the value of _status to the console, which can be used to see when build() is invoked.
In practice, when the button is pressed, the first status message does not appear either in the debug console, or on the UI. Doing a bit of experimentation, I added a pseudo sleep function that I call just prior to calling _fib(). This sometimes causes the first setState() call to work properly - invoking build(). The longer I make the sleep, the more often it works. (I'm using values from a few milliseconds up to a full second).
So my question are: What am I doing wrong? and What's the right way to do this? Using the pseudo sleep is obviously not the correct solution.
Other, probably not too relevant info: My dev environment is Android Studio 3.1.2 on a Win10 machine. Using Android SDK 27.0.3, with Flutter beta 0.3.2. My target device is the emulator for a pixel2 running Android 8.1. Also, sorry if my lack of 'new' keywords is off-putting, but from what I read in Dart 2 release notes, it's not usually necessary now.
import 'package:flutter/material.dart';
import "dart:async";
import "dart:math";
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Debug Toy',
home: MyWidget(),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
String _status = "Initialized";
final rand = Random();
Future sleep1() async {
return new Future.delayed(const Duration(milliseconds: 100),() => "1");
}
Future<Null> _resetState() async {
setState(() { _status = "State Reset"; });
}
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() { _status = "Calculating Fib of $num..."; });
//await sleep1(); // without this, the status above does not appear
int fn = await _fib(num);
// update the display
setState(() { _status = "Fib($num) = $fn"; });
}
Future<int> _fib(int n) async {
if (n<=0) return 0;
if ((n==1) || (n==2)) return 1;
return await _fib(n-1) + await _fib(n-2);
}
#override
Widget build(BuildContext context) {
print("Build called with status: $_status");
return Scaffold(
appBar: AppBar(title: Text('Flutter Debug Toy')),
body: Column(
children: <Widget>[
Container(
child: Row(children: <Widget>[
RaisedButton( child: Text("Reset"), onPressed: _resetState, ),
RaisedButton( child: Text("Calc"), onPressed: _calc, )
]),
),
Text(_status),
],
),
);
}
}
Let's start by going to one extreme and rewriting fib as fibSync
int fibSync(int n) {
if (n <= 0) return 0;
if (n == 1 || n == 2) return 1;
return fibSync(n - 1) + fibSync(n - 2);
}
and calling that
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() {
_status = "Calculating Fib of $num...";
});
//await Future.delayed(Duration(milliseconds: 100));
int fn = fibSync(num);
// update the display
setState(() {
_status = "Fib($num) = $fn";
});
}
The first setState just marks the Widget as needing to be rebuilt and (without the 'sleep') continues straight into the calculation, never giving the framework the chance to rebuild the Widget, so the 'Calculating' message isn't displayed. The second setState is called after the calculation and once again (redundantly) marks the Widget as needing to be rebuilt.
So, the execution order is:
Set status to Calculating, mark Widget as dirty
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (redundantly)
Framework finally gets chance to rebuild; build method is called
When we uncomment the 'sleep', the execution order changes to
Set status to Calculating, mark Widget as dirty
'Sleep', allowing the framework to call build
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (again)
Framework calls build
(As an aside, note how the synchronous fib calculation is an order of magnitude faster because it doesn't have to do all the microtask scheduling.)
Let's re-consider the async calculation. What's the motivation of making it async? So that the UI remains responsive during the calculation? As you've seen, that doesn't achieve the desired effect. You still only have one thread of execution, and you aren't allowing any gaps in execution for callbacks and rendering to occur. Sleeping for 100ms is not compute bound, so drawing etc can occur.
We use async functions to wait for external events, like replies from web servers, where we don't have anything to do until the reply arrives, and we can use that time to keep rendering the display, reacting to gestures, etc.
For compute bound stuff, you need a second thread of execution which is achieved with an Isolate. An isolate has its own heap, so you have to pass it its data, it works away in its own space, then passes back some results. You can also stop it, if it's taking too long, or the user cancels, etc.
(There are much less computationally expensive ways to calculate fibs, but I guess we're using the recursive version as a good example of an O(n^2) function, right?)