SilverNETGroupSGGW / Straciatella

Flutter app for viewing schedules from Bolero API
https://silvernetgroupsggw.github.io/Straciatella/
3 stars 1 forks source link

Widget dodawania planu zajęć #72

Closed matyjb closed 8 months ago

matyjb commented 10 months ago

To będzie pushowalny route na stos Navigatora, ale nie named, jako zwykły widget. Przy Navigator.pop niech jako result przekazuje wybrany plan zajęć (ScheduleKey), o ile został jakiś wybrany. W schedule_managerze dodałem drzewa dla zwykłych scheduli oraz dla planów lecturerów. Jest klasa OptionsTreeNode, która odpowiada za reprezentacje jednego wyboru (name nam mówi co wybieramy, klucze w SplayTreeMap są możliwymi odpowiedziami, a wartości kolejnym wyborem OptionsTreeNode, chyba, że to już koniec to wtedy jest null). W niej są też przydatne metody: isLeaf i leafValue.

Chyba w końcu stanęło na tym flow co na figmie jest, a design co Kuba na discorda wstawiał dawno.

mpygruu commented 9 months ago

Jutro zacznę

mpygruu commented 9 months ago

Cubity już w miarę ogarniałem ale bloca to musiałem się ciut douczyć. Udało mi sie załadować drzewo z bloca natomiast nic więcej jeszcze nie robiłem. Zerknij pliska @matyjb czy dobrze to robię bo nie mam jeszcze wprawy z blociem. Wrzuciłem na branch mpygruu/issue72.

Jak wszystko jest w porządku to jutro zacznę sie zajmować prezentacją danych i flow przechodzenia drzewa.

matyjb commented 9 months ago

Cubity vs Bloc

Cubit jest uproszczoną wersją bloc'a różniący się tylko tym od bloca, że nie mamy stream'u eventów przychodzących, a mamy zamiast tego funkcje. Zabiera nam to trochę boilerplateu dostyczącego eventów (zobacz, że w cubitach nie ma plików cośtam_event.dart) i daje nam to, że po wywołaniu funkcji od jakiejś akcji np. increment() stan zmienia się natychmiastowo, synchronicznie. W blocu po użyciu .add(...) event jest obsługiwany asynchronicznie. W blocu też możemy zarządzać eventami przychodzącymi i dodawać np. debounce

Co do użycia różnice są tylko w wywoływaniu eventów. W blocu używamy .add(...) z przekazanym obiektem reprezentującym argumenty eventu, a w cubitcie mamy proste funkcje. Co do zmiany i odczytu stanu działają tak samo. Nadal używamy BlocProvider by zainicjalizować cubita czy bloca. BlocBuilder/BlocListener/BlocConsumer by stan odczytać i reagować na jego zmiany. BlocProvider.of by znaleźć w widget tree ten nasz najbliższy Bloc by można było na nim wywołać jakiś event albo odczytać stan (ale tylko raz, bez reagowania na zmiany). Paczka flutter_bloc daje nam też przydatne funkcje dla zmiennej context .read<BlocType>(), .watch<...>(), .select<...>(), które nam trochę skracają kod i są czytelniejsze.

Co nie tak

  1. Nie wywołuj eventów, które chcesz odpalić raz w metodzie build(). Ta metoda może się odpalać nawet i 60 razy na sekundę. image Przez takie coś możesz nawet wywołać 60 requestów/sec do api o aktualizację. Zamiast tego lepiej użyj initState(). Przekonwertuj ten widget na statefull i dodaj metodę initState o tak:

    @override
    void initState() {
    super.initState();
    
    context
      .read<ScheduleManagerBloc>()
      .add(const ScheduleManagerEvent.updateIndex());
    }
  2. Tego pewnie nie wiedziałeś, nawet ja o tym zapomniałem, ale gdy używamy metody .tr() od tłumaczenia to musimy usunąć const konstruktor, bo Flutter optymalizuje rerenderowanie widgetów tak, że jak jest const to nie trzeba wywoływać metody build() (oczywiście to, że dla tego widgeta nie odpali build nadal nie oznacza, że dla dzieci tego widgeta nie odpali). Więc zasada jest taka, gdy użyjemy tr() usuwamy const z konstruktora. image Tam linter będzie krzyczał, że powinien być const, więc możemy dwie rzeczy zrobić. Jak widget nie ma żadnych argumentów to po prostu wywalić całą tą linijkę z konstruktorem albo dodać linijkę dla lintera gdy mamy jakieś argumenty Code_61IV8huTXI
