Skip to content

Instantly share code, notes, and snippets.

@GroovinChip
Last active March 28, 2021 02:39
Show Gist options
  • Save GroovinChip/23564f0c60183350fe3b566fdb69f70b to your computer and use it in GitHub Desktop.
Save GroovinChip/23564f0c60183350fe3b566fdb69f70b to your computer and use it in GitHub Desktop.
My "Vintage bloc" usage style

My use of the classic (or "vintage") BLoC pattern (not to be confused with Felix Angelov's great bloc library) has evolved over the years. I do no use dedicated sinks for input and dedicated streams for output. Rather, I directly use rxdart's excellent BehaviorSubject directly. BehaviorSubject implements ValueStream, which itself implements Stream, so I found that I could reduce boilerplate a lot by doing this. Values can be directly added to, and read from, a BehaviorSubject. I then use provider to pass my services (I don't really think of them as "bloc"s any more) through my app. This gist provides a real example of how I currently use this pattern.

  • prefs_service.dart is where the service is defined.
  • main.dart shows how to initialize the service.
  • app.dart shows how I use MultiProvider to pass down my service. I always use MultiProvider rather than one single provider to allow for more services. I listen to the BehaviorSubject in my service to set the ThemeMode of my MaterialApp.
  • provided.dart shows how I use a mixin as a shortcut to my service.
  • theme_mode_switcher.dart shows how I use the mixin to get the service and update the value of the BehaviorSubject on user selection.

Disclaimer: I do not claim that this is the “right” way to do state management (there’s no such thing) and my style evolves over time. Additionally, there is nothing wrong with Felix's bloc ecosystem. Felix is great, he does amazing work for the Flutter community, and his work is of the highest caliber. I have nothing but respect for him. The discussion over the nature of BLoC pattern vs. his bloc library that pops up every now and then is not a criticism of him or his work. I hope very much that no one confuses it as such.

import 'package:flutter/material.dart';
import 'package:my_app/screens/home_screen.dart';
import 'package:my_app/services/prefs_service.dart';
import 'package:my_app/theme/app_themes.dart';
import 'package:provider/provider.dart';
class MyApp extends StatefulWidget {
const MyApp({
Key? key,
required this.prefsService,
}) : super(key: key);
final PrefsService prefsService;
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<PrefsService>.value(value: widget.prefsService),
],
child: StreamBuilder<ThemeMode?>(
stream: widget.prefsService.themeModeSubject,
initialData: widget.prefsService.currentThemeMode,
builder: (context, snapshot) {
return MaterialApp(
title: 'MyApp',
theme: basicLight,
darkTheme: basicDark,
themeMode: snapshot.data,
home: HomeScreen(),
);
},
),
);
}
}
import 'package:gisthub/services/prefs_service.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefsService = await PrefsService.init();
runApp(
MyApp(
prefsService: prefsService,
),
);
}
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// User preferences service
class PrefsService {
PrefsService._();
// Public initializer
static Future<PrefsService> init() async {
final service = PrefsService._();
await service._init();
return service;
}
// Private initializer
Future<void> _init() async {
preferences = await SharedPreferences.getInstance();
readThemeModePref();
}
late SharedPreferences preferences;
final themeModeSubject = BehaviorSubject<ThemeMode?>();
ThemeMode? get currentThemeMode => themeModeSubject.value;
/// Read user's ThemeMode preference from storage
///
/// Defaults to ThemeMode.system
void readThemeModePref() {
String tm =
preferences.get('themeModePref') as String? ?? 'ThemeMode.system';
ThemeMode themeMode =
ThemeMode.values.firstWhere((element) => element.toString() == tm);
themeModeSubject.add(themeMode);
}
/// Set user's ThemeMode preference
Future<void> setThemeModePref(ThemeMode? themeMode) async {
await preferences.setString('themeModePref', '${themeMode.toString()}');
themeModeSubject.add(themeMode);
}
// Close streams
void close() {
themeModeSubject.close();
}
}
import 'package:flutter/material.dart';
import 'package:my_app/services/prefs_service.dart';
import 'package:provider/provider.dart';
mixin Provided<T extends StatefulWidget> on State<T> {
PrefsService? _prefsService;
PrefsService get prefsService =>
_prefsService ??= Provider.of<PrefsService>(context, listen: false);
}
import 'package:flutter/material.dart';
import 'package:my_app/mixins/provided.dart';
/// A dialog for selecting which ThemeMode to use.
class ThemeSwitcherDialog extends StatefulWidget {
@override
_ThemeSwitcherDialogState createState() => _ThemeSwitcherDialogState();
}
class _ThemeSwitcherDialogState extends State<ThemeSwitcherDialog> with Provided {
void _onThemeSelection(ThemeMode? themeMode) {
prefsService.setThemeModePref(themeMode);
// By calling setState, the app refreshes and the new value is read by the StreamBuilder above MaterialApp.
// The theme of the app will change immediately.
setState(() {});
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return SimpleDialog(
backgroundColor: Theme.of(context).canvasColor,
title: const Text('Change app theme'),
children: [
RadioListTile(
title: const Text('System theme'),
value: ThemeMode.system,
selected: prefsService.currentThemeMode == ThemeMode.system ? true : false,
activeColor: Theme.of(context).accentColor,
groupValue: prefsService.currentThemeMode,
onChanged: _onThemeSelection,
),
RadioListTile(
title: const Text('Light theme'),
value: ThemeMode.light,
selected: prefsService.currentThemeMode == ThemeMode.light ? true : false,
activeColor: Theme.of(context).accentColor,
groupValue: prefsService.currentThemeMode,
onChanged: _onThemeSelection,
),
RadioListTile(
title: const Text('Dark theme'),
value: ThemeMode.dark,
selected: prefsService.currentThemeMode == ThemeMode.dark ? true : false,
activeColor: Theme.of(context).accentColor,
groupValue: prefsService.currentThemeMode,
onChanged: _onThemeSelection,
),
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment