Большой слон в комнате приложения Organize заключается в том, что оно не запоминает ничего, что вы добавляете в него. Как только вы закрываете приложение или останавливаете отладчик, каждый элемент TODO исчезает навсегда.
Причина этой проблемы в том, что он хранит все в памяти и, как ни странно, компьютерная память, а точнее оперативная память, может напоминать людям рыбку Дори!
Приложения могут сохранять свои данные, если они хранят их в энергонезависимой памяти. Примерами этого типа хранилища являются HDD или жесткий диск, SSD или твердотельное хранилище и флэш-память.
Если оставить в стороне детали того, как работают компьютеры, и перейти на более высокий уровень, вы можете в основном сохранять данные, используя три разных механизма:
- Хранилище ключ-значение
- База данных
- Файловая система
В этой главе вы познакомитесь с первыми двумя вариантами, которые являются более структурированными и более простыми, чем непосредственная работа с файловыми системами.
Хранилище ключей-значений
Одним из наиболее распространенных вариантов использования при сохранении данных является сохранение битов информации в стиле словаря или карты.
Возможно, вы слышали SharedPreferences
об Android или UserDefaults
iOS. Как следует из обоих названий, люди используют их в основном для хранения пользовательских настроек и настроек.
Поскольку процесс настройки для использования каждого из этих классов зависит от платформы, вы можете использовать старый и приятный механизм ожидания/ действия для создания единого интерфейса для доступа к хранилищу ключей и значений на каждой платформе. Хотя вы полностью знаете, как сделать это вручную, для написания большого количества стандартного кода.
К счастью, есть библиотека 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.Settings
package.
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 ) } }
Вот что происходит в приведенном выше коде:
- Для настройки
SharedPreferences
требуется экземпляр AndroidContext
. Вы заявляете Koin, что он может использовать экземпляр приложения в качестве одноэлементного контекста. - Вы используете эту
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()) } }
Вот что происходит в этом коде:
- Поскольку вы обращались к JVM при конструировании
Platform
класса в предыдущих главах, вам нужно сделать то же самое и здесь. В JVM естьPreferences
класс, и вы можете использовать его для хранения пар ключ-значение. Существует два предопределенных контейнера дляPreferences
: один для пользовательских значений и один для системных значений. Вам нужно использоватьuserRoot
. - Имея экземпляр объекта 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)
}
}`
- Это ключ, с помощью которого вы будете хранить временную метку в формате
settings
. - Вы получаете
Long
значение с помощью ключа. - Если полученное значение равно
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
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
Рабочий стол
Откройте 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 — Страница «О программе» на рабочем столе
На этом интеграция библиотеки многоплатформенных настроек в 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) } }
- Добавьте свойство конструктора типа
DatabaseHelper
. Это позволит вам внедрить экземпляр позже. - Затем создайте
reminders
вычисляемое свойство, отражающее содержимое базы данных. Вы используетеmap
функцию для сопоставления экземпляровReminderDb
сReminder
. Ты скоро напишешьReminderDb::map
. - Этот метод вызовет
insertReminder
.DatabaseHelper
- Как и предыдущий метод, этот метод вызывается,
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 «Создать схему базы данных»
Во- вторых, откройте 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 = ?;
- Вы добавляете новый столбец с именем
dueDate
в таблицу. Оно может быть пустым, поэтому вы не добавляетеNOT NULL
ключевое слово. Поскольку в SQLite нетDate
типа, вы сохраните метку времени какINTEGER
. - Затем вы пишете заявление об обновлении, которое позволит вам установить дату выполнения напоминания.
В- третьих, создайте файл с именем 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
Если этот тест прошел успешно, запустите 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 предлагают артефакты тестирования, которые вы можете использовать.