mpygruu commented 9 months ago

Super, bardzo dziękuję, wprowadzę zmiany

mpygruu commented 9 months ago

Już wróciłem do roboty, robię flow przechodzenia drzewa. Mam pytanko @matyjb czy powinienem wczytać optionsTreeNode z ScheduleManagerBloc tylko raz na samym początku i potem sobie przechodzić to drzewo niezależnie od bloca, czy powinienem aktualizować stan tego drzewa w bloc?

matyjb commented 9 months ago

W blocu nic nie zmieniaj. Najlepiej to sobie w tym widgetcie zrób state, który jest stosem który określa po kolei jakie opcje zostały wybrane przez user'a. Żeby ten stos czasem nam się nie rozjechał z tym drzewem co jest w blocu (np gdy przyjdzie aktualizacja planu i ona nam zmieni to drzewo w trakcie tworzenia) to użyj BlocListenera i przytnij ten stos o te wartości, które są nieprawidłowe (cofnie wtedy to user'a do ostatniego poprawnego wyboru w takim przypadku)

mpygruu commented 9 months ago

Dobra, dzięki, właśnie dokładnie na stosie to robię, a jeszcze dokładniej to na Queue z dart:collection bo w darcie queue można używać jak stos.

matyjb commented 9 months ago

Edit:

W blocu nic nie zmieniaj. Najlepiej to sobie w tym widgetcie zrób state, który jest stosem który określa po kolei jakie opcje zostały wybrane przez user'a. Żeby ten stos czasem nam się nie rozjechał z tym drzewem co jest w blocu (np gdy przyjdzie aktualizacja planu i ona nam zmieni ten stos to drzewo w trakcie dodawania planu) to użyj BlocListenera i przytnij ten stos o te wartości, które są nieprawidłowe (cofnie wtedy to user'a do ostatniego poprawnego wyboru w takim przypadku)

mpygruu commented 9 months ago

Flow wybierania planu zrobiony. Nie wykrzacza się jeśli przechodząc drzewo dopasujemy jeden id (zauważyłem że teraz w jednej ze ścieżek są dwa id ale domyślam się że tymczasowy błąd). Jeszcze zostało dorobić super cool animacje i wersję dla wybierania planu lecturerów.

Screenshot_20240214_201347

matyjb commented 9 months ago

Tak, tam te dwa id to takiej sytuacji nigdy nie powinno być i on chyba właśnie wynika z tych danych testowych. Jak tworzyłem te klasy od tego drzewa to tam chyba właśnie była taka sytuacja. Do animacji polecam flutter_animate

matyjb commented 9 months ago

Popatrzyłem w kod i trochę imo przekombinowałeś. Jak ja to widzę to ten obiekt OptionsTreeNode reprezentuje jeden poziom wyborów (ma tytuł poziomu w name i opcje do wyboru w options gdzie w tym SplayTreeMap key'em jest wybrana wartość, np. "Informatyka", a value jest kolejny OptionsTreeNode).

By werenderować poziom potrzebujemy jeszcze wiedzieć jaki key wybrał user dla każdego poziomu, więc state tego NewFilterScreen możemy uprościć do stosu par wartości czyli late final List<({OptionsTreeNode level, dynamic selected})> userChoices;. Na samym początku w initState oczywiście musimy dodać pierwszy poziom do tej listy, a selected dać na null bo user jeszcze nic nie wybrał.

void initState() {
  userChoices = [
    (
      level: context.read<ScheduleManagerBloc>().state.schedulesOptionsTree!,
      selected: null,
    ),
  ];
 }

Wtedy renderowanie poziomów będzie wręcz trywialne. Dla każdego obiektu w userChoices renderujemy poziom. Oczywiście dla uproszczenia kodu możemy jeszcze stworzyć funkcje od poprawnych manipulacji stosem zależnie co zrobił user:

Dodatkowo w OptionsTreeNode dodałem takie coś jak isLeaf, które ci pomoże rozwikłać problem czy renderować dany poziom czy nie (ten isLeaf powinien być zazwyczaj true dla OptionsTreeNode'a o name="id"). No i jak jest isLeaf na true dla ostatniego OptionsTreeNode w userChoices to można pokazać buttona zatwierdzającego wybór Navigator.pop(context, (ScheduleType.student, userChoices.last.leafValue)) (w OptionsTreeNode też dałem leafValue przy okazji)

Ogólnie pod względem renderowania poradziłeś sobie, tylko z tym state'em trochę przekombinowałeś, że to tak rozdzieliłeś na dwie zmienne. I też spoko jak będziesz przekazywać jakie typy w tych klasach generycznych będziesz trzymać bo jak się wszystko zostawia na dynamicach to tak trochę razi 😕

Ale ogólnie jest git 👍


Druga sprawa to formatowanie i linter etc. Przez to, że kodujesz w android studio to nic ci nie działa i rozjeżdżasz się z tym co my mamy, więc mocno sugeruje byś się przesiadł na vscode i poinstaluj te extensions co ci wyskoczą jako recommended gdy zrobisz merge z devem (Po lewej > Extensions > wpisz w wyszukiwarce @recommended i to co jest w workspace recommendations, albo też takie powiadomienie ci wyskoczy po prawej stronie jak otworzysz projekt)

mpygruu commented 9 months ago

Faktycznie można to dużo uprościć, dzięki za tak szczegółową analizę i wyjaśnienie, bardzo doceniam 😄

Dzisiaj po południu i jutro po południu będę dalej robić, uwzględnię poprawki 😎

Co do VSCode niechaj będzie, będę korzystał bo faktycznie formatowanie jest niespójne

mpygruu commented 9 months ago

Uprościłem kod i dodałem tłumaczenia. Zostały animacje, loading screen i to samo dla lecturerów. A tera wychodzę z domu i wracam na weekly B).

Pytanko czy mam się spodziewać lecturerów w postaci drzewa czy to zawsze będzie taka jedna zwykła lista? Jeśli lista to warto dodać wyszukiwarę na górze listy bo troche tych wykładowców jest na wydziale a co dopiero na uczelni

matyjb commented 9 months ago

Dla lecturerów też jest takie drzewo lecturersOptionsTree w ScheduleManagerBloc, tylko oczywiście aktualnie tam jest tylko jeden level (nie licząć tego z id) o name="fullname" i w opcjach wyboru tam jest obiekt _LecturerFullNameValue (który w sumie nie wiem dlaczego dałem jako prywatny, powinien być publiczny żebyś mógł sobie tam zrobić cast, więc zmień). Dałem to drzewo dla lecturerów, bo pewnie w przyszłości dojdzie jeszcze academic_year czy coś.

Oczywiście, że można zrobić wyszukiwarkę i dać listę zamiast tych chipów.

Co do screena z ładowaniem. Gdy jest jakiś index w pamięci to nie dawaj screena ładowania tylko daj jakiś LinearProgressIndicator u góry ekranu. Mniej migających ekranów tym lepiej.

matyjb commented 9 months ago

A jeszcze mi się przypomniało. W lecturerach może być sytuacja, że jest dwóch co mają tak samo na imię i ten sam stopień naukowy, więc ta sytuacja gdzie będą dwa lub więcej id do wyboru jest tutaj możliwa. Może przy takiej sytuacji pokazać emaile do wyboru, żeby jakoś ich odróżnić? 🤔

mpygruu commented 8 months ago

Poleciał push z już raczej kompletnymi widgetami dla planów. Dodałem animację przy dodawaniu do stosu, Circular i Linear progress indicatory w odpowiednich stanach i przeniosłem całą tą listę z jej logiką do oddzielnego widgeta.

Teraz pora na lecturerów, zastanowię się jak to ugryźć w miarę rozszerzalnie.

mpygruu commented 8 months ago

Dodałem kompletne widgety dla drzewa lecturerów. Jeszcze nie jestem zadowolony z kolorów przycisku i nie do końca przekonany czy radio buttony będą tu odpowiednie, to do przemyślenia jeszcze. Ale przechodzenie drzewa już zasuwa, w troche inny sposób niż u scheduli.

A i jeszcze tą wyszukiwarę dodać pozostało i tłumaczenia i będzie komplet.

Dodatkowo zastosowałem pomocnicze modele i cubit na potrzeby tylko tego jednego screena, to trochę nie wiedziałem gdzie je umieścić więc stworzyłem folder models i cubits w tym folderze dla tego screena. Jeśli jakoś inaczej to zrobić to proszę pisać B).

