Celem komponentów architektury jest dostarczanie wskazówek dotyczących architektury aplikacji wraz z bibliotekami do wykonywania typowych zadań, takich jak zarządzanie cyklem życia i utrwalanie danych. Komponenty architektury pomagają strukturyzować aplikację w sposób, który zapewnia jej niezawodność, łatwość testowania i utrzymania przy mniejszej ilości kodu szablonowego. Biblioteki komponentów architektury są częścią Androida Jetpack.
To jest wersja ćwiczeń z programowania w Kotlinie. Wersję w języku programowania Java znajdziesz tutaj.
Jeśli podczas wykonywania tego laboratorium napotkasz jakiekolwiek problemy (błędy w kodzie, błędy gramatyczne, niejasne sformułowania itp.), zgłoś je, klikając link Zgłoś błąd w lewym dolnym rogu laboratorium.
Wymagania wstępne
Musisz znać język Kotlin, koncepcje projektowania obiektowego i podstawy programowania na Androida, w szczególności:
RecyclerView
i adaptery- baza danych SQLite i język zapytań SQLite,
- Podstawowe korutyny (jeśli nie znasz korutyn, możesz zapoznać się z artykułem Using Kotlin Coroutines in your Android App).
Warto też znać wzorce architektury oprogramowania, które oddzielają dane od interfejsu użytkownika, takie jak MVP lub MVC. Te ćwiczenia z programowania implementują architekturę zdefiniowaną w przewodniku po architekturze aplikacji.
Ten moduł skupia się na komponentach architektury Androida. Nieistotne koncepcje i kod zostały pominięte. Można je po prostu skopiować i wkleić.
Jeśli nie znasz języka Kotlin, możesz skorzystać z wersji tych warsztatów w języku programowania Java, którą znajdziesz tutaj.
Co musisz zrobić
Z tego laboratorium dowiesz się, jak zaprojektować i zbudować aplikację przy użyciu komponentów architektury Room, ViewModel i LiveData. Zobaczysz, jak utworzyć aplikację, która:
- Implementuje zalecaną architekturę przy użyciu komponentów architektury Androida.
- Współpracuje z bazą danych, aby pobierać i zapisywać dane, a także wstępnie wypełniać ją niektórymi słowami.
- Wyświetla wszystkie słowa w
RecyclerView
wMainActivity
. - Otwiera drugie działanie, gdy użytkownik kliknie przycisk +. Gdy użytkownik wpisze słowo, dodaje je do bazy danych i listy.
Aplikacja jest prosta, ale wystarczająco złożona, aby można było jej użyć jako szablonu do dalszego rozwijania. Oto podgląd:
Czego potrzebujesz
- Android Studio 3.0 lub nowsze i umiejętność korzystania z tego narzędzia. Upewnij się, że masz zaktualizowane Android Studio, pakiet SDK i Gradle.
- urządzenie z Androidem lub emulator.
To ćwiczenie zawiera cały kod potrzebny do utworzenia kompletnej aplikacji.
Korzystanie z komponentów architektury i wdrażanie zalecanej architektury wymaga wykonania wielu czynności. Najważniejsze jest stworzenie modelu mentalnego tego, co się dzieje, zrozumienie, jak poszczególne elementy są ze sobą powiązane i jak przepływają dane. Podczas wykonywania tego ćwiczenia nie kopiuj i nie wklejaj kodu, ale staraj się zrozumieć jego działanie.
Jakie są zalecane komponenty architektury?
Aby wprowadzić terminologię, przedstawiamy krótkie wprowadzenie do komponentów architektury i ich wzajemnego działania. Pamiętaj, że ten przewodnik skupia się na podzbiorze komponentów, a mianowicie LiveData, ViewModel i Room. Każdy komponent jest wyjaśniony w trakcie korzystania z niego.
Ten diagram przedstawia podstawową formę architektury:
Encja: klasa z adnotacjami, która opisuje tabelę bazy danych podczas pracy z biblioteką Room.
Baza danych SQLite: pamięć urządzenia. Biblioteka trwałości danych Room tworzy i utrzymuje tę bazę danych.
DAO: obiekt umożliwiający dostęp do danych. Mapowanie zapytań SQL na funkcje. Gdy używasz obiektu DAO, wywołujesz metody, a Room zajmuje się resztą.
Baza danych Room: upraszcza pracę z bazą danych i służy jako punkt dostępu do bazowej bazy danych SQLite (ukrywa SQLiteOpenHelper)
). Baza danych Room używa obiektu DAO do wysyłania zapytań do bazy danych SQLite.
Repozytorium: utworzona przez Ciebie klasa, która służy głównie do zarządzania wieloma źródłami danych.
ViewModel: działa jako centrum komunikacji między repozytorium (dane) a interfejsem. Interfejs użytkownika nie musi się już martwić o pochodzenie danych. Instancje klasy ViewModel przetrwają odtworzenie aktywności lub fragmentu.
LiveData: klasa przechowująca dane, które można obserwować. Zawsze przechowuje/buforuje najnowszą wersję danych i powiadamia obserwatorów o zmianach w danych. LiveData
uwzględnia cykl życia. Komponenty interfejsu obserwują tylko odpowiednie dane i nie przerywają ani nie wznawiają obserwacji. LiveData automatycznie zarządza tymi wszystkimi kwestiami, ponieważ podczas obserwacji jest świadoma odpowiednich zmian stanu cyklu życia.
Omówienie architektury RoomWordSample
Poniższy diagram przedstawia wszystkie elementy aplikacji. Każde z pudełek (z wyjątkiem bazy danych SQLite) reprezentuje klasę, którą utworzysz.
- Otwórz Android Studio i kliknij Start a new Android Studio project (Rozpocznij nowy projekt Android Studio).
- W oknie Create New Project (Utwórz nowy projekt) wybierz Empty Activity (Pusta aktywność) i kliknij Next (Dalej).
- Na następnym ekranie nadaj aplikacji nazwę RoomWordSample i kliknij Zakończ.
Następnie musisz dodać biblioteki komponentów do plików Gradle.
- W Android Studio kliknij kartę Projects (Projekty) i rozwiń folder Gradle Scripts (Skrypty Gradle).
Otwórz build.gradle
(moduł: app).
- Zastosuj wtyczkę Kotlin
kapt
annotation processor, dodając ją po innych wtyczkach zdefiniowanych u góry plikubuild.gradle
(Module: app).
apply plugin: 'kotlin-kapt'
- Dodaj blok
packagingOptions
w blokuandroid
, aby wykluczyć moduł funkcji atomowych z pakietu i zapobiec wyświetlaniu ostrzeżeń.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- Na końcu bloku
dependencies
dodaj ten kod.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
- W pliku
build.gradle
(Project: RoomWordsSample) dodaj numery wersji na końcu pliku, jak podano w poniższym kodzie.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
Dane tej aplikacji to słowa, więc potrzebujesz prostej tabeli do przechowywania tych wartości:
Room umożliwia tworzenie tabel za pomocą encji. Zróbmy to teraz.
- Utwórz nowy plik klasy Kotlin o nazwie
Word
zawierającyWord
klasę danych.
Ta klasa będzie opisywać encję (reprezentującą tabelę SQLite) dla Twoich słów. Każda właściwość w klasie reprezentuje kolumnę w tabeli. Room użyje tych właściwości do utworzenia tabeli i utworzenia instancji obiektów z wierszy w bazie danych.
Oto kod:
data class Word(val word: String)
Aby klasa Word
była przydatna w bazie danych Room, musisz dodać do niej adnotacje. Adnotacje określają, jak każda część tej klasy jest powiązana z wpisem w bazie danych. Room używa tych informacji do generowania kodu.
Jeśli wpiszesz adnotacje samodzielnie (zamiast wklejać), Android Studio automatycznie zaimportuje klasy adnotacji.
- Zaktualizuj klasę
Word
za pomocą adnotacji, jak pokazano w tym kodzie:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Zobaczmy, co robią te adnotacje:
@Entity(tableName =
"word_table"
)
Każda klasa@Entity
reprezentuje tabelę SQLite. Dodaj adnotację do deklaracji klasy, aby wskazać, że jest to encja. Jeśli chcesz, aby nazwa tabeli różniła się od nazwy klasy, możesz ją określić. Nadaje to tabeli nazwę „word_table”.@PrimaryKey
Każdy podmiot musi mieć klucz podstawowy. Dla uproszczenia każde słowo pełni funkcję klucza podstawowego.@ColumnInfo(name =
"word"
)
Określa nazwę kolumny w tabeli, jeśli ma się ona różnić od nazwy zmiennej składowej. W ten sposób kolumna zostanie nazwana „word”.- Każda właściwość przechowywana w bazie danych musi mieć widoczność publiczną, która jest domyślną wartością w Kotlinie.
Pełną listę adnotacji znajdziesz w podsumowaniu pakietu Room.
Czym jest DAO?
W DAO (obiekcie dostępu do danych) określasz zapytania SQL i łączysz je z wywołaniami metod. Kompilator sprawdza SQL i generuje zapytania na podstawie adnotacji ułatwiających tworzenie typowych zapytań, takich jak @Insert
. Room używa obiektu DAO do tworzenia przejrzystego interfejsu API dla Twojego kodu.
Obiekt DAO musi być interfejsem lub klasą abstrakcyjną.
Domyślnie wszystkie zapytania muszą być wykonywane w osobnym wątku.
Biblioteka Room obsługuje współprogramy, dzięki czemu zapytania można oznaczać modyfikatorem suspend
, a następnie wywoływać je z współprogramu lub innej funkcji zawieszającej.
Wdrożenie DAO
Napiszmy DAO, które będzie udostępniać zapytania dotyczące:
- uzyskiwanie wszystkich słów w porządku alfabetycznym,
- Wstawianie słowa
- Usuwam wszystkie słowa
- Utwórz nowy plik klasy Kotlin o nazwie
WordDao
. - Skopiuj poniższy kod i wklej go do pliku
WordDao
. W razie potrzeby popraw importy, aby kod się kompilował.
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
Przyjrzyjmy się temu procesowi:
WordDao
to interfejs. Obiekty DAO muszą być interfejsami lub klasami abstrakcyjnymi.- Adnotacja
@Dao
identyfikuje ją jako klasę DAO dla Room. suspend fun insert(word: Word)
: deklaruje funkcję zawieszającą, która wstawia jedno słowo.@Insert
adnotacja to specjalna adnotacja metody DAO, w której nie musisz podawać żadnego kodu SQL. (Istnieją też adnotacje@Delete
i@Update
do usuwania i aktualizowania wierszy, ale nie są one używane w tej aplikacji).onConflict = OnConflictStrategy.IGNORE
: wybrana strategia onConflict ignoruje nowe słowo, jeśli jest ono identyczne z słowem, które już znajduje się na liście. Więcej informacji o dostępnych strategiach rozwiązywania konfliktów znajdziesz w dokumentacji.suspend fun deleteAll()
: deklaruje funkcję zawieszenia, która usuwa wszystkie słowa.- Nie ma wygodnej adnotacji do usuwania wielu elementów, więc jest ona oznaczona ogólną adnotacją
@Query
. @Query
("DELETE FROM word_table")
:@Query
wymaga podania zapytania SQL jako parametru ciągu znaków w adnotacji, co umożliwia złożone zapytania odczytu i inne operacje.fun getAlphabetizedWords(): List<Word>
: metoda pobierania wszystkich słów i zwracaniaList
Words
.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: zapytanie, które zwraca listę słów posortowanych w kolejności rosnącej.
Gdy dane się zmieniają, zwykle chcesz podjąć jakieś działanie, np. wyświetlić zaktualizowane dane w interfejsie. Oznacza to, że musisz obserwować dane, aby w razie ich zmiany móc odpowiednio zareagować.
W zależności od sposobu przechowywania danych może to być trudne. Obserwowanie zmian w danych w wielu komponentach aplikacji może tworzyć wyraźne, sztywne ścieżki zależności między komponentami. Utrudnia to m.in. testowanie i debugowanie.
LiveData
– klasa biblioteki cyklu życia do obserwowania danych rozwiązuje ten problem. W opisie metody użyj wartości zwracanej typu LiveData
, a Room wygeneruje cały niezbędny kod do aktualizacji LiveData
po zaktualizowaniu bazy danych.
W WordDao
zmień sygnaturę metody getAlphabetizedWords()
tak, aby zwracana wartość List<Word>
była opakowana w LiveData
.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
W dalszej części tego laboratorium dowiesz się, jak śledzić zmiany danych za pomocą Observer
w MainActivity
.
Czym jest baza danych Room?
- Room to warstwa bazy danych oparta na bazie danych SQLite.
- Room wykonuje proste zadania, które wcześniej wykonywałeś(-aś) za pomocą
SQLiteOpenHelper
. - Room używa obiektu DAO do wysyłania zapytań do bazy danych.
- Domyślnie, aby uniknąć niskiej wydajności interfejsu, biblioteka Room nie zezwala na wykonywanie zapytań w głównym wątku. Gdy zapytania Room zwracają
LiveData
, są one automatycznie wykonywane asynchronicznie w wątku w tle. - Room zapewnia sprawdzanie instrukcji SQLite w czasie kompilacji.
Implementowanie bazy danych Room
Klasa bazy danych Room musi być abstrakcyjna i rozszerzać klasę RoomDatabase
. Zwykle w całej aplikacji wystarczy tylko jedna instancja bazy danych Room.
Utwórzmy go.
- Utwórz plik klasy Kotlin o nazwie
WordRoomDatabase
i dodaj do niego ten kod:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Przyjrzyjmy się kodowi:
- Klasa bazy danych dla Room musi być
abstract
i rozszerzaćRoomDatabase
- Klasę bazy danych Room oznaczysz adnotacją
@Database
, a za pomocą parametrów adnotacji zadeklarujesz encje należące do bazy danych i ustawisz numer wersji. Każda encja odpowiada tabeli, która zostanie utworzona w bazie danych. Migracje baz danych wykraczają poza zakres tych ćwiczeń z programowania, więc ustawiamy tutaj wartośćexportSchema
na false, aby uniknąć ostrzeżenia o kompilacji. W prawdziwej aplikacji warto ustawić katalog, którego Room będzie używać do eksportowania schematu, aby można było sprawdzić bieżący schemat w systemie kontroli wersji. - Baza danych udostępnia obiekty DAO za pomocą abstrakcyjnej metody „getter” dla każdego interfejsu @Dao.
- Zdefiniowaliśmy singleton,
WordRoomDatabase,
, aby zapobiec otwieraniu wielu instancji bazy danych w tym samym czasie. getDatabase
zwraca pojedynczą instancję. Przy pierwszym dostępie utworzy bazę danych, używając narzędzia do tworzenia baz danych Room, aby utworzyć obiektRoomDatabase
w kontekście aplikacji z klasyWordRoomDatabase
i nadać mu nazwę"word_database"
.
Co to jest repozytorium?
Klasa repozytorium abstrahuje dostęp do wielu źródeł danych. Repozytorium nie jest częścią bibliotek komponentów architektury, ale jest sugerowaną sprawdzoną metodą separacji kodu i architektury. Klasa Repository udostępnia przejrzysty interfejs API do uzyskiwania dostępu do danych w pozostałej części aplikacji.
Dlaczego warto korzystać z repozytorium?
Repozytorium zarządza zapytaniami i umożliwia korzystanie z wielu backendów. W najczęstszym przypadku repozytorium implementuje logikę decydującą o tym, czy pobrać dane z sieci, czy użyć wyników zapisanych w pamięci podręcznej w lokalnej bazie danych.
Implementowanie repozytorium
Utwórz plik klasy Kotlin o nazwie WordRepository
i wklej do niego ten kod:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
Główne wnioski:
- Obiekt DAO jest przekazywany do konstruktora repozytorium, a nie do całej bazy danych. Wynika to z faktu, że potrzebuje on tylko dostępu do obiektu DAO, ponieważ zawiera on wszystkie metody odczytu i zapisu w bazie danych. Nie musisz udostępniać całego repozytorium bazy danych.
- Lista słów jest własnością publiczną. Jest ona inicjowana przez pobranie
LiveData
listy słów z Room. Możemy to zrobić, ponieważ w kroku „Klasa LiveData” zdefiniowaliśmygetAlphabetizedWords
metodę zwracającąLiveData
. Room wykonuje wszystkie zapytania w osobnym wątku. Następnie obserwowanyLiveData
powiadomi obserwatora w głównym wątku, gdy dane ulegną zmianie. - Modyfikator
suspend
informuje kompilator, że tę funkcję należy wywołać z korutyny lub innej funkcji zawieszającej.
Co to jest ViewModel?
Jego zadaniem jest dostarczanie danych do interfejsu i przetrwanie zmian konfiguracji.ViewModel
ViewModel
pełni funkcję centrum komunikacji między repozytorium a interfejsem. Możesz też użyć ViewModel
, aby udostępniać dane między fragmentami. ViewModel jest częścią biblioteki cyklu życia.
Wprowadzenie do tego tematu znajdziesz w ViewModel Overview
lub w poście na blogu ViewModel: prosty przykład.
Dlaczego warto używać ViewModel?
ViewModel
przechowuje dane interfejsu aplikacji w sposób uwzględniający cykl życia, który przetrwa zmiany konfiguracji. Oddzielenie danych interfejsu aplikacji od klas Activity
i Fragment
pozwala lepiej przestrzegać zasady pojedynczej odpowiedzialności: aktywności i fragmenty odpowiadają za wyświetlanie danych na ekranie, a klasa ViewModel
może zajmować się przechowywaniem i przetwarzaniem wszystkich danych potrzebnych do interfejsu.
W przypadku zmiennych danych, które będą używane lub wyświetlane w interfejsie, użyj symbolu ViewModel
.LiveData
Korzystanie z LiveData
ma kilka zalet:
- Możesz umieścić obserwatora danych (zamiast odpytywać o zmiany) i aktualizować interfejs tylko wtedy, gdy dane faktycznie się zmienią.
- Repozytorium i interfejs użytkownika są całkowicie oddzielone znakiem
ViewModel
. - Nie ma wywołań bazy danych z
ViewModel
(wszystko jest obsługiwane w repozytorium), co ułatwia testowanie kodu.
viewModelScope
W Kotlinie wszystkie korutyny działają w ramach CoroutineScope
. Zakres kontroluje czas życia korutyn za pomocą zadania. Gdy anulujesz zadanie w zakresie, anulujesz wszystkie korutyny uruchomione w tym zakresie.
Biblioteka AndroidX lifecycle-viewmodel-ktx
dodaje viewModelScope
jako funkcję rozszerzającą klasy ViewModel
, co umożliwia pracę z zakresami.
Więcej informacji o korzystaniu z korutyn w ViewModel znajdziesz w kroku 5 w samouczku Using Kotlin Coroutines in your Android App (Korzystanie z korutyn Kotlin w aplikacji na Androida) lub w tym artykule na blogu.
Implementowanie ViewModel
Utwórz plik klasy Kotlin dla WordViewModel
i dodaj do niego ten kod:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
Tutaj:
- Utworzono klasę o nazwie
WordViewModel
, która przyjmujeApplication
jako parametr i rozszerzaAndroidViewModel
. - Dodano prywatną zmienną członkowską, która będzie przechowywać odwołanie do repozytorium.
- Dodaliśmy publiczną zmienną
LiveData
, która służy do przechowywania listy słów w pamięci podręcznej. - Utworzono blok
init
, który pobiera odwołanie do elementuWordDao
z elementuWordRoomDatabase
. - W bloku
init
zbudowanoWordRepository
na podstawieWordRoomDatabase
. - W bloku
init
zainicjujallWords
LiveData za pomocą repozytorium. - Utworzono metodę opakowującą
insert()
, która wywołuje metodęinsert()
repozytorium. W ten sposób implementacjainsert()
jest odseparowana od interfejsu. Nie chcemy, aby wstawianie blokowało główny wątek, więc uruchamiamy nową korutynę i wywołujemy funkcję wstawiania repozytorium, która jest funkcją zawieszającą. Jak już wspomnieliśmy, obiekty ViewModel mają zakres coroutine oparty na ich cyklu życia, który nazywa sięviewModelScope
i którego używamy w tym przykładzie.
Następnie musisz dodać układ XML listy i elementów.
W tym laboratorium przyjęto założenie, że znasz tworzenie układów w XML, więc podajemy tylko kod.
Zmień motyw aplikacji na Material Design, ustawiając element nadrzędny AppTheme
na Theme.MaterialComponents.Light.DarkActionBar
. Dodaj styl do elementów listy w values/styles.xml
:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
Dodawanie układu layout/recyclerview_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
W layout/activity_main.xml
zastąp TextView
symbolem RecyclerView
i dodaj pływający przycisk polecenia (FAB). Układ powinien teraz wyglądać tak:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Wygląd FAB powinien odpowiadać dostępnemu działaniu, dlatego chcemy zastąpić ikonę symbolem „+”.
Najpierw musimy dodać nowy komponent wektorowy:
- Wybierz Plik > Nowy > Zasób wektorowy.
- W polu Clip Art: kliknij ikonę robota Androida.
- Wyszukaj „dodaj” i wybierz komponent „+”. Kliknij OK
.
- Następnie kliknij Dalej.
- Potwierdź ścieżkę ikony jako
main > drawable
i kliknij Zakończ, aby dodać komponent. - W pliku
layout/activity_main.xml
zaktualizuj pływający przycisk działania, aby zawierał nowy element rysowalny:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>
Dane wyświetlisz w RecyclerView
, co jest nieco lepsze niż umieszczenie ich w TextView
. W tym ćwiczeniu zakłada się, że wiesz, jak działają RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
i RecyclerView.Adapter
.
Zmienna words
w adapterze buforuje dane. W następnym zadaniu dodasz kod, który automatycznie aktualizuje dane.
Utwórz plik klasy Kotlin dla WordListAdapter
, który rozszerza RecyclerView.Adapter
. Oto kod:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
Dodaj RecyclerView
do metody onCreate()
klasy MainActivity
.
W metodzie onCreate()
po setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Uruchom aplikację, aby sprawdzić, czy wszystko działa. Nie ma żadnych elementów, ponieważ nie zostały jeszcze połączone dane.
W bazie danych nie ma danych. Dane dodasz na 2 sposoby: dodaj część danych po otwarciu bazy danych i dodaj Activity
do dodawania słów.
Aby usunąć całą zawartość i ponownie wypełnić bazę danych przy każdym uruchomieniu aplikacji, utwórz RoomDatabase.Callback
i zastąp onOpen()
. Operacji na bazie danych Room nie można wykonywać w wątku interfejsu, więc onOpen()
uruchamia korutynę w usłudze IO Dispatcher.
Aby uruchomić korutynę, potrzebujemy CoroutineScope
. Zaktualizuj metodę getDatabase
klasy WordRoomDatabase
, aby jako parametr otrzymywać też zakres współprogramu:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Zaktualizuj inicjator pobierania bazy danych w bloku init
pliku WordViewModel
, aby przekazywać też zakres:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
W WordRoomDatabase
tworzymy niestandardową implementację RoomDatabase.Callback()
, która otrzymuje też CoroutineScope
jako parametr konstruktora. Następnie zastępujemy metodę onOpen
, aby wypełnić bazę danych.
Oto kod tworzenia wywołania zwrotnego w klasie WordRoomDatabase
:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}
Na koniec dodaj wywołanie zwrotne do sekwencji tworzenia bazy danych tuż przed wywołaniem funkcji .build()
w Room.databaseBuilder()
:
.addCallback(WordDatabaseCallback(scope))
Oto jak powinien wyglądać ostateczny kod:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Dodaj te zasoby ciągów znaków w values/strings.xml
:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
Dodaj ten zasób koloru w value/colors.xml
:
<color name="buttonLabel">#FFFFFF</color>
Utwórz nowy plik zasobu wymiaru:
- W oknie Project (Projekt) kliknij moduł aplikacji.
- Wybierz File > New > Android Resource File (Plik > Nowy > Plik zasobów Androida).
- W sekcji Dostępne kwalifikatory wybierz Wymiar .
- Ustaw nazwę pliku: dimens
Dodaj te zasoby wymiarów w values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Utwórz nowy pusty projekt Androida Activity
za pomocą szablonu Empty Activity (Pusta aktywność):
- Wybierz Plik > Nowy > Aktywność > Pusta aktywność.
- Wpisz
NewWordActivity
w polu Nazwa aktywności. - Sprawdź, czy nowa aktywność została dodana do pliku manifestu Androida.
<activity android:name=".NewWordActivity"></activity>
Zaktualizuj plik activity_new_word.xml
w folderze układu za pomocą tego kodu:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
Zaktualizuj kod aktywności:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
Ostatnim krokiem jest połączenie interfejsu z bazą danych przez zapisywanie nowych słów wpisywanych przez użytkownika i wyświetlanie bieżącej zawartości bazy danych słów w RecyclerView
.
Aby wyświetlić bieżącą zawartość bazy danych, dodaj obserwatora, który obserwuje LiveData
w ViewModel
.
Gdy dane się zmienią, wywoływane jest wywołanie zwrotne onChanged()
, które wywołuje metodę setWords()
adaptera, aby zaktualizować dane w pamięci podręcznej adaptera i odświeżyć wyświetlaną listę.
W MainActivity
utwórz zmienną elementu dla ViewModel
:
private lateinit var wordViewModel: WordViewModel
Użyj ViewModelProvider
, aby powiązać ViewModel
z Activity
.
Gdy Activity
zacznie działać, ViewModelProviders
utworzy ViewModel
. Gdy aktywność zostanie zniszczona, np. w wyniku zmiany konfiguracji, obiekt ViewModel
pozostanie. Gdy aktywność zostanie ponownie utworzona, ViewModelProviders
zwróć istniejący ViewModel
. Więcej informacji znajdziesz w sekcji ViewModel
.
W onCreate()
poniżej bloku kodu RecyclerView
pobierz ViewModel
z ViewModelProvider
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
W onCreate()
dodaj też obserwatora właściwości allWords LiveData
z WordViewModel
.
Metoda onChanged()
(domyślna metoda w przypadku naszej funkcji Lambda) jest wywoływana, gdy zmieniają się obserwowane dane, a aktywność jest na pierwszym planie:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Chcemy, aby po kliknięciu przycisku FAB otworzył się ekran NewWordActivity
, a po powrocie do ekranu MainActivity
nowe słowo zostało wstawione do bazy danych lub wyświetlił się ekran Toast
. Aby to osiągnąć, zacznijmy od zdefiniowania kodu żądania:
private val newWordActivityRequestCode = 1
W pliku MainActivity
dodaj kod onActivityResult()
dla NewWordActivity
.
Jeśli aktywność zwróci wartość RESULT_OK
, wstaw zwrócone słowo do bazy danych, wywołując metodę insert()
obiektu WordViewModel
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
W MainActivity,
start NewWordActivity
, gdy użytkownik kliknie FAB. W MainActivity
onCreate
znajdź FAB i dodaj onClickListener
z tym kodem:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
Gotowy kod powinien wyglądać tak:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
Teraz uruchom aplikację. Gdy dodasz słowo do bazy danych w NewWordActivity
, interfejs automatycznie się zaktualizuje.
Masz już działającą aplikację. Podsumujmy, co udało Ci się stworzyć. Oto ponownie struktura aplikacji:
Aplikacja składa się z tych komponentów:
MainActivity
: wyświetla słowa na liście, używającRecyclerView
iWordListAdapter
. WMainActivity
znajduje sięObserver
, który obserwuje słowa LiveData w bazie danych i otrzymuje powiadomienia o ich zmianach.NewWordActivity:
dodaje nowe słowo do listy.WordViewModel
: udostępnia metody dostępu do warstwy danych i zwraca LiveData, dzięki czemu MainActivity może skonfigurować relację obserwatora*.LiveData<List<Word>>
: umożliwia automatyczne aktualizacje komponentów interfejsu. WMainActivity
znajduje sięObserver
, który obserwuje słowa LiveData w bazie danych i otrzymuje powiadomienia o ich zmianach.Repository:
zarządza co najmniej 1 źródłem danych.Repository
udostępnia metody, za pomocą których ViewModel może wchodzić w interakcje z bazowym dostawcą danych. W tej aplikacji backendem jest baza danych Room.Room
: jest otoczką bazy danych SQLite i ją implementuje. Room wykonuje wiele czynności, które wcześniej trzeba było robić samodzielnie.- DAO: mapuje wywołania metod na zapytania do bazy danych, dzięki czemu gdy repozytorium wywołuje metodę taką jak
getAlphabetizedWords()
, Room może wykonaćSELECT * from word_table ORDER BY word ASC
. Word
: to klasa jednostki zawierająca jedno słowo.
* Views
i Activities
(oraz Fragments
) wchodzą w interakcję z danymi tylko za pomocą ViewModel
. Dlatego nie ma znaczenia, skąd pochodzą dane.
Przepływ danych w przypadku automatycznych aktualizacji interfejsu (reaktywny interfejs)
Automatyczna aktualizacja jest możliwa, ponieważ używamy LiveData. W MainActivity
znajduje się Observer
, który obserwuje słowa LiveData w bazie danych i otrzymuje powiadomienia o ich zmianach. Gdy nastąpi zmiana, wykonywana jest metoda onChange()
obserwatora, która aktualizuje mWords
w WordListAdapter
.
Dane można zaobserwować, ponieważ są LiveData
. Obserwowana jest wartość LiveData<List<Word>>
zwracana przez właściwość WordViewModel
allWords
.
WordViewModel
ukrywa wszystko, co dotyczy backendu, przed warstwą interfejsu. Zawiera metody dostępu do warstwy danych i zwraca wartość LiveData
, aby MainActivity
mogło skonfigurować relację obserwatora. Views
i Activities
(oraz Fragments
) wchodzą w interakcję z danymi tylko za pomocą ViewModel
. Dlatego nie ma znaczenia, skąd pochodzą dane.
W tym przypadku dane pochodzą z Repository
. ViewModel
nie musi wiedzieć, z czym to repozytorium wchodzi w interakcję. Musi tylko wiedzieć, jak wchodzić w interakcję z Repository
, czyli za pomocą metod udostępnianych przez Repository
.
Repozytorium zarządza co najmniej 1 źródłem danych. W aplikacji WordListSample
backendem jest baza danych Room. Room to otoka i implementacja bazy danych SQLite. Room wykonuje wiele czynności, które wcześniej trzeba było robić samodzielnie. Na przykład Room umożliwia wykonywanie wszystkich czynności, które były dostępne w przypadku zajęć SQLiteOpenHelper
.
Metoda DAO mapuje wywołania na zapytania do bazy danych, więc gdy repozytorium wywołuje metodę taką jak getAllWords()
, Room może wykonać SELECT * from word_table ORDER BY word ASC
.
Ponieważ wynik zwrócony przez zapytanie jest obserwowany LiveData
, za każdym razem, gdy dane w Roomie ulegną zmianie, wykonywana jest metoda onChanged()
interfejsu Observer
, a interfejs jest aktualizowany.
[Opcjonalnie] Pobierz kod rozwiązania
Jeśli jeszcze tego nie zrobiono, możesz zapoznać się z kodem rozwiązania w tym laboratorium. Możesz przejrzeć repozytorium GitHub lub pobrać kod tutaj:
Rozpakuj pobrany plik ZIP. Spowoduje to rozpakowanie folderu głównego android-room-with-a-view-kotlin
, który zawiera kompletną aplikację.