sqflite_live 1.2.1 copy "sqflite_live: ^1.2.1" to clipboard
sqflite_live: ^1.2.1 copied to clipboard

Sqflite live server for viewing db data on web-based local server.

example/lib/main.dart

import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_live/sqflite_live.dart';

class DatabaseHelper {
  DatabaseHelper._();
  static final DatabaseHelper instance = DatabaseHelper._();

  static const _dbName = 'demo.db';
  static const _dbVersion = 1;

  // table names
  static const userTable = 'users';
  static const taskTable = 'tasks';

  final _rnd = Random();

  // singleton Database reference
  Database? _db;
  Future<Database> get database async {
    if (_db != null) return _db!;
    _db = await _initDatabase();
    return _db!;
  }

  // open the database & create tables if they don't exist
  Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, _dbName);
    return (await openDatabase(
      path,
      version: _dbVersion,
      onCreate: _onCreate,
    )
    /// JUST ADD THIS:::
    ..live(enabled: kDebugMode, level: LogLevel.info, port: 8881));
  }

  // SQL to create the two tables
  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $userTable (
        id   INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age  INTEGER NOT NULL
      )
    ''');
    await db.execute('''
      CREATE TABLE $taskTable (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        title    TEXT NOT NULL,
        user_id  INTEGER NOT NULL,
        is_done  INTEGER NOT NULL DEFAULT 0,
        FOREIGN KEY(user_id) REFERENCES $userTable(id)
      )
    ''');
  }

  Future<List<Map<String, Object?>>> getUsers() async {
    final db = await database;
    return db.query(userTable, orderBy: 'id');
  }

  Future<List<Map<String, Object?>>> getTasks() async {
    final db = await database;
    return db.query(taskTable, orderBy: 'id');
  }

  /// Inserts one random user.
  Future<void> insertRandomUser() async {
    final db = await database;
    await db.insert(userTable, {
      'name': 'User${_rnd.nextInt(1000)}',
      'age': _rnd.nextInt(60) + 18,
    });
  }

  /// Inserts one random task assigned to a random existing user.
  Future<void> insertRandomTask() async {
    final db = await database;
    final users = await db.query(userTable, columns: ['id']);
    if (users.isEmpty) return;
    final uid = users[_rnd.nextInt(users.length)]['id'] as int;
    await db.insert(taskTable, {
      'title': 'Task ${_rnd.nextInt(10000)}',
      'user_id': uid,
      'is_done': _rnd.nextBool() ? 1 : 0,
    });
  }

  /// Renames a random user (an UPDATE, to test edits).
  Future<void> renameRandomUser() async {
    final db = await database;
    final users = await db.query(userTable, columns: ['id']);
    if (users.isEmpty) return;
    final uid = users[_rnd.nextInt(users.length)]['id'] as int;
    await db.update(
      userTable,
      {'name': 'Renamed${_rnd.nextInt(1000)}', 'age': _rnd.nextInt(60) + 18},
      where: 'id = ?',
      whereArgs: [uid],
    );
  }

  /// Toggles a task's done flag.
  Future<void> toggleTask(int id, bool done) async {
    final db = await database;
    await db.update(
      taskTable,
      {'is_done': done ? 1 : 0},
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  /// Deletes a specific user and their tasks.
  Future<void> deleteUser(int id) async {
    final db = await database;
    await db.delete(taskTable, where: 'user_id = ?', whereArgs: [id]);
    await db.delete(userTable, where: 'id = ?', whereArgs: [id]);
  }

  /// Deletes a random user (and their tasks).
  Future<void> deleteRandomUser() async {
    final db = await database;
    final users = await db.query(userTable, columns: ['id']);
    if (users.isEmpty) return;
    await deleteUser(users[_rnd.nextInt(users.length)]['id'] as int);
  }

  /// Removes every row from both tables.
  Future<void> clearAll() async {
    final db = await database;
    await db.delete(taskTable);
    await db.delete(userTable);
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Open the database and start the live server. Watch the console for the
  // 🗂️ SQFLITE Server @ http://… URL, open it in a browser on the same
  // network, then use the buttons in the app and refresh the browser to see
  // the changes live.
  await DatabaseHelper.instance.database;
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'sqflite_live Demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _db = DatabaseHelper.instance;
  late final ScrollController _scrollController;
  // Holding the rows in notifiers lets only the affected slivers rebuild
  // instead of the whole page on every refresh.
  final _users = ValueNotifier<List<Map<String, Object?>>>(const []);
  final _tasks = ValueNotifier<List<Map<String, Object?>>>(const []);

  @override
  void initState() {
    _scrollController = ScrollController();
    super.initState();
    _refresh();

  }

  @override
  void dispose() {
    _scrollController.dispose();
    _users.dispose();
    _tasks.dispose();
    super.dispose();
  }

  Future<void> _refresh() async {
    final users = await _db.getUsers();
    final tasks = await _db.getTasks();
    if (!mounted) return;
    _users.value = users;
    _tasks.value = tasks;
  }

  Future<void> _run(Future<void> Function() action, String message) async {
    await action();
    await _refresh();
    if (!mounted) return;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('sqflite_live Demo'),
        actions: [
          IconButton(
            tooltip: 'Reload',
            onPressed: _refresh,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (!_scrollController.hasClients) return;
          _scrollController.animateTo(
            0,
            duration: const Duration(milliseconds: 300),
            curve: Curves.easeOut,
          );
        },
        child: const Icon(Icons.arrow_upward),
      ),
      body: Column(
        children: [
          // Rebuilds only when the *emptiness* of either list flips, so the
          // buttons don't churn while rows change.
          _ActionBar(
            users: _users,
            tasks: _tasks,
            onAddUser: () => _run(_db.insertRandomUser, 'Added a user'),
            onAddTask: () => _run(_db.insertRandomTask, 'Added a task'),
            onDeleteUser: () => _run(_db.deleteRandomUser, 'Deleted a user'),
            onUpdateUser: () => _run(_db.renameRandomUser, 'Renamed a user'),
            onClear: () => _run(_db.clearAll, 'Cleared the database'),
          ),
          const Divider(height: 1),
          Expanded(
            child: ValueListenableBuilder<List<Map<String, Object?>>>(
              valueListenable: _users,
              builder: (context, users, _) {
                return ValueListenableBuilder<List<Map<String, Object?>>>(
                  valueListenable: _tasks,
                  builder: (context, tasks, _) {
                    if (users.isEmpty && tasks.isEmpty) {
                      return const Center(
                        child: Text(
                          'Empty database.\nUse "Add user" to start.',
                          textAlign: TextAlign.center,
                        ),
                      );
                    }
                    return CustomScrollView(
                      controller: _scrollController,
                      slivers: [
                        SliverToBoxAdapter(
                          child: _SectionHeader('Users (${users.length})'),
                        ),
                        SliverList.builder(
                          itemCount: users.length,
                          itemBuilder: (context, i) {
                            final u = users[i];
                            final id = u['id'] as int;
                            return _UserTile(
                              key: ValueKey('user-$id'),
                              id: id,
                              name: u['name'] as String,
                              age: u['age'] as int,
                              onDelete: () => _run(
                                () => _db.deleteUser(id),
                                'Deleted ${u['name']}',
                              ),
                            );
                          },
                        ),
                        SliverToBoxAdapter(
                          child: _SectionHeader('Tasks (${tasks.length})'),
                        ),
                        SliverList.builder(
                          itemCount: tasks.length,
                          itemBuilder: (context, i) {
                            final t = tasks[i];
                            final id = t['id'] as int;
                            final done = (t['is_done'] as int) == 1;
                            return _TaskTile(
                              key: ValueKey('task-$id'),
                              title: t['title'] as String,
                              userId: t['user_id'] as int,
                              done: done,
                              onToggle: () => _run(
                                () => _db.toggleTask(id, !done),
                                'Toggled task',
                              ),
                            );
                          },
                        ),
                      ],
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class _UserTile extends StatelessWidget {
  const _UserTile({
    super.key,
    required this.id,
    required this.name,
    required this.age,
    required this.onDelete,
  });

  final int id;
  final String name;
  final int age;
  final VoidCallback onDelete;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text('$id')),
      title: Text(name),
      subtitle: Text('Age: $age'),
      trailing: IconButton(
        icon: const Icon(Icons.delete_outline),
        onPressed: onDelete,
      ),
    );
  }
}

class _TaskTile extends StatelessWidget {
  const _TaskTile({
    super.key,
    required this.title,
    required this.userId,
    required this.done,
    required this.onToggle,
  });

  final String title;
  final int userId;
  final bool done;
  final VoidCallback onToggle;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(
        done ? Icons.check_circle : Icons.radio_button_unchecked,
      ),
      title: Text(title),
      subtitle: Text('user_id: $userId'),
      onTap: onToggle,
    );
  }
}

class _ActionBar extends StatelessWidget {
  const _ActionBar({
    required this.users,
    required this.tasks,
    required this.onAddUser,
    required this.onAddTask,
    required this.onDeleteUser,
    required this.onUpdateUser,
    required this.onClear,
  });

  final ValueListenable<List<Map<String, Object?>>> users;
  final ValueListenable<List<Map<String, Object?>>> tasks;
  final VoidCallback onAddUser;
  final VoidCallback onAddTask;
  final VoidCallback onDeleteUser;
  final VoidCallback onUpdateUser;
  final VoidCallback onClear;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Wrap(
        spacing: 8,
        runSpacing: 8,
        alignment: WrapAlignment.center,
        children: [
          FilledButton.icon(
            onPressed: onAddUser,
            icon: const Icon(Icons.person_add),
            label: const Text('Add user'),
          ),
          // Each gate widget rebuilds only when its own enabled state flips.
          _EnabledGate(
            listenable: users,
            isEnabled: () => users.value.isNotEmpty,
            builder: (enabled) => FilledButton.tonalIcon(
              onPressed: enabled ? onAddTask : null,
              icon: const Icon(Icons.add_task),
              label: const Text('Add task'),
            ),
          ),
          _EnabledGate(
            listenable: users,
            isEnabled: () => users.value.isNotEmpty,
            builder: (enabled) => OutlinedButton.icon(
              onPressed: enabled ? onUpdateUser : null,
              icon: const Icon(Icons.edit),
              label: const Text('Rename user'),
            ),
          ),
          _EnabledGate(
            listenable: users,
            isEnabled: () => users.value.isNotEmpty,
            builder: (enabled) => OutlinedButton.icon(
              onPressed: enabled ? onDeleteUser : null,
              icon: const Icon(Icons.person_remove),
              label: const Text('Delete user'),
            ),
          ),
          _EnabledGate(
            listenable: Listenable.merge([users, tasks]),
            isEnabled: () => users.value.isNotEmpty || tasks.value.isNotEmpty,
            builder: (enabled) => TextButton.icon(
              onPressed: enabled ? onClear : null,
              icon: const Icon(Icons.delete_sweep),
              label: const Text('Clear all'),
            ),
          ),
        ],
      ),
    );
  }
}

/// Rebuilds [builder] whenever [listenable] fires, passing the current
/// [isEnabled] result. Keeps each button's rebuild scope to just itself.
class _EnabledGate extends StatelessWidget {
  const _EnabledGate({
    required this.listenable,
    required this.isEnabled,
    required this.builder,
  });

  final Listenable listenable;
  final bool Function() isEnabled;
  final Widget Function(bool enabled) builder;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: listenable,
      builder: (context, _) => builder(isEnabled()),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  const _SectionHeader(this.title);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
      child: Text(
        title,
        style: Theme.of(context).textTheme.titleSmall?.copyWith(
              color: Theme.of(context).colorScheme.primary,
              fontWeight: FontWeight.bold,
            ),
      ),
    );
  }
}
4
likes
160
points
140
downloads

Documentation

API reference

Publisher

verified publishernemr.dev

Weekly Downloads

Sqflite live server for viewing db data on web-based local server.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter, mime, path, path_provider, sqflite

More

Packages that depend on sqflite_live