0

My future method isn't waiting for my list to be created before it returns my list. It returns an empty list and then creates my list properly.

My code:

Future<List<Song>> getSongsFromPlaylist(int id) async {
    final playlist = await getPlaylist(id); // get the playlist by its id
    List<Song> list = []; // create an empty list for the songs

    await playlist.songs.forEach((songId) async { // loop through the song-ids from the playlist 
      Song song = await getSong(songId); // get the song by its id
      print(song.id);
      list.add(song); // add the song to the list of songs
      print('list: $list');

    });
    print('returned list: $list');
    return list;
  }

Output:

I/flutter (19367): returned list: []
I/flutter (19367): 1
I/flutter (19367): list: [Instance of 'Song']
I/flutter (19367): 2
I/flutter (19367): list: [Instance of 'Song', Instance of 'Song']
I/flutter (19367): 3
I/flutter (19367): list: [Instance of 'Song', Instance of 'Song', Instance of 'Song']
I/flutter (19367): 4
I/flutter (19367): list: [Instance of 'Song', Instance of 'Song', Instance of 'Song', Instance of 'Song']
I/flutter (19367): 5
I/flutter (19367): list: [Instance of 'Song', Instance of 'Song', Instance of 'Song', Instance of 'Song', Instance of 'Song']

How can I fix this? Thanks!

JakesMD
  • 1,646
  • 2
  • 15
  • 35

3 Answers3

3

Use Future.wait to parallelly execute getSong.

Future<List<Song>> getSongsFromPlaylist(int id) async {
  final playlist = await getPlaylist(id);
  return Future.wait<Song>(playlist.songs.map((songId) => getSong(songId)));
}

Much better than for loop(which only gets the song one after another).

This code may help to understand better: DartPad.

