Skip to content

Instantly share code, notes, and snippets.

@jifalops
Last active June 30, 2021 21:14
Show Gist options
  • Save jifalops/3909e55a526c05f7e851355ccc956f9e to your computer and use it in GitHub Desktop.
Save jifalops/3909e55a526c05f7e851355ccc956f9e to your computer and use it in GitHub Desktop.
Creating a BLoC for formatted currency, and using it in Flutter.
import 'dart:async';
import 'package:flutter/material.dart';
class StreamHandler<T> extends StreamBuilder<T> {
StreamHandler(
{@required Stream<T> stream,
@required Widget Function(BuildContext context, T data) handler,
T initialData,
Widget waiting})
: super(
stream: stream,
initialData: initialData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return handler(context, snapshot.data);
} else if (snapshot.hasError) {
return new Text('${snapshot.error}');
}
return waiting ?? Center(child: CircularProgressIndicator());
});
}
class FutureHandler<T> extends FutureBuilder<T> {
FutureHandler(
{@required Future<T> future,
@required Widget Function(BuildContext context, T data) handler,
T initialData,
Widget waiting})
: super(
future: future,
initialData: initialData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return handler(context, snapshot.data);
} else if (snapshot.hasError) {
return new Text('${snapshot.error}');
}
return waiting ?? Center(child: CircularProgressIndicator());
});
}
import 'package:flutter/widgets.dart';
abstract class BlocState<T extends StatefulWidget> extends State<T> {
/// Create *BLoC*s and setup [StreamSubscription]s.
void initBloc();
/// Cancel [StreamSubscription]s and dispose of *BLoC*s.
void disposeBloc();
@override
@mustCallSuper
@protected
void initState() {
super.initState();
initBloc();
}
@override
@mustCallSuper
@protected
void dispose() {
disposeBloc();
super.dispose();
}
@override
@mustCallSuper
@protected
void didUpdateWidget(StatefulWidget oldWidget) {
super.didUpdateWidget(oldWidget);
disposeBloc();
initBloc();
}
}
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:intl/intl.dart';
import 'package:locales/locales.dart';
import 'package:locales/currency_codes.dart';
class LocalCurrency {
const LocalCurrency(this.locale, this.code);
final Locale locale;
final CurrencyCode code;
}
/// Emits currency strings according to a locale.
class CurrencyBloc {
// Inputs.
final _valueController = StreamController<double>();
final _currencyController = StreamController<LocalCurrency>();
// Outputs.
final _currency = BehaviorSubject<String>();
/// The last formatted currency value emitted from the output stream.
String lastCurrency;
// For synchronously receiving the latest inputs.
double _value;
NumberFormat _formatter;
CurrencyBloc({LocalCurrency initialCurrency, double initialValue}) {
_valueController.stream
.distinct()
.listen((value) => _updateCurrency(value: value));
_currencyController.stream
.distinct()
.listen((currency) => _updateCurrency(currency: currency));
// Initialize inputs.
locale.add(initialCurrency ??
LocalCurrency(Locale.en_US, CurrencyCode.usd));
value.add(initialValue ?? 0.0);
}
void dispose() {
_valueController.close();
_currencyController.close();
_currency.close();
}
_updateCurrency({double value, LocalCurrency currency}) {
if (currency != null) {
_formatter = NumberFormat.simpleCurrency(
locale: '${currency.locale}',
name: '${currency.code}',
decimalDigits: 2);
}
if (value != null) {
_value = value;
}
if (_value != null && _formatter != null) {
lastCurrency = _formatter.format(_value);
_currency.add(lastCurrency);
}
}
/// Change the current [Locale] and/or [CurrencyCode].
Sink<LocalCurrency> get locale => _currencyController.sink;
/// Change the the value to be formatted.
Sink<double> get value => _valueController.sink;
/// Formatted currency.
Stream<String> get currency => _currency.stream;
}
import 'package:flutter/widgets.dart';
import '...util/bloc_state.dart';
import '...util/async_handlers.dart';
import '...bloc/currency_bloc.dart';
export '...bloc/currency_bloc.dart';
/// Implements the Flutter provider pattern.
class CurrencyProvider extends InheritedWidget {
CurrencyProvider({Key key, @required this.bloc, @required Widget child})
: super(key: key, child: child);
final CurrencyBloc bloc;
@override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static CurrencyBloc of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(CurrencyProvider) as CurrencyProvider)
.bloc;
}
/// Introduces the [CurrencyProvider] widget and manages its corresponding
/// [BlocState].
class CurrencyProviderRoot extends StatefulWidget {
CurrencyProviderRoot({Key key, @required this.child}) : super(key: key);
final Widget child;
_CurrencyProviderRootState createState() => _CurrencyProviderRootState();
}
class _CurrencyProviderRootState extends BlocState<CurrencyProviderRoot> {
CurrencyBloc bloc;
@override
Widget build(BuildContext context) =>
CurrencyProvider(bloc: bloc, child: widget.child);
@override
void initBloc() => bloc = CurrencyBloc();
@override
void disposeBloc() => bloc.dispose();
}
/// Wraps the [CurrencyBloc]'s output stream and actually shows the text.
class CurrencyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = CurrencyProvider.of(context);
return StreamHandler(
stream: bloc.currency,
initialData: bloc.lastCurrency,
handler: (context, currency) => Text(currency),
);
}
}
...
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
title: 'My App',
home: CurrencyProviderRoot(
child: MyHomePage(),
));
}
class MyHomePage extends StatelessWidget {
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
TextField(controller: controller),
CurrencyWidget(),
FlatButton(
child: Text('Format Currency'),
onPressed: () => CurrencyProvider.of(context)
.value
.add(double.tryParse(controller.text)),
)
],
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment