Flutter z perspektywy C++/Qt developera

Wojciech-Kniec

Wojciech

Senior Software Engineer

Wstęp

Nadawanie nowego życia starym, nieużywanym rzeczom zawsze przynosi wiele radości bezradnemu nerdowi, takiemu jak ja. Ostatnio podłączyłem Raspberry pi do mojego starego wzmacniacza stereo - WS-303. Został on zakupiony przez mojego ojca w połowie lat 80-tych, więc pamięta jeszcze czasy komunizmu w Polsce! :). Chciałem móc nim sterować za pomocą pilota. Oto moje uzasadnienie pomysłu napisania klienta Mopidy we Flutterze.

W artykule przeczytasz:

Dlaczego warto używać Fluttera

Wprowadzenie

Znalazłem stary czujnik podczerwieni, przekaźnik i kilka rezystorów. Poświęciłem kilka godzin (sporo przy tym przeklinając), aby w końcu móc włączyć mój wzmacniacz pilotem od telewizora. Dodanie wsparcia dla większej ilości klawiszy od tego momentu było już proste (klawisze Vol+ i Vol- zmieniają głośność zarówno na moim wzmacniaczu jak i telewizorze, ale mi to nie przeszkadza).

Muzyka! To jest rzecz, której brakowało w mojej konfiguracji. Postanowiłem więc zainstalować serwer Mopidy. Posiada on wsparcie dla różnych formatów muzycznych, Spotify, a nawet youtube. Zapewnia podstawową komunikację przez web sockety używając protokołu json rpc 2. Istnieją dziesiątki klientów dla tego serwera, część z nich jest webowa, część działa w terminalu, są też klienci dla telefonów komórkowych. Żaden z nich oczywiście nie obsługuje włączania i wyłączania mojego własnego zestawu stereo.

Oto mój wzmacniacz, piękny, czyż nie?

flutter_1.png

Przepraszam za ten długi wstęp. Chciałem tylko uzasadnić mój pomysł napisania klienta Mopidy we Flutterze. Był to mój pierwszy kontakt z tym frameworkiem i chcę się podzielić kilkoma spostrzeżeniami na jego temat jako programista C++/Qt.

Powody, dla których warto pokochać Flutter

Ustawianie środowiska

W porównaniu do Qt, ustawienie całego środowiska pracy dla Androida było super proste. Wiele problemów można było po prostu rozwiązać za pomocą narzędzia wiersza poleceń o nazwie flutter doctor. Wystarczy uruchomić je w terminalu, a ono pobierze brakujące rzeczy, poprosi o potwierdzenie licencji i pokaże czytelne podsumowanie. Dużo prostsze niż ręczne instalowanie i ustawianie ścieżek w Qt Creator dla JRE, NDK, SDK... Oto przykład podsumowania flutter doctor:

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel master, v1.15.4-pre.144, on Microsoft Windows [Version 10.0.17763.1039], locale pl-PL)
[√] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[√] Chrome - develop for the web
[!] Android Studio (version 3.5)
X Flutter plugin not installed; this adds Flutter specific functionality.
X Dart plugin not installed; this adds Dart specific functionality.
[√] Connected device (2 available)
! Doctor found issues in 1 category.

Zależność i menedżer pakietów

Nienawidzę tego, że C++ nie ma standardowego menedżera pakietów. Conan zyskuje na popularności i C++20 będzie miał moduły, ale to wciąż nic w porównaniu do npm z node.js czy nawet pythons pip. Flutter ma coś podobnego do Cargo z Rust i to jest świetne. Korzystanie z zewnętrznych bibliotek jest bardzo proste, wystarczy dodać wpis do pliku pubspec.yaml. Sekcja Dependencies wygląda tak:

dependencies:
     flutter:
         sdk: flutter
# additional libraries with minimal versions
     cupertino_icons: ^0.1.2
     loading: ^1.0.2

Jeśli chcę skorzystać np. z biblioteki json_rpc_2 w wersji 2.1.0 lub wyższej, wystarczy, że dodam tę linijkę w sekcji zależności i zapiszę plik.

json_rpc_2: ^2.1.0

Biblioteka zostanie pobrana i będzie gotowa do użycia w ciągu sekundy!

Hot reload

Ta funkcja jest niesamowita! Budowanie i wdrażanie aplikacji może zająć dużo czasu. Staje się to irytujące, gdy jedyne co chcemy sprawdzić, to prosta, drobna poprawka. We Flutterze istnieje funkcja 'Hot reload', która oznacza superszybkie ładowanie przy każdym zapisie (jeśli kod da się skompilować). Ustawianie widżetów na stronie jest niemal tak wygodne, jak pisanie dokumentu Markdown z podglądem. To naprawdę działa!

Deklaratywne podejście

Flutter tworząc widok, odzwierciedla stan aplikacji. Jest to duża zmiana modelu, ale dzięki temu łatwiej jest utrzymywać stany UI. Zapomnij o np. ustawianiu koloru czcionki na czerwony gdy coś się dzieje, a następnie usuwaniu przycisków przy innym zdarzeniu. Tutaj definiujesz jak ma wyglądać UI dla danego stanu. Jeśli coś się zmieni, UI przebuduje się od zera. Wszystko odbywa się automatycznie, pod maską. Jest to proste i eleganckie rozwiązanie, ale tutaj pojawia się moja największa obawa - czy nie spowoduje to problemów z wydajnością dla skomplikowanych UI? Prawdopodobnie wszystko zależy od tego jak UI jest tworzone i projektowane.

Responsywność

Kod async jest całkiem naturalny w języku programowania Dart. Używanie futures jest tak proste jak używanie fetch w javascript. Oto przykład: załóżmy, że mamy asynchroniczny strumień, który od czasu do czasu produkuje jakieś dane. Możemy bezpośrednio zbudować widżet na podstawie otrzymanych informacji:

    body:  StreamBuilder<BrowsingDTO>(
       stream: widget._mopidy.browsingStream(),
       builder: (BuildContext context, AsyncSnapshot<BrowsingDTO> snapshot) {
         if (snapshot.hasError)
           return Text('Error: ${snapshot.error}');
         switch (snapshot.connectionState) {
           case ConnectionState.none: return Text("No connection");
           case ConnectionState.waiting: return Text("Waiting");
           case ConnectionState.active: return _createBrowsingList(snapshot.data);
           case ConnectionState.done: return _createBrowsingList(snapshot.data);
         }
         return null;
       },
 )

Widget _createBrowsingList(BrowsingDTO dto, mopidy) => ListView.builder(
   padding: const EdgeInsets.all(8),
   itemCount: dto.results.length,
   itemBuilder: (BuildContext context, int index) {
     return Container(
         height: 50,
         child: FlatButton(
           onPressed: () {
             switch (dto.results[index].type) {
               case "directory":
                 mopidy.browse(uri: dto.results[index].uri);
                 break;
               case "track":
                 mopidy.add(uris: [dto.results[index].uri]);
                 mopidy.play();
                 break;
               case "playlist":
                 mopidy.addPlaylist(dto.results[index].uri);
                 mopidy.play();
                 break;
             }
           },
           child: Text(dto.results[index].name),
         ));
   });});

Małe wyjaśnienie - ten mały blok kodu dołącza się do strumienia, który dostarcza dane na podstawie danych przeglądania przez użytkownika (aktualny folder, itp.). Za każdym razem, gdy pojawia się nowy obiekt BrowsingDTO, widżety są przebudowywane, a gdy użytkownik kliknie w wygenerowany płaski przycisk, tworzone jest nowe żądanie i cykl trwa dalej. Wygląda fajnie, prawda?

Wzorzec projektowy Bloc

Chociaż nie użyłem go w moim kliencie Mopidy (jeszcze), zrobiłem z nim kilka eksperymentów i jest bardzo obiecujący. Według mnie jest on bardziej testowalny niż standardowe podejście MVC i znacznie lepiej pasuje do deklaratywnego sposobu działania Flutter. To są moje pierwsze spostrzeżenia, nie znalazłem żadnych wad tego wzorca (choć wierzę, że jakieś są).

Visual Studio Code

Mogę się założyć, że ten punkt może być zaskakujący i kontrowersyjny dla niektórych osób. Osobiście uwielbiam VS Code za jego solidność i rozszerzalność. Nie wszystkie środowiska idą w parze z tym edytorem. Na szczęście Flutter nie jest jednym z nich. Skonfigurowanie go do pracy z Flutterem jest płynne i nie doświadczyłem żadnych problemów podczas pracy w nim.

Szybki development

To będzie podsumowanie. Wszystkie te małe rzeczy powyżej prowadzą do niego. Potrzeba mniej czasu na stworzenie w pełni funkcjonalnych aplikacji. Nie miałem większych problemów ze znalezieniem rozwiązania, gdy utknąłem, a to było moje zmartwienie, ponieważ Flutter jest dość nowy i społeczność jest stosunkowo mała.

wewnatrz.jpg

Za co nie lubię Fluttera

Duża zmiana modelu

Trzeba zmienić sposób myślenia o budowaniu aplikacji. Deklaratywne podejście jest zupełnie inne niż standardowe, iteracyjne. Potrzeba czasu, aby się do niego przyzwyczaić. Pomocne w tym przypadku jest doświadczenie w tworzeniu gier.

Problemy z debugowaniem i hot reload

Czasami debugger nie został uruchomiony we właściwym miejscu. Stos wywołań jest czasami niechlujny i przez to bezużyteczny. Hot reload jest świetną funkcją, ale czasami jest ona myląca. Znalezienie błędu w moim kodzie zajęło mi trochę czasu. Nie było żadnego - wszystko co musiałem zrobić to zrestartować całą aplikację.

Brzydki kod

Język Dart sam w sobie jest w porządku, ale budowanie widoków za jego pomocą może prowadzić do nieczytelnego, niechlujnego kodu z kaskadowymi widżetami. Nadal nie przyzwyczaiłem się do tego.

Pułapki kodowania

Flutter i Dart dają dużo swobody. Są bardzo elastyczne, ale kuszące jest tworzenie dużych klas all-in-one. Drugim problemem, który przychodzi mi do głowy jest to, że łatwo jest mieszać modele z logiką widoku. Zdarzyło mi się to kilka razy.

Zewnętrzne biblioteki

Większość ważnych rzeczy jest dostępna, ale to nic w porównaniu z dojrzałymi językami z ogromnymi bazami kodów. To się zmienia. Flutter się rozwija!

Brak natywnych buildów

Uruchamianie aplikacji na Windows czy Linux nie jest jeszcze możliwe, uruchomienie w przeglądarce tak, ale wciąż na wczesnym etapie. Wiem, wiem - to środowisko mobilne.

Podsumowanie
Plusy Minusy
Dobra dokumentacja Zła dokumentacja ;)
Łatwa konfiguracja Łatwa konfiguracja
VS Code Drobne problemy z debugowaniem
Hot reload Problemy z Hot reload
Łatwość reagowania Brzydki kod UI
Proste tworzenie widoków Łatwo o tworzenie dużych klas
Szybki development Łatwo pomieszać model z logiką widoku
Wzorzec Bloc Brak natywnych buildów
Proste testy UI Mała baza kodów
Dogodne zależności

Wnioski

Moje ogólne przemyślenia są zdecydowanie pozytywne. Warto spróbować! Ten projekt polegał głównie na nauce i eksperymentowaniu. Świetnie się przy tym bawiłem i chyba nie zawsze trzymałem się dobrych praktyk. Najlepsze jest to, że w rezultacie otrzymałem działającą i funkcjonalną aplikację w stosunkowo krótkim czasie.

Ostatnia rzecz - zrzuty ekranu, które zrobiłem telefonem:

flutter_2.png

Udostępnij w social mediach

Wybierz sposób realizacji i wspólnie zacznijmy realizować Twój projekt

Wycena projektu
Cofnij