Zmiana rozmiaru aplikacji na Androida

1. Wprowadzenie

Ekosystem urządzeń z Androidem stale się rozwija. Od początków wbudowanych klawiatur sprzętowych po nowoczesne urządzenia składane, tablety i okna z możliwością dowolnej zmiany rozmiaru – aplikacje na Androida nigdy nie działały na tak zróżnicowanych urządzeniach jak obecnie.

To świetna wiadomość dla deweloperów, ale pewne optymalizacje są konieczne, aby spełnić oczekiwania dotyczące łatwości obsługi i zapewnić użytkownikom doskonałe wrażenia na ekranach o różnych rozmiarach. Zamiast kierować reklamy na każde nowe urządzenie po kolei, elastyczny i adaptacyjny interfejs oraz odporna architektura mogą sprawić, że Twoja aplikacja będzie wyglądać i działać doskonale wszędzie tam, gdzie są obecni i przyszli użytkownicy – na urządzeniach każdej wielkości i o dowolnym kształcie.

Wprowadzenie środowisk Androida z możliwością zmiany rozmiaru to świetny sposób na przetestowanie elastycznego/adaptacyjnego interfejsu użytkownika i przygotowanie go do obsługi na każdym urządzeniu. Z tego modułu dowiesz się, jakie są konsekwencje zmiany rozmiaru aplikacji oraz jakie są sprawdzone metody, aby sprawnie i łatwo zmieniać rozmiar aplikacji.

Co utworzysz

Zapoznasz się z konsekwencjami swobodnej zmiany rozmiaru i zoptymalizujesz aplikację na Androida, aby zademonstrować sprawdzone metody dotyczące zmiany rozmiaru. Twoja aplikacja będzie:

mieć zgodny plik manifestu.

  • Usuwanie ograniczeń, które uniemożliwiają swobodne zmienianie rozmiaru aplikacji

Zachowuj stan po zmianie rozmiaru

  • Zachowuje stan interfejsu po zmianie rozmiaru za pomocą metody rememberSaveable.
  • Unikaj niepotrzebnie duplikowania zadań w tle w celu inicjowania interfejsu użytkownika

Czego potrzebujesz

  1. Wiedza na temat tworzenia podstawowych aplikacji na Androida
  2. Znajomość modelu ViewModel i stanu w narzędziu Compose.
  3. Urządzenie testowe obsługujące swobodną zmianę rozmiaru okna, np.:

Jeśli w trakcie wykonywania tych ćwiczeń natrafisz na błędy (błędy w kodzie, błędy gramatyczne, niejasne sformułowania itp.), zgłoś je, korzystając z linku Zgłoś błąd w lewym dolnym rogu ćwiczenia.

2. Pierwsze kroki

Sklonuj repozytorium z GitHub.

git clone https://github.com/android/large-screen-codelabs/

...lub pobierz plik ZIP repozytorium i rozpakuj go

Zaimportuj projekt

  • Otwórz Android Studio
  • Kliknij Importuj projekt lub Plik > Nowy > Importuj projekt.
  • Przejdź do miejsca, w którym projekt został sklonowany lub wyodrębniony
  • Otwórz folder resizing (zmiana rozmiaru).
  • Otwórz projekt w folderze start. Zawiera on kod startowy.

Wypróbuj aplikację

  • Kompilowanie i uruchamianie aplikacji
  • Zmień rozmiar aplikacji

Co myślisz?

Być może stwierdzisz, że w zależności od zgodności urządzenia testowego z urządzeniami testowymi interfejs nie jest idealny. Nie można zmienić rozmiaru aplikacji. Aplikacja jest zablokowana w początkowym formacie obrazu. Co się dzieje?

Ograniczenia w pliku manifestu

Jeśli spojrzysz w pliku AndroidManifest.xml aplikacji, zobaczysz, że jest kilka dodanych ograniczeń, które uniemożliwiają prawidłowe działanie aplikacji w środowisku o swobodnej zmianie rozmiaru.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Usuń te 3 problematyczne wiersze z pliku manifestu, utwórz aplikację ponownie i spróbuj jeszcze raz na urządzeniu testowym. Zauważysz, że aplikacja nie jest już ograniczona w zakresie swobodnej zmiany rozmiaru. Usunięcie takich ograniczeń z pliku manifestu jest ważnym krokiem optymalizacji aplikacji pod kątem swobodnej zmiany rozmiaru okna.

3. Zmiany w konfiguracji zmiany rozmiaru

Gdy rozmiar okna aplikacji zostanie zmieniony, aktualizowana jest jej konfiguracja. Te zmiany będą miały wpływ na Twoją aplikację. Zrozumienie ich i przewidywanie ich może pomóc w zapewnieniu użytkownikom doskonałych wrażeń. Najbardziej oczywiste zmiany to szerokość i wysokość okna aplikacji, ale wpływają też na format obrazu i orientację.

