Przewodnik po architekturze aplikacji

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

Wrażenia użytkowników aplikacji mobilnych

Typowa aplikacja na Androida zawiera wiele komponentów, w tym aktywności, fragmenty, usługi, dostawców treściodbiorniki transmisji. Większość tych komponentów aplikacji deklarujesz w manifeście aplikacji. System operacyjny Android używa tego pliku, aby określić, jak zintegrować aplikację z ogólnymi wrażeniami użytkownika na urządzeniu. Typowa aplikacja na Androida może zawierać wiele komponentów, a użytkownicy często wchodzą w interakcje z wieloma aplikacjami w krótkim czasie. Aplikacje muszą więc dostosowywać się do różnych rodzajów przepływów pracy i zadań wykonywanych przez użytkowników.

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. Może działać w jednej z nich lub w obu podczas jednej sesji. Zmiany konfiguracji, np. gdy użytkownik zmieni pozycję urządzenia składanego na tryb stołu lub książki, mogą wymusić ponowne skomponowanie interfejsu aplikacji, co może wpłynąć na dane i stan aplikacji.

Ograniczone zasoby

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

Warunki uruchamiania zmiennej

W tych warunkach komponenty aplikacji mogą być uruchamiane pojedynczo i w nieodpowiedniej kolejności, a system operacyjny lub użytkownik mogą je w każdej chwili zniszczyć. Ponieważ nie masz kontroli nad tymi zdarzeniami, nie należy przechowywać ani zachowywać w pamięci żadnych danych aplikacji ani stanu w komponentach aplikacji, a komponenty aplikacji nie powinny być od siebie zależne.

Typowe zasady architektury

Jeśli nie należy używać komponentów aplikacji do przechowywania danych i stanu aplikacji, jak w takim razie zaprojektować aplikację?

Wraz ze wzrostem rozmiaru aplikacji na Androida ważne jest zdefiniowanie architektury, która umożliwi jej skalowanie, dostosowywanie się do różnych kształtów i rozmiarów, zwiększanie odporności i ułatwianie testowania.

Architektura aplikacji określa granice między poszczególnymi częściami aplikacji i odpowiedzialność każdej z nich. Aby spełnić wszystkie wytyczne, musisz zaprojektować architekturę aplikacji zgodnie z kilkoma konkretnymi zasadami.

Rozdzielenie odpowiedzialności

Najważniejszą zasadą jest podział obowiązków. Częstym błędem jest pisanie całego kodu w Activity lub Fragment. Te klasy oparte na interfejsie powinny zawierać tylko logikę obsługującą interakcje z interfejsem i systemem operacyjnym. Dzięki temu, że klasy te będą jak najprostsze, możesz uniknąć wielu problemów związanych z cyklem życia komponentu i zwiększyć możliwość testowania tych klas.

Pamiętaj, że implementacje ActivityFragment nie należą do Ciebie. Są to tylko klasy łączące, które reprezentują umowę między systemem Android a aplikacją. System operacyjny może je w każdej chwili zniszczyć na podstawie interakcji użytkownika lub z powodu warunków systemowych, takich jak mała ilość pamięci. Aby zapewnić zadowalającą wygodę użytkownikom i ułatwić sobie utrzymanie aplikacji, najlepiej jest zminimalizować zależność od tych interfejsów.

Układy adaptacyjne

Aplikacja powinna prawidłowo obsługiwać zmiany konfiguracji, np. gdy użytkownik przełącza się między orientacją pionową i poziomą urządzenia oraz gdy zmienia się rozmiar wyświetlacza, np. gdy aplikacja jest uruchamiana na dużych ekranach. Aplikacja, która implementuje kanoniczne układy adaptacyjne, zapewnia optymalne wrażenia użytkownika na różnych urządzeniach.

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 zdecyduje się usunąć proces aplikacji z pamięci.

Modele trwałe są idealne z tych powodów:

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

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

Jeśli oprzesz architekturę aplikacji na klasach modelu danych, zwiększysz jej testowalność i niezawodność.

Jedno źródło danych

Gdy w aplikacji zdefiniujesz nowy typ danych, przypisz 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, a do modyfikowania danych udostępnia funkcje lub odbiera zdarzenia, które mogą wywoływać inne typy.

Ten wzorzec ma wiele zalet:

  • Umożliwia to centralizację wszystkich zmian dotyczących określonego typu danych w jednym miejscu.
  • Chroni dane przed manipulacją przez inne typy.
  • Ułatwia śledzenie zmian w danych. Dzięki temu łatwiej jest wykryć błędy.

W aplikacji działającej w trybie offline źródłem danych jest zwykle baza danych. W innych przypadkach źródłem prawdy może być ViewModel lub nawet interfejs.

Jednokierunkowy przepływ danych

W naszych przewodnikach często stosujemy zasadę jednego źródła danych w połączeniu z wzorcem jednokierunkowego przepływu danych (UDF). W funkcji UDF stan przepływa tylko w jednym kierunku. Zdarzenia, które modyfikują przepływ danych, działają 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 przynosi wszystkie korzyści wzorca SSOT.

W tej sekcji pokazujemy, jak tworzyć strukturę aplikacji zgodnie z zalecanymi sprawdzonymi metodami.

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

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

W typowym modelu architektury aplikacji warstwa interfejsu użytkownika pobiera dane aplikacji z warstwy danych lub z opcjonalnej warstwy domeny, która znajduje się między warstwą interfejsu użytkownika a warstwą danych.
Rysunek 1. Diagram typowej architektury aplikacji.

