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:
- Add sqflite and path dependencies in pubspec.yaml.
- Create a database and a table.
- 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:
- Open the database.
- 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:
- Type Safety: Moor generates strongly-typed code, reducing runtime errors.
- Reactive Updates: Automatically rebuilds UI when the database data changes.
- Query Flexibility: Supports both raw SQL and Dart-based queries.
- 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);
}