Zalecenia dotyczące architektury Androida

Na tej stronie znajdziesz kilka sprawdzonych metod i zaleceń dotyczących architektury. Stosuj je, aby poprawić jakość, niezawodność i skalowalność aplikacji. Ułatwiają też utrzymanie i testowanie aplikacji.

Poniższe sprawdzone metody są pogrupowane według tematu. Każdy z nich ma priorytet, który odzwierciedla, jak bardzo zespół go zaleca. Lista priorytetów:

  • Zalecane: należy wdrożyć tę metodę, chyba że jest ona sprzeczna z Twoim podejściem.
  • Zalecane: ta metoda prawdopodobnie poprawi jakość aplikacji.
  • Opcjonalnie: w pewnych okolicznościach ta praktyka może poprawić działanie aplikacji.

Architektura warstwowa

Nasza zalecana architektura warstwowa preferuje dzielenie potencjalnych problemów. Interfejs użytkownika jest generowany na podstawie modeli danych, jest zgodny z zasadą pojedynczego źródła informacji oraz przestrzega zasad jednokierunkowego przepływu danych. Oto kilka sprawdzonych metod w przypadku architektury warstwowej:

Rekomendacja Opis
Używaj wyraźnie zdefiniowanej warstwy danych. Warstwa danych udostępnia dane aplikacji reszcie aplikacji i zawiera zdecydowaną większość logiki biznesowej aplikacji.
  • Powinieneś utworzyć repozytoria, nawet jeśli zawierają tylko jedno źródło danych.
  • W małych aplikacjach możesz umieszczać typy warstw danych w pakiecie data lub module.
Używaj wyraźnie zdefiniowanej warstwy interfejsu użytkownika. Warstwa UI wyświetla na ekranie dane aplikacji i służy jako główny punkt interakcji z użytkownikiem.
  • W małych aplikacjach możesz umieszczać typy warstw danych w pakiecie ui lub module.
Więcej sprawdzonych metod dotyczących warstwy interfejsu użytkownika
Warstwa danych powinna udostępniać dane aplikacji za pomocą repozytorium.

Komponenty w warstwie interfejsu, takie jak obiekty kompozycyjne, aktywności czy ViewModels, nie powinny wchodzić w bezpośrednią interakcję ze źródłem danych. Przykłady źródeł danych:

  • Bazy danych, DataStore, SharedPreferences, Firebase API.
  • dostawcy lokalizacji GPS;
  • dostawcy danych Bluetooth;
  • dostawca informacji o stanie połączenia z internetem,
Używaj korobon i przepływów. Do komunikacji między warstwami używaj korobon i przepływów.

Więcej sprawdzonych metod dotyczących coroutines

Użyj warstwy domeny. Użyj warstwy domeny, jeśli chcesz ponownie użyć logiki biznesowej, która współdziała z warstwą danych w różnych widokach ViewModel, lub jeśli chcesz uprościć złożoność logiki biznesowej w konkretnym widoku ViewModel.

Warstwa interfejsu

Rola warstwy interfejsu polega na wyświetlaniu danych aplikacji na ekranie i służeniu za główny punkt interakcji użytkownika. Oto kilka sprawdzonych metod dotyczących warstwy UI:

Rekomendacja Opis
Postępuj zgodnie z jednokierunkowym przepływem danych (UDF). Stosuj zasady jednokierunkowego przepływu danych (UDF), w których ViewModels udostępnia stan interfejsu użytkownika za pomocą wzorca obserwatora i odbiera działania z interfejsu użytkownika przez wywołania metod.
Użyj modeli ViewModel w AAC, jeśli ich zalety pasują do Twojej aplikacji. Użyj ViewModels AAC, aby obsługiwać logikę biznesową, i pobierz dane aplikacji, aby ujawnić stan interfejsu użytkownika (Compose lub widoki Androida).

Więcej sprawdzonych metod dotyczących ViewModel znajdziesz tutaj.

Korzyści płynące z użycia klasy ViewModel

Używaj kolekcji stanu interfejsu uwzględniającej cykl życia. Zbieraj stan interfejsu z interfejsu za pomocą odpowiedniego kreatora coroutine uwzględniającego cykl życia: repeatOnLifecycle w systemie View i collectAsStateWithLifecycle w Jetpack Compose.

Dowiedz się więcej o repeatOnLifecycle.

Dowiedz się więcej na ten temat: collectAsStateWithLifecycle.

Nie wysyłaj zdarzeń z ViewModel do interfejsu użytkownika. Przetwórz zdarzenie natychmiast w modelu ViewModel i wywołaj aktualizację stanu w wyniku obsługi zdarzenia. Więcej informacji o zdarzeniach interfejsu użytkownika.
Użyj aplikacji z jednym działaniem. Jeśli Twoja aplikacja ma więcej niż 1 ekran, do poruszania się między ekranami i precyzyjnych linków do aplikacji używaj fragmentów nawigacji lub kompozycji nawigacyjnych.
Użyj Jetpack Compose. Za pomocą Jetpack Compose możesz tworzyć nowe aplikacje na telefony, tablety, urządzenia składane i Wear OS.

Ten fragment kodu pokazuje, jak pobierać stan interfejsu użytkownika z uwzględnieniem cyklu życia:

Wyświetlenia

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Compose

@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
}

ViewModel

Obiekty ViewModels odpowiadają za udostępnianie stanu interfejsu użytkownika i dostępu do warstwy danych. Oto kilka sprawdzonych metod dotyczących widoków modelu:

Rekomendacja Opis
Modele widoku danych powinny być niezależne od cyklu życia Androida. Obiekty ViewModels nie powinny zawierać odwołania do żadnego typu powiązanego z cyklem życia. Nie przekazuj wartości Activity, Fragment, Context ani Resources jako zależności. Jeśli coś wymaga Context w modelu widoku, musisz zdecydowanie sprawdzić, czy znajduje się on we właściwej warstwie.
Używaj korobon i przepływów.

Model widoku danych współdziała z warstwami danych lub domeny za pomocą:

  • przepływy Kotlin do odbierania danych aplikacji;
  • suspend wykonuje działania przy użyciu viewModelScope.
Używaj widoków ViewModel na poziomie ekranu.

Nie używaj widoków w elementach interfejsu użytkownika, które można ponownie wykorzystać. ViewModels należy używać w tych sytuacjach:

  • komponenty na poziomie ekranu,
  • aktywności lub fragmenty w widokach,
  • Miejsca docelowe lub wykresy podczas korzystania z nawigacji Jetpacka.
W komponentach UI do wielokrotnego użytku używaj zwykłych klas uchwytów stanu. Aby poradzić sobie z zaawansowanymi funkcjami w wielokrotnie używanych komponentach UI, użyj zwykłych klas przechowujących stan. Dzięki temu stan może być podnoszony i kontrolowany zewnętrznie.
Nie używaj AndroidViewModel. Użyj klasy ViewModel, a nie AndroidViewModel. Klasy Application nie należy używać w klasie ViewModel. Zamiast tego przenieś zależność do interfejsu użytkownika lub warstwy danych.
udostępniać stan interfejsu, Modele widoku powinny udostępniać dane interfejsowi za pomocą jednej właściwości o nazwie uiState. Jeśli interfejs użytkownika wyświetla wiele niezwiązanych ze sobą elementów danych, maszyna wirtualna może wyświetlać wiele właściwości stanu interfejsu użytkownika.
  • Zmień rolę uiState jako StateFlow.
  • Jeśli dane pochodzą z potoku danych z innych warstw hierarchii, utwórz element uiState za pomocą operatora stateIn z zasadami WhileSubscribed(5000) (przykład).
  • W prostszych przypadkach, gdy z warstwy danych nie pochodzą żadne strumienie danych, można użyć MutableStateFlow jako niezmiennej zmiennej StateFlow (przykład).
  • Możesz ustawić ${Screen}UiState jako klasę danych, która może zawierać dane, błędy i sygnały wczytywania. Ta klasa może być też klasą zamkniętą, jeśli różne stany są wzajemnie wykluczające się.

Ten fragment kodu pokazuje, jak ujawnić stan UI z interfejsu ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Cykl życia

Oto kilka sprawdzonych metod dotyczących cyklu życia aplikacji na Androida:

Rekomendacja Opis
Nie zastępuj metod cyklu życia w aktywnościach ani fragmentach. Nie zastępuj metod cyklu życia, takich jak onResume, w działaniach lub fragmentach. Zamiast niego użyj LifecycleObserver. Jeśli aplikacja musi wykonać zadanie, gdy cykl życia osiągnie określony Lifecycle.State, użyj interfejsu API repeatOnLifecycle.

W tym fragmencie kodu opisano, jak wykonywać operacje w zależności od stanu cyklu życia:

Wyświetlenia

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Compose

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

Zarządzanie zależnościami

Jest kilka sprawdzonych metod, o których warto pamiętać, zarządzając zależnościami między komponentami:

Rekomendacja Opis
Używaj wstrzykiwania zależności. Stosuj sprawdzone metody korzystania z wstrzykiwania zależności, głównie wstrzykiwania konstruktora, gdy to możliwe.
W razie potrzeby ogranicz zakres do komponentu. Ogranicz dostęp do kontenera zależności, gdy typ zawiera dane, które można zmienić i które trzeba udostępnić, lub gdy typ jest kosztowny w inicjalizacji i jest często używany w aplikacji.
Użyj Hilt. W prostych aplikacjach używaj Hilt lub ręcznego wstrzykiwania zależności. Użyj Hilt, jeśli Twój projekt jest na tyle złożony. Jeśli na przykład masz:
  • Wiele ekranów z użyciem ViewModels – integracja
  • Korzystanie z WorkManagera – integracja
  • Zaawansowane korzystanie z funkcji nawigacji, np. z modeli widoku ograniczonych do grafu nawigacji – integracja.

Testowanie

Oto kilka sprawdzonych metod dotyczących testowania:

Rekomendacja Opis
Co warto przetestować.

Jeśli projekt nie jest tak prosty jak aplikacja „hello world”, należy go przetestować, używając co najmniej:

  • testy jednostkowe ViewModels, w tym Flows;
  • Encje warstwy danych w teście jednostkowym. czyli repozytoria i źródła danych.
  • testy nawigacji w interfejsie użytkownika, które są przydatne jako testy regresji w ciągłym ulepszaniu kodu;
preferować fałszywe treści zamiast mockupów. Więcej informacji znajdziesz w dokumentacji Androida na temat używania podwójnych testów.
Testowanie StateFlow. Podczas testowania StateFlow:

Więcej informacji znajdziesz w przewodniku DAC na temat Androida.

Modele

Podczas tworzenia modeli w aplikacjach należy przestrzegać tych sprawdzonych metod:

Rekomendacja Opis
tworzenie modelu na warstwę w skomplikowanych aplikacjach.

W skomplikowanych aplikacjach, gdy ma to sens, twórz nowe modele na różnych warstwach lub komponentach. Zapoznaj się z tymi przykładami:

  • Źródło danych zdalnych może mapować model, który otrzymuje przez sieć, na prostszą klasę zawierającą tylko dane potrzebne aplikacji.
  • Repozytoria mogą mapować modele DAO na prostsze klasy danych zawierające tylko informacje potrzebne warstwie interfejsu.
  • ViewModel może zawierać modele warstwy danych w klasach UiState.

Konwencje nazewnictwa

Podczas nadawania nazwy kodowi źródłowemu pamiętaj o tych sprawdzonych metodach:

Rekomendacja Opis
Nazwy metod.
Opcjonalnie
Metody powinny być wyrażeniem czasownikowym. Na przykład: makePayment().
Nazywanie właściwości.
Opcjonalny
Właściwości powinny być wyrażeniami rzeczownikowymi. Na przykład: inProgressTopicSelection.
Nazywanie strumieni danych.
Opcjonalnie
Gdy klasa ujawnia strumień Flow, LiveData lub dowolny inny strumień, konwencja nazewnictwa jest zgodna z konwencją get{model}Stream(). Na przykład getAuthorStream(): Flow<Author>Jeśli funkcja zwraca listę modeli, nazwa modelu powinna być w liczbie mnogiej: getAuthorsStream(): Flow<List<Author>>
Implementacje interfejsów nazewnictwa.
Opcjonalnie
Nazwy implementacji interfejsów powinny być zrozumiałe. Jeśli nie możesz znaleźć lepszej nazwy, użyj prefiksu Default. Na przykład w przypadku interfejsu NewsRepository możesz mieć OfflineFirstNewsRepository lub InMemoryNewsRepository. Jeśli nie możesz znaleźć odpowiedniej nazwy, użyj DefaultNewsRepository. Fałszywe implementacje powinny mieć prefiks Fake, np. FakeAuthorsRepository.