Dart Streams and SQLite Database Management in Flutter

1. Single Subscription vs. Broadcast Streams

Single Subscription Streams

  • Allows only one listener at a time.
  • Ideal for sequential or one-time data processing, such as reading a file or fetching data from an API.
  • Example: File I/O operations or HTTP requests.

Broadcast Streams

  • Allows multiple listeners simultaneously.
  • Best for scenarios involving real-time data or event broadcasting, such as WebSocket updates or UI events.
  • Example: Button clicks or live data updates.

Code Example:

import 'dart:async';

void main() {
  // Single subscription stream
  StreamController<String> singleController = StreamController();
  singleController.stream.listen((data) => print('Single subscription: $data'));
  singleController.add('Data 1');
  singleController.close();

  // Broadcast stream
  StreamController<String> broadcastController = StreamController.broadcast();
  broadcastController.stream.listen((data) => print('Listener 1: $data'));
  broadcastController.stream.listen((data) => print('Listener 2: $data'));
  broadcastController.add('Data 2');
  broadcastController.close();
}

2. StreamController, Stream, and StreamSink Usage

StreamController: A controller for creating and managing a stream.

Stream: A source of asynchronous data events.

StreamSink: Allows adding data into the stream.

Code Example:

import 'dart:async';

void main() {
  // Create a StreamController
  final StreamController<int> controller = StreamController<int>();

  // Access the stream and sink
  Stream<int> stream = controller.stream;
  StreamSink<int> sink = controller.sink;

  // Listen to the stream
  stream.listen((data) => print('Stream data: $data'));

  // Add data to the sink
  sink.add(1);
  sink.add(2);

  // Close the controller
  controller.close();
}

3. Understanding StreamBuilder

StreamBuilder is a Flutter widget that listens to a Stream and rebuilds itself whenever the stream’s data changes.

Example Usage:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('StreamBuilder Example')),
        body: CounterStreamBuilder(),
      ),
    );
  }
}

class CounterStreamBuilder extends StatelessWidget {
  final Stream<int> counterStream = (() {
    StreamController<int> controller = StreamController<int>();
    int counter = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      if (counter == 5) {
        timer.cancel();
        controller.close();
      } else {
        controller.add(counter++);
      }
    });
    return controller.stream;
  })();

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: counterStream,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError) {
          return Center(child: Text('Error: ${snapshot.error}'));
        } else if (!snapshot.hasData) {
          return Center(child: Text('No data'));
        } else {
          return Center(child: Text('Counter: ${snapshot.data}'));
        }
      },
    );
  }
}

4. Storing Data in SQLite with sqflite

Steps to Store Data:

  1. Add sqflite and path dependencies in pubspec.yaml.
  2. Create a database and a table.
  3. Use insert method to store data.

Code Example:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

void main() async {
  // Initialize the database
  final database = openDatabase(
    join(await getDatabasesPath(), 'example.db'),
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)',
      );
    },
    version: 1,
  );

  // Insert data
  Future<void> insertUser(String name) async {
    final db = await database;
    await db.insert(
      'users',
      {'name': name},
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  // Example
  await insertUser('John Doe');
  print('Data inserted!');
}

5. Reading Data from SQLite with sqflite

Steps to Read Data:

  1. Open the database.
  2. Use the query method to fetch data.

Code Example:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

void main() async {
  // Initialize the database
  final database = openDatabase(
    join(await getDatabasesPath(), 'example.db'),
  );

  // Fetch data
  Future<List<Map<String, dynamic>>> fetchUsers() async {
    final db = await database;
    return await db.query('users');
  }

  // Example
  final users = await fetchUsers();
  print('Fetched Users: $users');
}

6. Moor: Reactive Persistence Library

Moor:

Moor is a powerful, reactive persistence library for Flutter and Dart that simplifies database management. It is built on top of SQLite.

Advantages:

  1. Type Safety: Moor generates strongly-typed code, reducing runtime errors.
  2. Reactive Updates: Automatically rebuilds UI when the database data changes.
  3. Query Flexibility: Supports both raw SQL and Dart-based queries.
  4. Cross-Platform Support: Works seamlessly on Android, iOS, macOS, and the web.

Example:

// Include `moor` and `moor_flutter` in pubspec.yaml
import 'package:moor_flutter/moor_flutter.dart';

part 'database.g.dart';

@DataClassName('User')
class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text()();
}

@UseMoor(tables: [Users])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(FlutterQueryExecutor.inDatabaseFolder(path: 'db.sqlite'));

  @override
  int get schemaVersion => 1;

  Future<List<User>> getAllUsers() => select(users).get();
  Future insertUser(User user) => into(users).insert(user);
}