blob: f45882f7d3ace262d20b523227f9ad5b1d755976 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'store.dart';
/// Signature for a function the lets the caller listen to a store.
typedef Store ListenToStore(StoreToken token, [ValueChanged<Store> onStoreChanged]);
/// A widget that rebuilds when the [Store]s it is listening to change.
abstract class StoreWatcher extends StatefulWidget {
/// Creates a widget that watches stores.
StoreWatcher({ Key key }) : super(key: key);
/// Override this function to build widgets that depend on the current value
/// of the store.
@protected
Widget build(BuildContext context, Map<StoreToken, Store> stores);
/// Override this function to configure which stores to listen to.
///
/// This function is called by [StoreWatcherState] during its
/// [State.initState] lifecycle callback, which means it is called once per
/// inflation of the widget. As a result, the set of stores you listen to
/// should not depend on any constructor parameters for this object because
/// if the parent rebuilds and supplies new constructor arguments, this
/// function will not be called again.
@protected
void initStores(ListenToStore listenToStore);
@override
StoreWatcherState createState() => new StoreWatcherState();
}
/// State for a [StoreWatcher] widget.
class StoreWatcherState extends State<StoreWatcher> with StoreWatcherMixin<StoreWatcher> {
final Map<StoreToken, Store> _storeTokens = <StoreToken, Store>{};
@override
void initState() {
widget.initStores(listenToStore);
super.initState();
}
/// Start receiving notifications from the given store, optionally routed
/// to the given function.
///
/// The default action is to call setState(). In general, you want to use the
/// default function, which rebuilds everything, and let the framework figure
/// out the delta of what changed.
@override
Store listenToStore(StoreToken token, [ValueChanged<Store> onStoreChanged]) {
final Store store = super.listenToStore(token, onStoreChanged);
_storeTokens[token] = store;
return store;
}
@override
Widget build(BuildContext context) {
return widget.build(context, _storeTokens);
}
}
/// Listens to changes in a number of different stores.
///
/// Used by [StoreWatcher] to track which stores the widget is listening to.
mixin StoreWatcherMixin<T extends StatefulWidget> on State<T>{
final Map<Store, StreamSubscription<Store>> _streamSubscriptions = <Store, StreamSubscription<Store>>{};
/// Start receiving notifications from the given store, optionally routed
/// to the given function.
///
/// By default, [onStoreChanged] will be called when the store changes.
@protected
Store listenToStore(StoreToken token, [ValueChanged<Store> onStoreChanged]) {
final Store store = token._value;
_streamSubscriptions[store] = store.listen(onStoreChanged ?? _handleStoreChanged);
return store;
}
/// Stop receiving notifications from the given store.
@protected
void unlistenFromStore(Store store) {
_streamSubscriptions[store]?.cancel();
_streamSubscriptions.remove(store);
}
/// Cancel all store subscriptions.
@override
void dispose() {
final Iterable<StreamSubscription<Store>> subscriptions =
_streamSubscriptions.values;
for (final StreamSubscription<Store> subscription in subscriptions)
subscription.cancel();
_streamSubscriptions.clear();
super.dispose();
}
void _handleStoreChanged(Store store) {
// TODO(abarth): We cancel our subscriptions in [dispose], which means we
// shouldn't receive this callback when we're not mounted. If that's the
// case, we should change this check into an assert that we are mounted.
if (!mounted)
return;
setState(() { });
}
}
/// Represent a store so it can be returned by [StoreListener.listenToStore].
///
/// Used to make sure that callers never reference the store without calling
/// listen() first. In the example below, _itemStore would not be globally
/// available:
///
/// ```dart
/// final _itemStore = new AppStore(actions);
/// final itemStoreToken = new StoreToken(_itemStore);
/// ```
class StoreToken {
/// Creates a store token for the given store.
StoreToken(this._value);
final Store _value;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final StoreToken typedOther = other;
return identical(_value, typedOther._value);
}
@override
int get hashCode => identityHashCode(_value);
@override
String toString() => '[${_value.runtimeType}(${_value.hashCode})]';
}