Optymalizacja zadań asynchronicznych w Dart i Flutter - Future.wait()

Bardzo często podczas pisania wszelkiego rodzaju aplikacji, nie tylko tych mobilnych, spotkamy się z sytuacją, gdy w jednym momencie musimy wykonać kilka zadań, które zajmują dłuższy czas. Najczęściej dochodzi do takiej sytuacji, w przypadku pobierania danych z kilku endpointów API w jednej chwili, na przykład podczas synchronizacji danych użytkownika. Zobaczmy, co możemy zrobić, aby ten proces zdecydowanie usprawnić.

Częstym błędem w takich sytuacjach jest wykonywanie wszystkich działań jedno po drugim. W takiej sytuacji nasz kod może wyglądać w sposób następujący:

var responseDart = await http
  .get('https://www.googleapis.com/books/v1/volumes?q={dartlang}');
var responseFlutter = await http
  .get('https://www.googleapis.com/books/v1/volumes?q={flutter}');
var responseAndroid = await http
  .get('https://www.googleapis.com/books/v1/volumes?q={android}');
var responseIos = await http
  .get('https://www.googleapis.com/books/v1/volumes?q={ios}');

Widzimy tutaj kolejne żądania API odnośnie książek, każde dotyczące innego hasła. To, co powoduje, że powyższy kod nie jest optymalny, wynika z tego, że za każdym razem, aby wykonać kolejne żądanie, musimy odczekać aż poprzednie się wykona, chociaż w rzeczywistości nie potrzebujemy znać wcześniejszej odpowiedzi, aby wykonać kolejne odpytanie API.

Przyjmijmy w tym przypadku, że:

  • pierwsze żądanie wykona się w czasie 200 ms
  • drugie żądanie wykona się w czasie 150 ms
  • trzecie żądanie wykona się w czasie 300 ms
  • czwarte żądanie wykona się w czasie 250 ms
Kurs Poznaj Flutter!

Jesteś programistą i chcesz wejść na rynek aplikacji mobilnych? Lub programujesz już aplikacje na Android lub iOS, ale chcesz sprawić, żeby ten proces był szybki i przyjemny, a przy okazji zaoszczędzić na pracy nawet 50% czasu? Próbowałeś ukończyć jakiś tutorial, ale wszystko wydawało Ci się zbyt zawiłe lub szukasz czegoś w języku polskim? Albo w ogóle chcesz zacząć swoją przygodę z programowaniem i szukasz czegoś co da Ci szybko widoczne wyniki oraz będzie łatwe do nauki?

Wejdź na stronę Poznaj Flutter i dowiedz się więcej!

W tym przykładzie, aby obliczyć czas potrzebny na wykonanie tych wszystkich żądań, wystarczy wszystkie te czasy ze sobą zsumować. Znaczy to, że zostaną one wykonane w czasie 900 ms.

Więc jak to zoptymalizować? Z pomocą tutaj przychodzi funkcja Future.wait(), której zadaniem jest poczekanie na wykonanie się wszystkich zadań asynchronicznych, oraz zwrócenie listy zawierającej wyniki tych działań. Spróbujmy zobaczyć powyższy przykład napisany z wykorzystaniem Future.wait():

var responses = await Future.wait([
  http.get('https://www.googleapis.com/books/v1/volumes?q={dartlang}'),
  http.get('https://www.googleapis.com/books/v1/volumes?q={flutter}'),
  http.get('https://www.googleapis.com/books/v1/volumes?q={android}'),
  http.get('https://www.googleapis.com/books/v1/volumes?q={ios}')
]);

var responseDart = responses[0];
var responseFlutter = responses[1];
var responseAndroid = responses[2];
var responseIos = responses[3];

Biorąc pod uwagę wyżej wymienione czasy wykonywania się każdego z żądań, wykorzystanie Future.wait() sprawi, że wszystkie te żądania wykonają się w ciągu 300 ms. Ten czas tak naprawdę wynika z tego, że dokładnie tyle wynosi najdłuższy czas każdego z tych żądań.

Future.wait() przyjął listę zadań asynchronicznych, które zostały uruchomione w jednym momencie i oczekuje, aż wszystkie z nich zakończą się sukcesem. W tym przypadku wynikiem jest lista odpowiedzi w kolejności takiej, w jakiej żądania zostały przekazane, a nie w kolejności od najszybciej wykonanego.

Wiadome jest to, że w przypadku wykonywania każdego z tych działań może pojawić się błąd. W przypadku wystąpienia pierwszego błędu pozostałe zadania są anulowane oraz zostaje rzucony wyjątek.

Jednak co zrobić w przypadku, kiedy chcemy znać odpowiedzi żądań, które zakończyły się powodzeniem, a tylko ominąć te, w przypadku których pojawił się błąd? To również jest bardzo łatwe zadanie i wystarczy złapać wyjątek w wykonywanych zadaniach:

var responses = await Future.wait([
  http
    .get('https://www.googleapis.com/books/v1/volumes?q={dartlang}')
    .catchError((e) {}),
  http
    .get('https://www.googleapis.com/books/v1/volumes?q={flutter}')
    .catchError((e) {}),
  http
    .get('https://www.googleapis.com/books/v1/volumes?q={android}')
    .catchError((e) {}),
  http
    .get('https://www.googleapis.com/books/v1/volumes?q={ios}')
    .catchError((e) {})
]);

var responseDart = responses[0];
var responseFlutter = responses[1];
var responseAndroid = responses[2];
var responseIos = responses[3];

Jeśli w powyższym przykładzie, żądanie do API o wyniki dotyczące systemu iOS spowoduje wystąpienie błędu, zmienna responseIos będzie miała wartość Null, a te, które zostały wykonane prawidłowo, będą posiadały prawidłowe odpowiedzi.

Jak widać, przy pomocy Future.wait() możemy w znacznym stopniu przyspieszyć wykonywanie działań asynchronicznych, sprawiając, że zostaną one wystartowane w jednej chwili, a czas wykonywania wszystkich zadań, będzie dokładnie równy czasowi najdłuższego z tych zadań. Ta sztuczka zadziała jednak tylko w przypadku, gdy wynik wcześniejszego żądania nie jest przekazywany do któregoś z kolejnych.