blob: 2ac38647839a488aaa71a68c467d720623d6e9ab [file]
// Copyright 2020 The Chromium 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 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'common_widgets.dart';
import 'globals.dart';
import 'screen.dart';
import 'theme.dart';
import 'utils.dart';
const _runInProfileModeDocsUrl =
'https://flutter.dev/docs/testing/ui-performance#run-in-profile-mode';
const _profileGranularityDocsUrl =
'https://flutter.dev/docs/development/tools/devtools/performance#profile-granularity';
class BannerMessagesController {
final _messages = <String, ValueNotifier<List<BannerMessage>>>{};
final _dismissedMessageKeys = <Key>{};
void addMessage(BannerMessage message) {
// We push the banner message in a post frame callback because otherwise,
// we'd be trying to call setState while the parent widget `BannerMessages`
// is in the middle of `build`.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isMessageDismissed(message) || isMessageVisible(message)) return;
final messages = _messagesForScreen(message.screenId);
messages.value.add(message);
// TODO(kenz): we should make a ListValueNotifier class that handles
// notifying listeners so we don't have to make an illegal call to a
// protected method.
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
messages.notifyListeners();
});
}
void removeMessage(BannerMessage message, {bool dismiss = false}) {
// We push the banner message in a post frame callback because otherwise,
// we'd be trying to call setState while the parent widget `BannerMessages`
// is in the middle of `build`.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (dismiss) {
assert(!_dismissedMessageKeys.contains(message.key));
_dismissedMessageKeys.add(message.key);
}
final messages = _messagesForScreen(message.screenId);
messages.value.remove(message);
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
messages.notifyListeners();
});
}
void removeMessageByKey(Key key, String screenId) {
final currentMessages = _messagesForScreen(screenId);
final messageWithKey = currentMessages.value.firstWhere(
(m) => m.key == key,
orElse: () => null,
);
if (messageWithKey != null) {
removeMessage(messageWithKey);
}
}
@visibleForTesting
bool isMessageDismissed(BannerMessage message) {
return _dismissedMessageKeys.contains(message.key);
}
@visibleForTesting
bool isMessageVisible(BannerMessage message) {
return _messagesForScreen(message.screenId)
.value
.where((m) => m.key == message.key)
.isNotEmpty;
}
ValueNotifier<List<BannerMessage>> _messagesForScreen(String screenId) {
return _messages.putIfAbsent(
screenId, () => ValueNotifier<List<BannerMessage>>([]));
}
ValueListenable<List<BannerMessage>> messagesForScreen(String screenId) {
return _messagesForScreen(screenId);
}
}
class BannerMessages extends StatelessWidget {
const BannerMessages({Key key, @required this.screen}) : super(key: key);
final Screen screen;
// TODO(kenz): use an AnimatedList for message changes.
@override
Widget build(BuildContext context) {
final controller = Provider.of<BannerMessagesController>(context);
final messagesForScreen = controller?.messagesForScreen(screen.screenId);
return Column(
children: [
if (messagesForScreen != null)
ValueListenableBuilder<List<BannerMessage>>(
valueListenable: messagesForScreen,
builder: (context, messages, _) {
return Column(
children: messages,
);
},
),
Expanded(
child: screen.build(context),
)
],
);
}
}
class BannerMessage extends StatelessWidget {
const BannerMessage({
@required Key key,
@required this.textSpans,
@required this.backgroundColor,
@required this.foregroundColor,
@required this.screenId,
@required this.headerText,
}) : super(key: key);
final List<TextSpan> textSpans;
final Color backgroundColor;
final Color foregroundColor;
final String screenId;
final String headerText;
@override
Widget build(BuildContext context) {
return Card(
color: backgroundColor,
margin: const EdgeInsets.only(bottom: denseRowSpacing),
child: Padding(
padding: const EdgeInsets.all(defaultSpacing),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
headerText,
style: Theme.of(context)
.textTheme
.headline6
.copyWith(color: foregroundColor),
),
CircularIconButton(
icon: Icons.close,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
// TODO(kenz): animate the removal of this message.
onPressed: () => Provider.of<BannerMessagesController>(
context,
listen: false)
.removeMessage(this, dismiss: true),
),
],
),
const SizedBox(height: defaultSpacing),
RichText(
text: TextSpan(
children: textSpans,
),
),
],
),
),
);
}
}
class _BannerError extends BannerMessage {
const _BannerError({
@required Key key,
@required List<TextSpan> textSpans,
@required String screenId,
}) : super(
key: key,
textSpans: textSpans,
backgroundColor: devtoolsError,
foregroundColor: foreground,
screenId: screenId,
headerText: 'ERROR',
);
static const foreground = Colors.white;
static const linkColor = Color(0xFF54C1EF);
}
// TODO(kenz): add "Do not show this again" option to warnings.
class _BannerWarning extends BannerMessage {
const _BannerWarning({
@required Key key,
@required List<TextSpan> textSpans,
@required String screenId,
}) : super(
key: key,
textSpans: textSpans,
backgroundColor: devtoolsWarning,
foregroundColor: foreground,
screenId: screenId,
headerText: 'WARNING',
);
static const foreground = Colors.black87;
static const linkColor = Color(0xFF54C1EF);
}
class DebugModePerformanceMessage {
const DebugModePerformanceMessage(this.screenId);
final String screenId;
Widget build(BuildContext context) {
return _BannerError(
key: Key('DebugModePerformanceMessage - $screenId'),
textSpans: [
const TextSpan(
text: '''
You are running your app in debug mode. Debug mode performance is not indicative of release performance.
Relaunch your application with the '--profile' argument, or ''',
style: TextStyle(color: _BannerError.foreground),
),
TextSpan(
text: 'relaunch in profile mode from VS Code or IntelliJ',
style: const TextStyle(
decoration: TextDecoration.underline,
color: _BannerError.linkColor,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
await launchUrl(_runInProfileModeDocsUrl, context);
},
),
const TextSpan(
text: '.',
style: TextStyle(color: _BannerError.foreground),
),
],
screenId: screenId,
);
}
}
class ProviderUnknownErrorBanner {
const ProviderUnknownErrorBanner({@required this.screenId});
final String screenId;
BannerMessage build(BuildContext context) {
return _BannerError(
key: Key('ProviderUnknownErrorBanner - $screenId'),
screenId: screenId,
textSpans: const [
TextSpan(
text: '''
The devtool failed to connect with package:provider.
This could be caused by an outdated version of package:provider. Make sure that you are using a version >=5.0.0.''',
style: TextStyle(color: _BannerError.foreground),
),
],
);
}
}
class HighProfileGranularityMessage {
HighProfileGranularityMessage(this.screenId)
: key = Key('HighProfileGranularityMessage - $screenId');
final Key key;
final String screenId;
Widget build(BuildContext context) {
return _BannerWarning(
key: key,
textSpans: [
const TextSpan(
text: '''
You are opting in to a high CPU sampling rate. This may affect the performance of your application. Please read our ''',
style: TextStyle(color: _BannerWarning.foreground),
),
TextSpan(
text: 'documentation',
style: const TextStyle(
decoration: TextDecoration.underline,
color: _BannerWarning.linkColor,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
await launchUrl(_profileGranularityDocsUrl, context);
},
),
const TextSpan(
text: ' to understand the trade-offs associated with this setting.',
style: TextStyle(color: _BannerWarning.foreground),
),
],
screenId: screenId,
);
}
}
class DebugModeMemoryMessage {
const DebugModeMemoryMessage(this.screenId);
final String screenId;
BannerMessage build(BuildContext context) {
return _BannerWarning(
key: Key('DebugModeMemoryMessage - $screenId'),
textSpans: [
const TextSpan(
text: '''
You are running your app in debug mode. Absolute memory usage may be higher in a debug build than in a release build.
For the most accurate absolute memory stats, relaunch your application with the '--profile' argument, or ''',
style: TextStyle(color: _BannerWarning.foreground),
),
TextSpan(
text: 'relaunch in profile mode from VS Code or IntelliJ',
style: const TextStyle(
decoration: TextDecoration.underline,
color: _BannerWarning.linkColor,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
await launchUrl(_runInProfileModeDocsUrl, context);
},
),
const TextSpan(
text: '.',
style: TextStyle(color: _BannerWarning.foreground),
),
],
screenId: screenId,
);
}
}
void maybePushDebugModePerformanceMessage(
BuildContext context,
String screenId,
) {
if (offlineMode) return;
if (serviceManager.connectedApp?.isDebugFlutterAppNow ?? false) {
Provider.of<BannerMessagesController>(context)
.addMessage(DebugModePerformanceMessage(screenId).build(context));
}
}
void maybePushDebugModeMemoryMessage(
BuildContext context,
String screenId,
) {
if (offlineMode) return;
if (serviceManager.connectedApp?.isDebugFlutterAppNow ?? false) {
Provider.of<BannerMessagesController>(context)
.addMessage(DebugModeMemoryMessage(screenId).build(context));
}
}