in_app_database 1.4.0 copy "in_app_database: ^1.4.0" to clipboard
in_app_database: ^1.4.0 copied to clipboard

Efficient in-app database for seamless data storage and retrieval, ensuring optimal performance and user experience.

example/lib/main.dart

import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:in_app_database/in_app_database.dart';

Map<String, Map> databases = {};

class NoteKey {
  static const id = 'id';
  static const title = 'title';
  static const body = 'body';
  static const tags = 'tags';
  static const pinned = 'pinned';
  static const color = 'color';
  static const createdAt = 'createdAt';
  static const updatedAt = 'updatedAt';
  static const views = 'views';
}

class Note {
  final String id;
  final String title;
  final String body;
  final List<String> tags;
  final bool pinned;
  final String color;
  final int views;
  final int? createdAt;
  final int? updatedAt;

  const Note({
    required this.id,
    required this.title,
    required this.body,
    this.tags = const [],
    this.pinned = false,
    this.color = 'white',
    this.views = 0,
    this.createdAt,
    this.updatedAt,
  });

  static String _asString(Object? v, [String fallback = '']) {
    if (v == null) return fallback;
    if (v is String) return v;
    return v.toString();
  }

  static bool _asBool(Object? v, [bool fallback = false]) {
    if (v is bool) return v;
    if (v is num) return v != 0;
    if (v is String) {
      final s = v.toLowerCase();
      if (s == 'true' || s == '1') return true;
      if (s == 'false' || s == '0') return false;
    }
    return fallback;
  }

  static int _asInt(Object? v, [int fallback = 0]) {
    if (v == null) return fallback;
    if (v is int) return v;
    if (v is num) return v.toInt();
    if (v is String) {
      final n = int.tryParse(v);
      if (n != null) return n;
      final d = double.tryParse(v);
      if (d != null) return d.toInt();
      final t = DateTime.tryParse(v);
      if (t != null) return t.toUtc().millisecondsSinceEpoch;
    }
    if (v is DateTime) return v.toUtc().millisecondsSinceEpoch;
    return fallback;
  }

  static int? _asIntOrNull(Object? v) {
    if (v == null) return null;
    if (v is int) return v;
    if (v is num) return v.toInt();
    if (v is String) {
      final n = int.tryParse(v);
      if (n != null) return n;
      final d = double.tryParse(v);
      if (d != null) return d.toInt();
      final t = DateTime.tryParse(v);
      if (t != null) return t.toUtc().millisecondsSinceEpoch;
    }
    if (v is DateTime) return v.toUtc().millisecondsSinceEpoch;
    return null;
  }

  static List<String> _asStringList(Object? v) {
    if (v is List) {
      return v
          .map((e) => e?.toString() ?? '')
          .where((e) => e.isNotEmpty)
          .toList(growable: false);
    }
    if (v is String && v.isNotEmpty) {
      return v
          .split(',')
          .map((e) => e.trim())
          .where((e) => e.isNotEmpty)
          .toList(growable: false);
    }
    return const [];
  }

  factory Note.fromMap(InAppDocument map) {
    return Note(
      id: _asString(map[NoteKey.id]),
      title: _asString(map[NoteKey.title]),
      body: _asString(map[NoteKey.body]),
      tags: _asStringList(map[NoteKey.tags]),
      pinned: _asBool(map[NoteKey.pinned]),
      color: _asString(map[NoteKey.color], 'null'),
      views: _asInt(map[NoteKey.views]),
      createdAt: _asIntOrNull(map[NoteKey.createdAt]),
      updatedAt: _asIntOrNull(map[NoteKey.updatedAt]),
    );
  }

  factory Note.fromSnapshot(InAppDocumentSnapshot snap) {
    final data = snap.data();
    if (data == null) return Note.empty(snap.id);
    return Note.fromMap({...data, NoteKey.id: snap.id});
  }

  factory Note.empty(String id) =>
      Note(id: id, title: '', body: '', color: 'white');

  InAppDocument toMap() => {
    NoteKey.id: id,
    NoteKey.title: title,
    NoteKey.body: body,
    NoteKey.tags: tags,
    NoteKey.pinned: pinned,
    NoteKey.color: color,
    NoteKey.views: views,
    if (createdAt != null) NoteKey.createdAt: createdAt,
    if (updatedAt != null) NoteKey.updatedAt: updatedAt,
  };

  Note copyWith({
    String? title,
    String? body,
    List<String>? tags,
    bool? pinned,
    String? color,
  }) {
    return Note(
      id: id,
      title: title ?? this.title,
      body: body ?? this.body,
      tags: tags ?? this.tags,
      pinned: pinned ?? this.pinned,
      color: color ?? this.color,
      views: views,
      createdAt: createdAt,
      updatedAt: updatedAt,
    );
  }
}

class DatabaseDelegate extends InAppDatabaseDelegate {
  @override
  Future<bool> init(String dbName) async {
    databases[dbName] = databases[dbName] ??= {};
    return true;
  }

  @override
  Future<Iterable<String>> paths(String dbName) async {
    final x = databases[dbName]!.keys.whereType<String>().toList();
    return x;
  }

  @override
  Future<bool> delete(String dbName, String key) async {
    databases[dbName]!.remove(key);
    return true;
  }

  @override
  Future<bool> drop(String dbName) async {
    databases.remove(dbName);
    return true;
  }

  @override
  Future<Object?> read(String dbName, String key) async {
    return databases[dbName]![key];
  }

  @override
  Future<bool> write(String dbName, String key, Object? value) async {
    if (value != null) {
      databases[dbName]![key] = value;
      return true;
    } else {
      databases[dbName]!.remove(key);
      return true;
    }
  }

  @override
  Future<InAppWriteLimitation?> limitation(
    String dbName,
    PathDetails details,
  ) async {
    return {
      "users": const InAppWriteLimitation(50),
      "posts": const InAppWriteLimitation(10),
      "users/{user_id}/posts": const InAppWriteLimitation(10),
    }[details.format]; // OPTIONAL
  }
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    InAppDatabase.init(
      showLogs: true, // optional
      type: InAppDatabaseType.json, // optional
      version: InAppDatabaseVersion.v1, // optional
      delegate: DatabaseDelegate(), // required
    );
    InAppDatabase.i.collection("users").get().then((value) {
      log("USERS: $value");
    });
    InAppDatabase.i.collection("users").snapshots().listen((value) {
      log("USER_SNAPSHOTS: $value");
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'In App Database',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const LocalDataTestPage(),
    );
  }
}

class LocalDataTestPage extends StatefulWidget {
  const LocalDataTestPage({super.key});

  @override
  State<LocalDataTestPage> createState() => _LocalDataTestPageState();
}

class _LocalDataTestPageState extends State<LocalDataTestPage> {
  final _db = InAppDatabase.instance;
  String _log = '';

  static const _id = '1778319785575';

  InAppQueryReference get _notes =>
      _db.collection('notes_v2').doc('v1').collection('notes');

  InAppDocumentReference _noteDoc(String id) => _notes.doc(id);

  void _log_(String msg) {
    if (!mounted) return;
    setState(() => _log = msg);
  }

  Future<void> _safe(String label, Future<void> Function() action) async {
    try {
      await action();
    } catch (e) {
      _log_('$label error: $e');
      rethrow;
    }
  }

  Future<void> _create() => _safe('create', () async {
    final tagPool = ['work', 'personal', 'idea', 'todo', 'urgent'];
    final colors = ['yellow', 'blue', 'green', 'pink', 'white'];

    final rnd = DateTime.now().millisecondsSinceEpoch;
    final tags = (tagPool..shuffle()).take(2).toList();
    final color = colors[rnd % colors.length];

    final n = Note(
      id: _id,
      title: 'Note ${rnd % 1000}',
      body: 'This is a sample note body created at $rnd',
      tags: tags,
      pinned: rnd % 2 == 0,
      color: color,
    );

    await _noteDoc(_id).set({
      ...n.toMap(),
      NoteKey.createdAt: InAppFieldValue.serverTimestamp(true),
    });

    _log_('Created: ${n.title} | ${n.color} | pinned: ${n.pinned}');
  });

  Future<void> _createRandom() => _safe('createRandom', () async {
    final titles = ['Meeting', 'Idea', 'Shopping', 'Reminder', 'Quote', 'Task'];
    final colors = ['yellow', 'blue', 'green', 'pink', 'white'];
    final tagPool = ['work', 'personal', 'idea', 'todo', 'urgent'];

    final rnd = DateTime.now().millisecondsSinceEpoch;
    final n = Note(
      id: rnd.toString(),
      title: '${titles[rnd % titles.length]} ${rnd % 1000}',
      body: 'Body content at $rnd',
      tags: (tagPool..shuffle()).take(2).toList(),
      pinned: rnd % 2 == 0,
      color: colors[rnd % colors.length],
    );

    final ref = await _notes.add({
      ...n.toMap(),
      NoteKey.createdAt: InAppFieldValue.serverTimestamp(true),
    });

    _log_('Created: ${ref.id} | ${n.title} | ${n.color}');
  });

  Future<void> _update() => _safe('update', () async {
    await _noteDoc(_id).update({
      NoteKey.pinned: true,
      NoteKey.views: InAppFieldValue.increment(1),
      NoteKey.tags: InAppFieldValue.arrayUnion(['updated']),
      NoteKey.updatedAt: InAppFieldValue.serverTimestamp(true),
    });
    _log_('Updated: $_id (incremented views, added tag)');
  });

  Future<void> _setMerge() => _safe('setMerge', () async {
    await _noteDoc(_id).set({
      NoteKey.color: 'pink',
      NoteKey.title: 'Merged title',
    }, const InAppSetOptions(merge: true));
    _log_('Merged update on: $_id');
  });

  Future<void> _toggle() => _safe('toggle', () async {
    await _noteDoc(_id).update({NoteKey.pinned: InAppFieldValue.toggle()});
    _log_('Toggled pinned on: $_id');
  });

  Future<void> _removeTag() => _safe('removeTag', () async {
    await _noteDoc(_id).update({
      NoteKey.tags: InAppFieldValue.arrayRemove(['updated']),
    });
    _log_('Removed tag "updated"');
  });

  Future<void> _deleteField() => _safe('deleteField', () async {
    await _noteDoc(_id).update({NoteKey.color: InAppFieldValue.delete()});
    _log_('Deleted color field');
  });

  Future<void> _delete(String id) => _safe('delete', () async {
    await _noteDoc(id).delete();
    _log_('Deleted: $id');
  });

  Future<void> _checkById() => _safe('checkById', () async {
    final snap = await _noteDoc(_id).get();
    _log_(snap.exists ? 'Exists: $_id' : 'Not found: $_id');
  });

  Future<void> _deleteAll() => _safe('deleteAll', () async {
    await _notes.drop();
    _log_('Cleared all notes');
  });

  Future<void> _get() => _safe('get', () async {
    final snap = await _notes.get();
    _log_('Got ${snap.size} items');
  });

  Future<void> _getById() => _safe('getById', () async {
    final snap = await _noteDoc(_id).get();
    if (!snap.exists) return _log_('Not found');
    final n = Note.fromSnapshot(snap);
    _log_('Got: ${n.title}');
  });

  Future<void> _query() => _safe('query', () async {
    final snap =
        await _notes
            .where(NoteKey.color, isEqualTo: 'yellow')
            .orderBy(NoteKey.createdAt, descending: true)
            .limit(10)
            .get();
    _log_('Query: ${snap.size} yellow notes');
  });

  Future<void> _queryWhereIn() => _safe('queryWhereIn', () async {
    final snap =
        await _notes
            .where(NoteKey.color, whereIn: ['yellow', 'blue', 'green'])
            .get();
    _log_('whereIn: ${snap.size} items');
  });

  Future<void> _queryArrayContains() => _safe('queryArrayContains', () async {
    final snap = await _notes.where(NoteKey.tags, arrayContains: 'work').get();
    _log_('arrayContains "work": ${snap.size} items');
  });

