Unverified Commit 389a12f4 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added widgets/AppModel (#92297)

parent a4b51233
// 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 AppModel
import 'package:flutter/material.dart';
class ShowAppModelValue extends StatelessWidget {
const ShowAppModelValue({ Key? key, required this.appModelKey }) : super(key: key);
final String appModelKey;
@override
Widget build(BuildContext context) {
// The AppModel.getValue() call here causes this widget to depend
// on the value of the AppModel's 'foo' key. If it's changed, with
// AppModel.setValue(), then this widget will be rebuilt.
final String value = AppModel.getValue<String, String>(context, appModelKey, () => 'initial');
return Text('$appModelKey: $value');
}
}
// Demonstrates that changes to the AppModel _only_ cause the dependent widgets
// to be rebuilt. In this case that's the ShowAppModelValue 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 ShowAppModelValue(appModelKey: 'foo'),
const SizedBox(height: 16),
const ShowAppModelValue(appModelKey: 'bar'),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('change foo'),
onPressed: () {
_fooVersion += 1;
// Changing the AppModel's value for 'foo' causes the widgets that
// depend on 'foo' to be rebuilt.
AppModel.setValue<String, String?>(context, 'foo', 'FOO $_fooVersion'); // note: no setState()
},
),
const SizedBox(height: 16),
ElevatedButton(
child: const Text('change bar'),
onPressed: () {
_barVersion += 1;
AppModel.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 AppModel
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 AppModel.set() causes dependent widgets to be rebuilt.
AppModel.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 AppModel.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()));
}
...@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'actions.dart'; import 'actions.dart';
import 'app_model.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
...@@ -1664,17 +1665,19 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1664,17 +1665,19 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return RootRestorationScope( return RootRestorationScope(
restorationId: widget.restorationScopeId, restorationId: widget.restorationScopeId,
child: Shortcuts( child: AppModel(
debugLabel: '<Default WidgetsApp Shortcuts>', child: Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, debugLabel: '<Default WidgetsApp Shortcuts>',
// DefaultTextEditingShortcuts is nested inside Shortcuts so that it can shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
// fall through to the defaultShortcuts. // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
child: DefaultTextEditingShortcuts( // fall through to the defaultShortcuts.
child: Actions( child: DefaultTextEditingShortcuts(
actions: widget.actions ?? WidgetsApp.defaultActions, child: Actions(
child: FocusTraversalGroup( actions: widget.actions ?? WidgetsApp.defaultActions,
policy: ReadingOrderTraversalPolicy(), child: FocusTraversalGroup(
child: child, policy: ReadingOrderTraversalPolicy(),
child: child,
),
), ),
), ),
), ),
......
// 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 [AppModel.getValue] `init` parameter.
///
/// This callback is used to lazily create the initial value for
/// an [AppModel] keyword.
typedef AppModelInitCallback<T> = T Function();
/// Enables sharing key/value data with its `child` and all of the
/// child's descendants.
///
/// - `AppModel.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.
///
/// - `AppModel.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 AppModel.getValue(context,
/// keyword, initCallback) creates a dependency on the AppModel. When
/// the value of keyword changes with AppModel.setValue(), the widget
/// will be rebuilt. The values managed by the AppModel 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.
///
/// AppModel is not intended to be a substitute for Provider or any of
/// the other general purpose application state systems. AppModel 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 AppModel 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
/// `AppModel`. 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/app_model/app_model.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/app_model/app_model.1.dart **
/// {@end-tool}
class AppModel extends StatefulWidget {
/// Creates a widget based on [InheritedModel] that supports build
/// dependencies qualified by keywords. Descendant widgets create
/// such dependencies with [AppModel.getValue] and they trigger
/// rebuilds with [AppModel.setValue].
///
/// This widget is automatically created by the [WidgetsApp].
const AppModel({ 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() => _AppModelState();
/// Returns the app model's value for `key` and ensures that each
/// time the value of `key` is changed with [AppModel.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, AppModelInitCallback<V> init) {
final _AppModelData? model = InheritedModel.inheritFrom<_AppModelData>(context, aspect: key);
assert(_debugHasAppModel(model, context, 'getValue'));
return model!.appModelState.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 [AppModel.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 [AppModel.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 _AppModelData? model = context.getElementForInheritedWidgetOfExactType<_AppModelData>()?.widget as _AppModelData?;
assert(_debugHasAppModel(model, context, 'setValue'));
model!.appModelState.setValue<K, V>(key, value);
}
static bool _debugHasAppModel(_AppModelData? model, BuildContext context, String methodName) {
assert(() {
if (model == null) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary('No AppModel widget found.'),
ErrorDescription('AppModel.$methodName requires an AppModel widget ancestor.\n'),
context.describeWidget('The specific widget that could not find an AppModel ancestor was'),
context.describeOwnershipChain('The ownership chain for the affected widget is'),
ErrorHint(
'Typically, the AppModel 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 _AppModelState extends State<AppModel> {
late Map<Object, Object?> data = <Object, Object?>{};
@override
Widget build(BuildContext context) {
return _AppModelData(appModelState: this, child: widget.child);
}
V getValue<K extends Object, V>(K key, AppModelInitCallback<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 _AppModelData extends InheritedModel<Object> {
_AppModelData({
Key? key,
required this.appModelState,
required Widget child
}) : data = appModelState.data, super(key: key, child: child);
final _AppModelState appModelState;
final Map<Object, Object?> data;
@override
bool updateShouldNotify(_AppModelData old) {
return data != old.data;
}
@override
bool updateShouldNotifyDependent(_AppModelData old, Set<Object> keys) {
for (final Object key in keys) {
if (data[key] != old.data[key]) {
return true;
}
}
return false;
}
}
...@@ -22,6 +22,7 @@ export 'src/widgets/animated_size.dart'; ...@@ -22,6 +22,7 @@ export 'src/widgets/animated_size.dart';
export 'src/widgets/animated_switcher.dart'; export 'src/widgets/animated_switcher.dart';
export 'src/widgets/annotated_region.dart'; export 'src/widgets/annotated_region.dart';
export 'src/widgets/app.dart'; export 'src/widgets/app.dart';
export 'src/widgets/app_model.dart';
export 'src/widgets/async.dart'; export 'src/widgets/async.dart';
export 'src/widgets/autocomplete.dart'; export 'src/widgets/autocomplete.dart';
export 'src/widgets/autofill.dart'; export 'src/widgets/autofill.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'
' _AppModelData\n'
' AppModel\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('AppModel basics', (WidgetTester tester) async {
int columnBuildCount = 0;
int child1BuildCount = 0;
int child2BuildCount = 0;
late void Function(BuildContext context) setAppModelValue;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: AppModel(
child: Builder(
builder: (BuildContext context) {
columnBuildCount += 1;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setAppModelValue.call(context);
},
child: Column(
children: <Widget>[
Builder(
builder: (BuildContext context) {
child1BuildCount += 1;
return Text(AppModel.getValue<String, String>(context, 'child1Text', () => 'null'));
},
),
Builder(
builder: (BuildContext context) {
child2BuildCount += 1;
return Text(AppModel.getValue<String, String>(context, 'child2Text', () => 'null'));
}
),
],
),
);
},
),
),
),
);
expect(columnBuildCount, 1);
expect(child1BuildCount, 1);
expect(child2BuildCount, 1);
expect(find.text('null').evaluate().length, 2);
// AppModel.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.
setAppModelValue = (BuildContext context) {
AppModel.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);
// AppModel.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.
setAppModelValue = (BuildContext context) {
AppModel.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.
setAppModelValue = (BuildContext context) {
AppModel.setValue<String, String>(context, 'child1Text', 'child1');
AppModel.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..
setAppModelValue = (BuildContext context) {
AppModel.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);
setAppModelValue = (BuildContext context) {
AppModel.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 AppModel ', (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: () {
AppModel.setValue<String, String>(context, 'childText', 'child');
},
child: Center(
child: Builder(
builder: (BuildContext context) {
childBuildCount += 1;
return Text(AppModel.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 AppModel 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;
AppModel.setValue<String, String>(context, 'childText', 'child');
},
child: Center(
child: AppModel(
child: Builder(
builder: (BuildContext context) {
return GestureDetector(
onTap: () {
innerTapCount += 1;
AppModel.setValue<String, String>(context, 'childText', 'child');
},
child: Text(AppModel.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