Przewodnik programisty Androida dotyczący wzorca nawigacji fragmentarycznej
Opublikowany: 2022-03-11Przez lata widziałem wiele różnych implementacji wzorców nawigacji w Androidzie. Niektóre aplikacje korzystały tylko z działań, podczas gdy inne działania były mieszane z fragmentami i/lub widokami niestandardowymi.
Jedna z moich ulubionych implementacji wzorców nawigacji jest oparta na filozofii „Jedna czynność-wiele fragmentów”, lub po prostu wzorcu nawigacji fragmentów, gdzie każdy ekran w aplikacji jest fragmentem pełnoekranowym, a wszystkie lub większość tych fragmentów jest zawarta w jedno działanie.
Takie podejście nie tylko upraszcza sposób implementacji nawigacji, ale ma znacznie lepszą wydajność, a w konsekwencji zapewnia lepsze wrażenia użytkownika.
W tym artykule przyjrzymy się niektórym typowym implementacjom wzorców nawigacji w systemie Android, a następnie przedstawimy wzorzec nawigacji oparty na fragmentach, porównując i kontrastując z innymi. Aplikacja demonstracyjna implementująca ten wzorzec została przesłana do GitHub.
Świat działań
Typowa aplikacja na Androida, która wykorzystuje tylko czynności, jest zorganizowana w strukturę podobną do drzewa (a dokładniej w ukierunkowany graf), w której działanie roota jest uruchamiane przez program uruchamiający. Podczas nawigacji w aplikacji system operacyjny utrzymuje stos wsteczny aktywności.
Prosty przykład pokazano na poniższym schemacie:
Aktywność A1 to punkt wejścia w naszej aplikacji (na przykład ekran powitalny lub menu główne), z którego użytkownik może przejść do A2 lub A3. Gdy potrzebujesz komunikować się między działaniami, możesz użyć funkcji startActivityForResult() lub udostępnić między nimi globalnie dostępny obiekt logiki biznesowej.
Gdy chcesz dodać nową aktywność, musisz wykonać następujące czynności:
- Zdefiniuj nową aktywność
- Zarejestruj go w AndroidManifest.xml
- Otwórz go za pomocą startActivity() z innej aktywności
Oczywiście ten schemat nawigacyjny jest dość uproszczonym podejściem. Może to stać się bardzo złożone, gdy trzeba manipulować tylnym stosem lub gdy trzeba wielokrotnie użyć tej samej czynności, na przykład gdy chcesz nawigować użytkownika przez niektóre ekrany samouczków, ale każdy ekran w rzeczywistości wykorzystuje tę samą czynność, co baza.
Na szczęście mamy do tego narzędzia zwane zadaniami i kilka wskazówek, jak prawidłowo nawigować wstecz.
Potem, wraz z API level 11, pojawiły się fragmenty…
Świat fragmentów
System Android wprowadził fragmenty w systemie Android 3.0 (poziom interfejsu API 11), głównie w celu obsługi bardziej dynamicznych i elastycznych projektów interfejsu użytkownika na dużych ekranach, takich jak tablety. Ponieważ ekran tabletu jest znacznie większy niż ekran słuchawki, jest więcej miejsca na łączenie i wymianę elementów interfejsu użytkownika. Fragmenty umożliwiają takie projekty bez konieczności zarządzania złożonymi zmianami w hierarchii widoków. Dzieląc układ działania na fragmenty, możesz modyfikować wygląd działania w czasie wykonywania i zachować te zmiany w stosie zapasowym zarządzanym przez działanie. – cytowany z przewodnika po interfejsie API Google dotyczącym fragmentów.
Ta nowa zabawka umożliwiła programistom zbudowanie wielopanelowego interfejsu użytkownika i ponowne wykorzystanie komponentów w innych działaniach. Niektórzy programiści to uwielbiają, a inni nie. To jest popularna debata, czy używać fragmentów, czy nie, ale myślę, że wszyscy zgodziliby się, że fragmenty wniosły dodatkową złożoność i programiści naprawdę muszą je zrozumieć, aby używać ich właściwie.
Pełnoekranowy fragment koszmaru w systemie Android
Zacząłem widzieć coraz więcej przykładów, w których fragmenty nie tylko przedstawiały część ekranu, ale w rzeczywistości cały ekran był fragmentem zawartym w działaniu. Kiedyś widziałem nawet projekt, w którym każda aktywność miała dokładnie jeden pełnoekranowy fragment i nic więcej, a jedynym powodem, dla którego te działania istniały, było hostowanie tych fragmentów. Oprócz oczywistej wady konstrukcyjnej jest jeszcze jeden problem z tym podejściem. Spójrz na poniższy schemat:
Jak A1 może komunikować się z F1? Cóż, A1 ma całkowitą kontrolę nad F1, odkąd stworzyło F1. A1 może przekazać pakiet, na przykład podczas tworzenia F1 lub może wywołać jego metody publiczne. Jak F1 może komunikować się z A1? Cóż, jest to bardziej skomplikowane, ale można to rozwiązać za pomocą wzorca wywołania zwrotnego/obserwatora, w którym A1 subskrybuje F1, a F1 powiadamia A1.
Ale jak A1 i A2 mogą się ze sobą komunikować? Zostało to już omówione, na przykład poprzez startActivityForResult() .
I teraz pojawia się prawdziwe pytanie: jak F1 i F2 mogą się ze sobą komunikować? Nawet w tym przypadku możemy mieć komponent logiki biznesowej, który jest dostępny globalnie, dzięki czemu można go wykorzystać do przekazywania danych. Ale to nie zawsze prowadzi do eleganckiego designu. Co jeśli F2 musi przekazać niektóre dane do F1 w bardziej bezpośredni sposób? Cóż, przy wzorcu wywołania zwrotnego F2 może powiadomić A2, a następnie A2 kończy z wynikiem i ten wynik jest przechwytywany przez A1, który powiadamia F1.
Takie podejście wymaga dużo szablonowego kodu i szybko staje się źródłem błędów, bólu i gniewu.
Co by było, gdybyśmy mogli pozbyć się wszystkich czynności i zachować tylko jedną z nich, która zachowuje resztę fragmentów?
Wzorzec nawigacji po fragmentach
Z biegiem lat zacząłem używać wzorca „Jedna czynność – wiele fragmentów” w większości moich aplikacji i nadal go używam. Istnieje wiele dyskusji na temat tego podejścia, na przykład tutaj i tutaj. Brakowało mi jednak konkretnego przykładu, który sam mogę zobaczyć i przetestować.
Spójrzmy na poniższy diagram:
Teraz mamy tylko jedną aktywność kontenera i mamy wiele fragmentów, które ponownie mają strukturę podobną do drzewa. Nawigacja między nimi jest obsługiwana przez FragmentManager , ma swój stos tylny.
Zauważ, że teraz nie mamy funkcji startActivityForResult() , ale możemy zaimplementować wzorzec wywołania zwrotnego/obserwatora. Zobaczmy kilka zalet i wad tego podejścia:
Plusy:
1. Czystszy i łatwiejszy w utrzymaniu AndroidManifest.xml
Teraz, gdy mamy tylko jedną aktywność, nie musimy już aktualizować manifestu za każdym razem, gdy dodajemy nowy ekran. W przeciwieństwie do aktywności nie musimy deklarować fragmentów.
Może się to wydawać drobnostką, ale w przypadku większych aplikacji, które mają ponad 50 działań, może to znacznie poprawić czytelność pliku AndroidManifest.xml .
Spójrz na plik manifestu przykładowej aplikacji, który ma kilka ekranów. Plik manifestu nadal pozostaje bardzo prosty.

