Przewodnik po architekturze aplikacji

Ten przewodnik zawiera sprawdzone metody i polecaną architekturę tworzenia solidnych aplikacji wysokiej jakości.

Wygoda użytkowników aplikacji mobilnych

Typowa aplikacja na Androida zawiera wiele komponentów aplikacji, w tym aktywności, fragmenty, usługi, dostawców treści i odbiorniki transmisji. Większość tych komponentów aplikacji zgłaszasz w manifeście aplikacji. Na podstawie tego pliku system operacyjny Android podejmuje decyzję, jak zintegrować aplikację z ogólnymi funkcjami urządzenia. Ponieważ zwykła aplikacja na Androida może zawierać wiele komponentów, a użytkownicy często korzystają z wielu aplikacji w krótkim czasie, aplikacje muszą się dostosować do różnych procesów i zadań związanych z działaniami użytkownika.

Pamiętaj, że urządzenia mobilne też mają ograniczone zasoby, więc system operacyjny w każdej chwili może przerwać niektóre procesy aplikacji, żeby zrobić miejsce na nowe.

W warunkach tego środowiska komponenty aplikacji mogą być uruchamiane pojedynczo lub w złej kolejności, a system operacyjny lub użytkownik może je zniszczyć w dowolnym momencie. Zdarzenia te nie są pod Twoją kontrolą, dlatego nie przechowuj ani nie przechowuj w pamięci żadnych danych ani stanu aplikacji w komponentach aplikacji. Komponenty aplikacji nie powinny być od siebie zależne.

Wspólne zasady architektury

Jeśli do przechowywania danych i stanu aplikacji nie warto używać komponentów, to jak zaprojektować aplikację?

W miarę jak aplikacje na Androida rozrastają się, warto zdefiniować architekturę, która umożliwi ich skalowanie, zwiększa jej stabilność i ułatwia testowanie.

Architektura aplikacji określa granice między jej częściami i obowiązkami, jakie powinna mieć każda z nich. Aby spełnić wymienione wyżej potrzeby, zaprojektuj architekturę aplikacji zgodnie z kilkoma konkretnymi zasadami.

Rozdzielenie potencjalnych problemów

Najważniejszą zasadą, której należy przestrzegać, jest oddzielenie potencjalnych klientów. Częstym błędem jest wpisanie całego kodu w obiektach Activity lub Fragment. Te klasy oparte na interfejsie powinny zawierać tylko logikę obsługującą interakcje z interfejsem użytkownika i systemem operacyjnym. Starając się maksymalnie ograniczyć zakres tych klas, możesz uniknąć wielu problemów związanych z cyklem życia komponentów i zwiększyć możliwość testowania tych klas.

Pamiętaj, że nie masz implementacji Activity ani Fragment. Są to tylko klasy klejowe, które reprezentują umowę między systemem operacyjnym Android a Twoją aplikacją. System operacyjny może je zniszczyć w dowolnym momencie na podstawie interakcji użytkowników lub z powodu warunków systemu, takich jak brak pamięci. Aby zadbać o wygodę użytkowników i łatwiejsze zarządzanie aplikacjami, postaraj się zminimalizować ich zależność.

Interfejs Dysku z modeli danych

Inną ważną zasadą jest to, że należy kierować interfejs na modele danych, najlepiej trwałe. Modele danych reprezentują dane aplikacji. Są niezależne od elementów interfejsu i innych komponentów aplikacji. Oznacza to, że nie są powiązane z cyklem życia interfejsu użytkownika i komponentów aplikacji, ale zostaną zniszczone, gdy system operacyjny usunie proces aplikacji z pamięci.

Modele trwałe są idealne z następujących powodów:

  • Użytkownicy nie utracą danych, jeśli system operacyjny Android zniszczy Twoją aplikację, aby zwolnić zasoby.

  • Aplikacja działa nadal wtedy, gdy połączenie sieciowe jest niestabilne lub niedostępne.

Jeśli architektura aplikacji opiera się na klasach modeli danych, sprawia, że aplikacja jest bezpieczniejsza i bardziej niezawodna.

Jedno źródło wiarygodnych danych

Gdy w aplikacji zdefiniujesz nowy typ danych, przypisz do niego pojedyncze źródło prawdy (Single Source of Truth, SSOT). SSOT jest właścicielem tych danych i tylko SSOT może je modyfikować i modyfikować. Aby to osiągnąć, SSOT ujawnia dane za pomocą typu stałego, a w celu modyfikacji danych ujawnia funkcje lub odbiera zdarzenia, które mogą być wywoływane przez inne typy.

