KA0710 — Сохранение данных (Data Persistence)

  • Запись изменена:08.12.2022
  • Post category:Публикации
  • Reading time:24 минут чтения

Большой слон в комнате приложения Organize заключается в том, что оно не запоминает ничего, что вы добавляете в него. Как только вы закрываете приложение или останавливаете отладчик, каждый элемент TODO исчезает навсегда.

Причина этой проблемы в том, что он хранит все в памяти и, как ни странно, компьютерная память, а точнее оперативная память, может напоминать людям рыбку Дори!

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

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

  1. Хранилище ключ-значение
  2. База данных
  3. Файловая система

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

Хранилище ключей-значений

Одним из наиболее распространенных вариантов использования при сохранении данных является сохранение битов информации в стиле словаря или карты.

Возможно, вы слышали SharedPreferencesоб Android или UserDefaultsiOS. Как следует из обоих названий, люди используют их в основном для хранения пользовательских настроек и настроек.

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

К счастью, есть библиотека Multiplatform Settings , которая сделает за вас большую часть тяжелой работы.

В этой части главы вы воспользуетесь библиотекой многоплатформенных настроек , чтобы сохранить информацию о первом открытии определенной страницы в приложении « Организовать » .

Настройка мультиплатформенных настроек

Существует два способа установки библиотеки.

Один из способов — передать экземпляры механизма хранения каждой платформы, например, SharedPreferencesдля Android, UserDefaultsдля iOS или аналогичную реализацию для JVM или рабочего стола. Таким образом, вы можете настроить экземпляры по своему желанию. Например, вы можете использовать экземпляр SharedPreferencesпомимо созданного с помощью PreferenceManager.getDefaultSharedPreferences()или передать контейнер для iOS отдельно от UserDefaults.standard.

Другой способ — использовать модуль без аргументов . Используя это, вы получите более быструю и простую настройку за счет настраиваемости. В образовательных целях вы будете использовать длинный путь здесь.

Откройте build.gradle.kts в общем модуле и добавьте эту зависимость для исходного набора commonMain :

implementation("com.russhwolf:multiplatform-settings:${rootProject.extra["settingsVersion"]}")

settingsVersionДоступен в build.gradle.kts проекта .

Затем откройте build.gradle.kts модуля androidApp и добавьте ту же строку в его зависимости.

Обязательно синхронизируйте Gradle.

Затем откройте KoinCommon.kt из commonMain в общем модуле и добавьте expectв файл константу для модулей платформы:

expect val platformModule: Module

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

startKoin { modules( appModule, coreModule, repositoriesModule, viewModelsModule, platformModule, // Don't forget to add this module ) }

Андроид

Находясь в общем модуле, откройте KoinAndroid.kt из androidMain и добавьте этот блок кода:

actual val platformModule = module { single<Settings> { AndroidSettings(get()) } }

Обязательно импортируйте Settingsиз com.russhwolf.settings.Settingspackage.

AndroidSettingsявляется реализацией Android Settings.

Вы используете singleключевое слово Koin, поэтому оно предоставляет эту зависимость как синглтон.

AndroidSettingsнуждается в экземпляре SharedPreferencesв своем конструкторе. Вы просите Koin получить эту зависимость во время выполнения. Не волнуйтесь! Чтобы предотвратить сбой, вы скоро предоставите эту зависимость.

Откройте OrganizeApp.kt в модуле androidApp . Как вы помните, у initKoinфункции был параметр с именем appModule. Теперь пришло время использовать его.

Передайте этот блок кода в appModuleпараметр:

module { //1 single<Context> { this@OrganizeApp } //2 single<SharedPreferences> { get<Context>().getSharedPreferences( "OrganizeApp", Context.MODE_PRIVATE ) } }

Вот что происходит в приведенном выше коде:

  1. Для настройки SharedPreferencesтребуется экземпляр Android Context. Вы заявляете Koin, что он может использовать экземпляр приложения в качестве одноэлементного контекста.
  2. Вы используете эту get()функцию для получения экземпляра Contextи создания закрытого приложения с SharedPreferencesименем OrganizeApp .

iOS

Откройте KoinIOS.kt из iosMain и добавьте актуальную реализацию platformModuleконстанты:

actual val platformModule = module { }

Пустой модуль отключит компилятор.

Вы инициализировали Koin на iOS через initializeметод KoinIOSобъекта. Вы можете добавить параметр к этому методу, чтобы вы могли внедрить экземпляр UserDefaults— или в номенклатуре Objective-C, NSUserDefaults.

Внесите следующие изменения:

fun initialize( userDefaults: NSUserDefaults, ): KoinApplication = initKoin( appModule = module { single<Settings> { AppleSettings(userDefaults) } } )

Примечание . На устройствах Mac с Apple Silicon вы можете обнаружить, что Android Studio не может импортировать пакеты для AppleSettingsи NSUserDefaults. Убедитесь, что эти директивы импорта присутствуют:

import com.russhwolf.settings.AppleSettings import platform.Foundation.NSUserDefaults

Это похоже на аналог Android, но вы используете AppleSettings, который является реализацией Settingsна платформах Apple. AppleSettingsнуждается в экземпляре UserDefaultsв своем конструкторе.

Далее открываем стартовый проект в Xcode и переходим в Koin.swift . Внутри Koinкласса измените строку, в startкоторой вы инициализировали KoinIOS, чтобы учесть изменения, которые вы внесли в initialize:

let app = KoinIOS.shared.initialize( userDefaults: UserDefaults.standard )

Рабочий стол

Откройте KoinDesktop.kt и добавьте актуальную реализацию для platformModule.

@ExperimentalSettingsImplementation actual val platformModule = module { //1 single { Preferences.userRoot() } //2 single<Settings> { JvmPreferencesSettings(get()) } }

Вот что происходит в этом коде:

  1. Поскольку вы обращались к JVM при конструировании Platformкласса в предыдущих главах, вам нужно сделать то же самое и здесь. В JVM есть Preferencesкласс, и вы можете использовать его для хранения пар ключ-значение. Существует два предопределенных контейнера для Preferences: один для пользовательских значений и один для системных значений. Вам нужно использовать userRoot.
  2. Имея экземпляр объекта JVM Preferences, вы можете заявить о своей потребности Settings, например, в Koin и указать ему использовать JvmPreferencesSettingsдля его создания.

Примечание . На момент написания последней версией мультиплатформенных настроек была версия 0.8.1, и создатели этой библиотеки пометили ее использование JvmPreferencesSettingsкак экспериментальную. Вот почему вам нужно добавить @ExperimentalSettingsImplementationаннотацию к свойству.

Для настольного приложения больше нечего делать.

Соберите и запустите все приложения, чтобы убедиться в отсутствии проблем во время компиляции или времени выполнения.

Сохранение значений с использованием настроек мультиплатформенности

В этой части вы сохраните информацию о первом открытии страницы « Об устройстве ».

Откройте AboutViewModel.kt и добавьте параметр конструктора type Settingsв AboutViewModelопределение .

class AboutViewModel( platform: Platform, settings: Settings, ) : BaseViewModel() { // ... }

Добавьте свойство для хранения отформатированной метки времени при первом открытии этой страницы:

val firstOpening: String

Затем добавьте initблок для инициализации этого свойства следующим образом:

`init {
//1
val timestampKey = «FIRST_OPENING_TIMESTAMP»
//2
val savedValue = settings.getLongOrNull(timestampKey)
//3
firstOpening = if (savedValue == null) {
val time = Clock.System.now().epochSeconds — 1
settings.putLong(timestampKey, time)

DateFormatter.formatEpoch(time)

} else {
DateFormatter.formatEpoch(savedValue)
}
}`

  1. Это ключ, с помощью которого вы будете хранить временную метку в формате settings.
  2. Вы получаете Longзначение с помощью ключа.
  3. Если полученное значение равно null, вы получаете текущее время, используя Clockобъект в библиотеке kotlinx-datetime , и сохраняете его в settings. Если значение не null, вы используете savedValue. В любом случае вы форматируете сохраненную дату и сохраняете строку для пользователя в свойстве. Объект DateFormatterуже доступен для вас в материалах этой главы.

Теперь пришло время показать это значение в пользовательском интерфейсе.

Андроид

Сначала откройте OrganizeApp.kt и обновите создание AboutViewModelв viewModelблоке, чтобы учесть добавленный параметр в его конструкторе.

viewModel { AboutViewModel(get(), get()) }

Сделайте то же самое для своей фабрики в KoinCommon.kt :

factory { AboutViewModel(get(), get()) }

Затем откройте AboutView.kt в модуле androidApp . Измените ContentViewкомпонуемую функцию, чтобы она принимала нижний колонтитул, а затем отображала его в нижней части элементов строки:

@Composable private fun ContentView( items: List<AboutViewModel.RowItem>, footer: String?, ) { LazyColumn( modifier = Modifier .fillMaxSize() .semantics { contentDescription = "aboutView" }, ) { items(items) { row -> RowView(title = row.title, subtitle = row.subtitle) } footer?.let { item { Text( text = it, style = MaterialTheme.typography.caption, textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() .padding(8.dp), ) } } } }

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

Наконец, обновите AboutViewсоставную функцию, в которой вы использовали ContentView.

@Composable fun AboutView( viewModel: AboutViewModel = getViewModel(), onUpButtonClick: () -> Unit ) { Column { Toolbar(onUpButtonClick = onUpButtonClick) ContentView( items = viewModel.items, footer = "This page was first opened:\n${viewModel.firstOpening}" ) } }

Стройте и запускайте. Откройте страницу « Об устройстве », нажав кнопку i .

Рис. 10.1 — Страница «О программе» на Android

Рис. 10.1 — Страница «О программе» на Android

iOS

Откройте iosApp.xcodeproj и перейдите в AboutListView.swift . Во-первых, добавьте свойство для footerаналога для Android:

let footer: String

Затем обновите bodyвычисляемое свойство, чтобы отобразить его footerв файле Section.

var body: some View { List { Section( footer: Text(footer) .font(.caption2) .foregroundColor(.secondary) ) { ForEach(items, id: \.self) { item in // ... } } } }

Пока вы находитесь в этом файле, обновите previewsв AboutListView_Previewsструктуре, чтобы заставить замолчать проблемы Xcode.

static var previews: some View { AboutListView( items: [AboutViewModel.RowItem(title: "Title", subtitle: "Subtitle")], footer: "Section Footer" ) }

Затем откройте AboutView.swift и обновите использование AboutListViewучетной записи для footer.

AboutListView( items: viewModel.items, footer: "This page was first opened on \(viewModel.firstOpening)" )

Создайте и запустите приложение. Откройте страницу « Об устройстве », нажав кнопку « Об устройстве» на нижней панели инструментов.

Рис. 10.2 — Страница «О программе» в iOS

Рис. 10.2 — Страница «О программе» в iOS

Рабочий стол

Откройте AboutView.kt в модуле desktopApp . Измените ContentViewкомпонуемую функцию, чтобы она принимала нижний колонтитул, а затем показывала его внизу элементов строки. Это то же самое определение ContentViewв модуле androidApp . Вы можете оглянуться назад на реализацию выше.

Наконец, обновите AboutViewсоставную функцию следующим образом:

@Composable fun AboutView(viewModel: AboutViewModel = koin.get()) { ContentView( items = viewModel.items, footer = "This page was first opened:\n${viewModel.firstOpening}" ) }

Соберите и запустите, затем проверьте окно « Об устройстве» .

Рис. 10.3 — Страница «О программе» на рабочем столе

Рис. 10.3 — Страница «О программе» на рабочем столе

На этом интеграция библиотеки многоплатформенных настроек в Organize завершена.

База данных

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

Хотя базы данных имеют множество видов и моделей, реляционные базы данных, такие как SQLite , пользуются наибольшей популярностью среди мобильных разработчиков.

Например, Core Data , фреймворк для управления графом объектов на iOS, использует SQLite в качестве постоянного хранилища.

Кроме того, в мире Android Google некоторое время рекомендовал базу данных как Room , и это типобезопасная оболочка над SQLite с многочисленными дополнительными функциями.

К сожалению, ни один из этих двух популярных вариантов не доступен для KMP. Тем не менее, есть замечательная библиотека SQLDelight , которая генерирует безопасные для типов API-интерфейсы Kotlin из ваших операторов SQL и работает с KMP с довольно простой настройкой.

SQL

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

SQLDelight может работать с SQLite, MySQL или даже с PostgresSQL. Однако версия, которая работает в KMP, использует SQLite под капотом.

Вы уже определили некоторые действия в приложении « Упорядочить », такие как отображение всех напоминаний, создание нового напоминания и пометка напоминаний как выполненных. Вы собираетесь определить эти действия с помощью SQL, чтобы SQLDelight мог их понять.

В общем модуле внутри каталога commonMain создайте вложенные каталоги следующим образом — либо в файловом менеджере операционной системы, либо в Android Studio.

sqldelight/com/raywenderlich/organize/db

Внутри каталога db , который является сокращением от базы данных, создайте файл с именем Table.sq .

Примечание . sqОбычно это расширение файла для SQLDelight. Android Studio может предложить установить плагин для этого. Это поможет вам с автозаполнением при написании операторов SQL. Если он не рекомендует вам устанавливать плагин, вы можете вручную найти его на рынке плагинов Android Studio.

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

CREATE TABLE ReminderDb ( id TEXT NOT NULL PRIMARY KEY, title TEXT NOT NULL UNIQUE, isCompleted INTEGER NOT NULL DEFAULT 0 );

Этот фрагмент создает таблицу с именем ReminderDbиз этих столбцов :

  • id, первичный ключ этой таблицы
  • title
  • isCompleted

Вы можете рассматривать столбцы как поля или свойства в классах данных.

Как видно из кода, вы указываете TEXTили INTEGERдля типов и NOT NULLуказываете необнуляемость. Ключевое DEFAULTслово позволит вам указать значение по умолчанию для объекта. Ключевое UNIQUEслово запрещает вам добавлять новый элемент с тем же заголовком, что и у существующего элемента.

Следует иметь в виду, что во многих вариантах баз данных на основе SQL, таких как SQLite, Booleanтип не существует, и вы должны представлять этот тип каким-либо другим способом. Вот вы используете INTEGER.

После определения таблицы и спецификаций данных, которые вы в ней храните, пришло время определить действия, которые вы хотите выполнять с данными. Добавьте в этот же файл следующее:

selectAll: SELECT * FROM ReminderDb;

Вы определяете действие с именем selectAll, которое запускает следующую строку, когда вы его вызываете. Он выбирает все элементы в RemindersDbтаблице, которую вы определили ранее. Звездочка означает все в SQL.

Далее добавьте напоминание.

insertReminder: INSERT OR IGNORE INTO ReminderDb(id, title) VALUES (?,?);

Этот оператор позволит вам вставить новый элемент в ReminderDbтаблицу. Ключевое IGNOREслово заставит базу данных игнорировать значения, которые вызывают любые потенциальные ошибки. Например, вы определили titleуникальность, поэтому система будет игнорировать повторяющееся значение, если вы попытаетесь вставить его.

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

И последнее, но не менее важное: вам нужно действие, чтобы пометить напоминание как выполненное.

updateIsCompleted: UPDATE ReminderDb SET isCompleted = ? WHERE id = ?;

Используя WHEREключевое слово, вы можете найти элемент в таблице с определенным значением idи установить его isCompletedполе в определенное значение.

Настройка SQLDelight

Вам необходимо применить плагин SQLDelight Gradle в своем проекте.

Сначала откройте build.gradle.kts проекта и добавьте этот оператор classpath в dependeciesблок buildscript:

classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")

Затем откройте build.gradle.kts общего модуля и добавьте это в pluginsблок:

id("com.squareup.sqldelight")

Чтобы плагин SQLDelight Gradle мог читать файл Table.sq , вам необходимо определить базу данных. В этом же файле добавьте этот блок внизу:

sqldelight { database("OrganizeDb") { packageName = "com.raywenderlich.organize" schemaOutputDirectory = file("src/commonMain/sqldelight/com/raywenderlich/organize/db") } }

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

SQLDelight требует что-то, называемое драйвером , для запуска ваших операторов. Драйвер является связующим звеном между определенной схемой базы данных и специфическими потребностями платформы. Например, требуется экземпляр Contextобъекта на Android.

Вы должны добавить зависимости для драйверов на всех платформах.

val androidMain by getting { dependencies { implementation("com.squareup.sqldelight:android-driver:${rootProject.extra["sqlDelightVersion"]}") // ... } }

val iosMain by creating { dependencies { implementation("com.squareup.sqldelight:native-driver:${rootProject.extra["sqlDelightVersion"]}") } // ... }

val desktopMain by getting { dependencies { implementation("com.squareup.sqldelight:sqlite-driver:${rootProject.extra["sqlDelightVersion"]}") } // ... }

Обязательно синхронизируйте Gradle.

Помощник базы данных

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

Если использование SQLDelight или любой другой сторонней библиотеки не разбросано по всему вашему приложению, вы можете заменить ее в одном месте, и вам не нужно будет трогать что-либо еще.

Откройте DatabaseHelper.kt , который находится в папке данных внутри каталога commonMain , и определите класс следующим образом:

class DatabaseHelper( sqlDriver: SqlDriver, ) { }

Он принимает экземпляр SqlDriver, который вы будете внедрять через Koin на каждой платформе. Как вы читали ранее, вам понадобится драйвер для выполнения операторов SQL.

Затем создайте свойство для хранения ссылки на файл OrganizeDb. Плагин Gradle генерирует этот класс на основе того, что вы определили ранее в build.gradle.kts .

private val dbRef: OrganizeDb = OrganizeDb(sqlDriver)

Если Android Studio не может разрешить OrganizeDb, попробуйте создать проект один раз, чтобы произошло генерирование кода.

Добавьте метод для получения всех напоминаний из базы данных:

fun fetchAllItems(): List<ReminderDb> = dbRef.tableQueries .selectAll() .executeAsList()

Используйте tableQueriesсвойство on OrganizeApp, которое содержит все определенные вами операторы SQL. Как вы назвали одно из своих утверждений selectAll, вы используете то же имя. Затем позвоните, executeAsListчтобы получить результаты в виде списка.

Затем добавьте метод для вставки нового напоминания в базу данных:

fun insertReminder(id: String, title: String) { dbRef.tableQueries.insertReminder(id, title) }

Наконец, добавьте метод для обновления isCompletedстатуса каждого напоминания:

fun updateIsCompleted(id: String, isCompleted: Boolean) { dbRef.tableQueries .updateIsCompleted(isCompleted.toLong(), id) }

Вы также можете добавить функцию расширения, которая возвращает isCompletedстатус каждого напоминания в виде файла Boolean. Это поможет вам позже. Добавьте его вне класса:

fun ReminderDb.isCompleted(): Boolean = this.isCompleted != 0L

Использование базы данных в приложении

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

Откройте RemindersRepository.kt и полностью измените класс следующим образом:

//1 class RemindersRepository( private val databaseHelper: DatabaseHelper ) { //2 val reminders: List<Reminder> get() = databaseHelper.fetchAllItems().map(ReminderDb::map) //3 fun createReminder(title: String) { databaseHelper.insertReminder( id = UUID().toString(), title = title, ) } //4 fun markReminder(id: String, isCompleted: Boolean) { databaseHelper.updateIsCompleted(id, isCompleted) } }

  1. Добавьте свойство конструктора типа DatabaseHelper. Это позволит вам внедрить экземпляр позже.
  2. Затем создайте remindersвычисляемое свойство, отражающее содержимое базы данных. Вы используете mapфункцию для сопоставления экземпляров ReminderDbс Reminder. Ты скоро напишешь ReminderDb::map.
  3. Этот метод вызовет insertReminder.DatabaseHelper
  4. Как и предыдущий метод, этот метод вызывается, DatabaseHelperчтобы пометить напоминания как выполненные или наоборот.

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

В конце этого файла вне класса добавьте функцию расширения для сопоставления from ReminderDbс Reminder.

fun ReminderDb.map() = Reminder( id = this.id, title = this.title, isCompleted = this.isCompleted(), )

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

Откройте KoinCommon.kt и обновите repositoriesсвойство внутри Modulesобъекта:

val repositories = module { factory { RemindersRepository(get()) } }

Добавив простой get()вызов, вы можете отключить ошибки Android Studio. Тем не менее, вы не должны забывать предоставлять экземпляр DatabaseHelperчерез Koin.

Поскольку база данных является одной из основных функций приложения, добавьте модуль к coreсвойству внутри Modulesобъекта:

val core = module { factory { Platform() } factory { DatabaseHelper(get()) } }

Осталось объявить единственную зависимость — SqlDriver, которая DatabaseHelperнужна. Поскольку SqlDriverэто зависит от платформы, вы можете объявить его внутри platformModuleуже определенного вами механизма ожидаемого/фактического.

Андроид

Откройте KoinAndroid.kt и добавьте одноэлементное определение под Settingsобъявлением следующим образом:

single<SqlDriver> { AndroidSqliteDriver(OrganizeDb.Schema, get(), "OrganizeDb") }

Для создания экземпляра AndroidSqliteDriverтребуется как минимум схема базы данных, которую вы получите из сгенерированного OrganizeDbкласса, и экземпляр Context, который вы получите через Koin, воспользовавшись get()функцией. При желании можно указать имя.

Создайте и запустите приложение, добавьте несколько напоминаний и вычеркните некоторые из них из списка. Затем убейте приложение и запустите его снова. Все здесь, потому что так должно было быть всегда.

iOS

Откройте KoinIOS.kt и установите это как platformModuleфактическое свойство:

actual val platformModule: Module = module { single<SqlDriver> { NativeSqliteDriver(OrganizeDb.Schema, "OrganizeDb") } }

На этот раз вы используете нативную реализацию SqlDriver.

Добавьте следующий код для импорта NativeSqliteDriver:

import com.squareup.sqldelight.drivers.native.NativeSqliteDriver

Создайте и запустите приложение. Убедитесь, что напоминания сохраняются в сеансах приложения.

Рабочий стол

Откройте KoinDesktop.kt и добавьте SqlDriverопределение модуля следующим образом:

single<SqlDriver> { val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) OrganizeDb.Schema.create(driver) driver }

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

К счастью, JdbcSqliteDriverесть еще один конструктор, который принимает путь к базе данных в виде jdbc:sqlite:PATH. PATHМожет быть как относительным, так и абсолютным . Вы можете использовать этот инициализатор, однако вам следует обратить внимание на createметод. Вы должны вызвать его только один раз. Если вы попытаетесь вызвать createсуществующую базу данных, приложение выйдет из строя.

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

single<SqlDriver> { val driver = JdbcSqliteDriver("jdbc:sqlite:OrganizeDb.db") OrganizeDb.Schema.create(driver) driver }

Вы создаете файл с именем OrganizeDb.db в каталоге, где находятся коды приложения. Затем вы создаете схему с помощью драйвера. После этого попробуйте запустить приложение. Скорее всего рухнет. Не волнуйтесь и продолжайте.

Затем удалите строку, в которой вы создаете схему, и снова запустите приложение. На этот раз приложение использует файл базы данных и сохраняет все.

В рабочем приложении вы можете написать собственную логику для обработки этого танца.

Имейте это в виду — базы данных в памяти — хороший выбор при написании тестов.

Миграция

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

Сначала запустите generateCommonMainOrganizeDbSchemeзадачу на панели Gradle в Android Studio или в командной строке. Это создаст файл с именем 1.db и поместит его в ту же папку, где существует Table.sq .

Рис. 10.4 — Задача Gradle «Создать схему базы данных»

Рис. 10.4 — Задача Gradle «Создать схему базы данных»

Во- вторых, откройте Table.sq и обновите операторы создания таблицы, а также любые другие действия, которые вы хотите. Этот файл всегда должен отражать текущее состояние базы данных.

CREATE TABLE ReminderDb ( id TEXT NOT NULL PRIMARY KEY, title TEXT NOT NULL UNIQUE, isCompleted INTEGER NOT NULL DEFAULT 0, dueDate INTEGER ); setDueDate: UPDATE ReminderDb SET dueDate = ? WHERE id = ?;

  1. Вы добавляете новый столбец с именем dueDateв таблицу. Оно может быть пустым, поэтому вы не добавляете NOT NULLключевое слово. Поскольку в SQLite нет Dateтипа, вы сохраните метку времени как INTEGER.
  2. Затем вы пишете заявление об обновлении, которое позволит вам установить дату выполнения напоминания.

В- третьих, создайте файл с именем 1.sqm в том же каталоге, чтобы написать заявления о миграции. Вы всегда должны называть этот файл, используя этот шаблон: <version to upgrade from>.sqm.

ALTER TABLE ReminderDb ADD COLUMN dueDate INTEGER;

Вы говорите системе изменить ReminderDbтаблицу и добавить новый столбец для dueDate.

Чтобы убедиться, что миграция может произойти без ошибок, запустите verifySqlDelightMigrationзадачу из панели Gradle в Android Studio или в командной строке.

Это будет учитывать 1.db , 1.sqm и Table.sq для проверки правильности написанных вами операторов SQL.

Рис. 10.5 — Проверка задачи Gradle миграции SqlDelight

Рис. 10.5 — Проверка задачи Gradle миграции SqlDelight

Если этот тест прошел успешно, запустите generateCommonMainOrganizeDbSchemeзадачу еще раз, чтобы сохранить текущую схему как 2.db. Вы также можете безопасно проверить эти файлы в своем репозитории git.

Эта глава не поможет вам добавить пользовательский интерфейс для установки сроков выполнения напоминаний. Установите для себя срок выполнения, чтобы добавить поддержку срока выполнения в Организовать! :]

Добавление сопрограмм

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

Это делает работу; однако вы можете добиться того же результата, а также множества дополнительных функций, используя более надежное решение, такое как Kotlin Flow.

Kotlin Flow позволяет вам наблюдать за потоками данных. Они последовательны и могут выдавать отдельные значения для обработки наблюдателем.

Kotlin Coroutines — это строительные блоки Flows. Вы не можете собирать значения из потока без использования сопрограмм. Другими словами, вы используете потоки, когда хотите наблюдать за несколькими асинхронно вычисляемыми значениями. Асинхронное ключевое слово в Kotlin сразу вызовет suspendконцепцию функций, для работы с которой вам необходимо познакомиться с Coroutines.

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

Многопоточное программирование сложно. Корутины пришли, чтобы упростить это для разработчиков. Однако работа с Coroutines в среде, отличной от JVM, например, в iOS, всегда была проблемой.

Недавно JetBrains рекламировала новую модель памяти для Kotlin Native, которая обещает упростить работу с Coroutines и на нативных платформах. Вы познакомитесь с этим в следующих главах.

Следовательно, для краткости в этой главе не говорится об использовании SQLDelight с Kotlin Flows.

Испытание

Базы данных имеют четыре основные операции: создание, чтение, обновление и удаление, также известные как CRUD . В «Организовать» вы использовали три из этих операций. Реализация единственного оставшегося — Удалить — хороший кандидат на вызов.

Задача: добавление поддержки удаления напоминаний

Добавьте в «Упорядочить» функцию, позволяющую пользователю удалять напоминания по отдельности. Что касается пользовательского интерфейса, вы можете воспользоваться жестами смахивания на Android и iOS. На рабочем столе вы можете использовать контекстное меню, которое отображается, когда пользователь щелкает правой кнопкой мыши любое напоминание.

Ключевые моменты

  • Существует три основных способа сохранения данных на устройстве: хранилище ключей-значений, база данных и работа напрямую с файловой системой.
  • Multiplatform Settings — это библиотека, которая упрощает процесс хранения небольших битов данных в стиле словаря.
  • Вы можете использовать базы данных для хранения структурированных данных и доступа к ним определенным образом.
  • SQLDelight — это реляционная база данных, которая создает типизированный Kotlin API на основе написанных вами операторов SQL. При использовании в KMP под капотом используется SQLite.
  • Миграция баз данных — это деликатный и важный шаг, если вы хотите изменить схему базы данных.
  • SQLDelight имеет библиотеку расширений, которая позволяет вам наблюдать за изменениями в базе данных с помощью Kotlin Flows.

Куда пойти отсюда?

Это была длинная глава. Тем не менее, остается еще много земли для покрытия.

Вот несколько советов для вас, если вы хотите узнать больше:

  • Знакомство с SQL позволит вам писать более производительные запросы.
  • Просмотр документации SQLDelight, доступной здесь , позволит вам изучить больше его возможностей.
  • Тестирование является важным аспектом разработки. Как упоминалось ранее, вы можете использовать преимущества баз данных в памяти в своих тестах. И Multiplatform Settings, и SQLDelight предлагают артефакты тестирования, которые вы можете использовать.