Architektura aplikacji to podstawa wysokiej jakości aplikacji na Androida. Dobrze zdefiniowana architektura umożliwia tworzenie skalowalnych aplikacji, które można łatwo utrzymywać i dostosowywać do stale rozwijającego się ekosystemu urządzeń z Androidem, w tym telefonów, tabletów, urządzeń składanych, urządzeń z ChromeOS, wyświetlaczy samochodowych i XR.
Kompozycja aplikacji
Typowa aplikacja na Androida składa się z wielu komponentów, takich jak usługi, dostawcy treści i odbiorniki transmisji. Te komponenty deklarujesz w manifeście aplikacji.
Interfejs aplikacji jest również komponentem. W przeszłości interfejsy użytkownika były tworzone przy użyciu wielu aktywności. Nowoczesne aplikacje korzystają jednak z architektury z jednym działaniem. Pojedynczy Activity służy jako kontener ekranów zaimplementowanych jako fragmenty lub miejsca docelowe Jetpack Compose.
Kilka formatów
Aplikacje mogą działać na różnych urządzeniach, nie tylko na telefonach, ale też na tabletach, urządzeniach składanych, urządzeniach z ChromeOS i innych. Aplikacja nie może zakładać orientacji pionowej ani poziomej. Zmiany konfiguracji, takie jak obrócenie urządzenia lub złożenie i rozłożenie urządzenia składanego, wymuszają ponowne skomponowanie interfejsu aplikacji, co wpływa na dane i stan aplikacji.
Ograniczone zasoby
Urządzenia mobilne – nawet te z dużym ekranem – mają ograniczone zasoby, dlatego w każdej chwili system operacyjny może zatrzymać niektóre procesy aplikacji, aby zrobić miejsce na nowe.
Zmienne warunki uruchomienia
W środowisku o ograniczonych zasobach komponenty aplikacji mogą być uruchamiane pojedynczo i w dowolnej kolejności. Ponadto system operacyjny lub użytkownik mogą je w każdej chwili zniszczyć. Dlatego nie przechowuj w komponentach aplikacji żadnych danych aplikacji ani stanu. Komponenty aplikacji powinny być samodzielne i niezależne od siebie.
Typowe zasady architektury
Jeśli nie możesz używać komponentów aplikacji do przechowywania danych i stanu aplikacji, jak powinna być zaprojektowana Twoja aplikacja?
Wraz ze wzrostem rozmiaru aplikacji na Androida ważne jest zdefiniowanie architektury, która umożliwi jej skalowanie. Dobrze zaprojektowana architektura aplikacji określa granice między poszczególnymi częściami aplikacji i zakres ich odpowiedzialności.
Rozdzielenie odpowiedzialności
Zaprojektuj architekturę aplikacji zgodnie z kilkoma konkretnymi zasadami.
Najważniejsza zasada to podział obowiązków. Częstym błędem jest pisanie całego kodu w Activity lub Fragment.
Głównym zadaniem Activity lub Fragment jest hostowanie interfejsu aplikacji. System Android kontroluje ich cykl życia, często je niszcząc i tworząc ponownie w odpowiedzi na działania użytkownika, takie jak obracanie ekranu, lub zdarzenia systemowe, takie jak niski poziom pamięci.
Ze względu na ich ulotny charakter nie nadają się one do przechowywania danych aplikacji ani stanu. Jeśli przechowujesz dane w Activity lub Fragment, zostaną one utracone, gdy komponent zostanie ponownie utworzony. Aby zapewnić trwałość danych i stabilność działania aplikacji, nie powierzaj stanu tym komponentom interfejsu.
Układy adaptacyjne
Aplikacja powinna prawidłowo obsługiwać zmiany konfiguracji, takie jak zmiana orientacji urządzenia lub rozmiaru okna aplikacji. Wdróż adaptacyjne układy kanoniczne, aby zapewnić optymalny komfort użytkowania na różnych formatach.
Tworzenie interfejsu Dysku na podstawie modeli danych
Kolejną ważną zasadą jest to, że interfejs użytkownika powinien być oparty na modelach danych, najlepiej trwałych. Modele danych reprezentują dane aplikacji. Są one niezależne od elementów interfejsu i innych komponentów aplikacji. Oznacza to, że nie są powiązane z cyklem życia interfejsu i komponentów aplikacji, ale zostaną zniszczone, gdy system operacyjny usunie proces aplikacji z pamięci.
Modele trwałe są idealne z tych powodów:
Użytkownicy nie tracą danych, jeśli system operacyjny Android zniszczy Twoją aplikację, aby zwolnić zasoby.
Aplikacja nadal działa w przypadku, gdy połączenie sieciowe jest przerywane lub niedostępne.
Oprzyj architekturę aplikacji na klasach modelu danych, aby była niezawodna i łatwa do testowania.
Jedno źródło danych
Gdy w aplikacji zdefiniowany zostanie nowy typ danych, należy przypisać do niego jedno źródło danych. SSOT jest właścicielem tych danych i tylko SSOT może je modyfikować lub zmieniać. Aby to osiągnąć, SSOT udostępnia dane za pomocą typu niezmiennego. Aby zmodyfikować dane, SSOT udostępnia funkcje lub odbiera zdarzenia, które mogą wywoływać inne typy.
Ten wzorzec ma wiele zalet:
- Centralizuje wszystkie zmiany dotyczące określonego typu danych w jednym miejscu.
- chroni dane przed zmianami przez inne typy,
- Ułatwia śledzenie zmian w danych, dzięki czemu łatwiej jest wykrywać błędy.
W aplikacji działającej w trybie offline źródłem danych jest zwykle baza danych. W innych przypadkach źródłem informacji może być ViewModel.
Jednokierunkowy przepływ danych
Zasada jednego źródła danych jest często stosowana w przypadku wzorca jednokierunkowego przepływu danych (UDF). W UDF stan przepływa tylko w jednym kierunku, zwykle od komponentu nadrzędnego do komponentu podrzędnego. zdarzenia, które zmieniają przepływ danych w przeciwnym kierunku.
W Androidzie stan lub dane zwykle przepływają z typów o szerszym zakresie w hierarchii do typów o węższym zakresie. Zdarzenia są zwykle wywoływane z typów o mniejszym zakresie, aż osiągną SSOT dla odpowiedniego typu danych. Na przykład dane aplikacji zwykle przepływają ze źródeł danych do interfejsu. Zdarzenia użytkownika, takie jak naciśnięcia przycisków, są przesyłane z interfejsu do SSOT, gdzie dane aplikacji są modyfikowane i udostępniane w niezmiennym typie.
Ten wzorzec lepiej zachowuje spójność danych, jest mniej podatny na błędy, łatwiejszy do debugowania i zapewnia wszystkie zalety wzorca SSOT.
Zalecana architektura aplikacji
Zgodnie z ogólnymi zasadami architektury każda aplikacja powinna mieć co najmniej 2 warstwy:
- Warstwa interfejsu: wyświetla dane aplikacji na ekranie.
- Warstwa danych: zawiera logikę biznesową aplikacji i udostępnia dane aplikacji.
Możesz dodać dodatkową warstwę o nazwie warstwa domeny, aby uprościć i ponownie wykorzystać interakcje między warstwami interfejsu i danych.
Nowoczesna architektura aplikacji
Nowoczesna architektura aplikacji na Androida wykorzystuje m.in. te techniki:
- Adaptacyjna i warstwowa architektura
- Jednokierunkowy przepływ danych (UDF) we wszystkich warstwach aplikacji
- Warstwa interfejsu z zmiennymi stanu, które ułatwiają zarządzanie złożonością interfejsu
- Korutyny i przepływy
- Sprawdzone metody wstrzykiwania zależności
Więcej informacji znajdziesz w artykule Rekomendacje dotyczące architektury Androida.
Warstwa interfejsu
Zadaniem warstwy interfejsu (lub warstwy prezentacji) jest wyświetlanie danych aplikacji na ekranie. Gdy dane ulegną zmianie w wyniku interakcji użytkownika (np. naciśnięcia przycisku) lub danych wejściowych z zewnątrz (np. odpowiedzi sieci), interfejs powinien zostać zaktualizowany, aby odzwierciedlać te zmiany.
Warstwa interfejsu obejmuje 2 rodzaje konstrukcji:
- elementy interfejsu, które renderują dane na ekranie; Te elementy tworzysz za pomocą funkcji Jetpack Compose, aby obsługiwać układy adaptacyjne.
- Zmienne stanu (np.
ViewModel), które przechowują dane, udostępniają je interfejsowi i obsługują logikę.
W przypadku adaptacyjnych interfejsów użytkownika elementy przechowujące stan, takie jak obiekty ViewModel, udostępniają stan interfejsu użytkownika, który dostosowuje się do różnych klas rozmiaru okna. Możesz użyć
currentWindowAdaptiveInfo(), aby uzyskać ten stan interfejsu. Komponenty takie jak
NavigationSuiteScaffold mogą następnie używać tych informacji do automatycznego przełączania się
między różnymi wzorcami nawigacji (np. NavigationBar, NavigationRail lub NavigationDrawer) w zależności od dostępnej przestrzeni na ekranie.
Więcej informacji znajdziesz na stronie warstwy interfejsu.
Warstwa danych
Warstwa danych aplikacji zawiera logikę biznesową. Logika biznesowa nadaje wartość Twojej aplikacji – obejmuje reguły, które określają, jak aplikacja tworzy, przechowuje i zmienia dane.
Warstwa danych składa się z repozytoriów, z których każde może zawierać od zera do wielu źródeł danych. Utwórz klasę repozytorium dla każdego typu danych, które są obsługiwane w aplikacji. Możesz na przykład utworzyć klasę MoviesRepository dla danych związanych z filmami lub klasę PaymentsRepository dla danych związanych z płatnościami.
Klasy repozytorium są odpowiedzialne za:
- udostępnianie danych reszcie aplikacji,
- Centralizowanie zmian w danych
- Rozwiązywanie konfliktów między wieloma źródłami danych
- oddzielenie źródeł danych od reszty aplikacji;
- zawierające logikę biznesową,
Każda klasa źródła danych powinna być odpowiedzialna za pracę tylko z jednym źródłem danych, którym może być plik, źródło sieciowe lub lokalna baza danych. Klasy źródeł danych stanowią pomost między aplikacją a systemem w przypadku operacji na danych.
Więcej informacji znajdziesz na stronie warstwy danych.
Warstwa domeny
Warstwa domeny jest opcjonalną warstwą między warstwami interfejsu i danych.
Warstwa domeny odpowiada za enkapsulację złożonej logiki biznesowej lub prostszej logiki biznesowej, która jest ponownie używana przez wiele modeli widoku. Warstwa domeny jest opcjonalna, ponieważ nie wszystkie aplikacje mają takie wymagania. Używaj jej tylko wtedy, gdy jest to konieczne, np. w przypadku złożonych obliczeń lub gdy chcesz zwiększyć możliwość ponownego użycia.
Klasy w warstwie domeny są zwykle nazywane przypadkami użycia lub interaktorami.
Każdy przypadek użycia powinien odpowiadać za jedną funkcję. Na przykład aplikacja może mieć klasę GetTimeZoneUseCase, jeśli wiele modeli widoku korzysta ze stref czasowych, aby wyświetlać odpowiednie komunikaty na ekranie.
Więcej informacji znajdziesz na stronie warstwy domeny.
Zarządzanie zależnościami między komponentami
Zajęcia w aplikacji zależą od innych zajęć, aby działać prawidłowo. Aby zebrać zależności konkretnej klasy, możesz użyć jednego z tych wzorców projektowych:
- Wstrzykiwanie zależności (DI): wstrzykiwanie zależności umożliwia klasom definiowanie zależności bez ich konstruowania. W czasie działania za dostarczanie tych zależności odpowiada inna klasa.
- Lokalizator usług: wzorzec lokalizatora usług zapewnia rejestr, w którym klasy mogą uzyskiwać swoje zależności zamiast je tworzyć.
Te wzorce pozwalają skalować kod, ponieważ zapewniają jasne wzorce zarządzania zależnościami bez duplikowania kodu ani zwiększania złożoności. Wzorce umożliwiają też szybkie przełączanie się między wdrożeniami testowymi i produkcyjnymi.
Ogólne sprawdzone metody
Programowanie to dziedzina kreatywna, a tworzenie aplikacji na Androida nie jest tu wyjątkiem. Istnieje wiele sposobów rozwiązania problemu. Możesz przesyłać dane między wieloma aktywnościami lub fragmentami, pobierać dane zdalne i przechowywać je lokalnie w trybie offline lub obsługiwać dowolną liczbę innych typowych scenariuszy, z którymi spotykają się nietrywialne aplikacje.
Poniższe zalecenia nie są obowiązkowe, ale w większości przypadków ich stosowanie sprawia, że baza kodu jest bardziej niezawodna, łatwiejsza do testowania i utrzymania.
Nie przechowuj danych w komponentach aplikacji.
Nie wyznaczaj punktów wejścia do aplikacji, takich jak aktywności, usługi i odbiorniki transmisji, jako źródeł danych. Punkty wejścia powinny koordynować działania z innymi komponentami tylko w celu pobrania podzbioru danych, który jest istotny dla danego punktu wejścia. Każdy komponent aplikacji działa krótko, w zależności od interakcji użytkownika z urządzeniem i wydajności systemu.
Ogranicz zależności od klas Androida.
Komponenty aplikacji powinny być jedynymi klasami, które korzystają z interfejsów API pakietu SDK platformy Android, takich jak Context czy Toast. Odseparowanie innych klas w aplikacji od jej komponentów ułatwia testowanie i zmniejsza powiązania w aplikacji.
Określ wyraźne granice odpowiedzialności między modułami w aplikacji.
Nie rozdzielaj kodu, który wczytuje dane z sieci, na wiele klas ani pakietów w bazie kodu. Podobnie nie definiuj w tej samej klasie wielu niezwiązanych ze sobą zadań, takich jak buforowanie danych i wiązanie danych. Pomocne będzie zastosowanie zalecanej architektury aplikacji.
Udostępniaj z każdego modułu jak najmniej informacji.
Nie twórz skrótów, które ujawniają wewnętrzne szczegóły implementacji. Możesz zyskać trochę czasu w krótkim okresie, ale w miarę rozwoju bazy kodu prawdopodobnie wielokrotnie zwiększysz dług techniczny.
Skup się na unikalnych funkcjach aplikacji, aby wyróżnić ją na tle innych.
Nie musisz pisać tego samego kodu wielokrotnie. Zamiast tego skup się na tym, co wyróżnia Twoją aplikację. Pozwól bibliotekom Jetpack i innym polecanym bibliotekom obsługiwać powtarzalne fragmenty kodu.
Korzystaj z kanonicznych układów i wzorców projektowania aplikacji.
Biblioteki Jetpack Compose udostępniają zaawansowane interfejsy API do tworzenia adaptacyjnych interfejsów użytkownika. Używaj w aplikacji układów kanonicznych, aby zoptymalizować wrażenia użytkowników na różnych urządzeniach i ekranach. Przejrzyj galerię wzorców projektowania aplikacji, aby wybrać układy, które najlepiej pasują do Twoich przypadków użycia.
Zachowaj stan interfejsu po zmianach konfiguracji.
Projektując układy adaptacyjne, zachowuj stan interfejsu użytkownika podczas zmian konfiguracji, takich jak zmiana rozmiaru wyświetlacza, składanie i zmiana orientacji. Architektura powinna sprawdzać, czy bieżący stan użytkownika jest zachowany, aby zapewnić mu wygodę.
Projektowanie komponentów interfejsu do wielokrotnego użycia i komponowania.
Twórz komponenty interfejsu, które można ponownie wykorzystywać i łączyć, aby obsługiwać projektowanie adaptacyjne. Dzięki temu możesz łączyć i przekształcać komponenty, aby dopasowywać je do różnych rozmiarów ekranu i orientacji bez konieczności wprowadzania znaczących zmian w kodzie.
Zastanów się, jak sprawić, aby każdą część aplikacji można było testować osobno.
Dobrze zdefiniowany interfejs API do pobierania danych z sieci ułatwia testowanie modułu, który zapisuje te dane w lokalnej bazie danych. Jeśli jednak połączysz logikę tych 2 funkcji w jednym miejscu lub rozprowadzisz kod sieciowy w całym kodzie, testowanie stanie się znacznie trudniejsze, a nawet niemożliwe.
Typy są odpowiedzialne za swoje zasady współbieżności.
Jeśli typ wykonuje długotrwałą pracę blokującą, powinien przenieść obliczenia do odpowiedniego wątku. Typ wie, jaki rodzaj obliczeń wykonuje i w którym wątku powinny być one wykonywane. Typy powinny być bezpieczne dla wątku głównego, co oznacza, że można je wywoływać z wątku głównego bez jego blokowania.
Przechowuj jak najwięcej istotnych i aktualnych danych.
Dzięki temu użytkownicy będą mogli korzystać z funkcji aplikacji nawet wtedy, gdy ich urządzenie jest w trybie offline. Pamiętaj, że nie wszyscy użytkownicy mają stałe połączenie o wysokiej przepustowości, a nawet jeśli tak jest, w zatłoczonych miejscach mogą mieć słaby sygnał.
Korzyści wynikające z architektury
Dobra architektura wdrożona w aplikacji przynosi wiele korzyści projektowi i zespołom inżynieryjnym:
- Poprawia to łatwość utrzymania, jakość i niezawodność całej aplikacji.
- Zezwala aplikacji na skalowanie. Więcej osób i zespołów może pracować nad tym samym kodem przy minimalnej liczbie konfliktów.
- Pomaga w procesie wprowadzania. Architektura zapewnia spójność projektu, dzięki czemu nowi członkowie zespołu mogą szybko nadrobić zaległości i w krótszym czasie pracować wydajniej.
- Łatwiejsze testowanie. Dobra architektura zachęca do stosowania prostszych typów, które są zwykle łatwiejsze do testowania.
- Błędy można badać metodycznie za pomocą dobrze zdefiniowanych procesów.
Inwestycje w architekturę mają też bezpośredni wpływ na użytkowników. Dzięki temu użytkownicy mogą korzystać z bardziej stabilnej aplikacji i większej liczby funkcji. Architektura wymaga jednak początkowej inwestycji czasu. Aby pomóc Ci uzasadnić ten czas przed resztą organizacji, zapoznaj się z tymi studiami przypadku, w których inne firmy dzielą się historiami sukcesu związanymi z dobrą architekturą aplikacji.
Próbki
Poniższe przykłady pokazują dobrą architekturę aplikacji: