Pokój z widokiem na Androida – Kotlin

Komponenty architektury zawierają wskazówki dotyczące architektury aplikacji, a biblioteki są w nich dostępne do typowych zadań, takich jak zarządzanie cyklem życia i przechowywanie danych. Komponenty w architekturze pomagają wzbogacić aplikację w taki sposób, który jest solidny, możliwy do przetestowania i utrzymany. Nie wymaga przy tym powtarzalnego kodu. Biblioteki Architecture Components są częścią Android Jetpack.

To jest wersja ćwiczeń z kotlin. Wersję języka Java znajdziesz tutaj.

Jeśli podczas ćwiczeń z programowania napotkasz jakiekolwiek błędy (błędy w kodzie, błędy gramatyczne, niejasne słowa itp.), zgłoś je, klikając link Zgłoś błąd w lewym dolnym rogu ćwiczeń z programowania.

Wymagania wstępne

Musisz znać Kotlin, koncepcje projektowe zorientowane na obiekty oraz podstawy tworzenia aplikacji na Androida, w szczególności:

Pomocne jest też poznanie wzorców architektonicznych oprogramowania, które oddzielają dane od interfejsu, np. MVP czy MVC. To ćwiczenie programowania korzysta z architektury określonej w przewodniku po architekturze aplikacji.

To ćwiczenie skupia się na komponentach architektury architektury Androida. Wystarczy skopiować dane i koncepcje niezwiązane z tematem oraz kod.

Jeśli nie znasz jeszcze programu Kotlin, tutaj znajdziesz wersję tego ćwiczenia z programowania.

Co chcesz zrobić

Z tego modułu dowiesz się, jak zaprojektować i utworzyć aplikację za pomocą Pokoju Komponentów Architektury, ViewModel i LiveData oraz jak stworzyć aplikację, która:

  • stosuje naszą zalecaną architekturę za pomocą komponentów Android Architecture Components.
  • Współpracuje z bazą danych, aby pobierać i zapisywać dane, a także wstępnie wypełnia bazę danych niektórymi słowami.
  • Wyświetla wszystkie słowa z języka RecyclerView w języku: MainActivity.
  • Otwiera drugą aktywność, gdy użytkownik kliknie przycisk +. Gdy użytkownik wpisze słowo, zostanie ono dodane do bazy danych i listy.

Aplikacja jest prosta, ale na tyle złożona, że możesz używać jej jako szablonu. Oto podgląd:

Czego potrzebujesz

To ćwiczenie zawiera cały kod potrzebny do utworzenia pełnej aplikacji.

Aby zacząć korzystać z komponentów architektury i zaimplementować zalecaną architekturę, trzeba wykonać wiele czynności. Najważniejsze jest, aby utworzyć model mentalny i dowiedzieć się, co się dzieje, i jak te dane się ze sobą łączą. W trakcie tych ćwiczeń nie kopiuj i nie wklejaj kodu, ale zacznij budować to wewnętrznie.

Poniżej znajdziesz krótkie wprowadzenie do komponentów architektury i sposobu ich współdziałania. Ćwiczenia z programowania koncentrują się na podzbiorze komponentów, takich jak LiveData, ViewModel i Room. Każdy komponent jest objaśniony szczegółowo w miarę jego używania.

Ten schemat przedstawia podstawową formę architektury:

Jednostka: klasa z adnotacjami, która opisuje tabelę bazy danych podczas pracy z salą.

Baza danych SQLite: w pamięci urządzenia. Biblioteka trwałości pokoju tworzy i obsługuje tę bazę danych.

DAO: obiekt dostępu do danych. Mapowanie zapytań SQL do funkcji. Korzystając z DAO, wywołujesz odpowiednie metody, a reszta zajmuje się resztą.

Baza danych sal: upraszcza bazę danych i pełni funkcję punktu dostępu do bazowej bazy danych SQLite (ukryje SQLiteOpenHelper)). Baza danych sali używa DAO do wysyłania zapytań do bazy danych SQLite.

Repozytorium: tworzona przez Ciebie klasa, która służy głównie do zarządzania wieloma źródłami danych.

ViewModel: działa jako centrum komunikacyjne między repozytorium (danymi) a interfejsem. Interfejs nie musi już martwić się o pochodzenie danych. Instancje ViewModel przetrwają aktywność aktywności/odtwarzanie fragmentów.

