Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,19 @@ class DevToolsAppState extends State<DevToolsApp> {
return _routes ??= {
homeRoute: (_, params, __) {
if (params['uri']?.isNotEmpty ?? false) {
final embed = params['embed'] == 'true';
final page = params['page'];
final tabs = embed && page != null
? _visibleScreens().where((p) => p.screenId == page).toList()
: _visibleScreens();
return Initializer(
url: params['uri'],
allowConnectionScreenOnDisconnect: !embed,
builder: (_) => _providedControllers(
child: DevToolsScaffold(
initialPage: params['page'],
tabs: _visibleScreens(),
embed: embed,
initialPage: page,
tabs: tabs,
actions: [
if (serviceManager.connectedApp.isFlutterAppNow) ...[
HotReloadButton(),
Expand All @@ -151,7 +158,7 @@ class DevToolsAppState extends State<DevToolsApp> {
child: SnapshotScreenBody(args, _screens),
),
);
}
},
};
}

Expand Down
60 changes: 50 additions & 10 deletions packages/devtools_app/lib/src/initializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'app.dart';
import 'auto_dispose_mixin.dart';
import 'framework/framework_core.dart';
import 'globals.dart';
Expand All @@ -24,8 +25,12 @@ import 'url_utils.dart';
/// connected. As we require additional services to be available, add them
/// here.
class Initializer extends StatefulWidget {
const Initializer({Key key, @required this.url, @required this.builder})
: assert(builder != null),
const Initializer({
Key key,
@required this.url,
@required this.builder,
this.allowConnectionScreenOnDisconnect = true,
}) : assert(builder != null),
super(key: key);

/// The builder for the widget's children.
Expand All @@ -38,6 +43,9 @@ class Initializer extends StatefulWidget {
/// If null, the app will navigate to the [ConnectScreen].
final String url;

/// Whether to allow navigating to the connection screen upon disconnect.
final bool allowConnectionScreenOnDisconnect;

@override
_InitializerState createState() => _InitializerState();
}
Expand Down Expand Up @@ -68,9 +76,8 @@ class _InitializerState extends State<Initializer>
// If we become disconnected, attempt to reconnect.
autoDispose(
serviceManager.onStateChange.where((connected) => !connected).listen((_) {
// TODO(https://github.com/flutter/devtools/issues/1285): On losing
// the connection, only provide an option to reconnect; don't
// immediately go to the connection page.
// Try to reconnect (otherwise, will fall back to showing the disconnected
// overlay).
_attemptUrlConnection();
}),
);
Expand All @@ -86,7 +93,7 @@ class _InitializerState extends State<Initializer>

Future<void> _attemptUrlConnection() async {
if (widget.url == null) {
_navigateToConnectPage();
_handleNoConnection();
return;
}

Expand All @@ -99,19 +106,52 @@ class _InitializerState extends State<Initializer>
);

if (!connected) {
_navigateToConnectPage();
_handleNoConnection();
}
}

/// Goes to the connect page if the [service.serviceManager] is not currently connected.
void _navigateToConnectPage() {
/// Shows a "disconnected" overlay if the [service.serviceManager] is not currently connected.
void _handleNoConnection() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_checkLoaded() && ModalRoute.of(context).isCurrent) {
Navigator.of(context).popAndPushNamed('/');
Overlay.of(context).insert(_createDisconnectedOverlay());
}
});
}

OverlayEntry _createDisconnectedOverlay() {
final theme = Theme.of(context);
OverlayEntry overlay;
overlay = OverlayEntry(
builder: (context) => Container(
// TODO(dantup): Change this to a theme colour and ensure it works in both dart/light themes
color: const Color.fromRGBO(128, 128, 128, 0.5),
child: Center(
child: Column(
children: [
const Spacer(),
Text('Disconnected', style: theme.textTheme.headline3),
if (widget.allowConnectionScreenOnDisconnect)
RaisedButton(
onPressed: () {
overlay.remove();
Navigator.of(context).popAndPushNamed(homeRoute);
},
child: const Text('Connect to Another App'))
else
Text(
'Run a new debug session to reconnect',
style: theme.textTheme.bodyText2,
),
const Spacer(),
],
),
),
),
);
return overlay;
}

@override
Widget build(BuildContext context) {
return _checkLoaded() && _dependenciesLoaded
Expand Down
9 changes: 7 additions & 2 deletions packages/devtools_app/lib/src/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class DevToolsScaffold extends StatefulWidget {
@required this.tabs,
this.initialPage,
this.actions,
this.embed = false,
}) : assert(tabs != null),
super(key: key);

Expand Down Expand Up @@ -68,6 +69,9 @@ class DevToolsScaffold extends StatefulWidget {
/// The initial page to render.
final String initialPage;

/// Whether to render the embedded view (without the header).
final bool embed;

/// Actions that it's possible to perform in this Scaffold.
///
/// These will generally be [RegisteredServiceExtensionButton]s.
Expand Down Expand Up @@ -271,13 +275,14 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
// to make sure we are only handling drops from the active scaffold.
handleDrop: _importController.importData,
child: Scaffold(
appBar: _buildAppBar(),
appBar: widget.embed ? null : _buildAppBar(),
body: TabBarView(
physics: defaultTabBarViewPhysics,
controller: _tabController,
children: tabBodies,
),
bottomNavigationBar: _buildStatusLine(context),
bottomNavigationBar:
widget.embed ? null : _buildStatusLine(context),
),
),
),
Expand Down
36 changes: 24 additions & 12 deletions packages/devtools_app/test/initializer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// found in the LICENSE file.

