Nakładanie warstw architektury w Jetpack Compose

Na tej stronie znajdziesz ogólny przegląd warstw architektury, z których składa się Jetpack Compose, oraz podstawowych zasad, które leżą u podstaw tego projektu.

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

  • Używaj odpowiedniego poziomu abstrakcji do tworzenia aplikacji lub biblioteki
  • Dowiedz się, kiedy możesz przejść 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, łącząc funkcje w celu tworzenia komponentów wyższego poziomu. Każda warstwa jest oparta na publicznych interfejsach API warstw niższych, co umożliwia weryfikację granic modułów i w razie potrzeby zastąpienie dowolnej warstwy. Przyjrzyjmy się tym warstwom od dołu.

Czas trwania
Ten moduł zawiera podstawowe informacje o środowisku wykonawczym Compose, takie jak remember, mutableStateOf, adnotacja @ComposableSideEffect. Możesz rozważyć bezpośrednie korzystanie z tej warstwy, jeśli potrzebujesz tylko funkcji zarządzania drzewem w Compose, a nie interfejsu.
Interfejs
Warstwa interfejsu składa się z wielu modułów ( ui-text, ui-graphics, ui-tooling, itp.). Te moduły implementują podstawowe elementy zestawu narzędzi interfejsu, takie jak LayoutNode, Modifier, moduły obsługi danych wejściowych, niestandardowe układy i rysowanie. Możesz rozbudować tę warstwę, jeśli potrzebujesz tylko podstawowych pojęć związanych z zestawem narzędzi interfejsu.
Fundacja
Ten moduł zawiera niezależne od systemu projektowania bloki konstrukcyjne interfejsu Compose, takie jak Row, Column, LazyColumn, rozpoznawanie określonych gestów itp. Możesz wykorzystać warstwę podstawową do utworzenia własnego systemu projektowania.
Materiał
Ten moduł zawiera implementację systemu Material Design dla interfejsu Compose, w tym system motywów, komponenty ze stylami, efekty falowania i ikony. Korzystaj z tej warstwy, gdy używasz Material Design w aplikacji.

Zasady projektowania

Podstawową zasadą Jetpack Compose jest udostępnianie małych, wyspecjalizowanych elementów funkcjonalności, które można łączyć (komponować) ze sobą, zamiast kilku monolitycznych komponentów. Takie podejście ma wiele zalet.

Kontrola

Komponenty wyższego poziomu zwykle wykonują więcej czynności, ale ograniczają zakres bezpośredniej kontroli. Jeśli potrzebujesz większej kontroli, możesz „przejść” 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 koloru szarego, nie możesz tego zrobić za pomocą tego interfejsu API. Zamiast tego możesz użyć interfejsu API niższego poziomu:Animatable

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

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

Dostosowywanie

Składanie komponentów wyższego poziomu z mniejszych elementów ułatwia dostosowywanie komponentów w razie potrzeby. Rozważmy na przykład implementację 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. Materiał Surface zapewniający 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 do użycia.

  4. Row określa domyślne zasady układu treści przycisku.

Aby uprościć strukturę, pominęliśmy niektóre parametry i komentarze, ale cały komponent ma tylko około 40 wierszy kodu, ponieważ po prostu łączy te 4 komponenty w celu wdrożenia przycisku. Komponenty takie jak Buttonokreślają, które parametry mają być udostępniane, aby umożliwić typowe dostosowania, ale jednocześnie nie dopuścić do nadmiernej liczby parametrów, które mogłyby utrudnić korzystanie z komponentu. Komponenty Material Design oferują na przykład dostosowania określone w systemie Material Design, co ułatwia stosowanie zasad tego systemu.

Jeśli jednak chcesz wprowadzić zmiany wykraczające poza parametry komponentu, możesz „zejść” o poziom niżej i utworzyć rozwidlenie komponentu. Na przykład Material Design określa, że przyciski powinny mieć tło w jednolitym kolorze. Jeśli potrzebujesz tła z gradientem, ta opcja nie jest obsługiwana przez parametry Button. W takim przypadku możesz użyć implementacji Material Button jako punktu 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 korzysta z komponentów z warstwy Material, takich jak koncepcje bieżącej wartości alfa treści i bieżącego stylu tekstu. Zastępuje jednak materiał Surface elementem Row i nadaje mu styl, aby uzyskać pożądany wygląd.

Jeśli nie chcesz w ogóle korzystać z koncepcji Material, np. podczas tworzenia własnego systemu projektowania, możesz używać tylko 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 zbudowany na podstawie androidx.compose.foundation.text.BasicText. Dzięki temu możesz podać własną implementację z najbardziej rozpoznawalną nazwą, jeśli chcesz zastąpić wyższe poziomy.

Wybór odpowiedniej abstrakcji

Zgodnie z filozofią Compose, która polega na tworzeniu warstwowych komponentów wielokrotnego użytku, nie zawsze należy sięgać po bloki konstrukcyjne 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 to zrobić od podstaw, korzystając z Modifier.pointerInput. Istnieją jednak inne komponenty wyższego poziomu, które są na nim oparte i mogą stanowić lepszy punkt wyjścia, np. Modifier.draggable, Modifier.scrollable lub Modifier.swipeable.

Z reguły lepiej jest korzystać z 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.