LiveData: klasa posiadacza danych, którą można obserwować. Zawsze przechowuje lub przechowuje w pamięci podręcznej najnowszą wersję danych oraz powiadamia swoich obserwatorów o zmianach. LiveData ma świadomość cyklu życia. Komponenty interfejsu rejestrują tylko istotne dane i nie przestają wznawiać ani wznowić obserwacji. LiveData automatycznie zarządza tym procesem, ponieważ podczas obserwacji zauważa istotne zmiany stanu cyklu życia produktu.

Omówienie architektury architekturyRoomWordSample

Poniższy diagram przedstawia wszystkie elementy aplikacji. Każde z paczek otaczających (oprócz bazy danych SQLite) reprezentuje klasę, którą utworzysz.

  1. Otwórz Android Studio i kliknij Rozpocznij nowy projekt Android Studio.
  2. W oknie Utwórz nowy projekt wybierz Pusta aktywność, a potem kliknij Dalej.
  3. Na następnym ekranie nazwij aplikację RoomWordSample i kliknij Zakończ.

Następnie musisz dodać biblioteki komponentów do plików Gradle.

  1. W Android Studio kliknij kartę Projekty i rozwiń folder Gradle Scripts.

Otwórz aplikację build.gradle (Moduł: aplikacja).

  1. Zastosuj wtyczkę Kotlin z adnotacjami kapt, dodając ją po innych wtyczkach zdefiniowanych u góry pliku build.gradle (Module: app).
apply plugin: 'kotlin-kapt'
  1. Dodaj blok packagingOptions wewnątrz bloku android, aby wykluczyć z pakietu moduł funkcji atomowych i uniknąć ostrzeżeń.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. Dodaj następujący kod na końcu bloku dependencies.
// 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"
  1. W pliku build.gradle (Project: RoomWordsSample) dodaj na końcu pliku numery wersji podane 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'
}

W przypadku tej aplikacji dane są słowami. Aby je przechowywać, potrzebujesz prostej tabeli:

Pokój umożliwia tworzenie tabel przy użyciu jednostki. Zróbmy to teraz.

  1. Utwórz nowy plik zajęć Kotlin o nazwie Word zawierający klasę danych Word.
    Ta klasa opisujemy encję (reprezentującą tabelę SQLite) dla Twoich słów. Każda właściwość w klasie reprezentuje kolumnę w tabeli. Na koniec otworzą one właściwości tabeli i tworzyją instancje na podstawie wierszy w bazie danych.

Oto kod:

data class Word(val word: String)

Aby klasa Word była istotna dla bazy danych sali, musisz dodać do niej adnotacje. Adnotacje określają, jak każda część tych zajęć odnosi się do wpisu w bazie danych. Na podstawie tych informacji pokój zostanie wygenerowany.

Jeśli wpiszesz adnotacje samodzielnie (zamiast je wkleić), Android Studio automatycznie zaimportuje klasy adnotacji.

  1. Zaktualizuj klasę Word o adnotacje zgodnie z tym kodem:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

Zobaczmy, jak działają te adnotacje:

  • @Entity(tableName = "word_table")
    Każda klasa @Entity reprezentuje tabelę SQLite. Dodaj adnotację do klasy, by wskazać, że jest to element. Możesz określić nazwę tabeli, jeśli chcesz, aby różniła się od nazwy klasy. Spowoduje to nadanie tabeli „"word_table"”.
  • @PrimaryKey
    Każdy element musi mieć klucz podstawowy. Dla uproszczenia każde słowo pełni rolę głównego klucza.
  • @ColumnInfo(name = "word")
    Określa nazwę kolumny w tabeli, jeśli chcesz, by różniła się od nazwy zmiennej użytkownika. To nazwa kolumny "słowo&quot.
  • Każda usługa przechowywana w bazie musi być publicznie widoczna (jest to ustawienie domyślne Kotlin).

Pełną listę adnotacji znajdziesz w artykule z podsumowaniem pakietu pokoi.

Co to jest DAO?

W obiekcie DAO (obiektu dostępu do danych) możesz określić zapytania SQL i powiązać je z wywołaniami metod. Kompilator sprawdza SQL i generuje zapytania na podstawie adnotacji do wygodnych zapytań, takich jak @Insert. Do utworzenia przejrzystego interfejsu API na potrzeby tworzenia sal zostanie użyte środowisko DAO.

DAO musi być klasą interfejsu lub abstrakcyjną.

Domyślnie wszystkie zapytania muszą być wykonywane w oddzielnym wątku.

W pokoju jest dostępna pomoc w kohorcie, co pozwala dodawać do zapytań adnotacje z modyfikatorem suspend, a następnie wywoływać je z poziomu rutyny lub z innej funkcji zawieszenia.

Implementacja DAO

Napiszmy, że chcesz dodać DAO:

  • Układanie wszystkich alfabetycznie słów
  • Wstawianie słowa
  • Usuwanie wszystkich słów
  1. Utwórz nowy plik zajęć o nazwie WordDao w Kotlin.
  2. Skopiuj ten kod i wklej go w narzędziu WordDao, a następnie popraw zaimportowane elementy, by ułatwić kompilację.
@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()
}

Omówmy teraz:

  • WordDao jest interfejsem. DAO musi być interfejsami lub klasami abstrakcyjnymi.
  • Adnotacja @Dao określa ją jako klasę DAO pokoju.
  • suspend fun insert(word: Word) : deklaruje zawieszoną funkcję, która pozwala wstawić 1 słowo.
  • Adnotacja @Insert to specjalna adnotacja metody DAO, w której nie musisz podawać żadnego kodu SQL. Dostępne są również adnotacje @Delete i @Update do usuwania i aktualizowania wierszy, ale nie używasz ich w tej aplikacji.
  • onConflict = OnConflictStrategy.IGNORE: wybrana strategia onKonflikt ignoruje nowe słowo, jeśli jest dokładnie takie samo jak na liście. Aby dowiedzieć się więcej o dostępnych strategiach konfliktów, przeczytaj dokumentację.
  • suspend fun deleteAll(): deklaruje funkcję zawieszenia, która usuwa wszystkie słowa.
  • Nie ma adnotacji umożliwiającej usunięcie wielu elementów, więc jest oznaczona adnotacją „@Query”.
  • @Query("DELETE FROM word_table"): @Query wymaga podania w adnotacji zapytania SQL jako parametru w formie ciągu, co umożliwia wykonywanie złożonych zapytań odczytu i innych operacji.
  • fun getAlphabetizedWords(): List<Word>: metoda polegająca na uzyskaniu wszystkich słów i zwróceniu wartości List o wartości Words.
  • @Query("SELECT * from word_table ORDER BY word ASC"): zapytanie wyświetlające listę słów posortowanych w kolejności rosnącej.

Gdy zmieniają się dane, które zwykle chcesz wykonać, np. wyświetlić zaktualizowane dane w interfejsie. W takiej sytuacji musisz obserwować dane, aby zareagować na zmiany.

W zależności od sposobu przechowywania danych może to być trudne. Obserwowanie zmian w danych o wielu komponentach aplikacji może tworzyć wyraźne, sztywne ścieżki zależności między komponentami. Utrudnia to między innymi testowanie i debugowanie.

Ten problem rozwiązuje LiveData, czyli klasa cyklu życia w celu obserwacji danych. Użyj wartości zwracanej przez typ LiveData w opisie metody, a narzędzie Room wygeneruje cały niezbędny kod do zaktualizowania LiveData podczas aktualizowania bazy danych.

W WordDao zmień podpis metody getAlphabetizedWords(), tak by zwrócony tag List<Word> był opakowany ciągiem LiveData.

   @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): LiveData<List<Word>>

W dalszej części tego ćwiczenia będziesz śledzić zmiany danych za pomocą Observer w narzędziu MainActivity.

Co to jest baza danych pokoju?

  • Pokój to warstwa danych nad bazą danych SQLite.
  • Pokój służy do wykonywania zadań, które kiedyś były związane z SQLiteOpenHelper.
  • Pokój używa zapytania DAO do wysyłania zapytań do swojej bazy danych.
  • Aby uniknąć problemów z działaniem interfejsu, domyślnie pokój nie zezwala na zapytania w wątku głównym. Gdy zapytania dotyczące pokoju zwracają LiveData, zapytania są automatycznie uruchamiane asynchronicznie w wątku w tle.
  • Pokój umożliwia sprawdzanie instrukcji SQL przy tworzeniu kompilacji.

Wdrażanie bazy danych sali

Klasa bazy danych pokoju musi być abstrakcyjna i zawierać ciąg RoomDatabase. Zwykle wystarczy użyć jednego wystąpienia bazy danych pokoju dla całej aplikacji.

Zróbmy to teraz.

  1. Utwórz plik zajęć 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 pokoju musi mieć wartość abstract i zawierać rozszerzenie RoomDatabase
  • Dodaj do klasy informację, że jest to baza danych pokoju, korzystając z tagu @Database i za pomocą parametrów adnotacji zadeklaruj jednostki należące do bazy danych i ustaw numer wersji. Każdy element odpowiada tabeli, która zostanie utworzona w bazie danych. Migracje baz danych wykraczają poza zakres tego ćwiczenia z ćwiczeniami z programowania, dlatego ustawiliśmy tu wartość exportSchema na false, aby uniknąć ostrzeżenia o kompilacji. W prawdziwej aplikacji warto ustawić katalog tak, by usługa Room wyeksportowała schemat, co pozwoliło sprawdzić bieżący schemat w systemie kontroli wersji.
  • Baza danych ujawnia DAO za pomocą abstrakcyjnej metody "getter" dla każdego @Dao.
  • Zdefiniowaliśmy singleton (WordRoomDatabase,), by zapobiec jednoczesnemu otwieraniu wielu wystąpień bazy danych.
  • getDatabase zwraca pojedynczeton. Spowoduje to utworzenie bazy danych po raz pierwszy. Przy użyciu Kreatora baz danych Room&#39s utworzysz obiekt RoomDatabase w kontekście aplikacji z klasy WordRoomDatabase i nadasz mu nazwę "word_database".

Co to jest repozytorium?

Klasa repozytorium abstrakuje dostęp do wielu źródeł danych. Repozytorium nie jest częścią bibliotek Składników architektury, ale jest zalecaną sprawdzoną metodą rozdziału kodu i architektury. Klasa Repository zapewnia czysty interfejs API umożliwiający dostęp do reszty aplikacji.

Dlaczego warto korzystać z repozytorium?

Repozytorium zarządza zapytaniami i umożliwia korzystanie z wielu backendów. W typowym przykładzie repozytorium wdraża funkcję logiczną określającą, czy dane mają być pobierane z sieci, czy z wyników zapisanych w lokalnej bazie danych.

Wdrażanie repozytorium

Utwórz plik zajęć 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:

  • DAO jest przekazywany do konstruktora repozytorium, a nie do całej bazy danych. Dzieje się tak, ponieważ potrzebuje tylko dostępu do DAO, ponieważ DAO zawiera wszystkie metody odczytu/zapisu w bazie danych. Nie ma potrzeby udostępniania całej bazy danych repozytorium.
  • Lista słów jest własnością publiczną. Zainicjowaliśmy go, pobierając listę słów LiveData z pokoju. Jest to możliwe ze względu na sposób zdefiniowania metody getAlphabetizedWords, która zwraca LiveData w kroku &DataDataLive. Pokój wykonuje wszystkie zapytania w oddzielnym wątku. Następnie zaobserwowany LiveData powiadomi obserwatora w wątku głównym o zmianie danych.
  • Modyfikator suspend informuje kompilator, że należy go wywołać z poziomu rutyny lub innej funkcji zawieszającej.

Co to jest ViewModel?

Rola ViewModel to dostarczanie danych do interfejsu i przetrwanie zmian konfiguracji. ViewModel działa jako centrum komunikacyjne między repozytorium a interfejsem. Za pomocą ViewModel możesz też udostępniać dane między fragmentami. Element ViewModel należy do biblioteki cyklu życia.

Wstęp do tego tematu znajdziesz w artykule ViewModel Overview i w poście na blogu ViewModels: A Simple Example.

Dlaczego warto korzystać z modelu ViewModel?

ViewModel przechowuje dane aplikacji w interfejsie w sposób uwzględniający cykl życia, który przetrwa zmiany konfiguracji. Rozdzielenie danych aplikacji z klas Activity i Fragment pozwoli Ci lepiej zastosować zasadę jednoosobowej odpowiedzialności: rysunki i fragmenty ekranu będą odpowiadać za działania na Twoich ekranach, a ViewModel zajmie się przechowywaniem i przetwarzaniem wszystkich danych potrzebnych do działania interfejsu.

W polu ViewModel użyj LiveData do zmiennych danych, które będą używane lub wyświetlane przez interfejs. LiveData ma kilka zalet:

  • Możesz dodać obserwatora do danych (zamiast przeprowadzać ankietę pod kątem zmian) i aktualizować interfejs
    tylko wtedy, gdy te dane się zmienią.
  • Repozytorium i interfejs użytkownika są całkowicie oddzielone znakiem ViewModel.
  • Nie ma wywołań bazy danych z usługi ViewModel (wszystkie te elementy są obsługiwane w repozytorium), dzięki czemu kod może być bardziej testowany.

viewModelScope

W Kotlin wszystkie algorytmy są uruchamiane w CoroutineScope. Zakres kontroluje czas trwania współdecydowanych przez zadanie. Jeśli anulujesz zadanie w zakresie, zostaną anulowane wszystkie jego kopie rozpoczęte.

Biblioteka lifecycle-viewmodel-ktx na Androida dodaje viewModelScope jako funkcję rozszerzenia klasy ViewModel, co umożliwia pracę z zakresami.

Więcej informacji o współpracy z kohortami znajdziesz w kroku 5 w ćwiczeniach z programowania Używanie Kotlin Coroutines w swojej aplikacji na Androida lub w artykule Easy Coroutines in Android: viewModelScope post na blogu.

Implementowanie ViewView

Utwórz plik zajęć Kotlin dla domeny 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)
    }
}

Oto one:

  • Utworzono klasę o nazwie WordViewModel, która pobiera Application jako parametr i rozszerza AndroidViewModel.
  • Dodano prywatną zmienną użytkownika przechowywaną odniesienie do repozytorium.
  • Dodaliśmy publiczną zmienną LiveData użytkownika do pamięci podręcznej listy słów.
  • Utworzono blok init, który uzyskuje odniesienie do elementu WordDao z WordRoomDatabase.
  • W bloku init zbudowano obiekt WordRepository na podstawie elementu WordRoomDatabase.
  • W bloku init zainicjowano funkcję allWordsLiveData przy użyciu repozytorium.
  • Utworzono metodę kodu insert(), która wywołuje metodę insert() w Repozytorium. Dzięki temu implementacja insert() jest zawarta w interfejsie. Nie chcemy wstawiać bloku głównego wątku, więc uruchamiamy nową algorytm i wywołujemy repozytorium, które jest funkcją zawieszenia. Jak wspomnieliśmy, obiekty ViewModels mają zakres ograniczony na podstawie ich cyklu życia (viewModelScope), z którego korzystamy tutaj.

Następnie musisz dodać układ XML listy i elementów.

W tym ćwiczeniu zakładamy, że znasz się na tworzeniu układów w formacie XML, dlatego udostępniamy Ci kod.

Ustaw materiał motywu aplikacji, ustawiając wartość nadrzędną AppTheme na Theme.MaterialComponents.Light.DarkActionBar. Dodaj styl 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>

Dodaj układ 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 polu layout/activity_main.xml zastąp TextView wartością RecyclerView, a następnie dodaj pływający przycisk polecenia (FAB). Teraz Twój układ powinien 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 i kreacji powinien odpowiadać dostępnemu działaniu, dlatego chcemy zastąpić ikonę znakiem '+'.

Najpierw musimy dodać nowy zasób wektorowy:

  1. Wybierz File > New > Vector Asset.
  2. W polu Klip: kliknij ikonę robota Androida.
  3. Wyszukaj i wybierz zasób. Kliknij OK
    .
  4. Następnie kliknij Dalej.
  5. Potwierdź ścieżkę ikony jako main > drawable i kliknij Zakończ, aby dodać zasób.
  6. Nadal w aplikacji layout/activity_main.xml zaktualizuj przycisk FAB, aby zawierał nowy element, który można narysować:
<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 będą wyświetlane w: RecyclerView. To trochę ładniejsze niż wyrzucenie danych w: TextView. W tym ćwiczeniu zakładamy, że wiesz, jak działają RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder i RecyclerView.Adapter.

Uwaga: zmienna words w adapterze zapisuje dane w pamięci podręcznej. W następnym zadaniu dodasz kod, który będzie automatycznie aktualizować dane.

Utwórz plik zajęć Kotlin dla WordListAdapter z rozszerzeniem 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() 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 upewnić się, że wszystko działa. Brak elementów, ponieważ nie masz jeszcze połączonych danych.

W bazie danych nie ma danych. Możesz dodać dane na 2 sposoby: dodając część danych po otwarciu bazy danych i dodając znak Activity za dodanie słów.

Aby usuwać całą zawartość i ponownie wypełnić bazę danych przy każdym uruchomieniu aplikacji, utwórz RoomDatabase.Callback i zastąp wartość onOpen(). Nie możesz wykonywać operacji na bazach danych w wątku interfejsu, więc onOpen() uruchamia algorytm dyspozytora zamówienia reklamowego.

Aby uruchomić algorytm, potrzebujesz CoroutineScope. Zaktualizuj metodę getDatabase klasy WordRoomDatabase, aby uzyskać też zakres podstawowy:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

Zaktualizuj inicjator pobierania bazy danych w bloku init z WordViewModel, aby też przekazywać zakres:

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

W WordRoomDatabase tworzymy niestandardową implementację obiektu RoomDatabase.Callback(), która otrzymuje również parametr CoroutineScope jako parametr konstruktora. Następnie zastępujemy metodę onOpen danymi w bazie danych.

Oto kod do 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 kompilacji bazy danych tuż przed wywołaniem .build() w Room.databaseBuilder():

.addCallback(WordDatabaseCallback(scope))

Końcowy kod powinien wyglądać tak:

@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 typu ciąg znaków w języku 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 kolorów w value/colors.xml:

<color name="buttonLabel">#FFFFFF</color>

Utwórz nowy plik zasobu wymiaru:

  1. Kliknij moduł aplikacji w oknie Projekt.
  2. Wybierz File > New > Android Resource File (Plik &gt Nowy plik zasobu Android)
  3. Z dostępnych kwalifikatorów wybierz Wymiar.
  4. Ustawianie nazwy pliku: przyciemnione

Dodaj te zasoby wymiaru do grupy values/dimens.xml:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

Utwórz nowy pusty Android Activity z szablonem pustej aktywności:

  1. Wybierz Plik > Nowa > Aktywność > Pusta aktywność
  2. Wpisz NewWordActivity.
  3. 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, dodając ten kod:

<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 użytkownika z bazą danych poprzez zapisanie nowych słów wpisanych przez użytkownika i wyświetlenie w RecyclerView bieżącej zawartości bazy danych tych słów.

Aby wyświetlić bieżącą zawartość bazy danych, dodaj obserwatora, który rejestruje LiveData w ViewModel.

Po każdej zmianie danych wywoływane jest wywołanie zwrotne onChanged(), które wywołuje metodę setWords() adaptera, aby zaktualizować dane adaptera w pamięci podręcznej i wyświetlić odświeżoną listę.

W MainActivity utwórz zmienną członkostwa w ViewModel:

private lateinit var wordViewModel: WordViewModel

Użyj narzędzia ViewModelProvider, aby powiązać swoje konto ViewModel z kontem Activity.

Po uruchomieniu Activity pojawi się ViewModel w ViewModelProviders. Po zniszczeniu aktywności, na przykład po zmianie konfiguracji, właściwość ViewModel pozostaje niezmieniona. Po ponownym odtworzeniu aktywności element ViewModelProviders zwraca istniejący element ViewModel. Więcej informacji: ViewModel

W polu onCreate() pod blokiem kodu RecyclerView uzyskaj ViewModel z ViewModelProvider:

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

Dodaj też obserwatora właściwości allWords LiveData z elementu WordViewModel.

Metoda onChanged() (domyślna metoda dla Lambdy) uruchamia się, gdy obserwowane dane zmieniają się, a aktywność działa 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 otworzyć NewWordActivity po kliknięciu FAB. Kiedy wrócimy do MainActivity, aby wstawić nowe słowo w bazie danych lub wyświetlić Toast. Aby to osiągnąć, zacznij od zdefiniowania kodu żądania:

private val newWordActivityRequestCode = 1

W MainActivity dodaj kod onActivityResult() dla NewWordActivity.

Jeśli działanie zwraca wartość RESULT_OK, wstaw zwrócone słowo do bazy danych, wywołując metodę insert() metody 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,rozpoczęciu NewWordActivity, gdy użytkownik kliknie przycisk FAB. W MainActivity onCreate znajdź przycisk FAB i dodaj onClickListener za pomocą tego kodu:

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 zostanie automatycznie zaktualizowany.