@TestOn('vm')
import 'package:devtools_app/src/globals.dart';
import 'package:devtools_app/src/initializer.dart'
hide ensureInspectorDependencies;
import 'package:devtools_app/src/globals.dart';
import 'package:devtools_app/src/service_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -16,7 +16,6 @@ import 'support/mocks.dart';
void main() {
group('Initializer', () {
MaterialApp app;
const Key connectKey = Key('connect');
const Key initializedKey = Key('initialized');
setUp(() async {
await ensureInspectorDependencies();
Expand All @@ -29,12 +28,8 @@ void main() {
);

app = MaterialApp(
// This test uses a fake route of /init for the initializer but
// in the real app it's loaded based on whether there's a ?uri= on
// the querystring, with / loading the connect dialog.
initialRoute: '/init',
routes: {
'/': (_) => const SizedBox(key: connectKey),
'/init': (_) => Initializer(
url: null,
builder: (_) => const SizedBox(key: initializedKey),
Expand All @@ -43,23 +38,40 @@ void main() {
);
});

testWidgets('navigates back to the connection page when uninitialized',
testWidgets('shows disconnected overlay if not connected',
(WidgetTester tester) async {
setGlobal(
ServiceConnectionManager,
FakeServiceManager(useFakeService: true, hasConnection: false),
);
await tester.pumpWidget(app);
await tester.pumpAndSettle();
expect(find.byKey(connectKey), findsOneWidget);
expect(find.byKey(initializedKey), findsNothing);

await tester.pumpFrames(app, const Duration(milliseconds: 100));
expect(find.text('Disconnected'), findsOneWidget);
});

testWidgets('shows disconnected overlay upon disconnect',
(WidgetTester tester) async {
final serviceManager = FakeServiceManager(useFakeService: true);
setGlobal(ServiceConnectionManager, serviceManager);

// Expect standard connected state.
await tester.pumpFrames(app, const Duration(milliseconds: 100));
expect(find.byKey(initializedKey), findsOneWidget);
expect(find.text('Disconnected'), findsNothing);

// Trigger a disconnect.
serviceManager.changeState(false);

// Expect Disconnected overlay.
await tester.pumpFrames(app, const Duration(milliseconds: 100));
expect(find.text('Disconnected'), findsOneWidget);
});

testWidgets('builds contents when initialized',
(WidgetTester tester) async {
await tester.pumpWidget(app);
await tester.pumpAndSettle();
expect(find.byKey(connectKey), findsNothing);
expect(find.text('Disconnected'), findsNothing);
expect(find.byKey(initializedKey), findsOneWidget);
});
});
Expand Down
13 changes: 10 additions & 3 deletions packages/devtools_app/test/support/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import 'dart:async';

import 'package:devtools_app/src/banner_messages.dart';
import 'package:devtools_app/src/connected_app.dart';
import 'package:devtools_app/src/debugger/debugger_controller.dart';
import 'package:devtools_app/src/banner_messages.dart';
import 'package:devtools_app/src/initializer.dart' as initializer;
import 'package:devtools_app/src/logging/logging_controller.dart';
import 'package:devtools_app/src/memory/memory_controller.dart'
Expand Down Expand Up @@ -68,7 +68,7 @@ class FakeServiceManager extends Fake implements ServiceConnectionManager {
Future<double> getDisplayRefreshRate() async => 60;

@override
final bool hasConnection;
bool hasConnection;

@override
final IsolateManager isolateManager = FakeIsolateManager();
Expand Down Expand Up @@ -108,7 +108,14 @@ class FakeServiceManager extends Fake implements ServiceConnectionManager {
}

@override
Stream<bool> get onStateChange => const Stream.empty();
Stream<bool> get onStateChange => stateChangeStream.stream;

StreamController<bool> stateChangeStream = StreamController();

void changeState(bool value) {
hasConnection = value;
stateChangeStream.add(value);
}
}

class FakeVmService extends Fake implements VmServiceWrapper {
Expand Down