Obserwowanie zmian konfiguracji

Aby zobaczyć te zmiany zachodzące samodzielnie w aplikacji utworzonej w systemie widoku danych na Androida, możesz zastąpić parametr View.onConfigurationChanged. W Jetpack Compose mamy dostęp do usługi LocalConfiguration.current, która jest automatycznie aktualizowana przy każdym wywołaniu elementu View.onConfigurationChanged.

Aby zobaczyć te zmiany konfiguracji w przykładowej aplikacji, dodaj do swojej aplikacji funkcję kompozycyjną, która wyświetla wartości z LocalConfiguration.current, lub utwórz nowy przykładowy projekt z takim elementem kompozycyjnym. Przykładowy interfejs użytkownika, który pozwala je zobaczyć, będzie wyglądał tak:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Przykład implementacji znajdziesz w folderze projektu observing-configuration-changes. Spróbuj dodać ten parametr do interfejsu swojej aplikacji, uruchom go na urządzeniu testowym i obserwuj, jak zmienia się interfejs użytkownika, w miarę jak zmienia się konfiguracja aplikacji.

podczas zmiany rozmiaru aplikacji w interfejsie aplikacji wyświetlane są w czasie rzeczywistym informacje o konfiguracji

Te zmiany w konfiguracji aplikacji umożliwiają symulowanie szybkiego przejścia od ekstremalnych standardów z podzielonego ekranu na małego telefonu do pełnego ekranu na tablecie lub komputerze. Jest to nie tylko dobry sposób na przetestowanie układu aplikacji na różnych ekranach, ale też na sprawdzenie, jak radzi sobie z nagłymi zdarzeniami zmiany konfiguracji.

4. Rejestrowanie zdarzeń cyklu życia aktywności

Inną konsekwencją swobodnej zmiany rozmiaru okna aplikacji są różne zmiany w cyklu życia aplikacji Activity. Aby widzieć te zmiany w czasie rzeczywistym, dodaj obserwator cyklu życia do metody onCreate i zarejestruj każde nowe zdarzenie cyklu życia, zastępując wartość onStateChanged.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

Gdy to zrobisz, ponownie uruchom aplikację na urządzeniu testowym i sprawdź parametr logcat, gdy chcesz ją zminimalizować i ponownie wyświetlić na pierwszym planie.

Zwróć uwagę, że aplikacja jest wstrzymywana po zminimalizowaniu i wznowiona po umieszczeniu na pierwszym planie. Ma to wpływ na Twoją aplikację. Omówimy to w następnej sekcji tego ćwiczenia z programowania, w którym skupimy się na ciągłości.

Logcat pokazuje metody cyklu życia aktywności wywoływane przy zmianie rozmiaru

Teraz przyjrzyj się narzędziu Logcat, aby zobaczyć, które wywołania zwrotne cyklu życia aktywności są wywoływane przy zmianie rozmiaru aplikacji z najmniejszego do największego możliwego rozmiaru.

Zależnie od urządzenia testowego możesz zaobserwować różne zachowania, ale prawdopodobnie zauważasz, że Twoja aktywność jest niszczona i odtwarzana, gdy rozmiar okna aplikacji ulega znacznej zmianie, ale nie wtedy, gdy nastąpi nieznaczna zmiana. Dzieje się tak, ponieważ w przypadku interfejsu API w wersji 24 i nowszych tylko znaczne zmiany rozmiaru skutkują odtworzeniem Activity.

Zaobserwowaliśmy już kilka częstych zmian w konfiguracji, których można się spodziewać w środowisku eksploracji swobodnej, ale musisz też pamiętać o innych. Jeśli np. do urządzenia testowego jest podłączony zewnętrzny monitor, możesz zobaczyć, że Activity został zniszczony i odtworzony, aby uwzględnić zmiany w konfiguracji takie jak gęstość wyświetlacza.

Aby uprościć niektóre złożoności związane ze zmianami w konfiguracji, zaimplementuj adaptacyjny interfejs za pomocą interfejsów API wyższego poziomu, takich jak WindowSizeClass. Zobacz też Obsługa różnych rozmiarów ekranów.

5. Continuity – zachowanie utworów kompozycyjnych stan wewnętrzny po zmianie rozmiaru

W poprzedniej sekcji pokazaliśmy pewne zmiany w konfiguracji, których aplikacja może oczekiwać w środowisku swobodnej zmiany rozmiaru okna. W tej sekcji będziesz utrzymywać stan interfejsu aplikacji przez cały czas w ramach tych zmian.

Zacznij od rozwinięcia funkcji kompozycyjnej NavigationDrawerHeader (dostępnej w języku ReplyHomeScreen.kt), tak aby po kliknięciu wyświetlała się adres e-mail.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

