Номер Android с видом — Котлин

Компоненты архитектуры предназначены для предоставления рекомендаций по архитектуре приложений с библиотеками для общих задач, таких как управление жизненным циклом и сохранение данных. Компоненты архитектуры помогают структурировать приложение таким образом, чтобы оно было надежным, тестируемым и удобным в сопровождении с меньшим количеством стандартного кода. Библиотеки компонентов архитектуры являются частью Android Jetpack .

Это версия кодовой лаборатории Kotlin. Версию на языке программирования Java можно найти здесь .

Если вы столкнетесь с какими-либо проблемами (ошибки в коде, грамматические ошибки, нечеткие формулировки и т. д.) при работе с этой лабораторией кода, сообщите о проблеме по ссылке Сообщить об ошибке в левом нижнем углу лаборатории кода.

Предпосылки

Вы должны быть знакомы с Kotlin, концепциями объектно-ориентированного проектирования и основами разработки под Android, в частности:

Также полезно ознакомиться с шаблонами архитектуры программного обеспечения, которые отделяют данные от пользовательского интерфейса, такими как MVP или MVC. Эта лаборатория кода реализует архитектуру, определенную в Руководстве по архитектуре приложений .

Эта лаборатория кода ориентирована на компоненты архитектуры Android. Концепции и код, не относящиеся к теме, предоставляются вам для простого копирования и вставки.

Если вы не знакомы с Kotlin, версия этой кодлабы на языке программирования Java приведена здесь .

Что ты будешь делать

В этой лабораторной работе вы узнаете, как спроектировать и создать приложение, используя Комнату компонентов архитектуры, ViewModel и LiveData, а также создать приложение, которое выполняет следующие действия:

  • Реализует рекомендуемую нами архитектуру с использованием компонентов архитектуры Android.
  • Работает с базой данных для получения и сохранения данных и предварительно заполняет базу данных некоторыми словами.
  • Отображает все слова в RecyclerView в MainActivity .
  • Открывает второе действие, когда пользователь нажимает кнопку +. Когда пользователь вводит слово, оно добавляется в базу данных и в список.

Приложение без излишеств, но достаточно сложное, чтобы вы могли использовать его в качестве шаблона для дальнейшего развития. Вот предварительный просмотр:

Что вам понадобится

Эта лаборатория кода содержит весь код, необходимый для создания полного приложения.

Существует множество шагов по использованию компонентов архитектуры и реализации рекомендуемой архитектуры. Самое главное — создать мысленную модель того, что происходит, понять, как части соединяются друг с другом и как передаются данные. Работая с этой кодовой лабораторией, не просто копируйте и вставляйте код, а попытайтесь начать вырабатывать это внутреннее понимание.

Чтобы ввести терминологию, вот краткое введение в Компоненты Архитектуры и то, как они работают вместе. Обратите внимание, что эта кодовая лаборатория фокусируется на подмножестве компонентов, а именно на LiveData, ViewModel и Room. Каждый компонент объясняется больше, как вы его используете.

На этой диаграмме показана базовая форма архитектуры:

Entity : Аннотированный класс, описывающий таблицу базы данных при работе с Room .

База данных SQLite: в хранилище устройства. Библиотека постоянства Room создает и поддерживает эту базу данных для вас.

DAO : объект доступа к данным. Отображение SQL-запросов на функции. Когда вы используете DAO, вы вызываете методы, а Room позаботится обо всем остальном.

База данных комнаты : упрощает работу с базой данных и служит точкой доступа к базовой базе данных SQLite (скрывает SQLiteOpenHelper) . База данных Room использует DAO для выполнения запросов к базе данных SQLite.

Репозиторий: создаваемый вами класс, который в основном используется для управления несколькими источниками данных.

ViewModel : действует как центр связи между репозиторием (данными) и пользовательским интерфейсом. Пользовательскому интерфейсу больше не нужно беспокоиться о происхождении данных. Экземпляры ViewModel переживают воссоздание Activity/Fragment.

LiveData : Класс держателя данных, который можно наблюдать . Всегда хранит/кэширует последнюю версию данных и уведомляет своих наблюдателей, когда данные изменились. LiveData жизненный цикл. Компоненты пользовательского интерфейса просто наблюдают за соответствующими данными, не останавливая и не возобновляя наблюдение. LiveData автоматически управляет всем этим, так как он знает о соответствующих изменениях состояния жизненного цикла во время наблюдения.

Обзор архитектуры RoomWordSample

На следующей диаграмме показаны все части приложения. Каждая из рамок (за исключением базы данных SQLite) представляет класс, который вы создадите.

  1. Откройте Android Studio и нажмите «Начать новый проект Android Studio».
  2. В окне «Создать новый проект» выберите « Пустая активность » и нажмите « Далее».
  3. На следующем экране назовите приложение RoomWordSample и нажмите Finish .

Затем вам нужно будет добавить библиотеки компонентов в ваши файлы Gradle.

  1. В Android Studio перейдите на вкладку «Проекты» и разверните папку Gradle Scripts.

Откройте build.gradle ( Модуль: приложение ).

  1. Примените подключаемый модуль Kotlin обработчика аннотаций kapt , добавив его после других подключаемых модулей, определенных в верхней части build.gradle ( Module: app ).
apply plugin: 'kotlin-kapt'
  1. Добавьте блок packagingOptions внутрь блока android , чтобы исключить модуль атомарных функций из пакета и предотвратить появление предупреждений.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. Добавьте следующий код в конец блока 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. В build.gradle ( Project: RoomWordsSample ) добавьте номера версий в конец файла, как показано в приведенном ниже коде.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

Данные для этого приложения — это слова, и вам понадобится простая таблица для хранения этих значений:

Room позволяет создавать таблицы через Entity . Давайте сделаем это сейчас.

  1. Создайте новый файл класса Kotlin с именем Word , содержащий класс данных Word .
    Этот класс будет описывать сущность (которая представляет таблицу SQLite) для ваших слов. Каждое свойство в классе представляет столбец в таблице. Room в конечном итоге будет использовать эти свойства как для создания таблицы, так и для создания экземпляров объектов из строк в базе данных.

Вот код:

data class Word(val word: String)

Чтобы сделать класс Word значимым для базы данных Room, вам необходимо аннотировать его. Аннотации определяют, как каждая часть этого класса относится к записи в базе данных. Room использует эту информацию для генерации кода.

Если вы вводите аннотации самостоятельно (вместо вставки), Android Studio автоматически импортирует классы аннотаций.

  1. Обновите свой класс Word с помощью аннотаций, как показано в этом коде:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

Давайте посмотрим, что делают эти аннотации:

  • @Entity(tableName = "word_table" )
    Каждый класс @Entity представляет таблицу SQLite. Аннотируйте объявление своего класса, чтобы указать, что это сущность. Вы можете указать имя таблицы, если хотите, чтобы оно отличалось от имени класса. Это имя таблицы "word_table".
  • @PrimaryKey
    Каждому объекту нужен первичный ключ. Для простоты каждое слово действует как собственный первичный ключ.
  • @ColumnInfo(name = "word" )
    Указывает имя столбца в таблице, если вы хотите, чтобы оно отличалось от имени переменной-члена. Это имя столбца «слово».
  • Каждое свойство, хранящееся в базе данных, должно быть общедоступным, что по умолчанию используется в Kotlin.

Вы можете найти полный список аннотаций в сводном справочнике пакетов Room .

Что такое ДАО?

В DAO (объекте доступа к данным) вы указываете SQL-запросы и связываете их с вызовами методов. Компилятор проверяет SQL и создает запросы на основе удобных аннотаций для общих запросов, таких как @Insert . Room использует DAO для создания чистого API для вашего кода.

DAO должен быть интерфейсом или абстрактным классом.

По умолчанию все запросы должны выполняться в отдельном потоке.

Room поддерживает сопрограммы, что позволяет аннотировать ваши запросы с помощью модификатора suspend , а затем вызывать их из сопрограммы или из другой функции приостановки.

Внедрить ДАО

Давайте напишем DAO, который предоставляет запросы для:

  • Получение всех слов в алфавитном порядке
  • Вставка слова
  • Удаление всех слов
  1. Создайте новый файл класса Kotlin с именем WordDao .
  2. Скопируйте и вставьте следующий код в WordDao и при необходимости исправьте импорт, чтобы он скомпилировался.
@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()
}

Давайте пройдемся по нему:

  • WordDao — это интерфейс; DAO должны быть либо интерфейсами, либо абстрактными классами.
  • Аннотация @Dao идентифицирует его как класс DAO для Room.
  • suspend fun insert(word: Word) : Объявляет функцию приостановки для вставки одного слова.
  • Аннотация @Insert — это специальная аннотация метода DAO, в которой вам не нужно указывать какой-либо SQL! (Есть также аннотации @Delete и @Update для удаления и обновления строк, но вы не используете их в этом приложении.)
  • onConflict = OnConflictStrategy.IGNORE : выбранная стратегия onConflict игнорирует новое слово, если оно точно такое же, как уже существующее в списке. Чтобы узнать больше о доступных стратегиях конфликтов, ознакомьтесь с документацией .
  • suspend fun deleteAll() : Объявляет функцию приостановки для удаления всех слов.
  • Нет удобной аннотации для удаления нескольких сущностей, поэтому она аннотируется универсальным @Query .
  • @Query ("DELETE FROM word_table") : @Query требует, чтобы вы предоставили SQL-запрос в качестве строкового параметра аннотации, что позволяет выполнять сложные запросы на чтение и другие операции.
  • fun getAlphabetizedWords(): List<Word> : метод для получения всех слов и возврата List Words .
  • @Query( "SELECT * from word_table ORDER BY word ASC" ) : Запрос, который возвращает список слов, отсортированных в порядке возрастания.

При изменении данных обычно требуется выполнить какое-либо действие, например отобразить обновленные данные в пользовательском интерфейсе. Это означает, что вы должны наблюдать за данными, чтобы, когда они изменятся, вы могли реагировать.

В зависимости от того, как хранятся данные, это может быть сложно. Наблюдение за изменениями данных в нескольких компонентах вашего приложения может создать явные, жесткие пути зависимости между компонентами. Это, среди прочего, затрудняет тестирование и отладку.

LiveData , класс библиотеки жизненного цикла для наблюдения за данными, решает эту проблему. Используйте возвращаемое значение типа LiveData в описании метода, и Room сгенерирует весь необходимый код для обновления LiveData при обновлении базы данных.

В WordDao измените сигнатуру метода getAlphabetizedWords() , чтобы возвращаемый List<Word> был заключен в LiveData .

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

Позже в этой кодовой лаборатории вы отслеживаете изменения данных с помощью Observer в MainActivity .

Что такое база данных Room ?

  • Room — это уровень базы данных поверх базы данных SQLite.
  • Room позаботится о рутинных задачах, которые вы раньше выполняли с помощью SQLiteOpenHelper .
  • Room использует DAO для выполнения запросов к своей базе данных.
  • По умолчанию, чтобы избежать плохой производительности пользовательского интерфейса, Room не позволяет вам выполнять запросы в основном потоке. Когда запросы Room возвращают LiveData , запросы автоматически выполняются асинхронно в фоновом потоке.
  • Room обеспечивает проверку операторов SQLite во время компиляции.

Реализовать базу данных Room

Ваш класс базы данных Room должен быть абстрактным и расширять RoomDatabase . Обычно вам нужен только один экземпляр базы данных Room для всего приложения.

Давайте сделаем один сейчас.

  1. Создайте файл класса Kotlin с именем WordRoomDatabase и добавьте в него этот код:
// 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
            }
        }
   }
}