Nowoczesna architektura aplikacji

Ta nowoczesna architektura aplikacji zachęca do stosowania m.in. tych metod:

  • Adaptacyjna i warstwowa architektura.
  • Jednokierunkowy przepływ danych (UDF) we wszystkich warstwach aplikacji.
  • Warstwa interfejsu z zmiennymi stanu, która ułatwia zarządzanie złożonością interfejsu.
  • Korutyny i procesy.
  • Sprawdzone metody wstrzykiwania zależności.

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

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 składa się z 2 elementów:

  • elementy interfejsu, które renderują dane na ekranie; Te elementy tworzysz za pomocą widoków lub funkcji Jetpack Compose. Zarówno widoki, jak i Jetpack Compose obsługują układy adaptacyjne.
  • Klasy przechowujące stan (np. klasy ViewModel), które przechowują dane, udostępniają je interfejsowi i obsługują logikę.
W typowym przypadku elementy interfejsu warstwy interfejsu zależą od obiektów przechowujących stan, które z kolei zależą od klas z warstwy danych lub opcjonalnej warstwy domeny.
Rysunek 2. Rola warstwy interfejsu w architekturze aplikacji.

Więcej informacji o tej warstwie znajdziesz na stronie dotyczącej warstwy interfejsu.

Warstwa danych

Warstwa danych aplikacji zawiera logikę biznesową. Logika biznesowa nadaje aplikacji wartość – składa się z reguł, 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, z którymi Twoja aplikacja ma do czynienia. 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.

W typowym modelu architektury repozytoria warstwy danych dostarczają dane do pozostałych części aplikacji i są zależne od źródeł danych.
Rysunek 3. Rola warstwy danych w architekturze aplikacji.

Klasy repozytorium odpowiadają za te zadania:

  • udostępnianie danych pozostałej części aplikacji;
  • centralizowanie zmian w danych;
  • rozwiązywanie konfliktów między wieloma źródłami danych;
  • oddzielanie ź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 o tej warstwie znajdziesz na stronie warstwy danych.

Warstwa domeny

Warstwa domeny to opcjonalna warstwa, która znajduje się między warstwą interfejsu a warstwą danych.

Warstwa domeny odpowiada za enkapsulację złożonej logiki biznesowej lub prostszej logiki biznesowej, która jest ponownie używana przez wiele obiektów ViewModel. Ta warstwa jest opcjonalna, ponieważ nie wszystkie aplikacje mają takie wymagania. Używaj go tylko w razie potrzeby, np. aby poradzić sobie ze złożonością lub zwiększyć możliwość ponownego użycia.

Gdy jest uwzględniona, opcjonalna warstwa domeny zapewnia 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ą 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 obiektów ViewModel zależy od stref czasowych, aby wyświetlać odpowiednie komunikaty na ekranie.

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

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

Klasy w aplikacji zależą od innych klas, 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 udostępnia 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. Ponadto te wzorce umożliwiają szybkie przełączanie się między implementacjami testowymi i produkcyjnymi.

automatycznie tworzy obiekty, przechodząc przez drzewo zależności, zapewnia weryfikację zależności w czasie kompilacji i tworzy kontenery zależności dla klas platformy Android.

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 przestrzeganie sprawia, że baza kodu jest bardziej niezawodna, łatwiejsza do testowania i utrzymania w dłuższej perspektywie:

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. Zamiast tego powinny one tylko koordynować działania z innymi komponentami, aby pobrać podzbiór danych, który jest istotny dla tego punktu wejścia. Każdy komponent aplikacji działa stosunkowo krótko, w zależności od interakcji użytkownika z urządzeniem i ogólnego stanu 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 lub Toast. Odseparowanie innych klas w aplikacji od tych klas ułatwia testowanie i zmniejsza powiązanie w aplikacji.

Określ wyraźne granice odpowiedzialności między modułami w aplikacji.

Na przykład nie rozdzielaj kodu, który wczytuje dane z sieci, na wiele klas lub pakietów w bazie kodu. Podobnie nie definiuj w tej samej klasie wielu niezwiązanych ze sobą obowiązków, takich jak buforowanie danych i wiązanie danych. Pomoże Ci w tym zalecana architektura aplikacji.

Udostępniaj z każdego modułu jak najmniej informacji.

Nie twórz na przykład skrótu, który ujawnia wewnętrzny szczegół implementacji z modułu. 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ę, i pozwól bibliotekom Jetpack i innym zalecanym 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 kanonicznych układów, aby poprawić komfort użytkowania 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.

Zastanów się, jak sprawić, aby każdą część aplikacji można było testować osobno.

Na przykład 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 dwóch modułów w jednym miejscu lub rozproszysz kod sieciowy w całym kodzie, skuteczne 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. Ten konkretny typ wie, jaki rodzaj obliczeń wykonuje i w którym wątku powinien być wykonywany. Typy powinny być bezpieczne dla wątku głównego, co oznacza, że można je 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 ich urządzenie jest w trybie offline. Pamiętaj, że nie wszyscy użytkownicy mają stałe połączenie z szybkim internetem, 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.
  • Umożliwia skalowanie aplikacji. 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 osiągnąć większą wydajność.
  • Łatwiej go przetestować. 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, ponieważ zespół inżynierów jest bardziej wydajny. Architektura wymaga jednak początkowej inwestycji czasu. Aby przekonać do tego resztę firmy, zapoznaj się z tymi studiami przypadku, w których inne firmy dzielą się swoimi historiami sukcesu związanymi z dobrą architekturą aplikacji.

Próbki

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