Gdy dodasz rozwijany nagłówek do aplikacji,

  1. uruchom aplikację na urządzeniu testowym
  2. kliknij nagłówek, aby go rozwinąć
  3. spróbuj zmienić rozmiar okna

Po znacznym zmianie rozmiaru nagłówek straci swój stan.

Dotykając nagłówka w panelu nawigacji aplikacji, rozwija się on, ale zwija po zmianie rozmiaru aplikacji.

Stan interfejsu jest tracony z powodu tego, że remember pomaga w utrzymaniu stanu w przypadku przekompozycji, ale nie w ramach działań lub odtworzeń procesów. Często używane jest przenoszenie stanu, czyli przenoszenie stanu do wywołującego elementu kompozycyjnego, aby elementy kompozycyjne stały się bezstanowe, co może całkowicie uniknąć tego problemu. Możesz używać remember w niektórych miejscach, jeśli stan elementu interfejsu jest wewnętrzny dla funkcji kompozycyjnych.

Aby rozwiązać te problemy, zastąp remember elementem rememberSaveable. To działa, ponieważ rememberSaveable zapisuje i przywraca zapamiętaną wartość w polu savedInstanceState. Zmień remember na rememberSaveable, uruchom aplikację na urządzeniu testowym i ponownie spróbuj zmienić rozmiar aplikacji. Zauważysz, że podczas zmiany rozmiaru stan nagłówka rozwijanego jest zachowywany zgodnie z oczekiwaniami.

6. Unikanie niepotrzebnego powielania pracy w tle

Wiesz już, jak używać rememberSaveable do zachowania elementów kompozycyjnych wewnętrzny stan interfejsu użytkownika w postaci zmian konfiguracji, które mogą się często pojawiać w wyniku dowolnej zmiany rozmiaru okna. Należy jednak często przenosić stan i logikę interfejsu z elementów kompozycyjnych. Przeniesienie własności stanu do modelu widoku danych to jeden z najlepszych sposobów na zachowanie stanu podczas zmiany rozmiaru. Gdy przeniesiesz stan do systemu ViewModel, mogą wystąpić problemy z długotrwałą pracą w tle, np. intensywny dostęp do systemu plików lub wywołania sieciowe niezbędne do zainicjowania ekranu.

Aby zobaczyć przykład problemów, które mogą wystąpić, dodaj instrukcję logu do metody initializeUIState w ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Teraz uruchom aplikację na urządzeniu testowym i kilka razy zmień rozmiar okna aplikacji.

Gdy otworzysz Logcat, zobaczysz, że Twoja aplikacja wykazuje kilka uruchomień metody inicjowania. Może to być problem w przypadku pracy, którą chcesz uruchomić tylko raz w celu zainicjowania interfejsu użytkownika. Dodatkowe wywołania sieciowe, operacje wejścia-wyjścia pliku i inne czynności mogą obniżać wydajność urządzenia i powodować inne niezamierzone problemy.

Aby uniknąć niepotrzebnej pracy w tle, usuń wywołanie funkcji initializeUIState() z metody onCreate() aktywności. Zamiast tego zainicjuj dane w metodzie init ViewModel. Dzięki temu przy pierwszym tworzeniu instancji ReplyViewModel metoda inicjowania będzie uruchamiana tylko raz:

init {
    initializeUIState()
}

Spróbuj uruchomić aplikację jeszcze raz, a niepotrzebne symulowane zadanie inicjowania zostanie uruchomione tylko raz, niezależnie od tego, ile razy zmienisz rozmiar okna aplikacji. Dzieje się tak, ponieważ modele ViewModels utrzymują się poza cyklem życia modelu Activity. Jeśli kod jest inicjowany tylko raz przy tworzeniu elementu ViewModel, oddzielamy go od wszystkich odtworzeń Activity i zapobiegamy niepotrzebnej pracy. Gdyby w rzeczywistości takie wywołanie serwera było kosztownym wywołaniem serwera lub intensywną operacją wejścia-wyjścia pliku w celu zainicjowania interfejsu użytkownika, zaoszczędzilibyśmy znacznej ilości zasobów i poprawili komfort użytkowania.

7. GRATULUJEMY!

Udało się! Dobra robota! Masz już za sobą wdrożenie kilku sprawdzonych metod, które pomogą Ci umożliwić aplikacjom na Androida odpowiednią zmianę rozmiaru w ChromeOS i innych środowiskach, w których można korzystać z wielu okien i ekranów.

Przykładowy kod źródłowy

Klonowanie repozytorium z GitHuba

git clone https://github.com/android/large-screen-codelabs/

...lub pobierz plik ZIP repozytorium i rozpakuj go