Skoro masz już działającą aplikację, przypomnijmy sobie, co udało Ci się stworzyć. Powtórzę strukturę aplikacji:

Komponenty aplikacji:

  • MainActivity: wyświetla słowa na liście przy użyciu wyrażeń RecyclerView i WordListAdapter. W MainActivity jest Observer, który rejestruje słowa LiveData z bazy danych i otrzymuje powiadomienia o zmianach.
  • NewWordActivity: dodaje nowe słowo do listy.
  • WordViewModel: udostępnia metody dostępu do warstwy danych i zwraca dane LiveData, aby funkcja ActivityActivity mogła utworzyć relację z obserwatorem*.
  • LiveData<List<Word>>: umożliwia automatyczne aktualizowanie komponentów interfejsu. W polu MainActivity znajduje się Observer, który rejestruje słowa LiveData z bazy danych i jest powiadamiane, gdy zostaną zmienione.
  • Repository: zarządza co najmniej jednym źródłem danych. Repository udostępnia metody, które interfejs ViewModel wchodzi w interakcję z dostawcą danych. W tej aplikacji backend jest bazą danych sali.
  • Room: to opakowanie i wdraża bazę danych SQLite. Pokój wymaga od Ciebie dużo pracy, którą zazwyczaj zajmujesz się samodzielnie.
  • DAO: mapuje wywołania metod na zapytania do bazy danych, tak więc gdy Repozytorium wywołuje metodę, taką jak getAlphabetizedWords(), pokój może wykonać SELECT * from word_table ORDER BY word ASC.
  • Word: to klasa encji zawierająca jedno słowo.

* Views i Activities (oraz Fragments) wchodzą w interakcję z danymi tylko przez ViewModel. Nie ma więc znaczenia, skąd pochodzą dane.

Przepływ danych w automatycznych aktualizacjach interfejsu (interaktywny interfejs)

Aktualizacja automatyczna jest możliwa, ponieważ używamy danych LiveData. W polu MainActivity znajduje się Observer, który rejestruje słowa LiveData z bazy danych i jest powiadamiane, gdy zostaną zmienione. W przypadku zmiany wykonywana jest metoda onChange() obserwatora, która aktualizuje mWords w WordListAdapter.

Dane są dostępne z powodu LiveData. Zwracana jest też właściwość LiveData<List<Word>> zwracana przez właściwość WordViewModel allWords.

WordViewModel ukrywa całą warstwę backendu w warstwie interfejsu. Zapewnia metody dostępu do warstwy danych i zwraca LiveData, aby MainActivity mógł skonfigurować relację z obserwatorem. Views i Activities (oraz Fragments) wchodzą w interakcję z danymi tylko za pomocą ViewModel. Nie ma więc znaczenia, skąd pochodzą dane.

W tym przypadku dane pochodzą z Repository. ViewModel nie musi wiedzieć, z jakim repozytorium korzysta. Musi tylko wiedzieć, jak używać Repository, korzystając z metod Repository.

Repozytorium zarządza co najmniej jednym źródłem danych. W aplikacji WordListSample backendem jest baza danych sali. Sala jest opakowana i wdraża bazę danych SQLite. Pokój wymaga od Ciebie dużo pracy, którą zazwyczaj zajmujesz się samodzielnie. Na przykład usługa Room wykonuje wszystkie czynności znane z klasy SQLiteOpenHelper.

DAO mapuje wywołania metod na zapytania do bazy danych, więc gdy Repozytorium wywołuje metodę taką jak getAllWords(), pokój może wykonać SELECT * from word_table ORDER BY word ASC.

Wynik zwrócony w zapytaniu jest obserwowany przez LiveData, więc za każdym razem, gdy dane w pokoju się zmienią, uruchomiona zostanie metoda onChanged() interfejsu Observer, a interfejs użytkownika zostanie zaktualizowany.

[Opcjonalnie] Pobierz kod rozwiązania

Jeśli jeszcze nie masz tego za sobą, możesz przyjrzeć się kodowi rozwiązania ćwiczeń z programowania. Możesz wyświetlić repozytorium GitHub lub pobrać kod tutaj:

Pobierz kod źródłowy

Rozpakuj pobrany plik ZIP. Spowoduje to rozpakowanie folderu głównego android-room-with-a-view-kotlin, który zawiera kompletną aplikację.