Kotlin Android порядок выполнения кода [закрыт]
Закрыт. Этот вопрос не по теме. Ответы на него в данный момент не принимаются.
Вопросы с просьбами помочь с отладкой («почему этот код не работает?») должны включать желаемое поведение, конкретную проблему или ошибку и минимальный код для её воспроизведения прямо в вопросе. Вопросы без явного описания проблемы бесполезны для остальных посетителей. См. Как создать минимальный, самодостаточный и воспроизводимый пример.
Закрыт 1 год назад .
У меня есть функция, которая возвращает значение info, но происходит так, что возвращается info=»», сначала идет блок вне when, а потом всередине, как это исправить?
SQLite на Android
SQLite доступен на любом Android-устройстве, его не нужно устанавливать отдельно.
SQLite поддерживает типы TEXT (аналог String в Java), INTEGER (аналог long в Java) и REAL (аналог double в Java). Остальные типы следует конвертировать, прежде чем сохранять в базе данных. SQLite сама по себе не проверяет типы данных, поэтому вы можете записать целое число в колонку, предназначенную для строк, и наоборот.
Вы не найдёте здесь тип, работающий с датами. Можно использовать строковые значения, например, как 2013-03-28 (28 марта 2013 года). Для даты со временем рекомендуется использовать формат 2013-03-27T07:58. В таких случаях можно использовать некоторые функции SQLite для добавления дней, установки начала месяца и т.д. Учтите, что SQLite не поддерживает часовые пояса.
Также не поддерживается тип boolean. Используйте числа 0 для false и 1 для true.
Не используйте тип BLOB для хранения данных (картинки) в Android. Лучше хранить в базе путь к изображениям, а сами изображения хранить в файловой системе.
Обратите внимание, что здесь не используется популярный в MySQL тип varchar(n), ограничивающий длину строки.
Всё, что вам нужно для начала работы с базой данных — задать необходимые настройки для создания или обновления базы данных.
Так как сама база данных SQLite представляет собой файл, то по сути при работе с базой данных, вы взаимодействуете с файлом. Поэтому операции чтения и записи могут быть довольно медленными. Настоятельно рекомендуется использовать асинхронные операции, например, при помощи класса AsyncTask.
Когда ваше приложение создаёт базу данных, она сохраняется в каталоге DATA/data/имя_пакета/databases/имя_базы.db.
Метод Environment.getDataDirectory() возвращает путь к каталогу DATA.
Основными пакетами для работы с базой данных являются android.database и android.database.sqlite.
База данных SQLite доступна только приложению, которое создаёт её. Если вы хотите дать доступ к данным другим приложениям, вы можете использовать контент-провайдеры (ContentProvider).
Запускаем SQLite на эмуляторе
С помощью утилиты ADB можно запустить SQLite на эмуляторе и работать с базами данных напрямую.
Напомню, что можно активировать команду sqlitе3 для одной из перечисленных баз данных, введя следующую команду:
#sqlite3 /data/data/com.android.providers.contacts/databases/contacts.db
Для завершения работы с sqlite3 напишите:
sqlite> .exit
Обратите внимание: приглашение для adb — это #, а приглашение для sqlitе3 — это sqlite>.
Описание доступных команд sqlite3 есть на сайте: http://www.sqlite.org/sqlite.html. Перечислим некоторые важные команды.
Чтобы просмотреть список таблиц:
sqlite> .tables
Быстрый доступ к главной таблице:
SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite %' UNION ALL SELECT name FROM sqlite_temp_master WHERE type IN ('tаblе', 'view') ORDER ВY 1
Таблица sqlite_master — это главная таблица (master table), в которой отслеживаются таблицы и виды, содержащиеся в базе данных. Следующая команда распечатывает инструкцию create для таблицы people, находящейся в базе данных contacts.db:
.schema people
Это один из способов, позволяющих узнать названия всех столбцов, которые содержатся в таблице базы данных. Можно скопировать базу данных на локальный компьютер и изучать её в более комфортных условиях. Чтобы переместить файл contacts.db, можно дать следующую команду:
adb pull /data/data/com.android.providers.contacts/databases/contacts.db d:/somelocaldir/contacts.db
Классы для работы с SQLite
Работа с SQLite напрямую теперь считается устаревшим подходом. На Google I/O 2017 был представлен новых механизм доступа к базе данных через специальную обёртку Room
Работа с базой данных сводится к следующим задачам:
- Создание и открытие базы данных
- Создание таблицы
- Создание интерфейса для вставки данных (insert)
- Создание интерфейса для выполнения запросов (выборка данных)
- Закрытие базы данных
Класс ContentValues
Класс ContentValues используется для добавления новых строк в таблицу. Каждый объект этого класса представляет собой одну строку таблицы и выглядит как ассоциативный массив с именами столбцов и значениями, которые им соответствуют.
Курсоры
В Android запросы к базе данных возвращают объекты класса Cursor. Вместо того чтобы извлекать данные и возвращать копию значений, курсоры ссылаются на результирующий набор исходных данных. Курсоры позволяют управлять текущей позицией (строкой) в результирующем наборе данных, возвращаемом при запросе.
Класс SQLiteOpenHelper: Создание базы данных
Библиотека Android содержит абстрактный класс SQLiteOpenHelper, с помощью которого можно создавать, открывать и обновлять базы данных. Это основной класс, с которым вам придётся работать в своих проектах. При реализации этого вспомогательного класса от вас скрывается логика, на основе которой принимается решение о создании или обновлении базы данных перед ее открытием. Класс SQLiteOpenHelper содержит два обязательных абстрактных метода:
- onCreate() — вызывается при первом создании базы данных
- onUpgrade() — вызывается при модификации базы данных
Также используются другие методы класса:
- onDowngrade(SQLiteDatabase, int, int)
- onOpen(SQLiteDatabase)
- getReadableDatabase()
- getWritableDatabase()
В приложении необходимо создать собственный класс, наследуемый от SQLiteOpenHelper. В этом классе необходимо реализовать указанные обязательные методы, описав в них логику создания и модификации вашей базы.
В этом же классе принято объявлять открытые строковые константы для названия таблиц и полей создаваемой базы данных, которые клиенты могут использовать для определения столбцов при выполнении запросов к базе данных. Например, так можно объявить константы для таблицы CONTACT:
public static final String TABLE_NAME = "CONTACT"; public static final String NAME = "FIRST_NAME"; public static final String PHONE = "PHONE";
Лучше, если вы будете давать сразу понятные имена, указывающие на работу с таблицей. Если имя переменной TABLE_NAME ещё можно оставить, то для других лучше использовать более говорящие имена, например, KEY_NAME и KEY_PHONE или COLUMN_NAME, COLUMN_PHONE.
Ваш класс, расширяющий SQLiteOpenHelper, также неявно наследует интерфейс BaseColumns, в котором определена строковая константа _ID, представляющая имя поля для идентификаторов записей. В создаваемых таблицах базы данных поле _ID должно иметь тип INTEGER PRIMARY KEY AUTOINCREMENT. Описатель AUTOINCREMENT является необязательным. Часто в других примерах идентификатор создаётся вручную, смотрите как вам удобнее. Только всегда называйте его именно _id. Такое название используется в Android для работы с курсорами и поэтому придерживайтесь данного правила.
В методе onCreate() необходимо реализовать логику создания таблиц и при необходимости заполнить их начальными данными при помощи SQL-команды, например:
@Override public void onCreate(SQLiteDatabase db)
Здесь показана условная команда, в реальном коде вам нужно учитывать пробелы между константами, чтобы получилась правильная строка SQL-команды.
Также надо указать номер версии базы данных, начиная со значения 1. По этому номеру класс-обёртка будет понимать, что структуру базы данных следует обновить.
private static final int DATABASE_VERSION = 1;
В методе onUpgrade() можно, например, реализовать запрос в базу данных на уничтожение таблицы (DROP TABLE), после чего вновь вызвать метод onCreate() для создания версии таблицы с обновленной структурой, например, так:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
В параметрах метода используются номера версий базы данных — старая и новая. О номере версии говорилось выше.
Обновляется структура базы данных следующим образом. Сначала меняем порядковый номер.
private static final int DATABASE_VERSION = 2;
При первой установке приложения базы данных ещё не существует. Тут проверять пока нечего. При установке новой версии приложения система проверит номер версии базы данных. Если он больше, чем было, то вызовется метод onUpgrade(). Если наоборот, то вызовется метод onDowngrade() (обычно так происходит редко).
Как использовать? Просто ставим условие на проверку.
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) < if (oldVersion == 1) < // Код для работы с базой данных с версией 1 >if (oldVersion < 3) < // Код для работы с базой данных с версией 1 или 2 >>
Допустим, в таблицу нужно добавить новую колонку в дополнение к существующим колонкам NAME, DESCRIPTION и IMAGE_RESOURCE_ID. Для изменения таблицы используется выражение SQL с ключевым словом ALTER
ALTER TABLE DATABASE_TABLE ADD COLUMN AGE INTEGER
Для замены имени таблицы используется выражение (меняем имя таблицы DOG на CAT):
ALTER TABLE DOG RENAME TO CAT
Для удаления таблицы:
DROP TABLE DOG
Соответственно, нужно вызвать метод SQLiteDatabase.execSQL(), передав ему нужную команду SQL.
Для удобства создадим вспомогательный метод updateDatabase().
private void updateDatabase(SQLiteDatabase db, int oldVersion, int newVersion) < if (oldVersion < 1) < db.execSQL("CREATE TABLE DATABASE_TABLE (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "NAME TEXT, " + "DESCRIPTION TEXT, " + "IMAGE_RESOURCE_ID INTEGER);"); insertCat(db, "Barsik", "Very clever cat", R.drawable.barsik); insertCat(db, "Murzik", "Beautiful cat", R.drawable.murzik); insertCat(db, "Vaska", "Always hungry", R.drawable.vaska); >if (oldVersion < 2) < //Код для добавления новой колонки myDatabase.execSQL("ALTER TABLE CONTACT ADD COLUMN AGE INTEGER;"); >>
Созданный метод теперь можно использовать в коде следующим образом.
// Поменяли номер версии базы данных private static final int DATABASE_VERSION = 2; @Override public void onCreate(SQLiteDatabase db) < updateDatabase(db, 0, DB_VERSION); >@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
Чтобы использовать реализацию вспомогательного класса, создайте новый экземпляр, передайте его конструктору контекст, имя базы данных, текущую версию и объект класса CursorFactory (если вы его используете). Вызовите метод getReadableDatabase() или getWritableDatabase(), чтобы открыть и вернуть экземпляр базы данных, с которой мы имеем дело (он будет доступен как для чтения, так и для записи). Вызов метода getWritableDatabase() может завершиться неудачно из-за проблем с полномочиями или нехваткой места на диске, поэтому лучше предусмотреть откат к методу getReadableDatabase().
DbHelper dbHelper = new DbHelper(context, DATABASE_NAME, null, DATABASE_VERSION); SQLiteDatabase db; try < db = dbHelper.getWritableDatabase(); >catch (SQLiteException ex)
Если база данных не существует, вспомогательный класс вызывает свой обработчик onCreate(); если версия базы данных изменилась, вызовется обработчик onUpgrade(). В любом случае вызов методов getWritableDatabase/getReadableDatabase, в зависимости от ситуации, вернет существующую, только что созданную или обновленную базу данных.
Оба метода имеют разные названия, но возвращают один и тот же объект. Только метод для чтения getReadableDatabase() сначала проверит доступность на чтение/запись. В случае ошибки метод проверит доступность на чтение и только потом уже вызовет исключение. Второй метод сразу проверяет доступ к чтению/записи и вызывает исключение при отказе в доступе.
Если вы планируете использовать несколько таблиц, то рекомендуется создавать для каждой таблицы отдельный класс.
SQLiteDatabase
Для управления базой данных SQLite существует класс SQLiteDatabase. В классе SQLiteDatabase определены методы query(), insert(), delete() и update() для чтения, добавления, удаления, изменения данных. Кроме того, метод execSQL() позволяет выполнять любой допустимый код на языке SQL применимо к таблицам базы данных, если вы хотите провести эти (или любые другие) операции вручную.
Каждый раз, когда вы редактируете очередное значение из базы данных, нужно вызывать метод refreshQuery() для всех курсоров, которые имеют отношение к редактируемой таблице.
Для составления запроса используются два метода: rawQuery() и query(), а также класс SQLiteQueryBuilder.
Метод query()
Для чтения данных используют вызов метода query():
Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder)
В метод query() передают семь параметров. Если какой-то параметр для запроса вас не интересует, то оставляете null:
- table — имя таблицы, к которой передается запрос;
- String[] columnNames — список имен возвращаемых полей (массив). При передаче null возвращаются все столбцы;
- String whereClause — параметр, формирующий выражение WHERE (исключая сам оператор WHERE). Значение null возвращает все строки. Например: _id = 19 and summary = ?
- String[] selectionArgs — значения аргументов фильтра. Вы можете включить ? в «whereClause»». Подставляется в запрос из заданного массива;
- String[] groupBy — фильтр для группировки, формирующий выражение GROUP BY (исключая сам оператор GROUP BY). Если GROUP BY не нужен, передается null;
- String[] having — фильтр для группировки, формирующий выражение HAVING (исключая сам оператор HAVING). Если не нужен, передается null;
- String[] orderBy — параметр, формирующий выражение ORDER BY (исключая сам оператор ORDER BY). При сортировке по умолчанию передается null.
Объект Cursor, возвращаемый методом query(), обеспечивает доступ к набору записей результирующей выборки. Для обработки возвращаемых данных объект Cursor имеет набор методов для чтения каждого типа данных — getString(), getInt() и getFloat().
Чтобы получить все записи из нужных столбцов без условий, достаточно указать имя таблицы в первом параметре и строчный массив во втором. В остальных параметрах оставляем null:
Cursor cursor = db.query("CAT", new String[] , null, null, null, null, null);
Для запроса с условием используются третий и четвёртый параметр. Например, нам нужны записи, у которого имя кота будет Барсик:
Cursor cursor = db.query("CAT", new String[] , "NAME = ?", new String[] , null, null, null);
Параметр «NAME = ?» означает, что столбец NAME должен содержать значение, указанное следующем параметре.
Множественные условия. Об этом говорилось выше.
Cursor cursor = db.query("CAT", new String[] , "NAME = ? OR DESCRIPTION = ?", new String[] , null, null, null);
Пример при использовании числовых значений, например, идентификатор первичного ключа (об этом тоже говорилось)
Cursor cursor = db.query("CAT", new String[] , "_id = ?", new String[] , null, null, null);
Для сортировки используется последний параметр. Нужно указать нужный столбец, по которому будет проводиться сортировка. По алфавиту с буквы A — ASC, наоборот — DESC
Cursor cursor = db.query("CAT", new String[] , null, null, null, null, "NAME ASC");
Возможна сортировка по нескольким столбцам.
Cursor cursor = db.query("CAT", new String[] , null, null, null, null, "DESCRIPTION DESC, NAME");
Вы можете использовать функции SQL для составления запросов: AVG(), COUNT(), SUM(), MAX(), MIN() и др.
// Возвращает число котов в столбце count // Аналог SELECT COUNT(_id) AS COUNTER FROM CAT Cursor cursor = db.query("CAT", new String[] , null, null, null, null, null);
Ещё один абстрактный пример подсчёта средней суммы:
Cursor cursor = db.query("MILK", new String[] , null, null, null, null, null);
rawQuery()
Также существует метод rawQuery(), принимающий сырой SQL-запрос.
Метод rawQuery() — это сырой запрос, как есть, т.е. пишется строка запроса, как это обычно делается в SQL. Пример выглядит следующим образом:
Cursor cursor = getReadableDatabase(). rawQuery("select * from todo where _id = ?", new String[] < id >);
Класс SQLiteQueryBuilder — удобный способ для построения запросов. Выбор за вами!
Метод insert()
Для вставки новой записи в базу данных SQLite используется метод insert():
long insert (String table, String nullColumnHack, ContentValues values);
В метод insert() необходимо передать три параметра:
- table — имя таблицы, в которую будет вставлена запись;
- nullColumnHack — в базе данных SQLite не разрешается вставлять полностью пустую строку, и если строка, полученная от клиента контент-провайдера, будет пустой, то только этому столбцу явно будет назначено значение null;
- values — карта отображений (класс Map и его наследники), передаваемая клиентом контент-провайдера, которая содержит пары ключ-значение. Ключи в карте должны быть названиями столбцов таблицы, значения — вставляемыми данными.
Метод insert() возвращает идентификатор _id вставленной строки или -1 в случае ошибки.
Для создания новой строки понадобится объект ContentValues, точнее, его метод put(), чтобы обеспечить данными каждый столбец. Объект ContentValues представляет собой пару name/value данных. Вставьте новую строку, передавая в метод insert(), вызванный в контексте нужной нам базы данных, имя таблицы и объект ContentValues
// Создайте новую строку со значениями для вставки. ContentValues newValues = new ContentValues(); // Задайте значения для каждой строки. newValues.put(COLUMN_NAME, newValue); [ . Повторите для каждого столбца . ] // Вставьте строку в вашу базу данных. myDatabase.insert(DATABASE_TABLE, null, newValues);
Метод update()
Для обновления и удаления записей в базе данных используют соответственно методы update() и delete():
int update (String table, ContentValues values, String whereClause, String[] whereArgs) int delete (String table, String whereClause, String[] whereArgs)
В этих методах два последних параметра формируют SQL-выражение WHERE аналогично рассмотренному методу query() для чтения данных. Эти методы возвращают число модифицированных или удаленных строк.
Обновление строк также происходит с помощью класса ContentValues. Создайте новый объект ContentValues, используя метод put() для вставки значений в каждый столбец, который вы хотите обновить. Вызовите метод update() в контексте базы данных, передайте ему имя таблицы, обновленный объект ContentValues и оператор WHERE, указывающий на строку (строки), которую нужно обновить.
// Определите содержимое обновленной строки. ContentValues updatedValues = new ContentValues(); // Назначьте значения для каждой строки. newValues.put(COLUMN_NAME, newValue); [ . Повторите для каждого столбца . ] String where = KEY_ID + " bg-light"> ContentValues barsikValues = new ContentValues(); // Помещаем значение "Clever" в колонку DESCRIPTION barsikValuesValues.put("DESCRIPTION", "Clever"); // Обновляем колонку DESCRIPTION с новым значением "Clever" // в таблице WHERE NAME = "Barsik" myDatabase.update(DATABASE_TABLE, barsikValues, "NAME = ?", new String[] );
В первом параметре метода мы указываем имя таблицы, которую хотим обновить.
Во втором параметре мы указываем значение, которое хотим обновить. Например, в колонке "DESCRIPTION" нужно обновить значение на "Clever".
В третьем параметре указывается условие для обновления. Выражение "NAME = ?" означает, что колонка NAME должна быть равна указанному значению. Знак вопроса является подстановочным символом и из четвёртого параметра берётся нужное значение.
Обратите внимание, что последний параметр является массивом. Вы можете использовать множественные условия для запроса. Например, нам нужно обновить запись в таблице, используя сразу две колонки для составления условия.
myDatabase.update(DATABASE_TABLE, murzikValues, "NAME = ? OR DESCRIPTION = ?", new String[] );
Данный код соответствует выражению Where NAME = "Murzik" or DESCRIPTION = "Nice".
Для условий всегда используются строки, поэтому может понадобиться конвертация в некоторых случаях. Например, нужно использовать идентификатор таблицы со значением 1.
myDatabase.update(DATABASE_TABLE, newValues, "_id = ?", new String[] );
Если в последних двух параметрах использовать null, то будут обновлены все записи в таблице.
myDatabase.update(DATABASE_TABLE, barsikValues, null, null);
Метод delete()
int delete (String table, String whereClause, String[] whereArgs)
Чтобы удалить строку, просто вызовите метод delete() в контексте базы данных, указав имя таблицы и оператор WHERE. В результате вы получите строки, которые хотите удалить:
myDatabase.delete(DATABASE_TABLE, KEY_ID + " Murzik" myDatabase.delete(DATABASE_TABLE, "NAME = ?", new String[] );
Кроме вышеперечисленных методов, в этом классе также определены различные методы для выполнения других общих задач управления базой данных.
Метод openOrCreateDatabase: Открытие и создание баз данных без использования SQLiteOpenHelper
Вы можете открывать и создавать базы данных без помощи класса SQLiteOpenHelper, используя метод openOrCreateDatabase(), принадлежащий объекту Context вашего приложения. Получите доступ к базе данных в два шага. Сначала вызовите метод openOrCreateDatabase(), чтобы создать новую базу данных. Затем из полученного экземпляра базы данных вызовите execSQL(), чтобы выполнять команды на языке SQL, с помощью которых будут созданы таблицы и установлены отношения между ними.
private static final String DATABASE_NAME = "myDatabase.db"; private static final String DATABASE_TABLE = "mainTable"; private static final String DATABASE_CREATE = "create table " + DATABASE_TABLE + " ( _id integer primary key autoincrement," + "column_one text not null);"; SQLiteDatabase myDatabase; private void createDatabase()
Контент-провайдеры
Контент-провайдеры поддерживают стандартный синтаксис запросов для чтения, изменения, вставки и удаления данных. Если необходимо предоставить доступ к своим данных для других приложений, существует два варианта:
- создать собственный контент-провайдер, как подкласс класса ContentProvider;
- добавить данные к существующему провайдеру — если уже есть провайдер, который управляет теми же данными, и вы имеете разрешение для работы с этими данными.
Вставляем картинки в базу
Вставлять картинки в базу данных не самая лучшая идея. Оставлю только для ознакомления. Так как SQLite не работает с изображениями напрямую, то нужно сконвертировать картинку в байтовый массив. А при извлечении нужно проделать обратную задачу - из байтового массива воссоздать изображение. Создадим вспомогательный класс.
package ru.alexanderklimov.test; import java.io.ByteArrayOutputStream; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Bitmap.CompressFormat; public class Utilities < // convert from bitmap to byte array public static byte[] getBytes(Bitmap bitmap) < ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(CompressFormat.PNG, 0, stream); return stream.toByteArray(); >// convert from byte array to bitmap public static Bitmap getImage(byte[] image) < return BitmapFactory.decodeByteArray(image, 0, image.length); >>
Создадим таблицу следующим образом. Переменная DATABASE_TABLE - строковая константа.
CREATE TABLE DATABASE_TABLE ( _id INTEGER PRIMARY KEY AUTOINCREMENT, image_data BLOB );
Метод вставки в вашем классе-адаптере:
public long insert(byte[] image) < return mDb.insert(DATABASE_TABLE, null, createContentValues(image)); >private ContentValues createContentValues(byte[] image)
Обзор DataStore Library. Прощаемся с SharedPreference?

Привет, меня зовут Сергей, я работаю в команде Мобильного Банка Тинькофф. Недавно Google представила очередной инструмент для хранения данных. На этот раз это библиотека DataStore. В официальном блоге Google пишут, что она должна заменить SharedPreference.
В отличие от SharedPreference, DataStore работает асинхронно. Вся работа с библиотекой выполняется с помощью Kotlin Coroutines и Flow. DataStore позволяет нам хранить данные двумя способами:
- По принципу «ключ — значение», аналогично SharedPreference.
- Хранить типизированные объекты, основанные на protocol buffers.
Все взаимодействие с DataStore происходит через интерфейс DataStore , который содержит в себе всего два элемента:
interface DataStore < val data: Flowsuspend fun updateData(transform: suspend (t: T) -> T): T >
Интерфейс очень прост. Все, что мы можем сделать с ним, это получить объект Flow для чтения данных и вызвать метод updateData() для их записи.
Типы DataStore
- Preferences DataStore — хранит данные по принципу «ключ — значение» и не предоставляет нам никакой типобезопасности.
- Proto DataStore — хранит данные в объектах. Это дает нам типобезопасноть, но описывать схему нужно с помощью protocol buffers.
Поговорим о каждом из них.
Preferences DataStore

Для подключения библиотеки необходимо добавить зависимость в build.gradle нашего проекта:
// Preferences DataStore implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
Как получить экземпляр Preferences DataStore
Для этого нам предоставляется extension-функция, которую можно вызвать из объекта Context :
context.createDataStore( name = "user_data_store", corruptionHandler = null migrations = emptyList(), scope = CoroutineScope(Dispatchers.IO + Job()) )
Здесь есть четыре параметра. Давайте рассмотрим каждый из них.
- name — обязательный параметр. Это название нашего DataStore. Под капотом будет создан файл, путь которого формируется на основании параметра name.
File(context.filesDir, "datastore/" + name + ".preferences_pb")
- corruptionHandler — этот параметр необязательный. CorruptionHandler вызывается, если DataStore бросает CorruptionException при попытке чтения данных. Если CorruptionHandler успешно подменит данные, то исключение будет поглощено. Если в процессе подмены данных мы получим еще одно исключение, то оно будет добавлено к оригинальному исключению, после чего нам будет выброшено оригинальное исключение.
- migrations — необязательный параметр, который позволяет легко мигрировать из SharedPreference. Сюда принимается список объектов DataMigration . На самом деле Google уже создала реализацию SharedPreferencesMigration. Все, что нам нужно, это описать логику переноса данных для каждого Shared Preference и передать их списком в параметр migrations:
fun getSharedPreferenceMigrationPref(): SharedPreferencesMigration = SharedPreferencesMigration( context = context, sharedPreferencesName = "pref_name", deleteEmptyPreferences = true, shouldRunMigration = < true >, migrate = < prefs, userPref ->userPref[FIELD_NAME] = prefs.getString(KEY_NAME) userPref[FIELD_LAST_NAME] = prefs.getString(KEY_LAST_NAME) userPref[FIELD_AGE] = prefs.getInt(KEY_AGE, 0) userPref[FIELD_ACTIVE] = prefs.getBoolean(KEY_IS_ACTIVE, false) userPref > )
В отличие от обычных Shared Preference, в качестве ключа здесь не строка, но об этом мы поговорим чуть позже.
- scope — тоже необязательный параметр. Здесь можно указать, в каком Coroutine Scope мы хотим выполнять операции с DataStore. По умолчанию там Dispatchers.IO.
Создание ключей
Чтобы сделать запись в DataStore, нам необходимо определить ключи, под которыми будут храниться наши данные. Как упоминалось выше, это не строки. Поля имеют тип Preferences.Key . Создать подобное поле можно с помощью extension-функции:
object UserScheme < val FIELD_NAME = preferencesKey("name") val FIELD_LAST_NAME = preferencesKey("last_name") val FIELD_AGE = preferencesKey("age") val FIELD_ACTIVE = preferencesKey("active") >
Каждый ключ указывает на тип хранимых в нем данных и строковый ключ, по которому эти данные будут читаться. Поскольку при создании ключа мы указываем тип хранимых данных — мы получаем проверку на корректность передаваемого типа данных в compile time.
Стоит помнить, что создавать ключи можно только для примитивных типов данных: Int , Long , Boolean , Float , String . В противном случае мы получим исключение.
Также мы можем хранить Set :
val FIELD_STRINGS_SET = preferencesSetKey>("strings_set")
Скорее всего, количество типов будет расширяться, так как сейчас методы prefrencesKey() и prefrencesSetKey() на вход принимают дженерик и ограничение по типам сделано руками.
Запись данных
Для записи данных DataStore предоставляет нам два метода для изменения данных:
DataStore.updateData
coroutineScope.launch < prefDataStore.updateData < prefs ->prefs.toMutablePreferences().apply < set(UserScheme.FIELD_NAME, "John") set(UserScheme.FIELD_LAST_NAME, "Show") set(UserScheme.FIELD_AGE, 100) set(UserScheme.FIELD_IS_ACTIVE, false) >> >
DataStore.edit
coroutineScope.launch < prefDataStore.edit < prefs ->prefs[UserScheme.FIELD_NAME] = "John" prefs[UserScheme.FIELD_LAST_NAME] = "Show" prefs[UserScheme.FIELD_AGE] = 100 prefs[UserScheme.FIELD_IS_ACTIVE] = false > >
В обоих случаях мы получаем объект Preferences с разницей лишь в том, что во втором случае приведение к мутабельности спрятано под капотом «функции обертки» edit() .
Preferences очень похожа на Generic Map, в которую мы в качестве ключа указываем определенные нами ранее preferenceKey. Для работы с Preferences есть всего четыре метода get(), contains(), asMap() и set() . Метод set() доступен только в MutablePreferences. Запись в Preferences происходит асинхронно, и корутина завершается после того, как данные сохраняются на диске.
Чтение данных
DataStore предоставляет сохраненные данные в объекте Preferences. Все действия производятся на определенном нами при создании Dispatcher:
coroutineScope.launch < prefDataStore.data .collect < pref: Preferences ->val name: String? = pref[UserScheme.FIELD_NAME] val lastName: String? = pref[UserScheme.FIELD_LAST_NAME] val age: Int? = pref[UserScheme.FIELD_AGE] val isActive: Boolean? = pref[UserScheme.FIELD_IS_ACTIVE] > >
DataStore возвращает объект Flow, который будет возвращать нам либо значение, либо исключение, в случае ошибки чтения с диска.
Proto DataStore

Для подключения добавляем зависимость:
// Proto DataStore implementation "androidx.datastore:datastore-core:1.0.0-alpha01"
Перед работой с Proto DataStore нужно выполнить несколько действий:
- В build.gradle добавить плагин:
plugins
- Подключить зависимость в build.gradle:
implementation "com.google.protobuf:protobuf-javalite:3.10.0"
- Создать модель данных, используя protocol buffers.
Для этого нужно создать файл в app/src/main/proto/ с расширением .proto :
syntax = "proto3"; option java_package = "com.example.jetpackdatasource"; option java_multiple_files = true; message UserProto
Здесь есть подробное руководство по работе с proto buffer файлами.
Это будет наша схема хранения данных. Система сгенерирует модель, которую мы можем сохранять в наш DataStore.
Когда вы все это сделаете, Android Studio предложит установить плагин Protocol Buffer Editor. Он сделает вашу работу с файлами .proto удобной. Плагин будет подсвечивать синтаксические элементы, проводить семантический анализ и др.
Как получить экземпляр Proto DataStore
Для этого у нас тоже есть extension-функция:
context.createDataStore( fileName ="user.pb", serializer = UserSerializer, corruptionHandler = null, migrations = emptyList(), scope = CoroutineScope(Dispatchers.IO + Job()) )
Здесь все почти то же самое, как и с Preference DataStore. Но есть два отличия:
- Первое — это путь, по которому будет сохраняться файл префов: File(context.filesDir, "datastore/$fileName").
- Второе — наличие поля serializer. Давайте рассмотрим его подробнее. Чтобы Proto DataStore понимал, как ему сохранять данные в файл, мы должны к каждому модели прописать свой Serializer. Для этого нужно реализовать интерфейс Serializer , в котором мы и опишем логику записи/чтения нашего файла:
object UserSerializer : Serializer < override fun readFrom(input: InputStream): User < try < return User.parseFrom(input) >catch (exception: InvalidProtocolBufferException) < throw CorruptionException("Cannot read proto.", exception) >> override fun writeTo(t: User, output: OutputStream) = t.writeTo(output) >
В остальном тут все так же, как в Preference DataStore.
Запись данных
Для записи данных DataStore предоставляет нам функцию DataStore.updateData(). Она возвращает текущее состояние сохраненных данных. В качестве параметра мы получаем экземпляр модели, которую мы определили в файле .proto :
coroutineScope.launch < protoDataStore.updateData < user ->user.toBuilder() .setName(nameField.text.toString()) .setLastName(lastNameField.text.toString()) .setAge(ageField.text.toString().toIntOrNull() ?: 0) .setIsActive(isActiveSwitch.isChecked) .build() > >
Модель предоставляет нам билдер для записи данных в DataStore. Для каждого поля, указанного в модели, описанной в .proto -файле, мы имеем свой set-метод.
Чтение данных
Есть два способа для чтения данных из Proto DataStore:
Вызвать метод DataStore.updateData() . Так как в нем мы получаем актуальное состояние объекта, ничего не мешает прочитать их отсюда. Нюанс в том, что там нужно вернуть актуальное состояние модели в лямбде:
coroutineScope.launch < protoDataStore.updateData < user ->val name: String = user.name val lastName: String = user.lastName val age: Int = user.age val isActive: Boolean = user.isActive return@updateData user > >
Получить объект data : Flow , который вернет нам реактивный поток. Результатом этого Flow будет актуальный экземпляр хранимой в DataStore модели:
coroutineScope.launch(Dispatchers.Main) < protoDataStore.data .collect < user ->val receivedUser: User = user > >
SharedPreference vs DataStore
- DataStore предоставляет асинхронный API для записи и чтения данных, в отличие от Shared Preference, который предоставляет асинхронный API только при чтении данных.
- DataStore безопасен для работы на UI-потоке, так как есть возможность указать подходящий для нас Dispatcher.
- DataStore защищает от ошибок в рантайме, в то время как Shared Preference может бросить ошибку парсинга в рантайме.
- Proto DataStore предоставляет лучшую типобезопасность из коробки.
Тут стоит отдельно поговорить о транзакционности.
В Shared Preference транзакционность может быть достигнута за счет связки edit() -> apply()/commit() . Мы должны получить объект SharedPreferences.Editor, внести изменения и все это зафиксировать методами commit() или apply() :
val editor: SharedPreferences.Editor = pref.edit() editor.putString(KEY_LAST_NAME, lastName) editor.putBoolean(KEY_IS_ACTIVE, isActive) editor.apply()
В androidx этот же код будет выглядеть вот так:
pref.edit(commit = false)
По завершении операций в блоке edit<> внутри функции вызовется commit() или apply() , в зависимости от флага commit .
DataStore создает транзакцию всякий раз при вызове методов DataStore.updateData() или DataStore.edit() и делает запись после выполнения всех операций внутри этих функций.
DataStore vs Room
Если вам нужны частичные обновления, ссылочная целостность или поддержка больших/сложных наборов данных, подумайте об использовании Room вместо DataStore.
DataStore идеально подходит для небольших простых наборов данных и не поддерживает частичные обновления или ссылочную целостность.
Rx Java
В данный момент поддержки RX Java в DataStore нет. Поэтому, если мы хотим в проект на RX затащить DataStore, придется писать свои обертки. Как вариант, можно использовать тулы для совместимости вроде этой.
Вывод
У SharedPreferences есть несколько недостатков:
- Синхронный API, который может показаться безопасным для вызова на UI-потоке, но фактически он выполняет операции дискового ввода-вывода.
- Отсутствует механизм сигнализации об ошибках, транзакционный API и многое другое.
DataStore — это замена SharedPreferences, которая устраняет большинство этих недостатков. DataStore включает в себя полностью асинхронный API, использующий Kotlin Coroutines и Flow. Дает нам очень простой и удобный инструмент для миграции данных. Гарантирует согласованность данных и обработку поврежденных данных.
В данный момент библиотека находится в альфе, но вы всегда можете проверить последнюю версию в документации.
- Блог компании TINKOFF
- Разработка под Android
- Хранилища данных
