Prosta aplikacja w javafx - statystyki Covid. Tutorial
Zdarzało mi się wiele razy, że kiedy chciałam stworzyć małą, prostą aplikację (albo prototyp jakiegoś rozwiązania), to dość szybko natykałam się na nieprzyjemny etap “bootstrapingu”, czyli przygotowania odpowiednich narzędzi, konfigurowania systemu budowania, zapewnienia sobie wszystkich zasobów potrzebnych do szybkiego rozpoczęcia pracy.
Kroki potrzebne do rozpoczęcia projektu w Javafx są trochę inne niż te, które wykonałabym, gdybym tworzyła projekt w Clojure czy używała Reacta, choć w zasadzie sprowadzają się do kilku najważniejszych aspektów.
W tym wpisie opiszę krok po kroku jak stworzyć prostą aplikację desktopową z wykorzystaniem biblioteki openjfx.
Najważniejsze etapy rozpoczynania projektu
Przestrzeń robocza
W moim systemie plików utrzymuję specjalny katalog dev
, do którego wrzucam różne “deweloperskie” projekty, prototypy, testy bibliotek, szybkie skrypty its. dev
jest podzielony na podkatalogi nazwane według używanego języka programowania (ten podział nie zawsze ma sens, ale zwykle się sprawdza). Nowy projekt w javafx będzie miał swój “dom” w ~/dev/java:
|
|
Utworzenie repozytorium Git
Kolejnym krokiem jest utworzenie repozytorium Git-a. Pierwszy komit zawiera jedynie plik README.md, który będzie pełnił rolę podręcznego notatnika na pomysły, odnośniki, spis decyzji projektowych itd.
|
|
Utworzenie podstawowej struktury projektu javafx
W katalogy covidstat utworzę plik pom.xml zawierający zależności potrzebne do rozpoczęcia pracy z biblioteką javafx. Nie będę tworzyć go ręcznie - wykorzystam archetyp wspomniany na stronie openjfx.io
|
|
Ten archetyp wygeneruje następujący pom.xml:
|
|
Dokumentacja - ważne odnośniki
Ważnym zasobem w pracy z projektem będzie strona openjfx, z której jest dostęp do:
- dokumentacji API javafx
- dokumentacji społecznościowej fxdocs
- serii artykułów “getting started”
- dokumentacja referencyjna do javafx na stronie oracle
Warto mieć tę dokumentację otwartą w przeglądarce.
Test konfiguracji przy użyciu Mavena
Projekt utworzony przy pomocy archetypu można uruchomić poleceniem:
|
|
Przykładowa aplikacja w archetypie wygląda tak:
Jednak po zmianie pom.xml tak, aby:
- używał javy 15
- używał nowszej wersji openjdk (17-ea+6)
po uruchomieniu otrzymam okienko zawierające dane:
Dostęp do danych o COVID
Odnośniki do publicznego API i jego dokumentacji
Publicznie dostępne API znajduje się na stronie covid19api.com. Dokumentacja jest dostępna tutaj. Przykładowa aplikacja będzie pobierać dane na temat Polski.
Jaki jest “slug” dla Polski?
Informacje na temat danego kraju wymagają znajomości jego specjalnej nazwy, tzw. “sluga”, który będzie występował w URI żądań dotyczących tego kraju. Sprawdzę, jaki jest slug Polski - wykorzystam API GET Countries
(tutaj dokumentacja).
Wyjście cURL-a przepuszczę przez ripgrep (opcja -C5 pokazuje 5 wierszy kontekstu) i poszukam sluga dla Polski:
|
|
Całkowita liczba przypadków: confirmed, active, deaths, recovered
API GET By Country Total
pozwala zdobyć dane o wszystkich przypadkach w następującym formacie (poniżej ściągam dane dla Polski, zapisuję w pliku i wyświetlam jego ostatnie 15 wierszy, po uprzednim sformatowaniu):
|
|
Aplikacja
Co ma robić przykładowa aplikacja? Oto jej zakres:
- ściąga dane o dostępnych krajach
- umożliwia wybranie jednego kraju
- ściąga dane na temat tego kraju
- wyświetla informacje na temat kraju (np. ostatnie dane, wykres podsumowanie itp.)
Serwis do pobierania danych
Pobieraniem danych zajmie się serwis (stworzę dwie implementacje - jedna będzie dostarczać dane z dysku, druga będzie rzeczywiście korzystać z sieci).
API serwisu
Serwis będzie posiadał dwie metody:
Stream<Country> getCountries();
- do pobierania listy krajówStream<Case> getCases(Country data);
- do pobierania danych o przypadkach dla danego kraju
|
|
Użycie rekordów oznacza, że w pom.xml należy dodać --enable-preview
:
- jako opcję kompilatora:
|
|
- oraz w pluginie javafx:
|
|
Prosta implementacja serwisu
W prostej implementacji serwis zwraca statyczne dane. Ten serwis posłuży do zaprototypowania GUI.
Oto jego implementacja:
|
|
Sposób działania
Czas odpaić Intellij. Będzie łatwiej :) Plan jest taki:
- po uruchomieniu aplikacji startujemy w tle task, który pobierze dane o nazwach krajów (
getCountries()
) z serwisu - ładujemy dane o krajach do listy
- po wyborze kraju z listy startujemy w tle inny task, który pobierze dane o danym kraju
- task aktualizuje informacje wyświetlaną w GUI - pokazują się statystyki z ostatniego dnia
Architektura
Użyjemy standardowej (i polecanej dla aplikacji GUI w javafx) architektury MVVM - Model-View-ViewModel, dzięki której uzyskamy separację różnych fragmentów programu:
Model
Model będzie przechowywał dane domenowe i nie będzie w żaden sposób zależał od javafx. Do danych modelu należą instancje klas Country
i Case
. Funkcję modelu będzie spełniac prosta klasa CasesPerCountryModel
, która będzie działała jako cache dla obiektów domenowych.
*
|
|
Model oferuje oddzielnie operacje dostępu do istniejących danych (READ) oraz operacje modyfikujące (ustawiające) dane (WRITE). W przypadku braku danych zwraca puste kolekcje.
ViewModel
ViewModel to tzw. model widoku, czyli klasa zawierająca dane potrzebne do wyświetlenia w GUI stanu programu. Udostępnia zestaw obiektów Property
(do których podłączone zostaną kontrolki widoku) ualtualnianych w wyniku akcji użytkownika (bądź w rezultacie wywołań zwrotnych po zakończeniu operacji serwisów - patrz niżej).
Do jego zadań należy również implementacja wywołań zwrotnych (callbacków) a więc wykonywanie całej logiki programu (być może oddelegowanej do odrębnych wątków, aby nie blokować głównego wątku gui) i aktualizacja swojego stanu. Co warto oddelegować do innego wątku? Wszystkie operacje związane ze ściąganiem danych z internetu. Dlatego też wszystkie (dwie!) operacje, które wykonuje przekazana do ViewModel impelemntacja CovidService
będą się odbywać w osobnym wątku: zaimplementuję dwa Workery (podklasy Service):
CountriesProviderService
ściągnie dane o nazwach krajów (ten serwis jest uruchamiany tylko raz, w konstruktorze modelu widoku)CovidUpdateService
będzie ściągał - na żądanie - dane o konkretnum kraju (uruchamiany jest wówczas, gdy został wybrany inny kraj)
Jeśli uruchomione przez te workery Taski zakończą się powodzeniem, zaktualizuję model widoku w callbacku task.setOnSucceeded(), który wykonywany jest już w JavaFX ApplicationThread.
ViewModel jest reprezentowany przez klasę CovidViewModel
.
- w jej konstruktorze rejestrowane są handlery zmian propertiesów, handlery serwisów oraz odpalany jest serwis CountriesProviderService.
- metoda
update(country)
wywoływana po zmianie wyboru elementu na liście krajów ma dzięki temu bardzo prostą implementację:
|
|
View
View to kontrolki javafx:
- lista (ListView) krajów, która po wybraniu kraju deleguje odświeżenie danych do ViewModel (ChangeListener modelu selekcji wywołuje
viewModel.update(country)
) - labelki (Label) wyświetlające statystyki dla wybranego kraju
Po utworzeniu są one “bindowane” z właściwościami ViewModel, a więc automatycznie pokażą zmiany wartości obiektów Property
z ViewModel.
Ponieważ cała logika programu wykonuje się po ustawieniu wybranego kraju na liście, to w konstruktorze widoku, na samym końcu, “wybieram” przy pomocy API ListView pierwszy element. Muszę to zrobić w wątki JavaFX Application Thread, więc wykorzystuję javafx-owy odpowiednik SwingUtilities.invokeLater():
|
|
Aplikacja główna
W aplikacji głównej tworzę model widoku, przekazując do niego pożądaną implementaję CovidService
(obecnie tworzę HandmadeService
który zwraca stałe wartości).
Model widoku jest argumentem konstruktora widoku.
|
|
Screenshot (statystyki dla ostatniego dnia)
Wykres statystyk
Dodanie wykresu nie będzie trudne. Javafx posiada już gotowe klasy do tworzenia różnego rodzaju wykresów (patrz pakiet javafx.scene.chart).
W naszym przypadku potrzebujemy wykresu łączącego linią punkty danych - użyjemy XYChart.
Stworzę prosty widok (klasa ChartView
), który w parametrze konstruktora otrzyma property SimpleListProperty<Case>
.
Property to tworzone jest już w modelu widoku. Muszę tylko udostępnić je klasie widoku. Przechowuje ono ocase-y wybranego kraju, więc śledząc zmany tego property (tj. rejestrując ChartView jako słuchacza zmian) mogę łatwo aktualizować serie danych. Odrobina magii sprawi, że zmiana danych spowoduje automatyczne zaktualizowanie wykresów.
Serie danych
Obiekt Case przechowuje kilka różnych statystyk, a lista obiektów Case - która zmienia się po wybraniu innego kraju - pozwala na zaktualizowanie serii danych na wykresie.
Utworzę wykres od razu z czterema seriami danych - będą one początkowo puste. Serie będę przechowywać w mapie, której kluczami będą wartości typu enumeracyjmego CASETYPE.
Wartości CASETYPE dodatkowo potrafią “wyjąć” wartość odpowiedniego pola z obiektu Case:
|
|
Pojedyncza “dana”
Wykres XYChart
składa się z punktów danych typu XYChart.Data<K, V>
. W naszym przypadku:
- typ K - dziedzina - to String (na osi X będą daty jako napisy)
- typ V - przeciwdziedzina - to Integer.
Serie dla poszczególnych CASETYPE przechowuję w mapie oList
, której wartości będące ObservableList
będę aktualizować przy zmianie kraju.
|
|
Referencje do tych serii dodaję do obserwowanej listy observableSeries
która jest ustawiana jako dane wykresu; dzięki temu wykres będzie się aktualizował automatycznie podczas aktualizacji serii danych w mapie oList
.
Klasa ChartView
Oto cała klasa ChartView
:
|
|
Dodanie ChartView
do CovidView
ChartView
dodaję do CovidView
w metodzie createView(), przekazując odpowiedni Property z modelu widoku:
|
|
Screenshot (dodany wykres)
Oto okienko aplikacji z wykresem (do HandmadeService.java
dodałam kilka punktów danych, aby wykres przypominał wykres):
Kolejne kroki
Lista todo jest całkiem spora. Co należy teraz zrobić?
- zaimplementować
CovidService
tak, aby czytał dane z pliku (do zaimplementowania będzie parsowanie JSON-a); kolejny krok to użycie klasy HttpClient i łączenie z serwisem wystawiającym API covidowe - umożliwić przeglądanie danych z kolejnych dni (przyciski: “next”, “prev” lub jakiś DatePicker)
- umożliwienie wyboru krajów i rysowanie nałożonych na siebie wykresów
- …
Możliwości są nieskończone :) Tylko, jak zawsze, czasu mało… Ściąganie rzeczywistych danych zostawiam jako ćwiczenie dla czytelnika :D
Repozytorium
Kod dostępny jest na GitHubie: covidstat
Ten wpis jest częścią serii javafx-covid.