From b8df71fcaffb0aaca02b6b3ba5afe5689d8cefc5 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 15 Sep 2022 09:17:08 -0700 Subject: [PATCH] Add BuildContext.mounted (#111619) --- .../flutter/lib/src/widgets/framework.dart | 38 +++++++++++ .../test/widgets/build_context_test.dart | 63 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/flutter/test/widgets/build_context_test.dart diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index c9db0d382ca..343a5abe13f 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2081,6 +2081,29 @@ typedef ElementVisitor = void Function(Element element); /// /// {@youtube 560 315 https://www.youtube.com/watch?v=rIaaH87z1-g} /// +/// Avoid storing instances of [BuildContext]s because they may become invalid +/// if the widget they are associated with is unmounted from the widget tree. +/// {@template flutter.widgets.BuildContext.asynchronous_gap} +/// If a [BuildContext] is used across an asynchronous gap (i.e. after performing +/// an asynchronous operation), consider checking [mounted] to determine whether +/// the context is still valid before interacting with it: +/// +/// ```dart +/// @override +/// Widget build(BuildContext context) { +/// return OutlinedButton( +/// onPressed: () async { +/// await Future.delayed(const Duration(seconds: 1)); +/// if (context.mounted) { +/// Navigator.of(context).pop(); +/// } +/// }, +/// child: const Text('Delayed pop'), +/// ); +/// } +/// ``` +/// {@endtemplate} +/// /// [BuildContext] objects are actually [Element] objects. The [BuildContext] /// interface is used to discourage direct manipulation of [Element] objects. abstract class BuildContext { @@ -2091,6 +2114,18 @@ abstract class BuildContext { /// managing the rendering pipeline for this context. BuildOwner? get owner; + /// Whether the [Widget] this context is associated with is currently + /// mounted in the widget tree. + /// + /// Accessing the properties of the [BuildContext] or calling any methods on + /// it is only valid while mounted is true. If mounted is false, assertions + /// will trigger. + /// + /// Once unmounted, a given [BuildContext] will never become mounted again. + /// + /// {@macro flutter.widgets.BuildContext.asynchronous_gap} + bool get mounted; + /// Whether the [widget] is currently updating the widget or render tree. /// /// For [StatefulWidget]s and [StatelessWidget]s this flag is true while @@ -3271,6 +3306,9 @@ abstract class Element extends DiagnosticableTree implements BuildContext { Widget get widget => _widget!; Widget? _widget; + @override + bool get mounted => _widget != null; + /// Returns true if the Element is defunct. /// /// This getter always returns false in profile and release builds. diff --git a/packages/flutter/test/widgets/build_context_test.dart b/packages/flutter/test/widgets/build_context_test.dart new file mode 100644 index 00000000000..f6b78aa8b5c --- /dev/null +++ b/packages/flutter/test/widgets/build_context_test.dart @@ -0,0 +1,63 @@ +// 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('StatefulWidget BuildContext.mounted', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget(TestStatefulWidget( + onBuild: (BuildContext context) { + capturedContext = context; + } + )); + expect(capturedContext.mounted, isTrue); + await tester.pumpWidget(Container()); + expect(capturedContext.mounted, isFalse); + }); + + testWidgets('StatelessWidget BuildContext.mounted', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget(TestStatelessWidget( + onBuild: (BuildContext context) { + capturedContext = context; + } + )); + expect(capturedContext.mounted, isTrue); + await tester.pumpWidget(Container()); + expect(capturedContext.mounted, isFalse); + }); +} + +typedef BuildCallback = void Function(BuildContext); + +class TestStatelessWidget extends StatelessWidget { + const TestStatelessWidget({super.key, required this.onBuild}); + + final BuildCallback onBuild; + + @override + Widget build(BuildContext context) { + onBuild(context); + return Container(); + } +} + +class TestStatefulWidget extends StatefulWidget { + const TestStatefulWidget({super.key, required this.onBuild}); + + final BuildCallback onBuild; + + @override + State createState() => _TestStatefulWidgetState(); +} + +class _TestStatefulWidgetState extends State { + @override + Widget build(BuildContext context) { + widget.onBuild(context); + return Container(); + } +}