Flutter Rate Limit / Debouncer Design Implementation - flutter

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.

Related

Flutter Riverpod, how to set minimut loading state time?

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();
});
}
}

flutter: how to use 'await for' to wait for other BLoC event to finish

on screen init, I am loading my data via an externalData_bloc. On the same screen I am also rendering another widget controlled with internalData_bloc for user input which depends on the number of imported records (how many rows are needed). Of course, the rendering is faster than the data load, so I get null rows needed.
I found this nice question & answer, however, I do not get it to work. The solution says
Future loginProcess() async {
await for (var result in _userBloc.state) {
if (result.status == Status.finished) {
return;
}
}
}
I am within my repository. In here, I am also storing the external data in a variable. So within the repository class I have my function to get the number of records (properties are stored in the repository, and I want to return its length).
Future<int> getNoOfProperties(int problem) async {
LogicGraphsPStmntBloc bloc = LogicGraphsPStmntBloc();
Future propertiesLoad() async {
await for (var s in bloc) {
if (s == LogicGraphsPStmntLoadSuccess) {
return;
}
}
}
propertiesLoad();
return properties.length;
}
But no matter what I try to put in for await for (var result in XXXX) or if (s.YYY == ZZZ), it doesn't work. My understanding is, XXXX needs to be a stream which is why I used bloc = LogicGraphsPStmntBloc(). But I guess this creates another instance than the one used in my widgets. LogicGraphsPStmntBloc doesn't work either, nor do states. bloc at least doesn't throw an error already in the editor, but besides the instance issue, the if statement throws an error, where in cubit.dart StreamSubscription<State> listen(... is called with cancelOnError . Anyone having some enlightenment?
Bloc uses Stream and has a continuous flow of data. You might want to listen for changes in data instead of a finished task. Using a StreamBuilder is one way of doing this.
StreamBuilder<User>(
stream: bloc.userState, // Stream from bloc
builder: (context, AsyncSnapshot<State> snapshot) {
if (snapshot.hasData) {
// check if auth status is finished
}
}
)

How to modify current bloc state like adding, updating, and deleting?

I have 2 screens, the first screen is to display a list of items. The second one is a form for creating a new item. On the server side, after each store, update, destroy action, I returns its value back to the client as a response. The goal here is to update the state without having to make a new request over the network and showing a loading screen over and over every time the user creating a new resource and going back to the screen containing the list.
But since the BlocBuilder is only depends on a specific event and state.data is only available inside an if (event is NameOfEvent) statement, I couldn't modify the current state to push the new value to it.
I found that using BlocListener combined with InitState and setState is somehow working, but I think it makes the state.data is useless inside the BlocBuilder also making the algorithm complicated for such a small task.
Is there any simpler way to achieve this condition?
child: BlocBuilder<EducationBloc, EducationState>(
builder: (context, state) {
if (state is EducationLoaded) {
return ListEducationData(educations: state.data);
}
if (state is EducationCreated) {
final education = state.data;
// i want to push [education] to <ListEducationData>
}
...
...
Create list to store data and use BlocListener so you don't need to change ui every state changes.
var _listEducations = List<EducationData>();
child : BlocListener<EducationBloc,EducationState>(
listener: (_,state){
if (state is EducationLoaded) {
setState(() { //update _listEducations
_listEducations.addAll(state.data);
});
}
},
child : Container()
)

perform a navigation inside the build method of a widget? - flutter

Fast question: how can I perform a navigation inside the build method of a widget?
I'm developing a Flutter App.
I use Bloc architecture.
I have screen with a create form. When the user presses a button, it calls a REST api. While the call is being executed I display a circular progress. When the progress ends I want the screen to be popped (using navigation).
To display the job status I use a Stream in the bloc and a StreamBuilder in the widget. So I want to do something like this:
return StreamBuilder<Job<T>>(
stream: jobstream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.jobStatus == JobStatus.progress)
// job being executed, display progress
return Center(child: CircularProgressIndicator());
else if (snapshot.data.jobStatus == JobStatus.success)
Navigator.pop(context); // Problem here!
return Center(child: CircularProgressIndicator());
} else {
return DisplayForm();
}
});
I have problems with the line: Navigator.pop(context);
Because navigation is not allowed during a build.
How can I do this navigation?
My currect dirty solution is using the following function, but its ugly:
static void deferredPop(BuildContext context) async{
Future.delayed(
Duration(milliseconds: 1),
() => Navigator.pop(context)
);
}
You can add a callback to be executed after the build method is complete with this line of code:
WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.pop(context));
'Because navigation is not allowed during a build.' that being said you should consider creating a separate function that will receive as an input something that you will take into consideration whether to pop that screen.

How to initialize and load data when moving to a screen in flutter?

I want to populate my lists by making API calls when moving to a screen in flutter. I tried calling the async function in the init state however it takes time to load the data of course, and that leads to a null error.
I tried using a future builder in my view. However, I want to make multiple API calls and I don't think that is possible with one future builder. And I would like to avoid having different future builders for each list.
Is there a neat way to do this?
Also is it advisable to load data and pass it on from the previous screen.. since I would like to avoid the loading time?
current code
FutureBuilder(
future: getAllData,
initialData: [],
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.isEmpty) return Center(child: CircularProgressIndicator(valueColor: new AlwaysStoppedAnimation<Color>(Colors.red),));
else {
return createNewTaskView(context, snapshot.data);
}
}),
init method
#override
void initState() {
this.getAllData = getSocieties();
}