mpygruu commented 8 months ago

Wybieranie filtrów studenta jest już gotowe.

Wybieranie filtrów prowadzącego również działa, zastosowałem trochę inne podejście ale finalnie sprowadza się do tego samego czyli do przechodzenia drzewa. Dodałem również przycisk z lupką po którego kliknięciu pojawia się screen z listą prowadzących i wyszukiwanie konkretnego prowadzącego na podstawie wpisywanego tekstu. Jest jednak jeszcze małe to do: Gdy za pomocą wyszukiwarki wybierze się konkretnego wykładowcę i powróci się do ekranu drzewa, to wybrane id jest poprawne ale jeszcze należy dodać scrollowanie do wybranej pozycji, bo aż się prosi ale jeszcze nie wiem jak to ugryźć. Plus fajnie będzie pamiętać żeby zrobić Snackbara po dodaniu filtru, też aż się prosi.

A, no i plus, nie wiedziałem jak albo czy się da dostać do maila prowadzącego w drzewie lecturerów, ponieważ jest tam tylko fullname. Jeśli to aktualnie nie możliwe to może warto dodać ten mail, albo niech liść będzie lecturerem w postaci jsona?

Poniżej gif (trochę spowolniony ale niech będzie) z tym co naklepałem do tej pory:

output1

matyjb commented 8 months ago

W sumie ten ListTile z emailem w subtitle to dobry pomysł. Może zrobić ten ekran z wyszukiwarką jak pierwszy krok, a o tym drzewie jednak zapomnieć dla lecturerów. Wtedy academicYear trzeba by było dorobić jako drugi krok wyboru. Do typu ScheduleKey trzeba by było dodać jeszcze jeden nullowalny string academicYear by go móc zreturnować po wyborze planu lecturera. Jak myślisz? A może olać ten academicYear dla lecturera, ale wtedy by się pobierał cały plan ze wszystkich lat co pamięciowo nie wiem jakby to się wyrobiło po paru latach.

Ja bym jeszcze po zaznaczeniu lecturera dał go np. na buttona by było "dr Iza Antoniuk\nDodaj plan". By właśnie po scrollu nadal user wiedział co wybrał

Jakbyś robił to zmockuj to jakoś o ile będzie się łatwo dało ten academicYear tak tylko, żeby się pokazywał ale nic nie robił. Ja tam jeszcze z Fabianem ustalę jak to w modelach będzie wyglądać.

Jak już będzie takie gotowe gotowe to wtedy popatrzę w kod bo tam dużo tego jest

matyjb commented 8 months ago

W sensie dużo plików, nie że dużo złych rzeczy 😉

mpygruu commented 8 months ago

Tak, właśnie też mi przyszło do głowy że z taką wyszukiwarką można zrezygnować z drzewa dla lecturerów, w takim razie usunę i uwzględnię sugestie.

Co do academic year to hmm jeszcze pomyślę może wpadnę na jakiś pomysł konkretny ale na ten moment myślę żeby domyślnie pobierać aktualny plan a te z poprzednich lat gdzieś żeby były dodatkową opcją do wybrania. A może checkboxy dla poszczególnych lat, pomyślę też

matyjb commented 8 months ago

Tylko my nie mamy informacji, który jest aktualny (i user też musi wiedzieć jaki jest aktualny), więc lepiej zrobić ten drugi krok z automatycznie wybranym najświeższym (zakładam, że z api jak przyjdzie lista roków akademickich dla danego lecturera to one będą posortowane jakoś). Wtedy user może sobie wybrać inny, ale jak nie to ma już wybrane i może od razu kliknąć w guzik 'dodaj'

mpygruu commented 8 months ago

Wpadłem na jeszcze jeden pomysł. Skoro zdecydowana większość osób i tak będzie chciała śledzić najnowszy plan, to może nie róbmy drugiego kroku z wyborem roku akademickiego, ale niech gdzieś w apce będzie opcja przeglądania lub dodawania planów z archiwum. Wtedy można dodać funkcjonalność automatycznego przełączania na najnowszy plan z kolejnym rokiem akademickim, a poprzedni plan trafia do archiwum lub jest w ogóle usuwany z cache

matyjb commented 8 months ago

Hmm teraz widzę parę problemów z tymi planami:

No tu trzeba zwołać posiedzenie sejmu i to zrobię teraz

mpygruu commented 8 months ago

Męczę ostatni feature czyli scrollowanie do wybranego prowadzącego po wybraniu go w wyszukiwarce. W międzyczasie wpadłem jeszcze na kolejny pomysł zaprezentowania wyboru i przycisku dodania, dodaję na końcu.

Myślę że wygląda to schludniej i daje ewentualnie miejsce na ikonkę oka po lewej od przycisku "Dodaj" gdybyśmy w przyszłości chcieli na przykład dodać funkcjonalność szybkiego podglądu bez dodawania wykładowcy do zapisanych planów.

(Co do sposobu wyświetlania tytułów naukowych -- takie dostaję z API po zmianach jakie chłopaki zrobili, to będzie koniecznie do zmiany)

Zrzut ekranu 2024-02-26 o 13 22 56
matyjb commented 8 months ago

O fajne zrobiłeś to ze stopką gdzie jest dr inż Artur Krupa i guzik Dodaj. Ładnie to wygląda. Może zrób to samo na tym wyborze planu studenta? Tam by wtedy pisało np. WZIM Informatyka stacjonarne inż r.2020. Tylko tam w pliku byś musiał generowanie tego drzewa zmienić w pliku schedule_tree.dart. Wywalić semestr i zmockować rocznik jako startDate.year - year + 1.

Co do twojego problemu może zamiast używać showSearch po prostu zrób to wszystko na jednym screenie. W appbarze zrób sobie TextField i tym co tam wpiszesz filtruj pokazywane opcje w liście.

Drugi sposób to użycie scrollable_positioned_list, ale pierwszy sposób jest wiele lepszy bo ma mniej screenów i zatwierdzania

matyjb commented 8 months ago

A z tym okiem nasunąłeś mi pomysł, że możemy zrobić tak:

Wtedy ten screen twój stanie się jedynym miejscem gdzie będzie można przeglądać sobie plany. Nie będzie tak, że będzie miejsce od przeglądania planów i od dodawania sobie ich do tych subskrybowanych. Oczywiście będzie jeszcze jeden screen "Moje plany" gdzie będzie można wywalać plany z subskrypcji (i równocześnie z pamięci telefonu)

Co myślisz?

mpygruu commented 8 months ago

W sumie tym sposobem bez ekranu z listą będzie jeden screen mniej, zastosuję, dzisiaj wieczorem usiądę.

W widoku z wybieraniem planu studenta też fajnie będzie wstawić ten bottom app bar, ale jeszcze nie jestem przekonany jak sobie w myślach wizualizuję. Pokoduję coś i zobaczymy 😎

mpygruu commented 8 months ago

O, teraz dopiero drugi komentarz zauważyłem, brzmi fajnie. Wymaga to kolejnego przebudowania xD Zastanowię się w domku jak to ugryźć, ale myśle że warto. Wtedy na dole może być nawet nie koniecznie fab a ten sam bottom app bar.

matyjb commented 8 months ago

Nie nie tego teraz nie rób, zrób jak na razie by po zatwierdzeniu wyboru wyprintowało do konsoli ten ScheduleKey. Taki jest cel tego taska: stworzyć screen/y co po zakończeniu w popie dają rezultat w postaci ScheduleKey'a Navigator.pop(context, scheduleKey)

mpygruu commented 8 months ago

Niemal skończone. Przerobiłem to wyszukiwanie prowadzących, jest ekran z listą i wyszukiwarka bezpośrednio w app bar.

Zostały finalne todo:

Jutro się tym zajmę i liczymy na to że będzie pull requeścik 😎

mpygruu commented 8 months ago

Ogólnie już skończone, tylko się męczę z animacjami dla listy podczas wybierania filtru studenta. Nie wiem jakim sposobem używając AnimatedList zamiast ListView wychodzę poza zakres listy (ale tylko animowanej listy). To trzeba usiąść i na spokojnie

Narazie zpushowany kod jest bez animacji ale z kompletną funkcjonalnością. Załączam gifa (tym razem trochę przyspieszony xD):

Edit: ucięło gifa, nowego wstawiam

output

mpygruu commented 8 months ago

Działa. Wreszcie xD wrzucam pull request i do spanka na jutrzejsze sieci neuronowe o 8:00.