Taki schemat zapewnia wiele korzyści:

  • Gromadzi w jednym miejscu wszystkie zmiany konkretnego typu.
  • Chroni ono dane, dzięki czemu inne typy danych nie będą mieć do nich dostępu.
  • Dzięki temu zmiany w danych są łatwiejsze do śledzenia. Dzięki temu łatwiej je wykryć.

W aplikacji działającej w trybie offline źródłem wiarygodnych danych aplikacji jest zwykle baza danych. W niektórych przypadkach źródłem danych może być obiekt ViewModel, a nawet interfejs użytkownika.

Jednokierunkowy przepływ danych

W naszych przewodnikach często stosuje się zasadę pojedynczego źródła danych. W UDF stan przepływa tylko w jednym kierunku. Zdarzenia, które modyfikują przepływ danych w przeciwnym kierunku.

W Androidzie stan lub dane są zazwyczaj przesyłane z typów hierarchii o wyższym zakresie do tych o niższym zakresie. Zdarzenia są zwykle wywoływane od typów o niższym zakresie, aż do momentu osiągnięcia przez nie SSOT odpowiedniego typu danych. Na przykład dane aplikacji zwykle przepływają ze źródeł danych do interfejsu użytkownika. Zdarzenia użytkownika, takie jak naciśnięcia przycisku, przechodzą z interfejsu do SSOT, gdzie dane aplikacji są modyfikowane i udostępniane jako stały typ.

Ten wzorzec zapewnia większą spójność danych, jest mniej podatny na błędy, łatwiej jest debugować i zapewnia wszystkie zalety wzorca SSOT.

W tej sekcji dowiesz się, jak utworzyć strukturę aplikacji zgodnie z zalecanymi sprawdzonymi metodami.

Biorąc pod uwagę ogólne zasady architektury opisane w poprzedniej sekcji, każda aplikacja powinna mieć co najmniej 2 warstwy:

  • Warstwa interfejsu, która wyświetla na ekranie dane aplikacji.
  • Warstwa danych, która 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 interfejsem użytkownika a warstwami danych.

W typowej architekturze aplikacji warstwa interfejsu pobiera dane aplikacji z warstwy danych lub opcjonalnej warstwy domeny, która znajduje się między warstwą interfejsu a warstwą danych.
Rysunek 1. Schemat typowej architektury aplikacji.

Nowoczesna architektura aplikacji

Ta nowoczesna architektura aplikacji zachęca do stosowania między innymi tych technik:

  • Architektura reaktywna i warstwowa.
  • Jednokierunkowy przepływ danych (UDF) we wszystkich warstwach aplikacji.
  • Warstwa interfejsu z uprawnieniami stanów do zarządzania złożonością UI.
  • Korutyny i przepływy.
  • Sprawdzone metody wstrzykiwania zależności.

Więcej informacji znajdziesz w kolejnych sekcjach, na innych stronach dotyczących architektury w spisie treści oraz na stronie rekomendacji, która zawiera podsumowanie najważniejszych sprawdzonych metod.

Warstwa interfejsu

Rola warstwy UI (warstwy prezentacji) to wyświetlanie danych aplikacji na ekranie. Za każdym razem, gdy dane ulegają zmianie (np. w wyniku interakcji użytkownika (np. naciśnięcia przycisku)) lub danych z zewnątrz (np. odpowiedzi sieci), interfejs powinien się zaktualizować, aby uwzględnić te zmiany.

Warstwa interfejsu składa się z 2 elementów:

  • Elementy interfejsu renderujące dane na ekranie. Elementy te tworzysz za pomocą funkcji Views lub Jetpack Compose.
  • Właściciele stanów (np. klasy ViewModel), które przechowują dane, udostępniają je w interfejsie użytkownika i obsługują logikę.
W typowej architekturze elementy interfejsu warstwy UI zależą od stanów, które z kolei zależą od klas z warstwy danych lub opcjonalnej warstwy domeny.
Rysunek 2. Rola warstwy UI w architekturze aplikacji.

Więcej informacji o tej warstwie znajdziesz na stronie warstwy UI.

Warstwa danych

Warstwa danych aplikacji zawiera logikę biznesową. Logika biznesowa nada Twojej aplikacji wartość – składa się z reguł określających sposób, w jaki aplikacja tworzy, przechowuje i zmienia dane.

