I am having trouble getting a stream with a list of objects to populate in a ViewModel.
Load an asynchronous Stream<List<Habit>> from a Firestore Service
in a DailyViewModel.
Call a transform method to turn that stream into a Stream<List<HabitCompletionViewModel>> that looks at a specific
instance of each of my habits for today, and creates that instance if one doesn't exist. There are a few component pieces to this:
For each habit in the initial stream, run a private method that checks if there is an instance of the habit for today, and initializes one if not. This is an asynchronous method because it calls back to the database to update the habit with the new instance.
Then find the instance for today, and return a HabitCompletionViewModel with the habit and today's instance.
Map these to a list.
That new stream is set in a property in the DailyViewModel as todaysHabits.
todaysHabits is called as the stream in a StreamBuilder in the DailyView widget.
The issue I am running into is that I know a completion for today is being found.
I am very fuzzy on what can/should be called as asynchronous code, and whether I'm using correct async/async* return/yield statements in my code, especially since I am trying to kick this process off as part of my constructor function for the DailyViewModel. I've used a bunch of print comments and it seems like everything is running, but the todaysHabits in my ViewModel is always set to null, and the StreamBuilder doesn't return any items.
Is there anything off in my code that could be causing this?
The DailyViewModel has this todaysHabits property, which is loaded near the bottom of the constructor function:
late Stream<List<HabitCompletionViewModel>> todaysHabits;
DailyViewModel({required WeekDates week}) {
_log.v('initializing the daily viewmodel');
_week = week;
_habitService
.loadActiveHabitsByUser(_loginAndUserService.loggedInUser!.id!)
.then(
(activeUserHabits) {
todaysHabits = _getTodaysHabits(activeUserHabits);
_log.v('todaysHabits has a length of ${todaysHabits.length}');
},
);
setBusy(false);
}
That constructor calls this _getTodaysHabits function which is supposed to convert that Stream<List<Habit>> into a Stream<List<HabitCompletionViewModel>>:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _updateHabitWithNewCompletions(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
}
And my view (which is used the Stacked package to display the contents of the ViewModel and update accordingly) contains this StreamBuilder that should be returning a list of tiles for each HabitCompletionViewModel:
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
Based on pskink's comment, I made the following updates that seem to work. (There is a slight lag when I switch to that view), but it is now showing the correct data.
Basically seems like the issue was that my previous code was returning a list of futures, instead of just a list of HabitCompletionViewModels, and using Future.wait waits for all those to complete.
Pulled out the mapping from List to List into a separate sub-method (here is the main method):
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap(
(habitsList) async => await _mapHabitsToViewModel(habitsList));
}
Updated that sub-method so it first returns a List<Future>, and then uses Future.wait to wait for those to complete as HabitCompletionViewModels before returning that new list:
Future<List<HabitCompletionViewModel>> _mapHabitsToViewModel(
List<Habit> habitsList) async {
List<Future<HabitCompletionViewModel>> newList =
habitsList.map((habit) async {
HabitCompletion completion = habit.completions!.firstWhere((completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
}).toList();
List<HabitCompletionViewModel> transformed = await Future.wait(newList);
return transformed;
}
I am using GetX and I have following codes.
RxList<Outlets?> listPharmacyOutlets = <Outlets>[].obs;
RxList listOutletCoordinates = [].obs;
#override
void onInit() async{
super.onInit();
await getPharmacyOutlets();
}
Future getPharmacyOutlets() async{
Map params = {
//some parameters
}
var res = await CommonApiProvider.getPharmacyOutlets(params)
var outlets = res.data;
int idx = 0;
listPharmacyOutlets.clear();
for(final outlet in outlets){
listPharmacyOutlets.add(Outlets(
"latitude": outlet.latitude,
"longitude": outlet.longitude,
"pharmacyId": outlet.id,
"outletName": outlet.name,
"address": null
));
//now populating address list to fetch addresses for all outlets
listOutletCoordinates.add({
"index": idx,
"latitude": outlet.latitude,
"longitude": outlet.longitude,
});
idx++;
}
//I cannot call getOutletAddresses() here because view will not be populated unless this completes.
}
Future getOutletAddresses() async {
await Future.forEach(listOutletCoordinates, (item) async {
var result = await CommonApiProvider.getOutletAddresses(item);
//from result update "address" in "listPharmacyOutlets" by the help of index property in item
});
}
Here is my view.
SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: List.generate(controller.listPharmacyOutlets.length, (index) {
var item = controller.listPharmacyOutlets[index];
return Container(
child: .......
);
})
),
);
What I want to do is, first method getPharmacyOutlets() call only fetches outlets from Rest api and when list of outlets is completed rendering in list,
call getOutletAddresses() method to fetch address from google's geocoding service against the supplied latitude and longitudes. After address has been fetched, i will
update each row in the rendered list of outlets.
I am fetching addresses and outlets separately because I don't want user to wait for all results to come.
Problem, I am not sure when to call getOutletAddresses() api so that the addresses are fetched only after outlets has been rendered in the view.
I have in my mind that this could be achieved using WidgetsBinding like this.
WidgetsBinding.instance.addPostFrameCallback((_) {
// here call method to fetch address from google service.
});
But I am not sure where to use this because I am using GetX, which means all my pages are stateless
Any help or any other better Idea will help me a lot.
Thanks
The best approach is to use the worker functions provided by getx controller like:
ever - is called every time the Rx variable emits a new value.
everAll - Much like ever , but it takes a List of Rx values Called every time its variable is changed. That's it.
once - is called only the first time the variable has been changed.
For the present situation,
You can use once or everand based on the required condition you can then call your desired function
So the best time to call the getOutletAddresses() function is after listOutletCoordinates is filled .
And in my opinion it's not about the view rendering and then calling a function. Instead its to focus on the data being fetched sequentially. Because the View updation is taken care by Getx Builder or Obx() provided by Getx once the data is properly fetched
If using ever:
ever(listOutletCoordinates,(value){
// Based on the value you can call the getOutletAdresses() function
});
But with once you get more flexibility , Use something like:
once(listOutletCoordinates,(value){
// Based on the value you can call the getOutletAdresses() function
},condition:() => listOutletCoordinates.isNotEmpty);
Alternatively:
You can use listen property to call function based on listening value.
Example:
listOutletCoordinates.listen((val){
// Based on the value you can call the getOutletAdresses() function
});
class MyPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: new Scaffold(
appBar: TabBar(
tabs: [
Tab(child: Text("MY INFORMATION",style: TextStyle(color: Colors.black54),)),
Tab(child: Text("WEB CALENDER",style: TextStyle(color: Colors.black54),)),
],
),
body:PersonalInformationBlocProvider(
movieBloc: PersonalInformationBloc(),
child: TabBarView(
children: [
MyInformation(),
new SmallCalendarExample(),
],
),
),
),
);
}
}
class MyInformation extends StatelessWidget{
// TODO: implement build
var deviceSize;
//Column1
Widget profileColumn(PersonalInformation snapshot) => Container(
height: deviceSize.height * 0.24,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius:
new BorderRadius.all(new Radius.circular(50.0)),
border: new Border.all(
color: Colors.black,
width: 4.0,
),
),
child: CircleAvatar(
backgroundImage: NetworkImage(
"http://www.binaythapa.com.np/img/me.jpg"),
foregroundColor: Colors.white,
backgroundColor: Colors.white,
radius: 40.0,
),
),
ProfileTile(
title: snapshot.firstName,
subtitle: "Developer",
),
SizedBox(
height: 10.0,
),
],
)
],
),
);
Widget bodyData(PersonalInformation snapshot) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
profileColumn(snapshot)
],
),
);
}
#override
Widget build(BuildContext context) {
final personalInformationBloc = PersonalInformationBlocProvider.of(context);
deviceSize = MediaQuery.of(context).size;
return StreamBuilder(
stream: personalInformationBloc.results,
builder: (context,snapshot){
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(),
);
return bodyData(snapshot.data);
}
);
}
}
I am using Bloc Pattern for retrieving data from Rest API (just called the whole object from JSON and parsed user name only). The Page consists of two tabs MyInformation and SmallCalendar. When the app runs the data are fetched correctly and everything is good. When I go to tab two and return to tab one then the whole screens in tab one goes to red showing error:
Bad state: Stream has already been listened to.
You should use the following.
StreamController<...> _controller = StreamController<...>.broadcast();
The most common form of Stream can be listened only once at a time. If you try to add multiple listeners, it will throw
Bad state: Stream has already been listened to
To prevent this error, expose a broadcast Stream. You can convert your stream to a broadcast using myStream.asBroadcastStream
This needs to be done inside your class that expose Stream. Not as parameter of StreamBuilder. Since asBroadcastStream internally listen to the original stream to generate the broadcast one, this imply you can't call this method twice on the same stream.
You could use broadcast, which allows to listen stream more than once, but it also prevents from listening past events:
Broadcast streams do not buffer events when there is no listener.
A better option is to use BehaviorSubject from rxdart package class as StreamController. BehaviorSubject is:
A special StreamController that captures the latest item that has been added to the controller, and emits that as the first item to any new listener.
The usage is as simple as:
StreamController<...> _controller = BehaviorSubject();
In my case, I was getting this error because the same line of code myStream.listen() was being called twice in the same widget on the same stream. Apparently this is not allowed!
UPDATE:
If you intend to subscribe to the same stream more than once, you should use a behavior subject instead:
// 1- Create a behavior subject
final _myController = BehaviorSubject<String>();
// 2- To emit/broadcast new events, we will use Sink of the behavior subject.
Sink<String> get mySteamInputSink => _myController.sink;
// 3- To listen/subscribe to those emitted events, we will use Stream (observable) of the behavior subject.
Stream<String> get myStream => _myController.stream;
// 4- Firstly, Listen/subscribe to stream events.
myStream.listen((latestEvent) {
// use latestEvent data here.
});
// 5- Emit new events by adding them to the BehaviorSubject's Sink.
myStreamInputSink.add('new event');
That's it!
However, there is one final important step.
6- We must unsubscribe from all stream listeners before a widget is destroyed.
Why? (You might ask)
Because if a widget subscribes to a stream, and when this widget is destroyed, the destroyed widget stream subscription will remain in app memory causing memory leaks and unpredictable behavior.:
_flush() {
_myController.close();
_myController = StreamController<String>();
}
###############################
###############################
Old Answer:
What fixed it for me is to both create a my stream controller as a broadcast stream controller:
var myStreamController = StreamController<bool>.broadcast();
AND
use stream as a broadcast stream:
myStreamController.stream.asBroadcastStream().listen(onData);
The problem was due to not disposing the controllers in bloc.
void dispose() {
monthChangedController.close();
dayPressedController.close();
resultController.close();
}
Just to sum up:
The main difference is broadcast() creates a Stream listenable for multiple sources but it needs to be listened for at least one source to start emitting items.
A Stream should be inert until a subscriber starts listening on it (using the [onListen] callback to start producing events).
asBroadcastStream turns an existing Stream into a multi listenable one but it doesn't need to be listened to start emitting since it calls onListen() under the hood.
I have had the same issue when I used a result of Observable.combineLatest2 for StreamBuilder into Drawer:
flutter: Bad state: Stream has already been listened to.
As for me, the best solution has added the result of this combine to new BehaviorSubject and listen new one.
Don't forget to listen old one !!!
class VisitsBloc extends Object {
Map<Visit, Location> visitAndLocation;
VisitsBloc() {
visitAndLocations.listen((data) {
visitAndLocation = data;
});
}
final _newOne = new BehaviorSubject<Map<Visit, Location>>();
Stream<Map<Visit, Location>> get visitAndLocations => Observable.combineLatest2(_visits.stream, _locations.stream, (List<vis.Visit> visits, Map<int, Location> locations) {
Map<vis.Visit, Location> result = {};
visits.forEach((visit) {
if (locations.containsKey(visit.skuLocationId)) {
result[visit] = locations[visit.skuLocationId];
}
});
if (result.isNotEmpty) {
_newOne.add(result);
}
});
}
I didn't use .broadcast because it slowed my UI.
I think not all of the answers take into account the situation where you do not want or simply can't use broadcast stream.
More often than not, you have to rely on receiving past events because the listener might be created later than the stream it listens to and it's important to receive such information.
In Flutter what will often happen is that widget listening to the stream ("listener") gets destroyed and built again. If you attempt to attach listener to the same stream as before, you will get this error.
To overcome this, you will have to manage your streams manually. I created this gist demonstrating how that can be done. You can also run this code on this dartpad to see how it behaves and play with it. I have used simple String ids to refer to specific StreamController instances but there might be better solutions too (perhaps symbols).
The code from the gist is:
/* NOTE: This approach demonstrates how to recreate streams when
your listeners are being recreated.
It is useful when you cannot or do not want to use broadcast
streams. Downside to broadcast streams is that it is not
guaranteed that your listener will receive values emitted
by the stream before it was registered.
*/
import 'dart:async';
import 'dart:math';
// [StreamService] manages state of your streams. Each listener
// must have id which is used in [_streamControllers] map to
// look up relevant stream controller.
class StreamService {
final Map<String, StreamController<int>?> _streamControllers = {};
Stream<int> getNamedStream(String id) {
final controller = _getController(id);
return controller.stream;
}
// Will get existing stream controller by [id] or create a new
// one if it does not exist
StreamController<int> _getController(String id) {
final controller = _streamControllers[id] ?? _createController();
_streamControllers[id] = controller;
return controller;
}
void push(String id) {
final controller = _getController(id);
final rand = Random();
final value = rand.nextInt(1000);
controller.add(value);
}
// This method can be called by listener so
// memory leaks are avoided. This is a cleanup
// method that will make sure the stream controller
// is removed safely
void disposeController(String id) {
final controller = _streamControllers[id];
if (controller == null) {
throw Exception('Controller $id is not registered.');
}
controller.close();
_streamControllers.remove(id);
print('Removed controller $id');
}
// This method should be called when you want to remove
// all controllers. It should be called before the instance
// of this class is garbage collected / removed from memory.
void dispose() {
_streamControllers.forEach((id, controller) {
controller?.close();
print('Removed controller $id during dispose phase');
});
_streamControllers.clear();
}
StreamController<int> _createController() {
return StreamController<int>();
}
}
class ManagedListener {
ManagedListener({
required this.id,
required StreamService streamService,
}) {
_streamService = streamService;
}
final String id;
late StreamService _streamService;
StreamSubscription<int>? _subscription;
void register() {
_subscription = _streamService.getNamedStream(id).listen(_handleStreamChange);
}
void dispose() {
_subscription?.cancel();
_streamService.disposeController(id);
}
void _handleStreamChange(int n) {
print('[$id]: streamed $n');
}
}
void main(List<String> arguments) async {
final streamService = StreamService();
final listener1Id = 'id_1';
final listener2Id = 'id_2';
final listener1 = ManagedListener(id: listener1Id, streamService: streamService);
listener1.register();
streamService.push(listener1Id);
streamService.push(listener1Id);
streamService.push(listener1Id);
await Future.delayed(const Duration(seconds: 1));
final listener2 = ManagedListener(id: listener2Id, streamService: streamService);
listener2.register();
streamService.push(listener2Id);
streamService.push(listener2Id);
await Future.delayed(const Duration(seconds: 1));
listener1.dispose();
listener2.dispose();
streamService.dispose();
}
For those of you running into this while doing Future.asStream(), you'll need Future.asStream().shareReplay(maxSize: 1) to make it a broadcast/hot stream.
For me defining my stream as a global variable worked
Stream infostream (was inside the ...State in a stateful widget i defined it outside the widget and it worked
(not sure if the best solution but give it a try)
Call .broadcast() on your stream controller
example:
StreamController<T> sampleController =
StreamController<T>.broadcast();
StreamSplitter.split() from the async can be used for this use case
import 'package:async/async.dart';
...
main() {
var process = Process.start(...);
var stdout = StreamSplitter<List<int>>(process.stdout);
readStdoutFoo(stdout.split());
readStdoutBar(stdout.split());
}
readStdoutFoo(Stream<List<int>> stdout) {
stdout.transform(utf8.decoder)...
}
readStdoutBar(Stream<List<int>> stdout) {
stdout.transform(utf8.decoder)...
}
In my case I was Using the Package Connectivity while on flutter web.
Commenting all Connectivity calls solved the issue.
I'm now just using Connectivity while only on Android/iOS.
So maybe check your Packages im you are using some packages that have some issues on Web in case you are developing for web.
Hopefully I could help someone with this Information.
This is a problem for the provider, I solved it by change provider initialization
Eg
locator.registerSingleton<LoginProvider>(LoginProvider());
TO
locator.registerFactory(() => TaskProvider());
Where locator is
GetIt locator = GetIt.instance;
This could help any other person. In my case i was using two StreamBuilder one in each tab. So when i swipe to above the tab and back. The other stream was already listened so i get the error.
What i did was to remove the StreamBuilder from the tabs and put it on top. I setState each time there is a change. I return an empty Text('') to avoid showing anything. I hope this methods
For other case scenarios. Watch out if you are somehow using a stream inside a stateless class. This is one of the reasons you get the above error.
Convert the stateless class to stateful and call init and dispose method on the streamController:
#override
void initState() {
super.initState();
YourStreamController.init();
}
#override
void dispose() {
YourStreamController.dispose();
super.dispose();
}
make sure you dispose controllers!
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
I was getting this error when navigating away and then back to the view listening to the stream because I was pushing a new instance of the same view into the Navigator stack, which effectively ended up creating a new listener even though it was the same place in code.
Specifically and in more detail, I had a ListItemsView widget which uses StreamBuilder to show all the items in a stream. User taps on the "Add Item" button which pushes the AddItemView in the Navigator stack, and after submitting the form, the user is brought back to the ListItemsView, where the "Bad state: Stream has already been listened to." error happens.
For me the fix was to replace Navigator.pushNamed(context, ListItemsView.routeName) with Navigator.pop(context). This effectively prevents the instantiation of a new ListItemsView (as the second subscriber to the same stream), and just takes the user back to the previous ListItemsView instance.
I experienced this because, i was using a stream builder to create a list for tabs of tabview and anytime i switch tabs and come back to the previous i get this error. "wrapping the stream builder with a builder widget" did the magic for me.
i have experienced this, always closing the streamcontroller worked for me.
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.
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?)