<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Scentralizowane zarządzanie nawigacją
W moim przykładzie kodu zobaczysz, że używam NavigationManager , który w moim przypadku jest wstrzykiwany do każdego fragmentu. Ten menedżer może być używany jako scentralizowane miejsce do rejestrowania, zarządzania stosem wstecznym itd., dzięki czemu zachowania nawigacyjne są oddzielone od reszty logiki biznesowej i nie są rozproszone w implementacjach różnych ekranów.
Wyobraźmy sobie sytuację, w której chcielibyśmy uruchomić ekran, na którym użytkownik może wybrać niektóre elementy z listy osób. Chciałbyś również przekazać kilka argumentów filtrujących, takich jak wiek, zawód i płeć.
W przypadku Czynności napisałbyś:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Następnie musisz zdefiniować onActivityResult gdzieś poniżej i obsłużyć wynik.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Mój osobisty problem z tym podejściem polega na tym, że te argumenty są „dodatkami” i nie są obowiązkowe, więc muszę się upewnić, że działanie odbierające obsługuje wszystkie różne przypadki, w których brakuje dodatku. Później, gdy zostanie dokonana refaktoryzacja i np. dodatek „wiek” nie będzie już potrzebny, to muszę szukać wszędzie w kodzie, w którym rozpoczynam tę czynność i upewnić się, że wszystkie dodatki są poprawne.
Co więcej, czy nie byłoby ładniej, gdyby wynik (lista osób) pojawił się w postaci _Listy
W przypadku nawigacji fragmentarycznej wszystko jest prostsze. Wszystko, co musisz zrobić, to napisać metodę w Nawigatorze o nazwie startPersonSelectorFragment() z niezbędnymi argumentami i implementacją wywołania zwrotnego.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
Lub z RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Lepsze środki komunikacji między ekranami
Pomiędzy działaniami możemy udostępniać tylko pakiet, który może przechowywać prymitywne lub zserializowane dane. Teraz za pomocą fragmentów możemy zaimplementować wzorzec wywołania zwrotnego, w którym na przykład F1 może nasłuchiwać F2 przekazując dowolne obiekty. Proszę spojrzeć na implementację wywołania zwrotnego z poprzednich przykładów, która zwraca z powrotem _Listę
4. Fragmenty budynków są tańsze niż czynności związane z budowaniem
Staje się to oczywiste, gdy używasz szuflady, która ma na przykład 5 pozycji menu i na każdej stronie szuflada powinna być ponownie wyświetlana.
W przypadku nawigacji czysto aktywnościowej, każda strona powinna się napompować i zainicjalizować szufladę, co jest oczywiście drogie.
Na poniższym diagramie możesz zobaczyć kilka fragmentów głównych (FR*), które są fragmentami pełnoekranowymi, do których można uzyskać dostęp bezpośrednio z szuflady, a szuflada jest dostępna tylko wtedy, gdy te fragmenty są wyświetlane. Wszystko, co znajduje się na prawo od przerywanej linii na diagramie, jest przykładem dowolnego schematu nawigacji.
Ponieważ aktywność kontenera przechowuje szufladę, mamy tylko jedną instancję szuflady, więc na każdym kroku nawigacji, w którym szuflada powinna być widoczna, nie musisz jej ponownie napełniać i inicjować. Nadal nie jesteś przekonany, jak to wszystko działa? Spójrz na moją przykładową aplikację, która demonstruje użycie szuflady.
Cons
Moją największą obawą zawsze było to, że jeśli użyję wzorca nawigacji opartego na fragmentach w projekcie, gdzieś po drodze napotkam nieprzewidziany problem, który będzie trudny do rozwiązania w związku ze zwiększoną złożonością fragmentów, bibliotek zewnętrznych i różnych wersji systemu operacyjnego. A jeśli musiałbym przerobić wszystko, co zrobiłem do tej pory?
Rzeczywiście, musiałem rozwiązać problemy z zagnieżdżonymi fragmentami, bibliotekami innych firm, które również używają fragmentów, takich jak ShinobiControls, ViewPagers i FragmentStatePagerAdapters.
Muszę przyznać, że zdobycie wystarczającego doświadczenia z fragmentami, aby móc rozwiązać te problemy, było dość długim procesem. Ale w każdym przypadku problem nie polegał na tym, że filozofia jest zła, ale na tym, że nie zrozumiałem wystarczająco dobrze fragmentów. Może jeśli rozumiesz fragmenty lepiej niż ja, nie napotkasz nawet tych problemów.
Jedynym minusem, o którym mogę teraz wspomnieć, jest to, że nadal możemy napotkać problemy, których rozwiązanie nie byłoby trywialne, ponieważ nie ma tam dojrzałej biblioteki, która prezentuje wszystkie złożone scenariusze złożonej aplikacji z nawigacją opartą na fragmentach.
Wniosek
W tym artykule zobaczyliśmy alternatywny sposób implementacji nawigacji w aplikacji na Androida. Porównaliśmy to z tradycyjną filozofią nawigacji, która wykorzystuje czynności i widzieliśmy kilka dobrych powodów, dla których warto go używać w porównaniu z tradycyjnym podejściem.
Jeśli jeszcze tego nie zrobiłeś, sprawdź implementację aplikacji demonstracyjnej przesłanej do GitHub. Zapraszam do rozwidlenia lub dodawania do niego ładniejszych przykładów, które lepiej pokazują jego zastosowanie.