Warstwa danych składa się z repozytoriów, z których każde może zawierać od 0 do wielu źródeł danych. Utwórz klasę repozytorium dla każdego rodzaju danych, które obsługujesz w swojej aplikacji. Możesz na przykład utworzyć klasę MoviesRepository dla danych związanych z filmami lub PaymentsRepository dla danych związanych z płatnościami.

W typowej architekturze repozytoria warstwy danych udostępniają dane reszcie aplikacji i zależą od źródeł danych.
Rysunek 3. Rola warstwy danych w architekturze aplikacji.

Klasy repozytorium odpowiadają za te zadania:

  • Ujawnienie danych reszcie aplikacji.
  • Centralizacja zmian w danych.
  • rozwiązywanie konfliktów między wieloma źródłami danych;
  • Abstrakcyjne źródła danych z pozostałej części aplikacji.
  • Zawiera logikę biznesową.

Każda klasa źródła danych powinna odpowiadać za pracę z tylko jednym źródłem danych, którym może być plik, źródło sieci lub lokalna baza danych. Klasy źródła danych to most między aplikacją a systemem dla operacji na danych.

Więcej informacji o tej warstwie znajdziesz na stronie warstwy danych.

Warstwa domeny

Warstwa domeny to opcjonalna warstwa znajdująca się między interfejsem użytkownika a warstwami danych.

Warstwa domeny odpowiada za wykorzystywanie złożonej logiki biznesowej lub prostej logiki biznesowej, która jest wykorzystywana ponownie przez wiele modeli widoków. Ta warstwa jest opcjonalna, ponieważ nie wszystkie aplikacje będą spełniać te wymagania. Należy go używać tylko wtedy, gdy jest to konieczne, na przykład w celu obsługi złożoności lub możliwości wielokrotnego użytku.

Jeśli jest włączona, opcjonalna warstwa domeny określa zależności od warstwy interfejsu i zależy od warstwy danych.
Rysunek 4. Rola warstwy domeny w architekturze aplikacji.

Klasy w tej warstwie są często nazywane przypadkami użycia lub interaktorami. W przypadku każdego przypadku użycia należy odpowiadać na pojedynczą funkcję. Na przykład aplikacja może mieć klasę GetTimeZoneUseCase, jeśli wiele obiektów ViewModel używa stref czasowych do wyświetlania odpowiedniego komunikatu na ekranie.

Więcej informacji o tej warstwie znajdziesz na stronie warstwy domeny.

Zarządzanie zależnościami między komponentami

Aby klasy w aplikacji działały prawidłowo, zależą od innych klas. Do zbierania zależności konkretnej klasy możesz użyć dowolnego z tych wzorców projektowych:

  • Wstrzykiwanie zależności: wstrzykiwanie zależności umożliwia klasom definiowanie zależności bez ich tworzenia. W czasie działania odpowiedzi za te zależności odpowiada inna klasa.
  • Lokalizator usług: wzorzec lokalizatora usług udostępnia rejestr, w którym klasy mogą uzyskiwać zależności, zamiast je tworzyć.

Wzorce te umożliwiają skalowanie kodu, ponieważ zapewniają przejrzyste wzorce zarządzania zależnościami bez duplikowania kodu i jego złożoności. Co więcej, wzorce te umożliwiają szybkie przełączanie się między implementacjami testowymi a produkcyjnymi.

Zalecamy śledzenie wzorców wstrzykiwania zależności i korzystanie z biblioteki Hilt w aplikacjach na Androida. Hilt automatycznie konstruuje obiekty, przechodząc po drzewie zależności, gwarantuje czas kompilowania zależności i tworzy kontenery zależności dla klas platformy Androida.

Ogólne sprawdzone metody

Programowanie to dziedzina twórcza, a tworzenie aplikacji na Androida nie jest wyjątkiem. Problem można rozwiązać na wiele sposobów. Można np. przekazać dane między wieloma działaniami lub fragmentami, pobrać dane zdalne i utrwalić je lokalnie w trybie offline lub radzić sobie z dowolnymi innymi typowymi sytuacjami, z którymi mają do czynienia nietrafne aplikacje.

Te rekomendacje nie są obowiązkowe, ale w większości przypadków ich przestrzeganie zwiększa solidność, możliwość testowania i utrzymywania bazy kodu w dłuższej perspektywie:

Nie przechowuj danych w komponentach aplikacji.

Unikaj oznaczania jako źródeł danych punktów wejścia aplikacji, takich jak działania, usługi i odbiorniki transmisji. Zamiast tego powinny one koordynować wyłącznie z innymi komponentami, aby pobierać podzbiór danych odpowiedni dla danego punktu wejścia. Każdy komponent aplikacji jest ważny przez krótki czas, co zależy od interakcji użytkownika z urządzeniem i ogólnego bieżącego stanu systemu.

