Irbisa · Шпаргалка

Локальное хранилище

SharedPreferences, sqflite, Hive, flutter_secure_storage

Middle-разработчик · 25 вопросов

  1. 01

    Когда использовать SharedPreferences, sqflite, Hive и flutter_secure_storage?

    Выбирайте по форме данных: key-value для настроек, настоящая база для записей, платформенное хранилище для секретов.

  2. 02

    Как открыть базу sqflite и выполнить базовый CRUD?

    Откройте файл один раз через openDatabase (путь берётся из getDatabasesPath()), создайте таблицы в onCreate, а дальше пользуйтесь insert, query, update и delete; значения передаются в map или как плейсхолдеры ? в where, но никогда не через интерполяцию строк.

    Future<Database> openAppDb() async => openDatabase(      join(await getDatabasesPath(), 'app.db'), // join from package:path      version: 1,      onCreate: (db, version) => db.execute(        'CREATE TABLE notes(id INTEGER PRIMARY KEY AUTOINCREMENT, '        'title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)',…
  3. 03

    Какие типичные ловушки SharedPreferences и как их избегать?

    SharedPreferences представляет собой маленький, нетипизированный, нереактивный и незашифрованный key-value файл: загрузите его до того, как он понадобится UI, храните в нём только небольшие настройки и никогда не храните секреты.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  // Load once before the first frame, so the UI never flashes a default theme.  final prefs = await SharedPreferencesWithCache.create(    cacheOptions: const SharedPreferencesWithCacheOptions(      allowList: {'theme', 'filters'},…
  4. 04

    Что такое path_provider и в какую директорию класть файл: documents, application support, temporary или cache?

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

    Future<void> saveFiles(Note note, List<int> thumbBytes) async {  // user content: backed up, the user would miss it  final docs = await getApplicationDocumentsDirectory();  await File(p.join(docs.path, '${note.id}.json'))      .writeAsString(jsonEncode(note.toJson()));…
  5. 05

    Вставка в sqflite падает, как только вы передаёте DateTime: как правильно хранить и читать даты, bool и enum?

    sqflite умеет биндить только num, String, Uint8List и null, потому что SQLite хранит лишь числа, текст и байты; всё остальное преобразуйте в toMap/fromMap модели (или через TypeConverter из drift), но не в местах вызова.

    enum Priority { low, normal, high } class Task {  Task({this.id, required this.title, required this.done,      required this.dueAt, required this.priority});  final int? id;…
  6. 06

    Чем отличаются legacy SharedPreferences, SharedPreferencesAsync и SharedPreferencesWithCache?

    Они различаются тем, где находится источник истины: legacy SharedPreferences кеширует все ключи в Dart, SharedPreferencesAsync ходит на платформу при каждом вызове, а SharedPreferencesWithCache кеширует только разрешённые вами ключи.

    import 'package:shared_preferences/shared_preferences.dart';import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; Future<SharedPreferencesWithCache> initPrefs() async {  const options = SharedPreferencesOptions();  // One-time copy of keys the legacy API wrote (a different backend on Android).…
  7. 07

    Нужно посмотреть, что приложение реально записало на диск тестового устройства: где лежат эти файлы и чем их открыть?

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

    # Android, debuggable build: copy the database together with its WAL fileadb exec-out run-as com.example.app cat databases/app.db > app.dbadb exec-out run-as com.example.app cat databases/app.db-wal > app.db-wal # Preferences written by the legacy SharedPreferences APIadb exec-out run-as com.example.app cat shared_prefs/FlutterSharedPreferences.xml…
  8. 08

    Как обрабатывать миграции схемы в sqflite?

    Увеличьте version в openDatabase и доводите старые базы до актуальной схемы в onUpgrade шагами if (from < N); onCreate должен сразу создавать последнюю схему.

    Future<Database> openAppDb() async => openDatabase(      join(await getDatabasesPath(), 'app.db'),      version: 3,      // Fresh install: create the latest schema directly.      onCreate: (db, version) => db.execute(        'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT NOT NULL, '…
  9. 09

    Релиз 2 добавляет колонку due_at. У обновившихся всё в порядке, а чистые установки падают с no such column: due_at. Почему?

    Future<Database> openDb(String path) => openDatabase(  path,  version: 2,  onCreate: (db, version) async {    await db.execute(        'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT NOT NULL)');  },  onUpgrade: (db, oldVersion, newVersion) async {    if (oldVersion < 2) {      await db.execute('ALTER TABLE tasks ADD COLUMN due_at INTEGER');    }  },);

    На чистой установке sqflite вызывает только onCreate, а не onUpgrade, и сразу помечает файл версией 2.

  10. 10

    Как работают type adapters и шифрование box-ов в Hive?

    Hive хранит объекты в «боксах», то есть в файлах с ключами-строками или числами.

    @HiveType(typeId: 1)class User extends HiveObject {  User({required this.name, required this.email});   @HiveField(0)  String name;…
  11. 11

    Как flutter_secure_storage на самом деле защищает данные на iOS и на Android?

    Это тонкая обёртка над платформенным защищённым хранилищем, поэтому реальные гарантии даёт платформа: Keychain на iOS и ключ в Keystore на Android.

  12. 12

    Когда выбрать Drift вместо raw sqflite?

    Берите Drift, когда нужны проверяемые при компиляции запросы, стримы, которые переизлучают данные при изменении таблиц, и тестируемые миграции; для горстки запросов хватит чистого sqflite.

    class Tasks extends Table {  IntColumn get id => integer().autoIncrement()();  TextColumn get title => text().withLength(min: 1, max: 200)();  BoolColumn get done => boolean().withDefault(const Constant(false))();  DateTimeColumn get dueAt => dateTime().nullable()();}…
  13. 13

    Что такое Isar и ObjectBox и когда они побеждают sqflite?

    Isar и ObjectBox представляют собой NoSQL объектные базы на нативном коде с Dart-биндингами: вы кладёте в них Dart-объекты напрямую, без раскладывания по таблицам и строкам.

  14. 14

    Эта транзакция sqflite никогда не завершается, а в логе предупреждение, что база заблокирована: что не так?

    Future<void> moveTask(Database db, int taskId, int listId) async {  await db.transaction((txn) async {    await txn.update('tasks', {'list_id': listId},        where: 'id = ?', whereArgs: [taskId]);    final count = Sqflite.firstIntValue(await db.rawQuery(        'SELECT COUNT(*) FROM tasks WHERE list_id = ?', [listId]));    await txn.update('lists', {'task_count': count},        where: 'id = ?', whereArgs: [listId]);  });}

    Внутри транзакции вызывается db.rawQuery вместо txn.rawQuery.

  15. 15

    Найдите баг: этот NotesDao проходит юнит-тесты, но на экране, который одновременно грузит и сохраняет данные, часть вызовов падает с DatabaseException.

    class NotesDao {  NotesDao(this.path);  final String path;   Future<T> _use<T>(Future<T> Function(Database db) body) async {    final db = await openDatabase(path);    try {      return await body(db);    } finally {      await db.close();    }  }   Future<List<Map<String, Object?>>> all() => _use((db) => db.query('notes'));  Future<int> add(String text) => _use((db) => db.insert('notes', {'text': text}));}

    openDatabase возвращает один общий Database на путь, поэтому close() в finally одного вызова закрывает его под всеми остальными вызовами, которые уже выполняются и ещё не дошли до базы; они падают с DatabaseException о закрытой базе (вызовы, начатые позже, просто откроют её заново).

  16. 16

    Импорт JSON-выгрузки на 20 МБ подвешивает интерфейс на четыре секунды, хотя каждый вызов хранилища идёт через await: куда на самом деле уходит время?

    await лишь уступает управление между кусками работы, но не уносит их из изолята: зависание даёт синхронный Dart-код (jsonDecode на 20 МБ и цикл сборки моделей).

    // wrong: jsonDecode blocks the UI isolate, plus one hop per rowFuture<void> importSlow(Database db, String json) async {  final rows = (jsonDecode(json) as List).cast<Map<String, Object?>>();  for (final row in rows) {    await db.insert('items', row);  }…
  17. 17

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

    За разрастанием хранилища почти всегда стоит кэш, которому не задали правило вытеснения.

    Future<Map<String, int>> measureStorage() async {  final dirs = {    'documents': await getApplicationDocumentsDirectory(),    'support': await getApplicationSupportDirectory(),    'cache': await getApplicationCacheDirectory(),  };…
  18. 18

    Список, построенный из sqflite, не обновляется, когда фоновая синхронизация пишет новые строки: как сделать локальное хранилище реактивным без поллинга?

    sqflite не сообщает об изменениях вообще никак, поэтому либо все записи идут через один объект, который заодно публикует событие, либо берите хранилище, которое само следит за таблицами.

    class TaskRepository {  TaskRepository(this._db);  final Database _db;  final _changes = StreamController<List<Task>>.broadcast();   Stream<List<Task>> watchOpen() async* {…
  19. 19

    Что нужно стереть при выходе пользователя из аккаунта и о чём обычно забывают?

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

  20. 20

    Что идёт не так при конкурентной записи в локальную БД и как этого избежать?

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

    // Optimistic: the update lands only if nobody changed the row since we read it.Future<bool> rename(Database db, int id, String title, int readRevision) async {  final changed = await db.update(    'tasks',    {'title': title, 'revision': readRevision + 1},    where: 'id = ? AND revision = ?',…
  21. 21

    Запрос в sqflite был мгновенным на 500 строках и занимает две секунды на 50 000. Как диагностировать и починить?

    Сначала выполните EXPLAIN QUERY PLAN для этого запроса.

    // 1. Ask SQLite what it does with the slow query.final plan = await db.rawQuery(  'EXPLAIN QUERY PLAN SELECT * FROM orders '  'WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',  [userId],);…
  22. 22

    Пользователи, поднявшие новый телефон из бэкапа, жалуются, что приложение открывается пустым или падает на базе: что сделал восстановленный бэкап?

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

    <!-- res/xml/data_extraction_rules.xml (Android 12+), referenced by     android:dataExtractionRules in AndroidManifest.xml.     Android 11 and lower read android:fullBackupContent instead. --><data-extraction-rules>  <cloud-backup>    <!-- Encrypted with a Keystore key that never leaves the device -->…
  23. 23

    Пакет key-value хранилища, на котором построено приложение, заброшен: как перевезти локальные данные уже выпущенного приложения в новое хранилище и ничего не потерять?

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

  24. 24

    Фоновый обработчик FCM увеличивает счётчик в SharedPreferences и Hive, но интерфейс видит новое значение только после перезапуска: почему?

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

    @pragma('vm:entry-point')Future<void> onBackgroundMessage(RemoteMessage message) async {  // wrong: the UI isolate cached its map at launch and never sees this write  // final prefs = await SharedPreferences.getInstance();   // right: no Dart-side cache, every call goes to the platform store…
  25. 25

    В Crashlytics ровным ручейком идут ошибки «database disk image is malformed»: как найти причину и вернуть этих пользователей в рабочее состояние?

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