Dart Streams and SQLite Data Management

1. Single vs. Broadcast Streams

Single Subscription Streams

  • Allow only one listener at a time.
  • Ideal for sequential data processing (e.g., file reading, API calls).
  • Example: File I/O, HTTP requests.

Broadcast Streams

  • Allow multiple listeners simultaneously.
  • Best for real-time data/event broadcasting (e.g., WebSocket updates, UI events).
  • Example: Button clicks, 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

StreamController: Manages a stream.

Stream: Source of asynchronous data events.

StreamSink: Adds data to 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. StreamBuilder in Flutter

StreamBuilder is a Flutter widget that rebuilds itself when its 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:

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

Code Example

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

void main() async {
  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,
  );

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

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

5. Reading Data from SQLite with sqflite

Steps:

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

Code Example

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

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

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

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

6. Moor: Reactive Persistence Library

Moor simplifies database management in Flutter/Dart. Built on SQLite.

Advantages:

  1. Type safety: Reduces runtime errors.
  2. Reactive updates: Automatic UI rebuilds on data changes.
  3. Query flexibility: Raw SQL and Dart-based queries.
  4. Cross-platform support: Works on Android, iOS, macOS, and 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);
}