Zmniejsz zależności od klas Androida.

Komponenty aplikacji powinny być jedynymi klasami, które korzystają z interfejsów API pakietu Android Framework, takich jak Context czy Toast. Oddzielenie od nich innych klas w aplikacji ułatwia testowanie i ogranicza połączenie w niej.

Utwórz wyraźne granice odpowiedzialności pomiędzy różnymi modułami aplikacji.

Nie umieszczaj na przykład kodu, który wczytuje dane z sieci, na wiele klas lub pakietów w bazie kodu. Nie definiuj wielu niepowiązanych ze sobą obowiązków, takich jak przechowywanie danych w pamięci podręcznej i powiązania danych w ramach tej samej klasy. Pomoże Ci w tym zalecana architektura aplikacji.

Udostępniaj jak najmniejszą ilość informacji z każdego modułu.

Nie próbuj na przykład tworzyć skrótu, który będzie ujawniał wewnętrzne szczegóły implementacji z modułu. W krótkiej perspektywie możesz zyskać trochę czasu, ale w miarę ewoluowania bazy kodu wielokrotnie poniesiesz dług technologiczny.

Skup się na wyjątkowym elemencie aplikacji, aby wyróżniała się na tle innych aplikacji.

Nie wymyślaj koła na nowo, ciągle pisząc ten sam kod. Zamiast tego skup się na tym, co sprawia, że Twoja aplikacja jest wyjątkowa, i pozwól bibliotekom Jetpacka oraz innym zalecanym biblioteczkom zająć się powtarzalnym schematem.

Zastanów się, jak umożliwić testowanie każdej części aplikacji z osobna.

Na przykład dobrze zdefiniowany interfejs API do pobierania danych z sieci ułatwia testowanie modułu, który utrwala dane w lokalnej bazie danych. Jeśli zamiast tego pomieszasz logikę z tych 2 modułów w jednym miejscu lub rozpowszechnisz kod sieci w całej bazie kodu, skuteczne testowanie staje się znacznie trudniejsze, a nawet niemożliwe.

Typy są odpowiedzialne za stosowane zasady równoczesności.

Jeśli typ ma długotrwałe blokowanie, powinien odpowiadać za przeniesienie obliczeń do odpowiedniego wątku. Ten konkretny typ wie, jakiego rodzaju obliczenia wykonuje i w którym wątku ma być wykonywane. Typy powinny być bezpieczne w głównym wątku, co oznacza, że można je bezpiecznie wywoływać z wątku głównego bez blokowania go.

Przechowuj jak najwięcej istotnych i aktualnych danych.

Dzięki temu użytkownicy mogą korzystać z funkcji aplikacji nawet wtedy, gdy urządzenie jest w trybie offline. Pamiętaj, że nie wszyscy użytkownicy lubią stałą, szybką łączność, a nawet mogą źle reagować w zatłoczonych miejscach.

Zalety architektury

Wdrożona w aplikacji dobra architektura przynosi wiele korzyści zespołom projektowym i inżynierskim:

  • Poprawia to jakość i stabilność całej aplikacji oraz ułatwia jej konserwację.
  • Umożliwia to skalowanie aplikacji Więcej osób i zespołów może współpracować na tej samej bazie kodu przy minimalnym konfliktach kodu.
  • Pomaga w procesie wdrażania. Architektura zapewnia spójność projektów, dzięki czemu nowi członkowie zespołu mogą szybko zająć się swoimi sprawami i zwiększyć swoją wydajność w krótszym czasie.
  • Testowanie jest łatwiejsze. Dobra architektura bazuje na prostszych typach, które zwykle łatwiej jest testować.
  • Błędy można badać metodologicznie, używając jasno zdefiniowanych procesów.

Inwestowanie w architekturę ma też bezpośredni wpływ na Twoich użytkowników. Zyskują na tym stabilniejszą aplikację i więcej funkcji dzięki bardziej produktywnemu zespołowi inżynierów. Architektura wymaga jednak wczesnej inwestycji. Zapoznaj się z tymi studiami przypadków, w których inne firmy opowiadają o swoich doświadczeniach związanych z tworzeniem dobrej architektury w swojej aplikacji.

Próbki

Poniższe przykłady Google ilustrują prawidłową architekturę aplikacji. Zapoznaj się z nimi, aby zobaczyć te wskazówki w praktyce: