NextRiot

Elementy budowania UI

December 30, 2018 • ☕️☕️ 8 min read

W moim poprzednim poście pisałem o przyznawaniu się do naszej niewiedzy. Można by na tej podstawie uznać, że zachęcam do zadowolenia się przeciętnością. Nic podobnego! To rozległe zagadnienie.

Jestem głęboko przekonany, że można zacząć w dowolnym miejscu i nie trzeba uczyć się technologii w określonej kolejności. Jednocześnie uważam, że rozwijanie własnych kompetencji jest niezwykle istotne. Osobiście zawsze byłem zainteresowany przede wszystkim tworzeniem UI.

Zastanawiałem się nad tym, co takiego ja wiem i uważam za wartościowe. Jasne, znam kilka technologii (np. Javascript i React). Jednak ważniejsze lekcje płynące z mojego doświadczenia są mniej uchwytne. Nigdy nie próbowałem ubrać ich w słowa. To moja pierwsza próba uporządkowania i opisania niektórych z nich.


Można znaleźć wiele porad dotyczących nauki nowych technologii i bibliotek. Która biblioteka będzie na czasie w 2019? A w 2020? Lepiej uczyć się Vue czy Reacta? Angulara? A co z Reduxem albo Rx? Czy musisz znać Apollo? REST i GraphQL? Łatwo się pogubić. A co jeśli autor rady się myli?

Największy przełom w mojej nauce nie był związany z żadną konkretną technologią. Najwięcej nauczyłem się, kiedy próbowałem rozwiązać jakiś konkretny problem UI. Czasami znajdowałem bibliotekę albo wzorzec, które mi pomagały. Innym razem wymyślałem własne rozwiązania (zarówno te dobre, jak i złe).

Właśnie to połączenie rozumienia problemów, eksperymentowania z rozwiązaniami i wykorzystywania różnych strategii doprowadziło mnie do najbardziej satysfakcjonujących doświadczeń. Ten post koncentruje się tylko na problemach.


Jeśli pracowałeś z UI, najprawdopodobniej musiałeś zmierzyć się przynajmniej z niektórymi z tych wyzwań — albo bezpośrednio, albo używając biblioteki. Tak czy inaczej, zachęcam cię do stworzenia małej aplikacji bez żadnych bibliotek i pobawienia się z reprodukowaniem i rozwiązaniem tych problemów. Żaden z nich nie ma jednego, właściwego rozwiązania. Nauka przychodzi wraz z eksploracją problematycznego obszaru i próbowaniem różnych kompromisów.


  • Spójność. Klikasz na przycisk “Lubię to” i wyświetla się tekst: “Ty i 3 innych znajomych lubi ten post.” Klikasz jeszcze raz i tekst znika. Brzmi prosto. Ale ten sam tekst może wyświetlać się jednocześnie w kilku miejscach na ekranie. Może jest jeszcze jakiś inny element (np. tło przycisku), który powinien się zmieniać. Lista “lubiących” osób, która została pobrana wcześniej z serwera i która wyświetla się po najechaniu na przycisk, powinna teraz zawierać twoje imię. Jeśli przejdziesz do innej strony i z powrotem, post nie powinien “zapomnieć”, że był polubiony. Już lokalnie spójność stwarza wyzwanie. Ale inni użytkownicy także mogli zmodyfikować dane, które wyświetlamy (np. lubiąc post, który oglądamy). Jak utrzymujemy spójność danych w różnych częściach ekranu? Jak i kiedy uspójnić lokalne dane z tymi na serwerze i na odwrót?

  • Responsywność. Ludzie są w stanie tolerować brak wizualnej odpowiedzi na ich akcje przez ograniczony czas. W przypadku ciągłych akcji, takich jak gesty czy scrollowanie, ten czas jest krótki. (Pominięcie nawet jednej 16ms klatki sprawia wrażenie przeskakiwania) Dla dyskretnych akcji, takich jak klikanie, są badania, które pokazują, że użytkownik postrzega każde opóźnienie < 100ms jako równie szybkie. Jeśli akcja zajmuje więcej, musimy pokazać wizualny wskaźnik. Jest jednak kilka nieintuicyjnych kwestii, z którymi trzeba się zmierzyć. Wskaźnik, który powoduje, że layout strony “skacze” albo taki, który pokazuje kilka etapów ładowania, może spowodować, że akcja wydaje się dłuższa, niż jest w rzeczywistości. Podobnie, odpowiedź na interakcję w ciągu 20ms za cenę ominięcia klatki animacji może wydawać się wolniejsza niż odpowiedź w ciągu 30ms bez pomijania klatek. Umysły nie są benchmarkami. Jak zachować responsywność aplikacji na różne rodzaje interakcji?

  • Latencja. Zarówno obliczenia, jak i dostęp do sieci zajmują czas. Czasami możemy zignorować koszt obliczeniowy o ile nie ma on wpływu na responsywność urządzeń, na których nam zależy (pamiętaj o testowaniu również na urządzeniach z niższej półki). Trzeba natomiast odnieść się w jakiś sposób do latencji sieci - to może zająć całe sekundy! Nasza aplikacja nie może się po prostu zawiesić w oczekiwaniu na załadowanie danych czy kodu. Oznacza to, że każda akcja, która zależy od nowych danych, kodu czy plików jest potencjalnie asynchroniczna i musi jakoś zająć się kwestią “ładowania”. A to może dziać się niemal na każdym ekranie. Jak możemy “elegancko” rozwiązać kwestię latencji bez wyświetlania “kaskady” spinnerów albo pustych widoków? Jak uniknąć skakania layoutu? I jak zmienić asynchroniczne zależności bez “przepinania” naszego kodu za każdym razem?

  • Nawigacja. Oczekujemy, że UI pozostanie “stabilny” podczas interakcji. Elementy nie powinny znikać nam sprzed nosa. Nawigacja, zarówno rozpoczęta w aplikacji (np. kliknięcie linka), jak i spowodowana zewnętrznym eventem (np. kliknięcie przycisku “wróć”) powinna także odbywać się w zgodzie z tą zasadą. Na przykład, przejście pomiędzy zakładkami /profil/polubienia i profil/obserwowane w widoku profilu nie powinno wyczyścić pola wyszukiwania znajdującego się poza obszarem zakładek. Nawet przejście do innego widoku jest jak wejście do pokoju. Ludzie spodziewają się, że kiedy wrócą później do tego pokoju, zastaną wszystko tak jak to zostawili (ewentualnie z dodatkiem nowych rzeczy). Jeżeli jesteś w połowie długiej listy, klikniesz profil, a potem wrócisz, jeśli nie znajdziesz się w tym samym miejscu listy albo będziesz czekać, aż załaduje się na nowo, będzie to frustrujące. Jak zaprojektować aplikację tak, by radziła sobie z arbitralnym nawigowaniem bez utraty ważnego kontekstu?

  • Dezaktualizacja. Możemy spowodować, że przycisk “wróć” przeniesie nas z powrotem błyskawicznie, jeśli zastosujemy lokalny cache. Cache “zapamięta” jakieś dane do szybkiego dostępu, nawet jeśli, teoretycznie, moglibyśmy je pobrać ponownie. Ale caching powoduje także problemy. Cache się dezaktualizuje. Jeśli zmienię avatar, cache również powinien się zaktualizować. Jeśli wstawię nowy post, powinien się on natychmiast pojawić w cache albo cache powinien być usunięty. To może zrobić się trudne i łatwo tutaj o błędy. Co, jeśli wstawienie posta się nie powiedzie? Jak długo cache powinien pozostawać w pamięci? Kiedy ponownie pobieramy listę, to czy spinamy ją z tą, która jest już w cache czy opróżniamy cache? W jaki sposób paginacja i sortowanie są odzwierciedlone w cache?

  • Entropia. Drugie prawo termodynamiki mówi coś w rodzaju “wraz z upływem czasu, wszystko zmienia się w bałagan” (mniej więcej). Ma to zastosowanie również w UI. Nie możemy przewidzieć dokładnie interakcji użytkownika i ich kolejności. W każdym momencie nasza aplikacja może znajdować się w jednym z zaskakująco wysokiej liczby możliwych stanów. Staramy się, jak potrafimy, żeby rezultat był przewidywalny i ograniczony przez nasz projekt. Nie chcemy patrzeć na screenshot buga o zastanawiać się “jak do tego doszło”. Na N możliwych stanów, jest N×(N–1) możliwych przejść pomiędzy nimi. Na przykład, jeśli przycisk może by w jednym z 5 różnych stanów (normal, active, hover, danger, disabled), kod aktualizujący przycisk musi być poprawny dla 5×4=20 możliwych przejść — lub uniemożliwić niektóre z nich. Jak opanować eksplozję kombinacji i sprawić, aby efekt wizualny był przewidywalny?

  • Priorytety. Niektóre rzeczy są ważniejsze niż inne. Być może dialog powinien wyświetlać się fizycznie “nad” przyciskiem, który go wywołał i wystawać poza element, który go zawiera. Nowo stworzone zadanie (np. reakcja na kliknięcie) może być ważniejsze niż jakieś długo trwające zadanie, które jest już wykonywane (np. renderowanie następnego posta poniżej krawędzi ekranu). Wraz z rozwojem aplikacji różne części jej kodu, napisane przez różne osoby i zespoły, rywalizują o ograniczone zasoby takie jak procesor, sieć, obszar ekranu i ograniczenia rozmiaru aplikacji. Czasami możesz uporządkować rywalizujące elementy na wspólnej skali ważności tak jak właściwość z-index w CSS. Ale rzadko się to dobrze kończy. Każdy developer sądzi, że to jego kod jest ważny. A jeśli wszystko jest ważne, to nic nie jest! W jaki sposób sprawić, aby niezależne widgety współpracowały, a nie rywalizowały ze sobą o zasoby?

  • Dostępność. Niska dostępność stron nie jest niszowym problemem. Na przykład w Wielkiej Brytanii, niepełnosprawność dotyka 1 na 5 osób. (Tutaj fajna info grafika) Ja odczuwam to też osobiście. Mimo że mam tylko 26 lat, mam problem z czytaniem stron z wąską czcionką i niskim kontrastem. Próbuję używać mniej trackpada i obawiam się dnia, kiedy będę musiał nawigować za pomocą klawiatury po kiepsko wykonanych stronach. Musimy tworzyć aplikacje, które nie będą okropne dla osób z trudnościami - a dobra wiadomość jest taka, że jest tutaj dużo łatwo osiągalnych celów. To zaczyna się od edukacji i narzędzi. Ale musimy spowodować, żeby product developerom było łatwo robić to, co właściwe. Jak sprawić, żeby dostępność była domyślnym założeniem, a nie tylko pozostawała w sferze refleksji?

  • Internacjonalizacja. Nasz aplikacja musi działać wszędzie na świecie. Ludzie nie tylko mówią różnymi językami, ale musimy także wspierać layouty ułożone od prawej do lewej z najmniejszym wysiłkiem ze strony inżynierów produktu. Jak wspierać różne języki bez poświęcenia latencji i responsywności?

  • Dostarczanie. Musimy dostarczyć kod naszej aplikacji do komputera użytkownika. Jak go tam przetransportować i w jakim formacie? To może brzmi prosto, ale jest wiele kwestii, które trzeba tu pogodzić. Na przykład, natywne aplikacje zwykle ładują najpierw cały kod za cenę ogromnego rozmiaru aplikacji. Aplikacje internetowe zwykle początkowo pobierają mniej danych za cenę większej latencji podczas użytkowania. Jak zdecydować, w którym momencie wprowadzić latencję? Jak zoptymalizować dostarczanie w zależności od sposobu użytkowania? Jakie dane są potrzebne, by opracować optymalne rozwiązanie?

  • Odporność. Możesz lubić “bugi”, jeśli jesteś entomologiem, ale pewnie nie chcesz ich widzieć w swoich programach. Nie da się jednak uniknąć tego, że niektóre z nich dostaną się na produkcję. Co wtedy? Niektóre bugi powodują niepoprawne, ale dokładnie określone zachowania. Na przykład, może twój kod wyświetla niepoprawny efekt wizualny w określonych przypadkach. Ale co jeśli podczas renderowania kod zawiesza się? Nie możemy wtedy kontynuować, ponieważ efekt wizualny byłby niespójny. Nawet jeśli zawiesi się renderowanie pojedynczego posta, nie powinno to “rozwalić” całej listy albo sprawić, że aplikacja zacznie działać w częściowo popsutym trybie, który spowoduje dalsze problemy. Jak pisać kod tak, aby błędy renderowania i pobierania danych były izolowane i pomimo ich pojawienia, reszta aplikacji mogła dalej działać? Co oznacza odporność UI na błędy?

  • Abstrakcja. W maleńkiej aplikacji możemy zakodować wiele specjalnych przypadków, żeby poradzić sobie z powyższymi problemami. Ale aplikacje zwykle się rozrastają. Chcemy mieć możliwość reużwania, kopiowania i łączenia części naszego kodu, a także pracowania nad nim kolektywnie. Chcemy określić wyraźne granice pomiędzy częściami, z którymi zaznajomione są różne osoby i uniknąć tworzenia zbyt sztywnej logiki, która często się zmienia. Jak stworzyć abstrakcję, która ukrywa implementację szczegółów różnych części UI? Jak uniknąć ponownego pojawiania się, wraz z rozrostem aplikacji, problemów, które dopiero co rozwiązaliśmy?


Oczywiście, jest wiele problemów, o których nie wspomniałem. Ta lista nie jest w żaden sposób wyczerpująca! Na przykład, nie poruszyłem kwestii współpracy pomiędzy designerem i inżynierem czy kwestii debugowania i testowania. Może innym razem.

To kuszące, żeby czytać o tych problemach z myślą o jakiś konkretnych bibliotekach, które je rozwiązują. Ale polecam ci pomyśleć, że te biblioteki nie istnieją i przeczytać listę jeszcze raz z tej perspektywy. Jak ty podszedłbyś do rozwiązania tych kwestii? Spróbuj z małą aplikacją! (Chętnie zobaczę twoje eksperymenty na GitHubie - śmiało, daj mi znać na Twitterze).

Co jest interesujące w tych problemach, to fakt, że większość z nich pojawia się bez względu na skalę aplikacji. Możesz je zaobserwować zarówno w małych widgetach takich jak typehead czy tooltip, jak i ogromnych aplikacjach, takich jak Twitter czy Facebook.

Pomyśl o nietrywialnych elementach UI w aplikacji, którą lubisz używać i przejdź przez tę listę problemów. Czy możesz opisać niektóre kompromisy, na które zdecydowali się developerzy? Spróbuj od zera zreplikować podobne zachowania!

Nauczyłem się wiele o inżynierii UI przez eksperymentowanie z tymi problemami w małych aplikacjach, bez użycia bibliotek. Polecam to samo każdemu, kto chce zyskać głębsze uznanie dla kompromisów w budowaniu UI.