4

In google IO 18, the Flutter presenters have showed a feature but have not showed how to implement this. The video (at exact time) is: https://youtu.be/RS36gBEp8OI?t=1776

How to implement such thing? How can I properly make the Stream to be correctly formatted based on a Sink?

(sorry but I am not too familiar with Rx)

Daniel Oliveira
  • 8,053
  • 11
  • 40
  • 76

2 Answers2

2

Use the combineLatest function from the rxdart package. It takes the latest values of input streams, so any time either the locale or cart items change it will calculate and format the total cost.

import 'dart:async'; // Sink, Stream
import 'dart:ui'; // Locale
import 'package:rxdart/rxdart.dart'; // Observable, *Subject

class Bloc {
  var _locale = BehaviorSubject<Locale>(seedValue: Locale('en', 'US'));
  var _items = BehaviorSubject<List<CartItem>>(seedValue: []);
  Stream<String> _totalCost;

  Sink<Locale> get locale => _locale.sink;
  Stream<List<CartItem>> get items => _items.stream;
  Stream<String> get totalCost => _totalCost;

  Bloc() {
    _totalCost = Observable.combineLatest2<Locale, List<CartItem>, String>(
        _locale, _items, (locale, items) {
      // TODO calculate total price of items and format based on locale
      return 'USD 10.00';
    }).asBroadcastStream();
  }

  void dispose() {
    _locale.close();
    _items.close();
  }
}

Disclaimer: I didn't try to run this code so there might be errors but the basic idea should be solid.

apaatsio
  • 3,073
  • 1
  • 22
  • 18
0

The best candidate for doing this cross-platform is NumberFormat from the intl package. However you still have to pass it a locale string ("en_US") and ISO 4217 currency code ("USD").

After a little digging I couldn't find this information in any Dart package. The NumberFormat class has a private map for looking up a currency symbol ("$") from a currency code, but keys of the map, the currency codes, are inaccessible. So I decided to make a package that makes locale strings and currency codes available.

currency_bloc.dart

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;
  @override toString() => '$code ($locale)';
  @override operator==(o) => o is LocalCurrency && o.locale == locale && o.code == code;
  @override hashCode => toString().hashCode;
}

/// 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;
}

currency_provider.dart (conventional)

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;
}

Example usage

...

class MyHomePage extends StatefulWidget {
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CurrencyBloc bloc;

  @override
  Widget build(BuildContext context) =>
      CurrencyProvider(bloc: bloc, child: CurrencyExample());

  @override
  void initState() {
    super.initState();
    bloc = CurrencyBloc();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget(StatefulWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    bloc.dispose();
    bloc = CurrencyBloc();
  }
}

class CurrencyExample extends StatelessWidget {
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final bloc = CurrencyProvider.of(context);
    return ListView(
      children: <Widget>[
        TextField(controller: controller),
        StreamBuilder(
            stream: bloc.currency,
            initialData: bloc.lastCurrency,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data);
              } else if (snapshot.hasError) {
                return new Text('${snapshot.error}');
              }
              return Center(child: CircularProgressIndicator());
            }),
        FlatButton(
          child: Text('Format Currency'),
          onPressed: () => bloc.value.add(double.tryParse(controller.text)),
        )
      ],
    );
  }
}
Jacob Phillips
  • 8,841
  • 3
  • 51
  • 66