Давайте пройдемся по коду:

  • Класс базы данных для Room должен быть abstract и расширять RoomDatabase
  • Вы аннотируете класс как базу данных Room с помощью @Database и используете параметры аннотации для объявления объектов, принадлежащих базе данных, и устанавливаете номер версии. Каждой сущности соответствует таблица, которая будет создана в базе данных. Миграция базы данных выходит за рамки этой лаборатории кода, поэтому мы установили для exportSchema значение false, чтобы избежать предупреждения о сборке. В реальном приложении вам следует рассмотреть возможность установки каталога для Room, который будет использоваться для экспорта схемы, чтобы вы могли проверить текущую схему в вашей системе управления версиями.
  • База данных предоставляет DAO через абстрактный метод получения для каждого @Dao.
  • Мы определили синглтон WordRoomDatabase, чтобы предотвратить одновременное открытие нескольких экземпляров базы данных.
  • getDatabase возвращает синглтон. Он создаст базу данных при первом доступе к ней, используя конструктор базы данных Room для создания объекта RoomDatabase в контексте приложения из класса WordRoomDatabase и назовет его "word_database" .

Что такое репозиторий?

Класс репозитория абстрагирует доступ к нескольким источникам данных. Репозиторий не является частью библиотек компонентов архитектуры, но рекомендуется для разделения кода и архитектуры. Класс Repository предоставляет чистый API для доступа к данным для остальной части приложения.

Зачем использовать репозиторий?

Репозиторий управляет запросами и позволяет использовать несколько бэкэндов. В наиболее распространенном примере репозиторий реализует логику принятия решения о том, следует ли извлекать данные из сети или использовать результаты, кэшированные в локальной базе данных.

Реализация репозитория

Создайте файл класса Kotlin с именем WordRepository и вставьте в него следующий код:

// 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)
    }
}

Основные выводы:

  • DAO передается в конструктор репозитория, а не всей базы данных. Это связано с тем, что ему нужен доступ только к DAO, поскольку DAO содержит все методы чтения/записи для базы данных. Нет необходимости выставлять всю базу данных в репозиторий.
  • Список слов является общедоступной собственностью. Он инициализируется получением списка слов LiveData из Room; мы можем сделать это из-за того, как мы определили метод getAlphabetizedWords для возврата LiveData на этапе «Класс LiveData». Room выполняет все запросы в отдельном потоке. Затем наблюдаемые LiveData будут уведомлять наблюдателя в основном потоке об изменении данных.
  • Модификатор suspend сообщает компилятору, что его нужно вызывать из сопрограммы или другой функции приостановки.

Что такое ViewModel?

Роль ViewModel заключается в предоставлении данных пользовательскому интерфейсу и сохранении изменений конфигурации. ViewModel действует как центр связи между репозиторием и пользовательским интерфейсом. Вы также можете использовать ViewModel для обмена данными между фрагментами. ViewModel является частью библиотеки жизненного цикла .

Вводное руководство по этой теме см. в ViewModel Overview или в записи блога ViewModels: A Simple Example .

Зачем использовать ViewModel?

ViewModel хранит данные пользовательского интерфейса вашего приложения с учетом жизненного цикла, сохраняя изменения конфигурации. Отделение данных пользовательского интерфейса вашего приложения от классов Activity и Fragment позволяет лучше следовать принципу единой ответственности: ваши действия и фрагменты отвечают за отрисовку данных на экране, а ViewModel может позаботиться о хранении и обработке всех данных, необходимых для пользовательского интерфейса. .

В ViewModel используйте LiveData для изменяемых данных, которые будет использовать или отображать пользовательский интерфейс. Использование LiveData имеет несколько преимуществ:

  • Вы можете поставить наблюдателя на данные (вместо опроса на наличие изменений) и обновлять только
    пользовательский интерфейс, когда данные фактически изменяются.
  • Репозиторий и пользовательский интерфейс полностью разделены ViewModel .
  • Нет вызовов базы данных из ViewModel (все это обрабатывается в репозитории), что делает код более тестируемым.

ViewModelScope

В Kotlin все сопрограммы выполняются внутри CoroutineScope . Область действия контролирует время жизни сопрограмм через свою работу. Когда вы отменяете задание области, она отменяет все сопрограммы, запущенные в этой области.

Библиотека lifecycle-viewmodel-ktx добавляет viewModelScope в качестве функции расширения класса ViewModel , позволяя работать с областями.

Чтобы узнать больше о работе с сопрограммами в ViewModel, ознакомьтесь с Шагом 5 в лаборатории кода Использование сопрограмм Kotlin в приложении для Android или в блоге Easy Coroutines в Android: viewModelScope .

Реализовать ViewModel

Создайте файл класса Kotlin для WordViewModel и добавьте в него этот код:

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)
    }
}

Здесь у нас есть:

  • Создал класс WordViewModel , который получает Application в качестве параметра и расширяет AndroidViewModel .
  • Добавлена ​​частная переменная-член для хранения ссылки на репозиторий.
  • Добавлена ​​общедоступная переменная-член LiveData для кэширования списка слов.
  • Создал блок init , который получает ссылку на WordDao из WordRoomDatabase .
  • В блоке init WordRepository на основе WordRoomDatabase .
  • В блоке init инициализировал allWords LiveData с помощью репозитория.
  • Создан метод insert() оболочки, который вызывает метод insert() () репозитория. Таким образом, реализация insert() инкапсулируется из пользовательского интерфейса. Мы не хотим, чтобы вставка блокировала основной поток, поэтому мы запускаем новую сопрограмму и вызываем вставку репозитория, которая является функцией приостановки. Как уже упоминалось, ViewModels имеют область действия сопрограммы, основанную на их жизненном цикле, называемую viewModelScope , которую мы используем здесь.

Далее вам нужно добавить макет XML для списка и элементов.

В этой лаборатории кода предполагается, что вы знакомы с созданием макетов в XML, поэтому мы просто предоставляем вам код.

Сделайте тему вашего приложения материальной, задав для родительского объекта AppTheme значение Theme.MaterialComponents.Light.DarkActionBar . Добавьте стиль для элементов списка в 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>

Добавьте 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>

В layout/activity_main.xml замените TextView на RecyclerView и добавьте плавающую кнопку действия (FAB). Теперь ваш макет должен выглядеть так:

<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>

Внешний вид вашего FAB должен соответствовать доступному действию, поэтому мы захотим заменить значок на символ «+».

Во-первых, нам нужно добавить новый векторный актив:

  1. Выберите «Файл» > «Создать» > «Векторный актив» .
  2. Щелкните значок робота Android в поле « Клип Арт: ».
  3. Найдите «добавить» и выберите актив «+». Нажмите «ОК».
  4. После этого нажмите Далее .
  5. Подтвердите путь к значку как main > drawable и нажмите « Готово », чтобы добавить ресурс.
  6. По-прежнему в layout/activity_main.xml обновите FAB, чтобы включить новый drawable:
<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"/>

Вы собираетесь отображать данные в RecyclerView , что немного лучше, чем просто выбрасывать данные в TextView . В этой лаборатории кода предполагается, что вы знаете, как работают RecyclerView , RecyclerView.LayoutManager , RecyclerView.ViewHolder и RecyclerView.Adapter .

Обратите внимание, что переменная words в адаптере кэширует данные. В следующей задаче вы добавите код, который автоматически обновляет данные.

Создайте файл класса Kotlin для WordListAdapter , который расширяет RecyclerView.Adapter . Вот код:

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
}

Добавьте RecyclerView в метод onCreate() MainActivity .

В onCreate() после setContentView :

   val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
   val adapter = WordListAdapter(this)
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

Запустите приложение, чтобы убедиться, что все работает. Элементов нет, потому что вы еще не подключили данные.

В базе нет данных. Вы будете добавлять данные двумя способами: добавить некоторые данные при открытии базы данных и добавить Activity для добавления слов.

Чтобы удалить весь контент и повторно заполнить базу данных при каждом запуске приложения, вы создаете RoomDatabase.Callback и переопределяете onOpen() . Поскольку вы не можете выполнять операции с базой данных Room в потоке пользовательского интерфейса, onOpen() запускает сопрограмму в диспетчере ввода-вывода.

Для запуска сопрограммы нам нужен CoroutineScope . Обновите метод getDatabase класса WordRoomDatabase , чтобы также получить область сопрограммы в качестве параметра:

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

Обновите инициализатор извлечения базы данных в блоке init WordViewModel , чтобы также передать область:

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

В WordRoomDatabase мы создаем пользовательскую реализацию RoomDatabase.Callback() , которая также получает CoroutineScope в качестве параметра конструктора. Затем мы переопределяем метод onOpen для заполнения базы данных.

Вот код для создания обратного вызова в классе 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!
    }
}

Наконец, добавьте обратный вызов в последовательность сборки базы данных прямо перед вызовом .build() в Room.databaseBuilder() :

.addCallback(WordDatabaseCallback(scope))

Вот как должен выглядеть окончательный код:

@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
        }
     }
   }
}

Добавьте эти строковые ресурсы в 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>

Добавьте этот цветовой ресурс в value/colors.xml :

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

Создайте новый файл ресурсов измерения:

  1. Щелкните модуль приложения в окне проекта .
  2. Выберите «Файл» > «Создать» > «Файл ресурсов Android».
  3. В доступных квалификаторах выберите « Измерение ».
  4. Установите имя файла: размеры

Добавьте эти ресурсы измерений в values/dimens.xml :

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

Создайте новую пустую Activity Android с помощью шаблона «Пустая активность»:

  1. Выберите «Файл» > «Создать» > «Активность» > «Пустая активность».
  2. Введите NewWordActivity в качестве имени действия.
  3. Убедитесь, что новое действие добавлено в манифест Android.
<activity android:name=".NewWordActivity"></activity>

Обновите файл activity_new_word.xml в папке макета, указав следующий код:

<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>

Обновите код активности:

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"
    }
}

Последним шагом является подключение пользовательского интерфейса к базе данных путем сохранения новых слов, которые вводит пользователь, и отображения текущего содержимого базы данных слов в RecyclerView .

Чтобы отобразить текущее содержимое базы данных, добавьте наблюдателя, который наблюдает за LiveData в ViewModel .

Всякий раз, когда данные изменяются, вызывается обратный вызов onChanged() , который вызывает метод адаптера setWords() для обновления кэшированных данных адаптера и обновления отображаемого списка.

В MainActivity создайте переменную-член для ViewModel :

private lateinit var wordViewModel: WordViewModel

Используйте ViewModelProvider , чтобы связать ViewModel с вашей Activity .

При первом запуске Activity ViewModelProviders создаст ViewModel . Когда действие уничтожается, например, из-за изменения конфигурации, ViewModel сохраняется. При повторном создании действия ViewModelProviders возвращают существующую ViewModel . Дополнительные сведения см. в разделе ViewModel .

В onCreate() под блоком кода RecyclerView получите ViewModel из ViewModelProvider :

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

Также в onCreate() добавьте наблюдателя для свойства allWords LiveData из WordViewModel .

Метод onChanged() (метод по умолчанию для нашей Lambda) срабатывает, когда наблюдаемые данные изменяются, а активность находится на переднем плане:

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

Мы хотим открыть NewWordActivity при нажатии на FAB и, как только мы вернемся в MainActivity , либо вставить новое слово в базу данных, либо показать Toast . Для этого начнем с определения кода запроса:

private val newWordActivityRequestCode = 1

В MainActivity добавьте код onActivityResult() для NewWordActivity .

Если действие возвращается с RESULT_OK , вставьте возвращенное слово в базу данных, вызвав метод insert() модели 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()
    }
}

В MainActivity, запустите NewWordActivity , когда пользователь коснется FAB. В MainActivity onCreate найдите FAB и добавьте onClickListener с помощью этого кода:

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

Ваш готовый код должен выглядеть так:

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()
       }
   }
}

Теперь запустите ваше приложение! Когда вы добавляете слово в базу данных в NewWordActivity , пользовательский интерфейс автоматически обновляется.

Теперь, когда у вас есть работающее приложение, давайте подведем итоги того, что вы создали. Вот снова структура приложения:

Компоненты приложения:

  • MainActivity : отображает слова в списке, используя RecyclerView и WordListAdapter . В MainActivity есть Observer , который наблюдает за словами LiveData из базы данных и уведомляется об их изменении.
  • NewWordActivity: добавляет в список новое слово.
  • WordViewModel : предоставляет методы для доступа к слою данных и возвращает LiveData, чтобы MainActivity могла установить отношения наблюдателя.*
  • LiveData<List<Word>> : делает возможным автоматическое обновление компонентов пользовательского интерфейса. В MainActivity есть Observer , который наблюдает за словами LiveData из базы данных и уведомляется об их изменении.
  • Repository: управляет одним или несколькими источниками данных. Repository предоставляет методы ViewModel для взаимодействия с базовым поставщиком данных. В этом приложении этот сервер представляет собой базу данных Room.
  • Room : представляет собой оболочку и реализует базу данных SQLite. Room делает за вас много работы, которую раньше приходилось делать самому.
  • DAO: сопоставляет вызовы методов с запросами к базе данных, поэтому, когда репозиторий вызывает такой метод, как getAlphabetizedWords() , Room может выполнить SELECT * from word_table ORDER BY word ASC .
  • Word : это класс сущности, содержащий одно слово.

* Views и ActivitiesFragments ) взаимодействуют с данными только через ViewModel . Таким образом, не имеет значения, откуда берутся данные.

Поток данных для автоматических обновлений пользовательского интерфейса (реактивный пользовательский интерфейс)

Автоматическое обновление возможно, потому что мы используем LiveData. В MainActivity есть Observer , который наблюдает за словами LiveData из базы данных и уведомляется об их изменении. Когда происходит изменение, выполняется метод наблюдателя onChange() и обновляет mWords в WordListAdapter .

Данные можно наблюдать, потому что это LiveData . И то, что наблюдается, — это LiveData<List<Word>> , возвращаемый WordViewModel a llWords .

WordViewModel скрывает все, что касается серверной части, от слоя пользовательского интерфейса. Он предоставляет методы для доступа к слою данных и возвращает LiveData , чтобы MainActivity могла установить отношение наблюдателя. Views и ActivitiesFragments ) взаимодействуют с данными только через ViewModel . Таким образом, не имеет значения, откуда берутся данные.

В этом случае данные поступают из Repository . ViewModel не нужно знать, с чем взаимодействует этот репозиторий. Ему просто нужно знать, как взаимодействовать с Repository через методы, предоставляемые Repository .

Репозиторий управляет одним или несколькими источниками данных. В приложении WordListSample такой серверной частью является база данных Room. Room представляет собой оболочку и реализует базу данных SQLite. Room делает за вас много работы, которую раньше приходилось делать самому. Например, Room делает все, что вы привыкли делать с классом SQLiteOpenHelper .

DAO сопоставляет вызовы методов с запросами к базе данных, поэтому, когда репозиторий вызывает такой метод, как getAllWords() , Room может выполнить SELECT * from word_table ORDER BY word ASC .

Поскольку результатом, возвращаемым из запроса, является LiveData , при каждом изменении данных в Room выполняется метод onChanged() интерфейса Observer и обновляется пользовательский интерфейс.

[Необязательно] Загрузите код решения

Если вы еще этого не сделали, вы можете взглянуть на код решения для лаборатории кода. Вы можете посмотреть репозиторий github или скачать код здесь:

Скачать исходный код

Распакуйте загруженный zip-файл. Это распакует корневую папку android-room-with-a-view-kotlin , содержащую полное приложение.