(Note: By clicking the link, the dartpad will automatically start running the code. If you click run button, you may see some unwanted behaviour. So don't click run button while the code being executed)

Crazy Lazy Cat
  • 13,595
  • 4
  • 30
  • 54
  • 1
    @Eugene the time gained by processing the requests in parallel is infinitely more benefic – Augustin R Feb 24 '20 at 13:40
  • Bear in mind, using this approach MAY cause [ConcurrentModificationError](https://api.flutter.dev/flutter/dart-core/ConcurrentModificationError-class.html) -- although this approach offers decent concurrency/performance, when using this method you're not allowed to alter the collection (List, Map, Set,...) by adding or removing items to/from it. This will invalidate the iterator and cause runtime exception `ConcurrentModificationError`. i.e. **Addition/Removal to the accessed collection (array, map, ...) must be done outside the futures** – om-ha Feb 24 '20 at 13:41
  • For more info regarding `ConcurrentModificationError` see: [question1](https://stackoverflow.com/questions/22409666/exception-concurrent-modification-during-iteration-instancelength17-of-gr), [question2](https://stackoverflow.com/questions/24732611/dart-polymer-breaking-on-exception-concurrent-modification-during-iteration), or [question3](https://stackoverflow.com/questions/54150583/concurrent-modification-during-iteration-while-trying-to-remove-object-from-a-li) – om-ha Feb 24 '20 at 13:42
  • 1
    @om-ha The `songs.map` produces the list of `Future`s, then that is being processed by `Future.wait` to get the list of result as output. I don't think any possible case of getting this `ConcurrentModificationError` error here. – Crazy Lazy Cat Feb 24 '20 at 13:54
  • actually I'm using similar approach as in your code, and I'm agreed about parallel requests but I don't see what's going on in getSong() so I gave the simpliest solution, if topic starter will change his mind and accepted you answer it's no problem, I'll delete my option –  Feb 24 '20 at 13:57
  • @CrazyLazyCat correct, your solution is error-proof. Because it's a map that returns futures without mutating any list.. Though any side-effects that mutate a list we'd iterate over anywhere in the app would cause this exception `ConcurrentModificationError ` to be thrown. By `mutate` I mean changing number of elements of the collection via addition/removal. If this happens and we access the mutated collection, above exception will be thrown. I'll further illustrate this in my upcoming answer. – om-ha Feb 24 '20 at 14:05
  • 1
    @Eugene Run this code [Dartpad](https://dartpad.dartlang.org/6b250102358433a6edb7ec78c9689803). You may understand what I mean by parallel. – Crazy Lazy Cat Feb 24 '20 at 14:15
  • ok, I can't remove my answer, leave you guys measuring who's got the best rack in the snack –  Feb 24 '20 at 14:15
  • @CrazyLazyCat they don't excute parallelly. – Ali Qanbari Feb 24 '20 at 14:40
  • 1
    @aligator they do, map is returning a list of futures that are yet to be executed. `Future.wait` executes them in parallel. – om-ha Feb 24 '20 at 14:41
  • Added an [answer](https://stackoverflow.com/a/60378673/10830091) explaining and illustrating the options we have for solving this. – om-ha Feb 24 '20 at 15:09
1

why don't you change forEach with just for loop?

for( int i = 0; i< playslist.songs.length; i++) {

}
Augustin R
  • 7,089
  • 3
  • 26
  • 54
  • This does the trick and returns a valid result as you desire it to be, but it's not parallel/concurrent enough -- it's a *sequential* approach more than a *concurrent* approach . I'll illustrate this in my upcoming answer. – om-ha Feb 24 '20 at 14:07
  • Added an [answer](https://stackoverflow.com/a/60378673/10830091) explaining and illustrating the options we have for solving this. – om-ha Feb 24 '20 at 15:09
0

Crazy Lazy Cat's answer is great and compact. But if you need further operations and can't use a map like that, then you'd have to go for a solution similar to your code. Though these methods has a caveat that lists/iterables shouldn't be modified from WITHIN the futures that are executed concurrently/asynchronously. Go to the end of my answer to understand this more.

Anyways as per the solution, in this case you have two options:

Option A.

Iterating over the list in a batch manner using Future.wait

In this case, all iterations would be executed in parallel (theoretically). This ensures utmost performance/concurrency.

It boils down to storing the futures before asynchronously executing them using Future.wait.

class Song {
  int id;
}

class Playlist {
  List<int> songIds;
}

Future<Playlist> getPlaylist(int id) async {
  return Playlist();
}

Future<Song> getSong(int songId) async {
  return Song();
}

/// Returns list of songs from a `Playlist` using [playlistId]
Future<List<Song>> getSongsFromPlaylist(int playlistId) async {
  /// Local function that populates [songList] at [songListIndex] with `Song` object fetched using [songId]
  Future<void> __populateSongList(
    List<Song> songList,
    int songListIndex,
    int songId,
  ) async {
    // get the song by its id
    Song song = await getSong(songId);
    print(song.id);

    // add the song to the pre-filled list of songs at the specified index to avoid `ConcurrentModificationError`
    songList[songListIndex] = song;

    print(
        'populating list at index $songListIndex, list state so far: $songList');
  } // local function ends here

  // get the playlist object by its id
  final playlist = await getPlaylist(playlistId);

  // create a filled list of pre-defined size to avoid [ConcurrentModificationError](https://api.flutter.dev/flutter/dart-core/ConcurrentModificationError-class.html)
  List<Song> songList = List<Song>.filled(playlist.songIds.length, null);

  // store futures and execute them in a batch manner using [Future.wait](https://api.dart.dev/stable/2.7.1/dart-async/Future/wait.html)
  List<Future<void>> songFutures = [];
  for (int listIndex = 0; listIndex < playlist.songIds.length; listIndex++) {
    final songId = playlist.songIds[listIndex];
    songFutures.add(__populateSongList(songList, listIndex, songId));
  }

  // execute multiple futures concurrently/in parallel
  List<void> songFuturesResult = await Future.wait(songFutures);
  /* ALSO VALID
    List<void> _ = await Future.wait(songFutures);
    await Future.wait(songFutures);
  */

  print('returned list: $songList');

  return songList;
}

Option B.

Iterating over the list sequentially

In this case, each iteration is awaited until the next is executed. Performance is not as good as option A since each iteration/invocation is awaited halting control flow, the following iteration/invocation won't start until the previous one finishes. This method is somehow safer than the previous one in regard to ConcurrentModificationError

For references only this block is different from the previous option

  // populate songList sequentially -- each iteration/song halted until the previous one finishes execution
  for (int listIndex = 0; listIndex < playlist.songIds.length; listIndex++) {
    final songId = playlist.songIds[listIndex];
    await __populateSongList(songList, listIndex, songId);
  }

But here is the full solution anyways:

class Song {
  int id;
}

class Playlist {
  List<int> songIds;
}

Future<Playlist> getPlaylist(int id) async {
  return Playlist();
}

Future<Song> getSong(int songId) async {
  return Song();
}

/// Returns list of songs from a `Playlist` using [playlistId]
Future<List<Song>> getSongsFromPlaylist(int playlistId) async {
  /// Local function that populates [songList] at [songListIndex] with `Song` object fetched using [songId]
  Future<void> __populateSongList(
    List<Song> songList,
    int songListIndex,
    int songId,
  ) async {
    // get the song by its id
    Song song = await getSong(songId);
    print(song.id);

    // add the song to the pre-filled list of songs at the specified index to avoid `ConcurrentModificationError`
    songList[songListIndex] = song;

    print(
        'populating list at index $songListIndex, list state so far: $songList');
  } // local function ends here

  // get the playlist object by its id
  final playlist = await getPlaylist(playlistId);

  // create a filled list of pre-defined size to avoid [ConcurrentModificationError](https://api.flutter.dev/flutter/dart-core/ConcurrentModificationError-class.html)
  List<Song> songList = List<Song>.filled(playlist.songIds.length, null);

  // populate songList sequentially -- each iteration/song halted until the previous one finishes execution
  for (int listIndex = 0; listIndex < playlist.songIds.length; listIndex++) {
    final songId = playlist.songIds[listIndex];
    await __populateSongList(songList, listIndex, songId);
  }

  print('returned list: $songList');

  return songList;
}

Explanation

Addition/Removal to the accessed collection (array, map, ...) must be done outside the futures, otherwise a runtime error will be thrown for concurrently modifying an iterable during iteration.

References

Solutions and discussions

om-ha
  • 3,102
  • 24
  • 37