forked from flutter/devtools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoffline_data.dart
More file actions
237 lines (217 loc) · 8.68 KB
/
offline_data.dart
File metadata and controls
237 lines (217 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Copyright 2023 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
/// @docImport '../framework/screen.dart';
library;
import 'dart:async';
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';
import '../config_specific/import_export/import_export.dart';
import '../framework/routing.dart';
import '../framework/screen_controllers.dart';
import '../globals.dart';
/// Controller that manages offline mode for DevTools.
///
/// This class will be instantiated once and set as a global [offlineDataController]
/// that can be accessed from anywhere in DevTools.
class OfflineDataController {
/// Whether DevTools is in offline mode.
///
/// We consider DevTools to show offline data whenever there is data
/// that was previously saved from DevTools.
///
/// The value of [showingOfflineData] is independent of the DevTools connection
/// status. DevTools can be in offline mode both when connected to an app when
/// disconnected from an app.
ValueListenable<bool> get showingOfflineData => _showingOfflineData;
final _showingOfflineData = ValueNotifier<bool>(false);
/// The current offline data as raw JSON.
///
/// This value is set from [ImportController.importData] when offline data is
/// imported to DevTools.
var offlineDataJson = <String, Object?>{};
/// Stores the [ConnectedApp] instance temporarily while switching between
/// offline and online modes.
///
/// We store this because the `serviceConnection.serviceManager` is a global
/// manager and expects only one connected app. So we swap out the online
/// connected app with the offline app data while in offline mode.
ConnectedApp? previousConnectedApp;
/// Whether DevTools should load offline data for [screenId].
bool shouldLoadOfflineData(String screenId) {
return _showingOfflineData.value &&
offlineDataJson.isNotEmpty &&
offlineDataJson[screenId] != null;
}
void startShowingOfflineData({required ConnectedApp offlineApp}) {
previousConnectedApp = serviceConnection.serviceManager.connectedApp;
serviceConnection.serviceManager.connectedApp = offlineApp;
_showingOfflineData.value = true;
}
void stopShowingOfflineData() {
serviceConnection.serviceManager.connectedApp = previousConnectedApp;
_showingOfflineData.value = false;
offlineDataJson.clear();
previousConnectedApp = null;
}
}
/// Mixin that provides offline support for a DevTools screen controller.
///
/// The [Screen] that is associated with this controller must have
/// [Screen.worksWithOfflineData] set to true in order to enable offline support for the
/// screen.
///
/// Check [OfflineDataController.showingOfflineData] in controller constructor.
/// If it is true, the screen should ignore the connected application and just show
/// the offline data.
///
/// If a screen controller (A) is created for offline mode while another
/// instance of this screen controller (B) exists for interacting
/// with the current DevTools connection, screen controller (B) should
/// continue to work as it normally would in the background. This will
/// ensure that the user can return to what they were looking at
/// previously before entering offline mode to view offline data.
///
/// Example:
///
/// ```dart
/// class MyScreenController with OfflineScreenControllerMixin<MyScreenData> {
/// MyScreenController() {
/// init();
/// }
///
/// void init() {
/// if (offlineDataController.showingOfflineData.value) {
/// await maybeLoadOfflineData(
/// ScreenMetaData.myScreen.id,
/// createData: (json) => MyScreenData.parse(json),
/// shouldLoad: (data) => data.isNotEmpty,
/// loadData: (data) async {
/// // Set up the all the data models and notifiers that feed MyScreen's UI.
/// },
/// );
/// } else {
/// // Do screen initialization for connected application.
/// }
/// }
///
/// // Override the abstract methods from [OfflineScreenControllerMixin].
///
/// @override
/// OfflineScreenData prepareOfflineScreenData() => OfflineScreenData(
/// screenId: ScreenMetaData.myScreen.id,
/// data: {} // The data for this screen as a serializable JSON object.
/// );
/// }
/// ```
///
/// ...
///
/// Then in the DevTools [ScreenMetaData] enum,
/// set `worksWithOfflineData` to `true`.
///
/// ```dart
/// enum ScreenMetaData {
/// ...
/// myScreen(
/// ...
/// worksWithOfflineData: true,
/// ),
/// }
/// ```
mixin OfflineScreenControllerMixin<T>
on DevToolsScreenController, AutoDisposeControllerMixin {
final _exportController = ExportController();
/// Whether this controller is actively loading offline data.
///
/// It is likely that a screen will want to show a loading indicator in place
/// of its normal UI while this value is true.
ValueListenable<bool> get loadingOfflineData => _loadingOfflineData;
final _loadingOfflineData = ValueNotifier<bool>(false);
/// Returns an [OfflineScreenData] object with the data that should be
/// included in the offline data snapshot for this screen.
OfflineScreenData prepareOfflineScreenData();
/// Loads offline data for [screenId] when available, and when the
/// [shouldLoad] condition is met.
///
/// Screen controllers that mix in [OfflineScreenControllerMixin] should call
/// this during their initialization when DevTools is in offline mode,
/// defined by [OfflineDataController.showingOfflineData].
///
/// [loadData] defines how the offline data for this screen should be
/// processed and set.
/// Each screen controller that mixes in [OfflineScreenControllerMixin] is
/// responsible for setting up the data models and feeding the data to the
/// screen for offline viewing - that should occur in this method.
///
/// Returns true if offline data was loaded, false otherwise.
@protected
Future<bool> maybeLoadOfflineData(
String screenId, {
required T Function(Map<String, Object?> json) createData,
required bool Function(T data) shouldLoad,
required FutureOr<void> Function(T data) loadData,
}) async {
if (offlineDataController.shouldLoadOfflineData(screenId)) {
// TODO(kenz): investigate this line of code. Do we need to be creating a
// second copy of the Map from offlineDataController.offlineDataJson or
// can we use it directly to save this `Map.from` call?
final json = Map<String, Object?>.from(
(offlineDataController.offlineDataJson[screenId] as Map)
.cast<String, Object?>(),
);
final screenData = createData(json);
if (shouldLoad(screenData)) {
_loadingOfflineData.value = true;
await loadData(screenData);
_loadingOfflineData.value = false;
return true;
}
}
return false;
}
/// Exports the current screen data to a .json file and downloads the file to
/// the user's Downloads directory.
void exportData() {
final encodedData = _exportController.encode(
prepareOfflineScreenData().toJson(),
);
_exportController.downloadFile(encodedData);
}
/// Prepare the screen's current data for offline viewing after an app
/// disconnect.
///
/// This is in preparation for the user clicking the 'Review History' button
/// from the disconnect screen.
void maybePrepareDataForReviewingHistory() {
final currentScreenData = prepareOfflineScreenData();
// Only store data for the current page. We can change this in the
// future if we support offline imports for more than once screen at a
// time.
if (DevToolsRouterDelegate.currentPage == currentScreenData.screenId) {
final previouslyConnectedApp = offlineDataController.previousConnectedApp;
final offlineData = _exportController.generateDataForExport(
offlineScreenData: currentScreenData.toJson(),
connectedApp: previouslyConnectedApp,
);
offlineDataController.offlineDataJson = offlineData;
}
}
}
/// Stores data for a screen that will be used to create a DevTools data export.
class OfflineScreenData {
OfflineScreenData({required this.screenId, required this.data});
/// The screen id that this data is associated with.
final String screenId;
/// The JSON serializable data for the screen.
///
/// This data will be encoded as JSON and written to a file when data is
/// exported from DevTools. This means that the values in [data] must be
/// primitive types that can be encoded as JSON.
final Map<String, Object?> data;
Map<String, Object?> toJson() => {
DevToolsExportKeys.activeScreenId.name: screenId,
screenId: data,
};
}