Request data from a stream one item at time? - dart

I'm trying to fetch data from a REST endpoint that serves a paginated response. On a button click in flutter, I would like to get the next item in the response. I think I want to use a Stream that abstracts away the paginated nature of the request, and automatically polls the next result.
Something like this dartish pseudo-code:
Stream<String> nextUrl(int lastPage=0)
{
// Get paginated response
batch = server.getResponse(lastPage)
for (item in batch)
{
yield item;
}
// Recurse automatically
// Not a problem if the stream is suspended
// after every item request. A big problem
// if the stream never pauses.
await for (String item in nextUrl(lastPage++)
{
yield item;
}
}
// In flutter
class WidgetState extends state<MyPage> {
Stream<String> urlStream = new nextUrl(0);
String currentItem;
...
#override
Widget build(BuildContext context) {
return new InkWell(
onTap: () {
(() async {
// This is the call I haven't figured out.
await item = urlStream().getNext();
setState(() { currentItem = item; });
})();
}
);
}
}
I get the feeling that maybe something like Stream.getNext() doesn't exist? That I should be pausing / unpausing this stream? But I'm not sure how that would return only a single item at a time.

The async package provide StreamQueue that allows to do that
var events = new StreamQueue<String>(yourStream);
var first = await events.next;
while (...) {
...
first = await events.next;
}

Related

Is it possible to add an item to a List contained in a Stream in Dart?

Problem
I have a Stream<List> which is being listened to in several classes and I need all the classes to receive the updated stream once a value is added to the list in the Stream.
What I have tried
void main() {
StreamedList<String> dataStream = StreamedList();
dataStream.data.listen((list) => print(list));
dataStream.updateList(['Apple', 'Orange']);
dataStream.addToList('Mango'); // This is what I want to do
}
This is the code for StreamList class
class StreamedList<T> {
StreamController<List<T>> _controller = StreamController.broadcast();
Stream<List<T>> get data => _controller.stream;
void updateList(List<T> list) {
_controller.sink.add(list);
}
void addToList(T value) {
// Is it possible to do this?
// List<T> dataList = await _controller.data;
// dataList.add(value);
// updateList(dataList);
}
void dispose() {
_controller.close();
}
}
I have tried different APIs from the dart:async library including Stream.first, etc. which return a Future<List<T>>. But the problem is that this Future resolves only after something is added to the Stream later (eg. by calling the StreamedList.updateList function).
Question
How do I add a single value to the List inside the Stream?
You are misunderstanding what a Stream does. It doesn't "contain" data. It merely accepts data on one end (the sink) and propagates it out the other end (the stream), and any listeners gain access to the streamed object. Trying to "insert an item in a list within the stream" doesn't make any conceptual sense.
If you want to push a list with an additional item, take the old list, append the item to it, then re-add the list to the stream.
class StreamedList<T> {
StreamController<List<T>> _controller = StreamController.broadcast();
Stream<List<T>> get data => _controller.stream;
List<T> _list = [];
void updateList(List<T> list) {
_list = list;
_dispatch();
}
void addToList(T value) {
_list.add(value);
_dispatch();
}
void _dispatch() {
controller.sink.add(_list);
}
void dispose() {
_list = null;
_controller.close();
}
}
If you wanted to be doubly safe, you could recreate the list after every addToList, since if a listener captured the list elsewhere and modified its contents, that would affect _list as well.
void addToList(T value) {
_list = [..._list, value];
_dispatch();
}

Future.then() is executed too early

I have a method which is supposed to return a Future, containing a list of groups.
This works fine as I can loop the list of groups in that method itself, but somehow list is returned before it could be filled. Surely this is an error on my part but I can't seem to grasp what I'm doing wrong.
Future< List<GroupData> > getGroups(String uniqueUserID) async
List<GroupData> groups = new List<GroupData>();
try {
var result = Firestore.instance
.collection("groups")
.where("members", arrayContains: uniqueUserID);
result.snapshots()
.listen (
(data) {
// Handle all documents one by one
for (DocumentSnapshot ds in data.documents)
{
List<String> members = new List<String>();
for (dynamic member in ds.data['members'])
{
members.add( member );
}
groups.add( new GroupData(ds.data['name'], ds.data['description'], ds.documentID.toString(), ds.data['admin'], members) );
}
}
);
} catch (exception)
{
print ('Something went wrong while fetching the groups of user ' + uniqueUserID + ' !');
}
return groups;
}
This method is being called using the method Future.then() but the list is empty while there should be several resuls (and there are, I can loop all items in the list in the above method and access/print their data). What am I missing?
The execution of your function is never locked. It doesn't wait until your listened stream finished.
There are a few solutions:
change stream.listen into await for (final item in stream)
add an await stream.done
Example:
before:
Stream<List<T>> stream;
stream.listen((list) {
for (final item in list) {
print(item);
}
});
after:
await for (final list in stream) {
for (final item in list) {
print(item);
}
}

Return changes from BLoC after changes in the Firestore

I'm doing some experiments with Flutter and met some problem. Basically, I'm using Flutter with BLoC pattern and Firestore as a remote store. I have something like this:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
....
#override
Stream<HomeState> mapEventToState(HomeState state, HomeEvent event) async* {
if(event is HomeEventStartTrackingWherlos) {
Firestore.instance.collection('objects_collection')
.snapshots()
.listen((data) {
var collection = List<MyObject>();
data.documents.forEach((document) {
MyObject mo = MyObject(document["name"], document["description"]);
collection.add(mo);
});
// return HomeState.reloadedCollection(collection);
});
}}
...
}
In line:
// return HomeState.reloadedCollection(collection);
I want to return new state with objects collection. If I simply return new state, then my build method in StatefulWidget isn't called. Do you have any suggestion how can I pass new state with collection after every change in the Firestore collection and based on that rebuild widget?

Is there a way to load async data on InitState method?

I'm a looking for a way to load async data on InitState method, I need some data before build method runs. I'm using a GoogleAuth code, and I need to execute build method 'till a Stream runs.
My initState method is:
#override
void initState () {
super.initState();
_googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) {
setState(() {
_currentUser = account;
});
});
_googleSignIn.signInSilently();
}
I will appreciate any feedback.
You can create an async method and call it inside your initState
#override
void initState () {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
_asyncMethod();
});
}
_asyncMethod() async {
_googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) {
setState(() {
_currentUser = account;
});
});
_googleSignIn.signInSilently();
}
As of now using .then notation seems to work:
// ...
#override
initState() {
super.initState();
myAsyncFunction
// as suggested in the comment
// .whenComplete() {
// or
.then((result) {
print("result: $result");
setState(() {});
});
}
//...
Method 1 : You can use StreamBuilder to do this. This will run the builder method whenever the data in stream changes.
Below is a code snippet from one of my sample projects:
StreamBuilder<List<Content>> _getContentsList(BuildContext context) {
final BlocProvider blocProvider = BlocProvider.of(context);
int page = 1;
return StreamBuilder<List<Content>>(
stream: blocProvider.contentBloc.contents,
initialData: [],
builder: (context, snapshot) {
if (snapshot.data.isNotEmpty) {
return ListView.builder(itemBuilder: (context, index) {
if (index < snapshot.data.length) {
return ContentBox(content: snapshot.data.elementAt(index));
} else if (index / 5 == page) {
page++;
blocProvider.contentBloc.index.add(index);
}
});
} else {
return Center(
child: CircularProgressIndicator(),
);
}
});
}
In the above code StreamBuilder listens for any change in contents, initially its an empty array and shows the CircularProgressIndicator. Once I make API call the data fetched is added to contents array, which will run the builder method.
When the user scrolls down, more content is fetched and added to contents array which will again run builder method.
In your case only initial loading will be required. But this provides you an option to display something else on the screen till the data is fetched.
Hope this is helpful.
EDIT:
In your case I am guessing it will look something like shown below:
StreamBuilder<List<Content>>(
stream: account, // stream data to listen for change
builder: (context, snapshot) {
if(account != null) {
return _googleSignIn.signInSilently();
} else {
// show loader or animation
}
});
Method 2: Another method would be to create an async method and call it from you initState() method like shown below:
#override
void initState() {
super.initState();
asyncMethod();
}
void asyncMethod() async {
await asyncCall1();
await asyncCall2();
// ....
}
Create anonymous function inside initState like this:
#override
void initState() {
super.initState();
// Create anonymous function:
() async {
await _performYourTask();
setState(() {
// Update your UI with the desired changes.
});
} ();
}
#override
void initState() {
super.initState();
asyncInitState(); // async is not allowed on initState() directly
}
void asyncInitState() async {
await yourAsyncCalls();
}
Previous Answer!!
You can set a Boolean value like loaded and set it to true in your listen function and make your build function return your data when loaded is set to true otherwise just throw a CircularProgressIndicator
Edited --
I would not suggest calling setState in a method you call in initState. If the widget is not mounted while the setState is called (as the async operation completes) an error will be reported. I suggest you use a package after_layout
Take a look at this answer for better understanding setState in initState : https://stackoverflow.com/a/53373017/9206337
This post will give you an idea to know when the app finishes the build method. So that you can wait for your async method to setState after widget is mounted : https://stackoverflow.com/a/51273797/9206337
You can create an async method and call it inside your initState
#override
void initState() {
super.initState();
asyncMethod(); ///initiate your method here
}
Future<void> asyncMethod async{
await ///write your method body here
}
Per documentation at https://pub.dev/packages/provider
initState() {
super.initState();
Future.microtask(() =>
context.read<MyNotifier>(context).fetchSomething(someValue);
);
}
Sample code:
#override
void initState() {
super.initState();
asyncOperation().then((val) {
setState(() {});
print("success");
}).catchError((error, stackTrace) {
print("outer: $error");
});
//or
asyncOperation().whenComplete(() {
setState(() {});
print("success");
}).catchError((error, stackTrace) {
print("outer: $error");
});
}
Future<void> asyncOperation() async {
await ... ;
}
As loading or waiting for initial state is a (generally) aone off event FutureBuilder would seem to be a good option as it blocks once on an async method; where the async method could be the loading of json config, login etc. There is an post on it [here] in stack.(Flutter StreamBuilder vs FutureBuilder)
How about this?
#override
void initState() {
//you are not allowed to add async modifier to initState
Future.delayed(Duration.zero,() async {
//your async 'await' codes goes here
});
super.initState();
}
initState() and build cannot be async; but in these, you can call a function that is async without waiting for that function.
#override
void initState() {
super.initState();
_userStorage.getCurrentUser().then((user) {
setState(() {
if (user.isAuthenticated) {
Timer.run(() {
redirectTo();
});
}
});
});
}
void redirectTo() {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) => new ShopOrders()));
}
I would strongly suggest using a FutureBuilder. It takes care of executing the async function and building the widget according to the result!
Here's a link to a short intro video and the documentation.
Code Example:
Future<void> initControllers() async {
for (var filePath in widget.videoFilePaths) {
var val = VideoPlayerController.file(File(filePath));
await val.initialize();
controllers.add(val);
}
}
#override
Widget build(BuildContext context) {
FutureBuilder(
future: initControllers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return YourWidget();
} else {
return const ProgressIndicator();
}
},
));}
Tried all suggestions, none would keep my build from starting after the async method that I need in initState() finish, except one: the trick of having a a bool variable in the State class (let's call it _isDataLoaded) that is initialized to false upon definition, set to true inside a setState() that is invoked when the async function finishes inside initState(). In the build, condition a CircleProcessIndicator() or your Widget depending on the value of this variable.
I know it's dirty because it could break the build, but honestly nothing else that would make more sense -such as running super.initState() upon completion of the async function- has worked for me.
I came here because I needed to fetch some files from FTP on program start. My project is a flutter desktop application. The main thread download the last file added to the FTP server, decrypts it and displays the encrypted content, this method is called from initState(). I wanted to have all the other files downloaded in background after the GUI shows up.
None of the above mentioned methods worked. Constructing an Isolate is relatively complex.
The easy way was to use the "compute" method:
move the method downloading all files from the FTP out of the class.
make it an int function with an int parameter (I do not use the int parameter or the result)
call it from the initState() method
In that way, the GUI shows and the program downloads the files in background.
void initState() {
super.initState();
_retrieveFileList(); // this gets the first file and displays it
compute(_backgroundDownloader, 0); // this gets all the other files so that they are available in the local directory
}
int _backgroundDownloader(int value) {
var i = 0;
new Directory('data').createSync();
FTPClient ftpClient = FTPClient('www.guckguck.de',
user: 'maxmusterman', pass: 'maxmusterpasswort');
try {
ftpClient.connect();
var directoryContent = ftpClient.listDirectoryContent();
// .. here, fileNames list is reconstructed from the directoryContent
for (i = 0; i < fileNames.length; i++) {
var dirName = "";
if (Platform.isLinux)
dirName = 'data/';
else
dirName = r'data\';
var filePath = dirName + fileNames[i];
var myDataFile = new File(filePath);
if (!myDataFile.existsSync())
ftpClient.downloadFile(fileNames[i], File(filePath));
}
} catch (err) {
throw (err);
} finally {
ftpClient.disconnect();
}
return i;
I have used timer in initState
Timer timer;
#override
void initState() {
super.initState();
timer = new Timer.periodic(new Duration(seconds: 1), (Timer timer) async {
await this.getUserVerificationInfo();
});
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
getUserVerificationInfo() async {
await someAsyncFunc();
timer.cancle();
}

Distinguish between onClick and onDoubleClick on same element to perform different actions in Dart

I want to do the ill-advised and place both an onClick and onDoubleClick on the same element with each type of event resulting in a different action. Specifically on an image, click to advance to the next image, double-click to toggle fullscreen.
Naturally I get two clicks followed by a double-click (though I understand that some browsers only fire one click before the double-click).
I had thought to make it easy on myself and place each event into a buffer (List) - or rather to add the event.type string to a list, then, after a suitable elapse of time, say 250 or 300 milliseconds examine the last item in the buffer and if doubleclick then go fullscreen else advance the length of the list.
I have found that the list only ever has one item, and I have not worked out how to get the timer to work..
Amongst my attempts was this one:
void catchClickEvents(Event e) {
var eventTypes = new List<String>();
eventTypes.add(e.type);
Duration duration = const Duration(milliseconds: 300);
var timeout = new Timer(duration, () => processEvents(eventTypes));
}
void processEvents(List eTypes) {
// just to see what is going on...
print(eTypes);
}
this results in this output printed to the console:
[click]
[click]
[dblclick]
rather than
[click, click, dblclick]
If I slow it down there is a clear delay before those three event types are printed together
So...
The bigger question is
'What is the darty way to distiguish between single and double-click and perform a different action for each?'
The other questions are:
How do I fill a buffer with successive events (and later clear it down) - or even how do I use Dart's Stream of events as a buffer...
What is the real way timeout before examining the contents of the buffer?
(and I guess the final question is 'should I abandon the effort and settle for a conventional set of buttons with glyph-icons?'!)
I'm not sure if IE still has the event sequence explained here (no 2nd click event)
https://stackoverflow.com/a/5511527/217408
If yes you can use a slightly deviated variant of Roberts solution:
library app_element;
import 'dart:html' as dom;
import 'dart:async' as async;
Duration dblClickDelay = new Duration(milliseconds: 500);
async.Timer clickTimer;
void clickHandler(dom.MouseEvent e, [bool forReal = false]) {
if(clickTimer == null) {
clickTimer = new async.Timer(dblClickDelay, () {
clickHandler(e, true);
clickTimer = null;
});
} else if(forReal){
print('click');
}
}
void dblClickHandler(dom.MouseEvent e) {
if(clickTimer != null) {
clickTimer.cancel();
clickTimer = null;
}
print('doubleClick');
}
void main() {
dom.querySelector('button')
..onClick.listen(clickHandler)
..onDoubleClick.listen(dblClickHandler);
}
or just use Roberts solution with the mouseUp event instead of the click event.
The problem is that your variable is not global.
var eventTypes = new List<String>();
void catchClickEvents(Event e) {
eventTypes.add(e.type);
Duration duration = const Duration(milliseconds: 300);
var timeout = new Timer(duration, () => processEvents(eventTypes));
}
void processEvents(List eTypes) {
print(eTypes);
}
There also is e.detail that should return the number of the click. You can use that, if you don't need the Internet Explorer. The problem with your list is that it grows and never gets cleared.
Let's take into account what we know: You get click events and at somepoint you have doubleclicks.
You could use a click counter here. (Or use e.detail) to skip the second click event. So you only have click and dblclick.
Now when you get a click event, you create a new timer like you did before and run the click action. If you get the dblclick event you simply run you action. This could like this:
DivElement div = querySelector('#div');
Timer timeout = null;
div.onClick.listen((MouseEvent e) {
if(e.detail >= 2) {
e.preventDefault();
} else {
if(timeout != null) {
timeout.cancel();
}
timeout = new Timer(new Duration(milliseconds: 150), () => print('click'));
}
});
div.onDoubleClick.listen((MouseEvent e) {
if(timeout != null) {
timeout.cancel();
timeout = null;
}
print('dblclick');
});
This is the example code that works for me. If you can't rely on e.detail just us a counter and reset it after some ms after a click event.
I hope this helps you.
Regards, Robert
Your page should react on user inputs as fast as possible. If you wait to confirm double click - your onClick will become much less responsive. You can hide the problem by changing visual state of the element(for example, playing animation) after first click but it can confuse user. And it gets even worse with handheld. Also if element has to react only on onClick event, you can "cheat" and listen to onmousedown instead - it will make your UI much more responsive.
On top of all this, double click, from client to client, may have noticeably different trigger time interval and it isn't intuitive, for user, that you can double click something. You will have to bloat your interface with unnecessary hints.
edit: Added my solution. It should be fairly extensible and future proof.
import 'dart:html';
import 'dart:async';
import 'dart:math';
//enum. Kinda... https://code.google.com/p/dart/issues/detail?id=88
class UIAction {
static const NEXT = const UIAction._(0);
static const FULLSCREEN = const UIAction._(1);
static const ERROR = const UIAction._(2);
final int value;
const UIAction._(this.value);
}
/**
*[UIEventToUIAction] transforms UIEvents into corresponding UIActions.
*/
class UIEventToUIAction implements StreamTransformer<UIEvent, UIAction> {
/**
* I use "guesstimate" value for [doubleClickInterval] but you can calculate
* comfortable value for the user from his/her previous activity.
*/
final Duration doubleClickInterval = const Duration(milliseconds: 400);
final StreamController<UIAction> st = new StreamController();
Stream<UIAction> bind(Stream<UIEvent> originalStream) {
int t1 = 0,
t2 = 0;
bool isOdd = true;
Duration deltaTime;
originalStream.timeout(doubleClickInterval, onTimeout: (_) {
if ((deltaTime != null) && (deltaTime >= doubleClickInterval)) {
st.add(UIAction.NEXT);
}
}).forEach((UIEvent uiEvent) {
isOdd ? t1 = uiEvent.timeStamp : t2 = uiEvent.timeStamp;
deltaTime = new Duration(milliseconds: (t1 - t2).abs());
if (deltaTime < doubleClickInterval) st.add(UIAction.FULLSCREEN);
isOdd = !isOdd;
});
return st.stream;
}
}
void main() {
//Eagerly perform time consuming tasks to decrease the input latency.
Future NextImageLoaded;
Future LargeImageLoaded;
element.onMouseDown.forEach((_) {
NextImageLoaded = asyncActionStub(
"load next image. Returns completed future if already loaded");
LargeImageLoaded = asyncActionStub(
"load large version of active image to show in fullscreen mode."
"Returns completed future if already loaded");
});
Stream<UIEvent> userInputs = element.onClick as Stream<UIEvent>;
userInputs.transform(new UIEventToUIAction()).forEach((action) {
switch (action) {
case UIAction.FULLSCREEN:
LargeImageLoaded.then( (_) =>asyncActionStub("fullscreen mode") )
.then((_) => print("'full screen' finished"));
break;
case UIAction.NEXT:
NextImageLoaded.then( (_) =>asyncActionStub("next image") )
.then((_) => print("'next image' finished"));
break;
default:
asyncActionStub("???");
}
});
}
final DivElement element = querySelector("#element");
final Random rng = new Random();
final Set performed = new Set();
/**
*[asyncActionStub] Pretends to asynchronously do something usefull.
* Also pretends to use cache.
*/
Future asyncActionStub(String msg) {
if (performed.contains(msg)) {
return new Future.delayed(const Duration(milliseconds: 0));
}
print(msg);
return new Future.delayed(
new Duration(milliseconds: rng.nextInt(300)),
() => performed.add(msg));
}

Resources