  Future<void> _queryRange() => _safe('queryRange', () async {
    final snap =
        await _notes
            .where(NoteKey.views, isGreaterThanOrEqualTo: 1)
            .orderBy(NoteKey.views, descending: true)
            .get();
    _log_('views >= 1: ${snap.size} items');
  });

  Future<void> _search() => _safe('search', () async {
    final value = "Note";
    final snap =
        await _notes.orderBy(NoteKey.title).startAt([value]).endAt([
          '$value\uf8ff',
        ]).get();
    _log_(
      'Search "Note": ${snap.docs.length} items: '
      '${snap.docs.map((d) => d.data()[NoteKey.title]).join(", ")}',
    );
  });

  Future<void> _count() => _safe('count', () async {
    final snap = await _notes.count().get();
    _log_('Count: ${snap.count}');
  });

  Future<void> _batch() => _safe('batch', () async {
    final batch = _db.batch();
    batch.set(_noteDoc('batch_1'), {
      NoteKey.id: 'batch_1',
      NoteKey.title: 'Batch one',
      NoteKey.body: 'created via batch',
      NoteKey.color: 'green',
    });
    batch.set(_noteDoc('batch_2'), {
      NoteKey.id: 'batch_2',
      NoteKey.title: 'Batch two',
      NoteKey.body: 'created via batch',
      NoteKey.color: 'blue',
    });
    batch.update(_noteDoc('batch_1'), {NoteKey.pinned: true});
    await batch.commit();
    _log_('Batch committed: 3 ops');
  });

  Future<void> _transaction() => _safe('transaction', () async {
    final result = await _db.runTransaction<int>((txn) async {
      final ref = _noteDoc('counter_doc');
      final snap = await txn.get(ref);
      final current = (snap.data()?[NoteKey.views] as int?) ?? 0;
      final next = current + 1;
      txn.set(ref, {
        NoteKey.id: 'counter_doc',
        NoteKey.title: 'Counter',
        NoteKey.body: 'Atomic counter',
        NoteKey.views: next,
      });
      return next;
    });
    _log_('Txn committed: counter = $result');
  });

  Future<void> _subcollection() => _safe('subcollection', () async {
    final ref = _noteDoc(_id).collection('comments').doc('c1');
    await ref.set({
      'id': 'c1',
      'text': 'A nested comment',
      'createdAt': InAppFieldValue.serverTimestamp(true),
    });
    final snap = await ref.get();
    _log_('Subcollection: ${snap.exists ? snap.data() : "missing"}');
  });

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: ThemeData.dark(),
      child: Scaffold(
        appBar: AppBar(title: const Text('InAppDatabase Test')),
        floatingActionButton: SingleChildScrollView(
          child: Container(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [Colors.transparent, Colors.black],
              ),
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                _btn('Create', _create),
                _btn('CreateRandom (add)', _createRandom),
                _btn('Update', _update),
                _btn('Set Merge', _setMerge),
                _btn('Toggle Pin', _toggle),
                _btn('Remove Tag', _removeTag),
                _btn('Delete Field', _deleteField),
                _btn('Batch (3 ops)', _batch),
                _btn('Transaction', _transaction),
                _btn('Subcollection', _subcollection),
                _btn('Check By Id', _checkById),
                _btn('Get All', _get),
                _btn('Get By Id', _getById),
                _btn('Query (color)', _query),
                _btn('whereIn', _queryWhereIn),
                _btn('arrayContains', _queryArrayContains),
                _btn('Range', _queryRange),
                _btn('Search', _search),
                _btn('Count', _count),
                _btn('Delete By Id', () => _delete(_id)),
                _btn('Clear All', _deleteAll),
              ],
            ),
          ),
        ),
        body: SafeArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (_log.isNotEmpty)
                  Container(
                    width: double.infinity,
                    padding: const EdgeInsets.all(8),
                    color: Colors.black26,
                    child: Text(
                      _log,
                      style: const TextStyle(
                        fontSize: 12,
                        fontFamily: 'monospace',
                      ),
                    ),
                  ),
                const SizedBox(height: 12),
                const _Section('Listen Count (count().snapshots())'),
                StreamBuilder<InAppAggregateQuerySnapshot>(
                  stream: _notes.count().snapshots(),
                  builder: (context, s) {
                    return Text('Total notes: ${s.data?.count ?? 0}');
                  },
                ),
                const Divider(),
                const _Section('Listen By Id (doc().snapshots())'),
                StreamBuilder<InAppDocumentSnapshot>(
                  stream: _noteDoc(_id).snapshots(),
                  builder: (context, s) {
                    if (!s.hasData) return const Text('Loading...');
                    final snap = s.data!;
                    if (!snap.exists) return const Text('No data');
                    final n = Note.fromSnapshot(snap);
                    return ListTile(
                      contentPadding: EdgeInsets.zero,
                      title: Text(n.title),
                      subtitle: Text('${n.body} | pinned: ${n.pinned}'),
                      trailing: Text(n.color),
                    );
                  },
                ),
                const Divider(),
                const _Section('Listen All (collection.snapshots())'),
                StreamBuilder<InAppQuerySnapshot>(
                  stream: _notes.snapshots(),
                  builder: (context, s) {
                    final docs = s.data?.docs ?? const [];
                    if (docs.isEmpty) return const Text('No data');
                    return ListView.builder(
                      shrinkWrap: true,
                      physics: const NeverScrollableScrollPhysics(),
                      itemCount: docs.take(5).length,
                      itemBuilder: (_, i) {
                        final n = Note.fromSnapshot(docs[i]);
                        return ListTile(
                          onLongPress: () => _delete(n.id),
                          contentPadding: EdgeInsets.zero,
                          title: Text(n.title),
                          subtitle: Text('${n.color} | ${n.tags.join(", ")}'),
                          trailing: Text('Pinned: ${n.pinned}'),
                        );
                      },
                    );
                  },
                ),
                const Divider(),
                const _Section('Listen By Query (where + orderBy)'),
                StreamBuilder<InAppQuerySnapshot>(
                  stream:
                      _notes
                          .where(NoteKey.pinned, isEqualTo: true)
                          .orderBy(NoteKey.createdAt, descending: true)
                          .limit(10)
                          .snapshots(),
                  builder: (context, s) {
                    final docs = s.data?.docs ?? const [];
                    if (docs.isEmpty) return const Text('No pinned notes');
                    return ListView.builder(
                      shrinkWrap: true,
                      physics: const NeverScrollableScrollPhysics(),
                      itemCount: docs.take(5).length,
                      itemBuilder: (_, i) {
                        final n = Note.fromSnapshot(docs[i]);
                        return ListTile(
                          onLongPress: () => _delete(n.id),
                          contentPadding: EdgeInsets.zero,
                          title: Text(n.title),
                          subtitle: Text('${n.color} | ${n.tags.join(", ")}'),
                          trailing: Text('Views: ${n.views}'),
                        );
                      },
                    );
                  },
                ),
                const Divider(),
                const _Section('Listen Subcollection'),
                StreamBuilder<InAppQuerySnapshot>(
                  stream: _noteDoc(_id).collection('comments').snapshots(),
                  builder: (context, s) {
                    final docs = s.data?.docs ?? const [];
                    if (docs.isEmpty) return const Text('No comments');
                    return Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children:
                          docs
                              .take(5)
                              .map(
                                (d) => ListTile(
                                  contentPadding: EdgeInsets.zero,
                                  title: Text(
                                    d.data()['text']?.toString() ?? '',
                                  ),
                                  subtitle: Text('id: ${d.id}'),
                                ),
                              )
                              .toList(),
                    );
                  },
                ),
                const SizedBox(height: 40),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _btn(String label, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        color: Colors.transparent,
        padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
        child: Text(label),
      ),
    );
  }
}

class _Section extends StatelessWidget {
  final String title;

  const _Section(this.title);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 4, bottom: 8),
      child: Text(
        title,
        style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
      ),
    );
  }
}
3
likes
150
points
55
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Efficient in-app database for seamless data storage and retrieval, ensuring optimal performance and user experience.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

flutter, in_app_query, pluralize

More

Packages that depend on in_app_database