09A  | B  | C  | D  | E  | F  | G  | H  | I  | J  | K  | L  | M  | N  | O  | P  | Q  | R  | S  | T  | U  | V  | W  | X  | Z  |



Android 13




Pierwsze kroki z umiejętnościami programowania nowoczesnych systemów Android

System operacyjny Android (OS) to jedna z najpopularniejszych platform dla urządzeń mobilnych, z której korzysta wielu użytkowników na całym świecie. System operacyjny jest używany w samochodach i urządzeniach do noszenia, takich jak inteligentne zegarki, telewizory i telefony, co sprawia, że rynek jest dość szeroki dla programistów Androida. Dlatego nowi programiści muszą nauczyć się tworzyć aplikacje na Androida z wykorzystaniem nowych umiejętności w zakresie nowoczesnego programowania na Androidzie (MAD). Android przeszedł długą drogę od czasu jego premiery w 2008 roku i użycia go w pierwszych zintegrowanych środowiskach programistycznych (IDE), Eclipse i NetBeans. Obecnie zalecanym środowiskiem programistycznym dla systemu Android jest Android Studio. W odróżnieniu od wcześniejszych wersji, gdy preferowanym językiem była Java, obecnie preferowanym językiem jest Kotlin. Android Studio obsługuje Kotlin, Java, C++ i inne języki programowania, dzięki czemu to środowisko IDE jest odpowiednie dla programistów o różnych umiejętnościach. Dlatego pod koniec tego rozdziału, postępując zgodnie z przepisami, będziesz miał zainstalowane Android Studio, zbudujesz swoją pierwszą aplikację na Androida przy użyciu Jetpack Compose i nauczysz się składni Kotlina, wykorzystując preferowany język do programowania Androida. Ponadto to wprowadzenie przygotuje dla Ciebie podstawę do zrozumienia zaawansowanego materiału, który będzie kluczowy dla MAD. W tej części omówimy następujące przepisy:

•  Napisanie pierwszego programu w Kotlinie z wykorzystaniem zmiennych i idiomów
•  Tworzenie aplikacji Hello, Android Community przy użyciu Android Studio
•  Konfigurowanie emulatora w Android Studio
•  Tworzenie przycisku w Jetpack Compose
•  Używanie poleceń gradlew do czyszczenia i uruchamiania projektu w Android Studio
•  Zrozumienie struktury projektu Android
•  Debugowanie i logowanie w Android Studio

Wymagania techniczne

Pomyślne uruchomienie Androida IDE i emulatora może być trudne dla Twojego komputera. Być może słyszeliście dowcip o tym, jak maszyny z Androidem Studio mogą służyć zimą jako grzejniki. Cóż, jest w tym trochę prawdy, więc twój komputer powinien mieć następujące specyfikacje, aby mieć pewność, że twój system poradzi sobie z wymaganiami IDE:

•  Zainstalowany 64-bitowy system Microsoft Windows, macOS lub Linux i stabilne połączenie internetowe. Przepisy zostały opracowane w systemie macOS. Możesz także używać laptopa z systemem Windows lub Linux, ponieważ nie ma różnicy między używaniem obu.
•  Użytkownicy systemów Windows i Linux mogą kliknąć ten link, aby zainstalować Android Studio: https://developer.android.com/studio/install.
•  Minimum 8 GB pamięci RAM lub więcej.
•  Minimum 8 GB wolnego miejsca na dysku dla Android Studio, zestawu Android Software Development Kit (SDK) i emulatora Androida.
•  Preferowana jest minimalna rozdzielczość ekranu 1280 x 800.
•  Możesz pobrać Android Studio pod adresem https://developer.android.com/studio


Napisanie pierwszego programu w Kotlinie z wykorzystaniem zmiennych i idiomów

Kotlin jest zalecanym językiem do programowania na Androidzie; nadal możesz używać języka Java jako wybranego języka, ponieważ wiele starszych aplikacji w dalszym ciągu w dużym stopniu opiera się na języku Java. Jednakże będziemy używać Kotlina i jeśli po raz pierwszy tworzysz aplikacje na Androida przy użyciu języka Kotlin, organizacja Kotlin ma doskonałe zasoby, które pomogą Ci rozpocząć od bezpłatnych ćwiczeń praktycznych i samodzielnych ocen zwanych Kotlin Koany (https://play.kotlinlang.org/koans/overview). Ponadto możesz używać języka Kotlin do programowania wieloplatformowego za pomocą Kotlin Multiplatform Mobile (KMM), w którym możesz udostępniać standardowy kod między aplikacjami na iOS i Androida oraz pisać kod specyficzny dla platformy tylko tam, gdzie jest to konieczne. KMM jest obecnie w fazie Alpha.

Przygotowywanie się

W tym przepisie możesz użyć internetowego placu zabaw Kotlin (https://play.kotlinlang.org/), aby uruchomić swój kod, lub uruchomić kod w swoim środowisku IDE Android Studio. Alternatywnie możesz pobrać i używać IntelliJ IDEA IDE, jeśli planujesz wykonywać więcej pytań praktycznych dotyczących Kotlina za pomocą Koans.

Jak to zrobić…

W tym przepisie będziemy eksplorować i modyfikować prosty program, który napiszemy w Kotlinie; możesz pomyśleć o programie jako o instrukcjach, które wydajemy komputerowi lub urządzeniom mobilnym, aby wykonywał określone przez nas czynności. Na przykład utworzymy powitanie w naszym programie, a później napiszemy inny program. W przypadku tego przepisu możesz wybrać Android Studio lub darmowe IDE online, ponieważ omówimy niektóre funkcjonalności Kotlina:

1. Jeśli po raz pierwszy zdecydujesz się skorzystać z internetowego placu zabaw Kotlin, zobaczysz zrzut ekranu podobny do poniższego, z instrukcją println, która mówi Witaj, świecie, ale w naszym przykładzie zmienimy to powitanie na Witaj, społeczność Androida i uruchom kod.



2. Spójrzmy na inny przykład; popularny problem algorytmu stosowany w wywiadach - odwracanie ciągu znaków. Na przykład masz ciąg Społeczność i chcemy odwrócić ciąg, tak aby wynikiem było ytinummoC. Istnieje kilka sposobów rozwiązania tego problemu, ale rozwiążemy go za pomocą idiomatycznego sposobu Kotlina.

3. Wprowadź następujący kod na placu zabaw swojego IDE lub na placu zabaw Kotlina:

fun main() {
val stringToBeReversed = "Community"
println(reverseString(stringToBeReversed))
}
fun reverseString(stringToReverse: String): String { return stringToReverse.reversed()
}

Jak to działa…

W Kotlinie należy koniecznie wspomnieć, że istnieją unikalne sposoby na utrzymanie czystszego, bardziej precyzyjnego i prostszego kodu poprzez wykorzystanie domyślnej wartości parametru i ustawienie tylko tych parametrów, które chcesz zmienić. zabawa to słowo w języku programowania Kotlin oznaczające funkcję, a funkcja w języku Kotlin to sekcja programu, która wykonuje określone zadanie. Nazwa funkcji w naszym pierwszym przykładzie to main(), a w naszej funkcji main() nie mamy żadnych danych wejściowych. Ogólnie rzecz biorąc, funkcje mają nazwy, abyśmy mogli je od siebie odróżnić, jeśli nasza baza kodu jest złożona. Ponadto w Javie funkcja jest podobna do metody. Nazwa funkcji składa się z dwóch nawiasów i nawiasów klamrowych oraz funkcji println, która informuje system o wydrukowaniu wiersza tekstu. Jeśli korzystałeś z języka Java, możesz zauważyć, że język programowania Kotlin jest bardzo podobny do języka Java. Jednak programiści mówią teraz o tym, jak wspaniały jest język Kotlin dla programistów, ponieważ zapewnia bardziej wyrazistą składnię i wyrafinowane systemy typów oraz radzi sobie z problemem wskaźnika zerowego, z którym Java borykała się przez wiele lat. Aby w pełni wykorzystać możliwości języka Kotlin i napisać bardziej zwięzły kod, korzystna może być znajomość idiomów Kotlina. Idiomy Kotlina to często używane kolekcje, które pomagają manipulować danymi i ułatwiają pracę programistów Androida. W naszym drugim przykładzie mamy dwie funkcje: main() i ReverseString(). main() nie pobiera żadnych danych wejściowych, ale ReverseString() pobiera dane wejściowe typu String. Zauważysz również, że używamy val, które jest unikalnym słowem używanym przez Kotlina w odniesieniu do niezmiennej wartości, którą można ustawić tylko na jedną wartość, w porównaniu do var, który jest zmienną zmienną, co oznacza, że można z niej zrezygnować. Tworzymy wartość stringToBeReversed, która jest ciągiem znaków i nazywamy ją "Społeczność", następnie wywołujemy println w funkcji main() i przekazujemy tekst, który chcemy wydrukować, w naszej funkcji ReversingString(). Co więcej, w tym przykładzie nasza funkcja ReverseString pobiera argument typu string z obiektu String, a następnie zwraca typ ciągu.



Jest jeszcze wiele do nauczenia się i należy przyznać, że to, co omówiliśmy w tym przepisie, to tylko niewielka część tego, co można zrobić z idiomami Kotlina. Ten przepis miał na celu wprowadzenie koncepcji, które moglibyśmy poruszyć lub wykorzystać w późniejszych rozdziałach, jednak nie było to szczegółowe, ponieważ w późniejszych rozdziałach przyjrzymy się bliżej Kotlinowi. Dlatego warto wiedzieć, czym są idiomy Kotlina i dlaczego są obecnie niezbędne.

Tworzenie aplikacji Hello, Android Community przy użyciu Android Studio

Pierwszą aplikację na Androida stworzymy teraz, gdy mamy zainstalowane Android Studio. Ponadto użyjemy funkcji Compose - żeby wspomnieć z góry, w tym przepisie nie będziemy szczegółowo omawiać funkcji Compose, ponieważ mamy poświęcony jej Część

Przygotowywanie się

Zanim zaczniesz, warto wiedzieć, gdzie znajdują się Twoje projekty na Androida, aby zachować spójność. Domyślnie Android Studio tworzy pakiet w Twoim katalogu domowym, a nazwa pakietu to AndroidStudioProjects; tutaj znajdziesz wszystkie projekty, które tworzysz. Możesz także zdecydować, gdzie powinien znajdować się folder, jeśli chcesz go zmienić. Ponadto upewnij się, że używasz najnowszej wersji Android Studio, aby móc korzystać ze wszystkich wspaniałych funkcji. Aby dowiedzieć się, jaka jest najnowsza wersja Androida, możesz skorzystać z poniższego linku: https://developer.android.com/studio/releases.

Jak to zrobić…

W Android Studio IDE szablon projektu to aplikacja na Androida, która zawiera wszystkie niezbędne części do utworzenia aplikacji i pomaga w rozpoczęciu i konfiguracji. Zatem krok po kroku stworzymy naszą pierwszą aplikację na Androida i uruchomimy ją na emulatorze:

1. Uruchom Android Studio, klikając ikonę Android Studio w swoim doku lub w dowolnym miejscu, w którym zapisałeś Android Studio.
2. Otworzy się powitalne okno Android Studio, w którym możesz kliknąć Nowy projekt. Alternatywnie możesz przejść do Plik i kliknąć Nowy projekt.
3. Wybierz opcję Opróżnij czynność tworzenia i kliknij Dalej.
4. Po załadowaniu pustego ekranu aktywności tworzenia zobaczysz pola zawierające nazwę, nazwę pakietu, lokalizację zapisu, język i minimalny pakiet SDK. Na nasze potrzeby możesz nazwać projekt Android Community i pozostawić pozostałe ustawienia bez zmian. Zauważysz również, że domyślnym językiem jest Kotlin. Jeśli chodzi o Minimum SDK, naszym celem jest API 21: Android 5.0 (Lollipop), które wskazuje minimalną wersję Androida, na której może działać Twoja aplikacja, czyli w naszym przypadku około 98,8% urządzeń. Możesz także kliknąć menu rozwijane i dowiedzieć się więcej o minimalnym pakiecie SDK. Kliknij Zakończ i poczekaj, aż Gradle się zsynchronizuje.
5. Śmiało, pobaw się pakietami, a zauważysz klasę MainActivity, która rozszerza metodę ComponentActivity(), która rozszerza Activity(); wewnątrz mamy zabawę w onCreate, która jest nadpisaniem z ComponentActivity. Zobaczysz także setContent{}, która jest funkcją używaną do ustawiania zawartości funkcji Composable. Funkcja setContent{} pobiera wyrażenie lambda zawierające elementy interfejsu użytkownika, które powinny zostać wyświetlone i w naszym przypadku przechowuje motyw naszej aplikacji. W funkcji Powitanie() zmienimy to, co jest zapewnione i dodamy własne powitanie, czyli "Hello, Android Community" i uruchommy, i utworzymy nasze pierwsze powitanie:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
AndroidCommunityTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color =
MaterialTheme.colors.background
) {
Greeting("Hello, Android
Community")
}
}
}
}
}

6. Przejdźmy dalej i zmodyfikujmy funkcję Greeting() i przypiszmy argument name do tekstu:

@Composable
fun Greeting(name: String) {
Text(
text = name
)
}

Ponadto możesz po prostu przekazać "Hello, Android Community" do domyślnej implementacji, co spowoduje utworzenie tego samego interfejsu użytkownika.
7. Podobnie jak w widoku XML, możesz łatwo przeglądać tworzony interfejs użytkownika bez uruchamiania aplikacji w emulatorze, używając @Preview(showBackground = true), więc przejdźmy dalej i dodajmy to do naszego kodu, jeśli nie jest on dostępny. Domyślnie projekt zawiera szablon z funkcją Preview():

a Preview():
@Preview(showBackground = true) @Composable fun DefaultPreview() {
}

8. Na koniec po uruchomieniu aplikacji powinien pojawić się ekran jak na rysunku. W poniższym przepisie przyjrzymy się, jak krok po kroku skonfigurować emulator, więc nie martw się tym jeszcze.



Jak to działa…

Kluczową zaletą korzystania z Jetpack Compose do tworzenia widoku jest to, że przyspiesza czas programowania, ponieważ używasz tego samego języka do pisania całej bazy kodu (Kotlin) i łatwiej jest go testować. Możesz także tworzyć komponenty wielokrotnego użytku, które możesz dostosować do swoich potrzeb. Dlatego zapewnienie mniejszego ryzyka błędów i konieczność pisania widoków w formacie XML, ponieważ jest to żmudne i kłopotliwe. Funkcja onCreate() jest uważana za punkt wejścia do aplikacji w systemie Android. Ponadto używamy funkcji modyfikujących, aby dodać zachowanie i ozdobić komponowany. O tym, co potrafią modyfikatory i funkcje Surface, porozmawiamy więcej w następnej części.

Konfigurowanie emulatora w Android Studio

Android Studio to niezawodne i dojrzałe IDE. W rezultacie Android Studio jest preferowanym IDE do tworzenia aplikacji na Androida od 2014 roku. Oczywiście nadal możesz używać innych IDE, ale zaletą Android Studio jest to, że nie musisz instalować osobno pakietu Android SDK.

Przygotowywanie się

Aby móc zastosować się do tego przepisu, musisz wykonać poprzedni przepis, ponieważ będziemy konfigurować nasz emulator w celu uruchomienia właśnie utworzonego projektu.

Jak to zrobić…

Ta część ma być przyjazna dla początkujących, a także płynnie przejść do bardziej zaawansowanego Androida w miarę pracy z przepisami. Wykonajmy następujące kroki, aby zobaczyć, jak skonfigurować emulator i uruchomić projekt w przepisie Tworzenie aplikacji społecznościowej Hello, Android przy użyciu Android Studio:

1. Przejdź do Narzędzia | Menadżer urządzeń. Gdy menedżer urządzeń będzie gotowy, masz dwie opcje: wirtualną lub fizyczną. Wirtualny oznacza, że będziesz używać emulatora, a Fizyczny oznacza, że umożliwisz swojemu telefonowi z Androidem debugowanie aplikacji na Androida. Dla naszych celów wybierzemy Wirtualny.
2. Kliknij opcję Utwórz urządzenie. Pojawi się ekran konfiguracji urządzenia wirtualnego.
3. Wybierz telefon. Zauważysz, że Android Studio ma inne kategorie, takie jak telewizor, Wear OS, tablet i motoryzacja. Na razie użyjmy Phone′a, a w następnym rozdziale spróbujemy użyć Wear OS. Kliknij Dalej.
4. Na rysunku zobaczysz listę zalecanych obrazów systemów. Możesz wybrać dowolny lub użyć domyślnego, czyli S w naszym przypadku dla Androida 12, chociaż możesz chcieć użyć najnowszego API 33, a następnie kliknąć Dalej



5. Dojdziesz teraz do ekranu urządzenia wirtualnego Android (AVD), na którym możesz nadać nazwę swojemu urządzeniu wirtualnemu. Możesz wprowadzić nazwę lub po prostu pozostawić domyślną nazwę Pixel 2 API 31, a następnie nacisnąć Zakończ.
6. Przetestuj swoje urządzenie wirtualne, uruchamiając je i upewnij się, że działa zgodnie z oczekiwaniami. Po uruchomieniu aplikacji na emulatorze powinieneś zobaczyć coś podobnego do rysunku.



Ważna uwaga

Aby utworzyć fizyczne urządzenie testujące, przejdź do Ustawień na swoim telefonie z Androidem i wybierz Informacje o telefonie | Informacje o oprogramowaniu | Numer kompilacji i puszczaj przycisk, przytrzymaj go, aż zobaczysz, że dzielą Cię teraz cztery kroki od zostania programistą. Po zakończeniu zliczania zobaczysz powiadomienie z informacją, że opcje programisty zostały pomyślnie włączone. Wszystko, czego teraz potrzebujesz, to użyć uniwersalnej magistrali szeregowej (USB) i włączyć debugowanie USB. Wreszcie zobaczysz, że Twój telefon fizyczny jest gotowy do testów.

Jak to działa…

Testowanie i upewnianie się, że aplikacje wyświetlają oczekiwany wynik, jest bardzo ważne. Dlatego Android Studio używa emulatora, aby pomóc programistom zapewnić działanie aplikacji tak, jak na standardowych urządzeniach. Co więcej, telefony z Androidem są wyposażone w opcję programistyczną gotową do użycia przez programistów, co jeszcze bardziej ułatwia obsługę różnej liczby urządzeń obsługiwanych przez Androida, a także pomaga w odtwarzaniu błędów, które trudno znaleźć w emulatorach.

Tworzenie przycisku w Jetpack Compose

Musimy pamiętać, że nie możemy uwzględnić wszystkich poglądów w jednym przepisie; mamy rozdział poświęcony lepszemu poznaniu Jetpack Compose, więc w stworzonym przez nas projekcie spróbujemy po prostu stworzyć jeszcze dwa dodatkowe widoki dla naszego projektu.

Przygotowywanie się

Otwórz projekt społeczności Androida, ponieważ na nim będziemy opierać się w tym przepisie.

Jak to zrobć…

Zacznijmy od zaimplementowania prostego przycisku w Compose:

1. Przejdźmy dalej i uporządkujmy nasz kod i wyrównajmy tekst do środka, dodając funkcję Column(), aby uporządkować nasze widoki. Należy to dodać do funkcji setContent{}:

Column(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Greeting("Hello, Android Community")
}
}

2. Teraz utwórz funkcję i nazwij ją SampleButton; w tym przykładzie niczego nie przekażemy. Będziemy jednak mieli RowScope{}, który definiuje funkcje modyfikujące mające zastosowanie w tym przypadku do naszego przycisku i nadamy naszemu przyciskowi nazwę: kliknij mnie.
3. Podczas tworzenia przycisku w aplikacji Compose możesz ustawić jego kształt, ikonę i wysokość, sprawdzić, czy jest on włączony, sprawdzić jego zawartość i nie tylko. Możesz sprawdzić, jak dostosować swój przycisk, klikając z wciśniętym klawiszem Command komponent Button():

@Composable
fun SampleButton() {
Button(
onClick = { /*TODO*/ },
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
shape = RoundedCornerShape(20.dp),
border = BorderStroke(2.dp, Color.Blue),
colors = ButtonDefaults.buttonColors(
contentColor = Color.Gray,
backgroundColor = Color.White
)
) {
Text(
text = stringResource(id =
R.string.click_me),
fontSize = 14.sp,
modifier = Modifier.padding(horizontal =
30.dp, vertical = 6.dp)
)
}
}

W naszym SampleButton onClick nie robi nic; nasz przycisk ma modyfikator maksymalnej szerokości wypełnienia, wypełnienie 24 pikselami niezależnymi od gęstości (dp) i zaokrąglone rogi o promieniu 20 dp. Ustawiliśmy także kolor przycisku i dodaliśmy opcję "kliknij mnie" jako tekst. Ustawiliśmy rozmiar czcionki na 14 pikseli niezależnych od skali (sp), ponieważ pomaga to zapewnić, że tekst będzie dobrze dopasowany zarówno do ekranu, jak i preferencji użytkownika.

4. Kliknij także opcję Podziel w prawym górnym rogu, aby wyświetlić podgląd elementów ekranu, lub możesz kliknąć sekcję Projekt, aby wyświetlić cały ekran bez kodu.



5. Na koniec wywołajmy naszą funkcję SampleButton, w której znajduje się funkcja Powitanie, i uruchommy aplikację:

Column(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Greeting("Hello, Android Community")
SampleButton()
}

6. Skompiluj i uruchom program; Twoja aplikacja powinna wyglądać podobnie do rysunku



Jak to działa…

Aplikacja Composable składa się z kilku funkcji, które można komponować, czyli zwykłych funkcji opatrzonych adnotacją @Composable. Jak wyjaśnia dokumentacja Google, adnotacja informuje funkcję Compose o konieczności dodania wyjątkowej obsługi procedury aktualizacji i konserwacji interfejsu użytkownika w miarę upływu czasu. Funkcja Compose umożliwia także uporządkowanie kodu w małe, łatwe w utrzymaniu fragmenty, które można dostosowywać i ponownie wykorzystywać w dowolnym momencie.

Wykorzystanie poleceń gradlew do czyszczenia i uruchamiania projektu w Android Studio

Polecenie gradlew to solidne opakowanie Gradle, które ma doskonałe zastosowanie. Jednak w Android Studio nie trzeba go instalować, ponieważ jest to skrypt dostarczany w pakiecie z projektem.

Przygotowywanie się

Na razie jednak nie będziemy przyglądać się wszystkim poleceniom Gradle, zamiast tego użyjemy tych najpopularniejszych do czyszczenia, budowania, dostarczania informacji, debugowania i skanowania naszego projektu w celu znalezienia wszelkich problemów podczas uruchamiania naszej aplikacji. Możesz uruchamiać polecenia na terminalu swojego laptopa, o ile jesteś w odpowiednim katalogu lub korzystasz z terminala dostarczonego przez Android Studio.

Jak to zrobić…

Wykonaj poniższe kroki, aby sprawdzić i potwierdzić, czy Gradle działa zgodnie z oczekiwaniami:

1. Możesz sprawdzić wersję, po prostu uruchamiając ./gradlew.



2. Aby zbudować i wyczyścić projekt, możesz uruchomić polecenia ./gradlew clean i ./gradlew build. Jeśli coś jest nie tak z Twoim projektem, kompilacja zakończy się niepowodzeniem i będziesz mógł zbadać błąd. Ponadto w systemie Android zawsze możesz uruchomić projekt bez użycia poleceń Gradle i po prostu skorzystać z opcji uruchamiania i czyszczenia IDE.
3. Poniżej znajduje się kilka bardziej przydatnych poleceń gradlew; na przykład, jeśli kompilacja nie powiedzie się i chcesz wiedzieć, co poszło nie tak, użyj poleceń, aby to sprawdzić, lub kliknij komunikat o błędzie:

•  Uruchom z opcją --stacktrace, aby uzyskać ślad stosu
•  Uruchom z opcją --info lub --debug, aby uzyskać więcej danych wyjściowych dziennika
•  Uruchom z --scan, aby uzyskać pełny wgląd

Jak to działa

Gradle to narzędzie do kompilacji ogólnego przeznaczenia, które okazuje się bardzo przydatne w programowaniu Androida. Ponadto możesz tworzyć i publikować własne wtyczki, aby hermetyzować swoje konwencje i budować funkcjonalność. Zalety Gradle obejmują kompilację przyrostową do wykonywania testów, kompilacji i wszelkich innych zadań wykonywanych w systemie kompilacji.

Zrozumienie struktury projektu Android

Jeśli po raz pierwszy przeglądasz folder projektu Androida, możesz zastanawiać się, gdzie dodać swój kod i co oznaczają pakiety. W tym przepisie omówiono, co zawiera każdy folder i jaki kod trafia gdzie.

Przygotowywanie się

Jeśli otworzysz swój projekt, zauważysz wiele folderów. Główne foldery w Twoim projekcie na Androida są wymienione tutaj:

•  Folder manifestu
•  Folder Java (test/androidTest)
•  Folder Zasoby Res
•  Skrypty stopniowe
Jak to zrobić …


Przejrzyjmy każdy folder, dowiadując się, co, gdzie i dlaczego jest przechowywane:

1. Na rysunku możesz zobaczyć listę rozwijaną Pakiety; kliknij to, a pojawi się okno z projektem, pakietami, plikami projektu i innymi elementami.



2. Możesz wyświetlić swój projekt przy użyciu logo Androida, poprzez Projekt lub podświetloną sekcję Projekt obok menu rozwijanego. Widok Projekt jest najlepszy, gdy w aplikacji znajduje się wiele modułów i chcesz dodać konkretny kod. Kliknij sekcje i zobacz, co zawierają.

3. Folder manifestu jest źródłem prawdy dla aplikacji na Androida; zawiera plik AndroidManifest.xml. Kliknij wewnątrz pliku, a zauważysz, że masz program uruchamiający, który uruchamia aplikację na Androida na twoim emulatorze.

4. Ponadto numer wersji jest zwykle ustawiany w Gradle, a następnie łączony z manifestem, a w manifeście dodajemy wszystkie potrzebne uprawnienia. Zauważysz także nazwę pakietu, metadane, zasady ekstrakcji danych, motyw i ikonę; jeśli masz unikalną ikonę, możesz ją tutaj dodać.



5. Folder java zawiera wszystkie pliki Kotlin (.kt) i Java (.java), które tworzymy podczas tworzenia naszych aplikacji na Androida. Na przykład na rysunku mamy pakiet z (androidTest) i (test) i tutaj dodajemy nasze testy. Śmiało, kliknij wszystkie foldery i zobacz, co zawierają



6. W folderze androidTest piszemy nasze testy UI w celu przetestowania funkcjonalności UI, a w folderze test piszemy nasz test jednostkowy. Testy jednostkowe testują małe fragmenty naszego kodu, aby upewnić się, że wymagane zachowanie jest zgodne z oczekiwaniami. Rozwój oparty na testach (TDD) jest doskonały i cenny podczas tworzenia aplikacji. Niektóre firmy przestrzegają tej zasady, inne jednak jej nie egzekwują. Jednakże jest to świetna umiejętność, ponieważ dobrą praktyką jest zawsze testowanie kodu. Folder res zawiera układy XML, ciągi znaków interfejsu użytkownika, obrazy do rysowania i ikony Mipmap. Z drugiej strony folder wartości zawiera wiele przydatnych plików XML, takich jak wymiary, kolory i motywy. Śmiało, kliknij folder res, aby zapoznać się z tym, co tam jest, ponieważ będziemy go używać w następnej części.



Ważna uwaga: jeśli nie budujesz nowego projektu od zera, wiele aplikacji nadal korzysta z układów XML, a programiści decydują się teraz na opracowywanie nowych ekranów za pomocą Jetpack Compose jako udoskonalenia. Dlatego może być konieczne utrzymanie lub wiedza, jak pisać widoki w formacie XML.

7. Na koniec w Gradle Scripts zobaczysz pliki definiujące konfigurację kompilacji, którą możemy zastosować w naszych modułach. Na przykład w build.gradle (Project: AndroidCommunity) zobaczysz plik najwyższego poziomu, w którym możesz dodać opcje konfiguracyjne wspólne dla wszystkich modułów podprojektu.



Jak to działa…

W Android Studio niewiedza, gdzie trafiają pliki i co jest najważniejsze, może być przytłaczająca dla początkujących użytkowników. Dlatego posiadanie przewodnika krok po kroku pokazującego, gdzie dodać testy lub kod, a także zrozumienie struktury projektu Androida jest niezbędne. Ponadto w złożonych projektach możesz znaleźć różne moduły; dlatego pomocne jest zrozumienie struktury projektu. Moduł w Android Studio to zbiór plików źródłowych i ustawień kompilacji, które pozwalają podzielić projekt na odrębne jednostki o określonych celach.

Debugowanie i logowanie w Android Studio

Debugowanie i rejestrowanie mają kluczowe znaczenie w rozwoju Androida. Możesz pisać komunikaty dziennika, które pojawiają się w Logcat, aby pomóc Ci znaleźć problemy w kodzie lub sprawdzić, czy fragment kodu wykonuje się wtedy, gdy powinien. Wprowadzimy ten temat tutaj, ale niesprawiedliwe będzie stwierdzenie, że omówimy to wszystko w jednym przepisie.

Przygotowywanie się

Aby zrozumieć rejestrowanie, użyjmy przykładu. Poniższe metody dziennika są wymienione od najwyższego do najniższego priorytetu. Są one odpowiednie podczas rejestrowania błędów sieciowych, powołań i innych błędów:

•  Log.e(): błąd dziennika
•  Log.w(): Ostrzeżenie dotyczące dziennika
•  Log.i(): Informacje dziennika
•  Log.d(): Debugowanie pokazuje programistom krytyczne komunikaty, najczęściej używany dziennik
•  Log.v(): szczegółowe

Dobrą praktyką jest powiązanie każdego dziennika z TAGiem, aby szybko zidentyfikować komunikat o błędzie w Logcat. "TAG" oznacza etykietę tekstową, którą można przypisać do widoku lub innego elementu interfejsu użytkownika w aplikacji na Androida. Głównym celem używania tagów w systemie Android jest umożliwienie powiązania dodatkowych informacji lub metadanych z elementem interfejsu użytkownika.

Jak to zrobić…

Przejdźmy dalej i dodajmy komunikat dziennika do naszego małego projektu:

1. Przejdźmy dalej i utworzymy dziennik debugowania, a następnie uruchomimy aplikację:

Log.d(TAG, "asdf Połączenie testowe")

W sekcji Logcat w polu wyszukiwania wpisz asdf i sprawdź, czy możesz znaleźć wiadomość. Zauważysz, że dziennik ma nazwę klasy, nasz TAG (MainActivity) i wyświetlony komunikat dziennika; zobacz strzałkę w prawo na rysunku .



2. Strzałka w lewo pokazuje wymienione typy logów, a korzystając z rozwijanego menu, możesz szybko wyświetlić wiadomość w oparciu o specyfikację.

Jak to działa…

Debugowanie polega na umieszczeniu punktów przerwania w klasach, spowolnieniu emulatora i próbie znalezienia problemów w kodzie. Debugowanie jest bardzo przydatne, jeśli na przykład napotkasz w kodzie sytuację wyścigu lub jeśli kod działa na niektórych urządzeniach, a nie na innych. Dodatkowo, aby skorzystać z debugowania należy najpierw podłączyć do emulatora debuger, a następnie uruchomić w trybie debugowania. Z drugiej strony rejestrowanie pomaga rejestrować informacje, które mogą być pomocne w przypadku napotkania problemów. Czasami debugowanie może być trudne, ale umieszczenie dzienników w kodzie tam, gdzie jest to potrzebne, może być bardzo pomocne. Praktycznym przypadkiem jest ładowanie danych z interfejsu API; możesz chcieć zapisać to w przypadku wystąpienia błędu sieci, aby poinformować Cię, co się stanie, jeśli połączenie sieciowe nie powiedzie się. Dlatego debugowanie przy użyciu punktów przerwania może pomóc spowolnić proces oceny wartości

Tworzenie ekranów przy użyciu deklaratywnego interfejsu użytkownika i odkrywanie zasad tworzenia

Aplikacje mobilne wymagają interfejsu użytkownika (UI) do interakcji użytkownika. Na przykład stary sposób tworzenia interfejsu użytkownika był niezbędny w systemie Android. Oznaczało to posiadanie osobnego prototypu interfejsu użytkownika aplikacji przy użyciu unikalnych układów Extensible Markup Language (XML), a nie tego samego języka, który był używany do budowania logiki. Jednak w przypadku Modern Android Development istnieje potrzeba zaprzestania programowania imperatywnego i rozpoczęcia korzystania z deklaratywnego sposobu tworzenia interfejsu użytkownika, co oznacza, że programiści projektują interfejs użytkownika na podstawie otrzymanych danych. Ten paradygmat projektowania wykorzystuje jeden język programowania do stworzenia całej aplikacji. Trzeba przyznać, że nowym programistom może wydawać się trudno zdecydować, czego się nauczyć podczas tworzenia interfejsu użytkownika: starego sposobu tworzenia widoków czy wybrania nowego Jetpack Compose. Załóżmy jednak, że stworzyłeś aplikację na Androida przed erą Jetpack Compose. W takim przypadku możesz już wiedzieć, że używanie XML jest nieco nudne, szczególnie jeśli baza kodu jest złożona. Jednak wykorzystanie Jetpack Compose jako pierwszego wyboru ułatwia pracę. Ponadto upraszcza tworzenie interfejsu użytkownika, zapewniając programistom użycie mniejszej ilości kodu, ponieważ korzystają z intuicyjnych interfejsów API Kotlin. Dlatego nowi programiści logicznie naciskają, aby podczas tworzenia widoków używać Jetpack Compose zamiast XML. Jednak znajomość obu może być korzystna, ponieważ wiele aplikacji nadal korzysta z układów XML i być może będziesz musiał zachować widok, ale zbudować nowy za pomocą Jetpack Compose.

Implementacja widoków Androida w Jetpack Compose

W każdej aplikacji na Androida posiadanie elementu interfejsu użytkownika jest bardzo istotne. Widok w systemie Android to prosty element konstrukcyjny interfejsu użytkownika. Widok zapewnia użytkownikom możliwość interakcji z aplikacją poprzez dotknięcie lub inny ruch. W tym przepisie przyjrzymy się różnym elementom interfejsu użytkownika aplikacji Compose i zobaczymy, jak możemy je zbudować.

Przygotowywanie się

W tym przepisie utworzymy jeden projekt, który będziemy wykorzystywać przez całą tą część, więc przejdźmy dalej i wykonaj kroki opisane w Części 1, Pierwsze kroki z umiejętnościami nowoczesnego programowania na Androidzie, na temat tworzenia pierwszego projektu na Androida. Utwórz projekt i nadaj mu nazwę Compose Basics. Ponadto sekcji Podgląd będziemy najczęściej używać do przeglądania tworzonego przez nas elementu interfejsu użytkownika.

Jak to zrobić…

Po utworzeniu projektu wykonaj poniższe kroki, aby zbudować kilka elementów interfejsu użytkownika narzędzia Compose:

1. Wewnątrz naszego projektu utwórzmy nowy pakiet i nazwijmy go komponentami. Tutaj dodamy wszystkie utworzone przez nas komponenty.
2. Utwórz plik Kotlin i nazwij go UIComponents.kt; wewnątrz UIComponent utwórz funkcję do komponowania, nazwij ją EditTextExample() i wywołaj funkcję OutlinedTextField(); spowoduje to wyświetlenie monitu o zaimportowanie wymaganego importu, czyli androidx.Compose.material.OutlinedTextField:

@Composable
fun EditTextExample() {
OutlinedTextField()
}

3. Kiedy zajrzysz głębiej w OutlineTextField , zauważysz, że funkcja przyjmuje kilka danych wejściowych, co jest bardzo przydatne, gdy musisz dostosować własne funkcje, które można komponować.



4. W naszym przykładzie nie zrobimy zbyt wiele z interfejsem użytkownika, który tworzymy, a raczej przyjrzymy się, jak go tworzymy.
5. Teraz, aby w pełni utworzyć naszą OutlinedTextField() w oparciu o typy danych wejściowych, które akceptuje, możemy nadać jej tekst i kolor oraz ozdobić ją za pomocą Modifier(); to znaczy podając mu określone instrukcje, takie jak fillMaxWidth(), która ustawia maksymalną szerokość. Kiedy mówimy "wypełnij", po prostu określamy, że powinien on być całkowicie wypełniony. Ustawiamy .padding(top) na 16.dp, co powoduje zastosowanie dodatkowej przestrzeni wzdłuż każdej krawędzi zawartości w dp. Posiada również wartość, która jest wartością, którą należy wprowadzić w OutlinedTextField, oraz lambdę onValueChange, która nasłuchuje zmiany sygnału wejściowego
6. Nadajemy także naszemu OutlinedText kilka kolorów obramowania, gdy jest skupiony i gdy nie jest skupiony, aby odzwierciedlić różne stany. Dlatego jeśli zaczniesz wprowadzać dane, pole zmieni kolor na niebieski, zgodnie z kodem:

@Composable
fun EditTextExample() {
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text(stringResource(id =
R.string.sample)) },
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
colors =
TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black
)
)
}

7. Mamy także inny typ pola TextField, który nie został opisany w zarysie. Jeśli porównasz dane wejściowe OutlinedTextField, zauważysz, że są one dość podobne:

@Composable
fun NotOutlinedEditTextExample() {
TextField(
value = "",
onValueChange = {},
label = { Text(stringResource(id =
R.string.sample)) },
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 16.dp),
colors =
TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black
)
)
}

8. Możesz uruchomić aplikację, dodając funkcje Compose wewnątrz funkcji komponowalnej @Preview. W naszym przykładzie możemy utworzyć UIElementPreview(), która jest funkcją podglądu służącą do wyświetlania naszego interfejsu użytkownika. Na rysunku widok z góry to OutlinedTextField, podczas gdy drugi to normalne pole TextField.



9. Przejdźmy teraz do przykładów przycisków. Przyjrzymy się różnym sposobom tworzenia przycisków o różnych kształtach. Jeśli najedziesz kursorem na funkcję komponowania Button(), zobaczysz, co akceptuje jako dane wejściowe, jak pokazano na rysunku



W naszym drugim przykładzie spróbujemy utworzyć przycisk z ikoną. Dodatkowo dodamy tekst, który jest kluczowy przy tworzeniu przycisków, ponieważ musimy określić użytkownikowi, jaką akcję lub co zrobi przycisk po kliknięciu.

10. Zatem utwórz funkcję Compose w tym samym pliku Kotlina i nadaj jej nazwę ButtonWithIcon(), a następnie zaimportuj funkcję komponowania Button().
11. Wewnątrz niego będziesz musiał zaimportować Icon() z wejściem painterResource, opisem zawartości, modyfikatorem i odcieniem. Będziemy także potrzebować funkcji Text(), która nada naszemu przyciskowi nazwę. W naszym przykładzie nie użyjemy odcienia:

@Composable
fun ButtonWithIcon() {
Button(onClick = {}) {
Icon(
painterResource(id =
R.drawable.ic_baseline_shopping_bag_24 ),
contentDescription = stringResource(
id = R.string.shop),
modifier = Modifier.size(20.dp)
)
Text(text = stringResource(id = R.string.buy),
Modifier.padding(start = 10.dp))
}
}

12. Stwórzmy także nową funkcję z możliwością komponowania i nazwijmy ją CornerCutShapeButton(); w tym przykładzie spróbujemy utworzyć przycisk ze ściętymi narożnikami:

@Composable
fun CornerCutShapeButton() {
Button(onClick = {}, shape = CutCornerShape(10)) {
Text(text = stringResource(
id = R.string.cornerButton)) }}}}

13. Stwórzmy także nową funkcję z możliwością komponowania i nazwijmy ją RoundCornerShapeButton(); w tym przykładzie spróbujemy utworzyć przycisk z zaokrąglonymi rogami:

@Composable
fun RoundCornerShapeButton() {
Button(onClick = {}, shape =
RoundedCornerShape(10.dp)) {
Text(text = stringResource(
id = R.string.rounded))
}
}

14. Stwórzmy także nową funkcję z możliwością komponowania i nazwijmy ją ElevatedButtonExample(); w tym przykładzie spróbujemy utworzyć przycisk z podniesieniem:

@Composable
fun ElevatedButtonExample() {
Button(
onClick = {},
elevation = ButtonDefaults.elevation(
defaultElevation = 8.dp,
pressedElevation = 10.dp,
disabledElevation = 0.dp
)
) {
Text(text = stringResource(
id = R.string.elevated))
}
}

15. Po uruchomieniu aplikacji powinieneś otrzymać obraz podobny do rysunku 2.4; pierwszy przycisk po TextField to ButtonWithIcon(), drugi to CornerCutShapeButton(), trzeci to RoundCornerShapeButton(), a na koniec mamy ElevatedButtonExample().



16. Spójrzmy teraz na ostatni przykład, ponieważ w całej książce będziemy używać różnych poglądów i stylów, a przy okazji dowiemy się więcej. Przyjrzyjmy się teraz widokowi obrazu; funkcja komponowania Image() pobiera kilka danych wejściowych, jak pokazano na rysunku



17. W naszym przykładzie Image() będzie mieć tylko malarza, który nie dopuszcza wartości null, co oznacza, że musisz dostarczyć obraz dla tej funkcji, którą można komponować, opis treści dotyczący dostępności i modyfikator:

@Composable
fun ImageViewExample() {
Image(
painterResource(id = R.drawable.android),
contentDescription = stringResource(
id = R.string.image),
modifier = Modifier.size(200.dp)
)
}

18. Możesz także spróbować pobawić się innymi rzeczami, na przykład dodać elementy RadioButton() i CheckBox() i dostosować je. Po uruchomieniu aplikacji powinieneś mieć coś podobnego do rysunku



Jak to działa…

Każda funkcja, którą można komponować, jest opatrzona adnotacją @Composable. Ta adnotacja informuje kompilator Compose, że dostarczony kompilator ma na celu konwersję dostarczonych danych na plik Interfejsu użytkownika. Należy również pamiętać, że każda nazwa funkcji, którą można komponować, musi być rzeczownikiem, a nie czasownikiem czy przymiotnikiem, a Google udostępnia te wytyczne. Dowolna funkcja, którą utworzysz, może akceptować parametry, które umożliwiają logice aplikacji opisywanie lub modyfikowanie interfejsu użytkownika. Wspominamy o kompilatorze Compose, co oznacza, że kompilator to dowolny specjalny program, który pobiera napisany przez nas kod, sprawdza go i tłumaczy na coś, co komputer może zrozumieć - lub na język maszynowy. W Icon() painterResouce określa ikonę, którą dodamy do przycisku, opis zawartości pomaga w dostępności, a modyfikator służy do ozdobienia naszej ikony. Możemy wyświetlić podgląd budowanych przez nas elementów interfejsu użytkownika, dodając adnotację @Preview i dodając

showBackground = true:
@Preview(showBackground = true)
@Preview ma ogromne możliwości i w przyszłych rozdziałach przyjrzymy się, jak możesz lepiej go wykorzystać.

Implementacja przewijanej listy w Jetpack Compose

Jedną rzeczą, co do której wszyscy możemy się zgodzić podczas tworzenia aplikacji na Androida, jest to, że musisz wiedzieć, jak zbudować RecyclerView, aby wyświetlić dane. Dzięki naszemu nowemu, nowoczesnemu sposobowi budowania aplikacji na Androida, jeśli potrzebujemy użyć RecyclerView, możemy użyć LazyColumn, który jest podobny. W tym przepisie przyjrzymy się wierszom, kolumnom i LazyColumn i zbudujemy przewijaną listę, korzystając z naszych fikcyjnych danych. Ponadto przy okazji będziemy uczyć się języka Kotlin.

Przygotowywanie się

Będziemy nadal używać projektu Compose Basics do tworzenia przewijanej listy; dlatego, aby rozpocząć, musisz wykonać poprzedni przepis.
Jak to zrobić…

Wykonaj poniższe kroki, aby utworzyć pierwszą przewijaną listę:

1. Przejdźmy dalej i zbudujmy naszą pierwszą przewijaną listę, ale najpierw musimy utworzyć nasze fikcyjne dane i to jest element, który chcemy wyświetlić na naszej liście. Dlatego utwórz pakiet o nazwie ulubionemiasto, w którym będzie znajdować się nasz przewijany przykład.
2. W pakiecie ulubionychcity utwórz nową klasę danych i nazwij ją City; będzie to nasze fikcyjne źródło danych - klasa danych City ().
3. Zamodelujmy naszą klasę danych City. Po dodaniu wartości z adnotacjami pamiętaj o dodaniu niezbędnych importów:

data class City(
val id: Int,
@StringRes val nameResourceId: Int,
@DrawableRes val imageResourceId: Int
)

4. Teraz w naszych fikcyjnych danych musimy utworzyć klasę Kotlin i nazwać ją CityDataSource. W tej klasie utworzymy funkcję o nazwie LoadCities(), która zwróci naszą listę List, którą wyświetlimy na naszej przewijanej liście. Sprawdź sekcję Wymagania techniczne dla wszystkich wymaganych importów, aby uzyskać cały kod i obrazy:

class CityDataSource {
fun loadCities(): List {
return listOf(
City(1, R.string.spain, R.drawable.spain),
City(2, R.string.new_york,
R.drawable.newyork),
City(3, R.string.tokyo, R.drawable.tokyo),
City(4, R.string.switzerland,
R.drawable.switzerland),
City(5, R.string.singapore,
R.drawable.singapore),
City(6, R.string.paris, R.drawable.paris),
)
}
}

5. Mamy już fikcyjne dane i czas wyświetlić je na naszej przewijanej liście. Utwórzmy nowy plik Kotlin w naszym pakiecie komponentów i nazwijmy go CityComponents. W CityComponents utworzymy naszą funkcję @Preview:

@Preview(showBackground = true)
@Composable
private fun CityCardPreview() {
CityApp()
}

6. Wewnątrz naszej funkcji @Preview znajduje się kolejna funkcja, którą można komponować, CityApp(); wewnątrz tej funkcji wywołamy naszą funkcję komponowania CityList, która ma listę jako parametr. Dodatkowo w tej funkcji komponowalnej wywołamy LazyColumn, a elementami będą CityCard(miasta). Więcej informacji na temat LazyColumn i elementów znajdziesz w sekcji Jak to działa:

@Composable
fun CityList(cityList: List) {
LazyColumn {
items(cityList) { cities ->
CityCard(cities)
}
}
}

7. Na koniec skonstruujmy funkcję komponowalną CityCard(city):

@Composable
fun CityCard(city: City) {
Card(modifier = Modifier.padding(10.dp),
elevation = 4.dp) {
Column {
Image(
painter = painterResource(
city.imageResourceId),
contentDescription = stringResource(
city.nameResourceId),
modifier = Modifier
.fillMaxWidth()
.height(154.dp),
contentScale = ContentScale.Crop
)
Text(
text = LocalContext.current.getString(
city.nameResourceId),
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.h5
)
}
}
}

8. Po uruchomieniu funkcji komponowania CityCardPreview powinna pojawić się przewijalna lista, jak pokazano na rysunku



Jak to działa…

W Kotlinie lista ma dwa typy: niezmienną i zmienną. Listy niezmienne to elementy, których nie można modyfikować, natomiast listy zmienne to elementy na liście, które można modyfikować. Aby zdefiniować listę, możemy powiedzieć, że lista jest ogólnym uporządkowanym zbiorem elementów, a elementy te mogą mieć postać liczb całkowitych, ciągów znaków, obrazów itd., co jest głównie zależne od rodzaju danych, jakie chcemy uzyskać na naszych listach zawierać. Na przykład w naszym przykładzie mamy ciąg znaków i obraz, które pomagają zidentyfikować nasze ulubione miasta według nazwy i obrazu. W naszej klasie danych City używamy @StringRes i @DrawableRes, aby po prostu łatwo pobrać to bezpośrednio z folderów res dla Drawable i String, a także reprezentują one identyfikator obrazów i ciągu znaków. Stworzyliśmy CityList i opatrzyliśmy ją adnotacją za pomocą funkcji composable i zadeklarowaliśmy listę obiektów miejskich jako nasz parametr w tej funkcji. Przewijalna lista w Jetpack Compose jest tworzona przy użyciu LazyColumn. Główna różnica między LazyColumn i Column polega na tym, że podczas korzystania z Column można wyświetlać tylko małe elementy, ponieważ Compose ładuje wszystkie elementy na raz. Ponadto kolumna może zawierać tylko stałe funkcje, które można komponować, podczas gdy LazyColumn, jak sama nazwa wskazuje, ładuje zawartość zgodnie z wymaganiami na żądanie, dzięki czemu dobrze nadaje się do ładowania większej liczby elementów, gdy zajdzie taka potrzeba. Ponadto LazyColumn ma wbudowaną funkcję przewijania, która ułatwia pracę programistom. Stworzyliśmy także funkcję komponowalną CityCard, do której importujemy element Card() z Compose. Karta zawiera treść i działania dotyczące pojedynczego obiektu; w naszym przykładzie na naszej karcie znajduje się zdjęcie i nazwa miasta. Element Card() w Compose ma w swoim parametrze następujące dane wejściowe:

@Composable
fun Card(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.medium, backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
border: BorderStroke? = null,
elevation: Dp = 1.dp,
content: @Composable () -> Unit
),

Oznacza to, że możesz łatwo wymodelować swoją kartę tak, aby była najlepiej dopasowana; nasza karta ma dopełnienie i podniesienie, a zakres ma kolumnę. W tej kolumnie mamy obraz i tekst, który pomaga opisać obraz w celu uzyskania większego kontekstu.

Implementowanie układu pierwszej karty za pomocą pagera widoku przy użyciu Jetpack Compose

W programowaniu na Androida bardzo często zdarza się, że slajd między stronami jest wyświetlany, a znaczącym przypadkiem użycia jest wdrażanie lub nawet wtedy, gdy próbujesz wyświetlić określone dane w formie karuzeli z zakładkami. W tym przepisie zbudujemy prosty poziomy pager w Compose i zobaczymy, jak możemy wykorzystać nową wiedzę do tworzenia lepszych i nowocześniejszych aplikacji na Androida.

Przygotowywanie się

W tym przykładzie zbudujemy poziomy pager, który zmienia kolory po wybraniu, aby pokazać wybrany stan. Dla lepszego zrozumienia stanom przyjrzymy się w rozdziale 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i Używanie Hilt. Aby rozpocząć, otwórz projekt Compose Basics.

Jak to zrobić…

Wykonaj poniższe kroki, aby zbudować karuzelę kart:

1. Dodaj następujące zależności pagera do build.gradle(Module:app):

implementation "com.google.accompanist:accompanist-pager:0.x.x"
implementation "com.google.accompanist:accompanist-pagerindicators: 0.x.x"
implementation 'androidx.Compose.material:material:1.x.x'

Jetpack Compose oferuje Accompanist, grupę bibliotek, których celem jest wspieranie go w powszechnie wymaganych przez programistów funkcjach - na przykład w naszym przypadku pager.

2. W tym samym projekcie z poprzednich przepisów utwórzmy pakiet i nazwijmy go pagereprzykład; w nim utwórz plik Kotlin i nazwij go CityTabExample; w tym pliku utwórz funkcję umożliwiającą komponowanie i nazwij ją CityTabCarousel:

@Composable
fun CityTabCarousel(){}

3. Teraz przejdźmy dalej i zbudujmy naszą CityTabCarousel; dla naszego przykładu utworzymy fikcyjną listę stron z naszymi miastami z poprzedniego projektu:

@Composable
fun CityTabCarousel(
pages: MutableList = arrayListOf(
"Spain",
"New York",
"Tokyo",
"Switzerland",
"Singapore",
"Paris" )) {…}

4. Będziemy musieli zmienić kolor przycisku w zależności od stanu i aby to zrobić; musimy użyć LocalContext, który zapewnia kontekst, którego możemy użyć. Będziemy musieli także utworzyć zmienną var pagerState = RememberPagerState(), która zapamięta stan naszego pagera i na koniec, po kliknięciu, będziemy musieli przejść do następnego miasta w naszym pagerze, co będzie bardzo pomocne. Dlatego śmiało dodaj następujące elementy do funkcji komponowania CityTabCarousel:

val context = LocalContext.current
var pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()

5. Teraz utwórzmy element Column i dodajmy naszą funkcję komponowania ScrollableTabRow():

Column {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(…)
},
edgePadding = 0.dp,
backgroundColor = Color(
context.resources.getColor(R.color.white,
null)),
) {
pages.forEachIndexed { index, title ->
val isSelected =
pagerState.currentPage == index
TabHeader(
title,
isSelected,
onClick = { coroutineScope.launch {
pagerState.animateScrollToPage(index)
} },
)
}
}

6. Dodaj Text() i TabHeader() dla HorizontalPager:

HorizontalPager(
count = pages.size,
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.White)
) { page ->
Text(
text = "Display City Name:
${pages[page]}",
modifier = Modifier.fillMaxWidth(),
style = TextStyle(
textAlign = TextAlign.Center
)
)
}

7. Proszę pobrać cały kod tego przepisu, klikając link podany w sekcji Wymagania techniczne, aby dodać cały wymagany kod. Na koniec uruchom funkcję @Preview, a Twoja aplikacja powinna wyglądać jak Rysunek



Jak to działa… Accompanist zawiera kilka znaczących bibliotek - na przykład Kontroler interfejsu użytkownika systemu, Adapter motywu AppCompact Compose, Adapter motywu materiału, Pager, Malarz do rysowania i Układy przepływu, żeby wymienić tylko kilka. Funkcja ScrollableTabRow(), której używamy wewnątrz Column w funkcji CityTabCarousel, zawiera rząd zakładek i pomaga wyświetlić wskaźnik pod aktualnie zaznaczoną lub wybraną zakładką. Dodatkowo, jak sama nazwa wskazuje, umożliwia przewijanie i nie trzeba wdrażać dalszych narzędzi przewijania. Umieszcza również przesunięcia tabulatorów na krawędzi początkowej i można szybko przewijać karty znajdujące się poza ekranem, co zobaczysz po uruchomieniu funkcji @Preview i zabawie się nią. Kiedy wywołujemy funkcję Remember() w Compose, oznacza to, że zachowujemy spójność wartości podczas rekompozycji. Compose udostępnia tę funkcję, aby pomóc nam przechowywać pojedyncze obiekty w pamięci. Kiedy uruchamiamy naszą aplikację, Remember() przechowuje wartość początkową. Jak to słowo oznacza, po prostu zachowuje wartość i zwraca przechowywaną wartość, aby funkcja komponowalna mogła z niej skorzystać. Co więcej, za każdym razem, gdy przechowywana wartość ulegnie zmianie, możesz ją zaktualizować, a funkcja Remember() ją zachowa. Następnym razem, gdy uruchomimy kolejne uruchomienie w naszej aplikacji i nastąpi rekompozycja, funkcja Remember() zapewni najnowszą zapisaną wartość. Zauważysz również, że nasza MutableList jest indeksowana w każdej pozycji i robimy to, aby sprawdzić, która jest wybrana. To w tej Lambdzie nazywamy TabHeader i prezentujemy wybrane strony zakładek. forEachIndexed wykonuje daną akcję na każdym elemencie, udostępniając sekwencyjny indeks elementów. Zapewniamy również, że gdy użytkownik kliknie konkretną zakładkę, znajdziemy się na właściwej stronie:

onClick = { coroutineScope.launch { pagerState.
animateScrollToPage(index) } }

HorizontalPager to układ przewijany w poziomie, który umożliwia naszym użytkownikom przełączanie elementów od lewej do prawej. Pobiera kilka danych wejściowych, ale dostarczamy mu liczbę, stan i modyfikator, aby ozdobić go w naszym przypadku użycia. W Lambdzie wyświetlamy tekst - w naszym przykładzie pokazujący na której stronie się znajdujemy, co pomaga w nawigacji, jak pokazano na rysunku



Nasza funkcja komponowania TabHeader ma Box(); pudełko w Jetpack Compose zawsze dopasuje się do zawartości i podlega to określonym ograniczeniom. W naszym przykładzie dekorujemy nasze pudełko modyfikatorem selekcji, który konfiguruje komponenty tak, aby można je było wybierać jako część wzajemnie wykluczającej się grupy, dzięki czemu każdy element może zostać wybrany tylko raz w danym momencie.

Implementacja animacji w Compose

Animacja w systemie Android to proces dodawania efektów ruchu do widoków. Można to osiągnąć za pomocą obrazów, tekstu lub nawet rozpoczynając nowy ekran, na którym przejście jest zauważalne, dzięki efektom ruchu. Animacje są niezbędne w rozwoju nowoczesnego Androida, ponieważ nowoczesne interfejsy użytkownika są bardziej interaktywne i dostosowują się do płynniejszych wrażeń, a użytkownicy je lubią. Co więcej, obecnie aplikacje są oceniane na podstawie tego, jak świetny jest ich interfejs użytkownika i doświadczenia użytkownika, dlatego należy upewnić się, że aplikacja jest nowoczesna i solidna. W tym przykładzie zbudujemy zwijany pasek narzędzi, animację szeroko stosowaną w świecie Androida.

Przygotowywanie się

Będziemy nadal korzystać z projektu Compose Basics.

Jak to zrobić…

W tym przepisie będziemy budować zwijany pasek narzędzi; istnieją inne wspaniałe animacje, które możesz teraz zbudować, korzystając z możliwości tworzenia. Moc jest w Twoich rękach:

1. Nie będziemy musieli dodawać żadnej zależności do tego przepisu. Wszystko mamy już na miejscu. Przejdźmy więc dalej i utwórz nowy pakiet i dodaj plik Kotlin, collapsingtoolbar.
2. W pliku Kotlin utwórz nową funkcję, którą można komponować, CollapsingTool BarExample():

@Composable
fun CollapsingToolbarExample() {…}

3. Będziemy mieć wszystkie potrzebne funkcje komponowalne w pudełku; możesz odwołać się do poprzedniego przepisu, żeby odświeżyć sobie pamięć. Będziemy musieli także określić wysokość, na której zaczniemy zawijać nasz widok, może to być oparte na preferencjach; w naszym przykładzie możemy ustawić wysokość na 260.dp:

private val height = 260.dp
private val titleToolbar = 50.dp

4. Dodajmy więcej funkcji, które można komponować, z fikcyjnymi danymi tekstowymi wyświetlanymi podczas przewijania naszej zawartości. Możemy założyć, że ta aplikacja służy do odczytywania informacji o wyświetlanych przez nas miastach:

@Composable
fun CollapsingToolbarExample() {
val scrollState: ScrollState =
rememberScrollState(0)
val headerHeight = with(LocalDensity.current) {
height.toPx() }
val toolbarHeight = with(LocalDensity.current) {
titleToolbar.toPx() }
Box(
modifier = Modifier.fillMaxSize()
) {
CollapsingHeader(scrollState, headerHeight)
FactsAboutNewYork(scrollState)
OurToolBar(scrollState, headerHeight,
toolbarHeight)
City()
}
}

5. W naszej funkcji CollapsingHeader przekazujemy stan przewijania, a wysokość nagłówka jako wartość zmiennoprzecinkową. Dekorujemy Boxa Modifier.graphicLayer, gdzie ustawiamy efekt paralaksy, aby wyglądał dobrze i reprezentacyjnie.

6. Upewnij się również, że dodaliśmy metodę Brush() i ustawiliśmy potrzebne kolory oraz określimy, gdzie powinno się to zaczynać:
Box(
Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent,
Color(0xFF6D38CA)),
startY = 1 * headerHeight / 5
)
)
)

7. FactsAboutNewYork nie jest złożoną funkcją, którą można komponować, jest po prostu fikcyjnym tekstem; następnie w końcu na pasku narzędzi wykorzystujemy AnimatedVisibility i deklarujemy nasze przejście do wejścia i wyjścia:

AnimatedVisibility(
visible = showToolbar,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(200))
) {


8. Na koniec uruchom funkcję @Preview, a otrzymasz składany pasek narzędzi, który zapewni płynność interfejsu użytkownika.

Jak to działa…

W Modern Android Development biblioteka Jetpack Compose zawiera wiele interfejsów API animacji, które są dostępne jako funkcje komponowalne. Na przykład możesz chcieć, aby obraz lub tekst pojawiał się i znikał. Dlatego też, jeśli animujesz pojawianie się i znikanie, co może dotyczyć obrazu, tekstu, grupy opcji, przycisku itd., możesz w tym celu użyć AnimatedVisibility. W przeciwnym razie, jeśli zamieniasz zawartość w zależności od stanu i chcesz, aby zawartość przenikała, możesz użyć funkcji CrossFade lub AnimatedContent. val headerHeight = with(LocalDensity.current) { height.toPx() } zapewnia gęstość, która zostanie wykorzystana do przekształcenia jednostek DP i SP i możemy to wykorzystać, gdy zapewnimy DP, co zrobimy, a później przekształcimy w treść naszego układu. Możesz wywołać modyfikator i użyć GraphicLayer, aby niezależnie zaktualizować dowolną treść powyżej, aby zminimalizować nieważną treść. Ponadto GraphicLayer można wykorzystać do zastosowania efektów, takich jak skalowanie, obrót, krycie, cień, a nawet przycinanie. TranslationY = scroll.value.toFloat() / 2f zasadniczo ustawia pionowe przesunięcie pikseli warstwy względem jej górnej granicy. Wartość domyślna to zawsze zero, ale można ją dostosować do własnych potrzeb. Zapewniamy również, że gradient jest stosowany tylko do zawijania tytułu w startY = 1 * headerHeight / 5. EnterTransition definiuje, jak powinna wyglądać docelowa treść; celem może być obraz, tekst lub nawet grupa radiowa. Z drugiej strony ExitTransition definiuje, w jaki sposób początkowa treść docelowa powinna zniknąć podczas wychodzenia z aplikacji lub nawigacji. AnimatedContent oferuje slideIntoContainer i slideOutOfContainer i animuje swoją zawartość w miarę jej zmian w zależności od stanu docelowego, co jest niezwykłe. Ponadto możesz także hermetyzować przejście i umożliwić jego ponowne użycie, tworząc klasę przechowującą wszystkie wartości animacji oraz funkcję Update(), która zwraca instancję tej klasy. Warto również wspomnieć, że podobnie jak w przypadku starych sposobów tworzenia animacji w systemie Android przy użyciu MotionLayout, istnieje wiele sposobów wykonywania przejść w Jetpack Compose. Na przykład w tabeli zobaczysz różne typy przejść:

EnterTransition : ExitTransition

SlideIn : SlideOut
FadeIn : FadeOut
SlideInHorizontally : SlideOutHorizontally
SlideInVertically : SlideOutVertically
ScaleIn : SlaceOut
ExpandIn : ShrinkOut
ExpandHorizontally : ShinkHorizontally
ExpandVertically : ShrinkVertically

Ponadto możesz dodawać własne, niestandardowe efekty animacji w Jetpack Compose poza już wbudowanymi animacjami wejścia i wyjścia, po prostu uzyskując dostęp do instancji przejścia elementarnego za pośrednictwem właściwości przejścia wewnątrz lambda treści dla AnimatedVisibility. Zauważysz także wszelkie dodane stany animacji.

Implementacja ułatwień dostępu w Jetpack Compose

Tworząc aplikacje na Androida, zawsze musimy mieć na uwadze dostępność, ponieważ dzięki temu technologia jest włączająca i zapewnia, że podczas tworzenia aplikacji uwzględnimy wszystkie osoby o specjalnych potrzebach. Dostępność powinna być dziełem zespołowym. Jeśli będziesz dobrze obsługiwany, korzyścią będzie to, że z aplikacji będzie korzystać więcej osób. Przystępna aplikacja jest lepsza dla każdego. Zmniejszasz także ryzyko bycia pozwanym. Istnieją różne rodzaje niepełnosprawności, takie jak upośledzenie wzroku, słuchu i motoryki. Jeśli otworzysz ustawienia dostępności, zobaczysz różne opcje, z których korzystają osoby niepełnosprawne na swoich urządzeniach.



Podobnie jak w przypadku poprzednich przepisów, będziemy nadal korzystać z naszego przykładowego projektu z poprzednich przepisów; nie musisz niczego instalować.

Jak to zrobić…

W tym przepisie opiszemy elementy wizualne, które są bardzo istotne:

1. Domyślnie, gdy dodamy funkcję Obraz, możesz zauważyć, że ma ona dwa parametry: malarza obrazu i opis zawartości, który wizualnie opisuje element:

Image(painter = , contentDescription = )

2. Kiedy ustawisz opis treści na null, wskazujesz platformie Android, że z tym elementem nie jest powiązana żadna akcja ani stan. Przejdźmy więc dalej i zaktualizujmy wszystkie nasze opisy treści:

Image(
modifier = modifier
painter = painterResource(city.imageResourceId),
contentDescription =
stringResource(R.string.city_images)) )

3. Upewnij się, że dodałeś ciąg znaków do folderu string res:

City Images

4. Zatem pamiętaj o dodaniu opisu treści do każdego obrazu, który tego wymaga.

5. W programie Compose możesz łatwo wskazać, czy tekst jest nagłówkiem, określając to w modyfikatorze i używając semantyki, aby pokazać, że jest to nagłówek. Dodajmy to w naszym ozdobnym tekście:

...
modifier = Modifier
.padding(18.dp)
.semantics { heading() }
...

6. Wreszcie możemy przystąpić do kompilacji, uruchomienia i sprawdzenia, czy nasza aplikacja jest dostępna, klikając ten link, aby dowiedzieć się, jak ręcznie przetestować za pomocą funkcji talkback lub testów automatycznych: https://developer.android.com/guide/topics/ interfejs użytkownika/dostępność/testowanie.

Jak to działa…

Jetpack Compose został stworzony z myślą o dostępności; to znaczy, że komponenty materialne, takie jak RadioButton, Switch i tak dalej, mają swój rozmiar ustawiony wewnętrznie, ale tylko wtedy, gdy te komponenty mogą odbierać interakcje użytkownika. Co więcej, każdy element ekranu, na który użytkownicy mogą kliknąć lub z którym można wejść w interakcję, powinien być wystarczająco duży, aby zapewnić niezawodną interakcję. Standardowy format ustawia te elementy na rozmiar co najmniej 48 dp dla szerokości i wysokości. Na przykład przełącznik ma ustawiony parametr onCheckChanged na wartość różną od null, obejmującą szerokość i wysokość co najmniej 48 dp; mielibyśmy CheckableSwitch() i NonCheckableSwitch():

@Composable
fun CheckableSwitch(){
var checked by remember { mutableStateOf(false) }
Switch(checked = checked, onCheckedChange = {} )
}
@Composable
fun NonCheckableSwitch(){
var checked by remember { mutableStateOf(false) }
Switch(checked = checked, onCheckedChange = null )
}

Gdy już zaimplementujesz dostępność w swoich aplikacjach, możesz ją łatwo przetestować, instalując narzędzia analityczne ze Sklepu Play - uiautomatorviewer i lint. Możesz także zautomatyzować testy za pomocą Espresso lub Roboelectric, aby sprawdzić obsługę dostępności. Na koniec możesz ręcznie przetestować aplikację pod kątem obsługi ułatwień dostępu, przechodząc do Ustawień, następnie do Dostępności i wybierając opcję Talkback. Znajduje się ono w górnej części ekranu; następnie naciśnij przycisk Włącz lub Wyłącz, aby włączyć lub wyłączyć funkcję TalkBack. Następnie przejdź do okna dialogowego z potwierdzeniem i kliknij OK, aby potwierdzić pozwolenie.

Więcej…

Jest więcej kwestii związanych z dostępnością, które programiści powinni wziąć pod uwagę podczas tworzenia swoich aplikacji, w tym stanu, w którym powinni być w stanie powiadomić swoich użytkowników o tym, czy został wybrany przycisk Przełącz. Dzięki temu ich aplikacje obsługują dostępność i odpowiadają standardom.

Implementacja grafiki deklaratywnej przy użyciu Jetpack Compose

W przypadku programowania na Androida Twoja aplikacja może mieć inne potrzeby, a tą potrzebą może być tworzenie własnej, niestandardowej grafiki zgodnie z zamierzonym celem. Jest to bardzo powszechne w wielu stabilnych i dużych bazach kodów Androida. Istotną częścią każdego niestandardowego widoku jest jego wygląd. Co więcej, niestandardowy rysunek może być bardzo łatwym lub złożonym zadaniem w zależności od potrzeb Twojej aplikacji. W Modern Android Development Jetpack Compose ułatwia pracę z niestandardową grafiką po prostu dlatego, że zapotrzebowanie jest ogromne. Na przykład wiele aplikacji może wymagać dokładnego kontrolowania tego, co dzieje się na ekranie; przypadek użycia może być tak prosty, jak umieszczenie okręgu na ekranie lub zbudowanie bardziej złożonej grafiki do obsługi znanych przypadków użycia.

Przygotowywanie się

Otwórz projekt Compose Basics, aby rozpocząć korzystanie z tego przepisu. Cały kod znajdziesz w sekcji Wymagania techniczne.

Jak to zrobić…

W naszym projekcie utwórzmy nowy pakiet i nazwijmy go roundexample; wewnątrz tego pakietu utwórz plik Kotlin i nazwij go DrawCircleCompose; wewnątrz pliku utwórz funkcję komponowalną CircleProgressIndicatorExample. Na razie nie będziesz musiał niczego importować:

1. Przejdźmy teraz dalej i zdefiniujmy naszą funkcję komponowalną. Ponieważ w naszym przykładzie chcemy wyświetlić tracker w okręgu, musimy unosić się w powietrzu, aby wypełnić nasze koło. Zdefiniujemy również kolory, które pomogą nam zidentyfikować postęp:

@Composable
fun CircleProgressIndicatorExample(tracker: Float, progress:
Float) {
val circleColors = listOf(
colorResource(id = R.color.purple_700),
colorResource(id = R.color.teal_200)
)

2. Teraz wywołajmy Canvas, aby narysować nasz łuk. Nadajemy naszemu okręgowi rozmiar 200.dp z wypełnieniem 8.dp. Ciekawie robi się w onDraw. startAngle jest ustawiony na -90; kąt początkowy jest ustawiony w stopniach, aby lepiej go zrozumieć. Zero oznacza godzinę trzecią. Możesz także pobawić się kątem początkowym, aby zobaczyć, jak przekłada się -90. Wartość logiczna useCenter wskazuje, czy łuk ma zamykać środek granic. Dlatego w naszym przypadku ustawiliśmy go na false. Następnie na koniec ustalamy styl, który może być dowolny, w zależności od naszych preferencji:

Canvas(
modifier = Modifier
.size(200.dp)
.padding(8.dp),
onDraw = {
this.drawIntoCanvas {
drawArc(
color = colorSecondary,
startAngle = -90f,
sweepAngle = 360f,
useCenter = false,
style = Stroke(width = 55f, cap =
StrokeCap.Butt),
size = Size(size.width, size.height)
)
colorResource(id = R.color.teal_200)


3. Właśnie narysowaliśmy pierwszą część okręgu; teraz musimy narysować postęp za pomocą pędzla, który wykorzystuje linearGradient:

drawArc(
brush = Brush.linearGradient(colors =
circleColors),
startAngle = -90f,
sweepAngle = progress(tracker, progress),
useCenter = false,
style = Stroke(width = 55f, cap =
StrokeCap.Round),
size = Size(size.width, size.height)
) …


4. Na koniec nasza funkcja postępu informuje SweaAngle, gdzie powinien opierać się nasz postęp, na podstawie naszych możliwości śledzenia:

private fun progress(tracker: Float, progress: Float): Float {
val totalProgress = (progress * 100) / tracker
return ((360 * totalProgress) / 100)
}


5. Uruchom funkcję podglądu. Powinieneś zobaczyć okrągły wskaźnik postępu, jak na rysunku



Ważna uwaga: Funkcja komponowania Canvas wykorzystuje Canvas do komponowania obiektu, który z kolei tworzy Canvas oparty na widoku i pomaga nim zarządzać. Należy również wspomnieć, że Compose ułatwia programistom utrzymanie stanu oraz tworzenie i zwalnianie wszelkich niezbędnych obiektów pomocniczych.

Jak to działa…

Ogólnie rzecz biorąc, Canvas pozwala określić obszar na ekranie, w którym chcesz narysować. W starym sposobie tworzenia aplikacji na Androida korzystaliśmy również z Canvas, a teraz w Compose jest on potężniejszy i cenniejszy. linearGradient tworzy gradient liniowy o określonych kolorach wzdłuż podanych współrzędnych początkowych i końcowych. Dla naszego przykładu nadajemy mu prostą kolorystykę, która jest dołączona do projektu. Funkcje rysujące mają domyślne parametry instrumentalne, których można użyć. Na przykład, jak widać, domyślnie DrawArc pobiera kilka danych wejściowych:



SweepAngle w naszym przykładzie, który jest rozmiarem łuku w stopniu narysowanym zgodnie z ruchem wskazówek zegara względem startAngle, zwraca funkcję obliczającą postęp. Funkcję tę można dostosować do własnych potrzeb. W naszym przykładzie przekazujemy moduł śledzący, postęp i zwracamy wartość zmiennoprzecinkową. Ponieważ chcemy zapełnić okrąg tworzymy cal totalProgress, który sprawdza postęp * 100 podzielony przez tracker i zwraca 360 (kółko) * nasz postęp podzielony przez 100. Możesz dostosować tę funkcję do swoich potrzeb. Możesz także napisać kod, aby nasłuchiwać, gdzie jesteś, i sterować postępem w oparciu o wartość wejściową z utworzonego przez Ciebie słuchacza.

Więcej…

Dzięki Canvas i rysunkom niestandardowym możesz zrobić więcej. Niezwykłym sposobem na poszerzenie wiedzy na ten temat jest przejrzenie starych rozwiązań opublikowanych na Stack Overflow, takich jak rysowanie serca lub innego kształtu, i sprawdzenie, czy możesz zrobić to samo w aplikacji Compose.

Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt

Wszystkie aplikacje na Androida wyświetlają stan użytkownikom, co pomaga informować użytkowników o wyniku i czasie. Stan w aplikacji na Androida to dowolna wartość, która zmienia się w czasie, a dobrym przykładem jest toast wyświetlający komunikat w przypadku wystąpienia błędu. W tym rozdziale czytelnicy dowiedzą się, jak lepiej obsługiwać stan interfejsu użytkownika dzięki nowej bibliotece Jetpack. Można śmiało powiedzieć, że z wielką mocą wiąże się wielka odpowiedzialność, a zarządzanie stanem dowolnego komponentu Composable wymaga innego podejścia w porównaniu ze starszym sposobem tworzenia widoków Androida lub, jak wielu może to nazwać, sposobem imperatywnym. Oznacza to, że biblioteka Jetpack, Compose, całkowicie różni się od układów XML. Obsługa stanu interfejsu użytkownika w systemie widoku XML jest bardzo prosta. Proces ten obejmuje ustawienie właściwości widoków tak, aby odzwierciedlały bieżący stan - czyli odpowiednie pokazywanie lub ukrywanie widoków. Na przykład podczas ładowania danych z interfejsu API możesz ukryć widok ładowania, wyświetlić widok treści i wypełnić go żądanymi widokami. Jednakże w Compose nie można zmienić komponentu Composable po jego narysowaniu przez aplikację. Możesz jednak zmienić wartości przekazywane do każdego obiektu Composable, zmieniając stan otrzymywany przez każdy obiekt Composable. Dlatego przydaje się wiedza o lepszym zarządzaniu stanem podczas tworzenia solidnych aplikacji na Androida.

Implementacja DI za pomocą Jetpack Hilt

W programowaniu obiektowym DI jest niezbędne. Niektórzy ludzie go używają, a niektórzy wolą go nie używać z własnych powodów. Jednak DI to praktyka projektowania obiektów w taki sposób, że otrzymują instancje obiektu z innych fragmentów kodu, zamiast konstruować je wewnętrznie. Jeśli znasz zasady SOLID, wiesz, że ich głównym celem jest ułatwienie projektowania oprogramowania w utrzymaniu, czytaniu, testowaniu i rozwijaniu. Ponadto DI pomaga nam przestrzegać niektórych zasad SOLID. Zasada inwersji zależności pozwala na łatwą rozbudowę bazy kodu i poszerzanie jej o nowe funkcjonalności oraz poprawia możliwość ponownego wykorzystania. W nowoczesnym rozwoju Androida DI jest niezbędne i w tym przepisie zaimplementujemy go w naszej aplikacji. Istnieją różne typy bibliotek, których można używać w systemie Android do DI, takie jak Koin, Dagger i Hilt; Hilt wykorzystuje moc Daggera i czerpie korzyści z poprawności kompilacji, dobrej wydajności w czasie wykonywania, obsługi studia Android i skalowalności. Do tego przepisu użyjemy Hilt, który udostępnia kontenery dla każdej klasy Androida w naszym projekcie i automatycznie zarządza ich cyklem życia.

Przygotowywanie się

Podobnie jak w poprzednich przepisach, do dodania DI wykorzystamy projekt, z którego korzystaliśmy w poprzednich przepisach.

Jak to zrobić…

Hilt korzysta z funkcji Java; upewnij się, że Twój projekt znajduje się w pliku app/build.gradle i że masz następujące opcje kompilacji:

android {

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}

Ta opcja jest już dodawana automatycznie, ale na wszelki wypadek sprawdź, czy ją masz. Zacznijmy:

1. Najpierw musimy dodać wtyczkę Hilt-android-gradle-plugin do pliku głównego naszego projektu build.gradle(Project:SampleLogin):

plugins {
id 'com.google.dagger.Hilt.android' version '2.44'
apply false
}

2. Następnie w naszym pliku app/build.gradle dodaj te zależności i zsynchronizuj projekt. Powinno działać bez żadnych problemów:

plugins {
id 'kotlin-kapt'
id 'dagger.Hilt.android.plugin' }
dependencies {
implementation "com.google.dagger:Hiltandroid:
2.44"
kapt "com.google.dagger:Hilt-compiler:2.44"
}

3. Teraz przejdźmy dalej i dodajmy klasę Application. Wszystkie aplikacje korzystające z Hilt muszą mieć klasę Application z adnotacją @HiltAndroidApp i musimy wywołać klasę Application, którą tworzymy w Manifeście:

@HiltAndroidApp
class LoginApp : Application()
4. In our Manifest folder, let′s add LoginApp:
android:name=".LoginApp"


5. Teraz, gdy mamy już za sobą konfigurację, musimy rozpocząć pracę z Hiltem, dodając wymagane adnotacje do naszej klasy. W MainActivity.kt musimy dodać adnotację @AndroidEntryPoint:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {


6. Przejdźmy dalej i wyświetlmy to, co zrobiliśmy, uruchamiając polecenie ./gradlew :app: zależnośći, a zobaczymy coś podobnego do rysunku



Możesz także wyświetlić zależność w Android Studio. Można to zrobić, klikając kartę Gradle po prawej stronie i wybierając opcję rozwiń:twójmoduł | Zadania | android. Na koniec kliknij dwukrotnie androidDependencies, aby go uruchomić. Na koniec skompiluj i uruchom projekt; powinno działać pomyślnie.

Jak to działa…

@HiltAndroidApp wyzwala generację kodu Hilt, w tym klasy bazowej dla naszej aplikacji, która działa jako kontener zależności na poziomie aplikacji. Adnotacja @AndroidEntryPoint dodaje kontener DI do oznaczonej nim klasy Androida. W przypadku korzystania z Hilt wygenerowany komponent Hilt jest dołączany do cyklu życia obiektu Application i udostępnia jego zależności. Hilt obecnie obsługuje następujące klasy Androida:

•  ViewModel z adnotacją @HiltViewModel
•  Aplikacja oznaczona jako @HiltAndroidApp
•  Działalność
•  Fragment
•  Pogląd
•  Praca
•  Odbiornik transmisji

Później użyjemy innych niezbędnych adnotacji w Hilt, na przykład adnotacji @Module, @InstallIn i @Provides. Adnotacja @Module oznacza klasę, w której można dodać powiązanie dla typów, których nie można wstrzyknąć do konstruktora. @InstallIn wskazuje, który kontener DI Hiltgenerated (lub komponent singleton) musi być dostępny w powiązaniu modułu kodu. Na koniec @Provides wiąże typ, którego nie można wstrzyknąć do konstruktora. Jego typem zwracanym jest typ powiązania, może przyjmować parametry zależności i za każdym razem, gdy potrzebujesz instancji, treść funkcji jest wykonywana, jeśli typ nie ma zakresu.

Implementowanie narzędzia Compose w istniejącym projekcie opartym na układzie XML

Ponieważ Compose to nowa platforma interfejsu użytkownika, wiele baz kodu nadal w dużym stopniu opiera się na układach XML. Jednak wiele firm decyduje się na budowanie nowych ekranów przy użyciu narzędzia Compose, co można osiągnąć poprzez wykorzystanie istniejących układów XML i dodanie unikalnych widoków przy użyciu znaczników XML ComposeView. W tym przepisie omówimy dodanie widoku tworzenia do układu XML.

Przygotowywanie się

W tym przepisie możemy utworzyć nowy projekt lub zdecydować się na użycie istniejącego projektu, który nie opiera się w dużym stopniu na Compose. Spróbujemy wyświetlić powitanieDialog i użyć układu XML, aby pokazać, jak możemy użyć tagu ComposeView w układach XML. Jeśli masz już projekt, nie musisz go konfigurować; możesz przejść do kroku 4 w poprzedniej sekcji Jak to zrobić….

Jak to zrobić…

Przejdźmy teraz dalej i zbadajmy, w jaki sposób możemy wykorzystać istniejące układy XML za pomocą narzędzia Compose:

1. Zacznijmy od stworzenia nowego projektu lub wykorzystania już istniejącego; jeśli utworzysz nowe działanie inne niż Compose, możesz użyć opcji DesertActivity i nadać mu dowolną nazwę.
2. Jeśli masz już skonfigurowany projekt, możesz pominąć ten krok. Jeśli zdecydujesz się na utworzenie nowego projektu, będziesz mieć MainActivity, a ponieważ jest to stary sposób tworzenia widoków, zauważysz układ XML w folderze zasobów z TextView, który ma Hello world. Możemy to usunąć, ponieważ nie będziemy go używać.
3. Jeśli masz już gotowy projekt, możesz uruchomić PowitanieDialog na dowolnym ekranie. Ponadto, jeśli zdecydujesz się utworzyć przycisk zamiast okna dialogowego, to też jest w porządku, ponieważ chodzi o pokazanie, jak możemy używać tagów XML w Jetpack Compose.
4. Teraz przejdźmy dalej i dodajmy tag XML do pliku Activity_main.xml i nadajmy naszemu widokowi Compose wartość identyfikatora. Gdy dodasz ComposeView po raz pierwszy, pojawi się komunikat o błędzie, jeśli nadal będziesz musiał dodać zależność. Śmiało i kliknij Dodaj zależność od Android.compose. ui:ui, a projekt zostanie zsynchronizowany, jak pokazano na rysunku



5. Po zsynchronizowaniu projektu błąd zniknie i powinieneś móc używać tego widoku w MainActivity lub tam, gdzie chcesz używać ComposeView:

android:id="@+id/alert_dialog"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

6. Dodajmy także viewBinding do naszego build.gradle(Module:app), abyśmy mogli łatwo uzyskać dostęp do naszego widoku w MainActivity. Ponadto, jeśli masz już skonfigurowane viewBinding, możesz pominąć tę część:

buildFeatures{
viewBinding true
}

7. Po zsynchronizowaniu projektu możemy przejść dalej i w MainActivity uzyskać dostęp do ComposeView poprzez powiązanie. Co więcej, będzie zawierała metodę setContent{}, w której możesz ustawić wszystkie swoje obiekty Composable i zawinąć je w swój motyw:

class MainActivity : AppCompatActivity() {
private lateinit var activityBinding:
ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
activityBinding =
ActivityMainBinding.inflate(layoutInflater)
setContentView(activityBinding.root)
activityBinding.alertDialog.setContent {
GreetingAlertDialog()
}
}
}

8. Nasza PowitanieAlertDialog() będzie miała AlertDialog() Composable, tytuł i tekst, który dostarczy naszą wiadomość jako prosty element tekstowy. Tytuł będzie brzmieć "Witam", ponieważ jest to powitanie, a wiadomość będzie brzmieć "Witam i dziękuję za bycie częścią społeczności Androida". Możesz dostosować to do swoich potrzeb:

@Composable
fun SimpleAlertDialog() {
AlertDialog(
onDismissRequest = { },
confirmButton = {
TextButton(onClick = {})
{ Text(text = "OK") }
},
dismissButton = {
TextButton(onClick = {})
{ Text(text = "OK") }
},
title = { Text(text = "Hello") },
text = { Text(text = "Hello, and thank you for
being part of the Android community") }
)
}

9. Aby utworzyć komponenty Compose, musisz dodać zależność Compose Material Design do swojej aplikacji gradle. W zależności od tego, co obsługuje Twoja aplikacja, możesz wykorzystać komponenty Compose Material 3, które stanowią kolejną ewolucję Material Design i mają zaktualizowany motyw.

10. Możesz łatwo dostosować funkcje, takie jak dynamiczny kolor i inne. Dlatego na razie, ponieważ aplikacja, z której korzystam, nie została zmigrowana do Material 3, skorzystam z tego importu - implementacji "androidx.Compose. material:material:1.x.x". Tutaj możesz użyć dowolnego importu, który odpowiada Twoim potrzebom.

11. Możesz także utworzyć niestandardowy widok, który stanowi kontynuację AbstractComposeView:

class ComposeAlertDialogComponent @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
@Composable
override fun Content() {
GreetingAlertDialog()
}
}

12. Na koniec, po uruchomieniu aplikacji, powinno pojawić się okno dialogowe z tytułem i tekstem; Rysunek przedstawia okno dialogowe z już istniejącego projektu, więc na pewno będzie się różnić w zależności od wykonanych kroków:



Jak to działa…

Najpierw powiększamy układ XML, który definiujemy w naszym folderze zasobów układu. Następnie, używając wiązania, otrzymaliśmy ComposeView przy użyciu utworzonego identyfikatora XML, ustawiliśmy strategię Compose, która najlepiej sprawdza się w przypadku naszego widoku hosta, i wywołaliśmy setContent, aby użyć Compose. Aby w swojej działalności móc utworzyć dowolny ekran oparty na Compose, musisz upewnić się, że wywołałeś metodę setContent{} i przekazałeś dowolną utworzoną funkcję Composable. Aby dokładniej poznać metodę setContent, została ona napisana jako funkcja rozszerzenia ComponentActivity i oczekuje funkcji Composable jako ostatniego parametru. Istnieje również lepszy sposób zademonstrowania działania setContent{} w celu zintegrowania drzewa Composable z aplikacją na Androida.



ViewCompositionStrategy pomaga określić, kiedy pozbyć się kompozycji; stąd widoki interfejsu użytkownika Compose, takie jak ComposeView i AbstractComposeView, korzystają z ViewCompositonStrategy, która pomaga zdefiniować to zachowanie.

Zrozumienie i obsługa rekompozycji w Jetpack Compose

Jetpack Compose jest wciąż bardzo nowym rozwiązaniem i wiele firm zaczyna z niego korzystać. Co więcej, Google wykonało świetną robotę, udostępniając programistom obszerną dokumentację pomagającą im wdrożyć nowy zestaw narzędzi interfejsu użytkownika. Jednak pomimo całej dokumentacji, jedno pojęcie wymaga wyjaśnienia. I to jest rekompozycja. To prawda, każde nowe oprogramowanie ma swoje zalety i wady, a im więcej osób zaczyna z niego korzystać, tym więcej osób zaczyna przekazywać opinie - stąd potrzeba dalszych ulepszeń. Rekompozycja w aplikacji Compose polega na ponownym wywołaniu funkcji Composable po zmianie danych wejściowych. Możesz też o tym pomyśleć, gdy zmieni się struktura kompozycji i relacja. O ile jej parametry nie ulegną zmianie, chcemy uniknąć ponownego wywoływania funkcji Composable w większości przypadków użycia. Zatem w tym przepisie przyjrzymy się, jak zachodzi rekompozycja oraz w jaki sposób można debugować i rozwiązywać wszelkie rekompozycje w aplikacji.

Jak to zrobić…

Ponieważ nasz system widoków jest prosty, sprawdzimy, czy w naszym projekcie Login mamy jakąś rekompozycję:

1. Możemy spojrzeć na prosty przykład i zobaczyć, jak nastąpi rekompozycja:

@Composable
fun UserDetails(
name: String,
gender: String,
) {
Box() {
Text(name)
Spacer()
Text(gender)
}}

W naszym przykładzie funkcja Tekst zostanie ponownie ułożona po zmianie imienia, a nie po zmianie płci. Ponadto wartość wejściowa gender:String zostanie ponownie ułożona tylko w przypadku zmiany płci.

2. Możesz także uruchomić Inspektora układu i wykorzystać go do debugowania rekompozycji. Jeśli nie ma go w stacji dokującej Android Studio, możesz go uruchomić, przechodząc do Widok | Okna narzędziowe | Inspektor układu. Zajrzymy do LoginContent i sprawdzimy, czy mamy jakąś rekompozycję.



3. Po uruchomieniu Inspektora układu upewnij się, że masz podłączony do niego emulator.



4. Śmiało, rozwiń punkt wejściowy SampleLoginTheme, a zauważysz, że nasz obecny system widoku nie jest skomplikowany. Jak widać, Inspektor układu nie pokazuje żadnych liczników rekompozycji. Oznacza to, że gdyby nasza aplikacja zawierała jakiekolwiek liczniki rekompozycji, wyświetliłyby się one w Inspektorze układu.



5. Wreszcie, jak zauważyłeś, w naszej aplikacji nie zachodzi żadna rekompozycja, ale zawsze warto sprawdzić aplikację, aby dowiedzieć się, co może być przyczyną rekompozycji i ją naprawić.

Ważna uwaga


Korzystanie z efektów ubocznych może spowodować, że użytkownicy Twojej aplikacji doświadczą dziwnego i nieprzewidywalnego zachowania w Twojej aplikacji. Ponadto efektem ubocznym jest każda zmiana widoczna w pozostałej części aplikacji. Na przykład zapisywanie właściwości obiektu współdzielonego, aktualizacja obserwowalnego w ViewModel i aktualizacja współdzielonych preferencji to niebezpieczne skutki uboczne.

Jak to działa…

Aby ułatwić adaptację, Compose pomija wywołania lambda i wszelkie funkcje podrzędne, które nie mają żadnych zmian na wejściu. Lepsza obsługa zasobów ma sens, ponieważ w aplikacji Compose animacje i inne elementy interfejsu użytkownika mogą powodować rekompozycję w każdej klatce. Możemy zagłębić się w szczegóły i użyć diagramu, aby pokazać, jak działa cykl życia kompozycji Jetpack. Krótko mówiąc, cykl życia funkcji Composable jest definiowany przez trzy istotne zdarzenia:

•  Bycie opanowanym
•  Ponowne skomponowanie lub brak przekomponowania
•  Nie jest już skomponowany



Aby zrozumieć, jak działa Compose, warto wiedzieć, co składa się na warstwę architektoniczną Compose. Ogólny przegląd warstwy architektonicznej Jetpack Compose obejmuje aspekty Material, Foundation, UI i Runtime.



W Material ten moduł implementuje system Material Design dla interfejsu użytkownika Compose. Ponadto zapewnia system motywów, stylizowane komponenty i wiele więcej. Fundament to miejsce, w którym mamy elementy składowe systemu projektowania, takie jak interfejs użytkownika, wiersz, kolumna i inne. Warstwa interfejsu użytkownika składa się z wielu modułów, które implementują podstawy zestawu narzędzi interfejsu użytkownika.

Pisanie testów interfejsu użytkownika dla widoków tworzenia aplikacji

Testowanie kodu jest niezbędne podczas tworzenia aplikacji na Androida, szczególnie jeśli aplikacje mają wielu użytkowników. Co więcej, pisząc testy dla swojego kodu, w zasadzie weryfikujesz funkcje, zachowanie, poprawność i wszechstronność aplikacji na Androida. Najpopularniejsze narzędzia do testowania interfejsu użytkownika w systemie Android to Espresso, UI Automator, Calabash i Detox. W tym tekście będziemy jednak używać słowa Espresso. Najbardziej zauważalne zalety espresso to:

o Jest łatwy w konfiguracji
o Posiada bardzo stabilne cykle testowe
o Obsługuje JUnit 4
o Jest przeznaczony wyłącznie do testowania interfejsu użytkownika systemu Android
o Nadaje się do pisania testów czarnej skrzynki
o Wspiera także działania testowe poza aplikacją

Przygotowywanie się

Aby skorzystać z tego, musisz mieć ukończone poprzednie przepisy.

Jak to zrobić…

Podobnie jak w przypadku innych przepisów w tej części, użyjemy nowego projektu, który stworzyliśmy w części 1:

1. Przejdźmy dalej do pakietu androidTest w naszym folderze projektu.
2. Zacznij od utworzenia nowej klasy w pakiecie androidTest i nadaj jej nazwę LoginContentTest.kt. W Jetpack Compose testowanie jest bardziej dostępne i musimy mieć unikalne tagi dla naszych widoków.
3. W tym kroku wróćmy do naszego głównego pakietu (com.name.SampleLogin), utwórz nowy pakiet i nadaj mu nazwę util. Wewnątrz util utwórzmy nową klasę i nazwijmy ją TestTags, która będzie obiektem. Tutaj będziemy mieli inny obiekt, nazwiemy go LoginContent i utworzymy stałe wartości, które będziemy mogli wywołać w naszym widoku:

object TestTags {
object LoginContent {
const val SIGN_IN_BUTTON = "sign_in_button"
const val LOGO_IMAGE = "logo_image_button"
const val ANDROID_TEXT = "community_text"
const val USERNAME_FIELD = "username_fields"
const val PASSWORD_FIELD = "password_fields"
}
}

4. Teraz, gdy stworzyliśmy tagi testowe, wróćmy do naszego LoginContent i dodaj je do wszystkich widoków w Modifier(), aby podczas testowania łatwiej było zidentyfikować widok za pomocą dodanego tagu testowego. Zobacz następujący fragment kodu:

Image(
modifier = modifier.testTag(LOGO_IMAGE),
painter = painterResource(id =
R.drawable.ic_launcher_foreground),
contentDescription = "Logo"
)

5. Wewnątrz naszej klasy LoginCotentTest przejdźmy teraz do konfiguracji naszego środowiska testowego. Będziemy musieli utworzyć @get:Rule, który będzie zawierał adnotacje dla pól odwołujących się do reguł lub metod zwracających regułę. Zgodnie z regułą utwórzmy ComposeRuleTest i zainicjujmy go:

@get:Rule
val ComposeRuleTest = createAndroidComposeRule()

6. Dodaj następującą funkcję, która pomoże nam skonfigurować zawartość. Powinniśmy wywołać tę funkcję w naszej funkcji z adnotacjami Test

private fun initCompose() {
ComposeRuleTest.activity.setContent {
SampleLoginTheme {
LoginContent()
}
}
}

7. Na koniec przejdźmy dalej i dodajmy nasz pierwszy test. Na potrzeby testów, które napiszemy, zweryfikujemy, czy widoki wyświetlają się na ekranie tak, jak tego oczekujemy:

@Test
fun assertSignInButtonIsDisplayed(){
initCompose()
ComposeRuleTest.onNodeWithTag(SIGN_IN_BUTTON,
true).assertIsDisplayed()
}
@Test
fun assertUserInputFieldIsDisplayed(){
initCompose()
ComposeRuleTest.onNodeWithTag(USERNAME_FIELD,
true).assertIsDisplayed()
}

8. SIGN_IN_BUTTON i USERNAME_FIELD są importowane z tagów testowych, które stworzyliśmy i są już używane tylko przez jeden widok, przycisk logowania.

9. Śmiało, uruchom testy, a pojawi się okno dialogowe pokazujące działający proces; jeśli się powiedzie, testy zaliczą. W naszym przypadku testy powinny przejść pomyślnie.



Ważna uwaga

W przypadku tych testów nie będziesz musiał dodawać żadnych zależności; wszystko, czego potrzebujemy, jest już dostępne do naszego użytku.

Jak to działa…

Podczas uzyskiwania dostępu do działania używamy metody createAndroidComposeRule<>(). Testowanie i upewnianie się, że aplikacje wyświetlają oczekiwany wynik, jest niezbędne. Właśnie dlatego Android Studio używa emulatora, aby pomóc programistom testować swój kod, aby zapewnić działanie aplikacji tak samo, jak na standardowych urządzeniach. Co więcej, telefony z Androidem są wyposażone w opcję dla programistów, z której mogą korzystać programiści, co jeszcze bardziej ułatwia pracę na różnej liczbie urządzeń obsługiwanych przez Androida i pomaga odtwarzać błędy, które trudno znaleźć w emulatorach. Testując nasz kod Compose, poprawiamy jakość naszej aplikacji, wychwytując błędy na wczesnym etapie procesu tworzenia. W tym rozdziale omówiliśmy tworzenie większej liczby widoków, aby zademonstrować, jak działa Jetpack Compose; co więcej, nasze przypadki testowe muszą uwzględniać działania użytkownika, ponieważ żadnego nie wdrożyliśmy. W innym ustawieniu możemy napisać bardziej istotne testy potwierdzające zamierzone działanie i zrobimy to w późniejszych rozdziałach. Ponadto Compose udostępnia testowe interfejsy API umożliwiające wyszukiwanie elementów, weryfikowanie ich atrybutów i wykonywanie działań użytkownika. Ponadto zawierają również zaawansowane funkcje, takie jak między innymi manipulacja czasem. Jawne wywołanie adnotacji @Test jest bardzo ważne podczas pisania testów, ponieważ adnotacja ta informuje JUnit, że funkcja, do której jest dołączona, ma działać jako funkcja testowa. Ponadto testy interfejsu użytkownika w aplikacji Compose wykorzystują semantykę do interakcji z hierarchią interfejsu użytkownika. Semantyka, jak sama nazwa wskazuje, nadaje znaczenie fragmentowi interfejsu użytkownika, na przykład .onNodeWithTag. Część lub element interfejsu użytkownika może oznaczać wszystko, od pojedynczego elementu Composable po pełny ekran. Jeśli spróbujesz uzyskać dostęp do niewłaściwego węzła, drzewo semantyki generowane wraz z hierarchią interfejsu użytkownika będzie narzekać.

Więcej…

Istnieją inne narzędzia testujące:

•  Rejestrator testów espresso zapewnia programistom szybszy, interaktywny sposób testowania codziennych zachowań użytkownika i elementów wizualnych w aplikacji.
•  App Crawler niewątpliwie wykorzystuje podejście bardziej niewymagające użycia rąk, aby pomóc Ci testować działania użytkownika bez konieczności utrzymywania lub pisania jakiegokolwiek kodu. Za pomocą tego narzędzia możesz łatwo skonfigurować dane wejściowe, takie jak wprowadzenie nazwy użytkownika i hasła.
•  Monkey to urządzenie wiersza poleceń, które również testuje aplikację w warunkach skrajnych, wysyłając losowy przepływ danych wejściowych/weryfikacji użytkownika lub działań poprzez dotknięcie do instancji urządzenia lub emulatora.

Pisanie testów dla ViewModels

W przeciwieństwie do kontrolera Model-View-Controller (MVC) i Model-View-Presenter (MVP), MVVM jest preferowanym wzorcem projektowym w nowoczesnym programowaniu Androida ze względu na jednokierunkowe dane i zależność przepływu. Co więcej, staje się bardziej dostępny dla testów jednostkowych, jak zobaczysz w tym przepisie.

Przygotowywanie się

Wykorzystamy naszą poprzednią recepturę, implementację ViewModel i zrozumienie stanu w Compose, aby przetestować naszą logikę i zmiany stanu.

Jak to zrobić…

tym przepisie napiszemy testy jednostkowe, aby zweryfikować zmiany stanu uwierzytelnienia, ponieważ to właśnie wdrożyliśmy do tej pory:

1. Zacznij od utworzenia klasy LoginViewModelTest w pakiecie testowym:



2. Użyjemy biblioteki testowej cashapp/turbine dla przepływów współprogramowych, aby przetestować utworzony przez nas przepływ. Dlatego będziesz musiał dołączyć fragment kodu przetwarzającego do build.gradle:

repositories {
mavenCentral()
}
dependencies {
testImplementation 'app.cash.turbine:turbine:0.x.x'
}

3. Po utworzeniu klasy skonfiguruj @Before, który będzie uruchamiany przed każdym testem:

class LoginViewModelTest {
private lateinit var loginViewModel: LoginViewModel
@Before
fun setUp(){
loginViewModel = LoginViewModel(
dispatchers =
SampleLoginDispatchers.createTestDispatchers(
UnconfinedTestDispatcher()),
stateHandle = SavedStateHandle()
)
}
}

4. Jak widać, użyliśmy SampleLoginDispatchers.createTestDispatchers. W przypadku UnconfinedTestDispatcher należy uwzględnić zależności testowe i zaimportować, zaimportować kotlinx.coroutines.test.UnconfinedTestDispatcher.

5. Teraz, gdy mamy już gotowe ustawienia, przejdźmy do stworzenia naszego testu, weryfikującego zmiany stanu uwierzytelnienia:

@Test
fun `test authentication state changes`() = runTest {…}

6. Wewnątrz naszej funkcji Test będziemy teraz musieli uzyskać dostęp do funkcji loginViewModel i przekazać fałszywe wartości do parametrów:

@Test
fun `test authentication state changes`() = runTest {
loginViewModel.userNameChanged("Madona")
loginViewModel.passwordChanged("home")
loginViewModel.passwordVisibility(true)
loginViewModel.state.test {
val stateChange = awaitItem()
Truth.assertThat(stateChange).isEqualTo(
AuthenticationState(
userName = "Madona",
password = "home",
togglePasswordVisibility = true
)
)
}
}

7. Na koniec uruchom test, który powinien przejść pomyślnie.



Jak to działa…

Jak wspomniano wcześniej, najbardziej zauważalną zaletą MVVM jest możliwość napisania kodu, który można szybko przetestować. Ponadto architektura w systemie Android polega na wybieraniu kompromisów. Każda architektura ma swoje plusy i minusy; w zależności od potrzeb Twojej firmy możesz współpracować z inną. Tworzymy lateint var loginViewModel, aby skonfigurować klasę do testowania, a dzieje się tak, ponieważ logika do przetestowania znajduje się w ViewModel. Używamy UnconfinedDispatcher, który tworzy instancję dyspozytora Unconfined. Oznacza to, że wykonywane przez niego zadania nie są ograniczone do żadnego konkretnego wątku i tworzą pętlę zdarzeń. Tym różni się tym, że pomija opóźnienia, tak jak robią to wszystkie instancje TestDispatcher. Domyślnie runTest() udostępnia StandardTestDispatcher, który nie wykonuje natychmiast współprogramów potomnych. Używamy Prawdy do naszych twierdzeń, aby pomóc nam stworzyć bardziej czytelny kod, a znaczące zalety Prawdy są następujące:

•  Wyrównuje rzeczywiste wartości do lewej
•  Daje nam bardziej szczegółowe komunikaty o błędach
•  Oferuje bogatsze operacje pomagające w testowaniu

Istnieją również inne alternatywy, takie jak Mockito, Mockk i inne, ale w tej sekcji użyliśmy Prawdy. Korzystaliśmy również z biblioteki Cashapp, która pomaga nam testować przepływy współprogramów.

Nawigacja w rozwoju współczesnego Androida

W programowaniu na Androida nawigacja to interakcja, która umożliwia użytkownikom aplikacji na Androida nawigację do różnych ekranów aplikacji, z nich i wycofywanie się z nich. Jest to czynność bardzo istotna w ekosystemie mobilnym. Nawigacja Jetpack uprościła nawigację pomiędzy ekranami, a w tym rozdziale dowiemy się, jak zaimplementować nawigację za pomocą prostego kliknięcia widoku, z najczęściej używanego dolnego paska nawigacji, nawigowania za pomocą argumentów i nie tylko.

Implementacja dolnego paska nawigacyjnego wykorzystującego cele nawigacji

W programowaniu na Androida bardzo powszechne jest posiadanie dolnego paska nawigacyjnego; pomaga poinformować użytkowników, że w aplikacji znajdują się różne sekcje. Ponadto inne aplikacje decydują się na dołączenie szuflady nawigacji, w której przechowywany jest profil i dodatkowe informacje o aplikacji. Doskonałym przykładem aplikacji, która wykorzystuje zarówno szufladę nawigacyjną, jak i dolną nawigację, jest Twitter. Należy również wspomnieć, że niektóre firmy wolą mieć górny pasek nawigacyjny. Ponadto inne, takie jak Sklep Google Play, mają nawigację zarówno u dołu, jak i w szufladzie.
Przygotowywanie się

Utwórz nowy projekt na Androida za pomocą preferowanego edytora lub Android Studio lub możesz użyć dowolnego projektu z poprzednich przepisów.

Jak to zrobić…

W tym przepisie utworzymy nowy projekt i nazwiemy go BottomNavigationBarSample:

1. Po utworzeniu naszego nowego, pustego projektu Activity BottomNavigationBarSample zaczniemy od dodania wymaganej zależności nawigacyjnej w build.gradle, a następnie zsynchronizujemy projekt:

implementation 'android.navigation:navigation-compose:2.5.2'

2. Jak zauważyliśmy w poprzednim nowym projekcie, kiedy tworzysz nowy projekt, towarzyszy mu kod, funkcja Powitanie(); możesz śmiało usunąć ten kod.

3. Po usunięciu tego kodu utwórzmy zapieczętowaną klasę w głównym katalogu pakietu i nazwijmy ją Destination.kt, gdzie zdefiniujemy ciąg trasy, ikonę: Int i tytuł: String dla naszych dolnych elementów nawigacyjnych:

sealed class Destination(val route: String, val icon: Int, val
title: String) {…}

Ściśle mówiąc, być może nie będziemy potrzebować klasy zapieczętowanej, ale jest to lepszy sposób na wdrożenie nawigacji. Zapieczętowana klasa w Kotlinie reprezentuje ograniczoną hierarchię klas, która zapewnia większą kontrolę nad dziedziczeniem. Alternatywnie możesz myśleć o niej jak o klasie, która w swojej wartości może mieć jeden z typów z ograniczonego zestawu, ale nie może mieć żadnych innych typów.

4. W klasie zapieczętowanej przejdźmy dalej i stwórzmy nasze miejsca docelowe. W naszym przykładzie założymy, że tworzymy aplikację do budżetowania. Dlatego miejscami docelowymi, które możemy mieć, są transakcje, budżety, zadania i ustawienia. Zobacz następny krok, jak zdobyć ikony; ponadto będziesz musiał je zaimportować. W ramach dobrej praktyki można wyodrębnić zasób String i zapisać go w pliku XML String. Możesz spróbować tego jako małego ćwiczenia:

sealed class Destination(val route: String, val icon: Int, val
title: String) {
object Transaction : Destination(
route = "transactions", icon =
R.drawable.ic_baseline_wallet,
title = "Transactions"
)
object Budgets : Destination(
route = "budget", icon =
R.drawable.ic_baseline_budget,
title = "Budget"
)
object Tasks : Destination(route = "tasks", icon =
R.drawable.ic_add_task, title = "Tasks")
object Settings : Destination(
route = "settings", icon =
R.drawable.ic_settings,
title = "Settings"
)
companion object {
val toList = listOf(Transaction, Budgets,
Tasks, Settings)
}
}

5. Dostęp do ikon jest łatwy, wystarczy kliknąć folder zasobów (res), a następnie przejść do Vector Assets | Clip Art, który uruchomi i wyświetli darmowe ikony, których możesz użyć, jak pokazano na rysunku :



6. Możesz także przesłać plik SVG i uzyskać do niego dostęp za pośrednictwem Asset Studio.

7. Teraz w przypadku właśnie dodanych miejsc docelowych dodajmy fikcyjny tekst, aby sprawdzić, czy rzeczywiście podczas nawigacji znajdujemy się na właściwym ekranie. Utwórz nowy plik i nazwij go AppContent. kt. Wewnątrz AppContent dodamy funkcję Transaction, która będzie naszym ekranem głównym, na którym nowi użytkownicy będą wchodzić do aplikacji po raz pierwszy; później będą mogli przejść do innych ekranów:

@Composable
fun Transaction(){
Column(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
) {


}
}

8. Śmiało, dodaj pozostałe trzy ekrany: Zadanie, Budżet i Ustawienia, używając tego samego, możliwego do komponowania wzoru.

9. Musimy teraz utworzyć dolny pasek nawigacyjny z możliwością komponowania i poinformować funkcję Composable, jak ma reagować na kliknięcie, a także przywrócić stan po ponownym wybraniu wcześniej wybranego elementu: @Composable

@Composable
fun BottomNavigationBar(navController: NavController, appItems:
List) {
BottomNavigation(
backgroundColor = colorResource(id =
R.color.purple_700),
contentColor = Color.White
) {

}
}

10. Przejdźmy teraz do MainActivity i utwórz NavHost oraz kilka funkcji, które można komponować: AppScreen(), AppNavigation() i BottomNavigationBar(). Każdy kontroler nawigacji musi być powiązany z pojedynczym hostem nawigacji, który można komponować, ponieważ łączy on kontroler z wykresem nawigacji, który pomaga określić możliwe do komponowania kierunki:

@Composable
fun AppNavigation(navController: NavHostController) {
NavHost(navController, startDestination =
Destination.Transaction.route) {
composable(Destination.Transaction.route) {
Transaction()
}
composable(Destination.Budgets.route) {
Budget()
}
composable(Destination.Tasks.route) {
Tasks()
}
composable(Destination.Settings.route) {
Settings()
}
}
}

11. Na koniec sklejmy wszystko w jedną całość, tworząc kolejną funkcję komponowalną i wywołując ją AppScreen(). Wywołamy tę funkcję wewnątrz setContent w funkcji onCreate():

@Composable
fun AppScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigationBar(navController =
navController, appItems =
Destination.toList) },
content = { padding ->
Box(modifier = Modifier.padding(padding))
{
AppNavigation(navController =
navController)
}
}
)
}

12. Następnie wywołaj utworzoną funkcję w setContent{}; import powinien brzmieć import androidx.activity.compose.setContent, biorąc pod uwagę fakt, że czasami może się zdarzyć, że zaimportujesz niewłaściwy. Uruchom aplikację. Zobaczysz ekran z czterema zakładkami, a kiedy wybierzesz kartę, wybrana zostanie podświetlona, jak pokazano na rysunku:



Jak to działa…

W aplikacji Compose nawigacja ma kluczowe znaczenie zwane trasą. Kluczem jest ciąg znaków, który definiuje ścieżkę do Twojego pliku komponowalnego. Kluczem jest w zasadzie źródło prawdy - lub pomyśl o nim jak o głębokim łączu, które prowadzi do konkretnego miejsca docelowego, a każdy cel powinien mieć unikalną trasę. Ponadto każdy cel podróży powinien składać się z unikalnej trasy kluczowej. W naszym przykładzie dodaliśmy ikony i tytuł. Ikony, jak widać na rysunku , pokazują, co oznacza dolna nawigacja, a tytuł opisuje konkretny ekran, który przeglądamy dokładnie w tym momencie. Ponadto są one opcjonalne i potrzebne tylko w przypadku niektórych tras. NavController() to główny interfejs API naszego komponentu nawigacyjnego, który śledzi każdy wpis na stosie dla elementów składowych tworzących ekrany naszej aplikacji oraz stan każdego ekranu. Stworzyliśmy to za pomocą RememberNavController: jak wspomnieliśmy w poprzednim rozdziale, Remember, jak sama nazwa wskazuje, zapamiętuje wartość; w tym przypadku pamiętamy NavController:

val navController = rememberNavController()

Z drugiej strony NavHost() wymaga wcześniej utworzonej metody NavController() poprzez RememberNavController() oraz trasy docelowej punktu wejścia naszego wykresu. Ponadto RememberNavController() zwraca NavHostController, który jest podklasą NavController() oferującą dodatkowe interfejsy API wymagane przez NavHost. Przypomina to sposób, w jaki programiści Androida budują nawigację przed utworzeniem fragmentów. Kroki obejmują utworzenie dolnego menu nawigacyjnego z elementami menu, jak pokazano w następującym bloku kodu:

< ?xml version="1.0" encoding="utf-8"? >
< menu xmlns:android=http://schemas.android.com/apk/res/android >
< item
android:id="@+id/transaction_home"
android:icon="@drawable/card"
android:title="@string/transactions"/ >
< item
android:id="@+id/budget_home"
android:icon="@drawable/ic_shopping_basket_black_
24dp"
android:title="@string/budgets"
/ >

< /menu>

Następnie tworzymy w pakiecie nawigacyjnym kolejny zasób wskazujący ekrany (fragmenty):

< ?xml version="1.0" encoding="utf-8"? >
< menu xmlns:android=http://schemas.android.com/apk/res/android >
< item
android:id="@+id/transaction_home"
android:icon="@drawable/card"
android:title="@string/transactions"/ >
< item
android:id="@+id/budget_home"
android:icon="@drawable/ic_shopping_basket_black_
24dp"
android:title="@string/budgets"
/ >

< /menu>

Następnie tworzymy w pakiecie nawigacyjnym kolejny zasób wskazujący ekrany (fragmenty):

< ?xml version="1.0" encoding="utf-8"? >
< navigation xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@+id/transaction_home" >
< fragment
android:id="@+id/transaction_home"
android:name="com.fragments.TransactionsFragment"
android:label="@string/title_transcation"
tools:layout="@layout/fragment_transactions" >
< action
android:id="@+id/action_transaction_home_to_
budget_home"
app:destination="@id/budget_home" / >
< /fragmen t>
< fragment
android:id="@+id/budget_home"
android:name="com.fragments.BudgetsFragment"
android:label="@string/title_budget"
tools:layout="@layout/fragment_budget" >
< action
android:id="@+id/action_budget_home_to_tasks_
home"
app:destination="@id/tasks_home" / >
< /fragment >
< /navigation >

Przechodzenie do nowego ekranu w Compose

Na naszej stronie logowania zbudujemy monit o rejestrację, umożliwiający rejestrację nowych użytkowników naszej aplikacji. Jest to standardowy wzorzec, ponieważ musimy zapisać dane uwierzytelniające użytkownika, aby następnym razem logując się do naszej aplikacji, po prostu go zalogował, bez konieczności ponownej rejestracji.

Przygotowywanie się

Powinieneś ukończyć poprzedni przepis, Implementowanie dolnego paska nawigacyjnego przy użyciu miejsc docelowych nawigacji, zanim zaczniesz korzystać z tego.

Jak to zrobić…

W tym przepisie będziemy musieli skorzystać z naszego projektu SampleLogin i dodać nowy ekran, do którego użytkownicy będą mogli nawigować, jeśli będą korzystać z aplikacji po raz pierwszy. Jest to typowy przypadek użycia w wielu zastosowaniach:

1. Otwórz projekt SampleLogin, utwórz nową zapieczętowaną klasę i nadaj jej nazwę Destination. Aby mieć pewność, że utrzymamy doskonałe opakowanie, dodaj tę klasę do util. Podobnie jak dolny pasek, będziemy mieli trasę, ale tym razem nie potrzebujemy żadnych ikon ani tytułów:

sealed class Destination (val route: String){
object Main: Destination("main_route")
object LoginScreen: Destination("login_screen")
object RegisterScreen:
Destination("register_screen")
}

2. Po utworzeniu miejsc docelowych musimy teraz dodać klikalny tekst w LoginContent, aby zapytać użytkowników, czy korzystają z aplikacji po raz pierwszy. Powinni kliknąć opcję Zarejestruj się. Następnie możemy przejść do RegisterContent. Możesz otworzyć projekt, sprawdzając sekcję Wymagania techniczne, jeśli chcesz zapoznać się z jakimkolwiek krokiem:

@Composable
fun LoginContent(
..…
onRegister: () -> Unit
) {
ClickableText(
modifier = Modifier.padding(top = 12.dp),
text = AnnotatedString(stringResource(id =
R.string.register)),
onClick = { onRegister.invoke() },
style = TextStyle(
colorResource(id = R.color.purple_700),
fontSize = 16.sp
)
)

3. Teraz, gdy klikniesz ClickableText, nasz klikalny tekst będzie tekstem, który możesz kliknąć i który pomoże użytkownikom przejść do ekranu rejestracji poprzez First-time user? Zapisać się. Po kliknięciu powinno nastąpić przejście do innego ekranu, na którym użytkownicy mogą się teraz zarejestrować, jak pokazano na rysunku



4. W przypadku ekranu Rejestracja cały kod można uzyskać w sekcji Wymagania techniczne. Wykorzystamy ponownie utworzone przez nas pola wprowadzania danych przez użytkownika i po prostu zmienimy tekst:

@Composable
fun PasswordInputField(
text: String
) {
OutlinedTextField(
label = { Text(text = text) },

}

5. W MainActivity będziemy mieli funkcję nawigacji() w następujący sposób:

@Composable
fun Navigation(navController: NavHostController) {
NavHost(navController, startDestination =
Destination.LoginScreen.route) {
composable(Destination.LoginScreen.route) {
LoginContentScreen(loginViewModel =
hiltViewModel(),
onRegisterNavigateTo = {
navController.navigate(
Destination.RegisterScreen.route)
})
}
composable(Destination.RegisterScreen.route) {
RegisterContentScreen(registerViewModel =
hiltViewModel())
}
}
}

6. W PasswordInputField nazwiemy każde wejście odpowiednio pod kątem ponownego użycia:

PasswordInputField(
text = stringResource(id = R.string.password),
authState = uiState,
onValueChanged = onPasswordUpdated,
passwordToggleVisibility =
passwordToggleVisibility)

7. Ponadto możesz także przejść do poprzedniego ekranu logowania, klikając sprzętowy przycisk Wstecz.

8. Na koniec w setContent będziemy musieli zaktualizować kod, aby uwzględnić nową nawigację:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
SampleLoginTheme {
// A surface container using the
'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color =
MaterialTheme.colors.background
) {
val navController =
rememberNavController()
Navigation(navController =
navController)
}
}
}
}
}

Uruchom kod i kliknij tekst Zarejestruj się, a powinieneś zostać przeniesiony do nowego ekranu.

Jak to działa…

Zauważysz, że właśnie utworzyliśmy inny docelowy punkt wejścia, w którym ClickableText służy do nawigacji do nowo utworzonego ekranu. Ponadto, aby nawigować do miejsca docelowego, które można komponować na wykresie nawigacyjnym, musisz użyć navController.navigate(Destination. RegisterScreen.route) i jak wspomniano wcześniej, ciąg reprezentuje trasę docelową. Ponadto funkcja nawigacji() domyślnie dodaje nasz cel do stosu, ale jeśli będziemy musieli zmodyfikować zachowanie, możemy to łatwo zrobić, dodając dodatkowe opcje nawigacji do naszego wywołania nawigacji(). Załóżmy, że podczas nawigacji chcesz pracować z animacjami. W takim przypadku możesz to łatwo zrobić, korzystając z biblioteki Accompanist - https://github.com/google/accompanist - która oferuje grupę bibliotek mających na celu uzupełnienie Jetpack Compose o funkcje potrzebne głównie przez programistów i nie są jeszcze dostępne. Możesz użyć enterTransition, które jawnie określa animację uruchamianą podczas nawigacji do określonego miejsca docelowego, podczas gdy exitTransition działa odwrotnie:

AnimatedNavHost(
modifier = Modifier
.padding(padding),
navController = navController,
startDestination = Destination.LoginScreen.route,
route = Destination.LoginScreen.route,
enterTransition = { fadeIn(animationSpec = tween(2000)) },
exitTransition = { fadeOut(animationSpec = tween(200))
}
)

Można także użyć funkcji popEnterTransition, która określa animację uruchamianą, gdy miejsce docelowe ponownie pojawi się na ekranie po przejściu przez funkcję popBackStack() lub popExitTranstion, która działa odwrotnie.

Ważna uwaga

Należy zwrócić uwagę, że dobrą praktyką, która ma znaczenie przy podnoszeniu stanu, jest udostępnianie zdarzeń z funkcji komponowalnych obiektom wywołującym w aplikacji, które wiedzą, jak prawidłowo obsługiwać tę logikę. Ponadto cała nawigacja pod maską jest zarządzana przez stan.

Nawigacja za pomocą argumentów

Przekazywanie danych między miejscami docelowymi jest bardzo istotne w rozwoju Androida. Nowa nawigacja Jetpack umożliwia programistom dołączanie danych do operacji nawigacji poprzez zdefiniowanie argumentu miejsca docelowego. Czytelnicy dowiedzą się, jak przekazywać dane między miejscami docelowymi za pomocą argumentów. Dobrym przypadkiem użycia jest, powiedzmy, załadowanie interfejsu API danymi i chcesz wyświetlić więcej opisu właśnie wyświetlonych danych; możesz przejść za pomocą unikalnych argumentów do następnego ekranu.

Przygotowywanie się

Przyjrzymy się najczęstszemu wymaganiu projektu rozmowy kwalifikacyjnej, które polega na pobraniu danych z API i wyświetleniu jednego ekranu oraz dodaniu dodatkowego ekranu w celu uzyskania dodatkowych punktów. Załóżmy, że interfejs API to interfejs API GitHub i chcesz wyświetlić wszystkie organizacje. Następnie chcesz przejść do innego ekranu i zobaczyć liczbę repozytoriów każdej firmy.

Jak to zrobić…

W przypadku tego przepisu przyjrzymy się przykładowi nawigacji z argumentami jako koncepcją, ponieważ nie pozostaje nic więcej do zrobienia poza utworzeniem podstawowych argumentów do przekazania, aby wykorzystać już zbudowany projekt - SampleLogin:

1. Przejdźmy dalej i utwórzmy SearchScreen, a na tym ekranie będzie tylko funkcja wyszukiwania, EditText i kolumna wyświetlająca dane zwrócone z API:

SearchScreen(
viewModel = hiltViewModel(),
navigateToRepositoryScreen = { orgName ->
navController.navigate(
Destination.BrowseRepositoryScreen.route +
"/" + orgName
)
}
)

2. A teraz, konfigurując nawigację do BrowseRepository, będziesz musiał dodać następujący kod. Ten fragment kodu służy do przekazywania obowiązkowego parametru danych z jednego ekranu na drugi, ale dodaje przykład przekazywania opcjonalnego argumentu; wartość domyślna pomoże użytkownikowi:

composable(
route = Destination.BrowseRepositoryScreen.route +
"/{org_name}",
arguments = listOf(navArgument("org_name") { type
= NavType.StringType }),
enterTransition = { scaleIn(tween(700)) },
exitTransition = { scaleOut(tween(700)) },
) {
BrowseRepositoryScreen(
viewModel = hiltViewModel(),
)
}

Przejścia wejścia i wyjścia używamy także w animacjach. W tym przepisie właśnie poruszyliśmy koncepcję nawigacji po argumentach, którą można zastosować w wielu projektach.

Jak to działa…

Jeśli chcesz przekazać argument do miejsca docelowego, co może być wymagane, musisz jawnie dołączyć go do trasy podczas inicjowania wywołania funkcji nawigacji, jak widać w następującym fragmencie kodu:

navController.navigate(Destination.BrowseScreen.route + "/" + nazwa organizacji)

Do naszej trasy dodaliśmy symbol zastępczy argumentu, podobnie jak dodaliśmy argumenty do głębokiego łącza podczas korzystania z podstawowej biblioteki nawigacyjnej. Istnieje również lista tego, co obsługuje biblioteka nawigacyjna; jeśli masz inny przypadek użycia, możesz zajrzeć do tego dokumentu:

https://developer.android.com/guide/navigation/navigation-pass-data#supported_argument_types.

Więcej′

Można dowiedzieć się więcej o nawigacji, a aby dokładniej przyjrzeć się sposobom nawigacji za pomocą argumentów, odzyskiwać złożone dane podczas nawigacji i szczegółowo dodawać dodatkowe argumenty, możesz przeczytać więcej tutaj: https://developer.android .com/jetpack/compose/navigation.

Tworzenie głębokich linków do miejsc docelowych

W nowoczesnym rozwoju Androida głębokie linki są bardzo istotne. Link, który pomaga Ci przejść bezpośrednio do określonego miejsca docelowego w aplikacji, nazywa się głębokim linkiem. Komponent Nawigacja umożliwia tworzenie dwóch typów precyzyjnych linków: jawnych i ukrytych. Nawigacja tworzenia treści obsługuje ukryte głębokie linki, które mogą być częścią funkcji tworzenia. Warto również wspomnieć, że nie ma dużej różnicy pomiędzy sposobem obsługi tych plików przy użyciu układów XML.

Przygotowywanie się

Ponieważ w naszej aplikacji nie mamy przypadku użycia głębokich linków, w tym przepisie przyjrzymy się, jak możemy wykorzystać tę wiedzę, ucząc się, jak wdrożyć ukryte głębokie linki.

Jak to zrobić…

Głębokie linki można dopasowywać za pomocą Uniform Resource Locator (URI), działań związanych z intencjami lub typów wielozadaniowych rozszerzeń poczty internetowej (MIME). Co więcej, możesz łatwo określić wiele typów pasujących do pojedynczego głębokiego linku, ale pamiętaj, że zawsze priorytet ma porównanie argumentów URI, po nim następuje akcja zamierzenia, a następnie typ MIME. Funkcja Compose ułatwiła programistom pracę z precyzyjnymi linkami. Funkcja komponowalna akceptuje listę parametrów NavDeepLinks, którą można łatwo utworzyć za pomocą metody navDeepLink:

1. Zaczniemy od udostępnienia precyzyjnego linku na zewnątrz, dodając odpowiedni filtr intencji do naszego pliku AndroidManifest.xml:




android:host="www.yourcompanieslink.com" />



2. Teraz w naszej funkcji composable możemy użyć parametru deepLinks, określić listę navDeepLink, a następnie przekazać wzorzec URI:

val uri = "www.yourcompanieslink.com"
composable(deepLinks = listOf(navDeepLink { uriPattern = "$uri/
{id}" }))
{…}

3. Należy pamiętać, że nawigacja automatycznie utworzy głęboki link do pliku, gdy inna aplikacja uruchomi głęboki link. Wiele aplikacji nadal korzysta z trybu uruchamiania podczas nawigacji. Dzieje się tak w przypadku korzystania z komponentu nawigacji Jetpack, co widać w następującym fragmencie kodu:

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
navigationController.handleDeepLink(intent)
}

4. Na koniec możesz także użyć deepLinkPendingIntent, jak każdego innego PendingIntent, aby uruchomić aplikację na Androida w miejscu docelowym głębokiego linku. Ważna uwaga Podczas wyzwalania ukrytego głębokiego łącza stan stosu zależy od tego, kiedy ukryty zamiar został uruchomiony za pomocą Intent.FLAG_ACTIVITY_NEW_TASK. Ponadto, jeśli flaga jest ustawiona, zadanie stosu wstecznego jest czyszczone, a następnie zastępowane zamierzonym miejscem docelowym głębokiego łącza.

Jak to działa…

W programowaniu na Androida głęboki link odnosi się do konkretnego miejsca docelowego aplikacji. Na przykład, gdy wywołujesz precyzyjny link, otwiera on odpowiednie miejsce docelowe Twojej aplikacji, gdy użytkownik kliknie określone łącze. Odnosi się to do miejsca, do którego ma prowadzić link po kliknięciu. Jawny głęboki link to pojedyncza instancja, która używa PendingIntent do przenoszenia użytkowników do określonej lokalizacji w aplikacji. Dobrym przypadkiem użycia jest użycie powiadomień lub widżetów aplikacji.

Więcej…

Można dowiedzieć się więcej o głębokich linkach; na przykład, jak utworzyć wyraźny, głęboki link.

Pisanie testów do nawigacji

Teraz, gdy stworzyliśmy nowy ekran dla naszego projektu SampleLogin, musimy naprawić uszkodzony test i dodać nowe testy dla pakietu UI. Jeśli pamiętasz, w rozdziale 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt, przeprowadziliśmy testy jednostkowe, a nie testy interfejsu użytkownika. Oznacza to, że po dodaniu wszystkich instancji ViewModel nasze testy interfejsu użytkownika nie działają. W tym przepisie naprawimy nieudane testy i dodamy test nawigacji.

Przygotowywanie się

W tym przepisie nie musisz tworzyć żadnego nowego projektu; użyj już utworzonego projektu SampleLogin.

Jak to zrobić…

Możesz zastosować te koncepcje, aby przetestować utworzony przez nas dolny pasek nawigacyjny. Dlatego nie będziemy pisać testów dla projektu BottomNavigationBarSample. Otwórz SampleLogin i przejdź do pakietu androidTest. Dodamy tutaj testy dla nowej funkcji RegisterScreen() i Composable, a także naprawimy uszkodzone testy:

1. Otwórzmy klasę LoginContentTest. Teraz przenieśmy klasę LoginContent do klasy pomocniczej, którą utworzymy, aby pomóc nam w testowaniu logiki interfejsu użytkownika:

@Composable
fun contentLoginForTest(
uiState: AuthenticationState =
AuthenticationState(),
onUsernameUpdated : (String) -> Unit = {},
onPasswordUpdated :(String) -> Unit = {},
onLogin : () -> Unit = {},
passwordToggleVisibility: (Boolean) -> Unit = {},
onRegisterNavigateTo: () -> Unit = {}
) {
LoginContent(
uiState = uiState,
onUsernameUpdated = onUsernameUpdated,
onPasswordUpdated = onPasswordUpdated,
onLogin = onLogin,
passwordToggleVisibility =
passwordToggleVisibility,
onRegister = onRegisterNavigateTo
)
}

2. Wewnątrz klasy LoginContentTest zastąpimy teraz LoginContent nowo utworzoną funkcją contentLoginForTest() wewnątrz funkcji initCompose:

private fun initCompose() {
composeRuleTest.activity.setContent {
SampleLoginTheme {
contentLoginForTest()
launchRegisterScreenWithNavGraph()
}
}
}

3. Po naprawieniu testów możemy teraz dodać tag testowy do naszego nowo utworzonego klikalnego widoku TextView:

const val REGISTER_USER = "register_user"

4. Gdy już to zrobimy, musimy teraz utworzyć lateint var NavHostController i funkcję launchRegisterScreenWithNavGraph, która pomoże nam skonfigurować nawigację:

private fun launchRegisterScreenWithNavGraph() {
composeRuleTest.activity.setContent {
SampleLoginTheme {
navController = rememberNavController()
NavHost(
navController = navController,
startDestination =
Destination.LoginScreen.route
) {
composable(Destination.LoginScreen
.route) {
LoginContentScreen(
onRegisterNavigateTo = {
navController.navigate(
Destination.RegisterScreen
.route)
}, loginViewModel = hiltViewModel())
}
composable(
Destination.RegisterScreen
.route) {
RegisterContentScreen(
hiltViewModel())
}
}
}
}
}

Możesz wywołać utworzoną funkcję wewnątrz funkcji initCompose lub w nowej funkcji testowej, którą utworzymy.

5. Stwórzmy teraz funkcję testową i nadajmy jej nazwę twierdzenie RegisterClickableButtonNavigatesToRegisterScreen(). W tym przypadku testowym ustawimy naszą trasę, a następnie użyjemy potwierdzenia po kliknięciu prawidłowego TextView; pojedziemy do właściwego celu:

@Test
fun assertRegisterClickableButtonNavigatesToRegisterScreen() {
initCompose()
composeRuleTest.onNodeWithTag(
TestTags.LoginContent.REGISTER_USER)
.performClick(
)
val route =
navController.currentDestination?.route
assert(route.equals(
Destination.RegisterScreen.route))
}

6. Na koniec uruchom test. Test interfejsu użytkownika powinien zakończyć się pomyślnie, jak pokazano na rysunku :



Jak to działa…

Stworzyliśmy contentLoginForTest, który może pomóc nam zweryfikować naszą nawigację. Oznacza to, że gdy użytkownik wprowadzi prawidłową nazwę użytkownika i hasło, może przejść do ekranu głównego. Co więcej, utworzyliśmy launchRegisterScreenWithNavGraph(), funkcję pomocniczą, która tworzy wykres testowy dla naszego przypadku testowego nawigacji.

Używanie DataStore do przechowywania danych i testowania

Nowoczesne praktyki programistyczne dla Androida pomagają programistom Androida tworzyć lepsze aplikacje. DataStore to rozwiązanie do przechowywania danych dostarczane przez bibliotekę Android Jetpack. Umożliwia programistom przechowywanie par klucz-wartość lub złożonych obiektów asynchronicznie i z gwarancją spójności. Dane mają kluczowe znaczenie w rozwoju Androida, a sposób, w jaki je zapisujemy i przechowujemy, ma znaczenie. W tej części omówimy wykorzystanie DataStore do utrwalania naszych danych i przyjrzymy się najlepszym praktykom korzystania z DataStore.

Wdrażanie DataStore

Podczas tworzenia aplikacji mobilnych niezwykle ważne jest, aby zachować swoje dane, aby umożliwić płynne ładowanie, zmniejszyć problemy z siecią, a nawet obsługiwać dane całkowicie w trybie offline. W tym przepisie przyjrzymy się, jak przechowywać dane w naszych aplikacjach na Androida przy użyciu biblioteki Jetpack Modern Android Development o nazwie DataStore. DataStore to rozwiązanie do przechowywania danych dla aplikacji na Androida, które umożliwia przechowywanie par klucz-wartość lub dowolnych wpisanych obiektów z buforami protokołów. Co więcej, DataStore wykorzystuje współprogramy i przepływy Kotlina do przechowywania danych w sposób spójny, transakcyjny i asynchroniczny. Jeśli już wcześniej tworzyłeś aplikacje na Androida, być może korzystałeś z SharedPreferences. Nowy Preferences DataStore ma na celu zastąpienie tej starej metody. Można również powiedzieć, że Preferences DataStore wykorzystuje moc SharedPreferences, ponieważ są one dość podobne. Ponadto dokumentacja Google zaleca, aby jeśli obecnie używasz w swoim projekcie SharedPreferences do przechowywania danych, rozważenie migracji do najnowszej wersji DataStore. Innym sposobem przechowywania danych w systemie Android jest użycie Room. Zostanie to omówione w Rozdziale 6, Korzystanie z bazy danych pomieszczeń i testowanie; na razie przyjrzymy się tylko DataStore. Co więcej, należy pamiętać, że DataStore jest idealny dla prostych lub małych zbiorów danych i nie obsługuje częściowych aktualizacji ani integralności referencyjnej.

Jak to zrobić…

Przejdźmy dalej i utwórzmy nowy, pusty projekt Compose i nazwijmy go DataStoreSample. W naszym przykładowym projekcie utworzymy aplikację do wprowadzania zadań, w której użytkownicy będą mogli zapisywać zadania. Umożliwimy użytkownikom wprowadzenie tylko trzech zadań, następnie skorzystamy z DataStore do przechowywania zadań, a później będziemy logować dane i sprawdzać, czy zostały wstawione poprawnie. Dodatkowym ćwiczeniem, które warto wypróbować, jest wyświetlanie danych wtedy, gdy użytkownicy chcą je zobaczyć:

1. W naszym nowo powstałym projekcie przejdźmy dalej i usuńmy kod, którego nie potrzebujemy. W tym przypadku mamy na myśli powitanie (nazwa: String), które jest dostarczane ze wszystkimi pustymi projektami Compose. Zachowaj funkcję Podgląd, ponieważ będziemy jej używać do przeglądania utworzonego przez nas ekranu.

2. Przejdźmy teraz do dodania wymaganych zależności dla DataStore i zsynchronizowania projektu. Należy również pamiętać, że istnieją wersje biblioteki DataStore specyficzne dla RxJava 2 i 3:

dependencies {
implementation "androidx.DataStore:DataStore-preferences:1.x.x"
}

3. Utwórz nowy pakiet i nazwij go danymi. Wewnątrz danych utwórz nową klasę danych Kotlin i nazwij ją Zadaniami.

4. Konstruujmy teraz naszą klasę danych z oczekiwanymi polami wejściowymi:

data class Tasks(
val firstTask: String,
val secondTask: String,
val thirdTask: String
)

5. W tym samym pakiecie dodajmy wyliczenie TaskDataSource, ponieważ ponownie wykorzystamy ten projekt do zaprezentowania zapisywania danych przy użyciu Proto DataStore w przepisie Korzystanie z Android Proto DataStore kontra DataStore:

enum class TaskDataSource {
PREFERENCES_DATA_STORE
}

6. Wewnątrz naszego pakietu przejdźmy dalej i dodajmy interfejs DataStoreManager. Wewnątrz naszej klasy będziemy mieli funkcję saveTasks() do zapisywania danych oraz funkcję getTasks() pomagającą nam odzyskać zapisane dane. Funkcja zawieszenia w Kotlinie to po prostu funkcja, którą można wstrzymać i wznowić później. Ponadto funkcje zawieszenia mogą wykonywać długotrwałe operacje i czekać na zakończenie bez blokowania:

interface DataStoreManager {
suspend fun saveTasks(tasks: Tasks)
fun getTasks(): Flow
}

7. Następnie musimy zaimplementować nasz interfejs, zatem utwórzmy klasę DataStoreManagerImpl i zaimplementujmy DataStoreManager:

class DataStoreManagerImpl(): DataStoreManager {
override suspend fun saveTasks(tasks: Tasks) {
TODO("Not yet implemented")
}
override fun getTasks(): Flow {
TODO("Not yet implemented")
}
}

8. Zauważysz, że po zaimplementowaniu interfejsu przedstawiliśmy widok funkcji, ale jest tam napisane TODO i nic nie zostało zaimplementowane. Aby kontynuować ten krok, dodajmy DataStore i przekażmy Preferencje w naszym konstruktorze. Będziemy także musieli utworzyć klucz preferencji ciągu dla każdego zadania:

class DataStoreManagerImpl(
private val tasksPreferenceStore:
DataStore
) : DataStoreManager {
private val FIRST_TASK =
stringPreferencesKey("first_task")
private val SECOND_TASK =
stringPreferencesKey("second_task")
private val THIRD_TASK =
stringPreferencesKey("third_task")
override suspend fun saveTasks(tasks: Tasks) {
tasksPreferenceStore.edit {
taskPreferenceStore ->
taskPreferenceStore[FIRST_TASK] =
tasks.firstTask
taskPreferenceStore[SECOND_TASK] =
tasks.secondTask
taskPreferenceStore[THIRD_TASK] =
tasks.thirdTask
}
}
override fun getTasks(): Flow {
TODO("Not yet implemented")
}
}

9. Na koniec zakończmy implementację sekcji DataStore dodając funkcjonalność do funkcji getTasks:

override fun getTasks(): Flow = tasksPreferenceStore.
data.map { taskPreference ->
Tasks(
firstTask = taskPreference[FIRST_TASK] ?: "",
secondTask = taskPreference[SECOND_TASK] ?:
"",
thirdTask = taskPreference[THIRD_TASK] ?: ""
)
}

10. W naszej klasie MainActivity przejdźmy dalej i utwórzmy prosty interfejs użytkownika: trzy pola tekstowe i przycisk Zapisz. Przycisk Zapisz zapisze nasze dane i będziemy mogli spróbować zalogować dane, gdy wszystko będzie działać zgodnie z oczekiwaniami.



Teraz, gdy mamy już gotową implementację, w poniższym przepisie Dodawanie wstrzykiwania zależności do DataStore dodamy wstrzykiwanie zależności, a następnie sklejmy wszystko razem.

Jak to działa…

Głównym celem nowej biblioteki Jetpack Modern Android Development o nazwie Preferences DataStore jest zastąpienie SharedPreferences. Aby zaimplementować Preferences DataStore, jak widziałeś w przepisie, używamy interfejsu DataStore, który przyjmuje klasę abstrakcyjną Preference i możemy jej używać do edycji i mapowania danych wejściowych. Ponadto tworzymy klucze dla kluczowych części par klucz-wartość:

wartość prywatna FIRST_TASK = stringPreferencesKey("pierwsze_zadanie")
wartość prywatna SECOND_TASK = stringPreferencesKey("drugie_zadanie")
wartość prywatna THIRD_TASK = stringPreferencesKey("trzecie_zadanie")

Aby zapisać nasze dane w DataStore, używamy edit(), która jest funkcją zawieszającą, którą należy wywołać z CoroutineContext. Kluczową różnicą w korzystaniu z Preferences DataStore w porównaniu z SharedPreferences jest to, że DataStore można bezpiecznie wywoływać w wątku interfejsu użytkownika, ponieważ korzysta z modułu rozsyłającego. IO pod maską. Nie musisz także używać funkcji Apply{} ani zatwierdzania, aby zapisać zmiany, jak jest to wymagane w SharedPreferences. Ponadto obsługuje transakcyjną aktualizację danych. Więcej funkcji przedstawiono na rysunku



Jest jeszcze wiele do nauczenia się i trzeba przyznać, że to, co omówiliśmy w tym przepisie, to tylko niewielka część tego, co możesz zrobić z DataStore. Więcej funkcji omówimy w poniższych przepisach

Dodawanie wstrzykiwania zależności do DataStore

Wstrzykiwanie zależności to ważny wzorzec projektowy w inżynierii oprogramowania, a jego zastosowanie w tworzeniu aplikacji na Androida może prowadzić do czystszego i łatwiejszego w utrzymaniu kodu. Jeśli chodzi o DataStore w systemie Android, czyli nowoczesne rozwiązanie do przechowywania danych wprowadzone w Android Jetpack, dodanie Zależności Injection może przynieść kilka korzyści:

•  Używając wstrzykiwania zależności, możesz oddzielić problemy związane z tworzeniem instancji DataStore od kodu, który ją używa. Oznacza to, że Twój kod logiki biznesowej nie będzie musiał się martwić o to, jak utworzyć instancję DataStore i zamiast tego będzie mógł skupić się na tym, co musi zrobić z danymi.
•  Wstrzykiwanie zależności ułatwia pisanie testów jednostkowych dla aplikacji. Wstrzykując do testów próbną instancję DataStore, możesz mieć pewność, że rzeczywisty stan DataStore nie będzie miał na nie wpływu.
•  Wstrzykiwanie zależności może pomóc w podzieleniu kodu na mniejsze, łatwiejsze w zarządzaniu moduły. Ułatwia to dodawanie nowych funkcji lub modyfikowanie istniejących bez wpływu na całą bazę kodu.
•  Korzystając z wstrzykiwania zależności, możesz łatwo przełączać się pomiędzy różnymi implementacjami DataStore. Może to być przydatne podczas testowania różnych typów przechowywania danych lub podczas migracji z jednego rozwiązania do drugiego.

Jak to zrobić…

Aby kontynuować następny, musisz ukończyć poprzedni przepis, wykonując następujące kroki:

1. Otwórz swój projekt i dodaj niezbędną zależność Hilt. Jeśli potrzebujesz pomocy w konfiguracji, zobacz Obsługa stanu interfejsu użytkownika w Jetpack Compose i Używanie Hilt .
2. Następnie dodajmy naszą klasę @HiltAndroidApp i w naszym folderze Manifest dodajmy .name = TaskApp: android:name=".TaskApp":

@HiltAndroidApp
class TaskApp : Application()
android:allowBackup="true"
android:name=".TaskApp"
tools:targetApi="31">


3. Skoro już zaimplementowaliśmy Depency Injection, przejdźmy dalej i dodajmy @AndroidEntryPoint do klasy MainActivity, a w DataStoreManagerImpl dodajemy konstruktor @Inject. Powinniśmy mieć coś podobnego do poniższego fragmentu kodu:

class DataStoreManagerImpl @Inject constructor(
private val tasksPreferenceStore:
DataStore
) : DataStoreManager {

4. Teraz musimy utworzyć nowy folder i nazwać go di; w tym miejscu umieścimy naszą klasę DataStoreModule. Tworzymy plik o nazwie store_tasks do przechowywania wartości preferencji:

@Module
@InstallIn(SingletonComponent::class)
class DataStoreModule {
private val Context.tasksPreferenceStore :
DataStore by
preferencesDataStore(name = "store_tasks")
@Singleton
@Provides
fun provideTasksPreferenceDataStore(
@ApplicationContext context: Context
): DataStore =
context.tasksPreferenceStore
}

5. Będziemy także musieli utworzyć klasę abstrakcyjną dla DataStoreManagerModule wewnątrz naszego pakietu di. Aby zredukować szablonowy kod za pomocą ręcznego wstrzykiwania zależności, nasza aplikacja dostarcza również wymagane zależności klasom, które ich potrzebują. Możesz dowiedzieć się więcej na ten temat w Rozdziale 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt:

@Module
@InstallIn(SingletonComponent::class)
abstract class DataStoreManagerModule {
@Singleton
@Binds
abstract fun
bindDataStoreRepository(DataStoreManagerImpl:
DataStoreManagerImpl): DataStoreManager
}

6. Przejdźmy teraz do stworzenia nowego pakietu i nazwijmy go usługą:

interface TaskService {
fun getTasksFromPrefDataStore(): Flow
suspend fun addTasks(tasks: Tasks)
}
class TaskServiceImpl @Inject constructor(
private val DataStoreManager: DataStoreManager
) : TaskService {
override fun getTasksFromPrefDataStore() =
DataStoreManager.getTasks()
override suspend fun addTasks(tasks: Tasks) {
DataStoreManager.saveTasks(tasks)
}
}

7. Upewnijmy się także, że mamy wymagane zależności dla nowo utworzonej usługi:

@Singleton
@Binds
abstract fun bindTaskService(taskServiceImpl:
TaskServiceImpl): TaskService
}

8. Teraz, gdy skończyliśmy z wstrzykiwaniem zależności i dodaliśmy wszystkie funkcjonalności wymagane dla DataStore, przejdziemy dalej i dodamy klasę ViewModel oraz zaimplementujemy funkcjonalność zapisywania danych po kliknięciu przez użytkownika przycisku Zapisz:
v fun saveTaskData(tasks: Tasks) {
viewModelScope.launch {
Log.d("Task", "asdf Data was inserted
correctly")
taskService.addTasks(tasks)
}
}

9. Wywołaj funkcję saveTaskData znajdującą się w przycisku Utwórz Zapisz w widoku Utwórz, aby zapisać nasze dane:

TaskButton(onClick = {
val tasks = Tasks(
firstTask = firstText.value,
secondTask = secondText.value,
thirdTask = thirdText.value
)

10. Na koniec będziemy musieli sprawdzić, czy wszystko działa, czyli nasz interfejs użytkownika i proces przechowywania danych. Możemy to zweryfikować, wpisując dane wejściowe w naszych polach tekstowych i klikając przycisk Zapisz, a kiedy zarejestrujemy wiadomość, potwierdzimy, że dane zostały rzeczywiście zapisane.



11. Jeśli początkowo tego nie zauważyłeś, kod tego widoku można znaleźć w sekcji Wymagania techniczne. Teraz zauważysz, że kiedy wprowadzamy dane, jak na rysunku, powinniśmy być w stanie zarejestrować dane w naszym Logcat i sprawdzić, czy nasze dane zostały wprowadzone poprawnie.



12. Jeśli wszystko działa zgodnie z oczekiwaniami, w zakładce Logcat powinien zostać wyświetlony także komunikat dziennika.



Jak to działa… W tym przepisie zdecydowaliśmy się na użycie wstrzykiwania zależności w celu dostarczenia wymaganych zależności konkretnym klasom. Omówiliśmy już szczegółowo, na czym polega wstrzykiwanie zależności, więc nie będziemy tego wyjaśniać ponownie, ale zamiast tego porozmawiamy o stworzonych przez nas modułach. W naszym projekcie stworzyliśmy DataStoreManagerModule i DataStoreModule, a jedyne co zrobiliśmy to dostarczyliśmy wymagane zależności. Stworzyliśmy plik i nazwaliśmy go store_tasks, który pomaga nam przechowywać wartości preferencji:

private val Context.tasksPreferenceStore : DataStore by preferencesDataStore(name = "store_tasks")

Domyślnie DataStore używa współprogramów i zwraca wartość przepływu. Kilka ważnych zasad, o których należy pamiętać podczas korzystania z DataStore, zgodnie z dokumentacją, są następujące:

•  DataStore wymaga tylko jednej instancji dla danego pliku w tym samym procesie. Dlatego nigdy nie powinniśmy tworzyć więcej niż jednej instancji DataStore.
•  Zawsze upewnij się, że ogólny typ DataStore jest niezmienny, aby ograniczyć niepotrzebne i trudne do wyśledzenia błędy.
•  Nigdy nie należy mieszać wykorzystania jednoprocesowego magazynu danych i wieloprocesowego magazynu danych w tym samym pliku.

Korzystanie z Androida Proto DataStore a DataStore

W tym przepisie przyjrzymy się, jak możemy wykorzystać Proto DataStore. Implementacja Proto DataStore wykorzystuje DataStore i bufory protokołu do utrwalania wpisanych obiektów na dysku. Proto DataStore jest podobny do Preferences DataStore, ale w przeciwieństwie do Preferences DataStore, Proto nie używa par klucz-wartość i po prostu zwraca wygenerowany obiekt w przepływie. Typy plików i struktura danych zależą od schematu plików .protoc.

Przygotowywanie się

Wykorzystamy nasz już stworzony projekt, aby pokazać, jak można wykorzystać Proto DataStore w systemie Android. Będziemy także korzystać z już utworzonych klas i po prostu nadawać funkcjom inne nazwy.

Jak to zrobić…

1. Będziemy musieli zacząć od skonfigurowania wymaganych zależności, więc przejdźmy dalej i dodajmy następujące elementy do naszego pliku na poziomie aplikacji Gradle:

implementation "androidx.DataStore:DataStore:1.x.x"
implementation "com.google.protobuf:protobuf-javalite:3.x.x"

2. Następnie będziemy musieli dodać protobuf do wtyczek w naszym pliku build.gradle:

plugins {

id "com.google.protobuf" version "0.8.12"
}

3. Będziemy musieli dodać konfigurację protobuf do naszego pliku build.gradle, aby sfinalizować naszą konfigurację:

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}

4. Teraz w naszym folderze pakietu będziemy musieli dodać nasz plik proto do app/src/main/, następnie utworzyć nowy katalog i nazwać go proto. Powinieneś teraz mieć to w swoim katalogu plików app/src/main/proto:

syntax = "proto3";
option java_package =
"com.madonasyombua.DataStoreexample";
option java_multiple_files = true;
message TaskPreference {
string first_task = 1;
string second_task = 2;
string third_task = 3;
}

To było dużo do skonfigurowania. Możemy teraz zacząć dodawać kod, aby wszystko połączyć.

5. Zmodyfikujmy klasy, które mogą potrzebować ProtoDataStore. Najpierw dodajmy PROTO_DATA_STORE do klasy wyliczeniowej TaskDataSource:

enum class TaskDataSource {
PREFERENCES_DATA_STORE,
PROTO_DATA_STORE
}

6. W DataStoreManager dodajmy saveTaskToProtoStore() i getUserFromProtoStore(), a nasz nowy interfejs będzie wyglądał następująco:

interface DataStoreManager {
suspend fun saveTasks(tasks: Tasks)
fun getTasks(): Flow
suspend fun saveTasksToProtoStore(tasks: Tasks)
fun getTasksFromProtoStore(): Flow
}

7. Ponieważ właśnie zmodyfikowaliśmy nasz interfejs, będziemy musieli pójść dalej i dodać nowe funkcjonalności do klasy implementacyjnej. Zauważysz również, że projekt będzie narzekał, gdy dodasz funkcje:

override suspend fun saveTasksToProtoStore(tasks: Tasks) {

TODO("Not yet implemented")
}
override fun getTasksFromProtoStore(): Flow {
TODO("Not yet implemented")
}

8. Zgodnie z zaleceniami będziemy musieli zdefiniować klasę implementującą Serializer, gdzie typ jest zdefiniowany w pliku Proto. Celem tej klasy serializatora jest poinformowanie DataStore, jak czytać i zapisywać nasz typ danych. Stwórzmy więc nowy obiekt i nazwijmy go TaskSerializer():

object TaskSerializer : Serializer {
override val defaultValue: TaskPreference =
TaskPreference.getDefaultInstance()
override suspend fun readFrom(input: InputStream):
TaskPreference{
try {
return TaskPreference.parseFrom(input)
} catch (exception:
InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: TaskPreference,
output: OutputStream) = t.writeTo(output)
}
zastąp zawieszenie zabawy writeTo(t: TaskPreference,
wyjście: OutputStream) = t.writeTo(wyjście)
}

9. Klasa TaskPreference jest generowana automatycznie i możesz uzyskać do niej bezpośredni dostęp, klikając ją, ale nie możesz jej edytować. Plików wygenerowanych automatycznie nie można edytować, chyba że zmienisz oryginalny plik.



10. Teraz, gdy stworzyliśmy klasę typów danych, musimy utworzyć zadanieProtoDataStore: DataStore z kontekstem używanym z DataStore. Zatem wewnątrz DataStoreModule, dodajmy ten kod:

private val Context.taskProtoDataStore:
DataStore by DataStore(
fileName = "task.pd",
serializer = TaskSerializer
)
@Singleton
@Provides
fun provideTasksProtoDataStore(
@ApplicationContext context: Context
):DataStore = context.taskProtoDataStore

11. Wróćmy teraz do DataStoreManagerImpl i popracujmy nad funkcjami, których jeszcze nie zaimplementowaliśmy: zastąp zawieszenie zabawy saveTasksToProtoStore(tasks:

override suspend fun saveTasksToProtoStore(tasks: Tasks) {
taskProtoDataStore.updateData { taskData ->
taskData.toBuilder()
.setFirstTask(tasks.firstTask)
.setSecondTask(tasks.secondTask)
.setThirdTask(tasks.thirdTask)
.build()
}
}
override fun getTasksFromProtoStore(): Flow =
taskProtoDataStore.data.map { tasks ->
Tasks(
tasks.firstTask,
tasks.secondTask,
tasks.thirdTask
)

12. W TaskService również dodamy getTasksFromProto i getTasks():

interface TaskService {
fun getTasksFromPrefDataStore() : Flow
suspend fun addTasks(tasks: Tasks)
fun getTasks(): Flow
fun getTasksFromProtoDataStore(): Flow
}

13. Kiedy implementujesz interfejs, początkowo implementowana klasa może wyświetlić błąd kompilacji, który poprosi Cię o zastąpienie funkcjonalności interfejsu w klasie. Dlatego w klasie TaskServiceImpl dodaj następujący kod:

class TaskServiceImpl @Inject constructor(
private val DataStoreManager: DataStoreManager
) : TaskService {
override fun getTasksFromPrefDataStore() =
DataStoreManager.getTasks()
override suspend fun addTasks(tasks: Tasks) {
DataStoreManager.saveTasks(tasks)
DataStoreManager.saveTasksToProtoStore(tasks)
}
override fun getTasks(): Flow =
getTasksFromProtoDataStore()
override fun getTasksFromProtoDataStore():
Flow =
DataStoreManager.getTasksFromProtoStore()
}

Wreszcie, teraz, gdy mamy zapisane wszystkie nasze dane, możemy zalogować się, aby upewnić się, że dane w interfejsie użytkownika są zgodne z oczekiwaniami; sprawdź łącze z kodem w sekcji Wymagania techniczne, aby zobaczyć, jak jest to zaimplementowane.

Ważna uwaga

Apple M1 ma zgłoszony problem z proto. Istnieje w tej kwestii otwarta kwestia; kliknij ten link, aby rozwiązać problem: https://github.com/grpc/grpc-java/issues/7690. Miejmy nadzieję, że zostanie to naprawione do czasu publikacji książki. Należy pamiętać, że jeśli używasz artefaktu rdzenia preferencji DataStore z Proguard, musisz ręcznie dodać reguły Proguard do pliku reguł, aby zapobiec usunięciu już zapisanych pól. Możesz także wykonać ten sam proces, aby zarejestrować i sprawdzić, czy dane zostały wstawione zgodnie z oczekiwaniami.

Jak to działa…

Być może zauważyłeś, że przechowujemy nasz niestandardowy typ danych jako instancję. To właśnie robi Proto DataStore; przechowuje dane jako instancje niestandardowych typów danych. Wdrożenia tego od nas wymagają zdefiniuj schemat przy użyciu buforów protokołów, ale zapewnia bezpieczeństwo typu.

W bibliotece Proto Datastore systemu Android interfejs Serializer konwertuje obiekty określonego typu (typ) na odpowiadający im format bufora protokołu i odwrotnie. Ten interfejs udostępnia metody serializacji obiektów do bajtów i deserializacji bajtów z powrotem do obiektów. Bufory protokołów w systemie Android to rozszerzalny mechanizm neutralny pod względem języka i platformy, służący do serializacji danych strukturalnych. Bufory protokołów kodują i dekodują dane w strumieniu binarnym, który jest naprawdę lekki. Wartość zastąpienia wartości domyślnej jest używana podczas definiowania właściwości w klasie danych lub klasie modelu serializowanego. Jest częścią biblioteki Serialization Kotlin, która jest powszechnie używana do serializacji i deserializacji obiektów do i z różnych formatów danych, takich jak JSON lub bufory protokołów. Udostępniamy odpowiednią właściwość, ujawniając dane przepływu DataStore z naszego przechowywanego obiektu i pisząc proto DataStore, który udostępnia nam funkcję updateData(), która transakcyjnie aktualizuje przechowywany obiekt. Funkcja updateData podaje nam bieżący stan danych jako instancję naszego typu danych i aktualizuje go w niepodzielnej operacji odczytu, zapisu i modyfikacji.

Obsługa migracji danych za pomocą DataStore

Jeśli już tworzyłeś aplikacje na Androida, być może korzystałeś z SharedPreferences; dobrą wiadomością jest teraz obsługa migracji i możliwość migracji z SharedPreferences do DataStore za pomocą SharedPreferenceMigration. Podobnie jak w przypadku innych danych, zawsze będziemy modyfikować nasz zbiór danych; na przykład możemy chcieć zmienić nazwy wartości naszego modelu danych lub nawet zmienić ich typ. W takim scenariuszu będziemy potrzebować migracji DataStore do DataStore; nad tym będziemy pracować w tym przepisie. Proces jest dość podobny do migracji z SharedPreferences; w rzeczywistości SharedPreferencesMigration jest implementacją klasy interfejsu DataMigration.

Przygotowywanie się

Ponieważ właśnie utworzyliśmy nowy magazyn PreferenceDataStore, nie będziemy musieli go migrować, ale możemy zastanowić się nad sposobami wdrożenia migracji, jeśli zajdzie taka potrzeba.

Jak to zrobić…

W tym przepisie przyjrzymy się, jak możesz wykorzystać zdobytą wiedzę, aby pomóc Ci, gdy pojawi się potrzeba migracji do DataStore:

1. Zacznijmy od interfejsu ułatwiającego migrację. Poniższa sekcja kodu przedstawia interfejs DataMigration, który implementuje SharedPreferencesMigration:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public interface DataMigration {
public suspend fun shouldMigrate(currentData: T): Boolean
public suspend fun migrate(currentData: T): T
public suspend fun cleanUp()
}

2. W danych Zadania możemy zmienić wpisy na Int; oznacza to zmianę jednego z naszych typów danych. Wyobrazimy sobie taki scenariusz i spróbujemy na jego podstawie stworzyć migrację. Możemy zacząć od utworzenia nowego magazynu migracjiOnePreferencesDataStore:

private val Context.migrateOnePreferencesDataStore :
DataStore by preferencesDataStore(
name = "store_tasks"
)

3. Przejdźmy teraz do implementacji DataMigration i nadpisania jej funkcji. Będziesz musiał określić warunek, czy migracja powinna nastąpić. Dane migracji zawierają instrukcje dotyczące dokładnego przekształcania starych danych w nowe. Następnie po zakończeniu migracji wyczyść starą pamięć:

private val Context.migrationTwoPreferencesDataStore by
preferencesDataStore(
name = NEW_DataStore,
produceMigrations = { context ->
listOf(object : DataMigration {
override suspend fun
shouldMigrate(currentData:
Preferences) = true
override suspend fun migrate(currentData:
Preferences): Preferences {
val oldData = context
.migrateOnePreferencesDataStore
.data.first().asMap()
val currentMutablePrefs =
currentData.toMutablePreferences()
oldToNew(oldData, currentMutablePrefs)
return
currentMutablePrefs.toPreferences()
}
override suspend fun cleanUp() {
context.migrateOnePreferencesDataStore
.edit { it.clear() }
}
})
}
)

4. Na koniec utwórzmy funkcję oldToNew(), w której możemy dodać dane, które chcemy przenieść:

private fun oldToNew(
oldData: Map, Any>,
currentMutablePrefs: MutablePreferences
) {
oldData.forEach { (key, value) ->
when (value) {
//migrate data types you wish to migrate

}
}
}

Jak to działa…

Aby lepiej zrozumieć, jak działa DataMigration, będziemy musieli przyjrzeć się funkcjom dostępnym w interfejsie DataMigration. W naszym interfejsie mamy trzy funkcje, jak pokazano w następującym bloku kodu:

public suspend fun shouldMigrate(currentData: T):
Boolean
public suspend fun migrate(currentData: T): T
public suspend fun cleanUp()

Funkcja powinnaMigrate(), jak sama nazwa wskazuje, określa, czy należy przeprowadzić migrację, czy nie. Jeśli na przykład nie zostanie wykonana żadna migracja, co oznacza, że zwróci wartość false, wówczas nie nastąpi żadna migracja ani czyszczenie. Należy również pamiętać, że funkcja ta jest inicjowana za każdym razem, gdy wywołujemy naszą instancję DataStore. Z drugiej strony Migrate() wykonuje migrację. Przez przypadek, jeśli akcja się nie powiedzie lub nie będzie działać zgodnie z oczekiwaniami, DataStore nie zapisze żadnych danych na dysku. Ponadto proces czyszczenia nie zostanie wykonany i zostanie zgłoszony wyjątek. Wreszcie, cleanUp(), jak sugeruje, po prostu usuwa wszelkie stare dane z poprzedniego przechowywania danych.

Pisanie testów dla naszej instancji DataStore

Pisanie testów jest kluczowe w rozwoju Androida, dlatego w tym przepisie napiszemy kilka testów dla naszej instancji DataStore. Aby przetestować naszą instancję DataStore lub dowolną instancję DataStore, musimy najpierw skonfigurować testowanie oprzyrządowania, ponieważ będziemy czytać i zapisywać w rzeczywistych plikach (DataStore), dlatego ważne jest sprawdzenie, czy wprowadzane są dokładne aktualizacje.

Jak to zrobić…

Zaczniemy od stworzenia prostego testu jednostkowego w celu przetestowania naszej funkcji modelu widoku:

1. W naszym folderze testów jednostkowych utwórz nowy folder i nazwij go test, a w nim utwórz nową klasę o nazwie TaskViewModelTest:

class TaskViewModelTest {}

2. Następnie będziemy musieli dodać pewne zależności testowe:

testImplementation "io.mockk:mockk:1.13.3"
androidTestImplementation "io.mockk:mockk-android:1.13.3"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutinestest: 1.5.2"

3. Teraz, gdy dodaliśmy wymagane zależności, przejdźmy dalej i utwórz naszą klasę usługi próbnej zadania, wyśmiej ją, a następnie zainicjuj w konfiguracji:

private lateinit var classToTest: TaskViewModel
private val mockTaskService = mockk()
private val dispatcher = TestCoroutineDispatcher()
@Before
fun setUp(){
classToTest = TaskViewModel(mockTaskService)
}

4. Ponieważ używamy współprogramu, skonfigurujemy naszego dyspozytora w adnotacji @Before i wyczyścimy wszelkie dane zapisane w adnotacji @After za pomocą Dispatchers.resetMain(). Jeśli uruchomisz testy bez skonfigurowania współprogramu, zakończy się to niepowodzeniem i wystąpieniem błędu. Nie udało się zainicjować modułu z głównym dyspozytorem. Do testów Dispatchers.setMain z pliku

@Before
fun setUp(){
classToTest = TaskViewModel(mockTaskService)
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}

5. Po zakończeniu przejdźmy dalej i utwórz nowy test o nazwie Sprawdź, czy funkcja dodawania zadań dodaje zadania w razie potrzeby. W tym teście utworzymy fałszywe zadanie, dodamy te zadania do saveTaskData i upewnimy się, że dane zostały wstawione zgodnie z oczekiwaniami, sprawdzając, czy nie zapisaliśmy wartości null:

@Test
fun `Verify add tasks function adds tasks as needed`() =
runBlocking {
val fakeTasks = Tasks(
firstTask = "finish school work",
secondTask = "buy gifts for the holiday",
thirdTask = "finish work"
)
val expected = classToTest.saveTaskData(fakeTasks)
Assert.assertNotNull(expected)
}

Na koniec, po uruchomieniu testu jednostkowego, powinien on przejść pomyślnie, a zobaczysz zielony znacznik wyboru.



Jak to działa…

W systemie Android używane są różne biblioteki próbne: Mockito, Mockk i inne. W tym przepisie wykorzystaliśmy Mockk, przyjazną dla użytkownika bibliotekę dla Androida. testImplementation "io. mockk:mockk:1.13.3" służy do testów jednostkowych, a androidTestImplementation "io. mockk:mockk-android:1.13.3" służy do testów interfejsu użytkownika. Aby przetestować interfejs użytkownika, będziemy musieli postępować zgodnie ze wzorcem, tworząc testową instancję DataStore z zapisanymi w niej wartościami domyślnymi. Następnie tworzymy obiekt testowy i sprawdzamy, czy wartości testowe DataStore pochodzące z naszej funkcji odpowiadają oczekiwanym wynikom. Będziemy musieli także użyć TestCoroutineDispatcher:

private val coroutineDispatcher: TestCoroutineDispatcher =
TestCoroutineDispatcher()

Powyższy kod wykonuje wykonanie współprogramów, które domyślnie jest natychmiastowe. Oznacza to po prostu, że wszelkie zadania zaplanowane do wykonania bez opóźnień są wykonywane natychmiast. Używamy również tych samych współprogramów w naszych modelach widoków. Dzieje się tak również dlatego, że DataStore opiera się na współprogramach Kotlina; dlatego musimy upewnić się, że nasze testy mają odpowiednią konfigurację.

Korzystanie z bazy danych Room i testowanie

Aplikacje na Androida mogą znacząco zyskać na lokalnym przechowywaniu danych. Biblioteka trwałości Room wykorzystuje moc SQLite. W szczególności Room oferuje doskonałe korzyści dla programistów Androida. Ponadto Room oferuje wsparcie offline, a dane są przechowywane lokalnie. W tym rozdziale dowiemy się, jak zaimplementować Room, bibliotekę Jetpack. Należy również wspomnieć, że w Roomie używanych jest jeszcze kilka bibliotek - na przykład integracja RxJava i Paging. W tym rozdziale nie skupimy się na nich, ale na tym, jak wykorzystać Room do tworzenia nowoczesnych aplikacji na Androida.

Wdrażanie Room w Twoich aplikacjach

Room to biblioteka mapowania obiektowo-relacyjnego używana w trwałości danych systemu Android i jest zalecaną trwałością danych w nowoczesnym rozwoju systemu Android. Ponadto jest łatwy w użyciu, zrozumieniu i utrzymaniu, a także wykorzystuje możliwości SQLiteDatabase, pomaga także zredukować standardowy kod, problem, którego doświadcza wielu programistów podczas korzystania z SQLite. Pisanie testów jest również bardzo proste i łatwy do zrozumienia. Najbardziej zauważalną zaletą Room jest to, że można go łatwo zintegrować z innymi komponentami architektury i umożliwia programistom kontrolę kompilacji w czasie wykonywania - to znaczy, że Room złoży skargę, jeśli popełnisz błąd lub zmienisz schemat bez migracji, co jest praktyczne i pomaga ograniczyć awarie.

Jak to zrobić …

Przejdźmy dalej i utwórzmy nowy, pusty projekt tworzenia kompozycji i nazwijmy go RoomExample. W naszym przykładowym projekcie utworzymy formularz od użytkowników; w tym miejscu użytkownicy mogą zapisać swoje imię i nazwisko, datę urodzenia, płeć, miasto, w którym mieszkają oraz zawód. Zapisujemy dane naszych użytkowników w naszej bazie Pokoi, a następnie sprawdzamy, czy wstawione przez nas elementy zostały zapisane w naszej bazie i wyświetlamy dane na ekranie:

1. W naszym nowo utworzonym projekcie usuńmy niepotrzebny potrzebny kod - czyli Powitanie (nazwa: String), które pojawia się we wszystkich pustych projektach Compose. Zachowaj funkcję podglądu, ponieważ będziemy jej używać do oglądania tworzonego przez nas ekranu.

2. Teraz przejdźmy dalej i dodajmy potrzebne zależności dla Room i zsynchronizujmy projekt. Poruszymy kwestię zarządzania zależnościami za pomocą buildSrc w Rozdziale 12, Porady i triki Android Studio, które pomogą Ci podczas programowania. Najnowszą wersję Room znajdziesz pod adresem https://developer.android.com/jetpack/androidx/releases/room; dodamy kapt, co oznacza Kotlin Annotation Processing Tool, aby umożliwić nam korzystanie z procesora adnotacji Java z kodem Kotlin:

dependencies {
implementation "androidx.Room:Room-runtime:2.x.x"
kapt "androidx.Room:Room-compiler:2.x.x"
}
//include kapt on your plugins
plugins {
id 'kotlin-kapt'
}

3. Utwórz nowy pakiet i nazwij go danymi. Wewnątrz danych utwórz nową klasę Kotlin i nazwij ją UserInformationModel(). Klasa danych służy wyłącznie do przechowywania danych - w naszym przypadku typem danych, które będziemy zbierać od użytkowników, będzie imię, nazwisko, data urodzenia i tak dalej.

4. Używając Room, używamy adnotacji @Entity, aby nadać naszemu modelowi nazwę tabeli; dlatego w naszej nowo utworzonej klasie UserInformation dodajmy adnotację @Entity i wywołajmy naszą tabelę z informacjami o użytkowniku:

@Entity(tableName = "user_information")
data class UserInformationModel(
val id: Int = 0,
val firstName: String,
val lastName: String,
val dateOfBirth: Int,
val gender: String,
val city: String,
val profession: String
)

5. Następnie, jak we wszystkich bazach danych, musimy zdefiniować klucz podstawowy dla naszej bazy danych. Dlatego w naszym identyfikatorze dodamy adnotację @PrimaryKey, aby poinformować Room, że jest to nasz klucz podstawowy i powinien zostać wygenerowany automatycznie. Jeśli nie chcesz automatycznie generować, możesz ustawić wartość logiczną na false, ale może to nie być dobry pomysł ze względu na konflikty, które mogą pojawić się później w Twojej bazie danych:

@PrimaryKey(autoGenerate = true)

6. Teraz powinieneś mieć encję z nazwą tabeli, kluczem podstawowym i typami danych:

import androidx.Room.Entity
import androidx.Room.PrimaryKey
@Entity(tableName = "user_information")
data class UserInformationModel(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,

)

Wewnątrz naszego pakietu danych utwórzmy nowy pakiet i nazwijmy go DAO, co oznacza obiekt dostępny dla danych. Gdy już to zrobisz, utwórz nowy interfejs i nadaj mu nazwę UserInformationDao; ten interfejs będzie zawierał funkcje tworzenia, odczytu, aktualizacji i usuwania (CRUD), czyli aktualizowania, wstawiania, usuwania i wykonywania zapytań. Musimy także dodać adnotację do naszego interfejsu za pomocą @Dao, aby poinformować Room, że to jest nasze DAO. Używamy OnConfusedStrategy.REPLACE w funkcjach aktualizacji i wstawiania, aby pomóc nam w przypadku, gdy możemy napotkać konflikty w naszej bazie danych. OnConfusedStrategy w tym przypadku oznacza, że jeśli Insert ma ten sam ID, zastąpi te dane konkretnym ID:

private const val DEFAULT_USER_ID = 0
@Dao
interface UserInformationDao {
@Query("SELECT * FROM user_information")
fun getUsersInformation():
Flow>
@Query("SELECT * FROM user_information WHERE id =
:userId")
fun loadAllUserInformation(userId: Int =
DEFAULT_USER_ID): Flow
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUserInformation(userInformation:
UserInformationModel)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateUserInformation(userInformation:
UserInformationModel)
@Delete
suspend fun deleteUserInformation(userInformation:
UserInformationModel)
}

7. Teraz, gdy mamy już naszą encję i DAO, w końcu utworzymy klasę Database, która stanowi rozszerzenie RoomDatabase(). W tej klasie skorzystamy z adnotacji @Database, przekażemy utworzoną przez nas encję, czyli encję UserInformation, i nadamy naszej bazie danych nazwę wersji, która obecnie wynosi jedną. Określimy również, czy nasz schemat bazy danych powinien zostać wyeksportowany, czy nie. Przejdźmy więc dalej i utwórzmy klasę abstrakcyjną Database:

@Database(entities = [UserInformation::class], version = 1,
exportSchema = false)
abstract class UserInformationDatabase : RoomDatabase() {
abstract fun userInformationDao():
UserInformationDao
}

8. Wreszcie mamy skonfigurowany i gotowy pokój. Teraz musimy dodać Zastrzyk zależności i nasz interfejs użytkownika; kod znajdziesz w sekcji Wymagania techniczne. Ponadto interfejs użytkownika jest na tym etapie dość prosty; ulepszenie go może stanowić wyzwanie, ponieważ ten przykładowy projekt służy wyłącznie celom demonstracyjnym.

Jak to działa…

Biblioteka Modern Android Development Room zawiera trzy istotne komponenty bazy danych Room:

•  Jednostka
•  DAO
•  Baza danych

Jednostka to tabela w bazie danych. Room generuje tabelę dla każdej klasy z adnotacją @Entity; jeśli korzystałeś już wcześniej z języka Java, możesz pomyśleć o encji jako o zwykłym, starym obiekcie Java (POJO). Klasy encji są zwykle drobne, nie zawierają żadnej logiki i przechowują jedynie typ danych obiektu. Niektóre istotne adnotacje mapujące tabele w bazie danych to klucze obce, indeksy, klucze podstawowe i nazwy tabel. Istnieją inne istotne adnotacje, takie jak ColumnInfo, która podaje informacje o kolumnie, i Ignore, które, jeśli zostaną użyte, niezależnie od danych, które chcesz zignorować, nie zostaną utrwalone w Room. @DAO definiuje funkcje uzyskujące dostęp do bazy danych. Pomyśl o tym jak o CRUD; jeśli użyłeś SQLite przed Room, jest to podobne do używania obiektów kursora. Wreszcie @Database zawiera funkcje bazy danych i służy jako główny punkt wejścia dla dowolnego bazowego połączenia z relacyjnymi danymi naszej aplikacji. Jeśli chcesz tego użyć, dodajesz adnotacje za pomocą @Database, tak jak to zrobiliśmy w naszej klasie baz danych. Dodatkowo klasa ta rozszerza RoomDatabase i zawiera listę tworzonych przez nas encji. Zawiera także utworzoną przez nas metodę abstrakcyjną, nie ma argumentów i zwraca klasę, do której dodaliśmy adnotację @Dao. Bazę danych uruchamiamy wywołując Room.databaseBuilder().

Implementowanie wstrzykiwania zależności w Room

Podobnie jak w przypadku innych przepisów, wstrzykiwanie zależności jest niezbędne i w tym przepisie omówimy, w jaki sposób możemy wstrzyknąć nasz moduł bazy danych i udostępnić bazę danych Room tam, gdzie jest ona potrzebna.

Przygotowywanie się

Aby móc postępować zgodnie z tym przepisem krok po kroku, musisz mieć wcześniejszą wiedzę na temat działania Hilt.

Jak to zrobić…

Otwórz projekt RoomExample i dodaj Hilt, którego użyjemy do wstrzykiwania zależności. W rozdziale 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt, omówiliśmy Hilt, więc nie będziemy go tutaj omawiać, ale po prostu pokażemy, jak można go używać z Room:

1. Otwórz swój projekt i dodaj niezbędną zależność Hilt. Zobacz Rozdział 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt, jeśli potrzebujesz pomocy w konfiguracji Hilt lub odwiedź https://dagger.dev/hilt/.

2. Następnie dodajmy naszą klasę @HiltAndroidApp, a w folderze Manifest dodajmy nazwę naszej HiltAndroidApp, w naszym przypadku UserInformation:

@HiltAndroidApp
class UserInformation : Application()
android:allowBackup="true"
android:name=".UserInformation"
tools:targetApi="33">
..…

3. Teraz, gdy mamy już wstrzykiwanie zależności, przejdźmy dalej i dodajmy @AndroidEntryPoint w klasie MainActivity, a w naszym projekcie utwórzmy nowy pakiet i nazwijmy go di. Wewnątrz utworzymy nową klasę DatabaseModule i dodamy nasze funkcjonalności.

4. W DatabaseModule utwórzmy funkcję ProvideDatabase(), w której zwrócimy obiekt Room, dodamy nazwę bazy danych i upewnimy się, że zbudowaliśmy naszą bazę danych:

@Module
@InstallIn(SingletonComponent::class)
class DataBaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context:
Context): UserInformationDatabase {
return Room.databaseBuilder(
context,
UserInformationDatabase::class.java,
"user_information.db"
).build()
}
@Singleton
@Provides
fun provideUserInformationDao(
userInformationDatabase: UserInformationDatabase):
UserInformationDao {
return
userInformationDatabase.userInformationDao()
}
}

5. Teraz, gdy mamy już skonfigurowany moduł bazy danych Zastrzyk zależności, możemy rozpocząć dodawanie usługi, czyli funkcji, które pomogą nam dodawać informacje o użytkownikach do bazy danych i pobierać informacje o użytkownikach z bazy danych. Przejdźmy więc dalej i utwórzmy nowy pakiet o nazwie service. Wewnątrz pakietu utwórz nowy interfejs UserInfoService i dodaj dwie wyżej wymienione funkcje:

interface UserInfoService {
fun getUserInformationFromDB(): Flow
suspend fun addUserInformationInDB(
userInformation: UserInformation)
}

6. Ponieważ UserInfoService jest interfejsem, będziemy musieli zaimplementować funkcjonalności w naszej klasie Impl, więc przejdźmy dalej i utwórzmy nową klasę o nazwie UserInfoServiceImpl oraz klasę singleton, a następnie zaimplementujmy interfejs:

@Singleton
class UserInfoServiceImpl() : UserInfoService {
override fun getUserInformationFromDB():
Flow {
TODO("Not yet implemented")
}
override suspend fun addUserInformationInDB(
userInformation: UserInformation) {
TODO("Not yet implemented")
}
}

7. Będziemy musieli wstrzyknąć nasz konstruktor i przekazać UserInformationDao(), ponieważ użyjemy funkcji wstawiania, aby wstawić dane użytkownika:

class UserInfoServiceImpl @Inject constructor(
private val userInformationDao: UserInformationDao
): UserInfoService

8. Teraz musimy dodać do naszych funkcji kod zawierający TODO. Przejdźmy dalej i najpierw zobaczmy informacje o użytkowniku. Używając userInformationDao, wywołamy funkcję wstawiania, aby poinformować Room, że chcemy wstawić te informacje o użytkowniku:

override suspend fun addUserInformationInDB(
userInformation: UserInformation) {
userInformationDao.insertUserInformation(
UserInformation(
firstName = userInformation.firstName,
lastName = userInformation.lastName,
dateOfBirth = userInformation.dateOfBirth,
gender = userInformation.gender,
city = userInformation.city,
profession = userInformation.profession
)
)
}

9. Następnie musimy pobrać informacje o użytkowniku z bazy danych; spowoduje to wizualizację danych użytkownika na ekranie:

override fun getUserInformationFromDB() =
userInformationDao.getUsersInformation().filter {
information -> information.isNotEmpty()
}.flatMapConcat {
userInformationDao.loadAllUserInformation()
.map { userInfo ->
UserInfo(
id = userInfo.id,
firstName = userInfo.firstName,
lastName = userInfo.lastName,
dateOfBirth =
userInfo.dateOfBirth,
gender = userInfo.gender,
city = userInfo.city,
profession = userInfo.profession
)
}
}

10. Na koniec musimy upewnić się, że zapewniamy implementację poprzez wstrzykiwanie zależności, zatem przejdźmy teraz do przodu i dodajmy poprzedni kod, następnie wyczyśćmy projekt, uruchommy go i upewnijmy się, że wszystko działa zgodnie z oczekiwaniami:

@Module
@InstallIn(SingletonComponent::class)
abstract class UserInfoServiceModule {
@Singleton
@Binds
abstract fun bindUserService(
userInfoServiceImpl: UserInfoServiceImpl):
UserInfoService
}

11. Po uruchomieniu projektu powinieneś móc zobaczyć jego uruchomienie bez problemu. Pójdziemy dalej i dodamy funkcję w naszym ViewModelu, aby wstawić dane do naszej bazy danych; ViewModel zostanie użyty w widokach, które stworzyliśmy:

@HiltViewModel
class UserInfoViewModel @Inject constructor(
private val userInfoService: UserInfoService
) : ViewModel() {
fun saveUserInformationData(userInfo: UserInfo) {
viewModelScope.launch {
userInfoService.addUserInformationInDB(
userInfo)
}
}
}

12. Możemy teraz sprawdzić bazę danych i sprawdzić, czy została poprawnie utworzona. Uruchom aplikację, a gdy będzie gotowa w IDE, kliknij Inspekcja aplikacji. Powinieneś móc otworzyć Inspektora Bazy Danych.

13. Po załadowaniu Inspektora bazy danych powinieneś móc wybrać aktualnie działający emulator Androida

14. Na rysunku możesz zobaczyć otwartego Inspektora Bazy Danych i naszą bazę danych.



15. Na rysunku widać, że wstawione przez nas dane są wyświetlane, co oznacza, że nasza funkcja wstawiania działa zgodnie z oczekiwaniami.



Jak to działa…

W tym przepisie zdecydowaliśmy się na użycie wstrzykiwania zależności w celu dostarczenia potrzebnych zależności konkretnym klasom. Omówiliśmy szczegółowo, czym jest wstrzykiwanie zależności w poprzednich częściach, więc nie będziemy tego ponownie wyjaśniać w tej, ale zamiast tego porozmawiamy o stworzonych przez nas modułach. Użyliśmy adnotacji @Singleton w Hilt, aby wskazać, że ProvideDatabase, który udostępnia instancję Room, powinien zostać utworzony tylko raz w ciągu życia naszej aplikacji i że ta instancja powinna być współdzielona pomiędzy wszystkimi zależnymi od niej komponentami. Ponadto, gdy dodasz adnotację do klasy lub metody wiązania za pomocą @Singleton, Hilt gwarantuje, że utworzona zostanie tylko jedna instancja tej klasy lub obiektu, a wszystkie komponenty wymagające tego obiektu otrzymają tę samą instancję. Ważne jest również, aby wiedzieć, że użycie @Singleton w Hilt nie jest tym samym, co wzorzec Singleton w projektowaniu oprogramowania, co może łatwo spowodować zamieszanie. Hilt′s @Singleton gwarantuje jedynie, że jedna instancja klasy zostanie utworzona w kontekście określonej hierarchii komponentów. W naszym projekcie utworzyliśmy DatabaseModule() i UserInfoServiceModule(). W klasie DatabaseModule() mamy dwie funkcje, ProvideDatabase i ProvideUserInformationDao. Pierwsza funkcja, ProvideDatabase, zwraca instancję UserInformationDatabase Room, w której możemy utworzyć bazę danych i ją zbudować:

fun provideDatabase(@ApplicationContext context: Context):
UserInformationDatabase {
return Room
.databaseBuilder(context,
UserInformationDatabase::class.java,
"user_information.db") .build()
}

W ProvideUserInformationDao przekazujemy UserInformationDatabase w konstruktorze i zwróć klasę abstrakcyjną UserInformationDao:

fun provideUserInformationDao(userInformationDatabase:
UserInformationDatabase): UserInformationDao {
return userInformationDatabase.userInformationDao()
}

Ważna uwaga

Jeśli podczas migracji chcesz utracić istniejące dane lub brakuje ścieżki migracji, podczas tworzenia bazy danych możesz użyć funkcji .fallbackToDestructiveMigration().

Wspieranie wielu podmiotów w Room

W tym przepisie dowiesz się, jak obsługiwać wiele obiektów w Room. Jest to przydatne, gdy masz duży projekt, który wymaga wprowadzenia innych danych. Doskonałym przykładem, z którym możemy pracować, jest aplikacja do budżetowania. Aby obsługiwać wiele encji w Room, musisz zdefiniować wiele klas reprezentujących tabele bazy danych. Każda klasa powinna mieć własne adnotacje i pola odpowiadające kolumnom w tabeli. Na przykład aplikacja do budżetowania może potrzebować różnych typów modeli, takich jak następujące:

•  Dane budżetowe
•  Dane dotyczące wydatków
•  Pozycja wydatków

Dlatego czasami konieczne jest posiadanie wielu podmiotów, a wiedza, jak sobie z tym poradzić, przydaje się.

Przygotowywanie się

Aby zastosować się do tego przepisu, musisz ukończyć poprzedni przepis.

Jak to zrobić …

Do realizacji zagadnień omawianych w tym przepisie możesz wykorzystać dowolny wybrany przez siebie projekt. Ponadto możesz wykorzystać ten przykład w swoim wcześniej istniejącym projekcie, aby wdrożyć temat.

1. W RoomExample możesz dodać więcej funkcji do aplikacji i spróbować dodać więcej encji, ale w przypadku tego projektu przejdźmy dalej i pokażmy, jak możesz obsługiwać wiele encji w Room.
2. W tym przykładzie użyjemy przykładowej aplikacji do budżetowania, którą przedstawiliśmy we wcześniejszej części, a ponieważ pracujemy z podmiotami, będzie to łatwiejsze do naśladowania. Stwórzmy nową jednostkę i nazwijmy ją BudgetData; klasa danych budżetu może zawierać kilka pól, takich jak budgetName, budgetAmount, wydatki, data początkowa, data końcowa, powiadomienie, waluta i suma wydatków; dlatego nasza klasa danych BudgetData będzie wyglądać następująco:

@Entity(tableName = "budgets")
data class BudgetData(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
var budgetName: String = "",
var budgetAmount: Double = 0.0,
var expenses: String = "",
var startDate: String = "",
var endDate: String = "",
var notify: Int = 0,
var currency: String = "",
var totalExpenses: Double
)

3. Przejdźmy dalej i dodajmy jeszcze dwa byty. Najpierw dodamy ExpenseData, które mogą mieć następujące pola i typy:

@Entity(tableName = "expenses")
data class ExpenseData(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
var expenseName: String = "",
var expenseType: String = "",
var expenseAmount: Double = 0.0,
@ColumnInfo(name = "updated_at")
var expenseDate: String = "",
var note: String = "",
var currency: String = ""
)

4. Następnie dodajmy ExpenseItem, który może składać się z następujących pól:

@Entity(tableName = "items")
Data class ExpenseItem(
@PrimaryKey(autoGenerate = true)
private var _id: Int
val name: String
var type: String?
val imageContentId: Int
val colorContentId: Int)

5. Jak widać, mamy trzy byty; na podstawie tych encji powinieneś stworzyć dla każdego różne DAO:

abstract class AppDatabase : RoomDatabase() {
abstract fun budgetDao(): BudgetDao
abstract fun itemDao(): ItemDao
abstract fun expenseDao(): ExpenseDao
}

6. Na górze klasy abstrakcyjnej AppDatabase dodamy do niej adnotację @Database, a następnie przekażemy ją wszystkim naszym podmiotom:

@Database(
entities = [ExpenseItem::class, BudgetData::class,
ExpenseData::class],
version = 1
)
@TypeConverters(DateConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun budgetDao(): BudgetDao
abstract fun itemDao(): ItemDao
abstract fun expenseDao(): ExpenseDao
}

7. Możesz także używać obiektów osadzonych; adnotacja @Embedded obejmuje zagnieżdżone lub powiązane encje w encji. Umożliwia reprezentowanie relacji między encjami poprzez osadzenie jednego lub większej liczby powiązanych encji w encji nadrzędnej:

data class ExpenseItem(

@Embedded val tasks: Tasks
)
data class Tasks(…)

W naszym poprzednim przykładzie właściwość task w encji ExpenseItem dodaliśmy adnotację @Embedded. Dzięki temu Room ma uwzględnić pola klasy danych Zadania tabeli ExpenseItem, zamiast tworzyć osobną tabelę dla naszej encji ExpenseItem.

8. Następnie klasa danych Zadania może mieć opis, priorytet, updateAt i ID:

@PrimaryKey(autoGenerate = true)
var id = 0
var description: String
var priority: Int
@ColumnInfo(name = "updated_at")
var updatedAt: Date)

Tym samym tabela reprezentująca obiekt ExpenseItem będzie zawierać dodatkowe kolumny z nowo dodanymi polami. Otóż to; po zadeklarowaniu jednostek w bazie danych i przekazaniu ich zgodnie z wymaganiami, będziesz mieć możliwość obsługi wielu jednostek w swojej bazie danych.

Ważna uwaga

Jeśli encja ma wiele osadzonych pól tego samego typu, możesz zachować unikalność każdej kolumny, ustawiając właściwość Prefix; następnie Room doda podane wartości na początku każdej nazwy kolumny w osadzonym obiekcie. Więcej informacji znajdziesz na https://developer.android.com/.

Jak to działa…

Zgodnie z zasadami panującymi w Roomie relację encji można zdefiniować na trzy różne sposoby.

•  Relacje jeden do wielu lub wiele do jednego
•  Relacje jeden na jeden
•  Relacje wiele do wielu

Jak już widziałeś, użycie jednostek w jednej klasie sprawia, że jest ona łatwa w zarządzaniu i śledzeniu; dlatego jest to doskonałe rozwiązanie dla inżynierów Androida. Godną uwagi adnotacją jest @Relation, która określa, gdzie tworzysz obiekt pokazujący relacje między twoimi jednostkami.

Migracja istniejącej bazy danych SQL do Room

Jak wspomnieliśmy wcześniej, Room rzeczywiście wykorzystuje moc SQLite, a ponieważ wiele aplikacji nadal korzysta ze starszej wersji, możesz znaleźć aplikacje nadal korzystające z SQL i zastanawiać się, w jaki sposób możesz przeprowadzić migrację do Room i korzystaj z najnowszych funkcji Room. W tym przepisie omówimy krok po kroku migrację istniejącej bazy danych SQL do Room na przykładach. Co więcej, Room oferuje warstwę abstrakcji, która pomaga w migracjach SQLite - to znaczy oferuje programistom klasę Migration.

Jak to zrobić…

Ponieważ nie utworzyliśmy nowego przykładu bazy danych SQLite i nie jest to konieczne, spróbujemy emulować scenariusz z fikcyjną przykładową bazą danych SQLite i zaprezentujemy, w jaki sposób można przeprowadzić migrację istniejącej bazy danych SQLite do Room:

1. Ponieważ będziemy dodawać Room do istniejącego projektu SQLite, musisz upewnić się, że dodałeś wymagane zależności. Aby to skonfigurować, odwołaj się do Pokoju Wdrożeniowego w przepisie aplikacji.
2. Następnie musisz przejść dalej i utworzyć nowego DAO i podmiot, ponieważ Room tego wymaga. Dlatego w tym zestawie, zgodnie z pierwszą recepturą pomieszczenia, możesz zaktualizować klasy modelu do jednostek. Jest to całkiem proste, ponieważ przeważnie będziesz robić adnotacje do klas za pomocą @Entity i używać właściwości table Names, aby ustawić nazwę tabeli.
3. Musisz także dodać adnotacje @PrimaryKey i @ColumnInfo dla swoich klas encji. Oto przykładowa baza danych SQLite:

fun onCreate(db: SQLiteDatabase) {
// Create a String that contains the SQL statement
to create the items table
val SQL_CREATE_ITEMS_TABLE =(
"CREATE TABLE " + ItemsContract.ItemsEntry
.TABLE_NAME.toString() + " ("
+ ItemsContract.ItemsEntry.
_Id.toString()
+ " INTEGER PRIMARY KEY
AUTOINCREMENT, "
+ ItemsContract.ItemsEntry
.COLUMN_ITEM_NAME.toString()
+ " TEXT NOT NULL, "
+ ItemsContract.ItemsEntry
.COLUMN_ITEM_TYPE.toString()
+ " TEXT NOT NULL, "
+ ItemsContract.ItemsEntry
.COLUMN_ITEM_LOGO.toString()
+ " INTEGER NOT NULL DEFAULT 0, "
+ ItemsContract.ItemsEntry
.COLUMN_ITEM_COLOR.toString()
+ " INTEGER NOT NULL DEFAULT 0, "
+ ItemsContract.ItemsEntry
.COLUMN_ITEM_CREATED_DATE
.toString() + " DATE NOT NULL
DEFAULT CURRENT_TIMESTAMP);")
// Execute the SQL statement
db.execSQL(SQL_CREATE_ITEMS_TABLE)
}

Jednak Room uprościł ten proces i nie musimy już tworzyć umów. Kontrakty w systemie Android umożliwiają programistom definiowanie i egzekwowanie zestawu reguł dostępu do danych w aplikacji. Kontrakty te zazwyczaj definiują strukturę i schemat tabel bazy danych oraz oczekiwane typy i formaty danych w nich zawartych. W przypadku SQLite na Androidzie kontrakty często służą do definiowania tabel i kolumn bazy danych, a także wszelkich ograniczeń czy relacji pomiędzy nimi.

4. Kiedy już stworzymy wszystkie potrzebne podmioty i DAO, możemy przystąpić do tworzenia bazy danych. Jak widzieliśmy w Implementing Room w przepisie na aplikacje, możemy dodać wszystkie nasze encje w adnotacji @Database, a ponieważ jesteśmy w pierwszej (1) wersji, możemy zwiększyć wersję do (2):

val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database:
SupportSQLiteDatabase) {
//alter items table
database.execSQL("CREATE TABLE new_items (_id
INTEGER PRIMARY KEY AUTOINCREMENT NOT
NULL, name TEXT NOT NULL, type TEXT,
imageContentId INTEGER NOT NULL,
colorContentId INTEGER NOT NULL)")
database.execSQL("INSERT INTO new_items
(_id,name,type,imageContentId,
colorContentId)Select_id,name,type,
imageContentId, colorContentId FROM items")
database.execSQL("DROP TABLE items")
database.execSQL("ALTER TABLE new_items RENAME TO items")
}

5. Następnie ważną częścią jest wywołanie funkcji build() do bazy danych Room:

Room.databaseBuilder(
androidContext(),
AppDatabase::class.java, "budget.db"
)
.addCallback(object : RoomDatabase.Callback() {
override fun
onCreate(db:SupportSQLiteDatabase){
super.onCreate(db)
}
})
.addMigrations(MIGRATION_1_2)
.build()

6. Gdy Twoja warstwa danych zacznie korzystać z Room, możesz oficjalnie zastąpić cały kod kursora i ContentValue wywołaniami DAO. W naszej klasie AppDatabase mamy nasze encje, a nasza klasa rozszerza RoomDatabase():

@Database(
entities = [],
version = 2
)
abstract class AppDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
}

Ponieważ Room oferuje błędy w czasie wykonywania, jeśli wystąpi jakikolwiek błąd, zostaniesz o tym powiadomiony w Logcat.

7. Można śmiało powiedzieć, że nie wszystko da się ująć w jednym przepisie, ponieważ konfiguracja SQLite wymaga dużej ilości kodu - na przykład do tworzenia zapytań i obsługi kursorów - ale Room pomaga przyspieszyć te procesy, co jest dlaczego jest to wysoce zalecane.

Jak to działa?

Zgodnie z wcześniejszymi zaleceniami migracja złożonej bazy danych może być gorączkowa i wymagać ostrożności, ponieważ może mieć wpływ na użytkowników, jeśli zostanie przeniesiona do środowiska produkcyjnego bez dokładnych testów. Zdecydowanie zaleca się również użycie OpenHelper, udostępnianego przez RoomDatabase, w celu uzyskania prostszych lub minimalnych zmian w bazie danych. Ponadto warto wspomnieć, że jeśli masz jakiś starszy kod korzystający z SQLite, zostanie on napisany na wysokim poziomie w Javie, dlatego konieczna jest praca z zespołem w celu znalezienia lepszego rozwiązania dla migracji. W swoim projekcie musisz zaktualizować klasę rozszerzającą SQLiteOpenHelper. Używamy SupportSQLiteDatabase, ponieważ musimy zaktualizować wywołania, aby uzyskać zapisywalną i czytelną bazę danych. Jest to czystsza klasa abstrakcji bazy danych służąca do wstawiania i wysyłania zapytań do bazy danych.

Ważna uwaga

Należy pamiętać, że migracja do złożonej bazy danych zawierającej wiele tabel i złożonych zapytań może być skomplikowana. Jeśli jednak baza danych zawiera minimalną liczbę tabel i nie zawiera skomplikowanych zapytań, migrację można przeprowadzić szybko, wprowadzając stosunkowo niewielkie zmiany przyrostowe w gałęzi funkcji.

Testowanie lokalnej bazy danych

Do tej pory dbaliśmy o to, aby pisać testy zawsze, gdy jest to konieczne dla naszych projektów. Będziemy teraz musieli napisać testy dla naszego projektu RoomExample, ponieważ jest to kluczowe i może być konieczne wykonanie tego w rzeczywistym scenariuszu. Dlatego w tym przepisie przyjrzymy się krok po kroku, jak napisać testy CRUD dla naszej bazy danych.

Przygotowywanie się

Aby rozpocząć korzystanie z tego przepisu, musisz otworzyć projekt RoomExample.

Jak to zrobić…

Przejdźmy dalej i najpierw dodajmy wszystkie potrzebne zależności testowe Room, a następnie zacznijmy pisać nasze testy. Aby zapoznać się z konfiguracją testu Hilt, zapoznaj się z sekcją Wymagania techniczne, w której znajdziesz cały wymagany kod:

1. Będziesz musiał dodać następujące elementy do pliku build.gradle:

androidTestImplementacja "com.google.truth:truth:1.1.3"
androidTestImplementation "android.arch.core:core-testing:1.1.1"

2. Po dodaniu wymaganych zależności w teście Androida utwórz nową klasę, nadając jej nazwę UserInformationDBTest:

class UserInformationDBTest {…}

3. Zanim będziemy mogli skonfigurować naszą funkcję @Before, będziemy musieli utworzyć dwie instancje lateinit var, które zainicjujemy w naszej funkcji @Before:

private lateinit var database: UserInformationDatabase
private lateinit var userInformationDao: UserInformationDao
4. Teraz skonfigurujmy naszą funkcję @Before i utwórzmy naszą bazę danych, korzystając z bazy danych w pamięci do celów testowych:

@Before
fun databaseCreated() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
UserInformationDatabase::class.java
)
.allowMainThreadQueries()
.build()
userInformationDao = database.userInformationDao()
}

5. Ponieważ uruchamiamy i tworzymy bazę danych w pamięci, po zakończeniu będziemy musieli ją zamknąć; dlatego w naszym wywołaniu @After będziemy musieli wywołać funkcję Close() w naszej bazie danych:

@After
fun closeDatabase() {
database.close()
}

6. Teraz, gdy nasza konfiguracja jest już zakończona, przystąpimy do testowania naszego CRUD - czyli wstawiania, usuwania i aktualizacji. Przejdźmy dalej i utwórzmy najpierw test wstawiania:

@Test
fun insertUserInformationReturnsTrue() = runBlocking {
val userOne = UserInformationModel(
id = 1,
firstName = "Michelle",
lastName = "Smith",
dateOfBirth = 9121990,
gender = "Male",
city = "New york",
profession = "Software Engineer"
)
userInformationDao.insertUserInformation(userOne)
val latch = CountDownLatch(1)
val job = async(Dispatchers.IO) {
userInformationDao.getUsersInformation()
.collect {
assertThat(it).contains(userOne)
latch.countDown()
}
latch.await()
job.cancelAndJoin()
}

7. Na koniec dodajmy funkcję usuwania i na tym zakończymy nasz pokój testowy:

@Test
fun deleteUserInformation() = runBlocking {
val userOne = UserInformationModel(
id = 1,
firstName = "Michelle",
lastName = "Smith",
dateOfBirth = 9121990,
gender = "Male",
city = "New york",
profession = "Software Engineer"
)
val userTwo = UserInformationModel(
id = 2,
firstName = "Mary",
lastName = "Simba",
dateOfBirth = 9121989,
gender = "Female",
city = "New york",
profession = "Senior Android Engineer"
)
userInformationDao.insertUserInformation(userOne)
userInformationDao.insertUserInformation(userTwo)
userInformationDao.deleteUserInformation(userTwo)
val latch = CountDownLatch(1)
val job = async(Dispatchers.IO) {
userInformationDao.loadAllUserInformation()
.collect {
assertThat(it).doesNotContain(userTwo)
latch.countDown()
}
}
latch.await()
job.cancelAndJoin()
}

8. Po uruchomieniu testu wszystkie powinny przejść pozytywnie z zielonym znacznikiem wyboru:



Jak to działa…

Być może zauważyłeś, że użyliśmy Truth, który jest frameworkiem testowym zapewniającym płynny i wyrazisty interfejs API do pisania asercji w testach. Jest rozwijany przez Google, a niektóre zalety korzystania z Truth obejmują czytelność, elastyczność i jasne komunikaty o błędach. Możemy z łatwością używać konstrukcji bardziej przypominających język naturalny - na przykład isEqualTo i ShouldBe - dzięki czemu asercje testowe są bardziej intuicyjne i czytelne dla nas, programistów. Korzystając ze struktury, otrzymujesz szeroką gamę metod asercji, które pozwalają testować różne warunki, w tym równość, porządek i powstrzymywanie. Umożliwia także definiowanie niestandardowych metod asercji, co daje większą kontrolę nad zachowaniem testów. Adnotacja @Before gwarantuje, że nasza funkcja DatabaseCreated() zostanie wykonana przed każdą klasą. Następnie nasza funkcja tworzy bazę danych przy użyciu Room.inMemoryDatabaseBuilder, która tworzy bazę danych w pamięci o dostępie swobodnym (RAM) zamiast w pamięci trwałej. Oznacza to, że nasza baza danych zostanie wyczyszczona po zakończeniu procesu; dlatego w naszym wywołaniu @After zamykamy bazę danych:

@After
fun closeDatabase() {
database.close()
}

Jak zapewne widzieliście, nasze testy znajdują się w AndroidTest, ponieważ uruchamiamy Room w głównym wątku i zamykamy go po jego zakończeniu. Klasy testowe po prostu testują funkcje DAO - czyli Aktualizuj, Wstaw, Usuń i Zapytaj.

Pierwsze kroki z WorkManagerem

W systemie Android WorkManager to interfejs API wprowadzony przez Google jako część biblioteki Android Jetpack. Jest to potężna i elastyczna biblioteka do planowania zadań w tle, która umożliwia wykonywanie odroczonych, asynchronicznych zadań nawet wtedy, gdy aplikacja nie jest uruchomiona lub urządzenie znajduje się w stanie niskiego poboru mocy. WorkManager zapewnia ujednolicony interfejs API umożliwiający planowanie zadań, które należy wykonać w określonym czasie lub pod określonymi warunkami. Dba o efektywne zarządzanie zadaniami i ich wykonywanie w zależności od takich czynników, jak stan bezczynności urządzenia, łączność sieciowa i poziom naładowania baterii. Ponadto WorkManager umożliwia obserwację statusu pracy i tworzenie łańcucha. W tej części przyjrzymy się, jak możemy wdrożyć WorkManager na przykładach, a także dowiemy się, jak to działa i jakie są jego przypadki użycia.

Zrozumienie biblioteki Jetpack WorkManager

WorkManager jest jedną z najpotężniejszych bibliotek Jetpack i służy do ciągłej pracy. API umożliwia obserwację trwałego statusu i możliwość tworzenia złożonego łańcucha prac. Podczas tworzenia aplikacji na Androida może być wymagane zachowanie danych. WorkManager jest najczęściej polecanym interfejsem API dla dowolnego procesu w tle i jest znany z obsługi unikalnych typów bieżącej pracy, jak pokazano tutaj:

o Natychmiastowe: Jak sama nazwa wskazuje, są to zadania, które należy wykonać natychmiast lub zakończyć wkrótce
o Długotrwałe: Zadania trwające długo
o Odroczone: zadanie, które można przełożyć i któremu można przypisać inny czas rozpoczęcia, a także które może być uruchamiane okresowo

Oto kilka innych przykładowych przypadków użycia, w których możesz użyć WorkManager, jeśli Twoja firma chce tworzyć niestandardowe powiadomienia, wysyłać zdarzenia analityczne, przesyłać obrazy, okresowo synchronizować dane lokalne z siecią i nie tylko. Co więcej, WorkManager jest ulubionym interfejsem API i jest wysoce zalecany, ponieważ zastępuje wszystkie poprzednie interfejsy API planowania w tle w systemie Android. Istnieją inne interfejsy API używane do planowania pracy. Są one przestarzałe i w tej książce nie będziemy ich omawiać, ale wspomnimy o nich, ponieważ możesz je spotkać podczas pracy ze starszym kodem; są one następujące:

o Dyspozytor zadań Firebase
o Harmonogram zadań
o Menedżer sieci GCM
o Menedżer pracy

Przygotowywanie się

W tym przepisie przyjrzymy się prostemu przykładowi, jak możemy stworzyć własne niestandardowe powiadomienie za pomocą WorkManager. Możesz także użyć tej samej koncepcji do wysyłania dzienników lub analiz raportów dla swojej aplikacji, jeśli nasłuchujesz jakichkolwiek dzienników. Decydujemy się na to zadanie, ponieważ wysyłanie powiadomień do użytkowników jest kluczowe, a większość aplikacji to robi, w porównaniu do przesyłania zdjęć. Ponadto w przypadku Androida 13 i nowego API obowiązkowe jest zażądanie ndroid.permission.POST_NOTIFICATIONS.

Jak to zrobić…

W przypadku tego przepisu nie trzeba tworzyć projektu, ponieważ koncepcje można wykorzystać w już zbudowanym projekcie; zamiast tego przyjrzymy się przykładom i omówimy je z objaśnieniami:

1. Musimy upewnić się, że mamy wymaganą zależność:

implementtion "androidx.work:work-runtime-ktx:
version-number"

Możesz uzyskać najnowszy numer wersji, postępując zgodnie z dokumentacją pod adresem https://developer.android.com/jetpack/androidx/releases/work.

2. Przejdźmy teraz do stworzenia naszego kanału powiadomień. W tym celu Google oferuje świetny przewodnik, jak go utworzyć, pod adresem https://developer.android.com/develop/ui/views/notifications/channels, więc skopiuj następujący kod:

private fun createCustomNotificationChannel() {
if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.O) {
val name = getString(
R.string.notification_channel)
val notificationDescription = getString(
R.string.notification_description)
val importance =
NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID,
name, importance).apply {
description = notificationDescription
}
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(
Context.NOTIFICATION_SERVICE) as
NotificationManager
notificationManager.createNotificationChannel(
channel)
}
}

Pamiętaj też, że możliwe jest utworzenie różnych kanałów rozdzielania typów powiadomień. Zgodnie z zaleceniami w systemie Android 13 ułatwia to użytkownikom włączanie i wyłączanie tych funkcji, jeśli ich nie potrzebują. Na przykład użytkownik może chcieć wiedzieć, jakie najnowsze marki sprzedaje Twoja aplikacja, zamiast wysyłać użytkownikom informacje o starych, istniejących markach.

3. Teraz możemy stworzyć naszą workManagerInstance. Wyobraźmy sobie scenariusz, w którym musimy pobierać dane z naszych serwerów co 20 lub 30 minut i sprawdzać, czy są dostępne powiadomienia. W takim przypadku możemy napotkać problem polegający na tym, że użytkownicy nie będą już korzystać z naszej aplikacji, co oznacza, że aplikacja zostanie umieszczona w tle lub nawet proces może zostać zatrzymany. Stąd pojawia się pytanie, w jaki sposób pobrać dane, gdy aplikacja zostanie zabita? W tym momencie na ratunek przychodzi WorkManager.

4. Możemy teraz utworzyć instancję WorkManager:

val workManagerInstance = WorkManager.getInstance(application.
applicationContext)

5. Teraz będziemy musieli kontynuować i ustawić ograniczenia:
val ourConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(false)
.build()

6. Będziemy także musieli ustawić dane, które mają zostać przekazane pracownikowi; dlatego utworzymy nowe dane wartości, a następnie umieścimy ciąg znaków w żądaniu punktu końcowego:

val data = Data.Builder()
data.putString(ENDPOINT_REQUEST, endPoint)

7. Teraz możemy przejść dalej i utworzyć nasz PeriodicWorkRequestBuilder. W naszej pracy ustawimy ograniczenia, ustawimy nasze dane wejściowe i przekażemy typ GetDataWorker(), który utworzymy, a następnie zbudujemy. Ponadto, ponieważ chcemy pobierać dane z naszego serwera co 20 lub 30 minut, używamy PeriodicWorkRequestBuilder() w tym celu:

val job =
PeriodicWorkRequestBuilder(20,
TimeUnit.MINUTES)
.setConstraints(ourConstraints)
.setInputData(data.build())
.build()

8. Możemy teraz wreszcie wywołać workManagerInstance i umieścić nasze zadanie w kolejce:

workManagerInstance
.enqueue(work)

9. Możemy teraz przystąpić do konstrukcji naszej GetDataWorker(). W tej klasie rozszerzymy klasę Worker, co nadpisze funkcję doWork(). W naszym przypadku jednak zamiast rozszerzać klasę Worker, rozszerzymy klasę CoroutineWorker(context, workerParameters), co w naszym przypadku będzie pomocne, gdyż dane będziemy zbierać w przepływie. Będziemy również używać Hilt, więc wywołamy @HiltWorker:

@HiltWorker
class GetDataWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParameters: WorkerParameters,
private val viewModel: NotificationViewModel
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
val ourEndPoint = inputData.getString(
NotificationConstants.ENDPOINT_REQUEST)
if (endPoint != null) {
getData(endPoint)
}
val dataToOutput = Data.Builder()
.putString(
NotificationConstants.NOTIFICATION_DATA,
"Data")v .build()
return Result.success(dataToOutput)
}

W naszym przypadku zwracamy sukces. W naszej funkcji getData() przekazujemy punkt końcowy i możemy założyć, że nasze dane mają dwa lub trzy kluczowe atrybuty: identyfikator, tytuł i opis.

10. Możemy teraz wysyłać powiadomienia:

val notificationIntent = Intent(this, NotifyUser::class.java).
apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
}
notificationIntent.putExtra(NOTIFICATION_EXTRA, true)
notificationIntent.putExtra(NOTIFICATION_ID, notificationId)
val notifyPendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat
.Builder(context, Channel_ID_DEFAULT)
.setSmallIcon(notificationImage)
.setContentTitle(notificationTitle)
.setContentText(notificationContent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(notifyPendingIntent)
.setAutoCancel(true)
with(NotificationManagerCompat.from(context)) {
notify(notificationId, builder.build())
}

11. Musimy także utworzyć PendingIntent.getActivity(), co oznacza, że po kliknięciu w powiadomienie użytkownik rozpocznie działanie. Aby tak się stało, możemy uzyskaćStringExtra(NotificationConstants.NOTIFICATION_ID) po kliknięciu powiadomienia i umieścić w naszej intencji dodatki. To będzie musiało się wydarzyć w naszej działalności:

private fun verifyIntent(intent: Intent?) {
intent?.let {
if (it.hasExtra(
NotificationConstants.NOTIFICATION_EXTRA)){
it.getStringExtra(
NotificationConstants.NOTIFICATION_ID)
}
}
}

12. W naszej onResume() możemy teraz wywołać funkcję VerifyIntent():

override fun onResume() {
super.onResume()
verifyIntent(intent)
}

I to wszystko; mamy niestandardowe powiadomienia za pomocą naszego WorkManager().

Jak to działa…

Podczas tworzenia powiadomienia parametr valid pomaga określić, w jaki sposób przerwać użytkownikowi dla danego kanału, dlatego warto określić go w konstruktorze NotificationChannel. Jeśli znaczenie jest duże, a na urządzeniu działa system Android 5.0 lub nowszy, zobaczysz powiadomienie, w przeciwnym razie będzie to po prostu ikona na pasku stanu. Należy jednak pamiętać, że wszystkie powiadomienia, niezależnie od ich ważności, pojawiają się w niezakłóconym interfejsie użytkownika u góry ekranu. Słowo WorkManager jest bardzo proste, co eliminuje niejednoznaczność z API. Podczas korzystania z WorkManagera odwołanie do pracy odbywa się przy użyciu klasy Worker. Ponadto wywoływana przez nas funkcja doWork() działa asynchronicznie w wątku tła oferowanym przez WorkManager(). Funkcja doWork() zwraca wynik{}, a wynikiem może być sukces, niepowodzenie lub spróbować ponownie. Kiedy zwrócimy pomyślny Result{}, praca zostanie wykonana i zakończona pomyślnie. Niepowodzenie, jak sama nazwa wskazuje, oznacza, że praca nie powiodła się i wówczas wywołujemy opcję Retry, która ponawia próbę wykonania pracy. W naszej GetDataWorker() przekazujemy NotificationViewModel i wstrzykujemy go naszemu procesowi roboczemu za pomocą Hilt. Czasami możesz napotkać konflikt. Dobrą rzeczą jest to, że istnieje wsparcie dla takiego przypadku z czterema opcjami rozwiązywania wszelkich konfliktów, które mogą wystąpić. Ten przypadek jest wyjątkowy, gdy planujesz niepowtarzalną pracę; sensowne jest poinformowanie WorkManager, jakie działania należy podjąć, gdy pojawi się konflikt. Możesz łatwo rozwiązać ten problem, korzystając z istniejącej polityki pracy, ExisitingWorkPolicy, która zawiera REPLACE, KEEP APPEND i APPEND_ OR_REPLACE. REPLACE, jak sama nazwa wskazuje, zastępuje istniejącą pracę, podczas gdy Keep zachowuje istniejącą pracę i ignoruje nową. Wywołanie Append powoduje dodanie nowej pracy do istniejącej i ostatecznie Append lub Zamień po prostu nie zależy od wymaganego stanu pracy.

Ważna uwaga

WorkManager jest singletonem, dlatego można go zainicjować tylko raz, czyli w aplikacji lub w bibliotece. A jeśli używasz procesów roboczych z niestandardowymi zależnościami, musisz podać WorkerFactory() do konfiguracji w momencie niestandardowej inicjalizacji.

Zrozumienie stanu WorkManager

W poprzednim przepisie, Zrozumienie biblioteki Jetpack WorkManager, przyjrzeliśmy się, jak możemy wykorzystać WorkManager. W tym przepisie mogłeś zauważyć, że Work przechodzi przez serię zmian stanu, a funkcja doWork zwraca wynik. W tym przepisie szczegółowo zbadamy stany.

Jak to zrobić…

Będziemy kontynuować pracę nad przykładem zastosowania koncepcji poznanych w tym przepisie do już zbudowanego projektu:

1. Być może zauważyłeś, że wspomnieliśmy wcześniej, że mamy trzy stany: Sukces, Porażka i Ponowna próba. Stany pracy mają jednak różne typy procesów; możemy mieć stan pracy jednorazowej, stan pracy okresowej lub stan zablokowania:

Wynik
SUKCES, PORAŻKA, PONOWNA PRÓBA

Możesz przyjrzeć się tej klasie abstrakcyjnej bardziej szczegółowo, klikając wynik i sprawdzając, jak jest napisana.

2. W pierwszym przepisie, Zrozumienie biblioteki Jetpack WorkManager, przyjrzeliśmy się etapom konfiguracji WorkManager. Innym doskonałym przykładem jest pobieranie plików. Możesz zastąpić zabawną funkcję doWork() i sprawdzić, czy Twój URI nie jest równy null i zwrócić sukces, w przeciwnym razie niepowodzenie:

override suspend fun doWork(): Result {
val file = inputData.getString(
FileParameters.KEY_FILE_NAME) ?: ""
if (file.isEmpty()){
Result.failure()
}
val uri = getSavedFileUri(fileName = file,
context = context)
return if (uri != null){
Result.success(workDataOf(
FileParameters.KEY_FILE_URI to
uri.toString()))
}else{
Result.failure()
}
}

3. Podczas obsługi stanu możesz łatwo sprawdzić, kiedy stan pomyślnie określił akcję, kiedy nie wykonał akcji, a na koniec, kiedy WorkInfo.State jest równy RUNNING, wywołaj running(); zobacz następujący fragment kodu:

when (state) {
WorkInfo.State.SUCCEEDED -> {
success(
//do something
)
}
WorkInfo.State.FAILED -> {
failed("Downloading failed!")
}
WorkInfo.State.RUNNING -> {
running()
}
else -> {
failed("Something went wrong")
}
}

4. Wynik powodzenia zwraca instancję ListenableWorker.Result, która służy do wskazania, że praca została pomyślnie ukończona.

5. Dla wymienionych stanów możesz użyć metody nqueueUniqueWork(), która jest używana jednorazowo, lub PeriodicWorkRequestBuilder, która jest używana do pracy okresowej. W naszym przykładzie użyliśmy PeriodicWorkRequestBuilder:

WorkManager.enqueueUniqueWork()
WorkManager.enqueueUniquePeriodicWork()

Jak to działa…

Zawsze zaczynamy nasze żądanie od stanu Umieszczone w kolejce dla jednorazowego stanu pracy, co oznacza, że praca zostanie uruchomiona natychmiast po spełnieniu ograniczeń. Następnie przechodzimy do Biegania i jeśli osiągniemy Sukces, praca jest skończona. Jeśli w jakimkolwiek przypadku zakończymy bieg i nie osiągniemy sukcesu, oznacza to, że ponieśliśmy porażkę. Następnie wrócimy do kolejki, ponieważ będziemy musieli ponowić próbę. Rysunek poniższe lepiej wyjaśniają stany zarówno dla stanów pracy jednorazowej, jak i okresowej. Wreszcie, jeśli zdarzy się, że nasza kolejkowana praca zostanie anulowana, wówczas przenosimy ją do anulowanej.



Podczas gdy poprzedni obraz przedstawia jednorazowy stan pracy, poniższy diagram przedstawia okresowy stan pracy.



Zrozumienie wątków w WorkManager

Możesz myśleć o WorkManagerze jak o dowolnym procesie działającym w wątku w tle. Kiedy używamy Worker(), a WorkManager wywołuje funkcję doWork(), ta akcja działa w wątku w tle. Mówiąc szczegółowo, wątek tła pochodzi z Executora określonego w konfiguracji WorkManager. Możesz także utworzyć własny, niestandardowy executor dla potrzeb swojej aplikacji, ale jeśli nie jest to potrzebne, możesz użyć istniejącego. W tym przepisie omówimy, jak działa wątek w funkcji Worker() i jak utworzyć niestandardowy moduł wykonujący.

Przygotowywanie się

W tym przepisie, ponieważ będziemy przyglądać się przykładom, możesz śledzić dalej, czytając i sprawdzając, czy dotyczy to Ciebie.

Jak to zrobić…

Dowiedzmy się, jak działa wątki w WorkManager:

1. Aby ręcznie skonfigurować WorkManager, musisz określić swojego executora. Można to zrobić wywołując WorkManager.initialize(), następnie przekazując kontekst i kreator konfiguracji:

WorkManager.initialize(
context,
Configuration.Builder()
.setExecutor(Executors.newFixedThreadPool(
CONSTANT_THREAD_POOL_INT))
.build())

2. W naszym wcześniejszym przykładzie w poprzednim przepisie, Zrozumienie stanu WorkManager, mówiliśmy o przypadku użycia, w którym pobieramy pliki. Pliki te mogą mieć format PDF, JPG, PNG, a nawet MP4. Przyjrzymy się przykładowi, który pobiera zawartość 20 razy; możesz określić, ile razy chcesz pobierać zawartość:

Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
repeat(20) {
try {
downloadSynchronously("Your Link")
} catch (e: IOException) {
return
ListenableWorker.Result.failure()
}
}
return ListenableWorker.Result.success()
}
}

3. Obecnie, jeśli nie zajmiemy się przypadkiem zatrzymania Worker(), dobrą praktyką jest upewnienie się, że zostanie to rozwiązane, ponieważ jest to przypadek brzegowy. Aby rozwiązać ten przypadek, musimy zastąpić metodę Worker.onStopped() lub wywołać metodę Worker.isStopped tam, gdzie jest to konieczne, aby zwolnić część zasobów:

override fun doWork(): ListenableWorker.Result {
repeat(20) {
if (isStopped) {
break
}
try {
downloadSynchronously("Your Link")
} catch (e: IOException) {
return
ListenableWorker.Result.failure()
}
}
return ListenableWorker.Result.success()
}

4. Na koniec, gdy zatrzymasz proces roboczy, wynik zostanie całkowicie zignorowany, dopóki proces nie zostanie ponownie uruchomiony. W naszym wcześniejszym przykładzie użyliśmy CoroutineWorker, ponieważ WorkManager oferuje obsługę współprogramów, dlatego zebraliśmy dane w przepływie.

Ważna uwaga

Dostosowywanie modułu executora będzie wymagało ręcznej inicjalizacji WorkManagera.

Jak to działa…

W bibliotece WorkManager Jetpack można dowiedzieć się więcej i uczciwie przyznać, że nie da się tego wszystkiego ująć w kilku przepisach. Na przykład w niektórych scenariuszach, udostępniając niestandardową strategię wątków, należy użyć ListenableWorker. ListenableWorker to klasa w bibliotece Android Jetpack WorkManager, która umożliwia elastyczne i wydajne wykonywanie pracy w tle. Jest to podklasa klasy Worker i dodaje możliwość zwrócenia ListenableFuture z metody doWork(), co umożliwia lub ułatwia obsługę operacji asynchronicznych. Korzystając z ListenableWorker, możesz utworzyć proces roboczy, który zwraca ListenableFuture i zarejestrować wywołania zwrotne, które zostaną wykonane po zakończeniu przyszłości. Może to być przydatne w przypadku zadań takich jak żądania sieciowe lub operacje na bazach danych, które wymagają operacji asynchronicznych. Worker, CoroutineWorker i RxWorker wywodzą się z tej konkretnej klasy. Worker, jak wspomniano, działa w wątku w tle; CoroutineWorker jest wysoce zalecany dla programistów korzystających z Kotlina. RxWorker nie będzie tutaj poruszany, ponieważ Rx sam w sobie jest dużym tematem przeznaczonym dla użytkowników rozwijających się w programowaniu reaktywnym.

Zrozumienie łączenia i anulowania żądań pracy

W programowaniu Androida kluczowe znaczenie ma zapewnienie właściwej obsługi cyklu życia aplikacji. Nie trzeba dodawać, że dotyczy to również wszystkich prac w tle, ponieważ prosty błąd może spowodować, że aplikacja wyczerpie baterię użytkownika, wycieki pamięci, a nawet spowoduje awarię aplikacji lub błąd ANR. Może to oznaczać fatalne recenzje w Sklepie Play, co później wpłynie na Twój biznes i spowoduje stres dla programistów. Jak zapewnić dobre rozwiązanie tego problemu? Można tego dokonać, upewniając się, że wszystkie konflikty powstałe podczas korzystania z WorkManagera są odpowiednio rozwiązywane lub gwarantując, że zasady, o których wspominaliśmy w poprzednim przepisie, są dobrze zakodowane. W tym przepisie przyjrzymy się łączeniu i anulowaniu żądań pracy oraz temu, jak prawidłowo obsługiwać długotrwałą pracę. Załóżmy, że Twój projekt wymaga zamówienia, zgodnie z którym operacja powinna zostać uruchomiona; WorkManager daje możliwość kolejkowania i tworzenia łańcucha, który określa wiele zależnych zadań, a tutaj możesz ustawić kolejność, w jakiej chcesz, aby operacje były wykonywane.

Przygotowywanie się

W tym przepisie przyjrzymy się przykładowi, w jaki sposób możesz połączyć swoją pracę; ponieważ jest to oparte na koncepcji, przyjrzymy się przykładowi i wyjaśnimy, jak to działa.

Jak to zrobić…

Aby wykonać łączenie przy użyciu WorkManagera, wykonaj następujące kroki:

1. W naszym przykładzie założymy, że mamy cztery unikalne zadania pracownika, które można uruchamiać równolegle. Dane wyjściowe tych zadań zostaną przekazane do procesu roboczego wysyłającego. Następnie zostaną one przesłane na nasz serwer, podobnie jak przykładowy projekt, który mieliśmy w przepisie Zrozumienie przepisu biblioteki Jetpack WorkManager.

2. Będziemy mieć funkcję WorkManager() i przekazać ją w naszym kontekście; następnie wywołamy BeginWith i przekażemy listę naszych zadań:

WorkManager.getInstance(context)
.beginWith(listOf(job1, job2, job3, job4))
.then(ourCache)
.then(upload)
.enqueue()

3. Aby móc zachować lub zachować wszystkie wyniki naszego zadania, będziemy musieli użyć klasy ArrayCreatingInputMerger:::

val ourCache: OneTimeWorkRequest =
OneTimeWorkRequestBuilder()
.setInputMerger(ArrayCreatingInputMerger::class)
.setConstraints(constraints)
.build()

To tyle. Zdecydowanie jest jeszcze wiele do nauczenia się, ale służy to naszemu celowi.

Jak to działa…

Aby móc utworzyć łańcuch prac, używamy WorkManager.beginWith(OneTimeWorkRequest) lub WorkManager.beginWith i przekazujemy listę określonych przez Ciebie jednorazowych zleceń pracy. Operacje WorkManager.beginWith> zwracają instancję WorkContinuation. Używamy funkcji WorkContinuation.enqueue() do kolejkowania naszego łańcucha WorkContinuation. ArrayCreatingInputMerger gwarantuje, że połączymy każdy klucz z tablicą. Ponadto ArrayCreatingInputMerger to klasa w bibliotece Android Jetpack WorkManager, która umożliwia łączenie danych wejściowych z wielu instancji ListenableWorker w jedną tablicę. Ponadto, jeśli nasze klucze są unikalne, otrzymamy wynik w postaci tablic jednoelementowych. Rysunek przedstawia wynik:



Jeśli mamy jakieś kolidujące klucze, wówczas nasze wartości zostaną zgrupowane w naszej tablicy, jak na rysunku



Ogólna zasada jest taka, że łańcuchy pracy są zazwyczaj wykonywane sekwencyjnie. Zależy to od pomyślnego zakończenia prac. Być może zastanawiasz się, co się stanie, gdy zadanie zostanie umieszczone w łańcuchu kilku żądań pracy; podobnie jak w przypadku zwykłej kolejki, wszystkie kolejne prace są tymczasowo blokowane do czasu zakończenia pierwszego żądania pracy. Pomyśl o tym zgodnie z zasadą "kto pierwszy, ten lepszy".

Wdrożenie migracji z Firebase JobDispatcher do nowego, zalecanego WorkManagera

W przepisie na bibliotekę Jetpack WorkManager omówiliśmy inne biblioteki używane do planowania i wykonywania odroczonej pracy w tle. Firebase JobDispatcher to jeden z najpopularniejszych. Jeśli korzystałeś z Firebase JobDispatcher, być może wiesz, że używa on podklasy JobService() jako punktu wejścia. W tym przepisie przyjrzymy się, jak przeprowadzić migrację do nowo zalecanego WorkManagera.

Przygotowywanie się

Przyjrzymy się sposobom migracji z JobService do WorkerManager. Może to dotyczyć Twojego projektu lub nie. Jednak omówienie tego jest niezbędne, ponieważ WorkManager jest wysoce zalecany i wszyscy mamy jakiś starszy kod. Jeśli jednak Twój projekt jest nowy, możesz pominąć ten przepis.

Jak to zrobić…

Aby przeprowadzić migrację Firebase JobDispatcher do WorkManager, wykonaj następujące kroki:

1. Najpierw musisz dodać wymaganą zależność; w tym celu możesz odwołać się do przepisu dotyczącego biblioteki Jetpack WorkManager.
2. Jeśli w swoim projekcie masz już Firebase JobDispatcher, możesz mieć kod podobny do następującego fragmentu kodu:

class YourProjectJobService : JobService() {
override fun onStartJob(job: JobParameters):
Boolean {
// perform some job
return false
}
override fun onStopJob(job: JobParameters):
Boolean {
return false
}
}

3. Łatwiej jest, jeśli Twoja aplikacja korzysta z JobServices(); następnie zostanie zamapowany na ListenableWorker. Jeśli jednak Twoja aplikacja korzysta z SimpleJobService, w takim przypadku powinieneś użyć Workera:

class YourWorker(context: Context, params: WorkerParameters) :
ListenableWorker(context, params) {
override fun startWork():
ListenableFuture {
TODO("Not yet implemented")
}
override fun onStopped() {
TODO("Not yet implemented")
}
}

4. Jeśli Twój projekt korzysta z Job.Builder.setRecurring(true), w tym przypadku powinieneś zmienić go na klasę PeriodicWorkRequest oferowaną przez WorkManager. Możesz także określić tag, usługę, jeśli zadanie ma charakter cykliczny, okno wyzwalacza i nie tylko:

val job = dispatcher.newJobBuilder()

.build()

5. Ponadto, aby móc osiągnąć to, czego chcemy, będziemy musieli wprowadzić dane, które będą działać jako dane wejściowe dla naszego pracownika, a następnie zbudować nasz WorkRequest z naszymi danymi wejściowymi i konkretnym ograniczeniem. Możesz odwołać się do przepisu biblioteki Jetpack WorkManager i na koniec umieścić w kolejce żądanie pracy. Na koniec możesz utworzyć zlecenie pracy jako jednorazowe lub okresowe i upewnić się, że poradzisz sobie z wszelkimi przypadkami brzegowymi, takimi jak anulowanie pracy.

Jak to działa…

W Firebase JobDispatcher, JobService.onStartJob(), która jest funkcją w JobSccheduler, i startWork() są wywoływane w głównym wątku. Dla porównania, w WorkManager ListenableWorker jest podstawową jednostką pracy. W naszym przykładzie YourWorker implementuje ListenableWorker i zwraca instancję ListenableFuture, która pomaga w sygnalizowaniu zakończenia pracy. Możesz jednak wdrożyć strategię jednowątkową w zależności od potrzeb aplikacji. W Firebase FirebaseJobBuilder używa Job.Builder jako metadanych Jobs. Dla porównania WorkManager wykorzystuje WorkRequest do pełnienia podobnej roli. WorkManager zwykle inicjuje się, korzystając z ContentProvider.

Jak debugować WorkManager

Każda operacja wymagająca pracy w tle i czasami wykonywania połączeń sieciowych wymaga odpowiedniej obsługi wyjątków. Dzieje się tak dlatego, że nie chcesz, aby Twoi użytkownicy borykali się z problemami i brakiem obsługi wyjątków, które prześladowałyby Twój zespół lub Ciebie jako programistę. Dlatego przydatna będzie wiedza o tym, jak debugować WorkManager, ponieważ jest to jeden z tych problemów, które mogą trwać kilka dni, jeśli masz błąd. W tym przepisie przyjrzymy się, jak debugować WorkManager.

Przygotowywanie się

Aby zastosować się do tego przepisu, musisz ukończyć wszystkie poprzednie przepisy z tej części.

Jak to zrobić…

Może wystąpić problem polegający na tym, że WorkManager nie działa, jeśli nie jest zsynchronizowany. Postępuj zgodnie z tym przepisem, aby debugować WorkManager:

1. Aby móc skonfigurować debugowanie, musimy najpierw utworzyć niestandardową inicjalizację w naszym pliku AndroidManifest.xml, czyli wyłączając inicjator WorkManager:

..…
tools:node="remove"/>

2. Następnie ustalamy minimalny poziom logowania do debugowania w naszej klasie aplikacji:

class App() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setMinimumLoggingLevel(
android.util.Log.DEBUG)
.build()
}

Gdy to zrobisz, będziemy mogli łatwo zobaczyć logi z przedrostkiem WM- na naszym poziomie debugowania, co sprawi, że nasza praca będzie znacznie prostsza i voila, będziemy mogli przejść o krok bliżej do rozwiązania naszego problemu.

Jak to działa…

Czasami pomocne może być wykorzystanie pełnych dzienników WorkManager do wychwycenia wszelkich anomalii. Ponadto możesz włączyć rejestrowanie i użyć własnej, niestandardowej inicjalizacji. To właśnie robimy w pierwszym kroku naszego przepisu. Co więcej, gdy zadeklarujemy własną niestandardową konfigurację WorkManager, nasz WorkManager zostanie zainicjowany, gdy wywołamy WorkManager.getInstance(context), a nie naturalnie podczas uruchamiania aplikacji.

Testowanie implementacji Workera

Testowanie implementacji Workera ma kluczowe znaczenie, ponieważ pomaga upewnić się, że kod jest dobrze obsługiwany, a Twój zespół przestrzega odpowiednich wskazówek dotyczących pisania świetnego kodu. Będzie to test integracyjny, co oznacza, że dodamy nasz kod do folderu androidTest. W tym przepisie omówiono sposób dodawania testów dla pracownika.

Przygotowywanie się

Aby zastosować się do tego przepisu, musisz ukończyć wszystkie poprzednie przepisy z tej części.

Jak to zrobić…


Wykonaj poniższe kroki, aby rozpocząć testowanie WorkManagera. Przyjrzymy się przykładom w tym przepisie:

1. Najpierw musisz dodać zależność testową w pliku build.gradle:

androidTestImplementation("androidx.work:work-testing:$work_version")

W przypadku, gdy w przyszłości coś zmieni się w API, możesz skorzystać z wersji stabilnej, którą zawsze znajdziesz w dokumentacji, klikając ten link: https://developer.android.com/jetpack/androidx/ wydania/praca.

2. Będziemy musieli skonfigurować naszą funkcję @Before zgodnie z udostępnioną przez Google:

@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
@Before
fun setup() {
val context =
InstrumentationRegistry.getTargetContext()
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation
tests.
WorkManagerTestInitHelper.
initializeTestWorkManager(context, config)
}
}

3. Teraz, gdy mamy już skonfigurowanego menedżera WorkManager, możemy przystąpić do strukturyzacji naszego testu:class GetDataWorker(kontekst: Kontekst, parametry:

WorkerParameters) : Worker(context, parameters) {
override fun doWork(): Result {
return when(endpoint) {
0 -> Result.failure()
else -> Result.success(dataOutput)
}
}
}

4. Możesz łatwo przetestować i zweryfikować stany, postępując według poniższego przykładu:

@Test
@Throws(Exception::class)
fun testGetDataWorkerHasNoData() {

val workInfo =
workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state,
`is`(WorkInfo.State.FAILED))
}

Możesz dodać więcej testów, takich jak sprawdzanie, kiedy stan się powiedzie lub sprawdzanie początkowych opóźnień; możesz także pójść o krok dalej, przetestować ograniczenia i nie tylko.

Jak to działa…

Biblioteka, z której korzystamy, zapewnia doskonałe wsparcie przy testowaniu Workera. Na przykład za pośrednictwem biblioteki dostarczono nam WorkManagerTestInitHelper. Ponadto mamy SynchronousExecutor, który ułatwia nam pracę jako programistów, zapewniając łatwe synchroniczne pisanie testów. Rozwiązano także kwestię obsługi wielu wątków, zatrzasków i zamków. W naszym teścieGetDataWorkerHasNoData tworzymy żądanie, następnie kolejkujemy je i czekamy na wyniki. Później otrzymujemy informacje, a następnie stwierdzamy, że stan nie powiódł się, powinien zakończyć się niepowodzeniem. Możesz także przetestować, kiedy się powiedzie.

Pierwsze kroki z Paging

W przypadku programowania na Androida biblioteka Paging pomaga programistom ładować i wyświetlać strony danych z większego zestawu danych z pamięci lokalnej lub przez sieć. Może to być częsty przypadek, jeśli aplikacja ładuje znaczne ilości danych do odczytania przez innych. Dobrym przykładem jest na przykład Twitter; możesz zauważyć odświeżenie danych ze względu na wiele tweetów wysyłanych codziennie przez użytkowników. Dlatego w Modern Android Development (MAD) programiści Androida mogą chcieć zaimplementować bibliotekę Paging w swoich aplikacjach, aby pomóc im w takich przypadkach podczas ładowania danych. W tej części dowiesz się jak wykorzystać bibliotekę Paging w swoich projektach.

Implementacja biblioteki Jetpack Paging

Biblioteka Paging zawiera niesamowite funkcje dla programistów. Jeśli baza kodu jest ugruntowana i obszerna, istnieją inne niestandardowe sposoby opracowane przez programistów, które pomagają im efektywnie ładować dane. Godną uwagi zaletą stronicowania jest buforowanie danych strony w pamięci, co zapewnia, że aplikacja efektywnie wykorzystuje zasoby systemowe podczas pracy z już stronicowanymi danymi. Ponadto oferuje obsługę przepływów współprogramowych Kotlin i LiveData oraz ma wbudowaną deduplikację, która gwarantuje, że Twoja aplikacja efektywnie wykorzystuje przepustowość sieci i zasoby, co może pomóc w oszczędzaniu baterii. Wreszcie biblioteka Paging oferuje obsługę błędów, w tym podczas odświeżania i ponawiania danych.

Przygotowywanie się

W tym przepisie będziemy musieli stworzyć nowy projekt.

Jak to zrobić…

Przejdźmy dalej i utwórzmy nowy pusty projekt Compose i nazwijmy go PagingJetpackExample. W naszym przykładowym projekcie będziemy używać darmowego NewsApi do wyświetlania aktualności naszym użytkownikom. Aby rozpocząć, kliknij ten link pod adresem https://newsapi.org/docs/get-started. Upewnij się także, że masz API dla projektu, ponieważ jest to wymagane w przypadku tego przepisu. Aby rozpocząć, wykonaj następujące kroki:

1. Przejdźmy dalej i dodajmy następujące wymagane zależności. Ponadto, ponieważ będziemy wykonywać połączenie sieciowe, musimy dodać bibliotekę, aby to obsłużyć. Jeśli chodzi o prawidłową wersję, sprawdź sekcję Wymagania techniczne, aby zapoznać się z kodem i poprawną wersją. Dostarczymy wersję 2.x.x, abyś mógł sprawdzić kompatybilność, jeśli aktualizujesz lub masz już Retrofit w swoim projekcie oraz Coil, który jest szybką, lekką i elastyczną biblioteką do ładowania obrazów. Jest przeznaczony aby uprościć proces ładowania obrazów z różnych źródeł (takich jak sieć, lokalna pamięć masowa lub dostawcy treści) i wyświetlania ich w ImageView lub innych komponentach interfejsu użytkownika związanych z obrazami:

//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.x.x'
implementation 'com.squareup.retrofit2:converter-gson:2.x.x'
//Coil you can also use Glide in this case
implementation 'com.google.accompanist:accompanist-coil:0.x.x'
//Paging 3.0
implementation 'Androidx.Paging:Paging-compose:1.x.x'

2. Po zsynchronizowaniu i przygotowaniu projektu usuń dołączoną do projektu funkcję komponowania powitania. Powinieneś mieć tylko swój motyw, a powierzchnia powinna być pusta.

3. Ponadto, korzystając z API, programiści często zapominają o dodaniu uprawnienia Android.permission. INTERNET w manifeście, więc zróbmy to teraz, zanim o tym zapomnimy:



4. Teraz utwórz pakiet i nazwij go danymi; dodamy do tego pakietu pliki naszego modelu i usługi. Ponadto zapoznaj się z sekcją Dokumentacja API wiadomości, aby zrozumieć, jak działa interfejs API:

data class NewsArticle(
val author: String,
val content: String,val title: String…)

5. Stwórzmy teraz naszą klasę danych NewsArticleResponse, którą zaimplementujemy w naszym interfejsie NewsApiService. Nasz typ wywołania API to @GET(), co oznacza dokładnie "otrzymać". Bardziej szczegółowe wyjaśnienie metody GET znajduje się w sekcji Jak to działa. Nasze wywołanie ma na celu zwrócenie obiektu wywołania zawierającego dane w postaci klasy danych NewsArticleResponse:

data class NewsArticleResponse(
val articles: List,
val status: String,
val totalResults: Int
)
interface NewsApiService{
@GET("everything?q=apple&sortBy=popularity&apiKey=
${YOURAPIKEY}&pageSize=20")
suspend fun getNews(
@Query("page") page: Int
): NewsArticleResponse
}

6. Utwórz kolejną klasę o nazwie NewsArticlePagingSource(); nasza klasa użyje NewsApiService jako parametru wejściowego. Udostępniając duże zbiory danych za pośrednictwem interfejsów API, musimy zapewnić mechanizm stronicowania listy zasobów. Aby to zaimplementować musimy przekazać typ klucza Paging oraz typ danych do załadowania, czyli w naszym przypadku NewsArticle:

class NewsArticlePagingSource(
private val newsApiService: NewsApiService,
): PagingSource() {

}

7. Na koniec zastąpmy funkcję getRefreshKey() udostępnianą przez funkcje zawieszające PagingSource i Load(). Funkcje zawieszenia loading() i PagingSource omówimy szczegółowo w przepisie dotyczącym ładowania i wyświetlania danych stronicowanych:

class NewsArticlePagingSource(
private val newsApiService: NewsApiService,
) : PagingSource() {
override fun getRefreshKey(state: PagingState NewsArticle>): Int? {
return state.anchorPosition?.let {
anchorPosition ->
state.closestPageToPosition(
anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(
anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params:
LoadParams): LoadResult {
return try {
val page = params.key ?: 1
val response = newsApiService.getNews(
page = page)
LoadResult.Page(
data = response.articles,
prevKey = if (page == 1) null else
page.minus(1),
nextKey = if
(response.articles.isEmpty()) null
else page.plus(1),
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}

8. Teraz stwórzmy nasze repozytorium; repozytorium to klasa izolująca źródła danych, takie jak usługa internetowa lub baza danych Room, od reszty aplikacji. Ponieważ nie mamy bazy danych Room, będziemy pracować z danymi z serwisu internetowego:

class NewsArticleRepository @Inject constructor(
private val newsApiService: NewsApiService
) {
fun getNewsArticle() = Pager(
config = PagingConfig(
pageSize = 20,
),
PagingSourceFactory = {
NewsArticlePagingSource(newsApiService)
}
).flow
}

9. W naszym projekcie użyjemy Hilt do wstrzykiwania zależności i zbudujemy wymagane moduły, które zostaną dostarczone. :

@Module
@InstallIn(SingletonComponent::class)
class RetrofitModule{
@Singleton
@Provides
fun provideRetrofitInstance(): NewsApiService =
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(
GsonConverterFactory.create())
.build()
.create(NewsApiService::class.java)
}

10. Wreszcie, po zaimplementowaniu naszego źródła PagingSource, możemy przystąpić do tworzenia Pagera, który zazwyczaj odwołuje się do ViewPager w naszym ViewModelu i określić rozmiar naszej strony. Może to zależeć od potrzeb i preferencji projektu. Co więcej, korzystając z Paging 3.0, nie musimy indywidualnie przetwarzać ani konwertować żadnych danych, aby przetrwać zmiany konfiguracji ekranu, ponieważ dzieje się to za nas automatycznie. Możemy po prostu buforować nasz wynik API za pomocą cachedIn(viewModelScope). Ponadto, aby powiadomić o wszelkich zmianach w PagingData, możesz obsłużyć stan ładowania za pomocą wywołania zwrotnego CombinedLoadState:

@HiltViewModel
class NewsViewModel @Inject constructor(
private val repository: NewsArticleRepository,
) : ViewModel() {
fun getNewsArticle():
Flow> =
repository.getNewsArticle().cachedIn(
viewModelScope)
}

11. Na koniec, po uruchomieniu aplikacji, powinieneś zobaczyć ekran pokazujący nazwisko autora, zdjęcie i treść. Zawijamy również treść, ponieważ ten przykład służy wyłącznie celom edukacyjnym; możesz potraktować to jako wyzwanie ulepszenia interfejsu użytkownika i wyświetlenia większej liczby szczegółów

Jak to działa…

W przypadku programowania na Androida żądanie modernizacji zazwyczaj odnosi się do żądania sieciowego wykonanego przy użyciu biblioteki Retrofit, popularnej biblioteki klienta HTTP dla Androida. Oto kilka typowych typów żądań modernizacji i ich zastosowania:

•  GET: To żądanie służy do pobrania danych z serwera. Jest to najpopularniejszy typ żądania używany w aplikacjach na Androida i często służy do pobierania danych w celu wypełnienia elementu interfejsu użytkownika, takiego jak lista lub siatka.
•  POST: To żądanie służy do przesłania danych do serwera. Jest powszechnie używany do tworzenia nowych zasobów na serwerze, takich jak nowe konto użytkownika lub nowy post.
•  PUT: To żądanie służy do aktualizacji istniejącego zasobu na serwerze. Jest powszechnie używany do aktualizacji informacji o koncie użytkownika lub modyfikacji istniejącego wpisu.
•  DELETE: To żądanie służy do usunięcia zasobu na serwerze. Jest powszechnie używany do usuwania konta użytkownika lub usuwania wpisu.
•  PATCH: To żądanie częściowo aktualizuje istniejący zasób na serwerze. Jest powszechnie używany, gdy należy zaktualizować tylko niewielką część zasobu, zamiast aktualizować cały zasób za pomocą żądania PUT.

Tworząc żądania modernizacji, programiści zazwyczaj definiują interfejs opisujący punkt końcowy i parametry żądania. Następnie Retrofit generuje implementację klienta dla tego interfejsu, której można używać do wykonywania rzeczywistych połączeń sieciowych. Korzystając z Retrofit, programiści mogą wyodrębnić wiele szczegółów niskiego poziomu żądań sieciowych, dzięki czemu komunikacja z serwerem z poziomu aplikacji na Androida jest łatwiejsza i wydajniejsza. Przykłady dotyczące modernizacji można znaleźć pod następującym linkiem https://square.github.io/retrofit/. Biblioteka Paging zapewnia zgodność z zalecanymi wzorcami architektury systemu Android. Ponadto jego komponentami są warstwy Repozytorium, ViewModel i UI. Poniższy diagram pokazuje jak komponenty stronicowania działają w każdej warstwie i jak współdziałają, aby ładować i wyświetlać dane stronicowane:



Komponent Źródło stronicowania jest głównym komponentem warstwy Repozytorium, jak pokazano na rysunku. Obiekt zwykle deklaruje źródło każdego fragmentu danych, a także obsługuje sposób ponawiania próby danych z tego źródła. Jeśli zauważyłeś, właśnie to zrobiliśmy w naszym przykładzie:

class NewsArticleRepository @Inject constructor(
private val newsApiService: NewsApiService
) { …

Tworzymy nasz obiekt Retrofit builder() zawierający nasz bazowy URL API, który zdefiniowaliśmy w klasie Constant, const val BASE_URL = "https://newsapi.org/v2/", a do konwersji używamy konwertera Gson naszą odpowiedź API JSON. Następnie deklarujemy zmienną apiService, której użyjemy do połączenia obiektu Retrofit builder() z naszym interfejsem i uzupełnienia naszego modułu retrofit.

Ważna uwaga

Każdemu, kto korzysta z Biblioteki Paging, zaleca się migrację do wersji Paging 3 ze względu na jej ulepszenia i ponieważ niektóre funkcje są trudne w obsłudze przy użyciu Paging 2.

Zarządzanie stanami obecnymi i ładującymi

Biblioteka stronicowania oferuje użytkownikom informacje o stanie ładowania za pośrednictwem obiektu stanu ładowania, który może mieć różne formy w zależności od bieżącego stanu ładowania. Na przykład, jeśli masz aktywne obciążenie, wówczas stan będzie miał postać LoadState.Loading. Jeśli występuje stan błędu, będzie to stan LoadState.Error; i w końcu może być brak aktywnej operacji ładowania, a ten stan nosi nazwę LoadState.NotLoading. W tym przepisie zbadamy różne stany i zrozumiemy je; pokazany tutaj przykład można również znaleźć pod następującym linkiem: https://developer.android.com/topic/libraries/architecture/paging/load-state. W tym przykładzie zakładamy, że Twój projekt używa starszego kodu, który wykorzystuje XML w systemie widoków.

Przygotowywanie się

Aby zastosować się do tego przepisu, musisz uzupełnić kod z poprzedniego przepisu. Możesz to również pominąć, jeśli nie jest to wymagane w Twoim projekcie.
Jak to zrobić…

W tym przepisie nie będziemy tworzyć nowego projektu, ale raczej przyjrzymy się krok po kroku, w jaki sposób możemy uzyskać dostęp do stanu ładowania za pomocą słuchacza lub zaprezentować stan ładowania za pomocą adaptera. Postępuj zgodnie z nimi kroki, aby rozpocząć:

1. Jeśli chcesz uzyskać dostęp do stanu, przekaż tę informację do swojego interfejsu użytkownika. Możesz łatwo użyć strumienia loadingStateFlow dostarczonej funkcji addLoadStateListener przez PagingDataAdapter:

lifecycleScope.launch {
thePagingAdapter.loadStateFlow.collectLatest {
loadStates ->
progressBar.isVisible = loadStates.refresh is LoadState.Loading
retry.isVisible = loadState.refresh !is
LoadState.Loading
errorMessage.isVisible = loadState.refresh is
LoadState.Error
}
}

2. W naszym przykładzie nie będziemy się zajmować funkcją addLoadStateListener, ponieważ jest ona używana z klasą adaptera, a w przypadku nowej Jetpack Compose jest ona prawie wykonywana, ponieważ istnieje większy nacisk na korzystanie z aplikacji opartych na interfejsie użytkownika Jetpack Compose .

3. Filtrowanie pary stanu obciążenia może mieć sens w zależności od konkretnego zdarzenia aplikacji. Dzięki temu interfejs użytkownika aplikacji zostanie zaktualizowany we właściwym czasie, co pozwoli uniknąć problemów. Dlatego też, używając współprogramów, czekamy, aż nasz stan ładowania odświeżania zostanie zaktualizowany:

lifecycleScope.launchWhenCreated{
yourAdapter.loadStateFlow
.distinctUntilChangedBy { it.refresh }
.filter { it.refresh to LoadState.NotLoading }
.collect { bind.list.scrollToPosition(0) }
}

Jak to działa…

Aktualizacje pobierane z funkcji LoadStateFlow i addLoadStateListener() mają gwarancję, że będą one synchroniczne i w razie potrzeby aktualizują interfejs użytkownika. Oznacza to po prostu, że w bibliotece Paging 3 dla systemu Android LoadState.Error jest stanem wskazującym, że wystąpił błąd podczas ładowania danych ze źródła PagingSource. W bibliotece Paging 3 dla systemu Android LoadState.NotLoading to stan wskazujący, że PagingDataAdapter nie ładuje obecnie żadnych danych i że wszystkie dostępne dane zostały załadowane. Gdy element PagingDataAdapter jest tworzony po raz pierwszy, rozpoczyna się od stanu LoadState.NotLoading. Oznacza to, że żadne dane nie zostały jeszcze załadowane i adapter czeka na pierwsze ładowanie. Po pierwszym załadowaniu adapter może przejść do innego stanu obciążenia w zależności od aktualnego stanu procesu ładowania danych. Jednak po załadowaniu wszystkich dostępnych danych adapter powróci do stanu LoadState.NotLoading. Za pomocą LoadState.NotLoading można poinformować interfejs użytkownika, że proces ładowania danych został zakończony i że żadne dalsze dane nie zostaną załadowane, chyba że użytkownik zainicjuje odświeżenie lub inną akcję. Aby obsłużyć ten stan, możesz zarejestrować odbiornik zmian w LoadState w plikuPagingDataAdapter i odpowiednio zaktualizuj interfejs użytkownika. Na przykład możesz wyświetlić użytkownikowi wiadomość wskazującą, że wszystkie dane zostały załadowane lub wyłączyć przyciski lub gesty "ładuj więcej".

Implementowanie niestandardowej paginacji w Jetpack Compose

Biblioteka stronicowania ma niesamowite funkcje dla programistów, ale czasami napotykasz wyzwania i jesteś zmuszony stworzyć niestandardową paginację. Na początku rozdziału mówiliśmy o złożonych bazach kodu posiadających lub tworzących paginację. W tym przepisie przyjrzymy się, jak możemy to osiągnąć za pomocą prostego przykładu listy i jak możesz wykorzystać ten przykład do stworzenia niestandardowej paginacji w swojej aplikacji.

Przygotowywanie się

W tym przepisie będziemy musieli utworzyć nowy projekt i nazwać go CustomPagingExample.

Jak to zrobić…

W naszym przykładowym projekcie spróbujemy utworzyć kartę profilu ucznia i użyć niestandardowej paginacji, aby załadować profile do Jetpack Compose.

1. W przypadku tego przepisu dodajmy zależność cyklu życia-ViewModel, ponieważ będziemy jej potrzebować:

implementation "Androidx.lifecycle:lifecycle-viewmodelcompose: 2.x.x"

2. Stwórzmy nowy pakiet i nazwijmy go danymi. W naszym pakiecie danych dodamy pozycje, które będziemy wyświetlać na naszej karcie. Na razie wyświetlimy tylko imię i nazwisko ucznia, szkołę i kierunek:

data class StudentProfile(
val name: String,
val school: String,
val major: String
)

3. Teraz, gdy mamy już klasę danych, możemy przystąpić do budowy naszego repozytorium, a ponieważ w naszym przykładzie nie korzystamy z API, skorzystamy z naszego zdalnego źródła danych i będziemy mogli spróbować załadować, powiedzmy, 50 do 100 profili. Następnie w danych dodaj kolejną klasę i nazwij ją StudentRepository:

class StudentRepository {
private val ourDataSource = (1..100).map {
StudentProfile(
name = "Student $it",
school = "MIT $it",
major = "Computer Science $it"
)
}
suspend fun getStudents(page: Int, pageSize: Int):
Result> {
delay(timeMillis = 2000L) //the delay added is
just to mimic a network connection.
val start = page * pageSize
return if (start + pageSize <=
ourDataSource.size) {
Result.success(
ourDataSource.slice(start until start
+ pageSize)
)
} else Result.success(emptyList())
}
}

4. Skoro już stworzyliśmy nasze repozytorium, przejdźmy dalej i utwórzmy własną paginację. Zrobimy to tworząc nowy interfejs i nazywając go StudentPaginator:

interface StudentPaginator {
suspend fun loadNextStudent()
fun reset()
}

5. Ponieważ StudentPaginator jest interfejsem, musimy stworzyć klasę, która zaimplementuje dwie właśnie utworzone funkcje. Teraz przejdźmy dalej i utwórz StudentPaginatorImpl i zaimplementuj nasz interfejs:

class StudentPaginatorImpl(
) : StudentPaginator {
override suspend fun loadNextStudent() {
TODO("Not yet implemented")
}
override fun reset() {
TODO("Not yet implemented")
}
}

6. Następnie musisz popracować nad tym, co musisz zrobić w klasie implementacyjnej StudentPaginator. Na przykład w naszym konstruktorze będziemy musieli utworzyć klucz, który będzie nasłuchiwał obciążenia, żądania, błędu, sukcesu i następnego klucza, a następnie, za pomocą funkcji reset(), będziemy mogli zresetować naszą paginację. Możesz zobaczyć cały kod w sekcji Wymagania techniczne. Możesz także zauważyć, że wygląda podobnie do źródła stronicowania z pierwszego przepisu w tej części:

class StudentPaginatorImpl(
private val key: Key,
private inline val loadUpdated: (Boolean) -> Unit,
private inline val request: suspend (nextKey: Key)
->

) : StudentPaginator {
private var currentKey = key
private var stateRequesting = false
override suspend fun loadNextStudent() {
if (stateRequesting) {
return
}
stateRequesting = true …
}
override fun reset() {
currentKey = key
}

7. Stwórzmy nowy pakiet i nazwijmy go uistate. Wewnątrz uistate utworzymy nową klasę danych i nazwiemy ją UIState, aby pomóc nam w obsłudze stanu interfejsu użytkownika:

data class UIState(
val page: Int = 0,
val loading: Boolean = false,
val studentProfile: List =
emptyList(),
val error: String? = null,
val end: Boolean = false,
)

8. Teraz przejdźmy dalej i sfinalizujmy inicjację ViewModel w Kotlinie. Jest to blok, którego używamy do naszej inicjalizacji. Tworzymy również val ourPaginator, który deklarujemy klasie StudentPaginatorImpl i obsługujemy dane wejściowe potrzebne do naszego interfejsu użytkownika:

class StudentViewModel() : ViewModel() {
var state by mutableStateOf(UIState())
private val studentRepository =
StudentRepository()
init {
loadStudentProfile()
}
private val ourPaginator = StudentPaginatorImpl(
key = state.page,
loadUpdated = { state = state.copy(loading =
it) },
request = { studentRepository.getStudents(it,
24) },
nextKey = { state.page + 1 },
error = { state = state.copy(error =
it?.localizedMessage) },
success = { student, newKey ->
state = state.copy(
studentProfile = state.studentProfile
+ student,
page = newKey,
end = student.isEmpty()
)
}
)
fun loadStudentProfile(){
viewModelScope.launch {
ourPaginator.loadNextStudent()
}
}
}

9. Na koniec, w naszej klasie MainActivity, ładujemy teraz profil ucznia na naszą kartę i wyświetlamy go na ekranie, jak pokazano na rysunku 8.3. Ogromnym dodatkowym ćwiczeniem do wypróbowania jest użycie wstrzykiwania zależności w przykładowym projekcie w celu udoskonalenia umiejętności korzystania z Androida. Możesz skorzystać z rozdziału 3, Obsługa stanu interfejsu użytkownika w Jetpack Compose i używanie Hilt, aby dodać wstrzykiwanie zależności, a także spróbować napisać testy dla klasy ViewModel:



Na rysunku zobaczysz symbol postępu ładowania, gdy przewiniesz w dół do Studenta 4 itd., co może być świetne, gdy masz ogromne ilości danych:



Jak to działa…

Po otrzymaniu listy mogą wystąpić problemy, a powiadomienie pojedynczych pozycji może być trudne. Możesz jednak łatwo dokonać paginacji; w naszym projekcie symulujemy zdalne źródło danych, ale pamiętajmy, że w tym przykładzie można wykorzystać dowolne API. Naszym głównym celem jest klasa StudentPaginatorImpl - zauważysz, że przekazujemy klucz, wartość loadingUpdated i żądanie będące funkcją zawieszającą, która zwraca wynik z naszego typu Student; przekazujemy także klawisz next, który mówi nam, gdzie jesteśmy. Następnie, w przypadku błędu, mamy możliwy do wyrzucenia błąd i wartość zawieszenia, sukces, co daje nam wynik sukcesu:

class StudentPaginatorImpl(
private val key: Key,
private inline val loadUpdated: (Boolean) -> Unit,
private inline val request: suspend (nextKey: Key) ->
Result>,
private inline val nextKey: suspend (List) -> Key,
private inline val error: suspend (Throwable?) -> Unit,
private inline val success: suspend (items:
List, newKey: Key) -> Unit
) : StudentPaginator {

Kiedy więc nadpiszemy naszą funkcję z interfejsu LoadNextStudent(), najpierw sprawdzamy nasze bieżące żądanie stanu i zwracamy naszą wartość początkową jako fałszywą, ale aktualizujemy ją po sprawdzeniu statusu. Zapewniamy również, że zresetujemy klucz, ustawiając currentKey na nextKey.

currentKey = nextKey(studentProfiles)
succes(studentProfiles, currentKey)
loadingUpdated(false).

Ułatwia to dostosowywanie elementu w kolumnie LazyColumn i zapewnia świetne listy. Funkcja LoadStudentProfile() ma iewModelScope.launch {...}. Dla każdego ViewModel w naszej aplikacji zdefiniowany jest zakres ViewModel. Ponadto każda współprogram uruchomiona w tym zakresie jest automatycznie anulowana, jeśli ViewModel zostanie wyczyszczony.

Ładowanie i wyświetlanie stronicowanych danych

Podczas ładowania i wyświetlania danych stronicowanych należy wziąć pod uwagę istotne kroki. Ponadto biblioteka Paging zapewnia ogromne korzyści w zakresie ładowania i wyświetlania dużych, stronicowanych zestawów danych. Należy pamiętać o kilku krokach: najpierw zdefiniować źródło danych, w razie potrzeby skonfigurować strumienie w źródle stronicowania i nie tylko. W tym przepisie przyjrzymy się, jak działa ładowanie i wyświetlanie danych stronicowanych.

Jak to zrobić…

Aby móc postępować zgodnie z wyjaśnieniem tego przepisu, musisz ukończyć przepis dotyczący implementacji biblioteki stronicowania Jetpack:

1. Być może zauważyłeś w naszej pierwszej recepturze, że nadpisujemy metodę Load(), której używamy do wskazania sposobu pobierania danych stronicowanych z odpowiedniego źródła danych:

override suspend fun load(params: LoadParams):
LoadResult {
return try {
val page = params.key ?: 1
val response = newsApiService.getNews(page = page)
LoadResult.Page(
data = response.articles,
prevKey = if (page == 1) null else
page.minus(1),
nextKey = if (response.articles.isEmpty())
null else page.plus(1),
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

2. Rozpoczynamy odświeżanie na stronie 1, jeśli val page = params.key ?: 1 jest niezdefiniowane, gdy zastąpimy funkcję getRefreshKey(); staramy się znaleźć klucz strony najbliżej pozycji zakotwiczenia z naszego poprzedniego klucza lub następnego klucza. Musimy także mieć pewność, że zajmiemy się sprawami gdzie możemy mieć pewne wartości zerowe:

override fun getRefreshKey(state: PagingState NewsArticle>): Int? {
return state.anchorPosition?.let { anchorPosition
->
state.closestPageToPosition(anchorPosition)?
.prevKey?.plus(1)
?: state.closestPageToPosition(
anchorPosition)?.nextKey?.minus(1)
}
}}

Jak to działa…

Korzystając z biblioteki Paging, możesz określić położenie pierwszego elementu, który ma zostać wyświetlony na ekranie, za pomocą parametru kotwicyPosition. Ponadto kotwicaPosition jest opcjonalnym parametrem, który można przekazać do funkcji komponowalnej PagingItems, która służy do wyświetlania danych stronicowanych. Parametr kotwicyPosition służy do określenia pozycji pierwszego elementu wyświetlanego na ekranie podczas pierwszego renderowania elementu składowego. Obiekt LoadParams zawiera informację o operacji ładowania, która ma zostać wykonana. Ponadto wie, jaki klucz ma zostać załadowany i ile elementów ma być wyświetlonych w interfejsie użytkownika. Ponadto, aby lepiej zrozumieć, w jaki sposób funkcja Load() otrzymuje klucz dla każdego konkretnego obciążenia i aktualizuje go, przejrzyj poniższy diagram:



Zrozumienie sposobu przekształcania strumieni danych

Pisząc dowolny kod związany ze stronicowaniem, musisz zrozumieć, w jaki sposób możesz przekształcić strumień danych podczas ładowania go do użytkowników. Na przykład może być konieczne przefiltrowanie listy elementów lub nawet przekonwertowanie elementów na inny typ, zanim będzie można przesłać dane do interfejsu użytkownika. Dlatego upewnienie się, że zastosujesz transformację bezpośrednio do danych strumienia, pozwala zachować czyste oddzielenie repozytorium i logiki interfejsu użytkownika. W tym przepisie postaramy się zrozumieć, w jaki sposób możemy przekształcać strumienie danych.

Przygotowywanie się

Aby śledzić dalej, musisz znać podstawowe zastosowania biblioteki Paging; dlatego upewnij się, że przeczytałeś poprzednie przepisy w tej części.

Jak to zrobić…

W tym przepisie wykonamy następujące kroki:

1. Przyjrzyj się, jak możemy zastosować zasadniczą transformację.
2. Konwertuj i filtruj dane.
3. Obsługuj separatory w interfejsie użytkownika i konwertuj model interfejsu użytkownika. Przepis będzie dla Ciebie pomocny, jeśli korzystasz już ze stronicowania w swojej aplikacji.
4. Najpierw musimy umieścić transformację wewnątrz mapy{PagingData ->}. Mapa w Kotlinie stosuje podaną funkcję lambda do każdego elementu i zwraca listę wyników lambda:

yourPager.flow
.map { PagingData ->
// here is where the transformations are
applied to the items in the paged data.
}

5. Po drugie, gdy chcemy przekonwertować dane lub filtr, gdy już uzyskamy dostęp do naszego obiektu PagingData, możemy ponownie użyć funkcji map() dla każdego elementu listy stronicowanej z osobna. Typowym przypadkiem użycia jest mapowanie obiektu warstwy bazy danych lub sieci na obiekt, który może zostać użyty w warstwie interfejsu użytkownika:

yourPager.flow
.map { PagingData ->
PagingData.map { sports -> SportsModel(sports)
}
}

6. Będziemy musieli umieścić operację filtrowania wewnątrz mapy, ponieważ filtr dotyczy obiektu PagingData. Następnie, gdy dane zostaną odfiltrowane z naszych danych PagingData, nowa instancja zostanie przeniesiona do warstwy interfejsu użytkownika i wyświetlona:

yourPager.flow
.map { PagingData ->
PagingData.filter { sports ->
!sports.displayInUi }
}

7. Na koniec, podczas obsługi separatorów w interfejsie użytkownika lub konwertowania modelu interfejsu użytkownika, najważniejsze kroki obejmują wykonanie następujących czynności:

I. Konwertuj modele interfejsu użytkownika, aby uwzględnić elementy separatora.
II. Przekształcaj dane dynamicznie i dodaj separatory pomiędzy prezentacją i ładowaniem danych.
III. Zaktualizuj interfejs użytkownika, aby lepiej obsługiwał elementy separatora.

Jak to działa…

Dane PagingData są hermetyzowane w strumieniu reaktywnym; oznacza to, że przed załadowaniem danych i wyświetleniem ich użytkownikom można stopniowo zastosować transformację do danych. Przekształcanie strumieni danych może mieć kluczowe znaczenie w przypadku złożonej aplikacji, a zajęcie się tą sytuacją z wyprzedzeniem może pomóc zapewnić lepsze skalowanie aplikacji i zminimalizować złożoność wzrostu danych.

Migracja do Paging 3 i zrozumienie korzyści

Być może używasz starej wersji Paging, w tym przypadku Paging 2 lub 1, i może być konieczna migracja, aby móc korzystać z korzyści, jakie oferuje Paging 3. Paging 3 oferuje ulepszoną funkcjonalność i gwarantuje, że rozwiąże najczęstsze wyzwania, jakie napotykają ludzie korzystający z Paging 2. W tym przepisie przyjrzymy się, jak możesz przeprowadzić migrację do najnowszej zalecanej biblioteki Paging.

Przygotowywanie się

Jeśli Twoja aplikacja korzysta już z Paging 3, możesz pominąć ten przepis; ten przewodnik dotyczący migracji krok po kroku jest przeznaczony dla użytkowników korzystających obecnie ze starszych wersji biblioteki Paging.

Jak to zrobić…

Migracja ze starych wersji biblioteki Paging może wydawać się skomplikowana, ponieważ każda aplikacja jest wyjątkowa, a złożoność może się różnić. Jednakże w naszym przykładzie poruszymy kwestię migracji niskiego poziomu, ponieważ nasza przykładowa aplikacja nie wymaga żadnej migracji. Aby przeprowadzić migrację ze starych bibliotek stronicowania, wykonaj następujące kroki:

1. Pierwszym krokiem jest wymiana kluczy odświeżania, a to dlatego, że musimy zdefiniować, w jaki sposób odświeżanie będzie wznawiane od połowy ładowania danych. Zrobimy to, najpierw implementując funkcję getRefreshKey(), która odwzorowuje poprawny klucz początkowy, używając PagingState.anchorPosition jako ostatniego indeksu:

override fun getRefreshKey(PagingState: PagingState): String? {
return PagingState.anchorPosition?.let { position
->
PagingState.getClosestItemToPosition(
position)?.id
}
}

2. Następnie musimy upewnić się, że zastąpiliśmy źródło danych pozycyjnych:

override fun getRefreshKey(PagingState: PagingState): Int? {
return PagingState.anchorPosition
}

3. Jeśli używasz starej biblioteki stronicowania, dane stronicowane wykorzystują DataSource.map(), mapByPage, Factory.map() i Factory.mapByPage. Jednak w Paging 3 wszystkie one są stosowane jako operatory do PagingData.

4. Na koniec, aby mieć pewność, że dokonasz migracji z PageList, która znajduje się w Stronicowaniu 2, będziesz musiał przeprowadzić migrację do PagingData. Najbardziej zauważalną zmianą jest to, że PagedList.Config nie jest plikiem PagingConfig. Ponadto Pager() udostępnia obserwowalny Flow ze swoim przepływem:

val yourFlow = Pager(
PagingConfig(pageSize = 24)
) {
YourPagingSource(yourBackend, yourQuery)
}.flow
.cachedIn(viewModelScope)
Jak to działa…

Aby mieć pewność, że migracja zakończy się pomyślnie, musisz migrować wszystkie istotne komponenty z Stronicowania 2. Obejmuje to klasy DataSource, PagedList i PagedListAdapter, jeśli aplikacja ich używa. Co więcej, niektóre komponenty Paging 3 dobrze współpracują z innymi wersjami, co oznacza po prostu, że są kompatybilne wstecz. Najbardziej zauważalną zmianą w PagingSource w Paging 3 jest to, że łączy on wszystkie funkcje ładowania w jedną, teraz nazywaną ładowaniem() w PagingSource. Zapewnia to brak nadmiarowości w kodzie, ponieważ logika ładowania jest często identyczna jak w starym interfejsie API. Ponadto parametry funkcji ładowania w Paging 3 korzystają teraz z zapieczętowanej klasy LoadParams, która ma podklasy dla każdego typu ładowania. W PagedList, który jest używany w Paging 2, podczas migracji możesz używać PagingData i Pagera. Kiedy zaczniesz używać PagingData z Paging 3, powinieneś upewnić się, że konfiguracja została przeniesiona ze starego PagedList.Config do PagingConfig.

Pisanie testów dla źródła stronicowania

Pisanie testów dla swoich wdrożeń jest kluczowe. W tym przepisie napiszemy testy jednostkowe dla naszej implementacji PagingSource, aby przetestować naszą logikę. Niektóre testy, które warto napisać, obejmują sprawdzanie, kiedy wystąpi błąd ładowania wiadomości. Możemy również przetestować stan powodzenia i nie tylko. Możesz skorzystać ze wzorca, aby napisać testy dla swojego projektu lub przypadku użycia.

Przygotowywanie się

Aby postępować zgodnie z tym przepisem krok po kroku, musisz postępować zgodnie z przepisem Implementowanie biblioteki Jetpack Paging i musisz skorzystać z projektu PagingJetpackExample.

Jak to zrobić…

Otwórz PagingJetpackExample i postępuj zgodnie z tym projektem, aby dodać testy jednostkowe:

1. Dodaj następujące biblioteki testowe do swojej aplikacji build.gradle:

testImplementation 'org.assertj:assertj-core:3.x.x'
testImplementation "org.mockito:mockito-core:3.x.x"
testImplementation 'Androidx.arch.core:core-testing:2.x.x'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutinestest: 1.x.x'

2. Po dodaniu zależności utwórz nowy pakiet i nazwij go danymi w pakiecie testowym w strukturze projektu. Jeśli potrzebujesz pomocy w znalezieniu folderu, możesz odwołać się do przepisu Zrozumienie struktury projektu Androida w Rozdziale 1, Pierwsze kroki z umiejętnościami nowoczesnego programowania Androida.

3. Utwórz klasę testową i nazwij ją NewsArticlePagingSourceTest.

4. Wewnątrz klasy dodajmy Mock, aby wyśmiewać nasz interfejs ApiService i utworzyć lateinit var newsApiService, którą zainicjujemy w naszym kroku @Before:

@Mock
private lateinit var newsApiService: NewsApiService
lateinit var newsPagingSource: NewsArticlePagingSource

5. Teraz przejdźmy dalej i utwórzmy nasz @Before, abyśmy mogli uruchomić nasze oroutineDispatchers, których używają wszystkie standardowe kreatory, takie jak async, i uruchommy również nasz krok @Before:

@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
newsPagingSource = NewsArticlePagingSource(newsApiService)
}

6. Pierwszym testem, który będziemy musieli napisać, będzie sprawdzenie, kiedy nastąpi awaria. Dlatego przejdźmy dalej i skonfigurujmy nasz test. Odpowiedź 403 to zabroniony kod statusu wskazujący, że serwer zrozumiał Twoje żądanie, ale go nie autoryzował:

@Test
fun `news article Paging Source load failure http error`() =
runBlockingTest {
//setup
val error = HttpException(
Response.error(
403, "some content".toResponseBody(
"plain/text".toMediaTypeOrNull())
)
) …

7. Aby kontynuować nasz test, będziemy musieli użyć Mockito.doThrow(error):

Mockito.doThrow(error)
.`when`(newsApiService)
.getNews(

1
)…

8. Na koniec wyzwalamy PagingSource.LoadResult.Error i przekazujemy typ, a następnie potwierdzamy:

/assert
assertEquals(
expectedResult, newsPagingSource.load(
PagingSource.LoadParams.Refresh(
key = null,
loadSize = 1,
placeholdersEnabled = false
)
)
)

9. Możesz dodać jeszcze dwa dodatkowe testy, a następnie dodać łezDown, aby oczyścić współprogramy:

@After
fun tearDown() {
testDispatcher.cleanupTestCoroutines()
}

Jak to działa…

Używamy Mock w testach jednostkowych, a ogólna koncepcja opiera się na założeniu, że testowane obiekty mogą mieć zależności od innych złożonych obiektów. Na tej podstawie znacznie łatwiej jest wyizolować zachowanie pożądanego obiektu, kpiąc z obiektu, co zapewnia, że zachowuje się tak samo jak nasz prawdziwy obiekt i ułatwia testowanie:

@Mock
private lateinit var newsApiService: NewsApiService

Nasz lateinit var newsPagingSource: NewsArticlePagingSource jest używany do późnej inicjalizacji i inicjujemy go w naszej funkcji @Before.

Budowanie na duże ekrany

Wszyscy możemy się teraz zgodzić, że żyjemy w świecie składanych telefonów, technologii, której nigdy się nie spodziewaliśmy ze względu na rosnący popyt i popularność. Gdyby dziesięć lat temu ktoś powiedział programiście, że będziemy mieć składane telefony, nikt by w to nie uwierzył ze względu na niejednoznaczność złożoności ekranu i przekazywania informacji. Jednak teraz urządzenia są tu z nami. A ponieważ niektóre z tych urządzeń działają w systemie operacyjnym Android, ważne jest, aby wiedzieć, w jaki sposób programiści będą tworzyć nasze aplikacje, aby zapewnić możliwość składania, a także liczbę tabletów z Androidem, które obecnie widzimy na rynku. Obsługa dużych ekranów wydaje się teraz obowiązkowa i w tej części przyjrzymy się obsłudze dużych ekranów w nowym nowoczesnym rozwoju Androida.

Tworzenie układów adaptacyjnych w Modern Android Development

Budując interfejs użytkownika dla swojej aplikacji w Modern Android Development, można śmiało powiedzieć, że powinieneś rozważyć upewnienie się, że aplikacja reaguje na różne rozmiary, orientacje i kształty ekranu. Wreszcie programiści mogą teraz usunąć blokadę w trybie portretowym. W tym przepisie wykorzystamy pomysły, których nauczyliśmy się z poprzednich przepisów i stworzymy adaptacyjną aplikację dla różnych rozmiarów i orientacji ekranu.

Przygotowywanie się

Będziemy używać aplikacji miast do tworzenia profilu podróżnika, a nasz ekran powinien móc się zmieniać w zależności od różnych rozmiarów ekranu i obsługiwać składane urządzenia i tablety. Aby otrzymać cały kod, sprawdź sekcję Wymagania techniczne.

Jak to zrobić…

Na potrzeby tego przepisu utworzymy nowy projekt i tym razem zamiast wybierać pusty szablon działania tworzenia, wybierzemy puste działanie tworzenia (materiał 3). Material 3 ma na celu poprawę wyglądu i działania naszej aplikacji na Androidzie. Zawiera zaktualizowane motywy, komponenty i wspaniałe funkcje, takie jak personalizacja Material You za pomocą dynamicznych kolorów:

1. Zacznijmy od stworzenia projektu Pusta aktywność tworzenia treści (Material3) i nazwijmy go Traveller; pamiętaj, że możesz nazwać swój projekt jak chcesz.



Złożone aplikacje wykorzystują responsywny interfejs użytkownika i w większości przypadków przydaje się wybór odpowiedniego typu nawigacji dla aplikacji. Biblioteka materiałów oferuje programistom komponenty nawigacyjne, takie jak dolna nawigacja, szuflada nawigacji i szyna nawigacyjna. Kod startowy można uzyskać w sekcji Wymagania techniczne.

2. Dodaj następującą zależność i sprawdź projekt pod kątem poprawnego numeru wersji 1.1.0: implementation "androidx.compose.material3:material3-windowsize- class:1.1.0"

3. Dbając o to, aby nasz kod uwzględniał możliwości adaptacji, musimy pamiętać, że responsywny interfejs użytkownika zachowuje dane, gdy telefon jest obracany, składany lub rozkładany. Najważniejszą częścią jest upewnienie się, że radzimy sobie z postawą. Stworzymy funkcję cityPosture, która jako dane wejściowe pobiera FoldingFeature i zwraca wartość logiczną:

@OptIn(ExperimentalContracts::class)
fun cityPosture(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state ==
FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation ==
FoldingFeature.Orientation.VERTICAL
}

Obsługujemy stan w oparciu o trzy podane stany. Adnotujemy go również klasą eksperymentalną, ponieważ ten interfejs API jest nadal eksperymentalny, co oznacza, że może ulec zmianie w przyszłości i nie jest zbyt stabilny.

4. Następnie musimy omówić isSeparating, który nasłuchuje całkowicie otwartego FLAT i isSeparating Boolean, który oblicza, czy należy wziąć pod uwagę FoldingFeature, dzieląc okno na wiele fizycznych obszarów, które użytkownicy mogą postrzegać jako logicznie oddzielne:

@OptIn(ExperimentalContracts::class)
fun separating(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state ==
FoldingFeature.State.FLAT &&
foldFeature.isSeparating
}

5. Stworzymy także zapieczętowany interfejs DevicePosture. Jest to komponent interfejsu użytkownika Jetpack Compose, który umożliwia wykrycie postawy lub orientacji urządzenia, na przykład tego, czy urządzenie znajduje się w trybie pionowym, czy poziomym:

sealed interface DevicePosture {
object NormalPosture : DevicePosture
data class CityPosture(
val hingePosition: Rect
) : DevicePosture
data class Separating(
val hingePosition: Rect,
var orientation: FoldingFeature.Orientation
) : DevicePosture
}

6. W naszym MainActivity musimy teraz upewnić się, że obliczyliśmy rozmiar okna:

val windowSize = obliczWindowSizeClass(aktywność = to)

7. Następnie upewnimy się, że dobrze poradzimy sobie ze wszystkimi rozmiarami, tworząc postureStateFlow, który będzie nasłuchiwał naszego DevicePosture i reagował, gdy cityPosture jest złożone, rozłożone lub normalne:

val postureStateFlow = WindowInfoTracker.getOrCreate(this).
windowLayoutInfo(this)

when {
cityPosture(foldingFeature) ->
DevicePosture.CityPosture(foldingFeature.bounds)
separating(foldingFeature) ->
DevicePosture.Separating(foldingFeature.bounds,
foldingFeature.orientation)
else -> DevicePosture.NormalPosture
}
}

)

8. Teraz musimy przygotować składane wirtualne urządzenie testowe. Jeśli potrzebujesz odświeżenia wiedzy, możesz powtórzyć kroki z pierwszego rozdziału dotyczącego tworzenia urządzenia wirtualnego; w przeciwnym razie powinieneś stworzyć składane urządzenie. Strzałka na rysunku pokazuje, w jaki sposób będziesz sterować składanymi ekranami.



9. Wreszcie, kiedy uruchomisz aplikację, zobaczysz, że zmienia się ona w zależności od stanu złożonego i rozłożonego, działając dobrze. Rysunek pokazuje, kiedy stan jest złożony.



10. Na rysunku widać, że zmieniliśmy dolną nawigację i teraz nasza szuflada nawigacji jest ustawiona z boku, aby nawigacja była prostsza. Należy przyznać, że projekt ten jest obszerny, dlatego nie jesteśmy w stanie omówić wszystkich części kodu. Upewnij się, że wykorzystałeś koncepcje tworzenia tekstu poznane w poprzednim rozdziale tej sekcji.



Pamiętaj, że po rozwinięciu szuflady nawigacji możesz zobaczyć wszystkie elementy i nawigacja powinna być łatwa



Na panelu bocznym możesz także zobaczyć bardziej opisowy widok interfejsu użytkownika, co pomaga w debugowaniu problemów.



Jak to działa…

Dolną nawigację omówiliśmy w Rozdziale 4, Nawigacja w rozwoju współczesnego Androida. Jednak w tym rozdziale użyjemy go, aby pokazać, jak Twoja aplikacja może zmieniać się wraz ze zmianą ekranu, jeśli aplikacja jest zainstalowana na urządzeniu składanym, co jest bardzo ważne w nowoczesnym rozwoju Androida. Szyna nawigacyjna przeznaczona jest dla ekranów średniej wielkości, natomiast szuflada nawigacyjna podobnie jak w przypadku ekranów o średniej wielkości stary sposób pisania aplikacji, służy jako boczna szuflada i nadaje się do urządzeń z dużym ekranem. FoldFeature to wbudowany komponent interfejsu użytkownika Jetpack Compose, który umożliwia utworzenie efektu animacji składania po kliknięciu. Oto kroki, jak używać FoldFeature w aplikacji na Androida. Możesz także dostosować FoldFeature, podając niezbędne parametry:

•  foldableState: Ten stan kontroluje składanie i rozkładanie funkcji FoldFeature. Możesz utworzyć instancję FoldState za pomocą funkcji RememberFoldState().
•  FoldedContent: Treść zostanie wyświetlona po złożeniu funkcji FoldFeature.
•  rozszerzona zawartość: Jest to treść, która będzie wyświetlana, gdy FoldFeature będzie w stanie rozwiniętym.
• foldingIcon: Jest to ikona, która będzie wyświetlana, aby wskazać stan złożenia funkcji FoldFeature.
Składane urządzenie może znajdować się w różnych stanach i pozycjach. Klasa WindowLayoutInfo biblioteki Jetpack WindowManager, której używamy w naszym przykładzie, dostarcza nam następujących szczegółów. stan pomaga opisać stan złożony, w jakim znajduje się urządzenie. Gdy telefon jest całkowicie otwarty, jego stan to FLAT lub HALF_OPENED. Możemy także pobawić się orientacją, czyli orientacją zawiasu. Zawias może być POZIOMY lub PIONOWY. Mamy occlusionType i jest to wartość FULL, gdy zawias zakrywa część wyświetlacza. W przeciwnym razie wartość wynosi BRAK. Na koniec mamy funkcję isSeparating, która staje się ważna, gdy zawias tworzy dwa logiczne wyświetlacze.

Tworzenie układów adaptacyjnych przy użyciu ConstraintLayouts

Jetpack Compose, deklaratywny zestaw narzędzi interfejsu użytkownika do tworzenia świetnych interfejsów użytkownika, idealnie nadaje się do wdrażania i projektowania układów ekranu, które automatycznie dostosowują się i dobrze renderują zawartość na ekranach o różnych rozmiarach. Może to być przydatne do rozważenia przy budowaniu aplikacji, ponieważ szansa na zainstalowanie w składanym urządzeniu jest wysokaskładana przestrzeń przypominająca tablet.

Przygotowywanie się

Aby skorzystać z tego przepisu, musisz przeczytać poprzednie sekcje

. Jak to zrobić…

Na potrzeby tego przepisu zbudujemy oddzielną funkcję do komponowania, która pokaże Ci, jak używać ConstraintLayout w tym samym projekcie zamiast tworzyć nowy:

1. Przejdźmy dalej i otwórzmy Travellera. Dodaj nowy pakiet i nazwij go constraintllayoutexample. Wewnątrz pakietu utwórz plik Kotlin o nazwie ConstraintLayoutExample, a następnie dodaj do projektu następującą zależność:

implementation "Androidx.constraintlayout:constraintlayoutcompose:
1.x.x"

2. W naszym przykładzie utworzymy zabawną funkcję AndroidCommunity() i użyjemy ConstraintLayout do utworzenia odniesień do tytułu, aboutCommunity i AndroidImage:

@Composable
fun AndroidCommunity() {
ConstraintLayout {
val (title, aboutCommunity, AndroidImage) =
createRefs()

}

3. createRefs(), co oznacza tworzenie odniesień, po prostu tworzy odniesienie dla każdego elementu składowego w naszym ConstrainLayout.
4. Teraz przejdźmy dalej i utwórzmy tekst tytułowy, aboutCommunity i AndroidImage:

Text(
text = stringResource(id =
R.string.Android_community),
modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.padding(top = 12.dp),
style = TextStyle(
color = Color.Blue,
fontSize = 24.sp
)
)

5. Nasz tekst tytułowy ma modyfikator, który ma zdefiniowane ograniczenia, a jeśli wcześniej korzystałeś z XML, możesz zauważyć, że działa to dokładnie tak, jak działa XML. Ograniczenia zapewniamy za pomocą modyfikatora constrainAs(), który w naszym przypadku przyjmuje referencje jako parametr i pozwala nam określić swoje ograniczenia w treści lambda. W dalszej części nasze ograniczenia są określane za pomocą metody linkTo(...) lub innych, ale w tym przypadku użyjemy metody linkTo(parent.top). Możemy teraz połączyć części w podobny sposób, ponadto sprawdź sekcję Wymagania techniczne dla całego kodu:

Text(
text = stringResource(id =
R.string.Android_community),
modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.padding(top = 12.dp),
style = TextStyle(
color = Color.Blue,
fontSize = 24.sp
)
)

6. Następnie budujemy obraz:

Image(
painter = painterResource(id =
R.drawable.Android),
contentDescription = stringResource(id =
R.string.Android_image),
modifier = Modifier.constrainAs(AndroidImage) {
top.linkTo(aboutCommunity.bottom,
margin = 16.dp)
centerHorizontallyTo(parent)
}
)


7. Na koniec, aby uruchomić tę część kodu, możesz uruchomić sekcję @Preview:

@Preview(showBackground = true)
@Composable
fun ShowAndroidCommunity() {
TravellerTheme() {
AndroidCommunity()
}
}

8. Po uruchomieniu aplikacja powinna dobrze się renderować i dostosowywać do rozmiarów ekranu. Przykładowo, jeśli stan jest pełny (czyli nie jest złożony), dane powinny być wyświetlane na całym ekranie



9. Na rysunku możesz zobaczyć wersję danych po złożeniu ekranu i sposób, w jaki dostosowuje się on do określonych wymiarów.



Jak to działa…

Używamy modyfikatorów, aby dostosować odstępy między komponentami i wykorzystujemy zasoby wymiarów do zdefiniowania marginesu między obrazem a aboutCommunity. Nasz układ dostosuje się do rozmiaru ekranu, aby dobrze wyglądać zarówno na małych, jak i dużych ekranach. Używamy również ConstraintLayout, który jest menedżerem układu, który pozwala nam tworzyć złożone układy z płaską hierarchią widoków. Posiada również wbudowaną obsługę układów responsywnych, umożliwiając tworzenie różnych układów dla różnych rozmiarów i orientacji ekranu. Najlepsze przypadki użycia ConstraintLayout obejmują:

•  Gdy chcesz uniknąć zagnieżdżania wielu kolumn i wierszy; może to dotyczyć sytuacji, gdy chcesz ustawić elementy na ekranie w celu łatwiejszej czytelności kodu
•  Używanie go, gdy musisz zastosować wytyczne, łańcuchy lub bariery w swoim pozycjonowaniu
Wspomnieliśmy o modyfikatorach w poprzednich rozdziałach, które działają jak atrybuty w układach XML. Pozwalają nam zastosować różne style i zachowania do komponentów naszego układu. Możesz użyć modyfikatorów, aby zmienić rozmiar, położenie i inne właściwości komponentu w zależności od rozmiaru ekranu. W naszym przykładzie używamy dynamicznego dopełnienia i marginesów, za ich pomocą możesz dostosować odstępy między komponentami w zależności od rozmiaru ekranu. Na przykład możesz użyć modyfikatora, aby dodać więcej wypełnienia do komponentu na większych ekranach. Umożliwia to tworzenie responsywnych układów, które dostosowują się do rozmiaru ekranu.

Obsługa zmian konfiguracji na dużym ekranie i ciągłość

Urządzenia z Androidem w trakcie swojej pracy ulegają różnym zmianom konfiguracyjnym. Do najbardziej godnych uwagi lub standardowych należą:

•  Zmiana orientacji ekranu: Dzieje się tak, gdy użytkownik obraca ekran urządzenia, powodując zmianę konfiguracji. Dzieje się tak, gdy urządzenie przełącza się z trybu pionowego na poziomy i odwrotnie.
•  Zmiana rozmiaru ekranu: ma to miejsce, gdy użytkownik zmienia rozmiar ekranu urządzenia - na przykład podłączając lub odłączając zewnętrzny wyświetlacz, co powoduje zmianę konfiguracji.
•  Zmiana języka lub ustawień regionalnych: ma to miejsce, gdy użytkownik zmienia język lub ustawienia regionalne urządzenia, co powoduje zmianę konfiguracji. Może to mieć wpływ między innymi na formatowanie tekstu i dat.
•  Zmiana motywu: ma to miejsce, gdy użytkownik zmienia motyw urządzenia, co powoduje zmianę konfiguracji. Może to mieć wpływ na wygląd interfejsu użytkownika.
•  Zmiana dostępności klawiatury: ma to miejsce, gdy użytkownik podłącza lub odłącza klawiaturę od urządzenia, co powoduje zmianę konfiguracji. Może to mieć wpływ na układ interfejsu użytkownika i tak dalej.

W tym przepisie przyjrzymy się wykorzystaniu tej wiedzy, aby lepiej radzić sobie ze zmianami rozmiaru ekranu w przypadku urządzeń składanych.

Przygotowywanie się

W pierwszym przepisie, Budowanie układów adaptacyjnych w nowoczesnym rozwoju Androida, omówiliśmy różne konfiguracje stanów i sposoby lepszego radzenia sobie z nimi. W tym przepisie dowiemy się, jak wykorzystać udostępnioną już funkcję RememberFoldableState w Jetpack Compose do obsługi zmian na ekranie w urządzeniach składanych.

Jak to zrobić

W tym przykładzie wykorzystajmy już utworzony projekt Traveler; nie będziesz musiał tworzyć nowego projektu:

1. Aby móc korzystać z RememberFoldableState, będziemy musieli zaimportować go do naszego projektu:

import Androidx.window.layout.FoldableState

2. Następnie utworzymy nową wartość/właściwość FoldableState i zainicjujemy ją za pomocą naszego RememberFoldableState:

val foldState = rememberFoldableState()

3. Za pomocą obiektufoldState możemy uzyskać informacje o składanych urządzeniach, sprawić, że nasza aplikacja będzie reagowała na odpowiedni stan i wyświetlać dane w razie potrzeby. Dostępne są trzy stany: STATE_FLAT, STATE_HALF_OPENED i STATE_CLOSED:

when (foldState.state) {
FoldableState.STATE_FLAT -> {
// Our Device is flat (unfolded)do something
}
FoldableState.STATE_HALF_OPENED -> {
//Our Device is partially folded. Do something
}
FoldableState.STATE_CLOSED -> {
//Our Device is fully folded do something
}
}

4. Możemy następnie wykorzystać te informacje, aby odpowiednio dostosować nasz interfejs użytkownika, na przykład pokazać lub ukryć określone elementy w oparciu o stan złożenia lub określoną pozycję. Ponadto możemy stworzyć dwa różne układy urządzenia po złożeniu i rozłożeniu:

val isFolded = foldState.state == FoldableState.STATE_CLOSED
if (isFolded) {
// Create our layout for when the device is folded
} else {
// Create our layout for when the device is
unfolded
}

I to wszystko; pomoże to rozwiązać stan składania, jeśli masz złożony system interfejsu użytkownika, który wymaga lepszej obsługi.

Jak to działa…

Obsługa znaczących zmian w konfiguracji ekranu, zwłaszcza w przypadku urządzeń składanych, może stanowić wyzwanie w Androidzie Jetpack Compose. Oto kilka wskazówek, które mogą pomóc w korzystaniu z interfejsu API konfiguracji. Umożliwia uzyskanie informacji o konfiguracji ekranu urządzenia, takich jak rozmiar ekranu, orientacja i stan złożenia. Możesz wykorzystać te informacje, aby odpowiednio dostosować swój interfejs użytkownika. System układu Compose ułatwia tworzenie responsywnych interfejsów użytkownika, które można dostosować do różnych rozmiarów ekranu i proporcji. Użyj elastycznych układów, takich jak kolumny i wiersze, aby utworzyć interfejs użytkownika, który można skalować w górę lub w dół w zależności od potrzeb. Funkcja RememberFoldableState pozwala uzyskać informacje o stanie złożenia urządzenia i odpowiednio dostosować interfejs użytkownika. Możesz na przykład użyć tej funkcji, aby utworzyć dwa różne układy, jeden dla złożonego urządzenia i drugi dla rozłożonego. Aby upewnić się, że działa poprawnie, konieczne jest również przetestowanie aplikacji przy różnych konfiguracjach ekranu. Do testowania aplikacji możesz użyć emulatora Androida lub urządzeń fizycznych.

Zrozumienie osadzania działań

W Jetpack Compose osadzanie działań odnosi się do procesu włączania funkcji, którą można komponować, w kontekście działania. Umożliwia to tworzenie niestandardowych widoków, które można bezproblemowo zintegrować z istniejącymi działaniami systemu Android. Aby osadzić funkcję składalną w działaniu, można użyć metody setContent działania. Ta metoda przyjmuje jako parametr funkcję komponowalną, której można użyć do zdefiniowania układu działania.

Przygotowywanie się

Aby kontynuować, musisz ukończyć poprzednie przepisy.

Jak to zrobić…

Spójrzmy na przykład osadzania funkcji komponowalnej w działaniu:

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyCustomView()
}
}
}
@Composable
fun MyCustomView() {
Text(text = "Hello, Android Community!")
}

W tym przykładzie metoda setContent osadza funkcję komponowalną MyCustomView w MainActivity. Po utworzeniu działania zostanie wywołana funkcja MyCustomView w celu wygenerowania układu działania.

Jak to działa…

Funkcja MyCustomView jest zdefiniowana jako funkcja składalna przy użyciu adnotacji @Composable. Pozwala to na wielokrotne wywoływanie funkcji bez powodowania jakichkolwiek skutków ubocznych. W tym przypadku funkcja po prostu wyświetla tekst, który można utworzyć z tekstem Witaj, społeczność Androida!. Osadzając w działaniach funkcje, które można komponować, możesz tworzyć niestandardowe widoki, które można łatwo zintegrować z aplikacją na Androida. Może to być szczególnie przydatne do tworzenia komponentów wielokrotnego użytku lub dostosowywania układu istniejących działań.

Motywy materiałowe w kompozycji

Material Theming w Compose to system projektowania wprowadzony przez Google, który zapewnia wytyczne i zasady dotyczące projektowania interfejsów użytkownika. Material Theming pomaga projektantom tworzyć interfejsy, które są spójne, łatwe w użyciu i atrakcyjne wizualnie. Niektóre kluczowe funkcje motywów materiałowych w Jetpack Compose obejmują:

o Palety kolorów: zestaw predefiniowanych palet kolorów, których można używać do tworzenia spójnych i atrakcyjnych wizualnie interfejsów. Jetpack Compose udostępnia materiał do komponowania MaterialTheme, który umożliwia zastosowanie palety kolorów do całej aplikacji.
o Typografia: zestaw stylów typografii, które umożliwiają utworzenie spójnego i łatwego do odczytania interfejsu. Jetpack Compose zapewnia możliwość komponowania typografii, która umożliwia zastosowanie stylu typografii do tekstu.
o Kształty: zestaw kształtów, które umożliwiają tworzenie spójnych i atrakcyjnych wizualnie komponentów. Jetpack Compose zapewnia możliwość komponowania kształtu, która umożliwia nakładanie kształtu na komponenty.
o Ikony: Zestaw ikon, których można używać do tworzenia spójnych i rozpoznawalnych interfejsów. Jetpack Compose zapewnia możliwość komponowania ikon, która umożliwia używanie ikon materiałów w aplikacji.

Korzystając z motywów materiałów w Jetpack Compose, możesz tworzyć interfejsy, które są spójne, łatwe w użyciu i atrakcyjne wizualnie. Material Theming w Jetpack Compose pomaga skupić się na projektowaniu funkcjonalności aplikacji, podczas gdy system projektowania dba o szczegóły wizualne.

Przygotowywanie się

Aby móc kontynuować, musisz przepracować poprzedni przepis

Jak to zrobić…

Wiele aplikacji w dalszym ciągu nie korzysta z Materiału 3, ale jeśli tworzysz nową aplikację od zera, zdecydowanie zaleca się skorzystanie z Materiału 3. Należy pamiętać, że podczas tworzenia projektu Materiał 3 nie jest preinstalowany; oznacza to, że musisz samodzielnie zaktualizować biblioteki materiałów do wersji Material 3.Zobaczmy przykład implementacji motywu Material 3 w Jetpack Compose dla aplikacji na Androida:

1. Będziesz musiał dodać wymagane zależności Material 3 do pliku build.gradle swojej aplikacji:

implementation 'Androidx.compose.material3:material3:1.0.0-alpha14'

2. Następnie musisz zadeklarować motyw aplikacji:

@Composable
fun MyAppMaterialTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = /**/,
typography = /**/,
shapes = /**/,
content = content
)
}

3. Wreszcie możesz użyć motywu w całej aplikacji:

@Composable
fun MyApp() {
MyAppMaterialTheme {}
}

W tym przykładzie użyliśmy kolorów, typografii i kształtów Material 3, aby stworzyć spójny i atrakcyjny wizualnie interfejs. Wykorzystaliśmy także ikony Material 3, aby poprawić komfort użytkowania. Na koniec opakowaliśmy zawartość naszej aplikacji w kompozycję MyAppMaterialTheme, którą można komponować w celu zastosowania motywu Material 3.

Jak to działa…

Oto jak Materiał 3 działa w Jetpack Compose. Material 3 wprowadza nowe i zaktualizowane komponenty, takie jak AppBar, BottomNavigation i TabBar, których można używać w Jetpack Compose przy użyciu pakietu Androidx.compose.material3. Komponenty te mają zaktualizowany wygląd i funkcjonalność oraz są zgodne z wytycznymi Material 3. Material 3 wprowadza także nowy system motywów, który pozwala na większą personalizację i elastyczność - to znaczy, że w Jetpack Compose motywy Material 3 można zastosować za pomocą komponentu MaterialTheme3. Ten komponent umożliwia dostosowanie schematu kolorów, typografii i kształtów aplikacji, a także zapewnia nowe opcje dostosowywania wysokości i cieni komponentów. Ikony są teraz nowoczesne i łatwo dostępne, co dla nas, programistów, jest dużym plusem. Wreszcie Material 3 wprowadza nowy system typografii, który zapewnia zaktualizowane style i wytyczne dotyczące typografii w Twojej aplikacji. W Jetpack Compose typografię Material 3 można zastosować za pomocą obiektu Material3Typography, który udostępnia kilka predefiniowanych stylów tekstu. Korzystając z Material 3 w Jetpack Compose, możesz tworzyć nowoczesne i atrakcyjne wizualnie interfejsy, zgodne z najnowszymi wytycznymi projektowymi. Należy również pamiętać, że komponenty, motywy, ikony i typografia Material 3 mogą być użyte razem, aby stworzyć spójny i spójny system projektowania aplikacji.

Testowanie aplikacji na urządzeniu składanym

Testowanie aplikacji na urządzeniach składanych jest niezbędne, aby mieć pewność, że działają poprawnie i zapewniają doskonałe doświadczenia użytkownika. W tym przepisie przyjrzymy się kilku wskazówkom, jak testować aplikacje na urządzeniach składanych w Jetpack Compose.

Przygotowywanie się

Będziesz musiał skorzystać z poprzednich przepisów.

Jak to zrobić…

Oto kilka wskazówek, jak przetestować aplikacje na urządzeniach składanych:

•  Użyj emulatora: możesz użyć emulatora Androida, aby przetestować swoją aplikację na urządzeniach składanych bez konieczności kupowania urządzenia fizycznego. Emulator zapewnia szereg składanych konfiguracji urządzeń, których można użyć do testowania aplikacji.
•  Używaj prawdziwych urządzeń: testowanie aplikacji na rzeczywistym urządzeniu składanym może zapewnić dokładniejsze odwzorowanie działania aplikacji na tych urządzeniach. Jeśli masz dostęp do urządzenia składanego, zdecydowanie zalecamy przetestowanie na nim aplikacji.
•  Przetestuj różne tryby ekranu: urządzenia składane są dostępne w różnych trybach ekranu, takich jak jeden ekran, dwa ekrany i rozszerzone ekrany. Niezbędne jest przetestowanie aplikacji w różnych trybach ekranu, aby upewnić się, że działa poprawnie we wszystkich trybach.
•  Testuj na różnych rozmiarach ekranów: urządzenia składane są dostępne w różnych rozmiarach, dlatego niezwykle ważne jest przetestowanie aplikacji na ekranach o różnych rozmiarach, aby upewnić się, że działa dobrze na wszystkich urządzeniach.
•  Przetestuj przejście aplikacji: przetestowanie przejścia aplikacji między różnymi trybami ekranu może pomóc w zidentyfikowaniu wszelkich problemów z układem lub zachowaniem aplikacji. Pamiętaj, aby przetestować wszystkie tryby przejścia, takie jak składanie, rozkładanie i składanie.
•  Korzystaj z testów automatycznych: Testy automatyczne mogą pomóc w skuteczniejszym testowaniu aplikacji na ekranach o różnych rozmiarach, trybach i orientacjach. Możesz użyć narzędzi takich jak Espresso lub UI Automator, aby napisać automatyczne testy dla swojej aplikacji.

Jak to działa…

Ogólnie rzecz biorąc, testowanie aplikacji na urządzeniach składanych wymaga dokładnego rozważenia unikalnych funkcji i możliwości urządzenia. Postępując zgodnie z tymi wskazówkami, możesz mieć pewność, że Twoja aplikacja jest zoptymalizowana pod kątem urządzeń składanych i zapewnia doskonałe wrażenia użytkownika.

Wdrażanie pierwszego systemu operacyjnego Wear za pomocą Jetpack Compose

Wear OS to system operacyjny opracowany przez Google dla inteligentnych zegarków i innych urządzeń do noszenia. Istnieje kilka powodów, dla których tworzenie Wear OS dla Androida jest niezbędne w naszym nowoczesnym rozwoju Androida. Po pierwsze, oznacza to rozszerzenie ekosystemu Androida; Wear OS rozszerza ekosystem Androida, umożliwiając programistom tworzenie aplikacji i usług, do których można uzyskać dostęp za pośrednictwem smartwatcha lub innego urządzenia do noszenia. Rozszerza to zasięg Androida i stwarza nowe możliwości dla programistów i użytkowników. Ponadto zapewnia bezproblemową integrację ze smartfonami z systemem Android, umożliwiając użytkownikom łatwy dostęp do powiadomień, połączeń i innych informacji na ich smartwatchach, zapewniając w ten sposób wygodniejszy i wydajniejszy sposób interakcji użytkowników z aplikacją. Nie zapominajmy, że najbardziej godnymi uwagi aplikacjami, które mogą na tym skorzystać, są aplikacje do śledzenia zdrowia i kondycji, w tym do monitorowania tętna, śledzenia kroków i śledzenia treningu. Dzięki temu użytkownicy mogą śledzić swoje cele fitness i zachować motywację do ich osiągnięcia. Wreszcie Wear OS umożliwia użytkownikom dostosowywanie smartwatcha za pomocą różnych tarcz, aplikacji i widżetów. Zapewnia to spersonalizowane doświadczenie, które odpowiada indywidualnym potrzebom i preferencjom. Technologie ubieralne to szybko rozwijający się rynek, a wraz z ich ciągłym rozwojem Wear OS ma potencjał, aby stać się kluczowym graczem na rynku technologii ubieralnych. Wear OS jest wciąż bardzo nowym rozwiązaniem i w tym rozdziale przyjrzymy się prostym, podstawowym przykładom, ponieważ wiele interfejsów API może ulec zmianie w przyszłości. Dlatego pomocne będzie zrozumienie, jak to działa i jak tworzyć karty, przyciski i listy pokazów.

Rozpoczęcie pracy z pierwszym systemem Wear OS w Android Studio

System operacyjny Android jest używany na całym świecie, a jednym z przypadków użycia jest Wear OS (przez zużycie rozumiemy smartwatche). To dobra wiadomość dla programistów Androida, ponieważ oznacza to więcej miejsc pracy. Co więcej, wiele aplikacji musi teraz obsługiwać Wear OS, np. Spotify, aplikacje do śledzenia kondycji, aplikacje do monitorowania pracy serca i inne, co oznacza, że pojawi się więcej przypadków użycia, a firmy będą wdrażać budowanie dla Wear OS, nawet jeśli będzie to miało wyłącznie charakter powiadomień. Dlatego e omówimy, jak zacząć.

Przygotowywanie się

W tym przepisie przyjrzymy się, jak rozpocząć pracę z Wear OS i jak skonfigurować środowisko testowania wirtualnego zegarka.

Jak to zrobić…

Aby utworzyć swój pierwszy projekt na Wear OS w Jetpack Compose, wykonaj następujące kroki:

1. Najpierw utwórz nowy projekt Androida w Android Studio i upewnij się, że masz zainstalowaną najnowszą wersję Android Studio i pakiet SDK Wear OS.

2. Następnie, zgodnie z procedurą tworzenia pierwszej aplikacji, zamiast Telefonu i Tabletu wybierz Wear OS, jak pokazano na rysunku



3. Wybierz opcję Pusta czynność tworzenia (patrz rysunek 10.1); jak zapewne wiesz, znacznie lepiej jest używać Compose podczas tworzenia systemu dla Wear OS, ponieważ Google to zaleca. Następnie naciśnij Dalej i nazwij swój projekt Wear OS WearOSExample. Zauważysz, że używa minimalnego pakietu SDK API 30: Android 11.0 (R).



4. Kliknij Zakończ. Powinieneś zobaczyć dostarczony przykładowy szablon kodu.

5. Teraz przejdźmy dalej i skonfiguruj nasze wirtualne urządzenie testujące Wear OS do uruchamiania dostarczonego już szablonu kodu. Przejdź do Narzędzia | Menedżer urządzeń, a następnie utwórz nowe urządzenie

6. Teraz zobacz rysunek, aby wybrać wirtualne urządzenie testowe Wear OS. Pamiętaj, że możesz także wybrać urządzenie okrągłe, kwadratowe lub prostokątne. Użyjemy okrągłego.



7. Kliknij Dalej, a następnie pobierz obraz systemu - w naszym przypadku R, czyli poziom API 30.



8. Następnie naciśnij Zakończ i powinieneś mieć gotowe do użycia wirtualne urządzenie testowe Wear OS.

9. Teraz zmień tekst w funkcji Powitanie() w szablonie kodu na "Witaj, społeczność Androida" i uruchom, a powinieneś otrzymać coś podobnego do rysunku. Jeśli wszystko zostało poprawnie zainstalowane, nie powinieneś mieć błędu kompilacji.



10. Upewnij się także, że zmieniłeś tekst w zasobie okrągłego ciągu znaków.

To wszystko, pomyślnie skonfigurowałeś swój pierwszy system operacyjny Wear i mogliśmy uruchomić już udostępnioną funkcję Powitanie(). W poniższym przepisie przyjrzymy się tworzeniu prostego przycisku.

Jak to działa…

Zauważysz, że szablon wygląda dokładnie tak, jak tworzysz aplikacje na Androida, a jedyną różnicą są użyte biblioteki. Szablon wykorzystuje funkcję Compose, co ułatwia nam pracę podczas programowania, ponieważ będziemy korzystać z większości koncepcji, których nauczyliśmy się w poprzednich rozdziałach. Poniżej znajduje się porównanie, które pomoże Ci poznać różnicę między zależnością Wear OS a zależnością standardową:



Tworzenie pierwszego przycisku

W tym przepisie utworzymy nasz pierwszy przycisk w Wear OS, aby poznać zasady i najlepsze praktyki tworzenia w Wear OS.

Przygotowywanie się

Aby rozpocząć pracę nad tym, musisz ukończyć poprzedni przepis. Będziemy opierać się na naszym już utworzonym projekcie WearOSExample.

Jak to zrobić…

Aby utworzyć pierwszy przycisk w systemie Wear OS w Jetpack Compose, możesz wykonać następujące kroki:

1. Korzystając z już utworzonego projektu, dodamy nowy przycisk. Przejdźmy dalej i usuń część już dostarczonego kodu, zabawne powitanie (greetingName: String):



2. Usunięcie funkcji Powitanie() wywołanej w WearOSExampleTheme spowoduje narzekanie; śmiało i to też usuń.

3. Następnie utwórz nową funkcję Composable, która zdefiniuje Twój przycisk. Możesz użyć funkcji przycisku udostępnionej przez Jetpack Compose:

@Composable
fun SampleButton() {
Button(
onClick = { /* Handle button click */ },
modifier = Modifier.fillMaxWidth()
) {
Text("Click me")
}
}

4. Następnie wywołaj nową funkcję w naszej funkcji WearApp():

@Composable
fun WearApp() {
WearOSExampleTheme {
/* If you have enough items in your list, use
[ScalingLazyColumn] which is an optimized
version of LazyColumn for wear devices with
some added features. For more information,
see d.android.com/wear/compose./
*/
Column(
modifier = Modifier
.fillMaxSize()
.background(
MaterialTheme.colors.background),
verticalArrangement = Arrangement.Center
) {
SampleButton()
}
}
}

5. Następnie w naszym działaniu wywołaj metodę setContent z funkcją Composable swojego przycisku jako parametrem:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
WearApp()
}
}
}

6. Możesz także skorzystać z już dostępnej funkcji podglądu, aby zobaczyć zmiany. Zauważysz, że wyraźnie określamy urządzenie, @Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true):

@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
@Composable
fun DefaultPreview() {
WearApp()
}

7. Uruchom aplikację Wear OS. Powinieneś zobaczyć swój przycisk na ekranie, jak pokazano na rysunku:



8. Spójrzmy na inny przykład, którym jest przycisk z ikoną; jest to dość podobne do pierwszego przycisku, ale w tym przypadku dodamy po prostu ikonę zamiast tekstu.

9. Utwórz nową funkcję o nazwie SampleButton2() i dodaj następujący kod:

@Composable
fun SampleButton2(
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.size(
ButtonDefaults.LargeButtonSize),
onClick = { /* Handle button click */ },
) {
Icon(
imageVector =
Icons.Rounded.AccountBox,
contentDescription = stringResource(
id = R.string.account_box_icon),
modifier = Modifier
.size(24.dp)
.wrapContentSize(
align = Alignment.Center)
)
}
}
}

10. Na koniec skomentuj SampleButton, dodaj SampleButton2 i uruchom; powinieneś zobaczyć coś podobnego do rysunku:



Ważna uwaga: należy pamiętać, że platforma Wear OS uwzględnia pewne unikalne kwestie związane z projektowaniem i testowaniem aplikacji, takie jak mniejszy rozmiar ekranu i potrzeba optymalizacji czasu pracy baterii. Koniecznie przetestowaj aplikację na rzeczywistym urządzeniu, aby upewnić się, że działa zgodnie z oczekiwaniami w systemie Wear OS.

Jak to działa…

Sądząc po Twojej wcześniejszej znajomości narzędzia Compose, wszystko, nad czym do tej pory pracowaliśmy, powinno wyglądać znajomo. W naszym przykładzie używamy SampleButton i WearOSExampleTheme z biblioteki Wear OS Compose, aby utworzyć przycisk zaprojektowany specjalnie dla urządzeń z Wear OS. SampleButton pobiera lambdę onClick, która jest wywoływana po kliknięciu przycisku, oraz modyfikator ustawiający rozmiar przycisku na podstawie tego, co określimy, czyli w naszym przykładzie jest to prosta metoda fillMaxWidth(). Używamy HorizontalArrangement w kolumnie, aby wyśrodkować nasz przycisk i używamy koloru MaterialTheme do pomalowania tła. W przypadku Wear OS Google zaleca stosowanie domyślnych kształtów zużycia materiału; są one już zoptymalizowane pod kątem urządzeń nieokrągłych i okrągłych, co ułatwia nam pracę jako programistom. Aby uzyskać więcej informacji na temat kształtów, zobacz poniższy link: https://developer.android.com/reference/kotlin/androidx/wear/komponuj/materiał/kształty. Na koniec używamy możliwości tworzenia tekstu do wyświetlania tekstu przycisku, co jest istotne, ponieważ informuje użytkowników o zamierzonym zastosowaniu przycisku.

Implementacja przewijanej listy

Implementacja przewijanej listy jest niezbędna do stworzenia skutecznej i przyjaznej dla użytkownika aplikacji na Androida, która spełnia potrzeby Twoich użytkowników. Przewijalna lista pozwala na wyświetlenie dużej ilości informacji na małym ekranie, co może być korzystne, szczególnie w tak małym urządzeniu jak zegarek. Przewijając listę, użytkownicy mogą szybko i łatwo uzyskać dostęp do wszystkich elementów bez konieczności przechodzenia do różnych ekranów lub stron. Użytkownicy oczekują płynnego i responsywnego przewijania podczas interakcji z listami. Implementacja przewijanej listy o zoptymalizowanej wydajności może sprawić, że aplikacja będzie działać szybko i będzie reagować na potrzeby użytkownika. Listy przewijane można dostosować do różnych przypadków użycia i wymagań projektowych. Możesz dostosować układ, wygląd i zachowanie listy, aby dopasować ją do konkretnych potrzeb aplikacji i zapewnić użytkownikom wyjątkową wygodę. W tym przepisie przyjrzymy się, jak zaimplementować przewijaną listę w Wear OS.

Przygotowywanie się

Aby rozpocząć pracę nad tym, musisz ukończyć poprzedni przepis. Będziemy korzystać z naszego już utworzonego projektu WearOSExample, aby kontynuować tę część.

Jak to zrobić…

Wykonaj poniższe kroki, aby utworzyć przewijaną listę w Wear OS za pomocą Jetpack Compose:

1. W pliku MainActivity.kt utwórzmy nową funkcję Composable zawierającą przewijaną listę. Możesz to nazwać jak chcesz, ale w tym przykładzie nazwiemy to WearOSList.
2. Inną opcją jest utworzenie nowego pakietu w celu lepszego zorganizowania naszego kodu i wywołania komponentów pakietu. Wewnątrz komponentów utwórz nowy plik Kotlin i nadaj mu nazwę WearOsList.
3. W naszej funkcji WearOSList będziemy potrzebować listy ciągów znaków dla naszego przykładu; możemy po prostu utworzyć przykładowe fikcyjne dane, aby zaprezentować przykład:

@Composable
fun WearOSList(itemList: List) {….}

4. W naszej funkcji WearOSList utwórz ScalingLazyColumn, która jest zoptymalizowana pod kątem Wear OS. Będzie to kontener na naszą przewijaną listę. O ScalingLazyColumn porozmawiamy w dalszej części:

@Composable
fun WearOSList(itemList: List) {
ScalingLazyColumn() {
// TODO: Add items to the list here
}
}

Tworzenie systemu dla Wear OS może być wyzwaniem ze względu na rozmiar treści, dlatego należy zapoznać się z najlepszymi praktykami Wear.

5. Dla naszych przedmiotów utworzymy nową funkcję Composable o nazwie WearOSListItem, która będzie zawierała tylko tekst, ponieważ właśnie go prezentujemy:

@Composable
fun WearOSListItem(item: String) {
Text(text = item)
}

6. Dla naszych danych utworzymy listę fikcyjną, zatem śmiało dodaj w funkcji WearApp():

val itemList = listOf(
"Item 1",
"Item 2",
"Item 3",
"Item 4",
"Item 5",

)

7. Na koniec skomentuj dwa utworzone przez nas przyciski, wywołaj WearOSList, przekaż itemList i uruchom aplikację:

{
// SampleButton()
//SampleButton2()
WearOSList(
itemList = itemList,
modifier = contentModifier
)
}

8. Powinieneś zobaczyć listę podobną do tej z rysunku :



Jak to działa…

W tym przykładzie używamy WearOsList i WearOSExampleTheme z biblioteki Wear OS Compose, aby utworzyć listę zaprojektowaną specjalnie dla urządzeń z Wear OS. Zaczynamy od utworzenia elementu składowego WearOSList, który jako parametr przyjmuje listę elementów. Wewnątrz ScalingLazyColumn używamy funkcji items do przeglądania listy elementów i tworzenia dla każdego elementu WearOSListItem. Kompozytor WearOSListItem ma funkcję tekstu Composable.

Implementacja kart w Wear OS (TitleCard i AppCard)

Tworząc system dla Wear OS, musimy wziąć pod uwagę dwie istotne karty: AppCard i TitleCard. Dobrym przypadkiem użycia kart byłoby Powiadomienie i Inteligentna odpowiedź. Jeśli używasz urządzenia do noszenia, być może wiesz, co to jest; jeśli nie korzystasz z urządzenia do noszenia, możesz je sprawdzić, ale w tym przepisie przyjrzymy się również przykładom. Ponadto, jeśli utworzysz kartę powiadomień, zamierzasz zapewnić szybki i łatwy sposób przeglądania powiadomień z aplikacji i odpowiadania na nie. Gdy nadejdzie powiadomienie, pojawi się ono jako karta na tarczy zegarka, którą możesz następnie przesunąć lub dotknąć, aby otworzyć powiadomienie i wejść w interakcję z nim. Jeśli chodzi o karty Inteligentnej Odpowiedzi, ta funkcja wykorzystuje uczenie maszynowe do sugerowania odpowiedzi na otrzymane wiadomości na podstawie kontekstu wiadomości. Karty te pojawiają się jako opcja odpowiedzi na powiadomienia i umożliwiają szybkie wysłanie wiadomości bez konieczności jej ręcznego wpisywania. Zarówno karty powiadomień, jak i inteligentnych odpowiedzi są niezbędne, ponieważ zapewniają wydajny i usprawniony sposób zarządzania powiadomieniami i odpowiadania na wiadomości bez konieczności ciągłego wyciągania telefonu. Pozwalają Ci pozostać w kontakcie w podróży i informować Cię o ważnych informacjach bez zakłócania codziennej rutyny, dlatego też Wear OS pozostanie tutaj, a wiedza, jak go zbudować, przyda się. W tym przepisie stworzymy prostą kartę i zobaczymy jak obsługiwać nawigację w Wear OS.

Przygotowywanie się

Aby kontynuować korzystanie z tego przepisu, konieczne będzie ukończenie poprzednich przepisów.

Jak to zrobić…

Oto przykład tworzenia karty w Wear OS przy użyciu Jetpack Compose. Otwórz projekt WearOSExample i wykonaj kod:

1. Wewnątrz pakietu komponentów utwórzmy nowy plik Kotlin i nazwijmy go MessageCardExample.

2. W MessageCardExample utwórz nową funkcję umożliwiającą komponowanie o nazwie MessageCard:

@Composable
fun MessageCard(){…}

3. Musimy teraz wywołać AppCard(), ponieważ tego właśnie chcemy. Karta AppCard pobiera nazwę aplikacji, czas, tytuł i inne dane, jak pokazano na rysunku . Oznacza to, że możesz dostosować swoją kartę AppCard () do swoich potrzeb:



4. Ułatwia to nam pracę jako programistom, ponieważ dokładnie wiemy, czego potrzebujemy podczas budowania, zwiększając w ten sposób produktywność programistów:

@Composable
fun MessageCard() {
AppCard(
onClick = { /*TODO*/ },
appName = { /*TODO*/ },
time = { /*TODO*/ },
title = { /*TODO*/ }) {
}
}

5. Teraz przejdźmy dalej, zaimplementujmy naszą AppCard() i wyślijmy wiadomość do naszych użytkowników. W naszym przykładzie zakodujemy dane na stałe, ale jeśli masz punkt końcowy, możesz pobrać dane i wyświetlić je w razie potrzeby:

@Composable
fun MessageCard() {
AppCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
appImage = {
Icon(
modifier = Modifier
.size(24.dp)
.wrapContentSize(
align = Alignment.Center),
imageVector = Icons.Rounded.Email,
contentDescription = stringResource(
id = R.string.message_icon)
)
},
onClick = { /*Do something*/ },
appName = { stringResource(
id = R.string.notification_message) },
time = { stringResource(id = R.string.time) },
title = { stringResource(
id = R.string.notification_owner) }) {
Text(text = stringResource(
id = R.string.hi_android))
}
}

6. W MainActivity skomentuj inne funkcje, które można komponować, na razie dodaj MessageCard() i uruchom ją:



Jak to działa…

TitleCard i AppCard służą do wyświetlania informacji w systemie Wear OS, ale mają różne cele. W naszym przykładzie używamy AppCard(), ale jak widać na rysunku, TitleCard() pobiera kilka danych wejściowych podobnych do AppCard():



Za pomocą funkcji TitleCard() można wyświetlić informacje istotne w bieżącym kontekście, takie jak nazwa odtwarzanego utworu lub tytuł oglądanego filmu. Zwykle jest wyświetlany u góry ekranu i można go zamknąć, przesuwając go. Dobrym przykładem jest Spotify. Korzystając z AppCard(), możesz wyświetlić informacje o aktualnie uruchomionej aplikacji, takie jak nazwa aplikacji i krótki opis jej działania, tak jak to zrobiliśmy w naszym przykładzie. Zwykle jest wyświetlany na mniejszej karcie, której można dotknąć, aby otworzyć aplikację. Dlatego ma funkcję onClick{/**TODO*/}, która może prowadzić do uzyskania większej ilości informacji. Podejmując decyzję o użyciu TitleCard() lub AppCard(), należy wziąć pod uwagę następujące czynniki:

•  Ilość informacji, które należy wyświetlić
•  Znaczenie informacji w bieżącym kontekście
•  Pożądane doświadczenie użytkownika

Jeśli chcesz wyświetlić dużo informacji, lepszym rozwiązaniem może być TitleCard(). Jeśli potrzebujesz wyświetlić tylko niewielką ilość informacji, lepszym rozwiązaniem może być AppCard(). Jeśli chcesz, aby informacje były istotne w bieżącym kontekście, lepszą opcją może być TitleCard(). Jeśli chcesz, aby informacje były wyświetlane na mniejszej karcie, której można dotknąć, aby otworzyć aplikację, lepszą opcją może być AppCard().

Implementacja chipa i chipa przełączającego

W tym przepisie zbadamy istotne komponenty zużycia; Zarówno chip, jak i chip przełączający służą do wyświetlania danych i interakcji z nimi. Chip to mały, prostokątny element, którego można używać do wyświetlania tekstu, ikon i innych informacji. Zwykle służy do wyświetlania elementów powiązanych lub mających wspólny temat. Chip przełączający to komponent, którego można użyć do przedstawienia wartości binarnej. Zwykle jest używany do reprezentowania takich rzeczy, jak włączenie/wyłączenie, tak/nie lub prawda/fałsz. Warto wspomnieć, że możesz używać tych komponentów w swojej zwykłej aplikacji. Podejmując decyzję, którego komponentu użyć, należy wziąć pod uwagę następujące czynniki:

•  Typ danych, które chcesz wyświetlić
•  Typ interakcji, który chcesz włączyć
•  Wygląd i styl, jaki chcesz osiągnąć

Przygotowywanie się

W tej sekcji będziemy korzystać z naszego już utworzonego projektu.

Jak to zrobić…

W tym przepisie utworzymy chip i chip przełączający. Wykonaj następujące kroki:

1. Przejdźmy dalej i zbudujmy nasz pierwszy chip; wewnątrz pakietu komponentów utwórz plik Kotlin i nadaj mu nazwę ChipExample.kt.
2. W pliku utwórz funkcję umożliwiającą komponowanie o nazwie ChipWearExample().
3. Teraz przejdźmy dalej i wywołajmy funkcję komponowania Chip(). Komponentu Chip można także używać do wyświetlania informacji dynamicznych. Aby to zrobić, możesz użyć właściwości modifier, aby określić funkcję, która zostanie wywołana w celu aktualizacji informacji wyświetlanych na chipie:

@Composable
fun ChipWearExample(){
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
onClick = { /*TODO */ },
label = {
Text(
text = stringResource(
id = R.string.chip_detail),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
icon = {
Icon(
imageVector = Icons.Rounded.Phone,
contentDescription = stringResource(
id = R.string.phone),
modifier = Modifier
.size(24.dp)
.wrapContentSize(
align = Alignment.Center)
)
},
)
}

4. W MainActivity skomentuj istniejące funkcje Composable, dodaj ChipWearExample() i uruchom aplikację:



5. Teraz przejdźmy dalej i utwórzmy chip przełączający; w naszym pakiecie komponentów utwórz plik Kotlin i nadaj mu nazwę ToggleChipExample.

6. W ToggleChipExample utwórz funkcję Composable i nazwij ją ToggleChipWearExample(). Będziemy używać komponentu ToggleChip():

@Composable
fun ToggleChipWearExample() {
var isChecked by remember { mutableStateOf(true) }
ToggleChip(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
checked = isChecked,
toggleControl = {
Switch(
checked = isChecked
)
},
onCheckedChange = {
isChecked = it
},
label = {
Text(
text = stringResource(
id = R.string.alert),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}

7. Na koniec uruchom kod, a powinieneś móc włączać i wyłączać chip w zależności od tego, czy chcesz otrzymywać powiadomienia, czy nie:



? Jak to działa…

Aby zaimplementować chip w Wear OS Jetpack Compose, musimy skorzystać z dostarczonego już komponentu Chip(). Komponent Chip() ma kształt stadionu i jego maksymalną wysokość zaprojektowaną tak, aby zajmowała nie więcej niż dwie linie tekstu i można go używać do wyświetlania tekstu, ikon i innych informacji. Do wyświetlania informacji dynamicznych można także użyć komponentu Chip(). Aby to zrobić, możesz użyć właściwości modyfikatora do określenia funkcji, która zostanie wywołana w celu aktualizacji informacji wyświetlanych na chipie. Możesz przyjrzeć się komponentowi Chip(), aby zobaczyć, jakie przyjmuje parametry. Funkcja komponowania ToggleChip() przyjmuje kilka parametrów; oto kilka znaczących:

•  zaznaczone: Wartość logiczna określająca, czy element przełączający jest aktualnie sprawdzany
•  onCheckedChange: Funkcja lambda, która zostanie wywołana, gdy zmieni się sprawdzony stan układu przełączającego
•  modyfikator: opcjonalny modyfikator, którego można użyć do dostosowania wyglądu lub zachowania elementu przełączającego
•  kolory: opcjonalny obiekt ToggleChipColors, którego można użyć do dostosowania kolorów elementu przełączającego

Do obsługi przepełnionego tekstu używamy TextOverflow, ponieważ mamy do czynienia z małymi ekranami. Więcej szczegółów na temat parametrów ToggleChip przyjmuje rysunek:



Implementacja ScalingLazyColumn w celu zaprezentowania treści

ScalingLazyColumn rozszerza LazyColumn, który jest bardzo potężny w Jetpack Compose. Możesz myśleć o ScalingLazyColumn jako o komponencie Wear OS, który służy do wyświetlania listy elementów, które można przewijać w pionie. Elementy są skalowane i pozycjonowane w oparciu o ich położenie na liście, a całą listę można przewijać, przeciągając górę lub dół listy. Można go użyć np. do wyświetlenia listy komponentów; w naszym przykładzie użyjemy go do wyświetlenia wszystkich elementów, które stworzyliśmy w poprzednich przepisach. Zauważysz również, że użyliśmy go w przepisie Implementowanie listy przewijanej, gdzie mamy listę i wyświetlamy elementy.

Przygotowywanie się

Aby kontynuować korzystanie z tego przepisu, konieczne będzie ukończenie poprzednich przepisów. Dodatkowo w tym przepisie zamiast komentować wszystkie stworzone przez nas elementy, wyświetlimy je jako elementy w kolumnie ScalingLazyColumn.

Jak to zrobić…

Wykonaj poniższe kroki, aby zbudować swoją pierwszą kolumnę ScalingLazyColumn:

1. W MainActivity zauważysz komentarz:
/* Jeśli masz wystarczającą liczbę elementów na liście, użyj
[ScalingLazyColumn], który jest zoptymalizowany
* wersja LazyColumn dla urządzeń do noszenia z kilkoma dodatkowymi funkcjami. Po więcej informacji,
* zobacz d.android.com/wear/compose.
*/

Komentarz jest wezwaniem dla programistów, aby korzystali ze ScalingLazyColumn, która jest zoptymalizowaną wersją LazyColumn dla Wear OS.

2. Musimy zacząć od utworzenia wartości scalingListState i zainicjować ją, aby pamiętać - ScalingLazyListState():

val scalingListState = rememberScalingLazyListState()

Funkcja RememberScalingLazyListState() po prostu robi to, co sugeruje jej definicja, czyli zapamiętuje stan.

3. Będziemy teraz musieli oczyścić naszą funkcję Composable, usuwając dodane przez nas modyfikatory i używając jednego dla wszystkich widoków. Utwórzmy contentModifier = Modifier i jeden dla naszych ikon:

val contentModifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
val iconModifier = Modifier
.size(24.dp)
.wrapContentSize(align = Alignment.Center)

4. Będziemy także musieli utworzyć funkcję Scaffold(), która implementuje strukturę układu wizualnego Wear Material Design. Scaffold() używa modyfikatora, winiety, positionIndicator, pageIndicator, timeText i treści.

5. Przejdźmy dalej i zbudujmy nasz ekran. W Scaffold użyjemy trzech parametrów: winieta (która jest pełnoekranowym miejscem do nałożenia winiety na zawartość szkieletu), positionIndicator i timeText. Zajrzyj do sekcji Jak to działa…, aby dowiedzieć się więcej o parametrach:

Scaffold(timeText = {} , vignette = {}, positionIndicator = {})
{…}

6. W przypadku TimeText wywołamy Modifier.scrollAway i przekażemy scalingListState:

TimeText(modifier = Modifier.scrollAway(scalingListState))

7. Ponieważ mamy tylko jeden ekran dla naszego przykładowego projektu, który można przewijać, postaramy się pokazać wszystkie elementy jednocześnie i przez cały czas. Dlatego w winiecie powiemy, że pozycja będzie TopAndBottom:

Winieta(vignettePosition = VignettePosition.TopAndBottom)

8. Na koniec na positionIndicator po prostu przekażemy scalingListState:

PositionIndicator (scalingLazyListState = scalingListState)

9. Teraz możemy w końcu zbudować naszą funkcję ScalingLazyColumn(). Użyjemy fillMaxSize jako modyfikatora, a autoCentering zostanie ustawiony na indeks zero; następnie dla stanu przekaż już utworzony scalingListState, a w elementach przekaż nasze komponenty:

Scaffold(
timeText = { TimeText(modifier =
Modifier.scrollAway(scalingListState)) },
vignette = { Vignette(vignettePosition =
VignettePosition.TopAndBottom) },
positionIndicator = {
PositionIndicator(
scalingLazyListState = scalingListState
)
}
) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
autoCentering = AutoCenteringParams(
itemIndex = 0),
state = scalingListState
){
item { /*TODO*/ }
item { /*TODO*/ }
item { /*TODO*/ }
item { /*TODO*/ }
}
}

10. Cały kod znajdziesz w sekcji Wymagania techniczne. Aby wyczyścić część kodu w item{}, mamy co następuje:

item { SampleButton(contentModifier) }
item { SampleButton2(contentModifier, iconModifier) }
item { MessageCard(contentModifier, iconModifier) }
item { ChipWearExample(contentModifier, iconModifier) }
item { ToggleChipWearExample(contentModifier) }

11. Wreszcie, po uruchomieniu aplikacji, powinieneś widzieć wszystkie wyświetlane elementy i móc płynnie je przewijać.



Jak to działa…

Wear OS Jetpack Compose to zestaw narzędzi interfejsu użytkownika do tworzenia aplikacji Wear OS przy użyciu platformy Jetpack Compose. Został zaprojektowany, aby ułatwić i zwiększyć efektywność programistom tworzenie aplikacji do noszenia nowoczesnego i responsywnego interfejsu użytkownika. Jak wspomniano wcześniej, funkcja Composable o nazwie Scaffold() ma kilka danych wejściowych. Na rysunku zobaczysz ich znaczenie i powód, dla którego warto z nich skorzystać:



Niektóre ze znaczących zalet Wear OS w Jetpack Compose polegają na tym, że zapewnia zestaw gotowych komponentów interfejsu użytkownika, które są zoptymalizowane pod kątem unikalnych funkcji urządzeń z Wear OS. Jedną z kluczowych korzyści jest to, że upraszcza proces programowania, zmniejszając ilość standardowego kodu wymaganego do utworzenia interfejsu użytkownika. Zapewnia także spójny i elastyczny język projektowania interfejsu użytkownika, którego można używać w różnych aplikacjach. Można dowiedzieć się więcej o Wear OS; ponadto, ponieważ jest to nowa technologia, wiele przedstawionych tutaj koncepcji może ulec zmianie lub udoskonaleniu ze względu na zmiany API w przyszłości, ale na razie możesz dowiedzieć się więcej, klikając ten link: https://developer.android.com/wear .

Alerty GUI - co nowego w menu, oknach dialogowych, tostach, paskach przekąsek i nie tylko w nowoczesnym rozwoju Androida

Alerty graficznego interfejsu użytkownika (GUI) są niezbędne dla użytkowników, ponieważ dostarczają krytycznych informacji o stanie programu lub aplikacji i mogą pomóc użytkownikom uniknąć błędów i podejmować świadome decyzje. Alerty mogą być wyzwalane w różnych sytuacjach, na przykład w przypadku wystąpienia błędu, wykonania przez program krytycznej operacji lub gdy użytkownik ma zamiar wykonać nieodwracalną akcję. Jedną z głównych zalet alertów GUI jest to, że zapewniają natychmiastową informację zwrotną dla użytkowników. Na przykład, jeśli użytkownik wprowadzi nieprawidłowe informacje do formularza, alert może szybko poinformować go o błędzie, umożliwiając jego poprawienie przed kontynuowaniem. Może to pomóc w uniknięciu błędów i zaoszczędzeniu czasu w dłuższej perspektywie. Kolejną zaletą alertów GUI jest to, że mogą pomóc w zapobieganiu przypadkowym działaniom. Na przykład, jeśli użytkownik ma zamiar usunąć ważny plik, alert może ostrzec go o potencjalnych konsekwencjach tego działania, dając mu szansę na ponowne rozważenie przed kontynuowaniem. W tej części zbadamy, jak GUI jest implementowane w nowoczesnym rozwoju Androida.

Tworzenie i wyświetlanie menu w Modern Android Development

Tworzenie menu w aplikacji na Androida może zapewnić kilka korzyści:

•  Menu mogą pomóc użytkownikom w szybkim dostępie do różnych funkcji i funkcjonalności aplikacji. Dobrze zaprojektowane menu może poprawić komfort użytkownika, ułatwiając nawigację i korzystanie z aplikacji.
•  Spójne menu na różnych ekranach aplikacji może pomóc użytkownikom szybko znaleźć to, czego szukają, dzięki czemu aplikacja będzie bardziej dopracowana i profesjonalna.
•  Menu można wykorzystać do grupowania powiązanych opcji i funkcji w jednym miejscu, redukując potrzebę zaśmieconych ekranów z wieloma przyciskami i opcjami.
•  Menu można także dostosować do specyficznych potrzeb aplikacji, włączając różne typy menu, takie jak menu kontekstowe, menu wyskakujące i szuflady nawigacyjne.
•  Menu powinno być zaprojektowane z myślą o dostępności, ułatwiając użytkownikom niepełnosprawnym poruszanie się po aplikacji.

Oznacza to, że utworzenie menu w aplikacji na Androida może poprawić wygodę użytkownika, zapewnić spójność, zaoszczędzić miejsce i zwiększyć dostępność.

Przygotowywanie się

Na potrzeby tego rozdziału utworzymy nowy projekt Material 3 i nazwiemy go GUIAlerts; w tym miejscu dodamy wszystkie komponenty interfejsu użytkownika , a Ty będziesz mógł skorzystać z projektu i zmodyfikować widoki tak, aby odpowiadały Twoim potrzebom.

Jak to zrobić…

Wykonaj poniższe kroki, aby stworzyć swoje pierwsze menu hamburgerowe:

1. W naszym nowo utworzonym projekcie GUIAlerts utwórzmy komponent pakietu, a wewnątrz pakietu utwórz plik Kotlin i nazwijmy go MenuComponent.kt.
2. Utwórzmy funkcję komponowalną OurMenu w naszym pliku Kotlin:

@Composable
fun OurMenu(){ }

3. Teraz przejdźmy dalej i stwórzmy nasze menu. Dla naszych celów zaprezentujemy tylko niektóre elementy, a gdy ktoś kliknie, nic się nie stanie, ponieważ nie zaimplementujemy funkcji onClick. Po pierwsze, musimy się upewnić, że nie zaczyna się ono jako rozwinięte, co oznacza, że użytkownicy będą klikać, aby rozwinąć menu, a w odpowiedzi zmieni się ono na true:

@Composable
fun OurMenu(){
var expanded by remember { mutableStateOf(false) }
val menuItems = listOf("Item 1", "Item 2", "Item 3",
"Item 4") }
W przypadku naszych pozycji menu zaprezentujemy tylko cztery pozycje.

4. Następnie musimy utworzyć Box(), wyrównać go do środka i zareagować na stan rozwinięcia modyfikatora. Będziemy także musieli dodać ikonę ArrowDropDown, aby poinformować użytkowników, że mogą kliknąć i że mamy więcej elementów:

Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clickable { expanded = true }
) {
Text(stringResource(id = R.string.menu))
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = stringResource(
id = R.string.menu_drop_down),
modifier = Modifier.align(Alignment.CenterEnd)
)
}

5. Na koniec będziemy musieli dodać DropDownMenu, które rozwinie się po kliknięciu ikony i ustawimy onDismissRequest na false; jest wywoływana, gdy użytkownik chce zamknąć menu, na przykład podczas stukania.

6. Następnie wyświetlimy nasze pozycje na funkcji DropdownMenuItem tak, aby po jej kliknięciu wykonała akcję. W naszym przykładzie nic nie robimy:

DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
menuItems.forEachIndexed { index, item ->
DropdownMenuItem(text = { Text(item)},
onClick = { /*TODO*/ })
}
}

7. Na koniec, po uruchomieniu aplikacji, powinno pojawić się menu rozwijane z elementami, które możesz kliknąć.



Ważna uwaga: możesz dostosować menu rozwijane do swoich potrzeb i stylu.

Jak to działa…

W naszym przykładzie najpierw deklarujemy zmienną stanu zmienną, rozwiniętą, aby śledzić, czy menu jest rozwinięte, czy nie, oraz inną zmienną stanu, którą można modyfikować, wybraneMenuItem, aby śledzić aktualnie wybraną pozycję menu. Definiujemy również listę elementów menu, która pomaga nam poznać listę menu. Wewnątrz naszego Boxa definiujemy Column(){}, która zawiera tytuł menu, klikalne Box, które wyświetla wybrany element menu, oraz DropdownMenu, które wyświetla elementy menu po rozwinięciu. Używamy komponentów Box i DropdownMenu do pozycjonowania elementów menu względem klikalnego Boxa. Jak widać na rysunku, DropDownMenu pobiera kilka danych wejściowych, co pomaga dostosować menu rozwijane do własnych potrzeb.



Na koniec używamy komponentu DropdownMenuItem do wyświetlania każdego elementu menu i aktualizowania wybranego elementu menu oraz rozwiniętych zmiennych po kliknięciu elementu menu.
Implementacja paska tostów/przekąsek w celu ostrzegania użytkowników


W przypadku Androida pasek Toast/Snackbar to mały wyskakujący komunikat wyświetlany na ekranie, zwykle u dołu. Służy do przekazywania użytkownikowi krótkich informacji lub opinii. Jest to prosty sposób na wyświetlanie użytkownikowi krótkich komunikatów bez zakłócania jego pracy.

Przygotowywanie się

W tej sekcji będziemy reagować na elementy, które stworzyliśmy w naszym DropMenuItem, więc musisz zastosować się do poprzedniego przepisu, aby kontynuować ten.

Jak to zrobić…

Wykonaj poniższe kroki, aby po kliknięciu elementu dodać komunikat informujący użytkowników, że wybrali konkretny element:

1. Tworzenie toastu jest bardzo proste w systemie Android; możesz to po prostu zrobić za pomocą klasy Toast dostępnej w zestawie SDK systemu Android. Możesz utworzyć nowy obiekt Toast, wywołując statyczną metodę makeText() klasy Toast i przekazując jej kontekst, komunikat i czas trwania Toast.

2. Po utworzeniu obiektu Toast możesz wywołać metodę show() w celu wyświetlenia go na ekranie:

Toast.makeText(context, "Hello, Android!", Toast.LENGTH_SHORT).
show();

3. Jednak w Jetpack Compose, aby wyświetlić Toast, będziemy musieli użyć coroutineScope, ale pamiętaj, że nie potrzebujesz współprogramu do wyświetlenia Toastu we wszystkich przypadkach, jednak w naszym przykładzie użyjemy funkcji uruchamiania, aby uruchomić współprogram wyświetlający komunikat Toast:

val coroutineScope = rememberCoroutine()
coroutineScope.launch {
Toast.makeText(
context,
"Selected item: $item",
Toast.LENGTH_SHORT
).show()
}

4. Aby podłączyć onClick(), zobacz kod w sekcji Wymagania techniczne, aby uzyskać cały kod. Na koniec, po uruchomieniu aplikacji, powinien zostać wyświetlony komunikat Toast z wybranym elementem jako komunikat.



5. W poniższym przykładzie zamiast Toastu użyjemy teraz Snackbara:

coroutineScope.launch {
Toast.makeText(
context,
"Selected item: $item",
Toast.LENGTH_SHORT
).show()
}

Istnieją różne sposoby korzystania z paska przekąsek w Jetpack Compose; można go używać z rusztowaniem lub bez niego. Zaleca się jednak używanie Snackbara z rusztowaniem. W naszym przykładzie użyjemy szkieletu:

menuItems.forEachIndexed { index, item ->
DropdownMenuItem(
text = { Text(item) },
onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = "Selected Item: $item"
)
}
}
)
}

6. Na koniec, po uruchomieniu aplikacji, zobaczysz pasek Snackbar z tekstem wybranego elementu i wybranym elementem. Zarówno Tosty, jak i Snackbar służą temu samemu celowi.



Jak to działa…

Tosty i Snackbary to dwa rodzaje powiadomień, których można używać w aplikacjach na Androida do wyświetlania użytkownikowi krótkich wiadomości. Główne różnice między tostami a batonikami są następujące:

•  Komunikaty tostowe są zwykle wyświetlane na środku ekranu, natomiast komunikaty paska Snackbar są zwykle wyświetlane na dole ekranu.
•  Komunikaty tostowe zazwyczaj trwają przez krótki czas, zwykle około 2-3 sekund, a następnie znikają automatycznie. Komunikaty paska Snackbar są zwykle wyświetlane przez dłuższy czas i użytkownik może je zamknąć, przesuwając je lub naciskając przycisk.
•  Wiadomości wyskakujące nie są interaktywne i użytkownik nie może ich odrzucić. Z drugiej strony wiadomości na pasku Snackbar mogą zawierać przyciski akcji, które umożliwiają użytkownikowi podjęcie określonych działań w odpowiedzi na wiadomość.
•  Wiadomości tostowe to zazwyczaj zwykłe wiadomości tekstowe wyświetlane w małym wyskakującym okienku. Z drugiej strony, wiadomości na pasku Snackbar można stylizować tak, aby zawierały ikony, kolory i inne elementy wizualne, aby były bardziej atrakcyjne wizualnie i zawierały więcej informacji.

Modyfikujemy wywołanie zwrotne onClick komponentu DropdownMenuItem, aby uruchomić współprogram wyświetlający komunikat Toast za pomocą funkcji Toast.makeText. Bieżący kontekst przekazujemy za pomocą LocalContext.current, który pobiera funkcję bieżącego kontekstu i tekst do wyświetlenia w wiadomości Toast jako ciąg znaków. Powinieneś także określić czas trwania toastu, krótki lub długi. Korzystając z Snackbar, tworzymy SnackbarHostState, który przekazujemy w naszym Scaffold. Nasz element składowy zawiera parametr przekąskibarHost określający funkcję wyświetlania paska Snackbar, gdy jest on wyświetlany. Funkcja SnackbarHost przyjmuje dwa parametry: przekąskibarData, która zawiera komunikat i przycisk akcji paska Snackbar, oraz lambdę określającą sposób tworzenia elementu składowego paska Snackbar. W systemie Android Scaffold to wstępnie zbudowany komponent interfejsu użytkownika lub układ, który zapewnia niezbędną strukturę do tworzenia ekranów i komponentów interfejsu użytkownika. Termin rusztowanie jest często używany zamiennie z terminem szablon lub szablon. Rusztowania są powszechnie używane w platformach tworzenia aplikacji na Androida, takich jak Flutter lub Jetpack Compose, aby zapewnić punkt wyjścia do tworzenia nowych ekranów lub komponentów interfejsu użytkownika. Na przykład biblioteka Material Design w systemie Android udostępnia kilka gotowych szkieletów dla typowych typów ekranów, takich jak ekran logowania, ekran ustawień lub ekran listy. Te rusztowania zapewniają spójny wygląd i działanie oraz pomagają zapewnić, że aplikacja jest zgodna z wytycznymi Material Design. Korzystanie z szkieletów może zaoszczędzić czas i wysiłek w tworzeniu aplikacji, zapewniając punkt wyjścia do tworzenia ekranów i komponentów interfejsu użytkownika. Jednak programiści mogą również dostosowywać i rozszerzać szkielety, aby spełniały specyficzne wymagania ich aplikacji.

Tworzenie okna dialogowego alertu

Wyskakujące okna dialogowe z alertami są istotnym elementem interfejsu użytkownika w aplikacjach na Androida. Służą do wyświetlania użytkownikowi ważnych komunikatów, powiadomień i ostrzeżeń. Oto kilka powodów, dla których korzystanie z wyskakującego okna dialogowego z alertami jest niezbędne w systemie Android:

•  Mogą pomóc w podkreśleniu ważnych informacji, które użytkownik powinien znać. Na przykład, jeśli użytkownik ma zamiar wykonać czynność powodującą utratę lub uszkodzenie danych, aplikacja może wyświetlić komunikat ostrzegawczy w wyskakującym oknie dialogowym z ostrzeżeniem, aby upewnić się, że użytkownik zna konsekwencje.
•  Można ich używać do uzyskania potwierdzenia przez użytkownika niezbędnych działań, takich jak usunięcie pliku lub zakup czegoś. Wyświetlając komunikat proszący użytkownika o potwierdzenie akcji, aplikacja może pomóc zapobiec przypadkowym lub niechcianym działaniom.
•  Można ich używać do przekazywania użytkownikowi informacji zwrotnych, na przykład informowania go o powodzeniu lub niepowodzeniu działania. Na przykład, jeśli użytkownik spróbuje zapisać plik, który już istnieje, aplikacja może wyświetlić wyskakujące okno dialogowe z alertem, które informuje użytkownika o problemie i podaje sugestie dotyczące dalszego postępowania.
•  Mogą pomóc w poprawie ogólnego doświadczenia użytkownika w aplikacji, dostarczając precyzyjne i zwięzłe komunikaty, które pomagają użytkownikom zrozumieć, co dzieje się w aplikacji.
Okna dialogowe alertów są istotnym elementem projektowania aplikacji na Androida. Można ich używać do podkreślania ważnych informacji, uzyskiwania potwierdzeń użytkownika, przekazywania opinii i poprawy ogólnego doświadczenia użytkownika.

Przygotowywanie się

Będziemy nadal korzystać z tego samego projektu, więc upewnij się, że ukończyłeś poprzednie przepisy. Aby kontynuować, upewnij się, że otrzymałeś kod w sekcji Wymagania techniczne.

Jak to zrobić…

Wykonaj poniższe kroki, aby utworzyć okno dialogowe alertu:

1. Zacznijmy od utworzenia pliku Kotlin i wywołania go AlertDialogDemo.kt.

2. W programie AlertDialogDemo utwórz funkcję składalną i nazwij ją AlertDialogExample():

@Composable
fun AlertDialogExample() {…}

Istnieją różne sposoby implementacji AlertDialog(); w naszym przykładzie utworzymy przycisk, którego kliknięcie spowoduje uruchomienie okna dialogowego().

3. Następnie musimy dodać właściwości tytułu i tekstu do AlertDialog. Komponentu Text używamy do zdefiniowania tytułu i tekstu wiadomości oraz ustawienia właściwości czcionki i koloru zgodnie z potrzebami:

AlertDialog(
onDismissRequest = { dialog.value = false },
title = {
Text(
text = stringResource(
id = R.string.title_message),
fontWeight = FontWeight.Bold,
color = Color.Black
)
},
text = {
Text(
text = stringResource(id = R.string.body),
color = Color.Gray
)
},


4. Następnie do AlertDialog dodamy właściwości confirmButton i releaseButton. Do zdefiniowania przycisków używamy komponentu Button i ustawiamy właściwość onClick na lambdę, która po kliknięciu przycisku wykona odpowiednią akcję:

confirmButton = {
Button(
onClick = {/*TODO*/ }
) {
Text(text = stringResource(
id = R.string.ok))
}
},
dismissButton = {
Button(
onClick = { dialog.value = false }
) {
Text(text = stringResource(
id = R.string.cancel))
}
},
)
}


5. Na koniec, po uruchomieniu aplikacji, zobaczysz okno dialogowe z tytułem, komunikatem i dwoma wezwaniami do działania: Potwierdź lub Anuluj.



Jak to działa…

W naszym przykładzie najpierw tworzymy zmienną mutableStateOf o nazwie openDialog z wartością logiczną wskazującą, czy okno dialogowe powinno być wyświetlane. Następnie używamy tej zmiennej do warunkowego renderowania komponentu AlertDialog przy użyciu instrukcji if. Komponent AlertDialog ma kilka właściwości, które możemy ustawić, w tym tytuł, tekst, przycisk potwierdzenia i przycisk zwolnienia. Kolory tła i treści możemy również ustawić za pomocą właściwości tłaColor i contentColor. Na koniec dodajemy komponent Button, który po kliknięciu przełącza zmienną openDialog, powodując wyświetlenie lub ukrycie okna dialogowego.

Tworzenie okna dialogowego dolnego arkusza

Okna dialogowe w dolnym arkuszu są popularnym wzorcem projektowym w systemie Android, ponieważ zapewniają prosty i skuteczny sposób wyświetlania informacji kontekstowych lub działań bez zajmowania zbyt dużej ilości miejsca na ekranie. Oto kilka powodów, dla których okna dialogowe w dolnym arkuszu są uważane za dobry wybór podczas tworzenia aplikacji na Androida:

•  Są zaprojektowane tak, aby wysuwać się z dołu ekranu, zajmując minimalną przestrzeń na ekranie. Dzięki temu są doskonałą opcją do wyświetlania dodatkowych informacji lub działań bez przytłaczania użytkownika.
•  Są często używane do zapewnienia dodatkowych informacji istotnych dla bieżącego kontekstu, takich jak opcje lub ustawienia specyficzne dla bieżącego widoku.
•  Ponieważ okna dialogowe w dolnym arkuszu zaprojektowano tak, aby wysuwały się z dołu ekranu, dają użytkownikom poczucie kontroli nad interakcją. Użytkownicy mogą łatwo zamknąć okno dialogowe, przesuwając je w dół lub dotykając poza oknem dialogowym.

Ogólnie rzecz biorąc, okna dialogowe w dolnym arkuszu są doskonałym wyborem, ponieważ zapewniają oszczędzający miejsce, kontekstowy i przyjazny dla użytkownika sposób wyświetlania dodatkowych informacji lub działań.

Przygotowywanie się

Będziemy nadal korzystać z tego samego projektu, więc upewnij się, że ukończyłeś poprzednie przepisy.

Jak to zrobić…

Korzystając z tego samego projektu, wykonaj następujące kroki, aby zbudować pierwsze okno dialogowe BottomSheet:

1. Zacznijmy od utworzenia pliku Kotlin i nazwania go BottomSheetDemo.kt.
2. W programie BottomSheetDemo utwórz funkcję składalną i nadaj jej nazwę BottomSheetExample():

@Composable
fun BottomSheetExample() {…}

3. Ponieważ używamy Materiału 3, przyznajemy, że większość interfejsów API jest wciąż w fazie eksperymentalnej, co oznacza, że wiele może się zmienić. Stwórzmy nasz stan dla naszego dolnego okna dialogowego arkusza:

val bottomSheetState =
rememberModalBottomSheetState(skipPartiallyExpanded = true)

Wartość logiczna skipPartiallyExpanded sprawdza, czy stan częściowo rozwinięty powinien zostać pominięty, jeśli arkusz jest wystarczająco wysoki.

4. Teraz musimy przejść dalej i utworzyć naszą ModalBottomSheet(), która przyjmuje kilka parametrów; użyjemy po prostu onDismiss i sheetState:

ModalBottomSheet(
onDismissRequest = { openBottomSheet = false },
sheetState = bottomSheetState,
) {
Column(Modifier.fillMaxWidth(),
horizontalAlignment =
Alignment.CenterHorizontally) {
Button(
onClick = {
coroutineScope.launch {
bottomSheetState.hide() }
.invokeOnCompletion {
if (
!bottomSheetState.isVisible
) {
openBottomSheet = false
}
}
}
) {
Text(text = stringResource(
id = R.string.content))
}


5. Podążając za sekcją Wymagania techniczne, przejdźmy teraz do implementacji dwóch przycisków i uzyskania całego kodu.
6. Na koniec uruchom aplikację, a okno dialogowe dolnego arkusza zostanie zaimplementowane. Pamiętaj, że możesz dodać więcej logiki w zależności od potrzeb.



Jak to działa…

W naszym przykładzie ModalBottomSheet jest używany jako alternatywa dla wbudowanych menu lub prostych okien dialogowych na urządzeniach mobilnych, zwłaszcza gdy oferuje długą listę elementów akcji lub gdy elementy wymagają dłuższych opisów i ikon. Podobnie jak w każdym innym oknie dialogowym w systemie Android, modalne dolne arkusze pojawiają się przed zawartością aplikacji.

Tworzenie przycisku radiowego<

W nowoczesnym rozwoju Androida RadioButton jest używany podobnie jak w tradycyjnym rozwoju Androida. RadioButton pozwala użytkownikom wybrać pojedynczy element z listy wzajemnie wykluczających się opcji, co oznacza, że jednocześnie można wybrać tylko jedną opcję. W Jetpack Compose RadioButton jest częścią biblioteki Material Design i można go używać, importując pakiet Androidx.compose.Material.RadioButton. Aby utworzyć grupę instancji RadioButton, zazwyczaj użyjesz elementu składowego RadioGroup, który jest również częścią biblioteki Material Design. Komponent RadioGroup przyjmuje jako dane wejściowe listę opcji wraz z wybraną opcją i wywołaniem zwrotnym, które jest wywoływane, gdy wybrana opcja ulegnie zmianie. Poszczególne instancje RadioButton można utworzyć za pomocą komponentu RadioButton i dodać je jako elementy podrzędne RadioGroup.

Przygotowywanie się

W tym przepisie będziemy nadal korzystać z tego samego projektu, więc upewnij się, że ukończyłeś poprzednie przepisy.

Jak to zrobić…

Korzystając z tego samego projektu, wykonaj następujące kroki, aby zbudować swój pierwszy RadioButton:

1. Zacznijmy od utworzenia pliku Kotlin i nazwania go RadioButtonDemo.kt.

2. W RadioButtonDemo utwórz funkcję do komponowania i nadaj jej nazwę RadioButtonExample():

@Composable
fun RadioButtonExample() {…}

3. Zaczniemy tworzyć listę wyborów i w naszym przykładzie możemy wykorzystać owoce, a następnie będziemy śledzić wybrane wybory:

val choices = listOf("Mangoes", "Avocado", "Oranges")
var selectedOption by remember {
mutableStateOf(choices[0]) }

4. Ponieważ korzystamy z narzędzia do tworzenia RadioButton dostarczonego przez Google, w zależności od Twoich potrzeb możesz dostosować swój RadioButton w dowolny sposób:

Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedOption == option,
onClick = { selectedOption = option }
)
Text(
text = option,
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(start = 6.dp)
)
}

5. Na koniec, kiedy uruchomimy aplikację, powinniśmy zobaczyć coś podobnego do rysunku



Jak to działa…



W naszym przykładzie tworzymy funkcję kompozytorską RadioButtonExample(), która wyświetla grupę instancji RadioButton z następującymi opcjami: Mango, Awokado i Pomarańcze. Wybrana opcja jest przechowywana w zmiennej wybranejOpcji przy użyciu funkcji Remember Composable, aby zachować stan podczas rekompozycji. Każdy RadioButton jest opakowany w funkcję Row(){...} zawierającą wybrany tekst, a wybrana właściwość RadioButton jest ustawiana na podstawie tego, czy bieżąca opcja pasuje do wybranej opcji. Gdy użytkownik kliknie przycisk RadioButton, zmienna wybranej opcji zostanie zaktualizowana o nowo wybraną opcję.

Tworzenie FAB/rozszerzonego FAB

FAB to okrągły przycisk, który wydaje się unosić nad interfejsem użytkownika aplikacji na Androida. Często jest używany do reprezentowania głównej akcji w aplikacji i jest umieszczony w widocznym miejscu, aby zapewnić łatwy dostęp. Rozszerzony FAB to odmiana FAB w systemie Android, która zapewnia użytkownikom więcej opcji i funkcjonalności. Rozszerzony FAB to prostokątny przycisk, który może wyświetlać tekst i ikonę, a następnie się rozwija po naciśnięciu do menu powiązanych działań.

Przygotowywanie się

W tym przepisie będziemy nadal korzystać z tego samego projektu, więc upewnij się, że ukończyłeś poprzednie przepisy.

Jak to zrobić…

Korzystając z tego samego projektu, wykonaj następujące kroki, aby zbudować FAB i rozszerzony FAB:

1. Zacznijmy od utworzenia pliku Kotlin i nazwania go ActionComponentsDemo.kt.

2. W ActionComponentsDemo utwórz funkcję składalną i nazwij ją ActionComponentsExample():

@Composable
fun RadioButtonExample() {…}

3. W ActionComponentsDemo utwórz funkcję składalną i nazwij ją ActionComponentsExample().

4. Zaczniemy od stworzenia FAB-a. FloatingActionButton to okrągły przycisk unoszący się nad interfejsem użytkownika i zwykle używany do wyzwalania podstawowej akcji w aplikacji. Możesz użyć FloatingActionButton w Jetpack Compose, aby utworzyć element kompozytowy FloatingActionButton:

FloatingActionButton(onClick = { /* do something */ }) {
Icon(Icons.Default.Add, contentDescription =
stringResource(id = R.string.add))
}

ExtendedFloatingActionButton to instancja FloatingActionButton z dodatkowym tekstem lub ikonografią. Często jest używany do drugorzędnych działań w aplikacji.

5. Aby utworzyć przycisk ExtendedFloatingActionButton w Jetpack Compose, możesz użyć elementu kompozycyjnego ExtendedFloatingActionButton. Ten kod tworzy go z etykietą tekstową "Dodaj element" i ikoną plusa. Parametr onClick określa akcję, która ma zostać wykonana po kliknięciu przycisku:

ExtendedFloatingActionButton(
text = { Text("Add item") },
onClick = { /* do something */ },
icon = {
Icon(
Icons.Default.Add,
contentDescription = stringResource(
id = R.string.add)
)
}
)

6. Po uruchomieniu aplikacji powinieneś zobaczyć dwa przyciski, przycisk pływający i przycisk rozszerzony:



Jak to działa…

Rozszerzony FAB jest podobny do FAB, ale zapewnia dodatkowe miejsce na tekst i/lub ikonę. Zwykle służy do zapewnienia dodatkowego kontekstu lub informacji o akcji, która zostanie wykonana po dotknięciu przycisku. Na przykład rozszerzony pływający przycisk akcji (EFAB) może wyświetlać tekst Utwórz nowy budżet wraz z ikoną pióra. Zarówno FAB, jak i EFAB są częścią wytycznych Material Design i są dostępne jako komponent w Jetpack Compose.

Wskazówki i porady dotyczące Android Studio, które pomogą Ci podczas programowania

Dla programisty Androida pisanie kodu nie powinno być tylko celem końcowym; raczej zrozumienie, jak znaleźć problemy w aplikacjach, korzystanie ze wskazówek dotyczących formatowania w celu szybszego poruszania się po bazie kodu i inne umiejętności się przydadzą. Proces programowania wymaga dużej współpracy. Może to dotyczyć przeglądu kodu równorzędnego, programowania w parach lub problemów z debugowaniem. W takich sytuacjach przydatne jest szybkie działanie, na przykład podczas debugowania lub formatowania kodu, przed przesłaniem żądania ściągnięcia. W tym rozdziale poznasz świetne wskazówki i triki dotyczące Gita i Android Studio, które pomogą Ci w codziennym rozwoju.

Znaczenie profilowania aplikacji na Androida

W systemie Android profilowanie to proces analizowania wydajności aplikacji w celu określenia jej mocnych i słabych stron. Profilowanie aplikacji na Androida jest kluczowe z następujących powodów:

•  Pomaga zidentyfikować wąskie gardła wydajności, takie jak powolny kod, wycieki pamięci i nadmierne użycie procesora. Ta wiedza może pomóc Ci zoptymalizować kod i sprawić, że Twoja aplikacja będzie działać wydajniej.
•  Poprawia komfort użytkowania. Słabo działająca aplikacja może prowadzić do frustracji użytkowników i negatywnych recenzji. Profilując aplikację i optymalizując jej działanie, możesz zapewnić lepsze doświadczenie użytkownika, co przełoży się na większe zaangażowanie użytkowników i pozytywne recenzje.
•  Pomaga zaoszczędzić czas i pieniądze. O wiele łatwiej i taniej jest naprawić problemy z wydajnością na wczesnym etapie, niż próbować je naprawić później, gdy staną się bardziej złożone.

Dlatego w tym przepisie sprawdzimy, dlaczego profilowanie aplikacji na Androida jest niezbędne, a także przyjrzymy się najlepszym praktykom i wskazówkom.

Przygotowywanie się

W przypadku tego przepisu przyjrzymy się, jak używać Profilera do profilowania naszej aplikacji na Androida. Nie musisz tworzyć nowego projektu; możesz po prostu skorzystać z istniejącego projektu i kontynuować go.

Jak to zrobić…

Wykonaj poniższe kroki, aby rozpocząć korzystanie z Profilera w systemie Android:

1. W tym rozdziale używamy Android Studio Flamingo 2022.2.1 Patch 1. W swoim Android Studio przejdź do Widok | Okna narzędziowe | Profiler i kliknij Profiler, który go uruchomi.
Uwaga: Aby zobaczyć jakąkolwiek aktywność, musisz uruchomić emulator.



2. Możesz także przejść do dolnej opcji menu, obok Inspekcji aplikacji; spójrz na zieloną strzałkę na rysunku, która wskazuje inne miejsce, z którego możesz uruchomić Profiler. Strzałka odczytu wskazuje emulator podłączony do wizualizacji profilu.



3. Kiedy Twój Profiler zacznie działać, co oznacza, że będzie podłączony do Twojej aplikacji, powinieneś zobaczyć CPU, PAMIĘĆ i ENERGIĘ. W zależności od zasobów aplikacji dane mogą się różnić od tych widocznych na rysunku



4. Możesz wiele zrobić, na przykład po prostu zapisać wszystkie ślady swojej metody, sprawdzić, w jaki sposób wykorzystywane są Twoje zasoby i przeanalizować wykres płomienia.



5. Wykres płomienia procesora to rodzaj wizualizacji wydajności, która pokazuje hierarchiczną strukturę wykonywania programu w czasie. Zwykle u góry wykresu znajduje się oś czasu z wywołaniami funkcji przedstawionymi w postaci prostokątów ułożonych pionowo. W zależności od koloru szerokość każdego prostokąta reprezentuje czas trwania wywołania funkcji, a kolor prostokąta reprezentuje wykorzystanie procesora przez tę funkcję. Wykres pozwala programistom Androida określić, które funkcje szybko zajmują najwięcej czasu procesora i może pomóc im poinformować, gdzie debugować i optymalizować wydajność, jak pokazano na rysunku



Innymi słowy, sterta aplikacji to dedykowana, twarda i ograniczona pula pamięci przydzielona Twojej aplikacji.

Uwaga: jeśli aplikacja osiągnie pojemność sterty i spróbuje przydzielić dodatkową pamięć, zostanie wyświetlony komunikat OutOfMemoryError.

6. Wreszcie wyciek pamięci to błąd oprogramowania, w którym program lub aplikacja wielokrotnie nie zwalnia pamięci, której już nie potrzebuje, lub moduł zbierający elementy bezużyteczne nie działa zgodnie z oczekiwaniami. Może to spowodować, że program z biegiem czasu będzie stopniowo zużywał coraz więcej pamięci, co ostatecznie doprowadzi do słabej wydajności lub awarii aplikacji.

Jak to działa

Aplikacja działa słabo, jeśli reaguje powoli, ma nierówne animacje, często się zawiesza lub zużywa dużo energii. Naprawianie problemów z wydajnością polega na identyfikowaniu obszarów, w których działa aplikacja aby nie optymalizować wykorzystania zasobów, takich jak procesor, pamięć, grafika, sieć lub bateria urządzenia. Android Studio oferuje kilka narzędzi pomagających programistom wykryć i wizualizować potencjalne problemy:

•  CPU Profiler, który pomaga śledzić problemy z wydajnością w czasie wykonywania
•  Profiler pamięci, który pomaga śledzić alokację pamięci
•  Network Profiler, który monitoruje wykorzystanie ruchu sieciowego
•  Energy Profiler, który śledzi zużycie energii, które może przyczynić się do zużycia baterii

•  profilowaniu w systemie Android możesz pomyśleć przez pryzmat sprawdzania, ulepszania i monitorowania bazy kodu.



Szybkie skróty do Androida, które przyspieszają rozwój

Skróty mogą być pomocne dla programistów, przyspieszając i usprawniając ich pracę, pozwalając im skupić się na pisaniu kodu i rozwiązywaniu problemów, a nie na poruszaniu się po menu i paskach narzędzi. Skróty mogą pomóc w automatyzacji powtarzalnych zadań, takich jak formatowanie kodu, zmiana nazw zmiennych lub nawigacja między plikami, uwalniając czas i energię mentalną programistów do bardziej znaczącej pracy. Ponadto, gdy programiści używają tych samych skrótów w różnych narzędziach i aplikacjach, może to pomóc w utrzymaniu spójności ich przepływów pracy i zmniejszeniu ryzyka błędów spowodowanych przypadkowym użyciem niewłaściwego polecenia lub narzędzia. Ponadto dla programistów niepełnosprawnych lub z ograniczeniami fizycznymi używanie skrótów może być bardziej przystępnym sposobem interakcji z oprogramowaniem niż używanie myszy lub gładzika.

Przygotowywanie się

To nie jest tak naprawdę przepis, ale lista przydatnych skrótów. Przyjrzymy się powszechnie używanym skrótom w systemach Windows i Mac, które są najpopularniejszymi systemami operacyjnymi używanymi na laptopach.

Jak to zrobić

Oto kilka skrótów Android Studio w systemach operacyjnych Mac i Windows, które mogą pomóc przyspieszyć pracę:

•  Oto kilka podstawowych skrótów nawigacyjnych:

- Otwórz klasę lub plik: Ctrl + N (Windows) lub Cmd + O (Mac)
- Znajdź tekst w całym projekcie: Ctrl + Shift + F (Windows) lub Cmd + Shift + F (Mac)
- Wyskakujące okienko Otwórz ostatnie pliki: Ctrl + E (Windows) lub Cmd + E (Mac)
- Wyszukaj i wykonaj dowolną akcję lub polecenie: Ctrl + Shift + A (Windows) lub Cmd + Shift + A (Mac)

•  Skróty do edycji kodu:

- Sugestie uzupełnienia kodu: Ctrl + spacja (Windows i Mac)
- Uzupełnij bieżące zestawienie: Ctrl + Shift + Enter (Windows) lub Cmd + Shift + Enter (Mac)
- Zduplikuj bieżącą linię: Ctrl + D (Windows) i (Mac) Cmd + D
- Wytnij bieżącą linię: Ctrl + X (Windows) i (Mac) Cmd + X
- Przesuń bieżącą linię w górę lub w dół: Ctrl + Shift + góra/dół (Windows) lub Cmd + Shift + góra/dół (Mac)

•  Skróty refaktoryzacji:

- Wyodrębnij metodę z bieżącego bloku kodu: Ctrl + Alt + M (Windows) i Cmd + Opcja + M (Mac)
- Wyodrębnij zmienną z bieżącego bloku kodu: Ctrl + Alt + V (Windows) i Cmd + Opcja + V (Mac)
- Wyodrębnij pole z bieżącego bloku kodu: Ctrl + Alt + F (Windows) i Cmd + Option + F (Mac)
- Zmień nazwę klasy, metody lub zmiennej: Shift + F6 (Windows) i Fn + Shift +F6 (Mac)

•  Skróty debugowania:

- Przejdź do następnej linii kodu: F8 (Windows i Mac)
- Wejdź do bieżącej linii kodu: F7 (Windows i Mac)
- Wyjdź z obecnej metody: Shift + F8 (Windows i Mac)
- Przełącz punkt przerwania w bieżącej linii kodu: Ctrl + F8 (Windows) lub Cmd + F8 (Mac)

•  Różne skróty:

- Uruchom aplikację: Ctrl + Shift + F10 (Windows) lub Cmd + Shift + F10 (Mac)
- Aplikacja do debugowania: Ctrl + Shift + F9 (Windows) lub Cmd + Shift + F9 (Mac)

Ważna uwaga: należy pamiętać, że niektóre skróty mogą się różnić w zależności od układu klawiatury i preferencji systemu operacyjnego. Pamiętaj też, że dostępnych jest znacznie więcej skrótów do Android Studio, więc koniecznie przejrzyj ustawienia mapy klawiszy, aby znaleźć dodatkowe skróty, które mogą usprawnić proces programowania.

Jak to działa…

Skróty mogą być potężnym narzędziem dla programistów, którzy chcą usprawnić przepływ pracy, poprawić produktywność i zmniejszyć ryzyko błędów i powtarzających się kontuzji podczas programowania.

JetBrains Toolbox i niezbędne wtyczki, które warto znać

JetBrains Toolbox to narzędzie do zarządzania oprogramowaniem, które umożliwia programistom zarządzanie i instalowanie IDE JetBrains i powiązanych narzędzi na ich komputerach. JetBrains to firma zajmująca się oprogramowaniem, która zapewnia potężne IDE (mianowicie IntelliJ) dla różnych języków programowania, takich jak Java, Kotlin, Python, Ruby i JavaScript. Innymi słowy, wtyczka to po prostu dowolna klasa, która implementuje interfejs wtyczki. Oto niektóre funkcje JetBrains Toolbox i powody, dla których warto spróbować z niego skorzystać:

•  Możesz łatwo pobrać i zainstalować dowolne IDE JetBrains z Toolbox. Zapewnia również, że masz zainstalowaną najnowszą wersję IDE na swoim komputerze.
•  Toolbox automatycznie sprawdza dostępność aktualizacji i aktualizuje wszystkie zainstalowane IDE i wtyczki JetBrains, co oznacza, że jeśli Twoje obecne Android Studio nie jest stabilne, możesz powrócić do bardziej stabilnej wersji.
•  Możesz zarządzać swoimi licencjami JetBrains i aktywować/dezaktywować je z poziomu Toolbox.
•  Toolbox umożliwia udostępnianie projektów członkom zespołu poprzez utworzenie łącza do udostępniania.
•  Toolbox integruje się z usługami JetBrains, takimi jak JetBrains Account i JetBrains Space.

Przygotowywanie się

To nie jest tak naprawdę przepis, ale lista przydatnych wtyczek. Przyjrzymy się kilku przydatnym wtyczkom dla programistów.

Jak to zrobić…

Przyjrzyjmy się, jak możemy wykorzystać Gradle w codziennym rozwoju Androida.

•  Gradle to narzędzie do automatyzacji kompilacji, używane do tworzenia i wdrażania aplikacji na Androida. Może pomóc w zarządzaniu zależnościami, generowaniu plików APK i uruchamianiu testów.
•  Wtyczka ADB zapewnia graficzny interfejs użytkownika dla Android Debug Bridge (ADB), narzędzia wiersza poleceń, które może wchodzić w interakcję z urządzeniem z Androidem lub emulatorem.
•  Szablony aktywne umożliwiają szybkie wstawianie często używanych fragmentów kodu. Możesz na przykład utworzyć aktywny szablon wiadomości toastowej, a następnie po prostu wpisać skrót i nacisnąć klawisz Tab, aby wstawić kod. Aby utworzyć szablon na żywo, przejdź do Android Studio | Ustawienia | Redaktor | Szablony na żywo.



•  Funkcja uzupełniania kodu w Android Studio pozwala zaoszczędzić dużo czasu. Podczas pisania Android Studio będzie sugerować możliwe uzupełnienia kodu. Użyj klawisza Tab, aby zaakceptować sugestię.
•  Debuger to potężne narzędzie do wyszukiwania i naprawiania błędów w kodzie. Dowiesz się, jak korzystać z debugera, aby przejść przez kod i zobaczyć, co dzieje się na każdym kroku, w przepisie na debugowanie kodu.
•  Edytor układu Android Studio umożliwia łatwe tworzenie i modyfikowanie interfejsu użytkownika aplikacji. Za pomocą edytora układu możesz przeciągać i upuszczać komponenty interfejsu użytkownika na swój układ oraz łatwo modyfikować ich właściwości.
•  Menedżer zasobów umożliwia łatwe zarządzanie zasobami aplikacji, takimi jak obrazy, ciągi znaków i kolory. Możesz użyć menedżera zasobów, aby dodawać i modyfikować zasoby oraz łatwo odwoływać się do nich w swoim kodzie.
•  Android Studio obsługuje wiele wtyczek, które mogą rozszerzyć jego funkcjonalność. Możesz także łatwo wyszukiwać wtyczki, które pomogą Ci w zadaniach takich jak generowanie kodu lub zarządzanie zależnościami.
•  LeakCanary to biblioteka do wykrywania wycieków pamięci, która może pomóc w identyfikowaniu i naprawianiu wycieków pamięci w aplikacji. Pomaga to programistom w znajdowaniu wycieków. •  Firebase to pakiet mobilnych narzędzi programistycznych, których można używać do dodawania do aplikacji takich funkcji, jak uwierzytelnianie, analizy i przesyłanie wiadomości w chmurze. Możesz to wykorzystać, budując swój pierwszy projekt jako niezależny programista.

Jak to działa…

Możesz łatwo znaleźć Keymap, po prostu przechodząc do Android Studio | Ustawienie | Mapa klawiszy i korzystając z menu rozwijanego, aby zobaczyć, jakie mapy klawiszy są dostępne, jak pokazano na rysunku



Debugowanie kodu

Dla programisty Androida debugowanie jest istotną częścią procesu tworzenia oprogramowania, ponieważ pomaga identyfikować i naprawiać błędy w kodzie. Podczas debugowania możesz szybko zidentyfikować i naprawić błędy lub błędy w kodzie, które mogą powodować awarię aplikacji, nieoczekiwane zachowanie lub generowanie nieprawidłowych wyników. W tym przepisie przyjrzymy się, jak łatwo dodać punkt przerwania i debugować kod.

Przygotowywanie się

Aby rozpocząć korzystanie z tego przepisu, musisz otworzyć projekt i uruchomić go na emulatorze. Nie musisz tworzyć nowego projektu i możesz skorzystać z projektu GUIAlert.

Jak to zrobić…

Będziemy próbować zdebugować nasz kod i upewnić się, że po kliknięciu pozycji w menu wybieramy właściwą pozycję. Na przykład, jeśli wybierzemy element 2, podczas oceniania elementu powinniśmy zobaczyć wynik 2:

1. Najpierw upewnij się, że Twoja aplikacja działa; następnie kliknij ikonę pokazaną na rysunku



2. Po kliknięciu ikony pokazanej na rysunku powyżej pojawi się wyskakujące okienko, co oznacza, że podłączysz uruchomioną aplikację do debuggera.



3. Teraz wróć do bazy kodu i dodaj punkty przerwania. Dodajesz punkty przerwania, klikając na pasku bocznym numer linii, w której chcesz przetestować swoją logikę.



4. Jeśli Twoja aplikacja jest uruchomiona, gdy klikniesz element, powiedzmy opcję 1, debuger pokaże stan aktywny, co oznacza, że linie, na których umieściliśmy punkty przerwania, zostały trafione. Następnie pojawi się wyskakujące okienko z elementami sterującymi.



5. Możesz użyć zielonego przycisku po lewej stronie, aby uruchomić i czerwonego kwadratowego przycisku, aby się zatrzymać. Możesz także użyć opcji Step Over, Step In, Force Step In, Step Out, Drop Frame, Run to Cursor i Evaluate Expression…. W naszym przykładzie użyjemy wyrażenia Evaluate Expression…



6. Czasami możesz mieć dodatkowe punkty przerwania, które mogą spowolnić proces. W takim przypadku możesz użyć opcji wskazanej czerwoną strzałką na rysunku, aby zobaczyć wszystkie punkty przerwania.



7. Na koniec, gdy aplikacja jest nadal w trybie debugowania, otwórz sekcję Oceń, wróć do kroku 5 tego przepisu i wprowadź Element. W zależności od bieżącego elementu powinieneś zobaczyć wyświetlony numer.



Jak to działa…

Android Studio zawiera potężny debuger, z którego mogą korzystać programiści. Aby debugować aplikację za pomocą Android Studio, musisz najpierw skompilować i wdrożyć aplikację na urządzeniu lub emulatorze, a następnie podłączyć debuger do działającego procesu. Jest to także umiejętność, której należy się uczyć i ćwiczyć, aby stać się w niej dobrym. Dlatego przydaje się wiedza o tym, jak debugować aplikację przy użyciu dzienników lub punktów przerwania.

Jak wyodrębnić metody i parametry metod

Wyodrębnianie metod i parametrów metod może spowodować dodanie dodatkowych importów do kodu. Dzieje się tak, ponieważ podczas wyodrębniania metody lub parametru kod znajdujący się wcześniej w metodzie lub parametr zostaje przeniesiony do osobnej metody. Jeśli ten kod opiera się na innych klasach lub metodach, które nie zostały jeszcze zaimportowane do Twojego kodu, proces wyodrębniania może automatycznie dodać niezbędny import oświadczenia do swojego pliku. Załóżmy na przykład, że masz klasę Kotlin zawierającą metodę wykonującą pewne obliczenia i zwracającą wynik. Ta metoda opiera się na klasie pomocniczej zdefiniowanej w innym pakiecie i nadal musisz zaimportować tę klasę do swojego kodu. Jeśli zdecydujesz się wyodrębnić metodę do konkretnej metody w tej samej lub innej klasie, proces wyodrębniania może dodać instrukcję importu dla klasy pomocniczej, dzięki czemu kod wewnątrz wyodrębnionej metody będzie mógł odwoływać się do klasy pomocniczej. Podobnie podczas wyodrębniania parametru metody proces wyodrębniania może wymagać dodania instrukcji importu, aby zapewnić prawidłowe rozpoznanie wszystkich klas lub interfejsów używanych w typie parametru.

Przygotowywanie się

Aby skorzystać z tego przepisu, nie musisz tworzyć żadnego projektu.

Jak to zrobić…

Aby wyodrębnić metody i parametry metod w systemie Android, możesz wykonać następujące kroki:

1. Otwórz plik Kotlin, z którego chcesz wyodrębnić metody i parametry.
2. Zidentyfikuj klasę zawierającą metody i parametry, które chcesz wyodrębnić.
3. Umieść kursor wewnątrz deklaracji klasy i kliknij prawym przyciskiem myszy, aby otworzyć menu kontekstowe.
4. Wybierz opcję Refaktoryzuj z menu kontekstowego, a następnie wybierz Wyodrębnij z podmenu.
5. W podmenu Extract zobaczysz opcje wyodrębnienia metody lub parametru. Wybierz opcję pasującą do elementu, który chcesz wyodrębnić.
6. Postępuj zgodnie z instrukcjami kreatora wyodrębniania, aby skonfigurować proces wyodrębniania. Może być konieczne podanie nazwy wyodrębnionego elementu, określenie zakresu elementu lub skonfigurowanie innych ustawień w zależności od wyodrębnianego elementu.
7. Po skonfigurowaniu procesu wyodrębniania kliknij Zakończ, aby wyodrębnić element z kodu.

Jak to działa…

Dodawanie importów podczas wyodrębniania metod lub parametrów jest normalną częścią procesu refaktoryzacji i pomaga zapewnić dobrą organizację kodu i łatwość konserwacji.

Zrozumienie podstaw Gita

Ten przepis ma pomóc nowym programistom, którzy mogli natknąć się na tę książkę. Git to popularny system kontroli wersji, który pozwala programistom zarządzać zmianami w bazie kodu i śledzić je. Oto kilka podstawowych pojęć, które należy zrozumieć:

•  Repozytorium to zbiór plików i folderów śledzonych przez Git. Nazywa się to również repo. To najczęstsze określenie.
•  Zatwierdzenie to migawka zmian wprowadzonych w repozytorium. Każde zatwierdzenie posiada unikalny identyfikator zawierający informacje o dokonanych zmianach, takie jak autor, data i komunikat opisujący zmiany.
•  Gałąź to osobna linia rozwoju, która umożliwia programistom jednoczesną pracę nad różnymi funkcjami lub wersjami projektu. To jak równoległy wszechświat repozytorium.
•  Podczas tworzenia, łączenie swojej pracy odnosi się do łączenia zmian z jednej gałęzi w drugą. Zwykle używa się go, gdy funkcja jest kompletna i gotowa do zintegrowania z gałęzią główną.
•  Żądanie ściągnięcia to funkcja GitHuba, która umożliwia programistom proponowanie zmian w repozytorium i żądanie ich połączenia z gałęzią główną. Zawiera opis zmian oraz wszelką dokumentację uzupełniającą i testy.
•  Klonowanie polega na utworzeniu kopii repozytorium na komputerze lokalnym.
•  Wypychanie to proces wysyłania zmian z komputera lokalnego do zdalnego repozytorium, takiego jak GitHub lub GitLab.
•  Ściąganie to proces pobierania zmian ze zdalnego repozytorium na komputer lokalny.

Rozumiejąc te podstawowe pojęcia, możesz efektywnie używać Gita do zarządzania bazą kodu i współpracy z innymi programistami.

Przygotowywanie się

Nie będziemy tu trzymać się przepisu, ale przyjrzyjmy się, jakich poleceń Git możesz użyć, aby ułatwić współpracę.

Jak to zrobić…

Oto niektóre z najczęściej używanych poleceń Git:

•  Aby zainicjować nowe repozytorium Git w bieżącym katalogu. Możesz po prostu wykonać następujące czynności:

$ git init

•  Jeśli chcesz dodać zmiany do obszaru testowego, możesz po prostu użyć git add:

$ git add file.txt

•  Podczas zatwierdzania zmian w repozytorium użyj po prostu następujących poleceń:

$ git commit -m "message"

•  Najważniejsza, kiedy zaczynasz współpracę, jest możliwość sklonowania projektu; możesz po prostu uruchomić następujące polecenie:

$ git clone git@github.com

•  Jeśli chcesz pobrać zmiany z repozytorium zdalnego do repozytorium lokalnego, po prostu użyj następujących poleceń:

$ git pull origin main

•  Możesz także wypchnąć zmiany z repozytorium lokalnego do repozytorium zdalnego, korzystając z następujących poleceń:

$ git push origin main

•  Wyświetl listę wszystkich oddziałów lokalnych, używając oddziału git:

$ git branch

•  Następujące polecenie przełącza do innej gałęzi:

$ git checkout branch_name

•  Sprawdź nowy oddział z następującymi informacjami:

$ git checkout -b branch_name

•  Połącz zmiany z jednej gałęzi do drugiej za pomocą poniższych. Pamiętaj, że możesz także użyć rebase; opiera się to na preferencjach organizacji:

$ git merge branch_name

To tylko kilka z najczęściej używanych poleceń Git. Dostępnych jest znacznie więcej poleceń i opcji Git, dlatego warto zapoznać się z dokumentacją Git, aby dowiedzieć się więcej.

Jak to działa…
Git to rozproszony system kontroli wersji umożliwiający użytkownikom śledzenie zmian w kodzie w czasie. Oto ogólny przegląd działania Git. Git nie przechowuje tylko zmian wprowadzonych w kodzie; w rzeczywistości przechowuje migawki całego projektu w różnych momentach. Każda migawka reprezentuje stan projektu w określonym momencie. Przechowuje kod w strukturze przypominającej drzewo, a każda migawka projektu jest reprezentowana przez obiekt zatwierdzenia. Każdy obiekt zatwierdzenia wskazuje migawkę projektu, który reprezentuje, oraz obiekty zatwierdzenia, które pojawiły się przed nim. Używa także unikalnego wskaźnika o nazwie HEAD do śledzenia bieżącej gałęzi i najnowszego zatwierdzenia w tej gałęzi. Kiedy dokonujesz nowego zatwierdzenia, Git aktualizuje wskaźnik HEAD tak, aby wskazywał nowe zatwierdzenie. Ponadto każde zatwierdzenie w Git jest identyfikowane przez unikalną wartość skrótu, która jest 40-znakowym ciągiem liter i cyfr. Ta wartość skrótu jest generowana na podstawie zawartości zatwierdzenia i wartości skrótu i wszelkie poprzednie zatwierdzenia, na które wskazuje. Ponieważ Git przechowuje migawki Twojego projektu lokalnie na Twoim komputerze, możesz pracować w trybie offline i spokojnie zaangażuj się w swój projekt. Gdy będziesz gotowy do udostępnienia zmian, możesz wypchnąć je do zdalnego repozytorium. To tylko kilka kluczowych koncepcji działania Gita. Git to potężne i elastyczne narzędzie posiadające wiele zaawansowanych funkcji, dlatego warto dowiedzieć się więcej o jego działaniu.