Unverified Commit f4f23ecb authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added SharedAppData to the widgets library (#93175)

parent 6bc33812
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for SharedAppData
import 'package:flutter/material.dart';
class ShowSharedValue extends StatelessWidget {
const ShowSharedValue({ Key? key, required this.appDataKey }) : super(key: key);
final String appDataKey;
@override
Widget build(BuildContext context) {
// The SharedAppData.getValue() call here causes this widget to depend
// on the value of the SharedAppData's 'foo' key. If it's changed, with
// SharedAppData.setValue(), then this widget will be rebuilt.
final String value = SharedAppData.getValue<String, String>(context, appDataKey, () => 'initial');
return Text('$appDataKey: $value');
}
}
// Demonstrates that changes to the SharedAppData _only_ cause the dependent widgets
// to be rebuilt. In this case that's the ShowSharedValue widget that's
// displaying the value of a key whose value has been updated.
class Home extends StatefulWidget {
const Home({ Key? key }) : super(key: key);
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _fooVersion = 0;
int _barVersion = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ShowSharedValue(appDataKey: 'foo'),
const SizedBox(height: 16),
const ShowSharedValue(appDataKey: 'bar'),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('change foo'),
onPressed: () {
_fooVersion += 1;
// Changing the SharedAppData's value for 'foo' causes the widgets that
// depend on 'foo' to be rebuilt.
SharedAppData.setValue<String, String?>(context, 'foo', 'FOO $_fooVersion'); // note: no setState()
},
),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('change bar'),
onPressed: () {
_barVersion += 1;
SharedAppData.setValue<String, String?>(context, 'bar', 'BAR $_barVersion'); // note: no setState()
},
),
],
),
),
);
}
}
void main() {
runApp(const MaterialApp(home: Home()));
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for SharedAppData
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// A single lazily constructed object that's shared with the entire
// application via `SharedObject.of(context)`. The value of the object
// can be changed with `SharedObject.reset(context)`. Resetting the value
// will cause all of the widgets that depend on it to be rebuilt.
class SharedObject {
SharedObject._();
static final Object _sharedObjectKey = Object();
@override
String toString() => describeIdentity(this);
static void reset(BuildContext context) {
// Calling SharedAppData.setValue() causes dependent widgets to be rebuilt.
SharedAppData.setValue<Object, SharedObject>(context, _sharedObjectKey, SharedObject._());
}
static SharedObject of(BuildContext context) {
// If a value for _sharedObjectKey has never been set then the third
// callback parameter is used to generate an initial value.
return SharedAppData.getValue<Object, SharedObject>(context, _sharedObjectKey, () => SharedObject._());
}
}
// An example of a widget which depends on the SharedObject's value,
// which might be provided - along with SharedObject - in a Dart package.
class CustomWidget extends StatelessWidget {
const CustomWidget({ Key? key }) : super(key: key);
@override
Widget build(BuildContext context) {
// Will be rebuilt if the shared object's value is changed.
return ElevatedButton(
child: Text('Replace ${SharedObject.of(context)}'),
onPressed: () {
SharedObject.reset(context);
},
);
}
}
class Home extends StatelessWidget {
const Home({ Key? key }) : super(key: key);
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CustomWidget()
),
);
}
}
void main() {
runApp(const MaterialApp(home: Home()));
}
...@@ -24,6 +24,7 @@ import 'restoration.dart'; ...@@ -24,6 +24,7 @@ import 'restoration.dart';
import 'router.dart'; import 'router.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'semantics_debugger.dart'; import 'semantics_debugger.dart';
import 'shared_app_data.dart';
import 'shortcuts.dart'; import 'shortcuts.dart';
import 'text.dart'; import 'text.dart';
import 'title.dart'; import 'title.dart';
...@@ -1664,6 +1665,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1664,6 +1665,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return RootRestorationScope( return RootRestorationScope(
restorationId: widget.restorationScopeId, restorationId: widget.restorationScopeId,
child: SharedAppData(
child: Shortcuts( child: Shortcuts(
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
...@@ -1679,6 +1681,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1679,6 +1681,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
), ),
), ),
), ),
),
); );
} }
} }
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'inherited_model.dart';
/// The type of the [SharedAppData.getValue] `init` parameter.
///
/// This callback is used to lazily create the initial value for
/// a [SharedAppData] keyword.
typedef SharedAppDataInitCallback<T> = T Function();
/// Enables sharing key/value data with its `child` and all of the
/// child's descendants.
///
/// - `SharedAppData.getValue(context, key, initCallback)` creates a dependency
/// on the key and returns the value for the key from the shared data table.
/// If no value exists for key then the initCallback is used to create
/// the initial value.
///
/// - `SharedAppData.setValue(context, key, value)` changes the value of an entry
/// in the shared data table and forces widgets that depend on that entry
/// to be rebuilt.
///
/// A widget whose build method uses SharedAppData.getValue(context,
/// keyword, initCallback) creates a dependency on the SharedAppData. When
/// the value of keyword changes with SharedAppData.setValue(), the widget
/// will be rebuilt. The values managed by the SharedAppData are expected
/// to be immutable: intrinsic changes to values will not cause
/// dependent widgets to be rebuilt.
///
/// An instance of this widget is created automatically by [WidgetsApp].
///
/// There are many ways to share data with a widget subtree. This
/// class is based on [InheritedModel], which is an [InheritedWidget].
/// It's intended to be used by packages that need to share a modest
/// number of values among their own components.
///
/// SharedAppData is not intended to be a substitute for Provider or any of
/// the other general purpose application state systems. SharedAppData is
/// for situations where a package's custom widgets need to share one
/// or a handful of immutable data objects that can be lazily
/// initialized. It exists so that packages like that can deliver
/// custom widgets without requiring the developer to add a
/// package-specific umbrella widget to their application.
///
/// A good way to create an SharedAppData key that avoids potential
/// collisions with other packages is to use a static `Object()` value.
/// The `SharedObject` example below does this.
///
/// {@tool dartpad}
/// The following sample demonstrates using the automatically created
/// `SharedAppData`. Button presses cause changes to the values for keys
/// 'foo', and 'bar', and those changes only cause the widgets that
/// depend on those keys to be rebuilt.
///
/// ** See code in examples/api/lib/widgets/shared_app_data/shared_app_data.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// The following sample demonstrates how a single lazily computed
/// value could be shared within an app. A Flutter package that
/// provided custom widgets might use this approach to share a (possibly
/// private) value with instances of those widgets.
///
/// ** See code in examples/api/lib/widgets/shared_app_data/shared_app_data.1.dart **
/// {@end-tool}
class SharedAppData extends StatefulWidget {
/// Creates a widget based on [InheritedModel] that supports build
/// dependencies qualified by keywords. Descendant widgets create
/// such dependencies with [SharedAppData.getValue] and they trigger
/// rebuilds with [SharedAppData.setValue].
///
/// This widget is automatically created by the [WidgetsApp].
const SharedAppData({ Key? key, required this.child }) : super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
@override
State<StatefulWidget> createState() => _SharedAppDataState();
/// Returns the app model's value for `key` and ensures that each
/// time the value of `key` is changed with [SharedAppData.setValue], the
/// specified context will be rebuilt.
///
/// If no value for `key` exists then the `init` callback is used to
/// generate an initial value. The callback is expected to return
/// an immutable value because intrinsic changes to the value will
/// not cause dependent widgets to be rebuilt.
///
/// A widget that depends on the app model's value for `key` should use
/// this method in their `build` methods to ensure that they are rebuilt
/// if the value changes.
///
/// The type parameter `K` is the type of the keyword and `V`
/// is the type of the value.
static V getValue<K extends Object, V>(BuildContext context, K key, SharedAppDataInitCallback<V> init) {
final _SharedAppModel? model = InheritedModel.inheritFrom<_SharedAppModel>(context, aspect: key);
assert(_debugHasSharedAppData(model, context, 'getValue'));
return model!.sharedAppDataState.getValue<K, V>(key, init);
}
/// Changes the app model's `value` for `key` and rebuilds any widgets
/// that have created a dependency on `key` with [SharedAppData.getValue].
///
/// If `value` is `==` to the current value of `key` then nothing
/// is rebuilt.
///
/// The `value` is expected to be immutable because intrinsic
/// changes to the value will not cause dependent widgets to be
/// rebuilt.
///
/// Unlike [SharedAppData.getValue], this method does _not_ create a dependency
/// between `context` and `key`.
///
/// The type parameter `K` is the type of the value's keyword and `V`
/// is the type of the value.
static void setValue<K extends Object, V>(BuildContext context, K key, V value) {
final _SharedAppModel? model = context.getElementForInheritedWidgetOfExactType<_SharedAppModel>()?.widget as _SharedAppModel?;
assert(_debugHasSharedAppData(model, context, 'setValue'));
model!.sharedAppDataState.setValue<K, V>(key, value);
}
static bool _debugHasSharedAppData(_SharedAppModel? model, BuildContext context, String methodName) {
assert(() {
if (model == null) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary('No SharedAppData widget found.'),
ErrorDescription('SharedAppData.$methodName requires an SharedAppData widget ancestor.\n'),
context.describeWidget('The specific widget that could not find an SharedAppData ancestor was'),
context.describeOwnershipChain('The ownership chain for the affected widget is'),
ErrorHint(
'Typically, the SharedAppData widget is introduced by the MaterialApp '
'or WidgetsApp widget at the top of your application widget tree. It '
'provides a key/value map of data that is shared with the entire '
'application.',
),
],
);
}
return true;
}());
return true;
}
}
class _SharedAppDataState extends State<SharedAppData> {
late Map<Object, Object?> data = <Object, Object?>{};
@override
Widget build(BuildContext context) {
return _SharedAppModel(sharedAppDataState: this, child: widget.child);
}
V getValue<K extends Object, V>(K key, SharedAppDataInitCallback<V> init) {
data[key] ??= init();
return data[key] as V;
}
void setValue<K extends Object, V>(K key, V value) {
if (data[key] != value) {
setState(() {
data = Map<Object, Object?>.from(data);
data[key] = value;
});
}
}
}
class _SharedAppModel extends InheritedModel<Object> {
_SharedAppModel({
Key? key,
required this.sharedAppDataState,
required Widget child
}) : data = sharedAppDataState.data, super(key: key, child: child);
final _SharedAppDataState sharedAppDataState;
final Map<Object, Object?> data;
@override
bool updateShouldNotify(_SharedAppModel old) {
return data != old.data;
}
@override
bool updateShouldNotifyDependent(_SharedAppModel old, Set<Object> keys) {
for (final Object key in keys) {
if (data[key] != old.data[key]) {
return true;
}
}
return false;
}
}
...@@ -107,6 +107,7 @@ export 'src/widgets/scroll_view.dart'; ...@@ -107,6 +107,7 @@ export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollbar.dart'; export 'src/widgets/scrollbar.dart';
export 'src/widgets/semantics_debugger.dart'; export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/shared_app_data.dart';
export 'src/widgets/shortcuts.dart'; export 'src/widgets/shortcuts.dart';
export 'src/widgets/single_child_scroll_view.dart'; export 'src/widgets/single_child_scroll_view.dart';
export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/size_changed_layout_notifier.dart';
......
...@@ -200,6 +200,8 @@ void main() { ...@@ -200,6 +200,8 @@ void main() {
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' _SharedAppModel\n'
' SharedAppData\n'
' UnmanagedRestorationScope\n' ' UnmanagedRestorationScope\n'
' RestorationScope\n' ' RestorationScope\n'
' UnmanagedRestorationScope\n' ' UnmanagedRestorationScope\n'
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('SharedAppData basics', (WidgetTester tester) async {
int columnBuildCount = 0;
int child1BuildCount = 0;
int child2BuildCount = 0;
late void Function(BuildContext context) setSharedAppDataValue;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SharedAppData(
child: Builder(
builder: (BuildContext context) {
columnBuildCount += 1;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setSharedAppDataValue.call(context);
},
child: Column(
children: <Widget>[
Builder(
builder: (BuildContext context) {
child1BuildCount += 1;
return Text(SharedAppData.getValue<String, String>(context, 'child1Text', () => 'null'));
},
),
Builder(
builder: (BuildContext context) {
child2BuildCount += 1;
return Text(SharedAppData.getValue<String, String>(context, 'child2Text', () => 'null'));
}
),
],
),
);
},
),
),
),
);
expect(columnBuildCount, 1);
expect(child1BuildCount, 1);
expect(child2BuildCount, 1);
expect(find.text('null').evaluate().length, 2);
// SharedAppData.setValue<String, String>(context, 'child1Text', 'child1')
// causes the first Text widget to be rebuilt with its text to be
// set to 'child1'. Nothing else is rebuilt.
setSharedAppDataValue = (BuildContext context) {
SharedAppData.setValue<String, String>(context, 'child1Text', 'child1');
};
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(columnBuildCount, 1);
expect(child1BuildCount, 2);
expect(child2BuildCount, 1);
expect(find.text('child1'), findsOneWidget);
expect(find.text('null'), findsOneWidget);
// SharedAppData.setValue<String, String>(context, 'child2Text', 'child1')
// causes the second Text widget to be rebuilt with its text to be
// set to 'child2'. Nothing else is rebuilt.
setSharedAppDataValue = (BuildContext context) {
SharedAppData.setValue<String, String>(context, 'child2Text', 'child2');
};
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(columnBuildCount, 1);
expect(child1BuildCount, 2);
expect(child2BuildCount, 2);
expect(find.text('child1'), findsOneWidget);
expect(find.text('child2'), findsOneWidget);
// Resetting a key's value to the same value does not
// cause any widgets to be rebuilt.
setSharedAppDataValue = (BuildContext context) {
SharedAppData.setValue<String, String>(context, 'child1Text', 'child1');
SharedAppData.setValue<String, String>(context, 'child2Text', 'child2');
};
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(columnBuildCount, 1);
expect(child1BuildCount, 2);
expect(child2BuildCount, 2);
// More of the same, resetting the values to null..
setSharedAppDataValue = (BuildContext context) {
SharedAppData.setValue<String, String>(context, 'child1Text', 'null');
};
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(columnBuildCount, 1);
expect(child1BuildCount, 3);
expect(child2BuildCount, 2);
expect(find.text('null'), findsOneWidget);
expect(find.text('child2'), findsOneWidget);
setSharedAppDataValue = (BuildContext context) {
SharedAppData.setValue<String, String>(context, 'child2Text', 'null');
};
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(columnBuildCount, 1);
expect(child1BuildCount, 3);
expect(child2BuildCount, 3);
expect(find.text('null').evaluate().length, 2);
});
testWidgets('WidgetsApp SharedAppData ', (WidgetTester tester) async {
int parentBuildCount = 0;
int childBuildCount = 0;
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xff00ff00),
builder: (BuildContext context, Widget? child) {
parentBuildCount += 1;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
SharedAppData.setValue<String, String>(context, 'childText', 'child');
},
child: Center(
child: Builder(
builder: (BuildContext context) {
childBuildCount += 1;
return Text(SharedAppData.getValue<String, String>(context, 'childText', () => 'null'));
},
),
),
);
},
),
);
expect(find.text('null'), findsOneWidget);
expect(parentBuildCount, 1);
expect(childBuildCount, 1);
await tester.tap(find.byType(GestureDetector));
await tester.pump();
expect(parentBuildCount, 1);
expect(childBuildCount, 2);
expect(find.text('child'), findsOneWidget);
});
testWidgets('WidgetsApp SharedAppData Shadowing', (WidgetTester tester) async {
int innerTapCount = 0;
int outerTapCount = 0;
await tester.pumpWidget(
WidgetsApp(
color: const Color(0xff00ff00),
builder: (BuildContext context, Widget? child) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
outerTapCount += 1;
SharedAppData.setValue<String, String>(context, 'childText', 'child');
},
child: Center(
child: SharedAppData(
child: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
innerTapCount += 1;
SharedAppData.setValue<String, String>(context, 'childText', 'child');
},
child: Text(SharedAppData.getValue<String, String>(context, 'childText', () => 'null')),
);
},
),
),
),
);
},
),
);
expect(find.text('null'), findsOneWidget);
await tester.tapAt(const Offset(10, 10));
await tester.pump();
expect(outerTapCount, 1);
expect(innerTapCount, 0);
expect(find.text('null'), findsOneWidget);
await tester.tap(find.text('null'));
await tester.pump();
expect(outerTapCount, 1);
expect(innerTapCount, 1);
expect(find.text('child'), findsOneWidget);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment