Nakładanie warstw architektury w Jetpack Compose

Na tej stronie znajdziesz ogólne omówienie warstw architektury, które składają się na Jetpack Compose, oraz podstawowych zasad, które przyświecały temu projektowi.

Jetpack Compose nie jest pojedynczym monolitycznym projektem. Składa się z wielu modułów, które są połączone w kompletny stos. Znajomość różnych modułów, z których składa się Jetpack Compose, umożliwia:

  • Używanie odpowiedniego poziomu abstrakcji do tworzenia aplikacji lub biblioteki
  • Zrozum, kiedy możesz „zejść” na niższy poziom, aby uzyskać większą kontrolę lub możliwość dostosowania.
  • Zminimalizuj zależności

Warstwy

Główne warstwy Jetpack Compose to:

Rysunek 1. Główne warstwy Jetpack Compose.

Każda warstwa jest zbudowana na niższych poziomach i łączy funkcje, tworząc komponenty wyższego poziomu. Każda warstwa opiera się na publicznych interfejsach API niższych warstw, aby weryfikować granice modułów i umożliwiać zastąpienie dowolnej warstwy w razie potrzeby. Przyjrzyjmy się tym warstwom od dołu do góry.

Czas trwania
Ten moduł zawiera podstawowe informacje o środowisku wykonawczym Compose, takie jak remember, mutableStateOf, adnotacja @Composable i SideEffect. Możesz rozważyć tworzenie bezpośrednio na tej warstwie, jeśli potrzebujesz tylko możliwości zarządzania drzewem Compose, a nie interfejsu użytkownika.
UI
Warstwa interfejsu składa się z wielu modułów (ui-text, ui-graphics, ui-tooling itd.). Te moduły implementują podstawowe elementy zestawu narzędzi UI, takie jak LayoutNode, Modifier, moduły obsługi danych wejściowych, niestandardowe układy i rysowanie. Możesz rozważyć rozbudowanie tej warstwy, jeśli potrzebujesz tylko podstawowych pojęć związanych z zestawem narzędzi UI.
Fundacja
Ten moduł udostępnia niezależne od systemu projektowania elementy składowe interfejsu Compose, takie jak Row i Column, LazyColumn, rozpoznawanie określonych gestów itp. Możesz wykorzystać warstwę podstawową do utworzenia własnego systemu projektowania.
Materiał
Ten moduł udostępnia implementację systemu projektowania Material Design dla interfejsu Compose, zapewniając system motywów, komponenty ze stylem, wskaźniki efektu fali i ikony. Wykorzystaj tę warstwę, gdy używasz Material Design w swojej aplikacji.

Zasady projektowania

Zasadą przewodnią Jetpack Compose jest dostarczanie małych, ukierunkowanych elementów funkcjonalności, które można ze sobą łączyć (lub komponować), a nie kilku monolitycznych komponentów. Takie podejście ma wiele zalet.

Kontrola

Komponenty wyższego poziomu zwykle wykonują więcej zadań, ale ograniczają zakres bezpośredniej kontroli. Jeśli potrzebujesz większej kontroli, możesz „zejść” do komponentu niższego poziomu.

Jeśli na przykład chcesz animować kolor komponentu, możesz użyć interfejsu API animateColorAsState:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

Jeśli jednak chcesz, aby komponent zawsze zaczynał się od szarego koloru, nie możesz tego zrobić za pomocą tego interfejsu API. Zamiast tego możesz użyć interfejsu API Animatable niższego poziomu:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

Interfejs API animateColorAsState wyższego poziomu jest oparty na interfejsie API Animatable niższego poziomu. Korzystanie z interfejsu API niższego poziomu jest bardziej skomplikowane, ale zapewnia większą kontrolę. Wybierz poziom abstrakcji, który najlepiej odpowiada Twoim potrzebom.

Dostosowywanie

Składanie komponentów wyższego poziomu z mniejszych elementów znacznie ułatwia dostosowywanie komponentów w razie potrzeby. Rozważmy na przykład implementację funkcji Button udostępnianą przez warstwę Material:

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Button składa się z 4 komponentów:

  1. Komponent Material Surface, który zapewnia tło, kształt, obsługę kliknięć itp.

  2. A CompositionLocalProvider który zmienia przezroczystość treści, gdy przycisk jest włączony lub wyłączony.

  3. A ProvideTextStyle ustawia domyślny styl tekstu, który ma być używany.

  4. Row zapewnia domyślną zasadę układu zawartości przycisku

Pominęliśmy niektóre parametry i komentarze, aby struktura była bardziej przejrzysta, ale cały komponent ma tylko około 40 linii kodu, ponieważ po prostu łączy te 4 komponenty w celu zaimplementowania przycisku. Komponenty takie jak Button mają określone parametry, które udostępniają, co pozwala na zachowanie równowagi między umożliwieniem typowych dostosowań a nadmierną liczbą parametrów, które mogą utrudniać korzystanie z komponentu. Na przykład komponenty Material Design oferują dostosowania określone w systemie projektowania Material Design, co ułatwia przestrzeganie zasad Material Design.

Jeśli jednak chcesz dostosować komponent poza jego parametrami, możesz „zejść” o poziom niżej i utworzyć jego rozwidlenie. Na przykład Material Design określa, że przyciski powinny mieć jednolite tło. Jeśli potrzebujesz tła gradientowego, ta opcja nie jest obsługiwana przez parametry Button. W takim przypadku możesz użyć implementacji Material Button jako odniesienia i utworzyć własny komponent:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

Powyższa implementacja nadal używa komponentów z warstwy Material, takich jak koncepcje Material dotyczące bieżącej przezroczystości treści i bieżącego stylu tekstu. Zastępuje jednak materiał Surface materiałem Row i stylizuje go, aby uzyskać pożądany wygląd.

Jeśli nie chcesz w ogóle używać koncepcji Material, na przykład podczas tworzenia własnego systemu projektowania, możesz używać wyłącznie komponentów warstwy podstawowej:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose rezerwuje najprostsze nazwy dla komponentów najwyższego poziomu. Na przykład androidx.compose.material.Text jest oparty na androidx.compose.foundation.text.BasicText. Dzięki temu możesz podać własną implementację z najbardziej wykrywalną nazwą, jeśli chcesz zastąpić wyższe poziomy.

Wybór odpowiedniej abstrakcji

Filozofia Compose polegająca na tworzeniu warstwowych komponentów wielokrotnego użytku oznacza, że nie zawsze musisz sięgać po elementy składowe niższego poziomu. Wiele komponentów wyższego poziomu nie tylko oferuje więcej funkcji, ale często wdraża sprawdzone metody, takie jak obsługa ułatwień dostępu.

Jeśli na przykład chcesz dodać obsługę gestów do komponentu niestandardowego, możesz utworzyć go od podstaw za pomocą Modifier.pointerInput, ale istnieją inne komponenty wyższego poziomu, które mogą być lepszym punktem wyjścia, np. Modifier.draggable, Modifier.scrollable lub Modifier.swipeable.

Zasadniczo zalecamy tworzenie na podstawie komponentu najwyższego poziomu, który oferuje potrzebne funkcje, aby korzystać ze sprawdzonych metod, które zawiera.

Więcej informacji

Przykład tworzenia niestandardowego systemu projektowania znajdziesz w przykładzie Jetsnack.