[roll] Update third-party dart packages

Updated:
Change-Id: Ic14f46d6b8cd9ca12c404729f5ec6ab1b7d8721f
diff --git a/browser_launcher/BUILD.gn b/browser_launcher/BUILD.gn
index 4d25068..d6c36ec 100644
--- a/browser_launcher/BUILD.gn
+++ b/browser_launcher/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for browser_launcher-0.1.2
+# This file is generated by importer.py for browser_launcher-0.1.3
 
 import("//build/dart/dart_library.gni")
 
diff --git a/browser_launcher/CHANGELOG.md b/browser_launcher/CHANGELOG.md
index 9428cd5..6ac0537 100644
--- a/browser_launcher/CHANGELOG.md
+++ b/browser_launcher/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.1.3
+
+- widen the version constraint on `package:webkit_inspection_protocol`
+
 ## 0.1.2
 
 - lower min sdk version to match Flutter stable
diff --git a/browser_launcher/pubspec.yaml b/browser_launcher/pubspec.yaml
index ea8a373..7ea781e 100644
--- a/browser_launcher/pubspec.yaml
+++ b/browser_launcher/pubspec.yaml
@@ -1,7 +1,7 @@
 name: browser_launcher
 description: Provides a standardized way to launch web browsers for testing and tools.
 
-version: 0.1.2
+version: 0.1.3
 
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/browser_launcher
@@ -11,7 +11,7 @@
 
 dependencies:
   path: ^1.6.2
-  webkit_inspection_protocol: ^0.4.0
+  webkit_inspection_protocol: '>=0.4.0 <0.6.0'
 
 dev_dependencies:
   pedantic: ^1.5.0
diff --git a/built_value/BUILD.gn b/built_value/BUILD.gn
index b928ca8..33ad906 100644
--- a/built_value/BUILD.gn
+++ b/built_value/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for built_value-6.7.0
+# This file is generated by importer.py for built_value-6.7.1
 
 import("//build/dart/dart_library.gni")
 
diff --git a/built_value/CHANGELOG.md b/built_value/CHANGELOG.md
index ec1b90c..3d28bb1 100644
--- a/built_value/CHANGELOG.md
+++ b/built_value/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+# 6.7.1
+
+- Fix codegen for custom builders when fields use types that have an import
+  prefix.
+- Bump version of `analyzer_plugin`.
+
 # 6.7.0
 
 - Generate code compatible with 'no raw types'.
diff --git a/built_value/README.md b/built_value/README.md
index 5238780..6ef2c81 100644
--- a/built_value/README.md
+++ b/built_value/README.md
@@ -160,7 +160,7 @@
 It _supports changes to the data model_. Optional fields can be added or
 removed, and fields can be switched from optional to required, allowing your
 data model to evolve without breaking compatbility. Some other libraries break
-compatability on any change to any serializable class.
+compatibility on any change to any serializable class.
 
 It's _modular_. Each endpoint can choose which classes to know about;
 for example, you can have multiple clients that each know about only a subset of
diff --git a/built_value/lib/serializer.dart b/built_value/lib/serializer.dart
index 07a4fc4..f54f95e 100644
--- a/built_value/lib/serializer.dart
+++ b/built_value/lib/serializer.dart
@@ -188,10 +188,12 @@
   /// add your own. For example:
   ///
   /// ```dart
-  /// serializers = (serializers.toBuilder()..addBuilderFactory(
-  ///     const FullType(
-  ///         BuiltList,
-  ///         [FullType(Foo)]), () => new ListBuilder<Foo>)).build();
+  /// serializers = (serializers.toBuilder()
+  ///       ..addBuilderFactory(
+  ///         const FullType(BuiltList, [FullType(Foo)]),
+  ///         () => ListBuilder<Foo>(),
+  ///       ))
+  ///     .build();
   /// ```
   void addBuilderFactory(FullType specifiedType, Function function);
 
diff --git a/built_value/pubspec.yaml b/built_value/pubspec.yaml
index 5667d9e..7320cf1 100644
--- a/built_value/pubspec.yaml
+++ b/built_value/pubspec.yaml
@@ -1,5 +1,5 @@
 name: built_value
-version: 6.7.0
+version: 6.7.1
 description: >
   Value types with builders, Dart classes as enums, and serialization.
   This library is the runtime dependency.
diff --git a/codemirror/.gitignore b/codemirror/.gitignore
index 5094d65..c2e5830 100644
--- a/codemirror/.gitignore
+++ b/codemirror/.gitignore
@@ -18,9 +18,9 @@
 codemirror.dart.iml
 
 # Ignore the codemirror copied resources.
-#lib/addon/
-#lib/codemirror.js
-#lib/css/
-#lib/keymap/
-#lib/mode/
-#lib/theme/
+# lib/addon/
+# lib/codemirror.js
+# lib/css/
+# lib/keymap/
+# lib/mode/
+# lib/theme/
diff --git a/codemirror/BUILD.gn b/codemirror/BUILD.gn
index 51200b6..ee05c07 100644
--- a/codemirror/BUILD.gn
+++ b/codemirror/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for codemirror-0.5.9+5.48.4
+# This file is generated by importer.py for codemirror-0.5.10+5.48.4
 
 import("//build/dart/dart_library.gni")
 
diff --git a/codemirror/changelog.md b/codemirror/changelog.md
index 7634d6c..526e992 100644
--- a/codemirror/changelog.md
+++ b/codemirror/changelog.md
@@ -1,5 +1,8 @@
 # Changelog
 
+## 0.5.10
+- Fix bug in TextMarker.find() method introduced by Dart 2.0.
+
 ## 0.5.9
 - update to CodeMirror 5.48.4
 
diff --git a/codemirror/lib/codemirror.dart b/codemirror/lib/codemirror.dart
index 8fd1cd9..ec04c3c 100644
--- a/codemirror/lib/codemirror.dart
+++ b/codemirror/lib/codemirror.dart
@@ -1120,11 +1120,11 @@
   /// the current position of the marked range, or `null` if the marker is no
   /// longer in the document. For a bookmark, this list will be length 1.
   List<Position> find() {
-    var result = call('find');
+    final result = call('find');
     if (result is! JsObject) return null;
 
     try {
-      if (result is Map) {
+      if (result.hasProperty('from')) {
         return [
           Position.fromProxy(result['from']),
           Position.fromProxy(result['to'])
diff --git a/codemirror/pubspec.yaml b/codemirror/pubspec.yaml
index 0dee7c8..2433051 100644
--- a/codemirror/pubspec.yaml
+++ b/codemirror/pubspec.yaml
@@ -3,7 +3,7 @@
 # license that can be found in the LICENSE file.
 
 name: codemirror
-version: 0.5.9+5.48.4
+version: 0.5.10+5.48.4
 description: A Dart wrapper around the CodeMirror text editor.
 author: Devon Carew <devoncarew@google.com>
 homepage: https://github.com/google/codemirror.dart
diff --git a/devtools/BUILD.gn b/devtools/BUILD.gn
index 737cfa8..5f15814 100644
--- a/devtools/BUILD.gn
+++ b/devtools/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for devtools-0.1.1
+# This file is generated by importer.py for devtools-0.1.6-dev.3
 
 import("//build/dart/dart_library.gni")
 
@@ -20,14 +20,14 @@
     "//third_party/dart-pkg/pub/js",
     "//third_party/dart-pkg/pub/meta",
     "//third_party/dart-pkg/pub/devtools_server",
+    "//third_party/dart-pkg/pub/sse",
     "//third_party/dart-pkg/pub/path",
     "//third_party/dart-pkg/pub/codemirror",
     "//third_party/dart-pkg/pub/platform_detect",
-    "//third_party/dart-pkg/pub/rxdart",
     "//third_party/dart-pkg/pub/plotly_js",
     "//third_party/dart-pkg/pub/split",
-    "//third_party/dart-pkg/pub/vm_service_lib",
     "//third_party/dart-pkg/pub/octicons_css",
     "//third_party/dart-pkg/pub/primer_css",
+    "//third_party/dart-pkg/pub/vm_service",
   ]
 }
diff --git a/devtools/CHANGELOG.md b/devtools/CHANGELOG.md
index 8df94dd..6b3426f 100644
--- a/devtools/CHANGELOG.md
+++ b/devtools/CHANGELOG.md
@@ -1,3 +1,49 @@
+## 0.1.6-dev
+* Add a page to show Flutter SDK version and Dart VM flags details.
+* Various css fixes.
+* CSS polish for cursors, hover, and misc.
+* Use frame time in CPU profile unavailable message.
+* Fixes to our splitter control.
+* Rev to the latest version of `package:vm_service`
+* Remove the dependency on `package:mockito`.
+* Remove the dependency on `package:rxdart`.
+* Support `sse` and `sses` schemes for connection with a running app.
+* Address an npe in the memory page.
+* Polish button collapsing for small screen widths.
+* Adjust some of the logging flutter.error presentation.
+* Fix thread name bug.
+
+## 0.1.5 - 2019-08-05
+* Support expanding or collapsing all values in the Call Tree and Bottom Up views (parts of the CPU profiler).
+* Support touchscreen scrolling and selection in flame charts.
+* Display structured error messages in the Logging view when "show structured errors" is enabled.
+* Search and filter dialogs are now case-insensitive.
+* Link to Dart DevTools documentation from connect screen.
+* Disable unsupported DevTools pages for Dart web apps.
+* Debugger dark mode improvements.
+
+## 0.1.4 - 2019-07-19
+* Add Performance page. This has a traditional CPU profiler for Dart applications.
+* Add ability to specify the profile granularity for the CPU profiler.
+* Bug fixes for DevTools tables, memory page, and cpu profiler.
+
+## 0.1.3 - 2019-07-11
+* Link to new flutter.dev hosted DevTools documentation.
+* Inspector UI improvements.
+
+## 0.1.2 - 2019-07-01
+* Add Call Tree and Bottom Up views to CPU profiler.
+* Pre-fetch CPU profiles so that we have profiling information for every frame in the timeline.
+* Trim Mixins from class name reporting in the CPU profiler.
+* Add searching for a particular class from all active classes in a Snapshot. After a snapshot, use the search button, located to left of snapshot button (or the shortcut CTRL+f ), to find and select the class in the classes list.
+* Add ability to find which class and field hold a reference to the current instance.  Hovering on an instance's allocation icon (right-most side of the instance).  Clicking on a class/field entry in the hover card will locate that particular class instance that has a reference to the original instance being hovered.
+* Expose hover card navigation via a memory navigation history areas (group of links below the classes/instances lists).
+* Allow DevTools feedback to be submitted when DevTools is not connected to an app.
+* Support URL encoded urls in the connection dialog.
+* Add error handling for analytics.
+* Cleanup warning message presentation.
+* Bug fixes and improvements.
+
 ## 0.1.1 - 2019-05-30
 * Make timeline snapshot format compatible with trace viewers such as chrome://tracing.
 * Add ability to import timeline snapshots via drag-and-drop.
@@ -11,7 +57,7 @@
 * Inspector polish bug fixes. Handle very deep inspector trees and only show expand-collapse arrows on tree nodes where needed.
 * Fix case where error messages remained on the startup screen after the error had been fixed.
 * Add ability to inspect an instance of a memory object in the memory profiler page after a snapshot of active memory objects.
-* First time DevTools is launched, prompt with an opt-in dialog to report DevTools usage statistics and crash reports of DevTools to Google. 
+* First time DevTools is launched, prompt with an opt-in dialog to report DevTools usage statistics and crash reports of DevTools to Google.
 
 ## 0.0.19 - 2019-05-01
 * Update DevTools server to better handle failures when launching browsers.
diff --git a/devtools/README.md b/devtools/README.md
index 859b493..6c2484f 100644
--- a/devtools/README.md
+++ b/devtools/README.md
@@ -8,7 +8,7 @@
 ## Getting started
 
 For documentation on installing and trying out DevTools, please see our
-[docs](https://flutter.github.io/devtools/).
+[docs](https://flutter.dev/docs/development/tools/devtools/).
 
 ## Feedback
 
diff --git a/devtools/build/.build.manifest b/devtools/build/.build.manifest
index 9cd68fb..8a24ad0 100644
--- a/devtools/build/.build.manifest
+++ b/devtools/build/.build.manifest
@@ -2,7 +2,6 @@
 .packages
 devtools_analytics.js
 favicon.png
-icons/.DS_Store
 icons/actions/forceRefresh.svg
 icons/actions/forceRefresh_dark.svg
 icons/attachDebugger.png
@@ -118,11 +117,15 @@
 icons/inspector/threads@2x.png
 icons/inspector/value.png
 icons/inspector/value@2x.png
+icons/memory/alloc_icon.png
+icons/memory/alloc_icon@2x.png
 icons/memory/ic_delete_outline_black.png
 icons/memory/ic_delete_outline_black@2x.png
 icons/memory/ic_filter_alt_black_1x_web_24dp@2x.png
 icons/memory/ic_filter_list_alt_black.png
 icons/memory/ic_filter_list_alt_black@2x.png
+icons/memory/ic_search.png
+icons/memory/ic_search@2x.png
 icons/memory/reset_icon.png
 icons/memory/reset_icon@2x.png
 icons/memory/reset_icon@2x.psd
@@ -281,7 +284,6 @@
 packages/$sdk/dev_compiler/web/dart_stack_trace_mapper.js
 packages/$sdk/dev_compiler/web/ddc_web_compiler.js
 packages/analyzer/src/summary/format.fbs
-packages/build_modules/src/analysis_options.default.yaml
 packages/build_runner/src/server/README.md
 packages/build_runner/src/server/build_updates_client/hot_reload_client.dart.js
 packages/build_runner/src/server/build_updates_client/live_reload_client.js
@@ -364,127 +366,288 @@
 packages/codemirror/keymap/sublime.js
 packages/codemirror/keymap/vim.js
 packages/codemirror/mode/apl/apl.js
+packages/codemirror/mode/apl/index.html
 packages/codemirror/mode/asciiarmor/asciiarmor.js
+packages/codemirror/mode/asciiarmor/index.html
 packages/codemirror/mode/asn.1/asn.1.js
+packages/codemirror/mode/asn.1/index.html
 packages/codemirror/mode/asterisk/asterisk.js
+packages/codemirror/mode/asterisk/index.html
 packages/codemirror/mode/brainfuck/brainfuck.js
+packages/codemirror/mode/brainfuck/index.html
 packages/codemirror/mode/clike/clike.js
+packages/codemirror/mode/clike/index.html
+packages/codemirror/mode/clike/scala.html
+packages/codemirror/mode/clike/test.js
 packages/codemirror/mode/clojure/clojure.js
+packages/codemirror/mode/clojure/index.html
+packages/codemirror/mode/clojure/test.js
 packages/codemirror/mode/cmake/cmake.js
+packages/codemirror/mode/cmake/index.html
 packages/codemirror/mode/cobol/cobol.js
+packages/codemirror/mode/cobol/index.html
 packages/codemirror/mode/coffeescript/coffeescript.js
+packages/codemirror/mode/coffeescript/index.html
 packages/codemirror/mode/commonlisp/commonlisp.js
+packages/codemirror/mode/commonlisp/index.html
 packages/codemirror/mode/crystal/crystal.js
+packages/codemirror/mode/crystal/index.html
 packages/codemirror/mode/css/css.js
+packages/codemirror/mode/css/gss.html
+packages/codemirror/mode/css/gss_test.js
+packages/codemirror/mode/css/index.html
+packages/codemirror/mode/css/less.html
+packages/codemirror/mode/css/less_test.js
+packages/codemirror/mode/css/scss.html
+packages/codemirror/mode/css/scss_test.js
+packages/codemirror/mode/css/test.js
 packages/codemirror/mode/cypher/cypher.js
+packages/codemirror/mode/cypher/index.html
+packages/codemirror/mode/cypher/test.js
 packages/codemirror/mode/d/d.js
+packages/codemirror/mode/d/index.html
+packages/codemirror/mode/d/test.js
 packages/codemirror/mode/dart/dart.js
+packages/codemirror/mode/dart/index.html
 packages/codemirror/mode/diff/diff.js
+packages/codemirror/mode/diff/index.html
 packages/codemirror/mode/django/django.js
+packages/codemirror/mode/django/index.html
 packages/codemirror/mode/dockerfile/dockerfile.js
+packages/codemirror/mode/dockerfile/index.html
+packages/codemirror/mode/dockerfile/test.js
 packages/codemirror/mode/dtd/dtd.js
+packages/codemirror/mode/dtd/index.html
 packages/codemirror/mode/dylan/dylan.js
+packages/codemirror/mode/dylan/index.html
+packages/codemirror/mode/dylan/test.js
 packages/codemirror/mode/ebnf/ebnf.js
+packages/codemirror/mode/ebnf/index.html
 packages/codemirror/mode/ecl/ecl.js
+packages/codemirror/mode/ecl/index.html
 packages/codemirror/mode/eiffel/eiffel.js
+packages/codemirror/mode/eiffel/index.html
 packages/codemirror/mode/elm/elm.js
+packages/codemirror/mode/elm/index.html
 packages/codemirror/mode/erlang/erlang.js
+packages/codemirror/mode/erlang/index.html
 packages/codemirror/mode/factor/factor.js
+packages/codemirror/mode/factor/index.html
 packages/codemirror/mode/fcl/fcl.js
+packages/codemirror/mode/fcl/index.html
 packages/codemirror/mode/forth/forth.js
+packages/codemirror/mode/forth/index.html
 packages/codemirror/mode/fortran/fortran.js
+packages/codemirror/mode/fortran/index.html
 packages/codemirror/mode/gas/gas.js
+packages/codemirror/mode/gas/index.html
 packages/codemirror/mode/gfm/gfm.js
+packages/codemirror/mode/gfm/index.html
+packages/codemirror/mode/gfm/test.js
 packages/codemirror/mode/gherkin/gherkin.js
+packages/codemirror/mode/gherkin/index.html
 packages/codemirror/mode/go/go.js
+packages/codemirror/mode/go/index.html
 packages/codemirror/mode/groovy/groovy.js
+packages/codemirror/mode/groovy/index.html
 packages/codemirror/mode/haml/haml.js
+packages/codemirror/mode/haml/index.html
+packages/codemirror/mode/haml/test.js
 packages/codemirror/mode/handlebars/handlebars.js
+packages/codemirror/mode/handlebars/index.html
 packages/codemirror/mode/haskell-literate/haskell-literate.js
+packages/codemirror/mode/haskell-literate/index.html
 packages/codemirror/mode/haskell/haskell.js
+packages/codemirror/mode/haskell/index.html
 packages/codemirror/mode/haxe/haxe.js
+packages/codemirror/mode/haxe/index.html
 packages/codemirror/mode/htmlembedded/htmlembedded.js
+packages/codemirror/mode/htmlembedded/index.html
 packages/codemirror/mode/htmlmixed/htmlmixed.js
+packages/codemirror/mode/htmlmixed/index.html
 packages/codemirror/mode/http/http.js
+packages/codemirror/mode/http/index.html
 packages/codemirror/mode/idl/idl.js
+packages/codemirror/mode/idl/index.html
+packages/codemirror/mode/index.html
+packages/codemirror/mode/javascript/index.html
 packages/codemirror/mode/javascript/javascript.js
+packages/codemirror/mode/javascript/json-ld.html
+packages/codemirror/mode/javascript/test.js
+packages/codemirror/mode/javascript/typescript.html
+packages/codemirror/mode/jinja2/index.html
 packages/codemirror/mode/jinja2/jinja2.js
+packages/codemirror/mode/jsx/index.html
 packages/codemirror/mode/jsx/jsx.js
+packages/codemirror/mode/jsx/test.js
+packages/codemirror/mode/julia/index.html
 packages/codemirror/mode/julia/julia.js
+packages/codemirror/mode/livescript/index.html
 packages/codemirror/mode/livescript/livescript.js
+packages/codemirror/mode/lua/index.html
 packages/codemirror/mode/lua/lua.js
+packages/codemirror/mode/markdown/index.html
 packages/codemirror/mode/markdown/markdown.js
+packages/codemirror/mode/markdown/test.js
+packages/codemirror/mode/mathematica/index.html
 packages/codemirror/mode/mathematica/mathematica.js
+packages/codemirror/mode/mbox/index.html
 packages/codemirror/mode/mbox/mbox.js
 packages/codemirror/mode/meta.js
+packages/codemirror/mode/mirc/index.html
 packages/codemirror/mode/mirc/mirc.js
+packages/codemirror/mode/mllike/index.html
 packages/codemirror/mode/mllike/mllike.js
+packages/codemirror/mode/modelica/index.html
 packages/codemirror/mode/modelica/modelica.js
+packages/codemirror/mode/mscgen/index.html
 packages/codemirror/mode/mscgen/mscgen.js
+packages/codemirror/mode/mscgen/mscgen_test.js
+packages/codemirror/mode/mscgen/msgenny_test.js
+packages/codemirror/mode/mscgen/xu_test.js
+packages/codemirror/mode/mumps/index.html
 packages/codemirror/mode/mumps/mumps.js
+packages/codemirror/mode/nginx/index.html
 packages/codemirror/mode/nginx/nginx.js
+packages/codemirror/mode/nsis/index.html
 packages/codemirror/mode/nsis/nsis.js
+packages/codemirror/mode/ntriples/index.html
 packages/codemirror/mode/ntriples/ntriples.js
+packages/codemirror/mode/octave/index.html
 packages/codemirror/mode/octave/octave.js
+packages/codemirror/mode/oz/index.html
 packages/codemirror/mode/oz/oz.js
+packages/codemirror/mode/pascal/index.html
 packages/codemirror/mode/pascal/pascal.js
+packages/codemirror/mode/pegjs/index.html
 packages/codemirror/mode/pegjs/pegjs.js
+packages/codemirror/mode/perl/index.html
 packages/codemirror/mode/perl/perl.js
+packages/codemirror/mode/php/index.html
 packages/codemirror/mode/php/php.js
+packages/codemirror/mode/php/test.js
+packages/codemirror/mode/pig/index.html
 packages/codemirror/mode/pig/pig.js
+packages/codemirror/mode/powershell/index.html
 packages/codemirror/mode/powershell/powershell.js
+packages/codemirror/mode/powershell/test.js
+packages/codemirror/mode/properties/index.html
 packages/codemirror/mode/properties/properties.js
+packages/codemirror/mode/protobuf/index.html
 packages/codemirror/mode/protobuf/protobuf.js
+packages/codemirror/mode/pug/index.html
 packages/codemirror/mode/pug/pug.js
+packages/codemirror/mode/puppet/index.html
 packages/codemirror/mode/puppet/puppet.js
+packages/codemirror/mode/python/index.html
 packages/codemirror/mode/python/python.js
+packages/codemirror/mode/python/test.js
+packages/codemirror/mode/q/index.html
 packages/codemirror/mode/q/q.js
+packages/codemirror/mode/r/index.html
 packages/codemirror/mode/r/r.js
+packages/codemirror/mode/rpm/changes/index.html
+packages/codemirror/mode/rpm/index.html
 packages/codemirror/mode/rpm/rpm.js
+packages/codemirror/mode/rst/index.html
 packages/codemirror/mode/rst/rst.js
+packages/codemirror/mode/ruby/index.html
 packages/codemirror/mode/ruby/ruby.js
+packages/codemirror/mode/ruby/test.js
+packages/codemirror/mode/rust/index.html
 packages/codemirror/mode/rust/rust.js
+packages/codemirror/mode/rust/test.js
+packages/codemirror/mode/sas/index.html
 packages/codemirror/mode/sas/sas.js
+packages/codemirror/mode/sass/index.html
 packages/codemirror/mode/sass/sass.js
+packages/codemirror/mode/sass/test.js
+packages/codemirror/mode/scheme/index.html
 packages/codemirror/mode/scheme/scheme.js
+packages/codemirror/mode/shell/index.html
 packages/codemirror/mode/shell/shell.js
+packages/codemirror/mode/shell/test.js
+packages/codemirror/mode/sieve/index.html
 packages/codemirror/mode/sieve/sieve.js
+packages/codemirror/mode/slim/index.html
 packages/codemirror/mode/slim/slim.js
+packages/codemirror/mode/slim/test.js
+packages/codemirror/mode/smalltalk/index.html
 packages/codemirror/mode/smalltalk/smalltalk.js
+packages/codemirror/mode/smarty/index.html
 packages/codemirror/mode/smarty/smarty.js
+packages/codemirror/mode/solr/index.html
 packages/codemirror/mode/solr/solr.js
+packages/codemirror/mode/soy/index.html
 packages/codemirror/mode/soy/soy.js
+packages/codemirror/mode/soy/test.js
+packages/codemirror/mode/sparql/index.html
 packages/codemirror/mode/sparql/sparql.js
+packages/codemirror/mode/spreadsheet/index.html
 packages/codemirror/mode/spreadsheet/spreadsheet.js
+packages/codemirror/mode/sql/index.html
 packages/codemirror/mode/sql/sql.js
+packages/codemirror/mode/stex/index.html
 packages/codemirror/mode/stex/stex.js
+packages/codemirror/mode/stex/test.js
+packages/codemirror/mode/stylus/index.html
 packages/codemirror/mode/stylus/stylus.js
+packages/codemirror/mode/swift/index.html
 packages/codemirror/mode/swift/swift.js
+packages/codemirror/mode/swift/test.js
+packages/codemirror/mode/tcl/index.html
 packages/codemirror/mode/tcl/tcl.js
+packages/codemirror/mode/textile/index.html
+packages/codemirror/mode/textile/test.js
 packages/codemirror/mode/textile/textile.js
+packages/codemirror/mode/tiddlywiki/index.html
 packages/codemirror/mode/tiddlywiki/tiddlywiki.css
 packages/codemirror/mode/tiddlywiki/tiddlywiki.js
+packages/codemirror/mode/tiki/index.html
 packages/codemirror/mode/tiki/tiki.css
 packages/codemirror/mode/tiki/tiki.js
+packages/codemirror/mode/toml/index.html
 packages/codemirror/mode/toml/toml.js
+packages/codemirror/mode/tornado/index.html
 packages/codemirror/mode/tornado/tornado.js
+packages/codemirror/mode/troff/index.html
 packages/codemirror/mode/troff/troff.js
+packages/codemirror/mode/ttcn-cfg/index.html
 packages/codemirror/mode/ttcn-cfg/ttcn-cfg.js
+packages/codemirror/mode/ttcn/index.html
 packages/codemirror/mode/ttcn/ttcn.js
+packages/codemirror/mode/turtle/index.html
 packages/codemirror/mode/turtle/turtle.js
+packages/codemirror/mode/twig/index.html
 packages/codemirror/mode/twig/twig.js
+packages/codemirror/mode/vb/index.html
 packages/codemirror/mode/vb/vb.js
+packages/codemirror/mode/vbscript/index.html
 packages/codemirror/mode/vbscript/vbscript.js
+packages/codemirror/mode/velocity/index.html
 packages/codemirror/mode/velocity/velocity.js
+packages/codemirror/mode/verilog/index.html
+packages/codemirror/mode/verilog/test.js
 packages/codemirror/mode/verilog/verilog.js
+packages/codemirror/mode/vhdl/index.html
 packages/codemirror/mode/vhdl/vhdl.js
+packages/codemirror/mode/vue/index.html
 packages/codemirror/mode/vue/vue.js
+packages/codemirror/mode/webidl/index.html
 packages/codemirror/mode/webidl/webidl.js
+packages/codemirror/mode/xml/index.html
+packages/codemirror/mode/xml/test.js
 packages/codemirror/mode/xml/xml.js
+packages/codemirror/mode/xquery/index.html
+packages/codemirror/mode/xquery/test.js
 packages/codemirror/mode/xquery/xquery.js
+packages/codemirror/mode/yacas/index.html
 packages/codemirror/mode/yacas/yacas.js
+packages/codemirror/mode/yaml-frontmatter/index.html
 packages/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js
+packages/codemirror/mode/yaml/index.html
 packages/codemirror/mode/yaml/yaml.js
+packages/codemirror/mode/z80/index.html
 packages/codemirror/mode/z80/z80.js
 packages/codemirror/theme/3024-day.css
 packages/codemirror/theme/3024-night.css
@@ -541,12 +704,18 @@
 packages/codemirror/theme/xq-dark.css
 packages/codemirror/theme/xq-light.css
 packages/codemirror/theme/yeti.css
+packages/codemirror/theme/yonce.css
 packages/codemirror/theme/zenburn.css
 packages/devtools/src/debugger/debugger.css
 packages/devtools/src/inspector/inspector.css
 packages/devtools/src/inspector/inspector_dark.css
 packages/devtools/src/logging/logging.css
+packages/devtools/src/performance/performance.css
+packages/devtools/src/profiler/profiler.css
+packages/devtools/src/settings/settings.css
+packages/devtools/src/timeline/README.md
 packages/devtools/src/timeline/timeline.css
+packages/devtools/src/timeline/timeline_architecture.png
 packages/devtools/src/ui/MaterialIcons-Regular.woff2
 packages/devtools/src/ui/plotly.js
 packages/front_end/src/fasta/README.md
@@ -805,6 +974,7 @@
 packages/pedantic/analysis_options.1.5.0.yaml
 packages/pedantic/analysis_options.1.6.0.yaml
 packages/pedantic/analysis_options.1.7.0.yaml
+packages/pedantic/analysis_options.1.8.0.yaml
 packages/pedantic/analysis_options.yaml
 packages/plotly_js/plotly.js
 packages/plotly_js/plotly.min.js
@@ -821,8 +991,5 @@
 styles.css
 styles_dark.css
 styles_light.css
-third_party/split/LICENSE
-third_party/split/README.md
-third_party/split/split.min.js
 unsupported-browser.html
 widgets.json
\ No newline at end of file
diff --git a/devtools/build/icons/memory/alloc_icon.png b/devtools/build/icons/memory/alloc_icon.png
new file mode 100644
index 0000000..fb67f42
--- /dev/null
+++ b/devtools/build/icons/memory/alloc_icon.png
Binary files differ
diff --git a/devtools/build/icons/memory/alloc_icon@2x.png b/devtools/build/icons/memory/alloc_icon@2x.png
new file mode 100644
index 0000000..920f20c
--- /dev/null
+++ b/devtools/build/icons/memory/alloc_icon@2x.png
Binary files differ
diff --git a/devtools/build/icons/memory/ic_search.png b/devtools/build/icons/memory/ic_search.png
new file mode 100644
index 0000000..1244fe3
--- /dev/null
+++ b/devtools/build/icons/memory/ic_search.png
Binary files differ
diff --git a/devtools/build/icons/memory/ic_search@2x.png b/devtools/build/icons/memory/ic_search@2x.png
new file mode 100644
index 0000000..425d4bc
--- /dev/null
+++ b/devtools/build/icons/memory/ic_search@2x.png
Binary files differ
diff --git a/devtools/build/pack/$sdk/_internal/ddc_sdk.sum b/devtools/build/pack/$sdk/_internal/ddc_sdk.sum
index 3ef2a80..0e7cb7f 100644
--- a/devtools/build/pack/$sdk/_internal/ddc_sdk.sum
+++ b/devtools/build/pack/$sdk/_internal/ddc_sdk.sum
Binary files differ
diff --git a/devtools/build/pack/$sdk/_internal/strong.sum b/devtools/build/pack/$sdk/_internal/strong.sum
index a6918f3..9439a04 100644
--- a/devtools/build/pack/$sdk/_internal/strong.sum
+++ b/devtools/build/pack/$sdk/_internal/strong.sum
Binary files differ
diff --git a/devtools/build/pack/analyzer/src/summary/format.fbs b/devtools/build/pack/analyzer/src/summary/format.fbs
index 6cd172f..4ce0eca 100644
--- a/devtools/build/pack/analyzer/src/summary/format.fbs
+++ b/devtools/build/pack/analyzer/src/summary/format.fbs
@@ -1,9 +1,11 @@
-// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 //
 // This file has been automatically generated.  Please do not edit it manually.
-// To regenerate the file, use the script "pkg/analyzer/tool/generate_files".
+// To regenerate the file, use the SDK script
+// "pkg/analyzer/tool/summary/generate.dart $IDL_FILE_PATH",
+// or "pkg/analyzer/tool/generate_files" for the analyzer package IDL/sources.
 
 
 /// Enum of declaration kinds in available files.
@@ -12,16 +14,24 @@
 
   CLASS_TYPE_ALIAS,
 
+  CONSTRUCTOR,
+
   ENUM,
 
   ENUM_CONSTANT,
 
+  EXTENSION,
+
+  FIELD,
+
   FUNCTION,
 
   FUNCTION_TYPE_ALIAS,
 
   GETTER,
 
+  METHOD,
+
   MIXIN,
 
   SETTER,
@@ -42,6 +52,30 @@
   syntheticFunction
 }
 
+/// Enum representing nullability suffixes in summaries.
+///
+/// This enum is similar to [NullabilitySuffix], but the order is different so
+/// that [EntityRefNullabilitySuffix.starOrIrrelevant] can be the default.
+enum EntityRefNullabilitySuffix : byte {
+  /// An indication that the canonical representation of the type under
+  /// consideration ends with `*`.  Types having this nullability suffix are
+  /// called "legacy types"; it has not yet been determined whether they should
+  /// be unioned with the Null type.
+  ///
+  /// Also used in circumstances where no nullability suffix information is
+  /// needed.
+  starOrIrrelevant,
+
+  /// An indication that the canonical representation of the type under
+  /// consideration ends with `?`.  Types having this nullability suffix should
+  /// be interpreted as being unioned with the Null type.
+  question,
+
+  /// An indication that the canonical representation of the type under
+  /// consideration does not end with either `?` or `*`.
+  none
+}
+
 /// Enum used to indicate the kind of a name in index.
 enum IndexNameKind : byte {
   /// A top-level element.
@@ -148,11 +182,13 @@
 
 /// Kinds of formal parameters.
 enum LinkedNodeFormalParameterKind : byte {
-  required,
+  requiredPositional,
 
   optionalPositional,
 
-  optionalNamed
+  optionalNamed,
+
+  requiredNamed
 }
 
 /// Kinds of [LinkedNode].
@@ -193,6 +229,8 @@
 
   comment,
 
+  commentReference,
+
   compilationUnit,
 
   conditionalExpression,
@@ -233,6 +271,8 @@
 
   extendsClause,
 
+  extensionDeclaration,
+
   fieldDeclaration,
 
   fieldFormalParameter,
@@ -309,6 +349,10 @@
 
   namedExpression,
 
+  nativeClause,
+
+  nativeFunctionBody,
+
   nullLiteral,
 
   onClause,
@@ -333,8 +377,6 @@
 
   returnStatement,
 
-  scriptTag,
-
   setOrMapLiteral,
 
   showCombinator,
@@ -398,8 +440,6 @@
 
   function,
 
-  genericTypeAlias,
-
   interface,
 
   typeParameter,
@@ -991,19 +1031,91 @@
   /// other two are collection elements.  Push an "if" element having the given
   /// condition, with the two collection elements as its "then" and "else"
   /// clauses, respectively.
-  ifElseElement
+  ifElseElement,
+
+  /// Pop the top n+2 values from the stack, where n is obtained from
+  /// [UnlinkedExpr.ints].  The first two are the initialization and condition
+  /// of the for-loop; the remainder are the updaters.
+  forParts,
+
+  /// Pop the top 2 values from the stack.  The first is the for loop parts.
+  /// The second is the body.
+  forElement,
+
+  /// Push the empty expression (used for missing initializers and conditions in
+  /// `for` loops)
+  pushEmptyExpression,
+
+  /// Add a variable to the current scope whose name is obtained from
+  /// [UnlinkedExpr.strings].  This is separate from [variableDeclaration]
+  /// because the scope of the variable includes its own initializer.
+  variableDeclarationStart,
+
+  /// Pop the top value from the stack, and use it as the initializer for a
+  /// variable declaration; the variable being declared is obtained by looking
+  /// at the nth variable most recently added to the scope (where n counts from
+  /// zero and is obtained from [UnlinkedExpr.ints]).
+  variableDeclaration,
+
+  /// Pop the top n values from the stack, which should all be variable
+  /// declarations, and use them to create an untyped for-initializer
+  /// declaration.  The value of n is obtained from [UnlinkedExpr.ints].
+  forInitializerDeclarationsUntyped,
+
+  /// Pop the top n values from the stack, which should all be variable
+  /// declarations, and use them to create a typed for-initializer
+  /// declaration.  The value of n is obtained from [UnlinkedExpr.ints].  The
+  /// type is obtained from [UnlinkedExpr.references].
+  forInitializerDeclarationsTyped,
+
+  /// Pop from the stack `value` and get a string from [UnlinkedExpr.strings].
+  /// Use this string to look up a parameter.  Perform `parameter op= value`,
+  /// where `op` is the next assignment operator from
+  /// [UnlinkedExpr.assignmentOperators].  Push `value` back onto the stack.
+  ///
+  /// If the assignment operator is a prefix/postfix increment/decrement, then
+  /// `value` is not present in the stack, so it should not be popped and the
+  /// corresponding value of the parameter after/before update is pushed onto
+  /// the stack instead.
+  assignToParameter,
+
+  /// Pop from the stack an identifier and an expression, and create for-each
+  /// parts of the form `identifier in expression`.
+  forEachPartsWithIdentifier,
+
+  /// Pop the top 2 values from the stack.  The first is the for loop parts.
+  /// The second is the body.
+  forElementWithAwait,
+
+  /// Pop an expression from the stack, and create for-each parts of the form
+  /// `var name in expression`, where `name` is obtained from
+  /// [UnlinkedExpr.strings].
+  forEachPartsWithUntypedDeclaration,
+
+  /// Pop an expression from the stack, and create for-each parts of the form
+  /// `Type name in expression`, where `name` is obtained from
+  /// [UnlinkedExpr.strings], and `Type` is obtained from
+  /// [UnlinkedExpr.references].
+  forEachPartsWithTypedDeclaration,
+
+  /// Pop the top 2 values from the stack, compute `v1 >>> v2`, and push the
+  /// result back onto the stack.
+  bitShiftRightLogical
 }
 
 /// Enum used to indicate the kind of a parameter.
 enum UnlinkedParamKind : byte {
-  /// Parameter is required.
-  required,
+  /// Parameter is required and positional.
+  requiredPositional,
 
-  /// Parameter is positional optional (enclosed in `[]`)
-  positional,
+  /// Parameter is optional and positional (enclosed in `[]`)
+  optionalPositional,
 
-  /// Parameter is named optional (enclosed in `{}`)
-  named
+  /// Parameter is optional and named (enclosed in `{}`)
+  optionalNamed,
+
+  /// Parameter is required and named (enclosed in `{}`).
+  requiredNamed
 }
 
 /// TODO(scheglov) document
@@ -1049,6 +1161,8 @@
 
   BANG_EQ,
 
+  BANG_EQ_EQ,
+
   BAR,
 
   BAR_BAR,
@@ -1103,6 +1217,8 @@
 
   EQ_EQ,
 
+  EQ_EQ_EQ,
+
   EXPORT,
 
   EXTENDS,
@@ -1133,6 +1249,10 @@
 
   GT_GT_EQ,
 
+  GT_GT_GT,
+
+  GT_GT_GT_EQ,
+
   HASH,
 
   HEXADECIMAL,
@@ -1159,6 +1279,8 @@
 
   IS,
 
+  LATE,
+
   LIBRARY,
 
   LT,
@@ -1227,6 +1349,8 @@
 
   QUESTION_QUESTION_EQ,
 
+  REQUIRED,
+
   RETHROW,
 
   RETURN,
@@ -1339,6 +1463,9 @@
 
 /// Information about an error in a resolved unit.
 table AnalysisDriverUnitError {
+  /// The context messages associated with the error.
+  contextMessages:[DiagnosticMessage] (id: 5);
+
   /// The optional correction hint for the error.
   correction:string (id: 4);
 
@@ -1466,62 +1593,67 @@
 
   /// Unlinked information for the unit.
   unit:UnlinkedUnit (id: 1);
+
+  /// Unlinked information for the unit.
+  unit2:UnlinkedUnit2 (id: 5);
 }
 
 /// Information about a single declaration.
 table AvailableDeclaration {
-  defaultArgumentListString:string (id: 0);
+  children:[AvailableDeclaration] (id: 0);
 
-  defaultArgumentListTextRanges:[uint] (id: 1);
+  codeLength:uint (id: 1);
 
-  docComplete:string (id: 2);
+  codeOffset:uint (id: 2);
 
-  docSummary:string (id: 3);
+  defaultArgumentListString:string (id: 3);
 
-  fieldMask:uint (id: 4);
+  defaultArgumentListTextRanges:[uint] (id: 4);
 
-  isAbstract:bool (id: 5);
+  docComplete:string (id: 5);
 
-  isConst:bool (id: 6);
+  docSummary:string (id: 6);
 
-  isDeprecated:bool (id: 7);
+  fieldMask:uint (id: 7);
 
-  isFinal:bool (id: 8);
+  isAbstract:bool (id: 8);
+
+  isConst:bool (id: 9);
+
+  isDeprecated:bool (id: 10);
+
+  isFinal:bool (id: 11);
 
   /// The kind of the declaration.
-  kind:AvailableDeclarationKind (id: 9);
+  kind:AvailableDeclarationKind (id: 12);
 
-  locationOffset:uint (id: 10);
+  locationOffset:uint (id: 13);
 
-  locationStartColumn:uint (id: 11);
+  locationStartColumn:uint (id: 14);
 
-  locationStartLine:uint (id: 12);
+  locationStartLine:uint (id: 15);
 
   /// The first part of the declaration name, usually the only one, for example
   /// the name of a class like `MyClass`, or a function like `myFunction`.
-  name:string (id: 13);
+  name:string (id: 16);
 
-  /// The second, optional, part of the declaration name.  For example enum
-  /// constants all have the same [name], but their own [name2].
-  name2:string (id: 14);
+  parameterNames:[string] (id: 17);
 
-  parameterNames:[string] (id: 15);
+  parameters:string (id: 18);
 
-  parameters:string (id: 16);
-
-  parameterTypes:[string] (id: 17);
+  parameterTypes:[string] (id: 19);
 
   /// The partial list of relevance tags.  Not every declaration has one (for
   /// example, function do not currently), and not every declaration has to
   /// store one (for classes it can be computed when we know the library that
   /// includes this file).
-  relevanceTags:[string] (id: 18);
+  relevanceTags:[string] (id: 20);
 
-  requiredParameterCount:uint (id: 19);
+  requiredParameterCount:uint (id: 21);
 
-  returnType:string (id: 20);
+  returnType:string (id: 22);
 
-  typeParameters:string (id: 21);
+  typeParameters:string (id: 23);
 }
 
 /// Information about an available, even if not yet imported file.
@@ -1529,17 +1661,23 @@
   /// Declarations of the file.
   declarations:[AvailableDeclaration] (id: 0);
 
+  /// The Dartdoc directives in the file.
+  directiveInfo:DirectiveInfo (id: 1);
+
   /// Exports directives of the file.
-  exports:[AvailableFileExport] (id: 1);
+  exports:[AvailableFileExport] (id: 2);
 
   /// Is `true` if this file is a library.
-  isLibrary:bool (id: 2);
+  isLibrary:bool (id: 3);
 
   /// Is `true` if this file is a library, and it is deprecated.
-  isLibraryDeprecated:bool (id: 3);
+  isLibraryDeprecated:bool (id: 4);
+
+  /// Offsets of the first character of each line in the source code.
+  lineStarts:[uint] (id: 5);
 
   /// URIs of `part` directives.
-  parts:[string] (id: 4);
+  parts:[string] (id: 6);
 }
 
 /// Information about an export directive.
@@ -1569,6 +1707,30 @@
   offset:uint (id: 0);
 }
 
+table DiagnosticMessage {
+  /// The absolute and normalized path of the file associated with this message.
+  filePath:string (id: 0);
+
+  /// The length of the source range associated with this message.
+  length:uint (id: 1);
+
+  /// The text of the message.
+  message:string (id: 2);
+
+  /// The zero-based offset from the start of the file to the beginning of the
+  /// source range associated with this message.
+  offset:uint (id: 3);
+}
+
+/// Information about the Dartdoc directives in an [AvailableFile].
+table DirectiveInfo {
+  /// The names of the defined templates.
+  templateNames:[string] (id: 0);
+
+  /// The values of the defined templates.
+  templateValues:[string] (id: 1);
+}
+
 /// Summary information about a reference to an entity such as a type, top level
 /// executable, or executable within a class.
 table EntityRef {
@@ -1603,6 +1765,9 @@
   /// first to the class and then to the method.
   implicitFunctionTypeIndices:[uint] (id: 4);
 
+  /// If the reference represents a type, the nullability of the type.
+  nullabilitySuffix:EntityRefNullabilitySuffix (id: 10);
+
   /// If this is a reference to a type parameter, one-based index into the list
   /// of [UnlinkedTypeParam]s currently in effect.  Indexing is done using De
   /// Bruijn index conventions; that is, innermost parameters come first, and
@@ -1749,29 +1914,26 @@
 
 /// Information about a linked AST node.
 table LinkedNode {
-  variantField_2:[LinkedNode] (id: 2);
+  /// The explicit or inferred return type of a function typed node.
+  variantField_24:LinkedNodeType (id: 24);
 
-  variantField_11:LinkedNode (id: 11);
+  variantField_2:[LinkedNode] (id: 2);
 
   variantField_4:[LinkedNode] (id: 4);
 
   variantField_6:LinkedNode (id: 6);
 
-  variantField_15:uint (id: 15);
-
   variantField_7:LinkedNode (id: 7);
 
-  variantField_8:LinkedNode (id: 8);
-
-  variantField_16:uint (id: 16);
-
   variantField_17:uint (id: 17);
 
-  variantField_18:uint (id: 18);
+  variantField_8:LinkedNode (id: 8);
 
-  variantField_19:uint (id: 19);
+  variantField_38:LinkedNodeTypeSubstitution (id: 38);
 
-  variantField_24:LinkedNodeType (id: 24);
+  variantField_15:uint (id: 15);
+
+  variantField_28:UnlinkedTokenType (id: 28);
 
   variantField_27:bool (id: 27);
 
@@ -1783,7 +1945,7 @@
 
   variantField_13:LinkedNode (id: 13);
 
-  variantField_28:[uint] (id: 28);
+  variantField_33:[string] (id: 33);
 
   variantField_29:LinkedNodeCommentType (id: 29);
 
@@ -1791,29 +1953,45 @@
 
   variantField_10:LinkedNode (id: 10);
 
+  variantField_26:LinkedNodeFormalParameterKind (id: 26);
+
   variantField_21:double (id: 21);
 
   variantField_25:LinkedNodeType (id: 25);
 
-  variantField_26:LinkedNodeFormalParameterKind (id: 26);
+  variantField_20:string (id: 20);
+
+  flags:uint (id: 18);
+
+  variantField_1:string (id: 1);
+
+  variantField_36:uint (id: 36);
+
+  variantField_16:uint (id: 16);
 
   variantField_30:string (id: 30);
 
   variantField_14:LinkedNode (id: 14);
 
-  isSynthetic:bool (id: 1);
-
   kind:LinkedNodeKind (id: 0);
 
-  variantField_23:string (id: 23);
+  variantField_34:[string] (id: 34);
+
+  name:string (id: 37);
 
   variantField_31:bool (id: 31);
 
-  variantField_20:string (id: 20);
+  variantField_35:UnlinkedTokenType (id: 35);
+
+  variantField_32:TopLevelInferenceError (id: 32);
+
+  variantField_23:LinkedNodeType (id: 23);
+
+  variantField_11:LinkedNode (id: 11);
 
   variantField_22:string (id: 22);
 
-  variantField_32:LinkedNodeVariablesDeclaration (id: 32);
+  variantField_19:uint (id: 19);
 }
 
 /// Information about a group of libraries linked together, for example because
@@ -1849,13 +2027,16 @@
 
 /// Information about a Dart type.
 table LinkedNodeType {
-  /// References to [LinkedNodeReferences].
-  functionFormalParameters:[uint] (id: 0);
+  functionFormalParameters:[LinkedNodeTypeFormalParameter] (id: 0);
 
   functionReturnType:LinkedNodeType (id: 1);
 
-  /// References to [LinkedNodeReferences].
-  functionTypeParameters:[uint] (id: 2);
+  /// The typedef this function type is created for.
+  functionTypedef:uint (id: 9);
+
+  functionTypedefTypeArguments:[LinkedNodeType] (id: 10);
+
+  functionTypeParameters:[LinkedNodeTypeTypeParameter] (id: 2);
 
   /// Reference to a [LinkedNodeReferences].
   interfaceClass:uint (id: 3);
@@ -1864,31 +2045,54 @@
 
   kind:LinkedNodeTypeKind (id: 5);
 
-  /// Reference to a [LinkedNodeReferences].
-  typeParameterParameter:uint (id: 6);
+  nullabilitySuffix:EntityRefNullabilitySuffix (id: 8);
+
+  typeParameterElement:uint (id: 6);
+
+  typeParameterId:uint (id: 7);
+}
+
+/// Information about a formal parameter in a function type.
+table LinkedNodeTypeFormalParameter {
+  kind:LinkedNodeFormalParameterKind (id: 0);
+
+  name:string (id: 1);
+
+  type:LinkedNodeType (id: 2);
+}
+
+/// Information about a type substitution.
+table LinkedNodeTypeSubstitution {
+  typeArguments:[LinkedNodeType] (id: 1);
+
+  typeParameters:[uint] (id: 0);
+}
+
+/// Information about a type parameter in a function type.
+table LinkedNodeTypeTypeParameter {
+  bound:LinkedNodeType (id: 1);
+
+  name:string (id: 0);
 }
 
 /// Information about a single library in a [LinkedNodeLibrary].
 table LinkedNodeUnit {
+  isNNBD:bool (id: 4);
+
+  isSynthetic:bool (id: 3);
+
   node:LinkedNode (id: 2);
 
+  /// If the unit is a part, the URI specified in the `part` directive.
+  /// Otherwise empty.
+  partUriStr:string (id: 5);
+
   tokens:UnlinkedTokens (id: 1);
 
+  /// The absolute URI.
   uriStr:string (id: 0);
 }
 
-/// Information about a top-level declaration, or a field declaration that
-/// contributes information to [LinkedNodeKind.variableDeclaration].
-table LinkedNodeVariablesDeclaration {
-  comment:LinkedNode (id: 3);
-
-  isConst:bool (id: 0);
-
-  isFinal:bool (id: 1);
-
-  isStatic:bool (id: 2);
-}
-
 /// Information about the resolution of an [UnlinkedReference].
 table LinkedReference {
   /// If this [LinkedReference] doesn't have an associated [UnlinkedReference],
@@ -1977,6 +2181,9 @@
   /// package may have changed.
   apiSignature:string (id: 7, deprecated);
 
+  /// The version 2 of the summary.
+  bundle2:LinkedNodeBundle (id: 9);
+
   /// Information about the packages this package depends on, if known.
   dependencies:[PackageDependencyInfo] (id: 8, deprecated);
 
@@ -2537,6 +2744,39 @@
   strings:[string] (id: 3);
 }
 
+/// Unlinked summary information about an extension declaration.
+table UnlinkedExtension {
+  /// Annotations for this extension.
+  annotations:[UnlinkedExpr] (id: 4);
+
+  /// Code range of the extension.
+  codeRange:CodeRange (id: 7);
+
+  /// Documentation comment for the extension, or `null` if there is no
+  /// documentation comment.
+  documentationComment:UnlinkedDocumentationComment (id: 5);
+
+  /// Executable objects (methods, getters, and setters) contained in the
+  /// extension.
+  executables:[UnlinkedExecutable] (id: 2);
+
+  /// The type being extended.
+  extendedType:EntityRef (id: 3);
+
+  /// Field declarations contained in the extension.
+  fields:[UnlinkedVariable] (id: 8);
+
+  /// Name of the extension, or an empty string if there is no name.
+  name:string (id: 0);
+
+  /// Offset of the extension name relative to the beginning of the file, or
+  /// zero if there is no name.
+  nameOffset:uint (id: 1);
+
+  /// Type parameters of the extension, if any.
+  typeParameters:[UnlinkedTypeParam] (id: 6);
+}
+
 /// Unlinked summary information about an import declaration.
 table UnlinkedImport {
   /// Annotations for this import declaration.
@@ -2581,6 +2821,34 @@
   uriOffset:uint (id: 3);
 }
 
+table UnlinkedInformativeData {
+  variantField_2:uint (id: 2);
+
+  variantField_3:uint (id: 3);
+
+  variantField_9:uint (id: 9);
+
+  variantField_8:uint (id: 8);
+
+  /// Offsets of the first character of each line in the source code.
+  variantField_7:[uint] (id: 7);
+
+  variantField_6:uint (id: 6);
+
+  variantField_5:uint (id: 5);
+
+  /// If the parameter has a default value, the source text of the constant
+  /// expression in the default value.  Otherwise the empty string.
+  variantField_10:string (id: 10);
+
+  variantField_1:uint (id: 1);
+
+  variantField_4:[string] (id: 4);
+
+  /// The kind of the node.
+  kind:LinkedNodeKind (id: 0);
+}
+
 /// Unlinked summary information about a function parameter.
 table UnlinkedParam {
   /// Annotations for this parameter.
@@ -2850,6 +3118,9 @@
   /// Export declarations in the compilation unit.
   exports:[UnlinkedExportNonPublic] (id: 13);
 
+  /// Extensions declared in the compilation unit.
+  extensions:[UnlinkedExtension] (id: 22);
+
   /// If this compilation unit was summarized in fallback mode, the path where
   /// the compilation unit may be found on disk.  Otherwise empty.
   ///
@@ -2860,6 +3131,9 @@
   /// Import declarations in the compilation unit.
   imports:[UnlinkedImport] (id: 5);
 
+  /// Indicates whether this compilation unit is opted into NNBD.
+  isNNBD:bool (id: 21);
+
   /// Indicates whether the unit contains a "part of" declaration.
   isPartOf:bool (id: 18);
 
@@ -2908,6 +3182,33 @@
   variables:[UnlinkedVariable] (id: 3);
 }
 
+/// Unlinked summary information about a compilation unit.
+table UnlinkedUnit2 {
+  /// The MD5 hash signature of the API portion of this unit. It depends on all
+  /// tokens that might affect APIs of declarations in the unit.
+  apiSignature:[uint] (id: 0);
+
+  /// URIs of `export` directives.
+  exports:[string] (id: 1);
+
+  /// Is `true` if the unit contains a `library` directive.
+  hasLibraryDirective:bool (id: 6);
+
+  /// Is `true` if the unit contains a `part of` directive.
+  hasPartOfDirective:bool (id: 3);
+
+  /// URIs of `import` directives.
+  imports:[string] (id: 2);
+
+  informativeData:[UnlinkedInformativeData] (id: 7);
+
+  /// Offsets of the first character of each line in the source code.
+  lineStarts:[uint] (id: 5);
+
+  /// URIs of `part` directives.
+  parts:[string] (id: 4);
+}
+
 /// Unlinked summary information about a top level variable, local variable, or
 /// a field.
 table UnlinkedVariable {
@@ -2949,6 +3250,9 @@
   /// Indicates whether the variable is declared using the `final` keyword.
   isFinal:bool (id: 7);
 
+  /// Indicates whether the variable is declared using the `late` keyword.
+  isLate:bool (id: 16);
+
   /// Indicates whether the variable is declared using the `static` keyword.
   ///
   /// Note that for top level variables, this flag is false, since they are not
diff --git a/devtools/build/pack/build_modules/src/analysis_options.default.yaml b/devtools/build/pack/build_modules/src/analysis_options.default.yaml
deleted file mode 100644
index a10d4c5..0000000
--- a/devtools/build/pack/build_modules/src/analysis_options.default.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-analyzer:
-  strong-mode: true
diff --git a/devtools/build/pack/devtools/src/timeline/README.md b/devtools/build/pack/devtools/src/timeline/README.md
new file mode 100644
index 0000000..ad49e06
--- /dev/null
+++ b/devtools/build/pack/devtools/src/timeline/README.md
@@ -0,0 +1,23 @@
+## Timeline Code Architecture
+
+<img src="timeline_architecture.png" width="800" />
+
+### View
+
+The view has no awareness of the timeline data directly. It has access to the timeline data (frames, selected event,
+selected frame, cpu profile, etc.) through the `TimelineController`. The view receives updates from the
+`TimelineController` via streams.
+
+### Model
+
+`TimelineData` stores the current data for the DevTools timeline. Its main components include `frames`, `selectedFrame`,
+`selectedEvent`, `cpuProfileData`, and `traceEvents`. This data is maintained by `TimelineController`.
+
+### Controller
+
+The controller manages `TimelineData` and communicates with the view to give and receive data updates. It manages data
+processing via protocols `TimelineProtocol` (protocol for processing trace events and composing them into
+`TimelineEvent`s and `TimelineFrame`s) and `CpuProfileProtocol` (protocol for processing `CpuProfileData` and composing
+it into a structured tree of `CpuStackFrame`s). The controller also communicates with `TimelineService`, which manages
+interactions between the Timeline and the VmService. `TimelineController` has no dependency on `dart:html`, making it
+easily testable.
diff --git a/devtools/build/pack/devtools/src/timeline/timeline_architecture.png b/devtools/build/pack/devtools/src/timeline/timeline_architecture.png
new file mode 100644
index 0000000..a6776c6
--- /dev/null
+++ b/devtools/build/pack/devtools/src/timeline/timeline_architecture.png
Binary files differ
diff --git a/devtools/build/pack/pedantic/analysis_options.1.8.0.yaml b/devtools/build/pack/pedantic/analysis_options.1.8.0.yaml
new file mode 100644
index 0000000..88428be
--- /dev/null
+++ b/devtools/build/pack/pedantic/analysis_options.1.8.0.yaml
@@ -0,0 +1,37 @@
+# Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+#
+# Google internally enforced rules. See README.md for more information,
+# including a list of lints that are intentionally _not_ enforced.
+
+linter:
+  rules:
+    - avoid_empty_else
+    - avoid_init_to_null
+    - avoid_relative_lib_imports
+    - avoid_return_types_on_setters
+    - avoid_shadowing_type_parameters
+    - avoid_types_as_parameter_names
+    - curly_braces_in_flow_control_structures
+    - empty_catches
+    - empty_constructor_bodies
+    - library_names
+    - library_prefixes
+    - no_duplicate_case_values
+    - null_closures
+    - prefer_contains
+    - prefer_equal_for_default_values
+    - prefer_is_empty
+    - prefer_is_not_empty
+    - prefer_iterable_whereType
+    - recursive_getters
+    - slash_for_doc_comments
+    - type_init_formals
+    - unawaited_futures
+    - unnecessary_const
+    - unnecessary_new
+    - unnecessary_null_in_if_null_operators
+    - unrelated_type_equality_checks
+    - use_rethrow_when_possible
+    - valid_regexps
diff --git a/devtools/build/pack/pedantic/analysis_options.yaml b/devtools/build/pack/pedantic/analysis_options.yaml
index 9e6a448..c8cc699 100644
--- a/devtools/build/pack/pedantic/analysis_options.yaml
+++ b/devtools/build/pack/pedantic/analysis_options.yaml
@@ -10,4 +10,4 @@
 # whenever a new version of `package:pedantic` is released. To avoid this,
 # specify a specific version of `analysis_options.yaml` instead.
 
-include: package:pedantic/analysis_options.1.7.0.yaml
+include: package:pedantic/analysis_options.1.8.0.yaml
diff --git a/devtools/build/third_party/split/LICENSE b/devtools/build/third_party/split/LICENSE
deleted file mode 100644
index b01128a..0000000
--- a/devtools/build/third_party/split/LICENSE
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (c) 2018 Nathan Cahill
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/devtools/build/third_party/split/README.md b/devtools/build/third_party/split/README.md
deleted file mode 100644
index ca60c28..0000000
--- a/devtools/build/third_party/split/README.md
+++ /dev/null
@@ -1,594 +0,0 @@
-<p align="center">
-<img alt="Split.js" title="Split.js" src="logo.svg" width="430">
-<br><br>
-<a href="https://circleci.com/gh/nathancahill/split"><img src="https://img.shields.io/circleci/project/github/nathancahill/split/master.svg" alt="Build Status"></a>
-<img src="https://img.badgesize.io/https://unpkg.com/split.js/dist/split.min.js?compression=gzip&label=size&v=1.5.9" alt="File Size">
-<img src="https://badge.fury.io/js/split.js.svg" alt="npm version">
-<img src="https://david-dm.org/nathancahill/split/status.svg" alt="Dependencies">
-<img src = "https://opencollective.com/splitjs/backers/badge.svg" alt="Backers on Open Collective"/>
-<img src = "https://opencollective.com/splitjs/sponsors/badge.svg" alt="Sponsors on Open Collective"/>
-</p>
-
-# Split.js
-
-> 2kb unopinionated utility for resizeable split views.
-
--   **Zero Deps**
--   **Tiny:** Weights 2kb gzipped.
--   **Fast:** No overhead or attached window event listeners, uses pure CSS for resizing.
--   **Unopinionated:** Plays nicely with `float`, `flex` and other layouts.
--   **Compatible:** Works great in IE9, and _even loads in IE8_ with polyfills. Early Firefox/Chrome/Safari/Opera supported too.
-
-## Table of Contents
-
--   [Installation](#installation)
--   [Documentation](#documentation)
--   [Important Note](#important-note)
--   [Options](#options)
--   [Examples](#usage-examples)
--   [Saving State](#saving-state)
--   [Flexbox](#flexbox)
--   [API](#api)
--   [CSS](#css)
--   [React](#react)
--   [Browser Support](#browser-support)
--   [Credits](#credits)
--   [License](#license)
-
-## Installation
-
-Yarn:
-
-```bash
-$ yarn add split.js
-```
-
-npm:
-
-```bash
-$ npm install --save split.js
-```
-
-Include with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.github.io/):
-
-```js
-// using ES6 modules
-import Split from 'split.js'
-
-// using CommonJS modules
-var Split = require('split.js')
-```
-
-The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](http://unpkg.com/):
-
-```html
-<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
-```
-
-or [cdnjs](https://cdnjs.com/):
-
-```html
-<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.5.9/split.min.js"></script>
-```
-
-You can find the library on `window.Split`.
-
-## Documentation
-
-```js
-var split = Split(<HTMLElement|selector[]> elements, <options> options?)
-```
-
-| Options        | Type            | Default        | Description                                              |
-| -------------- | --------------- | -------------- | -------------------------------------------------------- |
-| `sizes`        | Array           |                | Initial sizes of each element in percents or CSS values. |
-| `minSize`      | Number or Array | `100`          | Minimum size of each element.                            |
-| `expandToMin`  | Boolean         | `false`        | Grow initial sizes to `minSize`                          |
-| `gutterSize`   | Number          | `10`           | Gutter size in pixels.                                   |
-| `gutterAlign`  | String          | `'center'`     | Gutter alignment between elements.                       |
-| `snapOffset`   | Number          | `30`           | Snap to minimum size offset in pixels.                   |
-| `dragInterval` | Number          | `1`            | Number of pixels to drag.                                |
-| `direction`    | String          | `'horizontal'` | Direction to split: horizontal or vertical.              |
-| `cursor`       | String          | `'col-resize'` | Cursor to display while dragging.                        |
-| `gutter`       | Function        |                | Called to create each gutter element                     |
-| `elementStyle` | Function        |                | Called to set the style of each element.                 |
-| `gutterStyle`  | Function        |                | Called to set the style of the gutter.                   |
-| `onDrag`       | Function        |                | Callback on drag.                                        |
-| `onDragStart`  | Function        |                | Callback on drag start.                                  |
-| `onDragEnd`    | Function        |                | Callback on drag end.                                    |
-
-## Important Note
-
-Split.js does not set CSS beyond the minimum needed to manage the width or height of the elements.
-This is by design. It makes Split.js flexible and useful in many different situations.
-If you create a horizontal split, you are responsible for (likely) floating the elements and the gutter,
-and setting their heights. See the [CSS](#css) section below. If your gutters are not showing up, check the applied CSS styles.
-
-**THIS IS THE #1 QUESTION ABOUT THE LIBRARY**.
-
-## Options
-
-#### sizes
-
-An array of initial sizes of the elements, specified as percentage values. Example: Setting the initial sizes to `25%` and `75%`.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-})
-```
-
-#### minSize. Default: `100`
-
-An array of minimum sizes of the elements, specified as pixel values. Example: Setting the minimum sizes to `100px` and `300px`, respectively.
-
-```js
-Split(['#one', '#two'], {
-    minSize: [100, 300],
-})
-```
-
-If a number is passed instead of an array, all elements are set to the same minimum size:
-
-```js
-Split(['#one', '#two'], {
-    minSize: 100,
-})
-```
-
-#### expandToMin. Default: `false`
-
-When the split is created, if `expandToMin` is `true`, the minSize for each element overrides the percentage value from the `sizes` option.
-Example: The first element (`#one`) is set to 25% width of the parent container. However, it's `minSize` is `300px`. Using `expandToMin: true` means that
-the first element will always load at at least `300px`, even if `25%` were smaller.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-    minSize: [300, 100],
-    expanedToMin: true,
-})
-```
-
-#### gutterSize. Default: `10`
-
-Gutter size in pixels. Example: Setting the gutter size to `20px`.
-
-```js
-Split(['#one', '#two'], {
-    gutterSize: 20,
-})
-```
-
-#### gutterAlign. Default: `'center'`
-
-Possible options are `'start'`, `'end'` and `'center'`. Determines how the gutter aligns between the two elements.
-`'start'` shrinks the first element to fit the gutter, `'end'` shrinks the second element to fit the gutter and `'center'` shrinks both
-elements by the same amount so the gutter sits between. Added in v1.5.3.
-
-Example: move gutter to the side of the second element:
-
-```js
-Split(['#one', '#two'], {
-    gutterAlign: 'end',
-})
-```
-
-#### snapOffset. Default: `30`
-
-Snap to minimum size at this offset in pixels. Example: Set to `0` to disable to snap effect.
-
-```js
-Split(['#one', '#two'], {
-    snapOffset: 0,
-})
-```
-
-#### dragInterval. Default: `1`
-
-Drag this number of pixels at a time. Defaults to `1` for smooth dragging, but can be set to a pixel value to
-give more control over the resulting sizes. Works particularly well when the `gutterSize` is set to the same size.
-Added in v1.5.3. Example: Drag 20px at a time:
-
-```js
-Split(['#one', '#two'], {
-    dragInterval: 20,
-})
-```
-
-#### direction. Default: `'horizontal'`
-
-Direction to split in. Can be `'vertical'` or `'horizontal'`. Determines which CSS properties are applied (ie. width/height) to each element and gutter. Example: split vertically:
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-})
-```
-
-#### cursor. Default: `'col-resize'`
-
-Cursor to show on the gutter (also applied to the body on dragging to prevent flickering). Defaults to `'col-resize'`for `direction: 'horizontal'` and `'row-resize'` for `direction: 'vertical'`:
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-    cursor: 'row-resize',
-})
-```
-
-#### gutter
-
-Optional function called to create each gutter element. The signature looks like this:
-
-```js
-;(index, direction, pairElement) => HTMLElement
-```
-
-Defaults to creating a `div` with `class="gutter gutter-horizontal"` or `class="gutter gutter-vertical"`, depending on the direction. The default gutter function looks like this:
-
-```js
-;(index, direction) => {
-    const gutter = document.createElement('div')
-    gutter.className = `gutter gutter-${direction}`
-    return gutter
-}
-```
-
-The returned element is then inserted into the DOM, and it's width or height are set. This option can be used to clone an existing DOM element, or to create a new element with custom styles.
-
-Returning a falsey value like `null` or `false` will not insert a gutter. This behavior was added in v1.4.1.
-An additional argument, `pairElement`, is passed to the gutter function: this is the DOM element after (to the right or below) the gutter. This argument was added in v1.4.1.
-
-This final argument makes it easy to return the gutter that has already been created, for example, if `split.destroy()` was called with the option to preserve the gutters.
-
-```js
-;(index, direction, pairElement) => pairElement.previousSibling
-```
-
-#### elementStyle
-
-Optional function called setting the CSS style of the elements. The signature looks like this:
-
-```js
-;(dimension, elementSize, gutterSize, index) => Object
-```
-
-Dimension will be a string, `'width'` or `'height'`, and can be used in the return style. `elementSize` is the target percentage value of the element, and `gutterSize` is the target pixel value of the gutter.
-
-It should return an object with CSS properties to apply to the element. For horizontal splits, the return object looks like this:
-
-```js
-{
-    'width': 'calc(50% - 5px)'
-}
-```
-
-A vertical split style would look like this:
-
-```js
-{
-    'height': 'calc(50% - 5px)'
-}
-```
-
-You might use this function if you're using a different layout like flex (see [Flex Layout](#flex-layout)).
-Flex styles for a horizontal split could return an object like this:
-
-```js
-{
-    'flex-basis': 'calc(50% - 5px)'
-}
-```
-
-#### gutterStyle
-
-Optional function called when setting the CSS style of the gutters. The signature looks like this:
-
-```js
-;(dimension, gutterSize, index) => Object
-```
-
-Dimension is a string, either `'width'` or `'height'`, and `gutterSize` is a pixel value representing the width of the gutter.
-
-It should return a similar object as `elementStyle`, an object with CSS properties to apply to the gutter. Since gutters have fixed widths, it will generally look like this:
-
-```js
-{
-    'width': '10px'
-}
-```
-
-Both `elementStyle` and `gutterStyle` are called continously while dragging, so don't do anything besides return the style object in these functions. Both of these functions should be _pure_, returning the same values for the same inputs and not modifying any external state.
-
-#### onDrag, onDragStart, onDragEnd
-
-Callbacks that can be added on drag (fired continously), drag start and drag end. If doing more than basic operations in `onDrag`, add a debounce function to rate limit the callback.
-
-`onDragStart` and `onDragEnd` are passed the initial and final sizes of the split since it's a common pattern to access the sizes this way.
-
-Their function signature looks like this, where `sizes` is an array of percentage values like returned by `getSizes()`:
-
-```js
-sizes => {}
-```
-
-## Usage Examples
-
-Reference HTML for examples. Gutters are inserted automatically:
-
-```html
-<div>
-    <div id="one">content one</div>
-    <div id="two">content two</div>
-    <div id="three">content three</div>
-</div>
-```
-
-A split with two elements, starting at `25%` and `75%` wide, with `200px` minimum width.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-    minSize: 200,
-})
-```
-
-A split with three elements, starting with even (default) widths and minimum widths set to `100px`, `100px` and `300px`, respectively.
-
-```js
-Split(['#one', '#two', '#three'], {
-    minSize: [100, 100, 300],
-})
-```
-
-A vertical split with two elements.
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-})
-```
-
-## Saving State
-
-Use local storage to save the most recent state:
-
-```js
-var sizes = localStorage.getItem('split-sizes')
-
-if (sizes) {
-    sizes = JSON.parse(sizes)
-} else {
-    sizes = [50, 50] // default sizes
-}
-
-var split = Split(['#one', '#two'], {
-    sizes: sizes,
-    onDragEnd: function(sizes) {
-        localStorage.setItem('split-sizes', JSON.stringify(sizes))
-    },
-})
-```
-
-## Flex Layout
-
-Flex layout is supported easily by adding a `display: flex` to the parent element. The `width` or `height` CSS values
-assigned by default by Split.js work well with flex.
-
-```html
-<div id="flex">
-    <div id="flex-1"></div>
-    <div id="flex-2"></div>
-</div>
-```
-
-And CSS style like this:
-
-```css
-#flex {
-    display: flex;
-    flex-direction: row;
-}
-```
-
-For more complicated flex layouts, the `elementStyle` and `gutterStyle` can be used to set flex-basis:
-
-```js
-Split(['#flex-1', '#flex-2'], {
-    elementStyle: function(dimension, size, gutterSize) {
-        return {
-            'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)',
-        }
-    },
-    gutterStyle: function(dimension, gutterSize) {
-        return {
-            'flex-basis': gutterSize + 'px',
-        }
-    },
-})
-```
-
-## API
-
-Split.js returns an instance with a couple of functions. The instance is returned on creation:
-
-```js
-var instance = Split([], ...)
-```
-
-#### `.setSizes([])`
-
-setSizes behaves the same as the `sizes` configuration option, passing an array of percentages. It updates the sizes of the elements in the split. Added in v1.1.0:
-
-```js
-instance.setSizes([25, 75])
-```
-
-#### `.getSizes()`
-
-getSizes returns an array of percents, suitable for using with `setSizes` or creation. Not supported in IE8. Added in v1.1.2:
-
-```js
-instance.getSizes() > [25, 75]
-```
-
-#### `.collapse(index)`
-
-collapse changes the size of element at `index` to it's `minSize`. Every element except the last is collapsed towards the front (left or top). The last is collapsed towards the back. Not supported in IE8. Added in v1.1.0:
-
-```js
-instance.collapse(0)
-```
-
-#### `.destroy(preserveStyles? = false, preserveGutters? = false)`
-
-Destroy the instance. It removes the gutter elements, and the size CSS styles Split.js set. Added in v1.1.1.
-Passing `preserveStyles = true` does not remove the CSS styles. Option added in v1.4.0.
-Passing `preserveGutters = true` does not remove the gutter elements. Option added in v1.4.1.
-
-```js
-instance.destroy()
-```
-
-## CSS
-
-In being non-opionionated, the only CSS Split.js sets is the widths or heights of the elements. Everything else is left up to you. You must set the elements and gutter heights when using horizontal mode. The gutters will not be visible if their height is 0px. Here's some basic CSS to style the gutters with, although it's not required. Both grip images are included in this repo:
-
-```css
-.gutter {
-    background-color: #eee;
-
-    background-repeat: no-repeat;
-    background-position: 50%;
-}
-
-.gutter.gutter-horizontal {
-    background-image: url('grips/vertical.png');
-    cursor: col-resize;
-}
-
-.gutter.gutter-vertical {
-    background-image: url('grips/horizontal.png');
-    cursor: row-resize;
-}
-```
-
-The grip images are small files and can be included with base64 instead:
-
-```css
-.gutter.gutter-vertical {
-    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
-}
-
-.gutter.gutter-horizontal {
-    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
-}
-```
-
-Split.js also works best when the elements are sized using `border-box`. The `split` class would have to be added manually to apply these styles:
-
-```css
-.split {
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box;
-}
-```
-
-And for horizontal splits, make sure the layout allows elements (including gutters) to be displayed side-by-side. Floating the elements is one option:
-
-```css
-.split,
-.gutter.gutter-horizontal {
-    float: left;
-}
-```
-
-If you use floats, set the height of the elements including the gutters. The gutters will not be visible otherwise if the height is set to 0px.
-
-```css
-.split,
-.gutter.gutter-horizontal {
-    height: 300px;
-}
-```
-
-Overflow can be handled as well, to get scrolling within the elements:
-
-```css
-.split {
-    overflow-y: auto;
-    overflow-x: hidden;
-}
-```
-
-## Browser Support
-
-This library uses [CSS calc()](https://developer.mozilla.org/en-US/docs/Web/CSS/calc#AutoCompatibilityTable), [CSS box-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing#AutoCompatibilityTable) and [JS getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#AutoCompatibilityTable). These features are supported in the following browsers:
-
-| <img src="http://i.imgur.com/dJC1GUv.png" width="48px" height="48px" alt="Chrome logo"> | <img src="http://i.imgur.com/o1m5RcQ.png" width="48px" height="48px" alt="Firefox logo"> | <img src="http://i.imgur.com/8h3iz5H.png" width="48px" height="48px" alt="Internet Explorer logo"> | <img src="http://i.imgur.com/iQV4nmJ.png" width="48px" height="48px" alt="Opera logo"> | <img src="http://i.imgur.com/j3tgNKJ.png" width="48px" height="48px" alt="Safari logo"> | [<img src="https://i.imgur.com/29eVTCg.png" height="28px" alt="Sauce Labs">](https://saucelabs.com) |
-| :-------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------- |
-|                                          22+ ✔                                          |                                           6+ ✔                                           |                                                9+ ✔                                                |                                         15+ ✔                                          |                                         6.2+ ✔                                          | Sponsored ✔                                                                                         |
-
-Gracefully falls back in IE 8 and below to only setting the initial widths/heights and not allowing dragging. IE 8 requires polyfills for `Array.isArray()`, `Array.forEach`, `Array.map`, `Array.filter`, `Object.keys()` and `getComputedStyle`. This script from [Polyfill.io](https://polyfill.io/) includes all of these, adding 1.91 kb to the gzipped size.
-
-This is **ONLY NEEDED** if you are supporting **IE8:**
-
-```html
-<script src="///polyfill.io/v2/polyfill.min.js?features=Array.isArray,Array.prototype.forEach,Array.prototype.map,Object.keys,Array.prototype.filter,getComputedStyle"></script>
-```
-
-Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com).
-
-## Credits
-
-### Contributors
-
-This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
-<a href="graphs/contributors"><img src="https://opencollective.com/splitjs/contributors.svg?width=890&button=false" /></a>
-
-### Backers
-
-Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/splitjs#backer)]
-
-<a href="https://opencollective.com/splitjs#backers" target="_blank"><img src="https://opencollective.com/splitjs/backers.svg?width=890"></a>
-
-### Sponsors
-
-Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/splitjs#sponsor)]
-
-[<img src="https://i.imgur.com/29eVTCg.png" height="28px" alt="Sauce Labs">](https://saucelabs.com)
-
-<a href="https://opencollective.com/splitjs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/0/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/1/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/1/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/2/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/2/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/3/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/3/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/4/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/4/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/5/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/5/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/6/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/6/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/7/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/7/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/8/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/8/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/9/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/9/avatar.svg"></a>
-
-## License
-
-Copyright (c) 2018 Nathan Cahill
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/devtools/build/widgets.json b/devtools/build/widgets.json
index 4f798fb..6e8f09a 100644
--- a/devtools/build/widgets.json
+++ b/devtools/build/widgets.json
@@ -8,7 +8,7 @@
       "Single-child layout widgets"
     ],
     "description": "A convenience widget that combines common painting, positioning, and sizing widgets.",
-    "link": "https://docs.flutter.io/flutter/widgets/Container-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Container-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-container-1' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker><marker id='arrow-container-2' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker><filter id='shadow-container' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur stdDeviation='4'/></filter><linearGradient id='gradient-container' x1='0' y1='0.2' x2='0.4' y2='0.9'><stop offset='55%' stop-color='#ffffff'/><stop offset='100%' stop-color='#fdccdd'/></linearGradient></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='15' y='25' width='70' height='47.5' rx='10' ry='10' fill='#000000' filter='url(#shadow-container)'/><rect x='15' y='25' width='70' height='47.5' rx='10' ry='10' fill='url(#gradient-container)' stroke-width='5' stroke='#3b75ad'/><rect x='30' y='40' width='40' height='30' fill='#4dd0e1'/><line x1='20' y1='55' x2='27' y2='55' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='73' y1='55' x2='80' y2='55' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='50' y1='30' x2='50' y2='37' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='50' y1='78' x2='50' y2='82' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='16' y1='17.5' x2='85' y2='17.5' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-container-2)' marker-end='url(#arrow-container-2)'/><line x1='7.5' y1='26' x2='7.5' y2='72' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-container-2)' marker-end='url(#arrow-container-2)'/></svg>"
   },
   {
@@ -20,7 +20,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Row-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Row-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='30' width='80' height='40' fill='#ffffff'/><rect x='15' y='40' width='20' height='20' fill='#4dd0e1'/><rect x='40' y='35' width='30' height='30' fill='#4dd0e1'/></svg>"
   },
   {
@@ -32,7 +32,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Column-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Column-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='40' y='15' width='20' height='20' fill='#4dd0e1'/><rect x='35' y='40' width='30' height='30' fill='#4dd0e1'/></svg>"
   },
   {
@@ -45,7 +45,7 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Image-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Image-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='60' fill='#ffffff'/><image x='22.5' y='22.5' width='55' height='55' xlink:href='/images/owl.jpg'/></svg>"
   },
   {
@@ -56,12 +56,12 @@
       "Text"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/widgets/Text-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Text-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='30' width='60' height='40' fill='#ffffff'/><text x='50' y='60' text-anchor='middle' font-family='Roboto' font-size='25' fill='#3b75ad'>Abc</text></svg>"
   },
   {
     "name": "Icon",
-    "description": "A material design icon.",
+    "description": "A Material Design icon.",
     "categories": [
       "Basics",
       "Assets, Images, and Icons"
@@ -69,45 +69,45 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Icon-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/Icon-class.html",
+    "image": "<img alt='' src='https://flutter.github.io/assets-for-api-docs/assets/widgets/icon.png'>"
   },
   {
     "name": "RaisedButton",
-    "description": "A material design raised button. A raised button consists of a rectangular piece of material that hovers over the interface.",
+    "description": "A Material Design raised button. A raised button consists of a rectangular piece of material that hovers over the interface.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/RaisedButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/RaisedButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VbDh6YmNiYVc3SHM/components_buttons_usage2.png'>"
   },
   {
     "name": "Scaffold",
-    "sample": "Scaffold_index",
-    "description": "Implements the basic material design visual layout structure. This class provides APIs for showing drawers, snack bars, and bottom sheets.",
+    "sample": "Scaffold",
+    "description": "Implements the basic Material Design visual layout structure. This class provides APIs for showing drawers, snack bars, and bottom sheets.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Scaffold-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Scaffold-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0Bx4BSt6jniD7T0hfM01sSmRyTG8/layout_structure_regions_mobile.png'>"
   },
   {
     "name": "Appbar",
-    "sample": "AppBar_index",
-    "description": "A material design app bar. An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar.",
+    "sample": "AppBar",
+    "description": "A Material Design app bar. An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/AppBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/AppBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VclpfSFpuelBGR1k/components_toolbars.png'>"
   },
   {
@@ -117,7 +117,7 @@
       "Basics"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/material/FlutterLogo-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FlutterLogo-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -129,7 +129,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Stack-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Stack-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='10' width='80' height='80' fill='#ffffff'/><rect x='25' y='25' width='40' height='30' fill='#3b75ad'/><rect x='45' y='35' width='30' height='30' fill='#4dd0e1'/><rect x='40' y='50' width='10' height='10' fill='#f50057'/></svg>"
   },
   {
@@ -139,7 +139,7 @@
       "Basics"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/widgets/Placeholder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Placeholder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -149,39 +149,39 @@
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWG5nei0wWXpoczA/components_bottom_navigation.png'>"
   },
   {
     "name": "TabBar",
-    "sample": "TabBar_index",
-    "description": "A material design widget that displays a horizontal row of tabs.",
+    "sample": "TabBar",
+    "description": "A Material Design widget that displays a horizontal row of tabs.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TabBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TabBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VaWdBdnhMT3ViXzQ/components_tabs.png'>"
   },
   {
     "name": "TabBarView",
-    "sample": "TabBarView_index",
+    "sample": "TabBarView",
     "description": "A page view that displays the widget which corresponds to the currently selected tab. Typically used in conjunction with a TabBar.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TabBarView-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TabBarView-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VaWdBdnhMT3ViXzQ/components_tabs.png'>"
   },
   {
     "name": "MaterialApp",
-    "description": "A convenience widget that wraps a number of widgets that are commonly required for material design applications.",
+    "description": "A convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/MaterialApp-class.html",
+    "link": "https://api.flutter.dev/flutter/material/MaterialApp-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0Bx4BSt6jniD7Y1huOXVQdlFPMmM/materialdesign_introduction.png'>"
   },
   {
@@ -191,17 +191,17 @@
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/WidgetsApp-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/WidgetsApp-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Drawer",
-    "description": "A material design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application.",
+    "description": "A Material Design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Drawer-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Drawer-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VaDhWUXJTTng4ZGs/patterns_navigation_drawer.png'>"
   },
   {
@@ -211,39 +211,49 @@
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/FloatingActionButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FloatingActionButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VN20tOXJoUjVxQjg/components_buttons_fab.png'>"
   },
   {
     "name": "FlatButton",
-    "description": "A flat button is a section printed on a Material widget that reacts to touches by filling with color.",
+    "description": "A flat button is a section printed on a Material Components widget that reacts to touches by filling with color.",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/FlatButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FlatButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VNDg3V3ZjU2hsNGc/components_buttons_usage3.png'>"
   },
   {
     "name": "IconButton",
-    "sample": "IconButton_index",
+    "sample": "IconButton",
     "description": "An icon button is a picture printed on a Material widget that reacts to touches by filling with color (ink).",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/IconButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/IconButton-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B_udO5B8pzrzdXVuTlBoOTBjcU0/components_buttons_other1.png'>"
   },
   {
+    "name": "DropdownButton",
+    "description": "Shows the currently selected item and an arrow that opens a menu for selecting another item.",
+    "categories": [],
+    "subcategories": [
+      "Buttons"
+    ],
+    "link": "https://api.flutter.dev/flutter/material/DropdownButton-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
     "name": "PopupMenuButton",
-    "sample": "PopupMenuButton_index",
+    "sample": "PopupMenuButton",
     "description": "Displays a menu when pressed and calls onSelected when the menu is dismissed because an item was selected.",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/PopupMenuButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/PopupMenuButton-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VakJ6a0F2MFJaaDQ/components_menus.png'>"
   },
   {
@@ -253,7 +263,7 @@
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ButtonBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ButtonBar-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -263,7 +273,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TextField-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TextField-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VbURLNTM0N0R6eUE/components_text_fields.png'>"
   },
   {
@@ -273,7 +283,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Checkbox-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Checkbox-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7T2xGbGo0cUlPVG8/components_switches_check1.png'>"
   },
   {
@@ -283,7 +293,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Radio-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Radio-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7Z1NaaXh2ZkpDRkE/components_switches_radio1.png'>"
   },
   {
@@ -293,7 +303,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Switch-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Switch-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7NDg4aGIzVXYxVEE/components_switches_switch1.png'>"
   },
   {
@@ -303,7 +313,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Slider-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Slider-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VTmJrQUYzajFIclE/components_sliders.png'>"
   },
   {
@@ -313,7 +323,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/showDatePicker.html",
+    "link": "https://api.flutter.dev/flutter/material/showDatePicker.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VY2h4WElGdEhPb2c/components_pickers.png'>"
   },
   {
@@ -323,7 +333,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/SimpleDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/material/SimpleDialog-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VVGNnN3NvMGdoQTg/components_dialogs.png'>"
   },
   {
@@ -333,7 +343,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/AlertDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/material/AlertDialog-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0Bzhp5Z4wHba3TzFHYVlrbWF2bnM/components_alerts_1.png'>"
   },
   {
@@ -343,7 +353,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/BottomSheet-class.html",
+    "link": "https://api.flutter.dev/flutter/material/BottomSheet-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VVWZzZ1FIN09XWGc/components_bottom_sheets.png'>"
   },
   {
@@ -353,7 +363,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ExpansionPanel-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ExpansionPanel-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VOXF3eEJ3azZMSjg/components_expansion_panels.png'>"
   },
   {
@@ -363,17 +373,17 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/SnackBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/SnackBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VSjZkendtc19iZ2M/components_snackbars.png'>"
   },
   {
     "name": "Chip",
-    "description": "A material design chip. Chips represent complex entities in small blocks, such as a contact.",
+    "description": "A Material Design chip. Chips represent complex entities in small blocks, such as a contact.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Chip-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Chip-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VM1VORGxxWUx5U0E/components_chips.png'>"
   },
   {
@@ -383,7 +393,7 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Tooltip-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Tooltip-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VZ1JKMzJFcmhOWkk/components_tooltips.png'>"
   },
   {
@@ -393,38 +403,38 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/DataTable-class.html",
+    "link": "https://api.flutter.dev/flutter/material/DataTable-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWTJHMmJZdWZ5LU0/components_data_tables.png'>"
   },
   {
     "name": "Card",
-    "description": "A material design card. A card has slightly rounded corners and a shadow.",
+    "description": "A Material Design card. A card has slightly rounded corners and a shadow.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Card-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Card-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VR0ptbC1RV1NLNlk/components_cards.png'>"
   },
   {
     "name": "LinearProgressIndicator",
-    "description": "Progress and activity indicators are visual indications of an app loading content. The LinearProgressIndicator widget implements this component. In addition you may also use the CircularProgressIndicator widget.",
+    "description": "A material design linear progress indicator, also known as a progress bar.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/LinearProgressIndicator-class.html",
+    "link": "https://api.flutter.dev/flutter/material/LinearProgressIndicator-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWkJiRjRLbzRNS3M/components_progress_and_activity.png'>"
   },
   {
-    "name": "GridView",
-    "description": "A grid list consists of a repeated pattern of cells arrayed in a vertical and horizontal layout. The GridView widget implements this component.",
+    "name": "CircularProgressIndicator",
+    "description": "A material design circular progress indicator, which spins to indicate that the application is busy.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
-    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VandQYXpNMG9aQUk/components_grid_lists.png'>"
+    "link": "https://api.flutter.dev/flutter/material/CircularProgressIndicator-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ListTile",
@@ -433,17 +443,17 @@
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ListTile-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ListTile-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0Bx4BSt6jniD7UUw0bzVwU2lMUHc/components_lists_keylines_single10.png'>"
   },
   {
     "name": "Stepper",
-    "description": "A material stepper widget that displays progress through a sequence of steps.",
+    "description": "A Material Design stepper widget that displays progress through a sequence of steps.",
     "categories": [],
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Stepper-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Stepper-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VTndyUnNCR2tQREE/components_steppers.png'>"
   },
   {
@@ -453,17 +463,27 @@
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Divider-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Divider-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VUVlmbHM1Q013RU0/components_dividers.png'>"
   },
   {
+    "name": "CupertinoActionSheet",
+    "description": "An iOS-style modal bottom action sheet to choose an option among many.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoActionSheet-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-action-sheet.png'>"
+  },
+  {
     "name": "CupertinoActivityIndicator",
     "description": "An iOS-style activity indicator. Displays a circular 'spinner'.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoActivityIndicator-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-activity-indicator.png'>"
   },
   {
@@ -473,8 +493,8 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoAlertDialog-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoAlertDialog-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-alert-dialog.png'>"
   },
   {
     "name": "CupertinoButton",
@@ -483,17 +503,27 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoButton-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoButton-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-button.png'>"
   },
   {
+    "name": "CupertinoDatePicker",
+    "description": "An iOS-style date or date and time picker.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDatePicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-date-picker.png'>"
+  },
+  {
     "name": "CupertinoDialog",
     "description": "An iOS-style dialog.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDialog-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-dialog.png'>"
   },
   {
@@ -503,17 +533,87 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoDialogAction-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDialogAction-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-dialog-action.png'>"
+  },
+  {
+    "name": "CupertinoFullscreenDialogTransition",
+    "description": "An iOS-style transition used for summoning fullscreen dialogs.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoFullscreenDialogTransition-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-fullscreen-dialog-transition.png'>"
+  },
+  {
+    "name": "CupertinoPageScaffold",
+    "description": "Basic iOS style page layout structure. Positions a navigation bar and content on a background.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPageScaffold-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-page-scaffold.png'>"
+  },
+  {
+    "name": "CupertinoPageTransition",
+    "description": "Provides an iOS-style page transition animation.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPageTransition-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-page-transition.png'>"
+  },
+  {
+    "name": "CupertinoPicker",
+    "description": "An iOS-style picker control. Used to select an item in a short list.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-picker.png'>"
+  },
+  {
+    "name": "CupertinoPopupSurface",
+    "description": "Rounded rectangle surface that looks like an iOS popup surface, such as an alert dialog or action sheet.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPopupSurface-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
+    "name": "CupertinoScrollbar",
+    "description": "An iOS-style scrollbar that indicates which portion of a scrollable widget is currently visible.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoScrollbar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-scrollbar.png'>"
+  },
+  {
+    "name": "CupertinoSegmentedControl",
+    "description": "An iOS-style segmented control. Used to select mutually exclusive options in a horizontal list.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSegmentedControl-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-segmented-control.png'>"
+  },
+  {
     "name": "CupertinoSlider",
     "description": "Used to select from a range of values.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoSlider-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSlider-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-slider.png'>"
   },
   {
@@ -523,28 +623,68 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSwitch-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-switch.png'>"
   },
   {
-    "name": "CupertinoPageTransition",
-    "description": "Provides an iOS-style page transition animation.",
+    "name": "CupertinoNavigationBar",
+    "description": "An iOS-style top navigation bar. Typically used with CupertinoPageScaffold.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoPageTransition-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoNavigationBar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-nav-bar.png'>"
   },
   {
-    "name": "CupertinoFullscreenDialogTransition",
-    "description": "An iOS-style transition used for summoning fullscreen dialogs.",
+    "name": "CupertinoTabBar",
+    "description": "An iOS-style bottom tab bar. Typically used with CupertinoTabScaffold.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoFullscreenDialogTransition-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabBar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-bar.png'>"
+  },
+  {
+    "name": "CupertinoTabScaffold",
+    "description": "Tabbed iOS app structure. Positions a tab bar on top of tabs of content.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabScaffold-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-scaffold.png'>"
+  },
+  {
+    "name": "CupertinoTabView",
+    "description": "Root content of a tab that supports parallel navigation between tabs. Typically used with CupertinoTabScaffold.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabView-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-view.png'>"
+  },
+  {
+    "name": "CupertinoTextField",
+    "description": "An iOS-style text field.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTextField-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-textfield.png'>"
+  },
+  {
+    "name": "CupertinoTimerPicker",
+    "description": "An iOS-style countdown timer picker.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTimerPicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-timer-picker.png'>"
   },
   {
     "name": "Padding",
@@ -555,7 +695,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Padding-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Padding-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-padding' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='25' y='30' width='50' height='30' fill='#4dd0e1'/><line x1='13' y1='45' x2='22' y2='45' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='78' y1='45' x2='87' y2='45' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='50' y1='23' x2='50' y2='27' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='50' y1='63' x2='50' y2='77' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/></svg>"
   },
   {
@@ -566,7 +706,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Center-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Center-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-center' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='25' y='35' width='50' height='30' fill='#4dd0e1'/><line x1='10' y1='50' x2='22' y2='50' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='90' y1='50' x2='78' y2='50' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='50' y1='20' x2='50' y2='32' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='50' y1='80' x2='50' y2='68' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/></svg>"
   },
   {
@@ -577,7 +717,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Align-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Align-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-align' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='15' y='50' width='50' height='30' fill='#4dd0e1'/><line x1='10' y1='65' x2='12' y2='65' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/><line x1='90' y1='65' x2='68' y2='65' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/><line x1='40' y1='20' x2='40' y2='47' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/></svg>"
   },
   {
@@ -588,7 +728,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FittedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FittedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -599,7 +739,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AspectRatio-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AspectRatio-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-aspect-ratio' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='35' width='50' height='30' fill='#ffffff'/><rect x='27.5' y='37.5' width='45' height='25' fill='#4dd0e1'/><line x1='31' y1='41' x2='69' y2='59' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-aspect-ratio)' marker-end='url(#arrow-aspect-ratio)'/></svg>"
   },
   {
@@ -610,7 +750,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ConstrainedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ConstrainedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-constrained-box' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='30' width='50' height='40' fill='#ffffff'/><rect x='22.5' y='32.5' width='45' height='35' fill='#4dd0e1'/><path d='M63 23 v 40' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M77 23 v 54' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M13 63 h 50' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M13 77 h 64' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M50 25 L60 25' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M90 25 L80 25' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M15 50 L15 60' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M15 90 L15 80' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/></svg>"
   },
   {
@@ -621,7 +761,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Baseline-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Baseline-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-baseline' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='50' fill='#ffffff'/><rect x='20' y='45' width='60' height='30' fill='#4dd0e1'/><line x1='85' y1='20' x2='85' y2='66' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-baseline)'/><line x1='15' y1='20' x2='15' y2='66' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-baseline)'/><line x1='10' y1='70' x2='90' y2='70' stroke='#ffffff' stroke-width='2'/><text x='50' y='69' text-anchor='middle' font-family='Roboto' font-size='25' fill='#3b75ad'>Abc </text></svg>"
   },
   {
@@ -632,7 +772,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FractionallySizedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FractionallySizedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -643,7 +783,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IntrinsicHeight-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IntrinsicHeight-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -654,7 +794,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IntrinsicWidth-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IntrinsicWidth-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -665,7 +805,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LimitedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LimitedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -676,7 +816,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Offstage-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Offstage-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -687,7 +827,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/OverflowBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/OverflowBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -698,7 +838,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-sized-box' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='35' width='50' height='30' fill='#ffffff'/><rect x='27.5' y='37.5' width='45' height='25' fill='#4dd0e1'/><line x1='28' y1='30' x2='72' y2='30' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-sized-box)' marker-end='url(#arrow-sized-box)'/><line x1='20' y1='38' x2='20' y2='62' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-sized-box)' marker-end='url(#arrow-sized-box)'/></svg>"
   },
   {
@@ -709,18 +849,19 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizedOverflowBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizedOverflowBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Transform",
     "description": "A widget that applies a transformation before painting its child.",
     "categories": [
+      "Painting and effects"
     ],
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Transform-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Transform-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='17.5' y='42.5' width='50' height='30' fill='#ffffff'/><rect x='20' y='45' width='45' height='25' fill='#3b75ad'/><rect x='20' y='45' width='45' height='25' fill='#4dd0e1' transform='translate(15 -5) rotate(-23) skewX(-10)'/></svg>"
   },
   {
@@ -731,7 +872,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomSingleChildLayout-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomSingleChildLayout-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -742,19 +883,21 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IndexedStack-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IndexedStack-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "GridView",
-    "description": "A scrollable, 2D array of widgets.",
+    "description": "A grid list consists of a repeated pattern of cells arrayed in a vertical and horizontal layout. The GridView widget implements this component.",
     "categories": [
+      "Scrolling"
     ],
     "subcategories": [
+      "Information displays",
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/GridView-class.html",
+    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VandQYXpNMG9aQUk/components_grid_lists.png'>"
   },
   {
     "name": "Flow",
@@ -764,7 +907,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Flow-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Flow-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -775,7 +918,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Table-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Table-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -786,7 +929,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Wrap-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Wrap-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -797,22 +940,10 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListBody-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ListBody-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
-    "name": "ListView",
-    "sample": "ListView_index",
-    "description": "A scrollable, linear list of widgets. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.",
-    "categories": [
-    ],
-    "subcategories": [
-      "Multi-child layout widgets"
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListView-class.html",
-    "image": "<svg viewBox='0 0 100 100'><filter id='inset-shadow-block' x='-50%' y='-50%' width='200%' height='200%'><feComponentTransfer in='SourceAlpha'><feFuncA type='table' tableValues='1 0' /></feComponentTransfer><feGaussianBlur stdDeviation='2' result='Blur'/><feFlood flood-color='#666666' result='color'/><feComposite in2='Blur' operator='in'/><feComposite in2='SourceAlpha' operator='in' /><feMerge><feMergeNode /></feMerge></filter><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='35' y='10' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='30' width='30' height='20' fill='#4dd0e1'/><rect x='35' y='55' width='30' height='5' fill='#4dd0e1'/><rect x='35' y='65' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='85' width='30' height='5' fill='#4dd0e1'/><rect x='30' y='10' width='40' height='80' fill='#ffffff' filter='url(#inset-shadow-block)'/></svg>"
-  },
-  {
     "name": "CustomMultiChildLayout",
     "description": "A widget that uses a delegate to size and position multiple children.",
     "categories": [
@@ -820,7 +951,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomMultiChildLayout-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomMultiChildLayout-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -829,9 +960,9 @@
     "categories": [
     ],
     "subcategories": [
-      "Layout helpers"
+      "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LayoutBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -842,7 +973,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RichText-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RichText-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -853,7 +984,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DefaultTextStyle-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -864,7 +995,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RawImage-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RawImage-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -875,7 +1006,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/services/AssetBundle-class.html",
+    "link": "https://api.flutter.dev/flutter/services/AssetBundle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -886,7 +1017,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Form-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Form-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -897,7 +1028,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FormField-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FormField-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -908,7 +1039,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RawKeyboardListener-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RawKeyboardListener-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -919,7 +1050,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedContainer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -930,7 +1061,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedCrossFade-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedCrossFade-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -942,7 +1073,7 @@
     "subcategories": [
       "Routing"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Hero-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Hero-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -953,7 +1084,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -964,7 +1095,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Draggable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Draggable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -975,7 +1106,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LongPressDraggable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LongPressDraggable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -986,7 +1117,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GestureDetector-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/GestureDetector-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -997,7 +1128,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DragTarget-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DragTarget-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1008,7 +1139,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Dismissible-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Dismissible-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1019,7 +1150,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IgnorePointer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IgnorePointer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1030,7 +1161,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AbsorbPointer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AbsorbPointer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1041,7 +1172,7 @@
     "subcategories": [
       "Routing"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Navigator-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Navigator-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1052,7 +1183,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/material/Theme-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Theme-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1063,40 +1194,31 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/MediaQuery-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/MediaQuery-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ListView",
+    "sample": "ListView",
     "description": "A scrollable, linear list of widgets. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
+      "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/ListView-class.html",
+    "image": "<svg viewBox='0 0 100 100'><filter id='inset-shadow-block' x='-50%' y='-50%' width='200%' height='200%'><feComponentTransfer in='SourceAlpha'><feFuncA type='table' tableValues='1 0' /></feComponentTransfer><feGaussianBlur stdDeviation='2' result='Blur'/><feFlood flood-color='#666666' result='color'/><feComposite in2='Blur' operator='in'/><feComposite in2='SourceAlpha' operator='in' /><feMerge><feMergeNode /></feMerge></filter><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='35' y='10' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='30' width='30' height='20' fill='#4dd0e1'/><rect x='35' y='55' width='30' height='5' fill='#4dd0e1'/><rect x='35' y='65' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='85' width='30' height='5' fill='#4dd0e1'/><rect x='30' y='10' width='40' height='80' fill='#ffffff' filter='url(#inset-shadow-block)'/></svg>"
   },
   {
     "name": "NestedScrollView",
-    "description": "",
+    "description": "A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/NestedScrollView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
-  },
-  {
-    "name": "GridView",
-    "description": "A scrollable, 2D array of widgets. The most commonly used grid layouts are GridView.count, which creates a layout with a fixed number of tiles in the cross axis, and GridView.extent, which creates a layout with tiles that have a maximum cross-axis extent. A custom SliverGridDelegate can produce an arbitrary 2D arrangement of children, including arrangements that are unaligned or overlapping.",
-    "categories": [
-      "Scrolling"
-    ],
-    "subcategories": [
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1107,7 +1229,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SingleChildScrollView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1119,18 +1241,18 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Scrollable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Scrollable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Scrollbar",
-    "description": "A material design scrollbar. A scrollbar indicates which portion of a Scrollable widget is actually visible.",
+    "description": "A Material Design scrollbar. A scrollbar indicates which portion of a Scrollable widget is actually visible.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/material/Scrollbar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Scrollbar-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1141,7 +1263,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomScrollView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1152,7 +1274,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/NotificationListener-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/NotificationListener-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1163,10 +1285,21 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ScrollConfiguration-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ScrollConfiguration-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
+    "name": "RefreshIndicator",
+    "description": "A Material Design pull-to-refresh wrapper for scrollables.",
+    "categories": [
+      "Scrolling"
+    ],
+    "subcategories": [
+    ],
+    "link": "https://api.flutter.dev/flutter/material/RefreshIndicator-class.html",
+    "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_12/assets/0B7WCemMG6e0VS2kzSmZwNnNKQVk/patterns-swipe-to-refresh.png'>"
+  },
+  {
     "name": "Opacity",
     "description": "A widget that makes its child partially transparent.",
     "categories": [
@@ -1174,21 +1307,10 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Opacity-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Opacity-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='10' width='60' height='60' fill='#3b75ad'/><rect x='20' y='30' width='70' height='50' fill='#ffffff' opacity='0.8'/></svg>"
   },
   {
-    "name": "Transform",
-    "description": "A widget that applies a transformation before painting its child.",
-    "categories": [
-      "Painting and effects"
-    ],
-    "subcategories": [
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/Transform-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
-  },
-  {
     "name": "DecoratedBox",
     "description": "A widget that paints a Decoration either before or after its child paints.",
     "categories": [
@@ -1196,7 +1318,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DecoratedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DecoratedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><filter id='shadow-decorated-box' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur stdDeviation='4'/></filter><linearGradient id='gradient-decorated-box' x1='0' y1='0' x2='0.5' y2='1'><stop offset='50%' stop-color='#ffffff'/><stop offset='100%' stop-color='#f50057'/></linearGradient></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='25' width='50' height='50' rx='10' ry='10' fill='#000000' filter='url(#shadow-decorated-box)'/><rect x='25' y='25' width='50' height='50' rx='10' ry='10' fill='url(#gradient-decorated-box)' stroke-width='5' stroke='#4dd0e1'/></svg>"
   },
   {
@@ -1207,7 +1329,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FractionalTranslation-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FractionalTranslation-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1218,7 +1340,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RotatedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RotatedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1229,7 +1351,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipOval-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipOval-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><clipPath id='clip-oval'><circle cx='40' cy='50' r='30'/></clipPath></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' opacity='0.05'/><circle cx='40' cy='50' r='30' fill='#ffffff'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' clip-path='url(#clip-oval)'/></svg>"
   },
   {
@@ -1240,7 +1362,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipPath-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipPath-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1251,7 +1373,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipRect-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipRect-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><clipPath id='clip-rect'><rect x='10' y='20' width='60' height='60'/></clipPath></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' opacity='0.05'/><rect x='10' y='20' width='60' height='60' fill='#ffffff'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' clip-path='url(#clip-rect)'/></svg>"
   },
   {
@@ -1262,7 +1384,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomPaint-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomPaint-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='60' fill='#ffffff'/><ellipse cx='40' cy='35' rx='10' ry='10' stroke-width='5' stroke='#4dd0e1' fill='transparent'/><ellipse cx='20' cy='71' rx='15' ry='20' stroke-width='5' stroke='#4dd0e1' fill='transparent' transform='rotate(-25)'/></svg>"
   },
   {
@@ -1273,7 +1395,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/BackdropFilter-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/BackdropFilter-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1284,7 +1406,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Semantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Semantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1295,18 +1417,18 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/MergeSemantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/MergeSemantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ExcludeSemantics",
-    "description": "A widget that drops all the semantics of its descendants. This can be used to hide subwidgets that would otherwise be reported but that would only be confusing. For example, the material library's Chip widget hides the avatar since it is redundant with the chip label.",
+    "description": "A widget that drops all the semantics of its descendants. This can be used to hide subwidgets that would otherwise be reported but that would only be confusing. For example, the Material Components Chip widget hides the avatar since it is redundant with the chip label.",
     "categories": [
       "Accessibility"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ExcludeSemantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ExcludeSemantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1317,7 +1439,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1328,7 +1450,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1339,7 +1461,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DecoratedBoxTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DecoratedBoxTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1350,7 +1472,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FadeTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FadeTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1361,7 +1483,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/PositionedTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/PositionedTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1372,7 +1494,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RotationTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RotationTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1383,7 +1505,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ScaleTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ScaleTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1394,7 +1516,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizeTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizeTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1405,7 +1527,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SlideTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SlideTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1416,19 +1538,19 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedDefaultTextStyle-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedDefaultTextStyle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "AnimatedListState",
-    "sample": "AnimatedListState_index",
+    "sample": "AnimatedListState",
     "description": "The state for a scrolling container that animates items when they are inserted or removed.",
     "categories": [
       "Animation and Motion"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedListState-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1439,7 +1561,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedModalBarrier-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedModalBarrier-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1450,7 +1572,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedOpacity-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedOpacity-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1461,7 +1583,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedPhysicalModel-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedPhysicalModel-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1472,7 +1594,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedPositioned-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1483,7 +1605,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedSize-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1494,7 +1616,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedWidget-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedWidget-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1505,7 +1627,40 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedWidgetBaseState-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedWidgetBaseState-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "Expanded",
+    "categories": [
+    ],
+    "subcategories": [
+      "Multi-child layout widgets"
+    ],
+    "description": "A widget that expands a child of a Row, Column, or Flex.",
+    "link": "https://api.flutter.dev/flutter/widgets/Expanded-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "PageView",
+    "description": "A scrollable list that works page by page.",
+    "categories": [
+      "Scrolling"
+    ],
+    "subcategories": [
+    ],
+    "link": "https://api.flutter.dev/flutter/widgets/PageView-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "SliverAppBar",
+    "description": "A material design app bar that integrates with a CustomScrollView.",
+    "categories": [
+    ],
+    "subcategories": [
+      "App structure and navigation"
+    ],
+    "link": "https://api.flutter.dev/flutter/material/SliverAppBar-class.html",
+    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VclpfSFpuelBGR1k/components_toolbars.png'>"
   }
 ]
\ No newline at end of file
diff --git a/devtools/example/test.dart b/devtools/example/test.dart
index d45eb4d..2c4da91 100644
--- a/devtools/example/test.dart
+++ b/devtools/example/test.dart
@@ -27,8 +27,9 @@
 
     final List<int> numbers = List.generate(10, (index) => index * index);
     // ignore: unused_local_variable
-    final Map<int, String> numberDescriptions =
-        Map.fromIterable(numbers, value: (key) => _toWord(key));
+    final Map<int, String> numberDescriptions = {
+      for (var n in numbers) n: _toWord(n)
+    };
 
     await bar(count++);
 
diff --git a/devtools/lib/devtools.dart b/devtools/lib/devtools.dart
index afa6bf1..9159f87 100644
--- a/devtools/lib/devtools.dart
+++ b/devtools/lib/devtools.dart
@@ -5,4 +5,4 @@
 /// The DevTools application version.
 // Note: when updating this, please update the corresponding version in the
 // pubspec.
-const String version = '0.1.1';
+const String version = '0.1.6-dev.3';
diff --git a/devtools/lib/src/timeline/flame_chart_canvas.dart b/devtools/lib/src/charts/flame_chart_canvas.dart
similarity index 71%
rename from devtools/lib/src/timeline/flame_chart_canvas.dart
rename to devtools/lib/src/charts/flame_chart_canvas.dart
index 96f7974..d370660 100644
--- a/devtools/lib/src/timeline/flame_chart_canvas.dart
+++ b/devtools/lib/src/charts/flame_chart_canvas.dart
@@ -9,6 +9,7 @@
 
 import 'package:meta/meta.dart';
 
+import '../ui/colors.dart';
 import '../ui/drag_scroll.dart';
 import '../ui/elements.dart';
 import '../ui/fake_flutter/fake_flutter.dart';
@@ -16,15 +17,12 @@
 import '../ui/theme.dart';
 import '../ui/viewport_canvas.dart';
 import '../utils.dart';
-import 'cpu_profile_protocol.dart';
-import 'frame_flame_chart.dart';
-import 'timeline.dart';
 
-// TODO(kenzie): add tooltips to stack frames on hover.
+// TODO(kenzie): add tooltips to nodes on hover.
 
 // We use the same color in light and dark mode because it aligns well with both
 // color schemes.
-const _selectedFlameChartNodeColor = ThemedColor(
+const _selectedNodeColor = ThemedColor(
   mainUiColorSelectedLight,
   mainUiColorSelectedLight,
 );
@@ -32,46 +30,40 @@
 const _shadedBackgroundColor =
     ThemedColor(Color(0xFFF6F6F6), Color(0xFF202124));
 
-const _fontSize = 14.0;
-const _textOffsetY = 18.0;
-const _flameChartTop = rowHeightWithPadding;
-const _rowHeight = 25.0;
-const _rowPadding = 2.0;
-const rowHeightWithPadding = _rowHeight + _rowPadding;
-
-const _flameChartInset = 70;
+const double _fontSize = 14.0;
+const double _textOffsetY = 18.0;
+const double rowPadding = 2.0;
+const double rowHeight = 25.0;
+const double rowHeightWithPadding = rowHeight + rowPadding;
+const double topOffset = rowHeightWithPadding;
+const double sideInset = 70.0;
 
 List<num> _asciiMeasurements;
 
-// TODO(kenzie): move this class to flame_chart.dart once the frame flame chart
-// is ported to canvas and the current implementation in flame_chart.dart is
-// deleted.
-abstract class FlameChart {
+abstract class FlameChart<T> {
   FlameChart({
     @required this.data,
-    @required this.flameChartWidth,
-    @required this.flameChartHeight,
-  }) : timelineGrid = TimelineGrid(data.duration, flameChartWidth) {
-    _initRows();
+    @required this.duration,
+    @required this.width,
+    @required this.height,
+  }) : timelineGrid = TimelineGrid(duration, width) {
+    initRows();
   }
 
-  static const stackFramePadding = 1;
+  final T data;
 
-  final CpuProfileData data;
+  final Duration duration;
 
   // These values are not final because the flame chart viewport can change in
   // size.
-  double flameChartWidth;
-  double flameChartHeight;
+  double width;
+  double height;
 
-  double get flameChartWidthWithInsets =>
-      getFlameChartWidth() + 2 * _flameChartInset;
+  double get widthWithInsets => calculatedWidth + 2 * sideInset;
 
-  final _stackFrameSelectedController =
-      StreamController<CpuStackFrame>.broadcast();
+  final _nodeSelectedController = StreamController<FlameChartNode>.broadcast();
 
-  Stream<CpuStackFrame> get onStackFrameSelected =>
-      _stackFrameSelectedController.stream;
+  Stream<FlameChartNode> get onNodeSelected => _nodeSelectedController.stream;
 
   FlameChartNode selectedNode;
 
@@ -88,77 +80,12 @@
   // scroll offset to reduce floating point error when zooming.
   num floatingPointScrollLeft = 0;
 
-  int _colorOffset = 0;
+  void initRows();
 
-  // TODO(kenzie): base colors on categories (Widget, Render, Layer, User code,
-  // etc.)
-  Color nextColor() {
-    final color = uiColorPalette[_colorOffset % uiColorPalette.length];
-    _colorOffset++;
-    return color;
-  }
-
-  void _initRows() {
-    for (int i = 0; i < data.cpuProfileRoot.depth; i++) {
-      rows.add(FlameChartRow(nodes: [], index: i));
-    }
-
-    final totalWidth = flameChartWidth - 2 * _flameChartInset;
-
-    final Map<String, double> stackFrameLefts = {};
-
-    double calculateLeftForStackFrame(CpuStackFrame stackFrame) {
-      double left;
-      if (stackFrame.parent == null) {
-        left = _flameChartInset.toDouble();
-      } else {
-        final stackFrameIndex = stackFrame.index;
-        if (stackFrameIndex == 0) {
-          // This is the first child of parent. [left] should equal the left
-          // value of [stackFrame]'s parent.
-          left = stackFrameLefts[stackFrame.parent.id];
-        } else {
-          assert(stackFrameIndex != -1);
-          // [stackFrame] is not the first child of its parent. [left] should
-          // equal the right value of its previous sibling.
-          final previous = stackFrame.parent.children[stackFrameIndex - 1];
-          left = stackFrameLefts[previous.id] +
-              (totalWidth * previous.cpuConsumptionRatio);
-        }
-      }
-      stackFrameLefts[stackFrame.id] = left;
-      return left;
-    }
-
-    void createChartNodes(CpuStackFrame stackFrame, int row) {
-      final double width =
-          totalWidth * stackFrame.cpuConsumptionRatio - stackFramePadding;
-      final left = calculateLeftForStackFrame(stackFrame);
-      final top = (row * rowHeightWithPadding + _flameChartTop).toDouble();
-
-      final node = FlameChartNode(
-        Rect.fromLTRB(left, top, left + width, top + _rowHeight),
-        nextColor(),
-        Colors.black,
-        Colors.black,
-        stackFrame,
-      );
-
-      rows[row].nodes.add(node);
-
-      for (CpuStackFrame child in stackFrame.children) {
-        createChartNodes(
-          child,
-          row + 1,
-        );
-      }
-    }
-
-    createChartNodes(data.cpuProfileRoot, 0);
-  }
+  double get calculatedWidth;
 
   void selectNodeAtOffset(Offset offset) {
-    final node = getNode(offset);
+    final node = nodeAtOffset(offset);
 
     // Do nothing if the tap did not occur on any nodes, if the tap was to
     // select the already selected node.
@@ -174,22 +101,18 @@
     node.selected = true;
     selectedNode = node;
 
-    _stackFrameSelectedController.add(node.stackFrame);
+    _nodeSelectedController.add(node);
   }
 
-  num getFlameChartWidth() {
-    return rows[0].nodes[0].rect.right - _flameChartInset;
-  }
-
-  FlameChartNode getNode(Offset offset) {
-    final int rowIndex = getRowIndexForY(offset.dy);
+  FlameChartNode nodeAtOffset(Offset offset) {
+    final int rowIndex = rowIndexForY(offset.dy);
     if (rowIndex < 0 || rowIndex >= rows.length) {
       return null;
     }
-    return getNodeInRow(rowIndex, offset.dx);
+    return nodeInRow(rowIndex, offset.dx);
   }
 
-  FlameChartNode getNodeInRow(int rowIndex, double x) {
+  FlameChartNode nodeInRow(int rowIndex, double x) {
     final row = rows[rowIndex];
     final nodes = row.nodes;
 
@@ -215,31 +138,36 @@
     return nodes.isEmpty ? null : binarySearchForNode();
   }
 
-  int getRowIndexForY(double y) {
-    if (y < _flameChartTop) {
+  double relativeYPosition(double absoluteY) => absoluteY - topOffset;
+
+  int rowIndexForY(double y) {
+    if (y < topOffset) {
       return -1;
     }
-    return math.max((y - _flameChartTop) ~/ rowHeightWithPadding, 0);
+    return math.max((relativeYPosition(y)) ~/ rowHeightWithPadding, 0);
   }
 }
 
-class FlameChartCanvas extends FlameChart {
+abstract class FlameChartCanvas<T> extends FlameChart {
   FlameChartCanvas({
-    @required CpuProfileData data,
-    @required flameChartWidth,
-    @required flameChartHeight,
+    @required T data,
+    @required Duration duration,
+    @required double width,
+    @required double height,
+    String classes,
   }) : super(
           data: data,
-          flameChartWidth: flameChartWidth,
-          flameChartHeight: flameChartHeight,
+          duration: duration,
+          width: width,
+          height: height,
         ) {
     _viewportCanvas = ViewportCanvas(
       paintCallback: _paintCallback,
       onTap: _onTap,
-      classes: 'ui-details-section cpu-flame-chart',
+      classes: 'fill-section $classes',
     )..element.element.style.overflow = 'hidden';
 
-    _viewportCanvas.setContentSize(flameChartWidth, flameChartHeight);
+    _viewportCanvas.setContentSize(width, height);
 
     _dragScroll.enableDragScrolling(_viewportCanvas.element);
     _dragScroll.onVerticalScroll = () {
@@ -271,6 +199,9 @@
   final _minZoomLevel = 1;
 
   void _initAsciiMeasurements() {
+    // We have already initialized the list of Ascii measurements.
+    if (_asciiMeasurements != null) return;
+
     final measurementCanvas = CanvasElement().context2D
       ..font = fontStyleToCss(const TextStyle(fontSize: _fontSize));
     _asciiMeasurements = List.generate(
@@ -282,9 +213,9 @@
   // TODO(kenzie): optimize painting to canvas by grouping paints with the same
   // canvas settings.
   void _paintCallback(CanvasRenderingContext2D canvas, Rect rect) {
-    final int startRow = math.max(getRowIndexForY(rect.top), 0);
+    final int startRow = math.max(rowIndexForY(rect.top), 0);
     final int endRow = math.min(
-      getRowIndexForY(rect.bottom) + 1,
+      rowIndexForY(rect.bottom) + 1,
       rows.length - 1,
     );
     for (int i = startRow; i < endRow; i++) {
@@ -344,11 +275,11 @@
       lastScrollLeft = floatingPointScrollLeft;
     }
     // Position in the zoomable coordinate space that we want to keep fixed.
-    final num fixedX = mouseX + lastScrollLeft - _flameChartInset;
+    final num fixedX = mouseX + lastScrollLeft - sideInset;
     // Calculate and set our new horizontal scroll position.
     if (fixedX >= 0) {
       floatingPointScrollLeft =
-          fixedX * newZoomLevel / zoomLevel + _flameChartInset - mouseX;
+          fixedX * newZoomLevel / zoomLevel + sideInset - mouseX;
     } else {
       // No need to transform as we are in the fixed portion of the window.
       floatingPointScrollLeft = lastScrollLeft;
@@ -365,20 +296,17 @@
       }
     }
 
-    timelineGrid.updateForZoom(zoomLevel, getFlameChartWidth());
+    timelineGrid.updateForZoom(zoomLevel, calculatedWidth);
 
-    forceRebuildForSize(
-      flameChartWidthWithInsets,
-      flameChartHeight,
-    );
+    forceRebuildForSize(widthWithInsets, height);
 
     _viewportCanvas.element.element.scrollLeft =
         math.max(0, floatingPointScrollLeft.round());
   }
 
   void forceRebuildForSize(double width, double height) {
-    flameChartWidth = width;
-    flameChartHeight = height;
+    this.width = width;
+    this.height = height;
 
     _viewportCanvas.setContentSize(width, height);
     _viewportCanvas.rebuild(force: true);
@@ -395,13 +323,14 @@
   final int index;
 }
 
-class FlameChartNode {
+class FlameChartNode<T> {
   FlameChartNode(
     this.rect,
     this.backgroundColor,
     this.textColor,
     this.selectedTextColor,
-    this.stackFrame, {
+    this.data,
+    this.displayTextProvider, {
     this.rounded = false,
   })  : startingLeft = rect.left,
         startingWidth = rect.width;
@@ -428,7 +357,9 @@
 
   final Color selectedTextColor;
 
-  final CpuStackFrame stackFrame;
+  final T data;
+
+  final String Function(T) displayTextProvider;
 
   final bool rounded;
 
@@ -436,9 +367,9 @@
 
   Rect rect;
 
-  String get text => stackFrame.name;
+  String get text => displayTextProvider(data);
 
-  String get tooltip => stackFrame.toString();
+  String get tooltip => '$data';
 
   num get maxTextWidth => rect.width - horizontalPadding * 2;
 
@@ -446,7 +377,7 @@
 
   void paint(CanvasRenderingContext2D canvas) {
     canvas.fillStyle =
-        colorToCss(selected ? _selectedFlameChartNodeColor : backgroundColor);
+        colorToCss(selected ? _selectedNodeColor : backgroundColor);
 
     if (rounded) {
       canvas
@@ -532,7 +463,7 @@
     // Do not round these values. Rounding the left could cause us to have
     // inaccurately placed events on the chart. Rounding the width could cause
     // us to lose very small events if the width rounds to zero.
-    final newLeft = (startingLeft - _flameChartInset) * zoom + _flameChartInset;
+    final newLeft = (startingLeft - sideInset) * zoom + sideInset;
     final newWidth = startingWidth * zoom;
 
     final updatedRect = Rect.fromLTWH(newLeft, rect.top, newWidth, rect.height);
@@ -571,12 +502,11 @@
       visible.left,
       viewport.top,
       visible.width,
-      _rowHeight,
+      rowHeight,
     );
 
-    num left =
-        (visible.left - _flameChartInset) ~/ currentInterval * currentInterval +
-            _flameChartInset;
+    num left = (visible.left - sideInset) ~/ currentInterval * currentInterval +
+        sideInset;
 
     final firstGridNodeText = msText(
       const Duration(microseconds: 0),
@@ -590,14 +520,14 @@
       ..fillStyle = colorToCss(timestampColor)
       ..fillText(
         firstGridNodeText,
-        _getTimestampLeft(firstGridNodeText, 0, _flameChartInset, canvas),
+        _timestampLeft(firstGridNodeText, 0, sideInset, canvas),
         viewport.top + _textOffsetY,
       )
       ..strokeStyle = colorToCss(gridLineColor)
       ..lineWidth = gridLineWidth
       ..beginPath()
-      ..moveTo(_flameChartInset, visible.top)
-      ..lineTo(_flameChartInset, visible.bottom)
+      ..moveTo(sideInset, visible.top)
+      ..lineTo(sideInset, visible.bottom)
       ..closePath()
       ..stroke();
 
@@ -610,15 +540,15 @@
       // TODO(kenzie): Instead of calculating timestamp based on position, track
       // timestamp var and increment it by time interval represented by each
       // grid item. See comment on https://github.com/flutter/devtools/pull/325.
-      final timestamp = Duration(
-          microseconds: getTimestampForPosition(left + currentInterval));
+      final timestamp =
+          Duration(microseconds: timestampForPosition(left + currentInterval));
 
       final timestampText = msText(
         timestamp,
         fractionDigits: timestamp.inMicroseconds == 0 ? 1 : 3,
       );
 
-      final timestampX = _getTimestampLeft(
+      final timestampX = _timestampLeft(
         timestampText,
         left,
         currentInterval,
@@ -638,7 +568,7 @@
     }
   }
 
-  num _getTimestampLeft(
+  num _timestampLeft(
     String timestampText,
     num left,
     num width,
@@ -652,8 +582,8 @@
 
   /// Returns the timestamp rounded to the nearest microsecond for the
   /// x-position.
-  int getTimestampForPosition(num gridItemEnd) {
-    return ((gridItemEnd - _flameChartInset) /
+  int timestampForPosition(num gridItemEnd) {
+    return ((gridItemEnd - sideInset) /
             _flameChartWidth *
             _duration.inMicroseconds)
         .round();
diff --git a/devtools/lib/src/config_specific/allowed_error.dart b/devtools/lib/src/config_specific/allowed_error.dart
new file mode 100644
index 0000000..d19e2ac
--- /dev/null
+++ b/devtools/lib/src/config_specific/allowed_error.dart
@@ -0,0 +1,6 @@
+// Copyright 2019 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.
+
+export 'allowed_error_default.dart'
+    if (dart.library.html) 'allowed_error_html.dart';
diff --git a/devtools/lib/src/config_specific/allowed_error_default.dart b/devtools/lib/src/config_specific/allowed_error_default.dart
new file mode 100644
index 0000000..a09ab99
--- /dev/null
+++ b/devtools/lib/src/config_specific/allowed_error_default.dart
@@ -0,0 +1,15 @@
+// Copyright 2019 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.
+
+/// Catch and print errors from the given future. These errors are part of
+/// normal operation for an app, and don't need to be reported to analytics
+/// (i.e., they're not DevTools crashes).
+Future<T> allowedError<T>(Future<T> future) {
+  return future.catchError((Object error) {
+    final errorLines = error.toString().split('\n');
+    print('[${error.runtimeType}] ${errorLines.first}');
+    print(errorLines.skip(1).join('\n'));
+    print('');
+  });
+}
diff --git a/devtools/lib/src/config_specific/allowed_error_html.dart b/devtools/lib/src/config_specific/allowed_error_html.dart
new file mode 100644
index 0000000..f8cda7c
--- /dev/null
+++ b/devtools/lib/src/config_specific/allowed_error_html.dart
@@ -0,0 +1,17 @@
+// Copyright 2019 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 'dart:html';
+
+/// Catch and print errors from the given future. These errors are part of
+/// normal operation for an app, and don't need to be reported to analytics
+/// (i.e., they're not DevTools crashes).
+Future<T> allowedError<T>(Future<T> future) {
+  return future.catchError((Object error) {
+    final errorLines = error.toString().split('\n');
+    window.console.groupCollapsed('[${error.runtimeType}] ${errorLines.first}');
+    window.console.log(errorLines.skip(1).join('\n'));
+    window.console.groupEnd();
+  });
+}
diff --git a/devtools/lib/src/connected_app.dart b/devtools/lib/src/connected_app.dart
index a3dc1ae..49e4b45 100644
--- a/devtools/lib/src/connected_app.dart
+++ b/devtools/lib/src/connected_app.dart
@@ -4,18 +4,19 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import 'globals.dart';
 
 const flutterLibraryUri = 'package:flutter/src/widgets/binding.dart';
 const flutterWebLibraryUri = 'package:flutter_web/src/widgets/binding.dart';
+const dartHtmlLibraryUri = 'dart:html';
 
 class ConnectedApp {
   ConnectedApp();
 
   Future<bool> get isFlutterApp async =>
-      _isFlutterApp ?? await _libraryUriAvailable(flutterLibraryUri);
+      _isFlutterApp ??= await _libraryUriAvailable(flutterLibraryUri);
 
   bool _isFlutterApp;
 
@@ -23,24 +24,29 @@
   // flutter merges with flutter_web. See
   // https://github.com/flutter/devtools/issues/466.
   Future<bool> get isFlutterWebApp async =>
-      _isFlutterWebApp ?? await _libraryUriAvailable(flutterWebLibraryUri);
+      _isFlutterWebApp ??= await _libraryUriAvailable(flutterWebLibraryUri);
 
   bool _isFlutterWebApp;
 
   Future<bool> get isProfileBuild async =>
-      _isProfileBuild ?? await _connectedToProfileBuild();
+      _isProfileBuild ??= await _connectedToProfileBuild();
 
   bool _isProfileBuild;
 
   Future<bool> get isAnyFlutterApp async =>
       await isFlutterApp || await isFlutterWebApp;
 
+  Future<bool> get isDartWebApp async =>
+      _isDartWebApp ??= await _libraryUriAvailable(dartHtmlLibraryUri);
+
+  bool _isDartWebApp;
+
   Future<bool> _connectedToProfileBuild() async {
     assert(serviceManager.serviceAvailable.isCompleted);
 
-    // Flutter web apps do not have profile and non-profile builds. If this
-    // changes in the future, we can remove this check.
-    if (await isFlutterWebApp) return false;
+    // Flutter web apps and CLI apps do not have profile and non-profile builds.
+    // If this changes in the future (flutter web), we can modify this check.
+    if (!await isFlutterApp) return false;
 
     try {
       final Isolate isolate = await serviceManager.service
@@ -66,6 +72,6 @@
     return serviceManager.isolateManager.selectedIsolateLibraries
         .map((ref) => ref.uri)
         .toList()
-        .contains(uri);
+        .any((u) => u.startsWith(uri));
   }
 }
diff --git a/devtools/lib/src/debugger/breakpoints_view.dart b/devtools/lib/src/debugger/breakpoints_view.dart
index 6992c4c..c820b47 100644
--- a/devtools/lib/src/debugger/breakpoints_view.dart
+++ b/devtools/lib/src/debugger/breakpoints_view.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 import 'dart:html' as html;
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../debugger/debugger.dart';
 import '../debugger/debugger_state.dart';
diff --git a/devtools/lib/src/debugger/callstack_view.dart b/devtools/lib/src/debugger/callstack_view.dart
index 5164cf4..713639e 100644
--- a/devtools/lib/src/debugger/callstack_view.dart
+++ b/devtools/lib/src/debugger/callstack_view.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../ui/custom.dart';
 import '../ui/elements.dart';
diff --git a/devtools/lib/src/debugger/console_area.dart b/devtools/lib/src/debugger/console_area.dart
index 46e1f3d..7d52780 100644
--- a/devtools/lib/src/debugger/console_area.dart
+++ b/devtools/lib/src/debugger/console_area.dart
@@ -20,7 +20,7 @@
     _editor = CodeMirror.fromElement(_container.element, options: options);
     _editor.setReadOnly(true);
     if (isDarkTheme) {
-      _editor.setTheme('zenburn');
+      _editor.setTheme('darcula');
     }
 
     final codeMirrorElement = _container.element.children[0];
diff --git a/devtools/lib/src/debugger/debugger.dart b/devtools/lib/src/debugger/debugger.dart
index e72f209..c0e7fc6 100644
--- a/devtools/lib/src/debugger/debugger.dart
+++ b/devtools/lib/src/debugger/debugger.dart
@@ -6,10 +6,9 @@
 import 'dart:html' as html;
 
 import 'package:codemirror/codemirror.dart';
-import 'package:devtools/src/ui/theme.dart';
 import 'package:meta/meta.dart';
 import 'package:split/split.dart' as split;
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../core/message_bus.dart';
 import '../debugger/breakpoints_view.dart';
@@ -25,6 +24,7 @@
 import '../ui/elements.dart';
 import '../ui/icons.dart';
 import '../ui/primer.dart';
+import '../ui/theme.dart';
 import '../ui/ui_utils.dart';
 
 // TODO(devoncarew): improve selection behavior in the left nav area
@@ -50,6 +50,7 @@
           disabled: disabled,
           disabledTooltip: disabledTooltip,
         ) {
+    shortcutCallback = debuggerShortcuts;
     deviceStatus = StatusItem();
     addStatusItem(deviceStatus);
   }
@@ -61,28 +62,81 @@
   StatusItem deviceStatus;
 
   CoreElement _breakpointsCountDiv;
+
   CoreElement _sourcePathDiv;
+
   CoreElement _popupTextfield;
+
   PopupView _popupView;
 
   SourceEditor sourceEditor;
+
   CallStackView callStackView;
+
   VariablesView variablesView;
+
   BreakpointsView breakpointsView;
+
   ScriptsView scriptsView;
+
   ScriptsView popupScriptsView;
+
   ConsoleArea consoleArea;
 
   ScriptsMatcher _matcher;
 
+  List<CoreElement> _navEditorPanels;
+
+  CoreElement _sourceArea;
+
+  CoreElement _consoleDiv;
+
+  // Handle shortcut keys
+  //
+  // All shortcut keys start with CTRL key plus another alphanumeric key.
+  //
+  // Shortcut keys supported:
+  //
+  //   O - open (letter O) a script file, sets focus to the script_name field
+  //       in the Scripts views list.
+  //
+  bool debuggerShortcuts(bool ctrlKey, bool shiftKey, bool altKey, String key) {
+    if (ctrlKey) {
+      switch (key) {
+        case 'o': // CTRL + o
+          if (_matcher != null && _matcher.active) {
+            _matcher.cancel();
+            _matcher = null;
+          }
+          _popupView.element.style.display = 'inline';
+
+          if (!_popupView.isPoppedUp) {
+            _popupView.showPopup();
+            _hookupListeners(_popupView.scriptsView);
+          } else {
+            _popupView.scriptsView.clearScripts();
+            _popupView.scriptsView.element.element.style.display = 'inline';
+          }
+
+          // Open a file set focus to the 'popup_script_name' textfield
+          // accepts key strokes.
+          _popupView.popupTextfield.element.focus();
+
+          ga.select(ga.debugger, ga.openShortcut);
+
+          return true;
+          break;
+      }
+    }
+
+    return false;
+  }
+
   @override
   CoreElement createContent(Framework framework) {
     ga_platform.setupDimensions();
 
-    final CoreElement screenDiv = div()..layoutVertical();
-
-    CoreElement sourceArea;
-    CoreElement consoleDiv;
+    final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
 
     final PButton resumeButton =
         PButton.icon('Resume', FlutterIcons.resume_white_disabled_2x)
@@ -116,9 +170,12 @@
       _updatePauseButton(disabled: false);
     });
 
-    debuggerState.onPausedChanged.listen((bool isPaused) {
-      _updatePauseButton(disabled: isPaused);
-      _updateResumeButton(disabled: !isPaused);
+    // TODO(#926): Is this necessary?
+    _updatePauseButton(disabled: debuggerState.isPaused.value);
+    _updateResumeButton(disabled: !debuggerState.isPaused.value);
+    debuggerState.isPaused.addListener(() {
+      _updatePauseButton(disabled: debuggerState.isPaused.value);
+      _updateResumeButton(disabled: !debuggerState.isPaused.value);
     });
 
     PButton stepOver, stepIn, stepOut;
@@ -128,17 +185,20 @@
     breakOnExceptionControl.onPauseModeChanged.listen((String mode) {
       debuggerState.setExceptionPauseMode(mode);
     });
-    debuggerState.onExceptionPauseModeChanged.listen((String mode) {
-      breakOnExceptionControl.exceptionPauseMode = mode;
+    // TODO(#926): Is this necessary?
+    breakOnExceptionControl.exceptionPauseMode =
+        debuggerState.exceptionPauseMode.value;
+    debuggerState.exceptionPauseMode.addListener(() {
+      breakOnExceptionControl.exceptionPauseMode =
+          debuggerState.exceptionPauseMode.value;
     });
 
     consoleArea = ConsoleArea();
-    List<CoreElement> navEditorPanels;
 
     _popupTextfield =
         CoreElement('input', classes: 'form-control input-sm popup-textfield')
           ..setAttribute('type', 'text')
-          ..setAttribute('placeholder', 'filter')
+          ..setAttribute('placeholder', 'search')
           ..id = 'popup_script_name'
           ..focus(() {
             _matcher ??= ScriptsMatcher(debuggerState);
@@ -179,7 +239,7 @@
       div(c: 'section')
         ..flex()
         ..layoutHorizontal()
-        ..add(navEditorPanels = <CoreElement>[
+        ..add(_navEditorPanels = <CoreElement>[
           div(c: 'debugger-menu')
             ..layoutVertical()
             ..add(<CoreElement>[
@@ -192,7 +252,7 @@
               div(c: 'section flex-wrap')
                 ..layoutHorizontal()
                 ..add(<CoreElement>[
-                  div(c: 'btn-group collapsible-700 flex-no-wrap')
+                  div(c: 'btn-group collapsible-785 flex-no-wrap')
                     ..add(<CoreElement>[
                       pauseButton,
                       resumeButton,
@@ -207,12 +267,12 @@
                   div(c: 'margin-right')..flex(),
                   breakOnExceptionControl,
                 ]),
-              sourceArea = div(c: 'section table-border')
+              _sourceArea = div(c: 'section table-border')
                 ..layoutVertical()
                 ..add(<CoreElement>[
                   _sourcePathDiv = div(c: 'source-head'),
                 ]),
-              consoleDiv = div(c: 'section table-border')
+              _consoleDiv = div(c: 'section table-border')
                 ..layoutVertical()
                 ..add(consoleArea.element),
             ]),
@@ -223,7 +283,7 @@
       _popupTextfield,
       _popupView = PopupView(
         popupScriptsView,
-        sourceArea,
+        _sourceArea,
         _sourcePathDiv,
         _popupTextfield,
       )
@@ -231,24 +291,8 @@
 
     _sourcePathDiv.setInnerHtml('&nbsp;');
 
-    // configure the navigation / editor splitter
-    split.flexSplit(
-      navEditorPanels.map((e) => e.element).toList(),
-      gutterSize: defaultSplitterWidth,
-      sizes: [22, 78],
-      minSize: [200, 600],
-    );
-
-    // configure the editor / console splitter
-    split.flexSplit(
-      [sourceArea.element, consoleDiv.element],
-      horizontal: false,
-      gutterSize: defaultSplitterWidth,
-      sizes: [80, 20],
-      minSize: [200, 60],
-    );
-
-    debuggerState.onSupportsStepping.listen((bool value) {
+    void updateStepCapabilities() {
+      final value = debuggerState.supportsStepping.value;
       stepIn.enabled = value;
 
       // Only enable step over and step out if we're paused at a frame. When
@@ -256,7 +300,11 @@
       // meaningful.
       stepOver.enabled = value && (debuggerState.lastEvent.topFrame != null);
       stepOut.enabled = value && (debuggerState.lastEvent.topFrame != null);
-    });
+    }
+
+    // TODO(#926): Is this necessary?
+    updateStepCapabilities();
+    debuggerState.supportsStepping.addListener(updateStepCapabilities);
 
     stepOver.click(() => debuggerState.stepOver());
     stepIn.click(() => debuggerState.stepIn());
@@ -268,23 +316,24 @@
       'gutters': <String>['breakpoints'],
     };
     final CodeMirror codeMirror =
-        CodeMirror.fromElement(sourceArea.element, options: options);
+        CodeMirror.fromElement(_sourceArea.element, options: options);
     codeMirror.setReadOnly(true);
     if (isDarkTheme) {
-      codeMirror.setTheme('zenburn');
+      codeMirror.setTheme('darcula');
     }
     final codeMirrorElement = _sourcePathDiv.element.parent.children[1];
     codeMirrorElement.setAttribute('flex', '');
 
     sourceEditor = SourceEditor(codeMirror, debuggerState);
 
-    debuggerState.onBreakpointsChanged
-        .listen((List<Breakpoint> breakpoints) async {
-      sourceEditor.setBreakpoints(breakpoints);
+    // TODO(#926): Is this necessary?
+    sourceEditor.setBreakpoints(debuggerState.breakpoints.value);
+    debuggerState.breakpoints.addListener(() {
+      sourceEditor.setBreakpoints(debuggerState.breakpoints.value);
     });
 
-    debuggerState.onPausedChanged.listen((bool paused) async {
-      if (paused) {
+    void updateFrames() async {
+      if (debuggerState.isPaused.value) {
         // Check for async causal frames; fall back to using regular sync frames.
         final Stack stack = await debuggerState.getStack();
         List<Frame> frames = stack.asyncCausalFrames ?? stack.frames;
@@ -317,11 +366,15 @@
         callStackView.clearFrames();
         sourceEditor.clearExecutionPoint();
       }
-    });
+    }
 
-    // Update the status line.
-    debuggerState.onPausedChanged.listen((bool paused) async {
-      if (paused && debuggerState.lastEvent.topFrame != null) {
+    // TODO(#926): Is this necessary?
+    updateFrames();
+    debuggerState.isPaused.addListener(updateFrames);
+
+    void updateStatusLine() async {
+      if (debuggerState.isPaused.value &&
+          debuggerState.lastEvent.topFrame != null) {
         final Frame topFrame = debuggerState.lastEvent.topFrame;
 
         final ScriptRef scriptRef = topFrame.location.script;
@@ -336,7 +389,11 @@
       } else {
         deviceStatus.element.text = '';
       }
-    });
+    }
+
+    // TODO(#926): Is this necessary?
+    updateStatusLine();
+    debuggerState.isPaused.addListener(updateStatusLine);
 
     callStackView.onSelectionChanged.listen((Frame frame) async {
       if (frame == null) {
@@ -374,49 +431,30 @@
       consoleArea.appendText('${event.data}\n\n');
     });
 
-    // Handle shortcut keys
-    //
-    // All shortcut keys start with CTRL key plus another alphanumeric key.
-    //
-    // Shortcut keys supported:
-    //
-    //   O - open (letter O) a script file, sets focus to the script_name field
-    //       in the Scripts views list.
-    //
-    html.window.onKeyDown.listen((html.KeyboardEvent e) {
-      if (e.ctrlKey) {
-        switch (e.key) {
-          case 'o': // CTRL + o
-            if (_matcher != null && _matcher.active) {
-              _matcher.cancel();
-              _matcher = null;
-            }
-            _popupView.element.style.display = 'inline';
-
-            if (!_popupView.isPoppedUp) {
-              _popupView.showPopup();
-              _hookupListeners(_popupView.scriptsView);
-            } else {
-              _popupView.scriptsView.clearScripts();
-              _popupView.scriptsView.element.element.style.display = 'inline';
-            }
-
-            // Open a file set focus to the 'popup_script_name' textfield
-            // accepts key strokes.
-            _popupView.popupTextfield.element.focus();
-
-            ga.select(ga.debugger, ga.openShortcut);
-
-            e.preventDefault();
-            break;
-        }
-      }
-    });
-
     return screenDiv;
   }
 
   @override
+  void onContentAttached() {
+    // configure the navigation / editor splitter
+    split.flexSplit(
+      _navEditorPanels.map((e) => e.element).toList(),
+      gutterSize: defaultSplitterWidth,
+      sizes: [22, 78],
+      minSize: [200, 600],
+    );
+
+    // configure the editor / console splitter
+    split.flexSplit(
+      [_sourceArea.element, _consoleDiv.element],
+      horizontal: false,
+      gutterSize: defaultSplitterWidth,
+      sizes: [80, 20],
+      minSize: [200, 60],
+    );
+  }
+
+  @override
   void entering() {
     if (!_initialized) {
       _initialize();
@@ -530,7 +568,7 @@
     final CoreElement textfield =
         CoreElement('input', classes: 'form-control input-sm margin-left')
           ..setAttribute('type', 'text')
-          ..setAttribute('placeholder', 'filter')
+          ..setAttribute('placeholder', 'search')
           ..element.style.width = 'calc(100% - 105px)'
           ..id = 'script_name';
     final CoreElement scriptCountDiv = span(text: '-', c: 'counter')
@@ -600,8 +638,10 @@
       ..flex()
       ..layoutVertical();
 
-    debuggerState.onBreakpointsChanged.listen((List<Breakpoint> breakpoints) {
-      breakpointsView.showBreakpoints(breakpoints);
+    // TODO(#926): Is this necessary?
+    breakpointsView.showBreakpoints(debuggerState.breakpoints.value);
+    debuggerState.breakpoints.addListener(() {
+      breakpointsView.showBreakpoints(debuggerState.breakpoints.value);
     });
 
     return menu;
diff --git a/devtools/lib/src/debugger/debugger_state.dart b/devtools/lib/src/debugger/debugger_state.dart
index ffab65d..1838d41 100644
--- a/devtools/lib/src/debugger/debugger_state.dart
+++ b/devtools/lib/src/debugger/debugger_state.dart
@@ -4,11 +4,12 @@
 
 import 'dart:async';
 
-import 'package:rxdart/rxdart.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../debugger/debugger.dart';
 import '../ui/analytics.dart' as ga;
+import '../ui/fake_flutter/fake_flutter.dart'
+    show ValueNotifier, ValueListenable;
 
 class DebuggerState {
   VmService _service;
@@ -20,32 +21,34 @@
 
   final Map<String, Script> _scriptCache = <String, Script>{};
 
-  final BehaviorSubject<bool> _paused = BehaviorSubject<bool>.seeded(false);
-  final BehaviorSubject<bool> _supportsStepping =
-      BehaviorSubject<bool>.seeded(false);
+  final _isPaused = ValueNotifier<bool>(false);
+  ValueListenable<bool> get isPaused => _isPaused;
+
+  final _hasFrames = ValueNotifier<bool>(false);
+  ValueNotifier<bool> _supportsStepping;
+  ValueListenable<bool> get supportsStepping {
+    return _supportsStepping ??= () {
+      final notifier = ValueNotifier<bool>(_isPaused.value && _hasFrames.value);
+      void update() {
+        notifier.value = _isPaused.value && _hasFrames.value;
+      }
+
+      _isPaused.addListener(update);
+      _hasFrames.addListener(update);
+      return notifier;
+    }();
+  }
 
   Event lastEvent;
 
-  final BehaviorSubject<List<Breakpoint>> _breakpoints =
-      BehaviorSubject<List<Breakpoint>>.seeded(<Breakpoint>[]);
+  final _breakpoints = ValueNotifier<List<Breakpoint>>([]);
+  ValueListenable<List<Breakpoint>> get breakpoints => _breakpoints;
 
-  final BehaviorSubject<String> _exceptionPauseMode = BehaviorSubject();
+  final _exceptionPauseMode = ValueNotifier<String>(null);
+  ValueListenable<String> get exceptionPauseMode => _exceptionPauseMode;
 
   InstanceRef _reportedException;
 
-  bool get isPaused => _paused.value;
-
-  Stream<bool> get onPausedChanged => _paused;
-
-  Stream<bool> get onSupportsStepping =>
-      Observable<bool>.concat(<Stream<bool>>[_paused, _supportsStepping]);
-
-  Stream<List<Breakpoint>> get onBreakpointsChanged => _breakpoints;
-
-  Stream<String> get onExceptionPauseModeChanged => _exceptionPauseMode;
-
-  List<Breakpoint> get breakpoints => _breakpoints.value;
-
   void setVmService(VmService service) {
     _service = service;
 
@@ -55,12 +58,12 @@
   void switchToIsolate(IsolateRef ref) async {
     isolateRef = ref;
 
-    _updatePaused(false);
+    _isPaused.value = false;
 
     _clearCaches();
 
     if (ref == null) {
-      _breakpoints.add(<Breakpoint>[]);
+      _breakpoints.value = [];
       return;
     }
 
@@ -72,12 +75,12 @@
           isolate.pauseEvent.kind != EventKind.kResume) {
         lastEvent = isolate.pauseEvent;
         _reportedException = isolate.pauseEvent.exception;
-        _updatePaused(true);
+        _isPaused.value = true;
       }
 
-      _breakpoints.add(isolate.breakpoints);
+      _breakpoints.value = isolate.breakpoints;
 
-      _exceptionPauseMode.add(isolate.exceptionPauseMode);
+      _exceptionPauseMode.value = isolate.exceptionPauseMode;
     }
   }
 
@@ -146,12 +149,12 @@
       return;
     }
 
-    _supportsStepping.add(event.topFrame != null);
+    _hasFrames.value = event.topFrame != null;
     lastEvent = event;
 
     switch (event.kind) {
       case EventKind.kResume:
-        _updatePaused(false);
+        _isPaused.value = false;
         _reportedException = null;
         break;
       case EventKind.kPauseStart:
@@ -161,20 +164,21 @@
       case EventKind.kPauseException:
       case EventKind.kPausePostRequest:
         _reportedException = event.exception;
-        _updatePaused(true);
+        _isPaused.value = true;
         break;
       case EventKind.kBreakpointAdded:
-        _breakpoints.value.add(event.breakpoint);
-        _breakpoints.add(_breakpoints.value);
+        _breakpoints.value = [..._breakpoints.value, event.breakpoint];
         break;
       case EventKind.kBreakpointResolved:
-        _breakpoints.value.remove(event.breakpoint);
-        _breakpoints.value.add(event.breakpoint);
-        _breakpoints.add(_breakpoints.value);
+        _breakpoints.value = [
+          for (var b in _breakpoints.value) if (b != event.breakpoint) b,
+          event.breakpoint
+        ];
         break;
       case EventKind.kBreakpointRemoved:
-        _breakpoints.value.remove(event.breakpoint);
-        _breakpoints.add(_breakpoints.value);
+        _breakpoints.value = [
+          for (var b in _breakpoints.value) if (b != event.breakpoint) b
+        ];
         break;
     }
   }
@@ -189,12 +193,6 @@
     _debugSubscription?.cancel();
   }
 
-  void _updatePaused(bool value) {
-    if (_paused.value != value) {
-      _paused.add(value);
-    }
-  }
-
   /// Get the populated [Instance] object, given an [InstanceRef].
   ///
   /// The return value can be one of [Instance] or [Sentinel].
@@ -289,6 +287,6 @@
   }
 
   void updateFrom(Isolate isolate) {
-    _breakpoints.add(isolate.breakpoints);
+    _breakpoints.value = isolate.breakpoints;
   }
 }
diff --git a/devtools/lib/src/debugger/scripts_view.dart b/devtools/lib/src/debugger/scripts_view.dart
index 0f73142..ac98ee3 100644
--- a/devtools/lib/src/debugger/scripts_view.dart
+++ b/devtools/lib/src/debugger/scripts_view.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 import 'dart:html' as html;
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../debugger/debugger_state.dart';
 import '../ui/custom.dart';
@@ -35,17 +35,18 @@
       this._popupTextfield)
       : super('div', classes: 'open-popup');
 
-  CoreElement _sourceArea; // Top div area container of _sourcePathDiv
-  CoreElement _sourcePathDiv; // Text name of the source
-  CoreElement _popupTextfield; // Textfield to show while popup is active.
+  final CoreElement _sourceArea; // Top div area container of _sourcePathDiv
+  final CoreElement _sourcePathDiv; // Text name of the source
+  final CoreElement _popupTextfield; // Textfield to show while popup is active.
 
   CoreElement get popupTextfield => _popupTextfield;
 
-  ScriptsView _scriptsView;
+  final ScriptsView _scriptsView;
 
   String _oldSourceNameTextColor;
 
   bool _poppedUp = false;
+
   bool get isPoppedUp => _poppedUp;
 
   void showPopup() {
@@ -124,20 +125,24 @@
       final String uri = scriptRef.uri;
       final String name = uriDescriber(uri);
 
+      // Case insensitive matching.
+      final matchingName = name.toLowerCase();
+
       CoreElement element;
       if (_matcherRendering != null && _matcherRendering.active) {
         // InputElement's need to fetch the value not text/textContent property.
         // The value and text are different, all nodes have a text. It the text
         // content of the node itself along with its descendants. However, input
         // elements have a value property - its the input data of the input
-        // element. Input elements may have a text/textContent but it is alway
+        // element. Input elements may have a text/textContent but it is always
         // empty because they are void elements.
         final html.InputElement inputElement =
             _matcherRendering._textfield.element as html.InputElement;
-        final String matchPart = inputElement.value;
+        // Case insensitive matching.
+        final String matchPart = inputElement.value.toLowerCase();
 
         // Compute the matched characters to be bolded.
-        final int startIndex = name.lastIndexOf(matchPart);
+        final int startIndex = matchingName.lastIndexOf(matchPart);
         final String firstPart = name.substring(0, startIndex);
         final int endBoldIndex = startIndex + matchPart.length;
         final String boldPart = name.substring(startIndex, endBoldIndex);
@@ -162,7 +167,7 @@
 
   ScriptsMatcher get matcher => _matcherRendering;
 
-  void setMatcher(_matcher) {
+  void setMatcher(ScriptsMatcher _matcher) {
     _matcherRendering = _matcher;
   }
 
@@ -170,8 +175,16 @@
     _highlightRef = null;
   }
 
-  void scrollAndHilight(int row, int topPosition,
-      {bool top = false, bool bottom = false}) {
+  void scrollAndHighlight(
+    int row,
+    int topPosition, {
+    bool top = false,
+    bool bottom = false,
+  }) {
+    // TODO(terry): this fixed a RangeError, but investigate why this method is
+    // called when the list is empty.
+    if (items.isEmpty) return;
+
     // Highlight this row.
     _highlightRef = items[row];
 
@@ -199,7 +212,7 @@
         }
         childToScrollTo = itemIndex;
         final int scrollPosition = startRow > 0 ? startRow * itemHeight : 0;
-        scrollAndHilight(childToScrollTo, scrollPosition, top: true);
+        scrollAndHighlight(childToScrollTo, scrollPosition, top: true);
         break;
       case ListDirection.pageUp:
         int itemIndex = startRow - itemsVis;
@@ -207,17 +220,17 @@
         childToScrollTo = itemIndex;
         final int scrollPosition =
             childToScrollTo > 0 ? childToScrollTo * itemHeight : 0;
-        scrollAndHilight(childToScrollTo, scrollPosition, top: true);
+        scrollAndHighlight(childToScrollTo, scrollPosition, top: true);
         break;
       case ListDirection.home:
         childToScrollTo = 0;
-        scrollAndHilight(childToScrollTo, childToScrollTo);
+        scrollAndHighlight(childToScrollTo, childToScrollTo);
         break;
       case ListDirection.end:
         childToScrollTo = _items.items.length - 1;
         final int scrollPosition =
             childToScrollTo > 0 ? (childToScrollTo - itemsVis) * itemHeight : 0;
-        scrollAndHilight(childToScrollTo, scrollPosition);
+        scrollAndHighlight(childToScrollTo, scrollPosition);
         break;
     }
 
@@ -253,7 +266,7 @@
     String rootLib,
     String commonPrefix, {
     bool selectRootScript = false,
-    ScriptRef selectScripRef,
+    ScriptRef selectScriptRef,
   }) {
     this.rootLib = rootLib;
 
@@ -286,8 +299,8 @@
     if (selectRootScript) {
       selection = scripts.firstWhere((script) => script.uri == rootLib,
           orElse: () => null);
-    } else if (selectScripRef != null) {
-      selection = selectScripRef;
+    } else if (selectScriptRef != null) {
+      selection = selectScriptRef;
     }
 
     _items.setItems(scripts,
@@ -325,9 +338,9 @@
   // and END.
   int _selectRow = -1;
 
-  StreamSubscription _subscription;
+  StreamSubscription _keyEventSubscription;
 
-  bool get active => _subscription != null;
+  bool get active => _keyEventSubscription != null;
 
   Function _finishCallback;
 
@@ -346,7 +359,8 @@
     _startMatching(revertScriptRef, true);
 
     // Start handling user's keystrokes to show matching list of files.
-    _subscription ??= _textfield.onKeyDown.listen((html.KeyboardEvent e) {
+    _keyEventSubscription ??=
+        _textfield.onKeyDown.listen((html.KeyboardEvent e) {
       switch (e.keyCode) {
         case DOM_VK_RETURN:
           reset();
@@ -377,7 +391,7 @@
           // Set selection one item up.
           if (_selectRow > 0) {
             _selectRow -= 1;
-            _scriptsView.scrollAndHilight(_selectRow, -1);
+            _scriptsView.scrollAndHighlight(_selectRow, -1);
           }
           e.preventDefault();
           break;
@@ -385,7 +399,7 @@
           // Set selection one item down.
           if (_selectRow < _scriptsView.items.length - 1) {
             _selectRow += 1;
-            _scriptsView.scrollAndHilight(_selectRow, -1);
+            _scriptsView.scrollAndHighlight(_selectRow, -1);
           }
           e.preventDefault();
           break;
@@ -400,7 +414,7 @@
 
   void selectFirstItem() {
     _selectRow = 0;
-    _scriptsView.scrollAndHilight(_selectRow, -1);
+    _scriptsView.scrollAndHighlight(_selectRow, -1);
   }
 
   // Finished matching - throw away all matching states.
@@ -416,10 +430,10 @@
       selectedScriptRef = _scriptsView._highlightRef;
     }
 
-    if (_subscription != null) {
-      // No more event routing until user has clicked again the the textfield.
-      _subscription.cancel();
-      _subscription = null;
+    if (_keyEventSubscription != null) {
+      // No more event routing until user has clicked again in the textfield.
+      _keyEventSubscription.cancel();
+      _keyEventSubscription = null;
     }
 
     // Remember the whole set of ScriptRefs
@@ -429,7 +443,7 @@
       originalRefs,
       _debuggerState.rootLib.uri,
       _debuggerState.commonScriptPrefix,
-      selectScripRef: selectedScriptRef,
+      selectScriptRef: selectedScriptRef,
     );
 
     // Lose all other intermediate matches - we're done.
@@ -447,14 +461,15 @@
   }
 
   /// Revert list and selection back to before the matcher (first click in the
-  /// textfield.
+  /// textfield).
   void revert() {
     reset();
+
     _scriptsView.showScripts(
       matchingState[''],
       _debuggerState.rootLib.uri,
       _debuggerState.commonScriptPrefix,
-      selectScripRef: _originalScriptRef,
+      selectScriptRef: _originalScriptRef,
     );
 
     if (_originalScriptRef != null) {
@@ -488,7 +503,13 @@
     lastMatchingRefs ??= matchingState[''];
 
     final List<ScriptRef> matchingRefs = lastMatchingRefs
-        .where((ScriptRef ref) => ref.uri.lastIndexOf('$charsToMatch') >= 0)
+        .where((ScriptRef ref) =>
+            _debuggerState
+                .getShortScriptName(ref.uri)
+                // Case insensitive matching.
+                .toLowerCase()
+                .lastIndexOf('${charsToMatch.toLowerCase()}') >=
+            0)
         .toList();
 
     matchingState.putIfAbsent(charsToMatch, () => matchingRefs);
diff --git a/devtools/lib/src/debugger/variables_view.dart b/devtools/lib/src/debugger/variables_view.dart
index 6bcdc67..3544949 100644
--- a/devtools/lib/src/debugger/variables_view.dart
+++ b/devtools/lib/src/debugger/variables_view.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../debugger/debugger_state.dart';
 import '../ui/custom.dart';
diff --git a/devtools/lib/src/eval_on_dart_library.dart b/devtools/lib/src/eval_on_dart_library.dart
index 717deb6..2a14e63 100644
--- a/devtools/lib/src/eval_on_dart_library.dart
+++ b/devtools/lib/src/eval_on_dart_library.dart
@@ -7,7 +7,7 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import 'globals.dart';
 import 'inspector/inspector_service.dart';
@@ -83,8 +83,8 @@
       }
       assert(!_libraryRef.isCompleted);
       _libraryRef.completeError(new LibraryNotFound(_candidateLibraryNames));
-    } catch (e) {
-      _handleError(e);
+    } catch (e, stack) {
+      _handleError(e, stack);
     }
   }
 
@@ -126,13 +126,13 @@
         throw result;
       }
       return result;
-    } catch (e) {
-      _handleError(e);
+    } catch (e, stack) {
+      _handleError(e, stack);
     }
     return null;
   }
 
-  void _handleError(dynamic e) {
+  void _handleError(dynamic e, StackTrace stack) {
     if (_disposed) return;
 
     switch (e.runtimeType) {
@@ -145,6 +145,9 @@
       default:
         print('Unrecognized error: $e');
     }
+    if (stack != null) {
+      print(stack);
+    }
   }
 
   Future<Library> getLibrary(LibraryRef instance, ObjectGroup isAlive) {
diff --git a/devtools/lib/src/framework/framework.dart b/devtools/lib/src/framework/framework.dart
index d34b050..2c4e6e7 100644
--- a/devtools/lib/src/framework/framework.dart
+++ b/devtools/lib/src/framework/framework.dart
@@ -10,14 +10,17 @@
 
 import '../globals.dart';
 import '../main.dart';
-import '../timeline/timeline.dart';
+import '../message_manager.dart';
 import '../timeline/timeline_controller.dart';
+import '../timeline/timeline_model.dart';
+import '../timeline/timeline_screen.dart';
 import '../ui/analytics.dart' as ga;
 import '../ui/analytics_platform.dart' as ga_platform;
 import '../ui/custom.dart';
 import '../ui/elements.dart';
 import '../ui/primer.dart';
 import '../ui/ui_utils.dart';
+import '../url_utils.dart';
 import '../utils.dart';
 import 'framework_core.dart';
 
@@ -29,7 +32,8 @@
 
     globalStatus = StatusLine(CoreElement.from(queryId('global-status')));
     pageStatus = StatusLine(CoreElement.from(queryId('page-status')));
-    auxiliaryStatus = StatusLine(CoreElement.from(queryId('auxiliary-status')));
+    auxiliaryStatus = StatusLine(CoreElement.from(queryId('auxiliary-status')))
+      ..defaultStatus = defaultAuxiliaryStatus;
 
     globalActions =
         ActionsContainer(CoreElement.from(queryId('global-actions')));
@@ -49,18 +53,32 @@
 
   final Completer<void> screensReady = Completer();
 
+  final MessageManager messageManager = MessageManager();
+
   Screen current;
 
   Screen _previous;
 
   StatusLine globalStatus;
+
   StatusLine pageStatus;
+
   StatusLine auxiliaryStatus;
+
   ActionsContainer globalActions;
+
   ConnectDialog connectDialog;
+
   SnapshotMessage snapshotMessage;
+
   AnalyticsOptInDialog analyticsDialog;
 
+  final StatusItem defaultAuxiliaryStatus = createLinkStatusItem(
+    span()..add(span(text: 'DevTools Docs', c: 'optional-700')),
+    href: 'https://flutter.dev/docs/development/tools/devtools/overview',
+    title: 'Documentation on using Dart DevTools',
+  );
+
   void _initDragDrop() {
     window.addEventListener('dragover', (e) => _onDragOver(e), false);
     window.addEventListener('drop', (e) => _onDrop(e), false);
@@ -139,13 +157,14 @@
   }
 
   void _importTimeline(Map<String, dynamic> import) {
-    final snapshot = TimelineSnapshot.parse(import);
-    if (snapshot.isEmpty) {
+    final offlineData = OfflineTimelineData.parse(import);
+    if (offlineData.isEmpty) {
       toast('Imported file does not contain timeline data.');
-      exitSnapshotMode();
       return;
     }
 
+    _enterOfflineMode();
+
     TimelineScreen timelineScreen = screens.firstWhere(
       (screen) => screen.id == timelineScreenId,
       orElse: () => null,
@@ -153,26 +172,28 @@
     if (timelineScreen == null) {
       addScreen(timelineScreen = TimelineScreen(disabled: false));
     }
-    _enterSnapshotMode();
     navigateTo(timelineScreenId);
-    loadTimelineSnapshotController.add(snapshot);
+
+    timelineScreen.clearTimeline();
+    timelineScreen.timelineController.loadOfflineData(offlineData);
   }
 
-  void _enterSnapshotMode() {
+  void _enterOfflineMode() {
     connectDialog.hide();
     snapshotMessage.hide();
-    snapshotMode = true;
+    offlineMode = true;
   }
 
-  void exitSnapshotMode() {
-    snapshotMode = false;
+  void exitOfflineMode() {
+    offlineMode = false;
     if (serviceManager.connectedApp == null) {
       showConnectionDialog();
       showSnapshotMessage();
       mainElement.clear();
       screens.removeWhere((screen) => screen.id == timelineScreenId);
+      auxiliaryStatus.defaultStatus = defaultAuxiliaryStatus;
     } else {
-      navigateTo(_previous.id);
+      navigateTo((_previous ?? current).id);
     }
   }
 
@@ -180,16 +201,21 @@
     screens.add(screen);
   }
 
-  void navigateTo(String id) {
-    ga.screen(id);
+  /// Returns false if the screen is disabled.
+  bool navigateTo(String id) {
     final Screen screen = getScreen(id);
     assert(screen != null);
+    if (screen.disabled) {
+      return false;
+    }
+    ga.screen(id);
 
     final String search = window.location.search;
     final String ref = search == null ? screen.ref : '$search${screen.ref}';
     window.history.pushState(null, screen.name, ref);
 
     load(screen);
+    return true;
   }
 
   void showAnalyticsDialog() {
@@ -204,7 +230,21 @@
     snapshotMessage.show();
   }
 
+  // Hookup for any keyDown event to handle shortcut keys for a screen.
+  void _hookupShortcuts() {
+    window.onKeyDown.listen((KeyboardEvent e) {
+      if (current != null &&
+          e.key.isNotEmpty &&
+          current.shortcutCallback != null &&
+          current.shortcutCallback(e.ctrlKey, e.shiftKey, e.altKey, e.key)) {
+        e.preventDefault();
+      }
+    });
+  }
+
   void loadScreenFromLocation() async {
+    _hookupShortcuts();
+
     await screensReady.future.whenComplete(() {
       // Screens are identified by the hash as that works better with the webdev
       // server.
@@ -214,7 +254,8 @@
         id = id.substring(1);
       }
       Screen screen = getScreen(id, onlyEnabled: true);
-      screen ??= screens.first;
+      screen ??= screens.firstWhere((screen) => !screen.disabled,
+          orElse: () => screens.first);
       if (screen != null) {
         ga_platform.setupAndGaScreen(id);
         load(screen);
@@ -249,6 +290,7 @@
       _previous.visible = false;
 
       pageStatus.removeAll();
+      messageManager.removeAll();
 
       _screenContents[_previous].hidden(true);
     }
@@ -269,6 +311,7 @@
       final CoreElement screenContent = current.createContent(this);
       screenContent.attribute('full');
       mainElement.add(screenContent);
+      current.onContentAttached();
 
       _screenContents[current] = screenContent;
 
@@ -290,6 +333,7 @@
     current.visible = true;
     current.entering();
     pageStatus.addAll(current.statusItems);
+    messageManager.showMessagesForScreen(current.id);
     auxiliaryStatus.defaultStatus = screen.helpStatus;
 
     updatePage();
@@ -304,13 +348,8 @@
     }
   }
 
-  void showInfo({String message, String title, List<CoreElement> children}) {
-    _showMessage(message: message, title: title, children: children);
-  }
-
-  void showWarning({String message, String title, List<CoreElement> children}) {
-    _showMessage(
-        message: message, title: title, children: children, warning: true);
+  void showMessage({@required Message message, String screenId = generalId}) {
+    messageManager.addMessage(message, screenId);
   }
 
   void showError(String title, [dynamic error]) {
@@ -323,44 +362,14 @@
         message = null;
       }
     }
-
-    _showMessage(message: message, title: title, error: true);
-  }
-
-  void _showMessage({
-    String message,
-    String title,
-    bool warning = false,
-    bool error = false,
-    List<CoreElement> children,
-  }) {
-    final PFlash flash = PFlash();
-    if (warning) {
-      flash.warning();
-    }
-    if (error) {
-      flash.error();
-    }
-    flash.addClose().click(clearMessages);
-    if (title != null) {
-      flash.add(label(text: title));
-    }
-    if (message != null) {
-      for (String text in message.split('\n\n')) {
-        flash.add(div(text: text));
-      }
-    }
-    if (children != null) {
-      children.forEach(flash.add);
-    }
-
-    final CoreElement errorContainer =
-        CoreElement.from(queryId('messages-container'));
-    errorContainer.add(flash);
+    messageManager.addMessage(
+      Message(MessageType.error, message: message, title: title),
+      generalId,
+    );
   }
 
   void clearMessages() {
-    queryId('messages-container').children.clear();
+    messageManager.removeAll();
   }
 
   void toast(
@@ -476,6 +485,10 @@
   }
 }
 
+// Each screen will get a chance to handle a shortcut key.
+typedef ShortCut = bool Function(
+    bool ctrlKey, bool shiftKey, bool altKey, String key);
+
 abstract class Screen {
   Screen({
     @required this.name,
@@ -483,11 +496,13 @@
     this.iconClass,
     this.disabledTooltip = 'This screen is not available',
     bool disabled = false,
+    this.shortcutCallback,
+    this.showTab = true,
   })  : helpStatus = createLinkStatusItem(
           span()
             ..add(span(text: '$name', c: 'optional-700'))
             ..add(span(text: ' Docs')),
-          href: 'https://flutter.github.io/devtools/$id',
+          href: 'https://flutter.dev/docs/development/tools/devtools/$id',
           title: 'Documentation on using the $name page',
         ),
         disabled = allTabsEnabledByQuery ? false : disabled;
@@ -498,6 +513,10 @@
   final StatusItem helpStatus;
   final String disabledTooltip;
   final bool disabled;
+  final bool showTab;
+
+  // Set to handle short-cut keys for a particular screen.
+  ShortCut shortcutCallback;
 
   bool needsResizing = false;
 
@@ -535,6 +554,13 @@
 
   @override
   String toString() => 'Screen($id)';
+
+  /// Callback invoked after the content for the screen has been added to the
+  /// DOM.
+  ///
+  /// Certain libraries such as package:split behave badly if invoked on
+  /// elements that are not yet attached to the DOM.
+  void onContentAttached() {}
 }
 
 class SetStateMixin {
@@ -655,7 +681,7 @@
 
   void _tryConnect() {
     final InputElement inputElement = textfield.element;
-    final String value = inputElement.value.trim();
+    String value = inputElement.value.trim();
     final int port = int.tryParse(value);
 
     void handleConnectError() {
@@ -674,7 +700,12 @@
       });
     } else {
       try {
-        final uri = getTrimmedUri(value);
+        // Check to see if the user pasted in a urlencoded url ('://').
+        if (value.contains('%3A%2F%2F')) {
+          value = Uri.decodeFull(value);
+        }
+
+        final uri = getNormalizedTrimmedUri(value);
         if (uri != null && uri.isAbsolute) {
           _connect(uri).catchError((dynamic error) {
             handleConnectError();
@@ -768,38 +799,30 @@
     parent.layoutVertical();
 
     parent.add([
-      h2(text: 'Analytics Data Collection'),
+      h2(text: 'Welcome to Dart DevTools'),
       CoreElement('dl', classes: 'form-group')
         ..add([
-          CoreElement('dt')
+          CoreElement('dd')
             ..add([
-              label(text: 'Welcome to Dart DevTools')
-                ..setAttribute('for', 'uri-field'),
+              span(
+                text: 'DevTools reports feature usage statistics and basic '
+                    'crash reports to Google in order to help Google improve '
+                    "the tool over time. See Google's ",
+              ),
+              a(
+                  text: 'privacy policy',
+                  href: 'https://www.google.com/intl/en/policies/privacy',
+                  target: '_blank'),
+              span(text: '.'),
+              p(),
             ]),
           CoreElement('dd')
             ..add([
-              p(
-                text: 'Dart DevTools reports features usage '
-                    'statistics and basic crash reports to Google in order '
-                    'to help Google contribute improvements to Dart DevTools '
-                    'over time.',
-              )..add([
-                  br(),
-                  a(
-                      text: 'See Google\'s privacy policy',
-                      c: 'note',
-                      href: 'https://www.google.com/intl/en/policies/privacy',
-                      target: '_blank'),
-                ]),
-            ]),
-          CoreElement('dd')
-            ..add([
-              p(text: 'Do you accept analytics collection for Dart DevTools?'),
-              acceptButton = PButton('I Accept')
+              p(text: 'Send usage statistics for DevTools?'),
+              acceptButton = PButton('Sounds good!')
                 ..small()
-                ..clazz('margin-left')
                 ..setAttribute('tabindex', '1'),
-              dontAcceptButton = PButton('I Do Not Accept')
+              dontAcceptButton = PButton('No thanks')
                 ..small()
                 ..clazz('margin-left')
                 ..setAttribute('tabindex', '2'),
diff --git a/devtools/lib/src/framework/framework_core.dart b/devtools/lib/src/framework/framework_core.dart
index 8d4661b..63ddd4c 100644
--- a/devtools/lib/src/framework/framework_core.dart
+++ b/devtools/lib/src/framework/framework_core.dart
@@ -5,8 +5,6 @@
 import 'dart:async';
 import 'dart:html' hide Screen;
 
-import 'package:vm_service_lib/utils.dart';
-
 import '../../devtools.dart' as devtools show version;
 import '../core/message_bus.dart';
 import '../globals.dart';
@@ -22,7 +20,7 @@
     // Print the version number at startup.
     print('DevTools version ${devtools.version}.');
 
-    final Uri uri = Uri.parse(window.location.toString());
+    final uri = Uri.parse(window.location.toString());
     theme.initializeTheme(uri.queryParameters['theme']);
 
     _setGlobals();
@@ -38,14 +36,10 @@
     Uri explicitUri,
     ErrorReporter errorReporter,
   }) async {
-    var uri = explicitUri ?? _getUriFromQuerystring();
+    final Uri uri = explicitUri ?? _getUriFromQuerystring();
 
     if (uri != null) {
-      final Completer<Null> finishedCompleter = Completer<Null>();
-
-      // Map the URI (which may be Observatory web app) to a WebSocket URI for
-      // the VM service.
-      uri = convertToWebSocketUrl(serviceProtocolUrl: uri);
+      final finishedCompleter = Completer<void>();
 
       try {
         final VmServiceWrapper service = await connect(uri, finishedCompleter);
@@ -88,7 +82,9 @@
           (uri.isScheme('ws') ||
               uri.isScheme('wss') ||
               uri.isScheme('http') ||
-              uri.isScheme('https'))) {
+              uri.isScheme('https') ||
+              uri.isScheme('sse') ||
+              uri.isScheme('sses'))) {
         return uri;
       }
     }
diff --git a/devtools/lib/src/globals.dart b/devtools/lib/src/globals.dart
index 898c2a1..33929df 100644
--- a/devtools/lib/src/globals.dart
+++ b/devtools/lib/src/globals.dart
@@ -7,7 +7,7 @@
 
 /// Snapshot mode is an offline mode where DevTools can operate on an imported
 /// data file.
-bool snapshotMode = false;
+bool offlineMode = false;
 
 final Map<Type, dynamic> globals = <Type, dynamic>{};
 
diff --git a/devtools/lib/src/inspector/diagnostics_node.dart b/devtools/lib/src/inspector/diagnostics_node.dart
index 7d93686..55f2e8c 100644
--- a/devtools/lib/src/inspector/diagnostics_node.dart
+++ b/devtools/lib/src/inspector/diagnostics_node.dart
@@ -6,7 +6,7 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../ui/fake_flutter/fake_flutter.dart';
 import '../ui/icons.dart';
@@ -24,9 +24,7 @@
   'info': DiagnosticLevel.info,
   'warning': DiagnosticLevel.warning,
   'hint': DiagnosticLevel.hint,
-  'fix': DiagnosticLevel.fix,
-  'contract': DiagnosticLevel.contract,
-  'violation': DiagnosticLevel.violation,
+  'summary': DiagnosticLevel.summary,
   'error': DiagnosticLevel.error,
   'off': DiagnosticLevel.off,
 };
@@ -43,8 +41,7 @@
   'error': DiagnosticsTreeStyle.error,
   'flat': DiagnosticsTreeStyle.flat,
   'singleLine': DiagnosticsTreeStyle.singleLine,
-  'headerLine': DiagnosticsTreeStyle.headerLine,
-  'indentedSingleLine': DiagnosticsTreeStyle.indentedSingleLine,
+  'errorProperty': DiagnosticsTreeStyle.errorProperty,
   'shallow': DiagnosticsTreeStyle.shallow,
   'truncateChildren': DiagnosticsTreeStyle.truncateChildren,
 };
@@ -152,7 +149,13 @@
 
   /// Hint for how the node should be displayed.
   DiagnosticsTreeStyle get style {
-    return getStyleMember('style', DiagnosticsTreeStyle.sparse);
+    return _styleOverride ??
+        getStyleMember('style', DiagnosticsTreeStyle.sparse);
+  }
+
+  DiagnosticsTreeStyle _styleOverride;
+  set style(DiagnosticsTreeStyle style) {
+    _styleOverride = style;
   }
 
   /// Dart class defining the diagnostic node.
diff --git a/devtools/lib/src/inspector/flutter_widget.dart b/devtools/lib/src/inspector/flutter_widget.dart
index 733fda9..4b401c3 100644
--- a/devtools/lib/src/inspector/flutter_widget.dart
+++ b/devtools/lib/src/inspector/flutter_widget.dart
@@ -111,7 +111,7 @@
   }
 
   static Future<Catalog> _loadHelper() async {
-    // Local copy of: https\://github.com/flutter/website/tree/master/_data/catalog/widget.json
+    // Local copy of: https://github.com/flutter/website/blob/master/src/_data/catalog/widgets.json
     final Response response = await get('widgets.json');
     _instance = decode(response.body);
     return _instance;
diff --git a/devtools/lib/src/inspector/inspector.dart b/devtools/lib/src/inspector/inspector.dart
index 747c8d3..b4436ad 100644
--- a/devtools/lib/src/inspector/inspector.dart
+++ b/devtools/lib/src/inspector/inspector.dart
@@ -8,10 +8,11 @@
 import 'dart:html' show Element;
 
 import 'package:split/split.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../framework/framework.dart';
 import '../globals.dart';
+import '../messages.dart';
 import '../service_extensions.dart' as extensions;
 import '../ui/analytics.dart' as ga;
 import '../ui/analytics_platform.dart' as ga_platform;
@@ -19,6 +20,7 @@
 import '../ui/elements.dart';
 import '../ui/icons.dart';
 import '../ui/primer.dart';
+import '../ui/service_extension_elements.dart';
 import '../ui/ui_utils.dart';
 import 'inspector_controller.dart';
 import 'inspector_service.dart';
@@ -26,8 +28,7 @@
 import 'inspector_tree_html.dart';
 import 'inspector_tree_web.dart';
 
-const trackWidgetCreationDocsUrl =
-    'https://flutter.github.io/devtools/inspector#track-widget-creation';
+const inspectorScreenId = 'inspector';
 
 // Generally the canvas tree renderer is a better fit for the inspector.
 // The html renderer is more appropriate for small static trees such as those
@@ -40,19 +41,26 @@
   InspectorScreen({bool disabled, String disabledTooltip})
       : super(
           name: 'Flutter Inspector',
-          id: 'inspector',
+          id: inspectorScreenId,
           iconClass: 'octicon-device-mobile',
           disabled: disabled,
           disabledTooltip: disabledTooltip,
         );
+
   PButton refreshTreeButton;
 
   SetStateMixin inspectorStateMixin = SetStateMixin();
+
   InspectorService inspectorService;
+
   InspectorController inspectorController;
+
   ProgressElement progressElement;
+
   CoreElement inspectorContainer;
+
   StreamSubscription<Object> splitterSubscription;
+
   bool displayedWidgetTrackingNotice = false;
 
   @override
@@ -65,7 +73,7 @@
     final CoreElement buttonSection = div(c: 'section')
       ..layoutHorizontal()
       ..add(<CoreElement>[
-        div(c: 'btn-group collapsible-700 nowrap')
+        div(c: 'btn-group collapsible-750 nowrap')
           ..add([
             ServiceExtensionButton(
               extensions.toggleSelectWidgetMode,
@@ -81,7 +89,7 @@
           ..display = 'none',
         div()..flex(),
       ]);
-    getServiceExtensionButtons().forEach(buttonSection.add);
+    getServiceExtensionElements().forEach(buttonSection.add);
 
     screenDiv.add(<CoreElement>[
       buttonSection,
@@ -96,15 +104,10 @@
     return screenDiv;
   }
 
-  @override
-  void exiting() {
-    framework.clearMessages();
-  }
-
   void _handleConnectionStart(VmService service) async {
     refreshTreeButton.disabled = false;
 
-    final Spinner spinner = Spinner()..clazz('padded');
+    final Spinner spinner = Spinner.centered();
     inspectorContainer.add(spinner);
 
     try {
@@ -114,7 +117,7 @@
       inspectorService =
           await InspectorService.create(service).catchError((e) => null);
     } finally {
-      spinner.element.remove();
+      spinner.remove();
       refreshTreeButton.disabled = false;
     }
 
@@ -189,25 +192,10 @@
         }
 
         displayedWidgetTrackingNotice = true;
-
-        framework.showWarning(children: <CoreElement>[
-          div()
-            ..add(span(text: 'The '))
-            ..add(a(
-                text: 'widget creation tracking feature',
-                href: trackWidgetCreationDocsUrl,
-                target: '_blank;'))
-            ..add(span(text: ' is not enabled. '))
-            ..add(span(
-                text: '''This feature allows the Flutter inspector to present 
-the widget tree in a manner similar to how the UI was defined in your source
-code. Without it, the tree of nodes in the widget tree are much deeper, and it
-can be more difficult to understand how the runtime widget hierarchy corresponds
-to your application\’s UI.''')),
-          div(text: '''To fix this, relaunch your application by running 
-'flutter run --track-widget-creation' (or run your application from VS Code or
-IntelliJ).'''),
-        ]);
+        framework.showMessage(
+          message: trackWidgetCreationWarning,
+          screenId: inspectorScreenId,
+        );
       });
     }
   }
diff --git a/devtools/lib/src/inspector/inspector_controller.dart b/devtools/lib/src/inspector/inspector_controller.dart
index 9d6ecbd..9f12068 100644
--- a/devtools/lib/src/inspector/inspector_controller.dart
+++ b/devtools/lib/src/inspector/inspector_controller.dart
@@ -17,7 +17,7 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../globals.dart';
 import '../ui/fake_flutter/fake_flutter.dart';
@@ -28,7 +28,7 @@
 import 'inspector_text_styles.dart' as inspector_text_styles;
 import 'inspector_tree.dart';
 
-void _logError(error) {
+void _logError(Object error) {
   print(error);
 }
 
@@ -570,7 +570,7 @@
     for (InspectorTreeNode child in node.children) {
       final RemoteDiagnosticsNode diagnosticsNode = child.diagnostic;
       targets.add(child);
-      if (!child.isLeaf && child.expanded) {
+      if (!child.isLeaf && child.isExpanded) {
         // Stop if we get to expanded children as they might be too large
         // to try to scroll into view.
         break;
diff --git a/devtools/lib/src/inspector/inspector_service.dart b/devtools/lib/src/inspector/inspector_service.dart
index ef4eb3e..954ae33 100644
--- a/devtools/lib/src/inspector/inspector_service.dart
+++ b/devtools/lib/src/inspector/inspector_service.dart
@@ -10,7 +10,7 @@
 import 'dart:convert';
 import 'dart:developer';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../eval_on_dart_library.dart';
 import '../globals.dart';
diff --git a/devtools/lib/src/inspector/inspector_text_styles.dart b/devtools/lib/src/inspector/inspector_text_styles.dart
index a5ab66f..974dd6e 100644
--- a/devtools/lib/src/inspector/inspector_text_styles.dart
+++ b/devtools/lib/src/inspector/inspector_text_styles.dart
@@ -2,7 +2,7 @@
 import '../ui/theme.dart';
 
 final TextStyle unimportant = TextStyle(
-  color: ThemedColor(Colors.grey.shade500, Colors.grey.shade400),
+  color: ThemedColor(Colors.grey.shade500, Colors.grey.shade600),
 );
 const TextStyle regular = TextStyle(color: defaultForeground);
 final TextStyle warning = TextStyle(
diff --git a/devtools/lib/src/inspector/inspector_tree.dart b/devtools/lib/src/inspector/inspector_tree.dart
index 8551750..5e784ea 100644
--- a/devtools/lib/src/inspector/inspector_tree.dart
+++ b/devtools/lib/src/inspector/inspector_tree.dart
@@ -14,7 +14,7 @@
 import 'dart:math' as math;
 
 import 'package:meta/meta.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../ui/fake_flutter/fake_flutter.dart';
 import '../ui/icons.dart';
@@ -26,9 +26,13 @@
 import 'inspector_service.dart';
 import 'inspector_text_styles.dart' as inspector_text_styles;
 
-/// Split text into two groups, word characters at the start of a string and all other
-/// characters. Skip an <code>-</code> or <code>#</code> between the two groups.
-final RegExp _primaryDescriptionPattern = RegExp('([\\w ]+)[-#]?(.*)');
+/// Split text into two groups, word characters at the start of a string and all
+/// other characters.
+final RegExp _primaryDescriptionPattern = RegExp(r'^([\w ]+)(.*)$');
+// TODO(jacobr): temporary workaround for missing structure from assertion thrown building
+// widget errors.
+final RegExp _assertionThrownBuildingError = RegExp(
+    r'^(The following assertion was thrown building [a-zA-Z]+)(\(.*\))(:)$');
 
 final ColorIconMaker _colorIconMaker = ColorIconMaker();
 final CustomIconMaker _customIconMaker = CustomIconMaker();
@@ -58,10 +62,9 @@
   Color.fromARGB(255, 150, 150, 150),
 );
 
-const double iconPadding = 3.0;
+const double iconPadding = 5.0;
 const double chartLineStrokeWidth = 1.0;
 const double columnWidth = 16.0;
-const double horizontalPadding = 10.0;
 const double verticalPadding = 10.0;
 const double rowHeight = 24.0;
 const Color arrowColor = Colors.grey;
@@ -140,13 +143,14 @@
 /// This class could be refactored out to be a reasonable generic collapsible
 /// tree ui node class but we choose to instead make it widget inspector
 /// specific as that is the only case we care about.
+// TODO(kenzie): extend TreeNode class to share tree logic.
 abstract class InspectorTreeNode {
   InspectorTreeNode({
     InspectorTreeNode parent,
     bool expandChildren = true,
   })  : _children = <InspectorTreeNode>[],
         _parent = parent,
-        _expanded = expandChildren;
+        _isExpanded = expandChildren;
 
   bool get showLinesToChildren {
     return _children.length > 1 && !_children.last.isProperty;
@@ -171,7 +175,7 @@
     final builder = createRenderBuilder();
     final icon = diagnostic.icon;
     if (showExpandCollapse) {
-      builder.addIcon(expanded ? collapseArrow : expandArrow);
+      builder.addIcon(isExpanded ? collapseArrow : expandArrow);
     }
     if (icon != null) {
       builder.addIcon(icon);
@@ -182,14 +186,15 @@
       // Display of inline properties.
       final String propertyType = diagnostic.propertyType;
       final Map<String, Object> properties = diagnostic.valuePropertiesJson;
-      if (isCreatedByLocalProject) {
-        textStyle = textStyle.merge(inspector_text_styles.regularItalic);
-      }
 
       if (name?.isNotEmpty == true && diagnostic.showName) {
         builder.appendText('$name${diagnostic.separator} ', textStyle);
       }
 
+      if (isCreatedByLocalProject) {
+        textStyle = textStyle.merge(inspector_text_styles.regularBold);
+      }
+
       String description = diagnostic.description;
       if (propertyType != null && properties != null) {
         switch (propertyType) {
@@ -234,7 +239,8 @@
       }
 
       // TODO(jacobr): custom display for units, iterables, and padding.
-      builder.appendText(description, textStyle);
+      _renderDescription(builder, description, textStyle, isProperty: true);
+
       if (diagnostic.level == DiagnosticLevel.fine &&
           diagnostic.hasDefaultValue) {
         builder.appendText(' ', textStyle);
@@ -252,8 +258,10 @@
         if (diagnostic.showSeparator) {
           builder.appendText(
               diagnostic.separator, inspector_text_styles.unimportant);
-        } else {
-          builder.appendText(' ', inspector_text_styles.unimportant);
+          if (diagnostic.separator != ' ' &&
+              diagnostic.description.isNotEmpty) {
+            builder.appendText(' ', inspector_text_styles.unimportant);
+          }
         }
       }
 
@@ -261,19 +269,8 @@
         textStyle = textStyle.merge(inspector_text_styles.regularBold);
       }
 
-      final String description = diagnostic.description;
-      final match = _primaryDescriptionPattern.firstMatch(description);
-      if (match != null) {
-        builder.appendText(' ', inspector_text_styles.unimportant);
-        builder.appendText(match.group(1), textStyle);
-        if (match.group(2).isNotEmpty) {
-          builder.appendText(' ', textStyle);
-          builder.appendText(match.group(2), inspector_text_styles.unimportant);
-        }
-      } else if (diagnostic.description?.isNotEmpty == true) {
-        builder.appendText(' ', inspector_text_styles.unimportant);
-        builder.appendText(diagnostic.description, textStyle);
-      }
+      _renderDescription(builder, diagnostic.description, textStyle,
+          isProperty: false);
     }
     _renderObject = builder.build();
     return _renderObject;
@@ -288,8 +285,8 @@
   bool get isCreatedByLocalProject => _diagnostic.isCreatedByLocalProject;
   bool get isProperty => diagnostic == null || diagnostic.isProperty;
 
-  bool get expanded => _expanded;
-  bool _expanded;
+  bool get isExpanded => _isExpanded;
+  bool _isExpanded;
 
   bool allowExpandCollapse = true;
 
@@ -298,9 +295,9 @@
         allowExpandCollapse;
   }
 
-  set expanded(bool value) {
-    if (value != _expanded) {
-      _expanded = value;
+  set isExpanded(bool value) {
+    if (value != _isExpanded) {
+      _isExpanded = value;
       dirty();
     }
   }
@@ -317,7 +314,7 @@
 
   set diagnostic(RemoteDiagnosticsNode v) {
     _diagnostic = v;
-    _expanded = v.childrenReady;
+    _isExpanded = v.childrenReady;
     dirty();
   }
 
@@ -334,7 +331,7 @@
   }
 
   int get childrenCount {
-    if (!expanded) {
+    if (!isExpanded) {
       _childrenCount = 0;
     }
     if (_childrenCount != null) {
@@ -389,6 +386,9 @@
     int current = 0;
     int depth = 0;
     while (node != null) {
+      final style = node.diagnostic?.style;
+      final bool indented = style != DiagnosticsTreeStyle.flat &&
+          style != DiagnosticsTreeStyle.error;
       if (selection == node) {
         highlightDepth = depth;
       }
@@ -416,14 +416,18 @@
           if (children.length > 1 &&
               i + 1 != children.length &&
               !children.last.isProperty) {
-            ticks.add(depth);
+            if (indented) {
+              ticks.add(depth);
+            }
           }
           break;
         }
         current += subtreeSize;
       }
       assert(i < children.length);
-      depth++;
+      if (indented) {
+        depth++;
+      }
     }
     assert(false); // internal error.
     return null;
@@ -446,6 +450,34 @@
     _children.clear();
     dirty();
   }
+
+  void _renderDescription(
+    InspectorTreeNodeRenderBuilder<InspectorTreeNodeRender<PaintEntry>> builder,
+    String description,
+    TextStyle textStyle, {
+    bool isProperty,
+  }) {
+    if (diagnostic.isDiagnosticableValue) {
+      final match = _primaryDescriptionPattern.firstMatch(description);
+      if (match != null) {
+        builder.appendText(match.group(1), textStyle);
+        if (match.group(2).isNotEmpty) {
+          builder.appendText(match.group(2), inspector_text_styles.unimportant);
+        }
+        return;
+      }
+    } else if (diagnostic.type == 'ErrorDescription') {
+      final match = _assertionThrownBuildingError.firstMatch(description);
+      if (match != null) {
+        builder.appendText(match.group(1), textStyle);
+        builder.appendText(match.group(3), textStyle);
+        return;
+      }
+    }
+    if (description?.isNotEmpty == true) {
+      builder.appendText(description, textStyle);
+    }
+  }
 }
 
 /// A row in the tree with all information required to render it.
@@ -457,7 +489,7 @@
     @required this.depth,
     @required this.isSelected,
     @required this.highlightDepth,
-    this.lineToParent = true,
+    @required this.lineToParent,
   });
 
   final InspectorTreeNode node;
@@ -614,10 +646,7 @@
     _computingHover = false;
   }
 
-  /// Split text into two groups, word characters at the start of a string
-  /// and all other characters. Skip an <code>-</code> or <code>#</code> between
-  /// the two groups.
-  static final RegExp primaryDescriptionRegExp = RegExp(r'(\w+)[-#]?(.*)');
+  double get horizontalPadding => 10.0;
 
   double getDepthIndent(int depth) {
     return (depth + 1) * columnWidth + horizontalPadding;
@@ -649,8 +678,8 @@
   void expandPath(InspectorTreeNode node) {
     setState(() {
       while (node != null) {
-        if (!node.expanded) {
-          node.expanded = true;
+        if (!node.isExpanded) {
+          node.isExpanded = true;
         }
         node = node.parent;
       }
@@ -685,14 +714,16 @@
   void onTapIcon(InspectorTreeRow row, Icon icon) {
     if (icon == expandArrow) {
       setState(() {
-        row.node.expanded = true;
-        onExpand(row.node);
+        row.node.isExpanded = true;
+        if (onExpand != null) {
+          onExpand(row.node);
+        }
       });
       return;
     }
     if (icon == collapseArrow) {
       setState(() {
-        row.node.expanded = false;
+        row.node.isExpanded = false;
       });
       return;
     }
@@ -704,9 +735,9 @@
     // This code matches the text style defaults for which styles are
     //  by default and which aren't.
     switch (style) {
+      case DiagnosticsTreeStyle.none:
       case DiagnosticsTreeStyle.singleLine:
-      case DiagnosticsTreeStyle.headerLine:
-      case DiagnosticsTreeStyle.indentedSingleLine:
+      case DiagnosticsTreeStyle.errorProperty:
         return false;
 
       case DiagnosticsTreeStyle.sparse:
@@ -765,7 +796,7 @@
   }) {
     assert(expandChildren != null);
     assert(expandProperties != null);
-    treeNode.expanded = expandChildren;
+    treeNode.isExpanded = expandChildren;
     if (treeNode.children.isNotEmpty) {
       // Only case supported is this is the loading node.
       assert(treeNode.children.length == 1);
diff --git a/devtools/lib/src/inspector/inspector_tree_canvas.dart b/devtools/lib/src/inspector/inspector_tree_canvas.dart
index ef7688f..8fc29e3 100644
--- a/devtools/lib/src/inspector/inspector_tree_canvas.dart
+++ b/devtools/lib/src/inspector/inspector_tree_canvas.dart
@@ -241,7 +241,7 @@
   bool _recomputeRows = false;
 
   @override
-  void setState(modifyState) {
+  void setState(VoidCallback modifyState) {
     // More closely match Flutter semantics where state is set immediately
     // instead of after a frame.
     modifyState();
diff --git a/devtools/lib/src/inspector/inspector_tree_html.dart b/devtools/lib/src/inspector/inspector_tree_html.dart
index 88f4a86..4abb7fa 100644
--- a/devtools/lib/src/inspector/inspector_tree_html.dart
+++ b/devtools/lib/src/inspector/inspector_tree_html.dart
@@ -20,6 +20,7 @@
 import '../ui/icons.dart';
 import 'diagnostics_node.dart';
 import 'inspector_service.dart';
+import 'inspector_text_styles.dart';
 import 'inspector_tree.dart';
 import 'inspector_tree_web.dart';
 
@@ -100,9 +101,17 @@
     }
     if (textStyle != lastStyle) {
       if (textStyle.color != lastStyle?.color) {
-        color = colorToCss(textStyle.color);
+        if (textStyle.color == regular.color) {
+          color = null;
+        } else {
+          color = colorToCss(textStyle.color);
+        }
       }
-      font = fontStyleToCss(textStyle);
+      if (textStyle == regular) {
+        font = null;
+      } else {
+        font = fontStyleToCss(textStyle);
+      }
       lastStyle = textStyle;
     }
     _entries.add(HtmlTextPaintEntry(text: text, color: color, font: font));
@@ -229,7 +238,7 @@
   bool _recomputeRows = false;
 
   @override
-  void setState(modifyState) {
+  void setState(VoidCallback modifyState) {
     // More closely match Flutter semantics where state is set immediately
     // instead of after a frame.
     modifyState();
@@ -286,6 +295,11 @@
   @override
   InspectorTreeNode createNode() => InspectorTreeNodeHtml();
 
+  // Horizontal padding is specified by CSS so including it here would throw
+  // off calculations.
+  @override
+  double get horizontalPadding => 0.0;
+
   Element paintRow(
     int index, {
     @required InspectorTreeNode selection,
@@ -368,7 +382,7 @@
       if (renderObject == null) {
         return container;
       }
-      currentX = getDepthIndent(row.depth) - columnWidth;
+      currentX = getDepthIndent(row.depth - 1) - columnWidth;
       if (!row.node.showExpandCollapse) {
         currentX += columnWidth;
       }
diff --git a/devtools/lib/src/logging/logging.dart b/devtools/lib/src/logging/logging.dart
index 23ca18c..593d708 100644
--- a/devtools/lib/src/logging/logging.dart
+++ b/devtools/lib/src/logging/logging.dart
@@ -2,36 +2,24 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-import 'dart:convert';
-
-import 'package:intl/intl.dart';
 import 'package:split/split.dart' as split;
-import 'package:vm_service_lib/vm_service_lib.dart';
 
-import '../core/message_bus.dart';
 import '../framework/framework.dart';
-import '../globals.dart';
-import '../inspector/diagnostics_node.dart';
+import '../inspector/inspector.dart';
 import '../inspector/inspector_service.dart';
 import '../inspector/inspector_tree.dart';
 import '../inspector/inspector_tree_html.dart';
+import '../inspector/inspector_tree_web.dart';
+import '../service_extensions.dart';
 import '../tables.dart';
 import '../ui/analytics.dart' as ga;
 import '../ui/analytics_platform.dart' as ga_platform;
 import '../ui/elements.dart';
+import '../ui/fake_flutter/fake_flutter.dart';
 import '../ui/primer.dart';
+import '../ui/service_extension_elements.dart';
 import '../ui/ui_utils.dart';
-import '../utils.dart';
-import '../vm_service_wrapper.dart';
-
-// For performance reasons, we drop old logs in batches, so the log will grow
-// to kMaxLogItemsUpperBound then truncate to kMaxLogItemsLowerBound.
-const int kMaxLogItemsLowerBound = 5000;
-const int kMaxLogItemsUpperBound = 5500;
-final DateFormat timeFormat = DateFormat('HH:mm:ss.SSS');
-
-bool _verboseDebugging = false;
+import 'logging_controller.dart';
 
 class LoggingScreen extends Screen {
   LoggingScreen()
@@ -39,23 +27,19 @@
     logCountStatus = StatusItem();
     logCountStatus.element.text = '';
     addStatusItem(logCountStatus);
-
-    serviceManager.onConnectionAvailable.listen(_handleConnectionStart);
-    if (serviceManager.hasConnection) {
-      _handleConnectionStart(serviceManager.service);
-    }
-    serviceManager.onConnectionClosed.listen(_handleConnectionStop);
+    controller = LoggingController(
+      isVisible: () => visible,
+      onLogCountStatusChanged: (status) {
+        logCountStatus.element.text = status;
+      },
+    );
   }
 
-  Table<LogData> loggingTable;
+  Table<LogData> _loggingTable;
+
+  LoggingController controller;
   LogDetailsUI logDetailsUI;
   StatusItem logCountStatus;
-  SetStateMixin loggingStateMixin = SetStateMixin();
-
-  bool hasPendingDomUpdates = false;
-
-  /// ObjectGroup for Flutter (completes with null for non-Flutter apps).
-  Future<ObjectGroup> objectGroup;
 
   @override
   CoreElement createContent(Framework framework) {
@@ -65,8 +49,16 @@
 
     this.framework = framework;
 
+    _loggingTable = _createTableView();
+    _loggingTable.element
+      ..layoutHorizontal()
+      ..clazz('section')
+      ..clazz('full-size')
+      ..clazz('log-summary');
     // TODO(devoncarew): Add checkbox toggles to enable specific logging channels.
 
+    logDetailsUI = LogDetailsUI();
+
     screenDiv.add(<CoreElement>[
       div(c: 'section')
         ..add(<CoreElement>[
@@ -76,557 +68,64 @@
             ..add(<CoreElement>[
               PButton('Clear logs')
                 ..small()
-                ..click(_clear),
+                ..click(() {
+                  ga.select(ga.logging, ga.clearLogs);
+                  controller.clear();
+                }),
+              div()..flex(),
+              ServiceExtensionCheckbox(structuredErrors).element,
             ])
         ]),
       div(c: 'section log-area bidirectional')
         ..flex()
         ..add(<CoreElement>[
-          _createTableView()
-            ..layoutHorizontal()
-            ..clazz('section')
-            ..clazz('full-size')
-            ..flex(),
-          logDetailsUI = LogDetailsUI(),
+          _loggingTable.element,
+          logDetailsUI,
         ]),
     ]);
 
-    // configure the table / details splitter
-    split.flexSplitBidirectional(
-      [loggingTable.element.element, logDetailsUI.element],
-      gutterSize: defaultSplitterWidth,
-      horizontalSizes: [60, 40],
-      verticalSizes: [70, 30],
+    controller.loggingTableModel = _loggingTable.model;
+    controller.detailsController = LoggingDetailsController(
+      onShowInspector: () {
+        framework.navigateTo(inspectorScreenId);
+      },
+      onShowDetails: logDetailsUI.onShowDetails,
+      createLoggingTree: logDetailsUI.createLoggingTree,
     );
-
-    loggingTable.onSelect.listen((LogData selection) {
-      logDetailsUI.setData(selection);
-    });
-
-    _updateStatus();
-    loggingTable.onRowsChanged.listen((_) {
-      _updateStatus();
-    });
-
-    messageBus.onEvent(type: 'reload.end').listen((BusEvent event) {
-      _log(LogData(
-        'hot.reload',
-        event.data,
-        DateTime.now().millisecondsSinceEpoch,
-      ));
-    });
-    messageBus.onEvent(type: 'restart.end').listen((BusEvent event) {
-      _log(LogData(
-        'hot.restart',
-        event.data,
-        DateTime.now().millisecondsSinceEpoch,
-      ));
-    });
-
     return screenDiv;
   }
 
   @override
-  void entering() {
-    if (hasPendingDomUpdates) {
-      loggingTable.setRows(data);
-
-      hasPendingDomUpdates = false;
-    }
-  }
-
-  CoreElement _createTableView() {
-    loggingTable = Table<LogData>.virtual();
-
-    loggingTable.addColumn(LogWhenColumn());
-    loggingTable.addColumn(LogKindColumn());
-    loggingTable.addColumn(LogMessageColumn());
-
-    loggingTable.setRows(data);
-
-    return loggingTable.element;
-  }
-
-  void _updateStatus() {
-    final int count = loggingTable.rowCount;
-    final String label = count >= kMaxLogItemsLowerBound
-        ? '${nf.format(kMaxLogItemsLowerBound)}+'
-        : nf.format(count);
-    logCountStatus.element.text = '$label events';
-  }
-
-  void _clear() {
-    ga.select(ga.logging, ga.clearLogs);
-    data.clear();
-    logDetailsUI?.setData(null);
-    loggingTable.setRows(data);
-  }
-
-  void _handleConnectionStart(VmServiceWrapper service) async {
-    // Log stdout events.
-    final _StdoutEventHandler stdoutHandler =
-        _StdoutEventHandler(this, 'stdout');
-    service.onStdoutEvent.listen(stdoutHandler.handle);
-
-    // Log stderr events.
-    final _StdoutEventHandler stderrHandler =
-        _StdoutEventHandler(this, 'stderr', isError: true);
-    service.onStderrEvent.listen(stderrHandler.handle);
-
-    // Log GC events.
-    service.onGCEvent.listen(_handleGCEvent);
-
-    // Log `dart:developer` `log` events.
-    // TODO(devoncarew): Remove `_Logging` support on or after approx. Oct 1 2019.
-    service.onEvent('_Logging').listen(_handleDeveloperLogEvent);
-    service.onLoggingEvent.listen(_handleDeveloperLogEvent);
-
-    // Log Flutter extension events.
-    service.onExtensionEvent.listen(_handleExtensionEvent);
-
-    await ensureInspectorServiceDependencies();
-
-    objectGroup = InspectorService.createGroup(service, 'console-group')
-        .catchError((e) => null,
-            test: (e) => e is FlutterInspectorLibraryNotFound);
-  }
-
-  void _handleExtensionEvent(Event e) async {
-    if (e.extensionKind == 'Flutter.Frame') {
-      final FrameInfo frame = FrameInfo.from(e.extensionData.data);
-
-      final String frameId = '#${frame.number}';
-      final String frameInfo =
-          '<span class="pre">$frameId ${frame.elapsedMs.toStringAsFixed(1).padLeft(4)}ms </span>';
-      final String div = createFrameDivHtml(frame);
-
-      _log(LogData(
-        e.extensionKind.toLowerCase(),
-        jsonEncode(e.extensionData.data),
-        e.timestamp,
-        summaryHtml: '$frameInfo$div',
-      ));
-      // todo (pq): add tests for error extension handling once framework changes are landed.
-    } else if (e.extensionKind == 'Flutter.Error') {
-      final RemoteDiagnosticsNode node =
-          RemoteDiagnosticsNode(e.extensionData.data, objectGroup, false, null);
-      if (_verboseDebugging) {
-        print('node toStringDeep:######\n${node.toStringDeep()}\n###');
-      }
-
-      _log(LogData(
-        e.extensionKind.toLowerCase(),
-        jsonEncode(e.json),
-        e.timestamp,
-        summary: node.toDiagnosticsNode().toString(),
-        node: node,
-      ));
-    } else {
-      _log(LogData(
-        e.extensionKind.toLowerCase(),
-        jsonEncode(e.json),
-        e.timestamp,
-        summary: e.json.toString(),
-      ));
-    }
-  }
-
-  void _handleGCEvent(Event e) {
-    final HeapSpace newSpace = HeapSpace.parse(e.json['new']);
-    final HeapSpace oldSpace = HeapSpace.parse(e.json['old']);
-    final Map<dynamic, dynamic> isolateRef = e.json['isolate'];
-
-    final int usedBytes = newSpace.used + oldSpace.used;
-    final int capacityBytes = newSpace.capacity + oldSpace.capacity;
-
-    final int time = ((newSpace.time + oldSpace.time) * 1000).round();
-
-    final String summary = '${isolateRef['name']} • '
-        '${e.json['reason']} collection in $time ms • '
-        '${printMb(usedBytes)} MB used of ${printMb(capacityBytes)} MB';
-
-    final Map<String, dynamic> event = <String, dynamic>{
-      'reason': e.json['reason'],
-      'new': newSpace.json,
-      'old': oldSpace.json,
-      'isolate': isolateRef,
-    };
-
-    final String message = jsonEncode(event);
-    _log(LogData('gc', message, e.timestamp, summary: summary));
-  }
-
-  void _handleDeveloperLogEvent(Event e) {
-    final VmServiceWrapper service = serviceManager.service;
-
-    final dynamic logRecord = e.json['logRecord'];
-
-    String loggerName =
-        _valueAsString(InstanceRef.parse(logRecord['loggerName']));
-    if (loggerName == null || loggerName.isEmpty) {
-      loggerName = 'log';
-    }
-    final int level = logRecord['level'];
-    final InstanceRef messageRef = InstanceRef.parse(logRecord['message']);
-    String summary = _valueAsString(messageRef);
-    if (messageRef.valueAsStringIsTruncated == true) {
-      summary += '...';
-    }
-    final InstanceRef error = InstanceRef.parse(logRecord['error']);
-    final InstanceRef stackTrace = InstanceRef.parse(logRecord['stackTrace']);
-
-    final String details = summary;
-    Future<String> detailsComputer;
-
-    // If the message string was truncated by the VM, or the error object or
-    // stackTrace objects were non-null, we need to ask the VM for more
-    // information in order to render the log entry. We do this asynchronously
-    // on-demand using the `detailsComputer` Future.
-    if (messageRef.valueAsStringIsTruncated == true ||
-        _isNotNull(error) ||
-        _isNotNull(stackTrace)) {
-      detailsComputer = Future<String>(() async {
-        // Get the full string value of the message.
-        String result =
-            await _retrieveFullStringValue(service, e.isolate, messageRef);
-
-        // Get information about the error object. Some users of the
-        // dart:developer log call may pass a data payload in the `error`
-        // field, encoded as a json encoded string, so handle that case.
-        if (_isNotNull(error)) {
-          if (error.valueAsString != null) {
-            final String errorString =
-                await _retrieveFullStringValue(service, e.isolate, error);
-            result += '\n\n$errorString';
-          } else {
-            // Call `toString()` on the error object and display that.
-            final dynamic toStringResult = await service.invoke(
-              e.isolate.id,
-              error.id,
-              'toString',
-              <String>[],
-              disableBreakpoints: true,
-            );
-
-            if (toStringResult is ErrorRef) {
-              final String errorString = _valueAsString(error);
-              result += '\n\n$errorString';
-            } else if (toStringResult is InstanceRef) {
-              final String str = await _retrieveFullStringValue(
-                  service, e.isolate, toStringResult);
-              result += '\n\n$str';
-            }
-          }
-        }
-
-        // Get info about the stackTrace object.
-        if (_isNotNull(stackTrace)) {
-          result += '\n\n${_valueAsString(stackTrace)}';
-        }
-
-        return result;
-      });
-    }
-
-    const int severeIssue = 1000;
-    final bool isError = level != null && level >= severeIssue ? true : false;
-
-    _log(LogData(
-      loggerName,
-      details,
-      e.timestamp,
-      isError: isError,
-      summary: summary,
-      detailsComputer: detailsComputer,
-    ));
-  }
-
-  Future<String> _retrieveFullStringValue(
-    VmServiceWrapper service,
-    IsolateRef isolateRef,
-    InstanceRef stringRef,
-  ) async {
-    if (stringRef.valueAsStringIsTruncated != true) {
-      return stringRef.valueAsString;
-    }
-
-    final dynamic result = await service.getObject(isolateRef.id, stringRef.id,
-        offset: 0, count: stringRef.length);
-    if (result is Instance) {
-      final Instance obj = result;
-      return obj.valueAsString;
-    } else {
-      return '${stringRef.valueAsString}...';
-    }
-  }
-
-  void _handleConnectionStop(dynamic event) {}
-
-  List<LogData> data = <LogData>[];
-
-  DateTime _lastScrollTime;
-
-  void _log(LogData log) {
-    // Insert the new item and clamped the list to kMaxLogItemsLength. The table
-    // is rendered reversed so new items are at the top but we can use .add()
-    // here which is much faster than inserting at the start of the list.
-    data.add(log);
-    // Note: We need to drop rows from the start because we want to drop old
-    // rows but because that's expensive, we only do it periodically (eg. when
-    // the list is 500 rows more).
-    if (data.length > kMaxLogItemsUpperBound) {
-      int itemsToRemove = data.length - kMaxLogItemsLowerBound;
-      // Ensure we remove an even number of rows to keep the alternating
-      // background in-sync.
-      if (itemsToRemove % 2 == 1) {
-        itemsToRemove--;
-      }
-      data = data.sublist(itemsToRemove);
-    }
-
-    if (visible && loggingTable != null) {
-      // TODO(jacobr): adding data should be more incremental than this.
-      // We are blowing away state for all already added rows.
-      loggingTable.setRows(data);
-      // Smooth scroll if we havent scrolled in a while, otherwise use an
-      // immediate scroll because repeatedly smooth scrolling on the web means
-      // you never reach your destination.
-      final DateTime now = DateTime.now();
-      final bool smoothScroll = _lastScrollTime == null ||
-          _lastScrollTime.difference(now).inSeconds > 1;
-      _lastScrollTime = now;
-      loggingTable.scrollTo(data.last,
-          scrollBehavior: smoothScroll ? 'smooth' : 'auto');
-    } else {
-      hasPendingDomUpdates = true;
-    }
-  }
-
-  String createFrameDivHtml(FrameInfo frame) {
-    final String classes = (frame.elapsedMs >= FrameInfo.kTargetMaxFrameTimeMs)
-        ? 'frame-bar over-budget'
-        : 'frame-bar';
-
-    final int pixelWidth = (frame.elapsedMs * 3).round();
-    return '<div class="$classes" style="width: ${pixelWidth}px"/>';
-  }
-}
-
-/// Receive and log stdout / stderr events from the VM.
-///
-/// This class buffers the events for up to 1ms. This is in order to combine a
-/// stdout message and its newline. Currently, `foo\n` is sent as two VM events;
-/// we wait for up to 1ms when we get the `foo` event, to see if the next event
-/// is a single newline. If so, we add the newline to the previous log message.
-class _StdoutEventHandler {
-  _StdoutEventHandler(this.loggingScreen, this.name, {this.isError = false});
-
-  final LoggingScreen loggingScreen;
-  final String name;
-  final bool isError;
-
-  LogData buffer;
-  Timer timer;
-
-  void handle(Event e) {
-    final String message = decodeBase64(e.bytes);
-
-    if (buffer != null) {
-      timer?.cancel();
-
-      if (message == '\n') {
-        loggingScreen._log(LogData(
-          buffer.kind,
-          buffer.details + message,
-          buffer.timestamp,
-          summary: buffer.summary + message,
-          isError: buffer.isError,
-        ));
-        buffer = null;
-        return;
-      }
-
-      loggingScreen._log(buffer);
-      buffer = null;
-    }
-
-    String summary = message;
-    if (message.length > 200) {
-      summary = message.substring(0, 200) + '…';
-    }
-
-    final LogData data = LogData(
-      name,
-      message,
-      e.timestamp,
-      summary: summary,
-      isError: isError,
+  void onContentAttached() {
+    split.fixedSplitBidirectional(
+      [_loggingTable.element.element, logDetailsUI.element],
+      gutterSize: defaultSplitterWidth,
+      horizontalSizes: [60, 40],
+      verticalSizes: [70, 30],
+      minSize: [200, 200],
     );
+  }
 
-    if (message == '\n') {
-      loggingScreen._log(data);
-    } else {
-      buffer = data;
-      timer = Timer(const Duration(milliseconds: 1), () {
-        loggingScreen._log(buffer);
-        buffer = null;
-      });
-    }
+  @override
+  void entering() {
+    controller.entering();
+  }
+
+  Table<LogData> _createTableView() {
+    final table = Table<LogData>.virtual();
+    table.model
+      ..addColumn(LogWhenColumn())
+      ..addColumn(LogKindColumn())
+      ..addColumn(LogMessageColumn());
+    return table;
   }
 }
 
-bool _isNotNull(InstanceRef serviceRef) {
-  return serviceRef != null && serviceRef.kind != 'Null';
-}
-
-String _valueAsString(InstanceRef ref) {
-  if (ref == null) {
-    return null;
-  }
-
-  if (ref.valueAsString == null) {
-    return ref.valueAsString;
-  }
-
-  if (ref.valueAsStringIsTruncated == true) {
-    return '${ref.valueAsString}...';
-  } else {
-    return ref.valueAsString;
-  }
-}
-
-/// A log data object that includes an optional summary (in either text or html
-/// form), information about whether the log entry represents an error entry,
-/// the log entry kind, and more detailed data for the entry.
-///
-/// The details can optionally be loaded lazily on first use. If this is the
-/// case, this log entry will have a non-null `detailsComputer` field. After the
-/// data is calculated, the log entry will be modified to contain the calculated
-/// `details` data.
-class LogData {
-  LogData(
-    this.kind,
-    this._details,
-    this.timestamp, {
-    this.summary,
-    this.summaryHtml,
-    this.isError = false,
-    this.detailsComputer,
-    this.node,
-  });
-
-  final String kind;
-  final int timestamp;
-  final bool isError;
-  final String summary;
-  final String summaryHtml;
-
-  final RemoteDiagnosticsNode node;
-  String _details;
-  Future<String> detailsComputer;
-
-  String get details => _details;
-
-  bool get needsComputing => detailsComputer != null;
-
-  Future<void> compute() async {
-    _details = await detailsComputer;
-    detailsComputer = null;
-  }
-}
-
-class LogKindColumn extends Column<LogData> {
-  LogKindColumn() : super('Kind');
-
-  @override
-  bool get supportsSorting => false;
-
-  @override
-  bool get usesHtml => true;
-
-  @override
-  String get cssClass => 'log-label-column';
-
-  @override
-  dynamic getValue(LogData item) {
-    final String cssClass = getCssClassForEventKind(item);
-
-    return '<span class="label $cssClass">${item.kind}</span>';
-  }
-
-  @override
-  String render(dynamic value) => value;
-}
-
-class LogWhenColumn extends Column<LogData> {
-  LogWhenColumn() : super('When');
-
-  @override
-  String get cssClass => 'pre monospace';
-
-  @override
-  bool get supportsSorting => false;
-
-  @override
-  dynamic getValue(LogData item) => item.timestamp;
-
-  @override
-  String render(dynamic value) {
-    return value == null
-        ? ''
-        : timeFormat.format(DateTime.fromMillisecondsSinceEpoch(value));
-  }
-}
-
-class LogMessageColumn extends Column<LogData> {
-  LogMessageColumn() : super('Message', wide: true);
-
-  @override
-  String get cssClass => 'pre-wrap monospace';
-
-  @override
-  bool get usesHtml => true;
-
-  @override
-  bool get supportsSorting => false;
-
-  @override
-  dynamic getValue(LogData item) => item;
-
-  @override
-  String render(dynamic value) {
-    final LogData log = value;
-
-    if (log.summaryHtml != null) {
-      return log.summaryHtml;
-    } else {
-      return escape(log.summary ?? log.details);
-    }
-  }
-}
-
-String getCssClassForEventKind(LogData item) {
-  String cssClass = '';
-
-  if (item.kind == 'stderr' || item.isError) {
-    cssClass = 'stderr';
-  } else if (item.kind == 'stdout') {
-    cssClass = 'stdout';
-  } else if (item.kind == 'flutter.error') {
-    cssClass = 'stderr';
-  } else if (item.kind.startsWith('flutter')) {
-    cssClass = 'flutter';
-  } else if (item.kind == 'gc') {
-    cssClass = 'gc';
-  }
-
-  return cssClass;
-}
-
+// TODO(jacobr): refactor this code to have a cleaner view-controller
+// separation.
 class LogDetailsUI extends CoreElement {
   LogDetailsUI() : super('div', classes: 'full-size') {
     layoutVertical();
-    flex();
 
     add(<CoreElement>[
       content = div(c: 'log-details table-border')
@@ -635,105 +134,31 @@
     ]);
   }
 
-  static const JsonEncoder jsonEncoder = JsonEncoder.withIndent('  ');
-
-  LogData data;
-
   CoreElement content;
   CoreElement message;
 
-  InspectorTreeHtml tree;
-
-  void setData(LogData data) {
+  void onShowDetails({String text, InspectorTree tree}) {
     // Reset the vertical scroll value if any.
     content.element.scrollTop = 0;
-
-    this.data = data;
-
-    tree = null;
-
-    if (data == null) {
-      message.text = '';
-      return;
+    message.clear();
+    if (text != null) {
+      message.text = text;
     }
-
-    if (data.node != null) {
-      message.clear();
-      tree = InspectorTreeHtml(
-        summaryTree: false,
-        treeType: FlutterTreeType.widget,
-        onSelectionChange: () {
-          final InspectorTreeNode node = tree.selection;
-          if (node != null) {
-            tree.maybePopulateChildren(node);
-          }
-          node.diagnostic.setSelectionInspector(false);
-          // TODO(jacobr): warn if the selection can't be set as the node is
-          // stale which is likely if this is an old log entry.
-        },
-      );
-
-      final InspectorTreeNode root = tree.setupInspectorTreeNode(
-        tree.createNode(),
-        data.node,
-        expandChildren: true,
-        expandProperties: true,
-      );
-      // No sense in collapsing the root node.
-      root.allowExpandCollapse = false;
-      tree.root = root;
-      message.add(tree.element);
-
-      return;
-    }
-
-    // See if we need to asynchronously compute the log entry details.
-    if (data.needsComputing) {
-      message.text = '';
-
-      data.compute().then((_) {
-        // If we're still displaying the same log entry, then update the UI with
-        // the calculated value.
-        if (this.data == data) {
-          _updateUIFromData();
-        }
-      });
-    } else {
-      _updateUIFromData();
+    if (tree != null) {
+      message.add((tree as InspectorTreeWeb).element);
     }
   }
 
-  void _updateUIFromData() {
-    if (data.details.startsWith('{') && data.details.endsWith('}')) {
-      try {
-        // If the string decodes properly, then format the json.
-        final dynamic result = jsonDecode(data.details);
-        message.text = jsonEncoder.convert(result);
-      } catch (e) {
-        message.text = data.details;
-      }
-    } else {
-      message.text = data.details;
-    }
+  InspectorTreeWeb createLoggingTree({VoidCallback onSelectionChange}) {
+    return InspectorTreeHtml(
+      summaryTree: false,
+      treeType: FlutterTreeType.widget,
+      onHover: (node, icon) {
+        element.style.cursor = (node?.diagnostic?.isDiagnosticableValue == true)
+            ? 'pointer'
+            : 'auto';
+      },
+      onSelectionChange: onSelectionChange,
+    );
   }
 }
-
-class FrameInfo {
-  FrameInfo(this.number, this.elapsedMs, this.startTimeMs);
-
-  static const double kTargetMaxFrameTimeMs = 1000.0 / 60;
-
-  static FrameInfo from(Map<String, dynamic> data) {
-    return FrameInfo(
-        data['number'], data['elapsed'] / 1000, data['startTime'] / 1000);
-  }
-
-  final int number;
-  final num elapsedMs;
-  final num startTimeMs;
-
-  num get endTimeMs => startTimeMs + elapsedMs;
-
-  @override
-  String toString() => 'frame $number ${elapsedMs.toStringAsFixed(1)}ms';
-}
diff --git a/devtools/lib/src/logging/logging_controller.dart b/devtools/lib/src/logging/logging_controller.dart
new file mode 100644
index 0000000..96be662
--- /dev/null
+++ b/devtools/lib/src/logging/logging_controller.dart
@@ -0,0 +1,808 @@
+// Copyright 2019 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 'dart:async';
+import 'dart:convert';
+import 'dart:math' as math;
+
+import 'package:intl/intl.dart';
+import 'package:meta/meta.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../core/message_bus.dart';
+import '../globals.dart';
+import '../inspector/diagnostics_node.dart';
+import '../inspector/inspector_service.dart';
+import '../inspector/inspector_tree.dart';
+import '../memory/heap_space.dart';
+import '../table_data.dart';
+import '../ui/fake_flutter/fake_flutter.dart';
+import '../utils.dart';
+import '../vm_service_wrapper.dart';
+
+// For performance reasons, we drop old logs in batches, so the log will grow
+// to kMaxLogItemsUpperBound then truncate to kMaxLogItemsLowerBound.
+const int kMaxLogItemsLowerBound = 5000;
+const int kMaxLogItemsUpperBound = 5500;
+final DateFormat timeFormat = DateFormat('HH:mm:ss.SSS');
+
+bool _verboseDebugging = false;
+
+typedef OnLogCountStatusChanged = void Function(String status);
+
+typedef OnShowDetails = void Function({String text, InspectorTree tree});
+
+typedef CreateLoggingTree = InspectorTree Function(
+    {VoidCallback onSelectionChange});
+
+class LoggingDetailsController {
+  LoggingDetailsController({
+    @required this.onShowInspector,
+    @required this.onShowDetails,
+    @required this.createLoggingTree,
+  });
+
+  static const JsonEncoder jsonEncoder = JsonEncoder.withIndent('  ');
+
+  LogData data;
+
+  /// Callback to execute to show the inspector.
+  final VoidCallback onShowInspector;
+
+  /// Callback to executue to show the data from the details tree in the view.
+  final OnShowDetails onShowDetails;
+
+  /// Callback to create an inspectorTree for the logging view of the correct
+  /// type.
+  final CreateLoggingTree createLoggingTree;
+
+  InspectorTree tree;
+
+  void setData(LogData data) {
+    this.data = data;
+
+    tree = null;
+
+    if (data == null) {
+      onShowDetails(text: '');
+      return;
+    }
+
+    if (data.node != null) {
+      tree = createLoggingTree(
+        onSelectionChange: () {
+          final InspectorTreeNode node = tree.selection;
+          if (node != null) {
+            tree.maybePopulateChildren(node);
+          }
+          // TODO(jacobr): node.diagnostic.isDiagnosticableValue isn't quite
+          // right.
+          if (node.diagnostic.isDiagnosticableValue) {
+            // TODO(jacobr): warn if the selection can't be set as the node is
+            // stale which is likely if this is an old log entry.
+            if (onShowInspector != null) {
+              onShowInspector();
+            }
+            node.diagnostic.setSelectionInspector(false);
+          }
+        },
+      );
+
+      final InspectorTreeNode root = tree.setupInspectorTreeNode(
+        tree.createNode(),
+        data.node,
+        expandChildren: true,
+        expandProperties: true,
+      );
+      // No sense in collapsing the root node.
+      root.allowExpandCollapse = false;
+      tree.root = root;
+      onShowDetails(tree: tree);
+
+      return;
+    }
+
+    // See if we need to asynchronously compute the log entry details.
+    if (data.needsComputing) {
+      assert(data.node == null);
+      onShowDetails(text: '');
+
+      data.compute().then((_) {
+        // If we're still displaying the same log entry, then update the UI with
+        // the calculated value.
+        if (this.data == data) {
+          _updateUIFromData();
+        }
+      });
+    } else {
+      _updateUIFromData();
+    }
+  }
+
+  void _updateUIFromData() {
+    assert(data.node == null);
+    if (data.details.startsWith('{') && data.details.endsWith('}')) {
+      try {
+        // If the string decodes properly, then format the json.
+        final dynamic result = jsonDecode(data.details);
+        onShowDetails(text: jsonEncoder.convert(result));
+      } catch (e) {
+        onShowDetails(text: data.details);
+      }
+    } else {
+      onShowDetails(text: data.details);
+    }
+  }
+}
+
+class LoggingController {
+  LoggingController({
+    @required this.onLogCountStatusChanged,
+    @required this.isVisible,
+  }) {
+    _listen(serviceManager.onConnectionAvailable, _handleConnectionStart);
+    if (serviceManager.hasConnection) {
+      _handleConnectionStart(serviceManager.service);
+    }
+    _listen(serviceManager.onConnectionClosed, _handleConnectionStop);
+  }
+
+  LoggingDetailsController detailsController;
+
+  /// Listen on a stream and track the stream subscription for automatic
+  /// disposal if the dispose method is called.
+  StreamSubscription<T> _listen<T>(Stream<T> stream, void onData(T event)) {
+    final subscription = stream.listen(onData);
+    _subscriptions.add(subscription);
+    return subscription;
+  }
+
+  TableData<LogData> get loggingTableModel => _loggingTableModel;
+  TableData<LogData> _loggingTableModel;
+  set loggingTableModel(TableData<LogData> model) {
+    _loggingTableModel = model;
+    _listen(_loggingTableModel.onSelect, (LogData selection) {
+      detailsController?.setData(selection);
+    });
+
+    _updateStatus();
+    _listen(_loggingTableModel.onRowsChanged, (_) {
+      _updateStatus();
+    });
+
+    // TODO(jacobr): expose the messageBus for use by vm tests.
+    if (messageBus != null) {
+      _listen(messageBus.onEvent(type: 'reload.end'), (BusEvent event) {
+        _log(LogData(
+          'hot.reload',
+          event.data,
+          DateTime.now().millisecondsSinceEpoch,
+        ));
+      });
+      _listen(messageBus.onEvent(type: 'restart.end'), (BusEvent event) {
+        _log(LogData(
+          'hot.restart',
+          event.data,
+          DateTime.now().millisecondsSinceEpoch,
+        ));
+      });
+    }
+  }
+
+  /// Callbacks to apply changes in the controller to other views.
+  final OnLogCountStatusChanged onLogCountStatusChanged;
+
+  /// Callback returning whether the logging screen is visible.
+  final bool Function() isVisible;
+
+  List<LogData> data = <LogData>[];
+
+  final List<StreamSubscription> _subscriptions = [];
+
+  DateTime _lastScrollTime;
+
+  bool _hasPendingUiUpdates = false;
+
+  /// ObjectGroup for Flutter (completes with null for non-Flutter apps).
+  Future<ObjectGroup> objectGroup;
+
+  void _updateStatus() {
+    final int count = _loggingTableModel.rowCount;
+    final String label = count >= kMaxLogItemsLowerBound
+        ? '${nf.format(kMaxLogItemsLowerBound)}+'
+        : nf.format(count);
+    onLogCountStatusChanged('$label events');
+  }
+
+  void clear() {
+    data.clear();
+    detailsController?.setData(null);
+    _loggingTableModel.setRows(data);
+  }
+
+  void _handleConnectionStart(VmServiceWrapper service) async {
+    // Log stdout events.
+    final _StdoutEventHandler stdoutHandler =
+        _StdoutEventHandler(this, 'stdout');
+    _listen(service.onStdoutEvent, stdoutHandler.handle);
+
+    // Log stderr events.
+    final _StdoutEventHandler stderrHandler =
+        _StdoutEventHandler(this, 'stderr', isError: true);
+    _listen(service.onStderrEvent, stderrHandler.handle);
+
+    // Log GC events.
+    _listen(service.onGCEvent, _handleGCEvent);
+
+    // Log `dart:developer` `log` events.
+    // TODO(devoncarew): Remove `_Logging` support on or after approx. Oct 1 2019.
+    _listen(service.onEvent('_Logging'), _handleDeveloperLogEvent);
+    _listen(service.onLoggingEvent, _handleDeveloperLogEvent);
+
+    // Log Flutter extension events.
+    _listen(service.onExtensionEvent, _handleExtensionEvent);
+
+    await ensureInspectorServiceDependencies();
+
+    objectGroup = InspectorService.createGroup(service, 'console-group')
+        .catchError((e) => null,
+            test: (e) => e is FlutterInspectorLibraryNotFound);
+  }
+
+  void _handleExtensionEvent(Event e) async {
+    // Events to show without a summary in the table.
+    const Set<String> untitledEvents = {
+      'Flutter.FirstFrame',
+      'Flutter.FrameworkInitialization',
+    };
+
+    // TODO(jacobr): make the list of filtered events configurable.
+    const Set<String> filteredEvents = {
+      // Suppress these events by default as they just add noise to the log
+      ServiceExtensionStateChangedInfo.eventName,
+    };
+
+    if (filteredEvents.contains(e.extensionKind)) {
+      return;
+    }
+    if (e.extensionKind == FrameInfo.eventName) {
+      final FrameInfo frame = FrameInfo.from(e.extensionData.data);
+
+      final String frameId = '#${frame.number}';
+      final String frameInfo =
+          '<span class="pre">$frameId ${frame.elapsedMs.toStringAsFixed(1).padLeft(4)}ms </span>';
+      final String div = _createFrameDivHtml(frame);
+
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.extensionData.data),
+        e.timestamp,
+        summaryHtml: '$frameInfo$div',
+      ));
+    } else if (e.extensionKind == NavigationInfo.eventName) {
+      final NavigationInfo navInfo = NavigationInfo.from(e.extensionData.data);
+
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.json),
+        e.timestamp,
+        summary: navInfo.routeDescription,
+      ));
+    } else if (untitledEvents.contains(e.extensionKind)) {
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.json),
+        e.timestamp,
+        summary: '',
+      ));
+    } else if (e.extensionKind == ServiceExtensionStateChangedInfo.eventName) {
+      final ServiceExtensionStateChangedInfo changedInfo =
+          ServiceExtensionStateChangedInfo.from(e.extensionData.data);
+
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.json),
+        e.timestamp,
+        summary: '${changedInfo.extension}: ${changedInfo.value}',
+      ));
+    } else if (e.extensionKind == 'Flutter.Error') {
+      // TODO(pq): add tests for error extension handling once framework changes
+      // are landed.
+      final RemoteDiagnosticsNode node =
+          RemoteDiagnosticsNode(e.extensionData.data, objectGroup, false, null);
+      // Workaround the fact that the error objects from the server don't have
+      // style error.
+      node.style = DiagnosticsTreeStyle.error;
+      if (_verboseDebugging) {
+        print('node toStringDeep:######\n${node.toStringDeep()}\n###');
+      }
+
+      final RemoteDiagnosticsNode summary = _findFirstSummary(node) ?? node;
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.json),
+        e.timestamp,
+        summary: summary.toDiagnosticsNode().toString(),
+        node: node,
+      ));
+    } else {
+      _log(LogData(
+        e.extensionKind.toLowerCase(),
+        jsonEncode(e.json),
+        e.timestamp,
+        summary: e.json.toString(),
+      ));
+    }
+  }
+
+  void _handleGCEvent(Event e) {
+    final HeapSpace newSpace = HeapSpace.parse(e.json['new']);
+    final HeapSpace oldSpace = HeapSpace.parse(e.json['old']);
+    final Map<dynamic, dynamic> isolateRef = e.json['isolate'];
+
+    final int usedBytes = newSpace.used + oldSpace.used;
+    final int capacityBytes = newSpace.capacity + oldSpace.capacity;
+
+    final int time = ((newSpace.time + oldSpace.time) * 1000).round();
+
+    final String summary = '${isolateRef['name']} • '
+        '${e.json['reason']} collection in $time ms • '
+        '${printMb(usedBytes)} MB used of ${printMb(capacityBytes)} MB';
+
+    final Map<String, dynamic> event = <String, dynamic>{
+      'reason': e.json['reason'],
+      'new': newSpace.json,
+      'old': oldSpace.json,
+      'isolate': isolateRef,
+    };
+
+    final String message = jsonEncode(event);
+    _log(LogData('gc', message, e.timestamp, summary: summary));
+  }
+
+  void _handleDeveloperLogEvent(Event e) {
+    final VmServiceWrapper service = serviceManager.service;
+
+    final dynamic logRecord = e.json['logRecord'];
+
+    String loggerName =
+        _valueAsString(InstanceRef.parse(logRecord['loggerName']));
+    if (loggerName == null || loggerName.isEmpty) {
+      loggerName = 'log';
+    }
+    final int level = logRecord['level'];
+    final InstanceRef messageRef = InstanceRef.parse(logRecord['message']);
+    String summary = _valueAsString(messageRef);
+    if (messageRef.valueAsStringIsTruncated == true) {
+      summary += '...';
+    }
+    final InstanceRef error = InstanceRef.parse(logRecord['error']);
+    final InstanceRef stackTrace = InstanceRef.parse(logRecord['stackTrace']);
+
+    final String details = summary;
+    Future<String> detailsComputer;
+
+    // If the message string was truncated by the VM, or the error object or
+    // stackTrace objects were non-null, we need to ask the VM for more
+    // information in order to render the log entry. We do this asynchronously
+    // on-demand using the `detailsComputer` Future.
+    if (messageRef.valueAsStringIsTruncated == true ||
+        _isNotNull(error) ||
+        _isNotNull(stackTrace)) {
+      detailsComputer = Future<String>(() async {
+        // Get the full string value of the message.
+        String result =
+            await _retrieveFullStringValue(service, e.isolate, messageRef);
+
+        // Get information about the error object. Some users of the
+        // dart:developer log call may pass a data payload in the `error`
+        // field, encoded as a json encoded string, so handle that case.
+        if (_isNotNull(error)) {
+          if (error.valueAsString != null) {
+            final String errorString =
+                await _retrieveFullStringValue(service, e.isolate, error);
+            result += '\n\n$errorString';
+          } else {
+            // Call `toString()` on the error object and display that.
+            final dynamic toStringResult = await service.invoke(
+              e.isolate.id,
+              error.id,
+              'toString',
+              <String>[],
+              disableBreakpoints: true,
+            );
+
+            if (toStringResult is ErrorRef) {
+              final String errorString = _valueAsString(error);
+              result += '\n\n$errorString';
+            } else if (toStringResult is InstanceRef) {
+              final String str = await _retrieveFullStringValue(
+                  service, e.isolate, toStringResult);
+              result += '\n\n$str';
+            }
+          }
+        }
+
+        // Get info about the stackTrace object.
+        if (_isNotNull(stackTrace)) {
+          result += '\n\n${_valueAsString(stackTrace)}';
+        }
+
+        return result;
+      });
+    }
+
+    const int severeIssue = 1000;
+    final bool isError = level != null && level >= severeIssue ? true : false;
+
+    _log(LogData(
+      loggerName,
+      details,
+      e.timestamp,
+      isError: isError,
+      summary: summary,
+      detailsComputer: detailsComputer,
+    ));
+  }
+
+  Future<String> _retrieveFullStringValue(
+    VmServiceWrapper service,
+    IsolateRef isolateRef,
+    InstanceRef stringRef,
+  ) async {
+    if (stringRef.valueAsStringIsTruncated != true) {
+      return stringRef.valueAsString;
+    }
+
+    final dynamic result = await service.getObject(isolateRef.id, stringRef.id,
+        offset: 0, count: stringRef.length);
+    if (result is Instance) {
+      final Instance obj = result;
+      return obj.valueAsString;
+    } else {
+      return '${stringRef.valueAsString}...';
+    }
+  }
+
+  void _handleConnectionStop(dynamic event) {}
+
+  void _log(LogData log) {
+    // Insert the new item and clamped the list to kMaxLogItemsLength.
+    data.add(log);
+    // Note: We need to drop rows from the start because we want to drop old
+    // rows but because that's expensive, we only do it periodically (eg. when
+    // the list is 500 rows more).
+    if (data.length > kMaxLogItemsUpperBound) {
+      int itemsToRemove = data.length - kMaxLogItemsLowerBound;
+      // Ensure we remove an even number of rows to keep the alternating
+      // background in-sync.
+      if (itemsToRemove % 2 == 1) {
+        itemsToRemove--;
+      }
+      data = data.sublist(itemsToRemove);
+    }
+
+    if (isVisible() && _loggingTableModel != null) {
+      // TODO(jacobr): adding data should be more incremental than this.
+      // We are blowing away state for all already added rows.
+      _loggingTableModel.setRows(data);
+      // Smooth scroll if we haven't scrolled in a while, otherwise use an
+      // immediate scroll because repeatedly smooth scrolling on the web means
+      // you never reach your destination.
+      final DateTime now = DateTime.now();
+      final bool smoothScroll = _lastScrollTime == null ||
+          _lastScrollTime.difference(now).inSeconds > 1;
+      _lastScrollTime = now;
+      _loggingTableModel.scrollTo(data.last,
+          scrollBehavior: smoothScroll ? 'smooth' : 'auto');
+    } else {
+      _hasPendingUiUpdates = true;
+    }
+  }
+
+  void entering() {
+    if (_hasPendingUiUpdates) {
+      _loggingTableModel.setRows(data);
+      _loggingTableModel.scrollTo(data.last, scrollBehavior: 'auto');
+      _hasPendingUiUpdates = false;
+    }
+  }
+
+  RemoteDiagnosticsNode _findFirstSummary(RemoteDiagnosticsNode node) {
+    if (node.level == DiagnosticLevel.summary) {
+      return node;
+    }
+    RemoteDiagnosticsNode summary;
+    if (node.inlineProperties != null) {
+      for (RemoteDiagnosticsNode property in node.inlineProperties) {
+        summary = _findFirstSummary(property);
+        if (summary != null) return summary;
+      }
+    }
+    if (node.childrenNow != null) {
+      for (RemoteDiagnosticsNode child in node.childrenNow) {
+        summary = _findFirstSummary(child);
+        if (summary != null) return summary;
+      }
+    }
+    return null;
+  }
+
+  void dispose() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+}
+
+/// Receive and log stdout / stderr events from the VM.
+///
+/// This class buffers the events for up to 1ms. This is in order to combine a
+/// stdout message and its newline. Currently, `foo\n` is sent as two VM events;
+/// we wait for up to 1ms when we get the `foo` event, to see if the next event
+/// is a single newline. If so, we add the newline to the previous log message.
+class _StdoutEventHandler {
+  _StdoutEventHandler(this.loggingController, this.name,
+      {this.isError = false});
+
+  final LoggingController loggingController;
+  final String name;
+  final bool isError;
+
+  LogData buffer;
+  Timer timer;
+
+  void handle(Event e) {
+    final String message = decodeBase64(e.bytes);
+
+    if (buffer != null) {
+      timer?.cancel();
+
+      if (message == '\n') {
+        loggingController._log(LogData(
+          buffer.kind,
+          buffer.details + message,
+          buffer.timestamp,
+          summary: buffer.summary + message,
+          isError: buffer.isError,
+        ));
+        buffer = null;
+        return;
+      }
+
+      loggingController._log(buffer);
+      buffer = null;
+    }
+
+    String summary = message;
+    if (message.length > 200) {
+      summary = message.substring(0, 200) + '…';
+    }
+
+    final LogData data = LogData(
+      name,
+      message,
+      e.timestamp,
+      summary: summary,
+      isError: isError,
+    );
+
+    if (message == '\n') {
+      loggingController._log(data);
+    } else {
+      buffer = data;
+      timer = Timer(const Duration(milliseconds: 1), () {
+        loggingController._log(buffer);
+        buffer = null;
+      });
+    }
+  }
+}
+
+bool _isNotNull(InstanceRef serviceRef) {
+  return serviceRef != null && serviceRef.kind != 'Null';
+}
+
+String _valueAsString(InstanceRef ref) {
+  if (ref == null) {
+    return null;
+  }
+
+  if (ref.valueAsString == null) {
+    return ref.valueAsString;
+  }
+
+  if (ref.valueAsStringIsTruncated == true) {
+    return '${ref.valueAsString}...';
+  } else {
+    return ref.valueAsString;
+  }
+}
+
+/// A log data object that includes an optional summary (in either text or html
+/// form), information about whether the log entry represents an error entry,
+/// the log entry kind, and more detailed data for the entry.
+///
+/// The details can optionally be loaded lazily on first use. If this is the
+/// case, this log entry will have a non-null `detailsComputer` field. After the
+/// data is calculated, the log entry will be modified to contain the calculated
+/// `details` data.
+class LogData {
+  LogData(
+    this.kind,
+    this._details,
+    this.timestamp, {
+    this.summary,
+    this.summaryHtml,
+    this.isError = false,
+    this.detailsComputer,
+    this.node,
+  });
+
+  final String kind;
+  final int timestamp;
+  final bool isError;
+  final String summary;
+  final String summaryHtml;
+
+  final RemoteDiagnosticsNode node;
+  String _details;
+  Future<String> detailsComputer;
+
+  String get details => _details;
+
+  bool get needsComputing => detailsComputer != null;
+
+  Future<void> compute() async {
+    _details = await detailsComputer;
+    detailsComputer = null;
+  }
+}
+
+class LogKindColumn extends ColumnData<LogData> {
+  LogKindColumn() : super('Kind');
+
+  @override
+  bool get supportsSorting => false;
+
+  @override
+  bool get usesHtml => true;
+
+  @override
+  String get cssClass => 'log-label-column';
+
+  @override
+  dynamic getValue(LogData dataObject) {
+    final String cssClass = getCssClassForEventKind(dataObject);
+
+    return '<span class="label $cssClass">${dataObject.kind}</span>';
+  }
+
+  @override
+  String render(dynamic value) => value;
+}
+
+class LogWhenColumn extends ColumnData<LogData> {
+  LogWhenColumn() : super('When');
+
+  @override
+  String get cssClass => 'pre monospace';
+
+  @override
+  bool get supportsSorting => false;
+
+  @override
+  dynamic getValue(LogData dataObject) => dataObject.timestamp;
+
+  @override
+  String render(dynamic value) {
+    return value == null
+        ? ''
+        : timeFormat.format(DateTime.fromMillisecondsSinceEpoch(value));
+  }
+}
+
+class LogMessageColumn extends ColumnData<LogData> {
+  LogMessageColumn() : super.wide('Message');
+
+  @override
+  String get cssClass => 'pre-wrap monospace';
+
+  @override
+  bool get usesHtml => true;
+
+  @override
+  bool get supportsSorting => false;
+
+  @override
+  dynamic getValue(LogData dataObject) => dataObject;
+
+  @override
+  String render(dynamic value) {
+    final LogData log = value;
+
+    if (log.summaryHtml != null) {
+      return log.summaryHtml;
+    } else {
+      return escape(log.summary ?? log.details);
+    }
+  }
+}
+
+class FrameInfo {
+  FrameInfo(this.number, this.elapsedMs, this.startTimeMs);
+
+  static const String eventName = 'Flutter.Frame';
+
+  static const double kTargetMaxFrameTimeMs = 1000.0 / 60;
+
+  static FrameInfo from(Map<String, dynamic> data) {
+    return FrameInfo(
+        data['number'], data['elapsed'] / 1000, data['startTime'] / 1000);
+  }
+
+  final int number;
+  final num elapsedMs;
+  final num startTimeMs;
+
+  @override
+  String toString() => 'frame $number ${elapsedMs.toStringAsFixed(1)}ms';
+}
+
+class NavigationInfo {
+  NavigationInfo(this._route);
+
+  static const String eventName = 'Flutter.Navigation';
+
+  static NavigationInfo from(Map<String, dynamic> data) {
+    return NavigationInfo(data['route']);
+  }
+
+  final Map<String, dynamic> _route;
+
+  String get routeDescription => _route == null ? null : _route['description'];
+}
+
+class ServiceExtensionStateChangedInfo {
+  ServiceExtensionStateChangedInfo(this.extension, this.value);
+
+  static const String eventName = 'Flutter.ServiceExtensionStateChanged';
+
+  static ServiceExtensionStateChangedInfo from(Map<String, dynamic> data) {
+    return ServiceExtensionStateChangedInfo(data['extension'], data['value']);
+  }
+
+  final String extension;
+  final dynamic value;
+}
+
+String getCssClassForEventKind(LogData item) {
+  String cssClass = '';
+
+  if (item.kind == 'stderr' || item.isError) {
+    cssClass = 'stderr';
+  } else if (item.kind == 'stdout') {
+    cssClass = 'stdout';
+  } else if (item.kind == 'flutter.error') {
+    cssClass = 'stderr';
+  } else if (item.kind.startsWith('flutter')) {
+    cssClass = 'flutter';
+  } else if (item.kind == 'gc') {
+    cssClass = 'gc';
+  }
+
+  return cssClass;
+}
+
+String _createFrameDivHtml(FrameInfo frame) {
+  const double maxFrameEventBarMs = 100.0;
+  final String classes = (frame.elapsedMs >= FrameInfo.kTargetMaxFrameTimeMs)
+      ? 'frame-bar over-budget'
+      : 'frame-bar';
+
+  final int pixelWidth =
+      (math.min(frame.elapsedMs, maxFrameEventBarMs) * 3).round();
+  return '<div class="$classes" style="width: ${pixelWidth}px"/>';
+}
diff --git a/devtools/lib/src/main.dart b/devtools/lib/src/main.dart
index 3a7c023..85e8202 100644
--- a/devtools/lib/src/main.dart
+++ b/devtools/lib/src/main.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 import 'dart:html' as html;
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import 'core/message_bus.dart';
 import 'debugger/debugger.dart';
@@ -15,9 +15,10 @@
 import 'logging/logging.dart';
 import 'memory/memory.dart';
 import 'model/model.dart';
-import 'performance/performance.dart';
+import 'performance/performance_screen.dart';
 import 'service_registrations.dart' as registrations;
-import 'timeline/timeline.dart';
+import 'settings/settings_screen.dart';
+import 'timeline/timeline_screen.dart';
 import 'ui/analytics.dart' as ga;
 import 'ui/analytics_platform.dart' as ga_platform;
 import 'ui/custom.dart';
@@ -29,25 +30,29 @@
 
 // TODO(devoncarew): make the screens more robust through restarts
 
-const bool showPerformancePage = false;
-
 const flutterLibraryUri = 'package:flutter/src/widgets/binding.dart';
 const flutterWebLibraryUri = 'package:flutter_web/src/widgets/binding.dart';
 
 class PerfToolFramework extends Framework {
   PerfToolFramework() {
     html.window.onError.listen(_gAReportExceptions);
+
     initGlobalUI();
     initTestingModel();
   }
 
   void _gAReportExceptions(html.Event e) {
     final html.ErrorEvent errorEvent = e as html.ErrorEvent;
-    ga.error(
-        '${errorEvent.message}\n'
+
+    final message = '${errorEvent.message}\n'
         '${errorEvent.filename}@${errorEvent.lineno}:${errorEvent.colno}\n'
-        '${errorEvent.error}',
-        true);
+        '${errorEvent.error}';
+
+    // Report exceptions with DevTools to GA.
+    ga.error(message, true);
+
+    // Also write them to the console to aid debugging.
+    print(message);
   }
 
   StatusItem isolateSelectStatus;
@@ -60,19 +65,27 @@
   static const _restartActionId = 'restart-action';
 
   void initGlobalUI() async {
+    // Listen for clicks on the 'send feedback' button.
+    queryId('send-feedback-button').onClick.listen((_) {
+      ga.select(ga.devToolsMain, ga.feedback);
+      // TODO(devoncarew): Fill in useful product info here, like the Flutter
+      // SDK version and the version of DevTools in use.
+      html.window
+          .open('https://github.com/flutter/devtools/issues', '_feedback');
+    });
+
     await serviceManager.serviceAvailable.future;
     await addScreens();
-    sortScreens();
     screensReady.complete();
 
-    final CoreElement mainNav = CoreElement.from(queryId('main-nav'));
-    mainNav.clear();
+    final mainNav = CoreElement.from(queryId('main-nav'))..clear();
+    final iconNav = CoreElement.from(queryId('icon-nav'))..clear();
 
     for (Screen screen in screens) {
-      final CoreElement link = CoreElement('a')
+      final link = CoreElement('a')
         ..add(<CoreElement>[
           span(c: 'octicon ${screen.iconClass}'),
-          span(text: ' ${screen.name}', c: 'optional-950')
+          span(text: ' ${screen.name}', c: 'optional-1060')
         ]);
       if (screen.disabled) {
         link
@@ -90,7 +103,7 @@
             navigateTo(screen.id);
           });
       }
-      mainNav.add(link);
+      (screen.showTab ? mainNav : iconNav).add(link);
     }
 
     isolateSelectStatus = StatusItem();
@@ -116,19 +129,11 @@
         toast('Device connection lost.');
       }
     });
-
-    // Listen for clicks on the 'send feedback' button.
-    queryId('send-feedback-button').onClick.listen((_) {
-      ga.select(ga.devToolsMain, ga.feedback);
-      // TODO(devoncarew): Fill in useful product info here, like the Flutter
-      // SDK version and the version of DevTools in use.
-      html.window
-          .open('https://github.com/flutter/devtools/issues', '_feedback');
-    });
   }
 
   void initTestingModel() {
-    App.register(this);
+    final app = App.register(this);
+    screensReady.future.then(app.devToolsReady);
   }
 
   void disableAppWithError(String title, [dynamic error]) {
@@ -145,18 +150,26 @@
     final _isFlutterWebApp = await serviceManager.connectedApp.isFlutterWebApp;
     final _isProfileBuild = await serviceManager.connectedApp.isProfileBuild;
     final _isAnyFlutterApp = await serviceManager.connectedApp.isAnyFlutterApp;
+    final _isDartWebApp = await serviceManager.connectedApp.isDartWebApp;
+
+    const notRunningFlutterApp =
+        'This screen is disabled because you are not running a Flutter '
+        'application';
+    const runningFlutterWeb =
+        'This screen is disabled because it is not yet ready for Flutter Web';
+    const runningProfileBuild =
+        'This screen is disabled because you are running a profile build of '
+        'your application';
+    const duplicateDebuggerFunctionality =
+        'This screen is disabled because it provides functionality already '
+        'available in your code editor';
+    const runningDartWeb =
+        'This screen is disabled because you are running a Dart web app';
 
     String getDebuggerDisabledTooltip() {
-      if (_isFlutterWebApp) {
-        return 'This screen is disabled because it is not yet ready for Flutter'
-            ' Web';
-      }
-      if (_isProfileBuild) {
-        return 'This screen is disabled because you are running a profile build'
-            ' of your application';
-      }
-      return 'This screen is disabled because it provides functionality already'
-          ' available in your code editor';
+      if (_isFlutterWebApp) return runningFlutterWeb;
+      if (_isProfileBuild) return runningProfileBuild;
+      return duplicateDebuggerFunctionality;
     }
 
     // Collect all platform information flutter, web, chrome, versions, etc. for
@@ -165,29 +178,22 @@
 
     addScreen(InspectorScreen(
       disabled: !_isAnyFlutterApp || _isProfileBuild,
-      disabledTooltip: !_isAnyFlutterApp
-          ? 'This screen is disabled because you are not running a Flutter '
-              'application'
-          : 'This screen is disabled because you are running a profile build '
-              'of your application',
+      disabledTooltip:
+          !_isAnyFlutterApp ? notRunningFlutterApp : runningProfileBuild,
     ));
     addScreen(TimelineScreen(
       disabled: !_isFlutterApp,
-      disabledTooltip: _isFlutterWebApp
-          ? 'This screen is disabled because it is not yet ready for Flutter'
-              ' Web'
-          : 'This screen is disabled because you are not running a '
-              'Flutter application',
+      disabledTooltip:
+          _isFlutterWebApp ? runningFlutterWeb : notRunningFlutterApp,
     ));
     addScreen(MemoryScreen(
-      disabled: _isFlutterWebApp,
-      disabledTooltip:
-          'This screen is disabled because it is not yet ready for Flutter'
-          ' Web',
+      disabled: _isFlutterWebApp || _isDartWebApp,
+      disabledTooltip: _isFlutterWebApp ? runningFlutterWeb : runningDartWeb,
     ));
-    if (showPerformancePage) {
-      addScreen(PerformanceScreen());
-    }
+    addScreen(PerformanceScreen(
+      disabled: _isFlutterWebApp || _isDartWebApp,
+      disabledTooltip: _isFlutterWebApp ? runningFlutterWeb : runningDartWeb,
+    ));
     addScreen(DebuggerScreen(
       disabled: _isFlutterWebApp ||
           _isProfileBuild ||
@@ -195,16 +201,7 @@
       disabledTooltip: getDebuggerDisabledTooltip(),
     ));
     addScreen(LoggingScreen());
-  }
-
-  void sortScreens() {
-    // Move disabled screens to the end, but otherwise preserve order.
-    final sortedScreens = screens
-        .where((screen) => !screen.disabled)
-        .followedBy(screens.where((screen) => screen.disabled))
-        .toList();
-    screens.clear();
-    screens.addAll(sortedScreens);
+    addScreen(SettingsScreen());
   }
 
   IsolateRef get currentIsolate =>
@@ -255,7 +252,7 @@
     // is initialed, and react to those events in the UI. Going forward, we'll
     // want to instead have flutter_tools fire hot reload events, and react to
     // them in the UI. That will mean that our UI will update appropriately
-    // even when other clients (the CLI, and IDE) initial the hot reload.
+    // even when other clients (the CLI, and IDE) initiate the hot reload.
 
     final ActionButton reloadAction = ActionButton(
       _reloadActionId,
diff --git a/devtools/lib/src/memory/heap_space.dart b/devtools/lib/src/memory/heap_space.dart
new file mode 100644
index 0000000..1865f20
--- /dev/null
+++ b/devtools/lib/src/memory/heap_space.dart
@@ -0,0 +1,51 @@
+// Copyright 2019 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.
+
+class HeapSpace {
+  HeapSpace._fromJson(this.json)
+      : avgCollectionPeriodMillis = json['avgCollectionPeriodMillis'],
+        capacity = json['capacity'],
+        collections = json['collections'],
+        external = json['external'],
+        name = json['name'],
+        time = json['time'],
+        used = json['used'];
+
+  static HeapSpace parse(Map<String, dynamic> json) =>
+      json == null ? null : new HeapSpace._fromJson(json);
+
+  final Map<String, dynamic> json;
+
+  final double avgCollectionPeriodMillis;
+
+  final int capacity;
+
+  final int collections;
+
+  final int external;
+
+  final String name;
+
+  final double time;
+
+  final int used;
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+    json['type'] = 'HeapSpace';
+    json.addAll({
+      'avgCollectionPeriodMillis': avgCollectionPeriodMillis,
+      'capacity': capacity,
+      'collections': collections,
+      'external': external,
+      'name': name,
+      'time': time,
+      'used': used,
+    });
+    return json;
+  }
+
+  @override
+  String toString() => '[HeapSpace]';
+}
diff --git a/devtools/lib/src/memory/memory.dart b/devtools/lib/src/memory/memory.dart
index b950983..3cffa6c 100644
--- a/devtools/lib/src/memory/memory.dart
+++ b/devtools/lib/src/memory/memory.dart
@@ -4,13 +4,15 @@
 
 import 'dart:async';
 import 'dart:collection';
+import 'dart:html' as html;
 
-import 'package:devtools/src/debugger/debugger_state.dart';
 import 'package:meta/meta.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../framework/framework.dart';
 import '../globals.dart';
+import '../popup.dart';
+import '../table_data.dart';
 import '../tables.dart';
 import '../ui/analytics.dart' as ga;
 import '../ui/analytics_platform.dart' as ga_platform;
@@ -24,58 +26,120 @@
 import 'memory_controller.dart';
 import 'memory_data_view.dart';
 import 'memory_detail.dart';
+import 'memory_inbounds.dart';
 import 'memory_protocol.dart';
+import 'memory_service.dart';
+
+const memoryScreenId = 'memory';
 
 class MemoryScreen extends Screen with SetStateMixin {
   MemoryScreen({bool disabled, String disabledTooltip})
-      : _debuggerState = DebuggerState(),
-        super(
+      : super(
           name: 'Memory',
-          id: 'memory',
+          id: memoryScreenId,
           iconClass: 'octicon-package',
           disabled: disabled,
           disabledTooltip: disabledTooltip,
         ) {
+    // Hookup for memory UI short-cut keys.
+    shortcutCallback = memoryShortcuts;
+
     classCountStatus = StatusItem();
     addStatusItem(classCountStatus);
 
     objectCountStatus = StatusItem();
     addStatusItem(objectCountStatus);
+
+    experimentStatus = StatusItem();
+    addStatusItem(experimentStatus);
   }
 
   final MemoryController memoryController = MemoryController();
 
   StatusItem classCountStatus;
+
   StatusItem objectCountStatus;
 
+  StatusItem experimentStatus;
+
   PButton pauseButton;
+
   PButton resumeButton;
 
+  // The autocomplete view manages the textfield and popup list.
+  CoreElement vmSearchField;
+  PopupListView<String> heapPopupList;
+  PopupAutoCompleteView heapAutoCompletePopup;
+
+  // Hover card shows where allocation occurred and references to instance.
+  final CoreElement hoverPopup = div(c: 'allocation-hover-card');
+
+  PButton vmMemorySearchButton;
   PButton vmMemorySnapshotButton;
+
   PButton resetAccumulatorsButton;
+
   PButton filterLibrariesButton;
+
   PButton gcNowButton;
 
-  ListQueue<Table<Object>> tableStack = ListQueue<Table<Object>>();
+  ListQueue<Table<dynamic>> tableStack = ListQueue<Table<dynamic>>();
+
   MemoryChart memoryChart;
+
   CoreElement tableContainer;
 
-  final DebuggerState _debuggerState;
+  InboundsTree _inboundTree;
+
+  // Memory navigation history. Driven from selecting items in the list of
+  // known classes, instances of a particular class and clicking on the class
+  // and field that allocated the instance (holds the reference).
+  // This list is displayed as a set of hyperlinks e.g.,
+  //
+  //     class1 (instance) > class2.extra > class3.mainHolder
+  //     -----------------   ------------   -----------------
+  //
+  // Clicking on one of the above links would select the class and instance that
+  // was associated with that hover navigation.  In this case:
+  //    [class3.mainHolder] - class3 called class2 constructor storing the
+  //                          reference to class2 in the field mainHolder.
+  //    [class2.extra]      - class2 called class1 constructor and stored the
+  //                          reference to class1 in field extra.
+  CoreElement history;
+
+  // This remembers how memory was navigated using the hover card to render the
+  // links in the history element (see above).
+  NavigationPath memoryPath = NavigationPath();
+
+  // Signals if navigation is happening as a result of clicking in a hover card.
+  // If true, keep recording the navigation instead of resetting history.
+  bool fromMemoryHover = false;
+
   MemoryDataView memoryDataView;
 
   MemoryTracker memoryTracker;
+
   ProgressElement progressElement;
 
+  // TODO(terry): Remove experiment after binary snapshot is added.
+  bool get isMemoryExperiment => _memoryExperiment;
+
+  bool _memoryExperiment = false;
+
+  // Handle shortcut keys
+  bool memoryShortcuts(bool ctrlKey, bool shiftKey, bool altKey, String key) {
+    if (ctrlKey && key == 'f') {
+      _search();
+      return true;
+    }
+    return false;
+  }
+
   @override
   void entering() {
     _updateListeningState();
   }
 
-  @override
-  void exiting() {
-    framework.clearMessages();
-  }
-
   void updateResumeButton({@required bool disabled}) {
     resumeButton.disabled = disabled;
   }
@@ -97,12 +161,31 @@
 
     pauseButton = PButton.icon('Pause', FlutterIcons.pause_black_2x)..small();
 
+    heapPopupList = PopupListView<String>();
+
+    vmSearchField = CoreElement('input', classes: 'search-text')
+      ..setAttribute('type', 'text')
+      ..setAttribute('placeholder', 'search')
+      ..id = 'popup_search_memory';
+    vmMemorySearchButton =
+        PButton.icon('', FlutterIcons.search, title: 'Memory Search')
+          ..small()
+          ..click(_search)
+          ..disabled = true;
     // TODO(terry): Need to correctly handle enabled and disabled.
     vmMemorySnapshotButton = PButton.icon('Snapshot', FlutterIcons.snapshot,
         title: 'Memory Snapshot')
       ..clazz('margin-left')
       ..small()
-      ..click(_loadAllocationProfile)
+      ..click(
+        _loadAllocationProfile,
+        () {
+          // TODO(terry): Disable when real binary snapshot is exposed.
+          // Shift key pressed while clicking on Snapshot button enables live
+          // memory inspection.
+          _loadAllocationProfile(memoryExperiment: true);
+        },
+      )
       ..disabled = true;
     resetAccumulatorsButton = PButton.icon(
         'Reset', FlutterIcons.resetAccumulators,
@@ -114,6 +197,12 @@
         PButton.icon('Filter', FlutterIcons.filter, title: 'Filter')
           ..small()
           ..disabled = true;
+    heapAutoCompletePopup = PopupAutoCompleteView(
+      heapPopupList,
+      screenDiv,
+      vmSearchField,
+      _callbackPopupSelectClass,
+    );
     gcNowButton =
         PButton.icon('GC', FlutterIcons.gcNow, title: 'Manual Garbage Collect')
           ..small()
@@ -138,6 +227,18 @@
       memoryChart.pause();
     });
 
+    // Handle keeping card active while mouse in the hover card.
+    hoverPopup.onMouseOver.listen((html.MouseEvent evt) {
+      _mouseInHover(evt);
+    });
+
+    // Handle hiding card once mouse is outside of the hover card.
+    hoverPopup.onMouseLeave.listen((html.MouseEvent evt) {
+      _mouseOutHover(evt);
+    });
+
+    history = div(c: 'history-navigation section', a: 'hidden');
+
     screenDiv.add(<CoreElement>[
       div(c: 'section')
         ..add(<CoreElement>[
@@ -145,14 +246,19 @@
             ..layoutHorizontal()
             ..clazz('align-items-center')
             ..add(<CoreElement>[
-              div(c: 'btn-group flex-no-wrap')
+              div(c: 'btn-group collapsible-885 flex-no-wrap')
                 ..add(<CoreElement>[
                   pauseButton,
                   resumeButton,
                 ]),
               div()..flex(),
-              div(c: 'btn-group collapsible-700 flex-no-wrap margin-left')
+              div(
+                  c: 'btn-group collapsible-785 nowrap margin-left '
+                      'memory-buttons')
+                ..flex()
                 ..add(<CoreElement>[
+                  vmSearchField,
+                  vmMemorySearchButton,
                   vmMemorySnapshotButton,
                   resetAccumulatorsButton,
                   filterLibrariesButton,
@@ -164,13 +270,16 @@
       tableContainer = div(c: 'section overflow-auto')
         ..layoutHorizontal()
         ..flex(),
+      history,
+      heapAutoCompletePopup,
+      hoverPopup, // Hover card
     ]);
 
     memoryController.onDisconnect.listen((__) {
-      serviceDisconnet();
+      serviceDisconnect();
     });
 
-    maybeShowDebugWarning(framework);
+    maybeAddDebugMessage(framework, memoryScreenId);
 
     _pushNextTable(null, _createHeapStatsTableView());
 
@@ -179,9 +288,270 @@
     return screenDiv;
   }
 
-  void _pushNextTable(Table<dynamic> current, Table<dynamic> next) {
+  ClassHeapDetailStats findClass(String className) {
+    final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
+    return classesData.firstWhere(
+      (stat) => stat.classRef.name == className,
+      orElse: () => null,
+    );
+  }
+
+  Future<List<InstanceSummary>> findInstances(ClassHeapDetailStats row) async {
+    try {
+      final List<InstanceSummary> instances =
+          await memoryController.getInstances(
+        row.classRef.id,
+        row.classRef.name,
+        row.instancesCurrent,
+      );
+
+      return instances;
+    } catch (e) {
+      // TODO(terry): Cleanup error.
+      print('findInstances ERROR: $e');
+      return [];
+    }
+  }
+
+  ClassHeapDetailStats findClassDetails(String classRefId) {
+    final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
+    return classesData.firstWhere(
+      (stat) => stat.classRef.id == classRefId,
+      orElse: () => null,
+    );
+  }
+
+  void _selectClass(String className, {bool record = true}) {
+    final List<ClassHeapDetailStats> classesData = tableStack.first.model.data;
+    int row = 0;
+    for (ClassHeapDetailStats stat in classesData) {
+      if (stat.classRef.name == className) {
+        tableStack.first.selectByIndex(row, scrollBehavior: 'auto');
+        if (record) {
+          memoryPath.add(NavigationState.classSelect(className));
+        }
+        return;
+      }
+      row++;
+    }
+
+    framework.toast('Unable to find class $className', title: 'Error');
+  }
+
+  Future<int> _selectInstanceInFieldHashCode(
+      String fieldName, int instanceHashCode) async {
+    final Table<Object> instanceTable = tableStack.elementAt(1);
+    final spinner = Spinner.centered();
+    instanceTable.element.add(spinner);
+
+    // There's an instances table up.
+    // TODO(terry): Need more efficient way to match ObjectRefs than hashCodes.
+    final List<InstanceSummary> instances = instanceTable.model.data;
+    int row = 0;
+    for (InstanceSummary instance in instances) {
+      // Check the field in each instance looking to find the object being held
+      // (the hashCode passed in matches the particular field's hashCode)
+
+      // TODO(terry): Enable below once expressions accessing private fields
+      // TODO(terry): e.g., _extra.hashCode works again.  Better yet code that
+      // TODO(terry): is more efficient that allows objectRef identity.
+      //
+      // final evalResult = await evaluate(instance.objectRef, '$fieldName.hashCode');
+      // int fieldHashCode =
+      //     evalResult != null ? int.parse(evalResult.valueAsString) : null;
+      //
+      // if (fieldHashCode == instanceHashCode) {
+      //   // Found the object select the instance.
+      //   instanceTable.selectByIndex(row, scrollBehavior: 'auto');
+      //   spinner.remove();
+      //   return row;
+      // }
+
+      // TODO(terry): Temporary workaround since evaluate fails on expressions
+      // TODO(terry): accessing a private field e.g., _extra.hashcode.
+      if (await memoryController.matchObject(
+          instance.objectRef, fieldName, instanceHashCode)) {
+        instanceTable.selectByIndex(row, scrollBehavior: 'auto');
+        spinner.remove();
+        return row;
+      }
+
+      row++;
+    }
+
+    spinner.remove();
+
+    framework.toast(
+      'Unable to find instance for field $fieldName [$hashCode]',
+      title: 'Error',
+    );
+
+    return -1;
+  }
+
+  void _resetHistory() {
+    history.hidden(true);
+    history.clear();
+    memoryPath = NavigationPath();
+  }
+
+  /// Finish callback from search class selected (auto-complete).
+  void _callbackPopupSelectClass([bool cancel]) {
+    if (cancel) {
+      heapAutoCompletePopup.matcher.reset();
+      heapPopupList.reset();
+    } else {
+      // Reset memory history selecting a class.
+      _resetHistory();
+
+      // Highlighted class is the class to select.
+      final String selectedClass = heapPopupList.highlightedItem;
+      if (selectedClass != null) _selectClass(selectedClass);
+    }
+
+    // Done with the popup.
+    heapAutoCompletePopup.hide();
+  }
+
+  void _selectInstanceByObjectRef(String objectRefToFind) {
+    removeInstanceView();
+
+    // There's an instances table up.
+    final Table<Object> instanceTable = tableStack.last;
+    final List<InboundsTreeNode> nodes = instanceTable.model.data;
+
+    final foundNode = nodes.firstWhere(
+      (node) => node.instance?.objectRef == objectRefToFind,
+      orElse: () => null,
+    );
+    if (foundNode != null) {
+      instanceTable.selectByIndex(
+        nodes.indexOf(foundNode),
+        scrollBehavior: 'auto',
+      );
+    }
+  }
+
+  Future<void> _selectInstanceByHashCode(int instanceHashCode) async {
+    // There's an instances table up.
+    final Table<Object> instanceTable = tableStack.last;
+    final List<InstanceSummary> instances = instanceTable.model.data;
+    int row = 0;
+    for (InstanceSummary instance in instances) {
+      // Check each instance looking to find a particular object.
+      // TODO(terry): Is there something faster for objectRef identity check?
+      final eval = await evaluate(instance.objectRef, 'hashCode');
+      final int evalHashCode = int.parse(eval?.valueAsString);
+
+      if (evalHashCode == instanceHashCode) {
+        // Found the object select the instance.
+        instanceTable.selectByIndex(row, scrollBehavior: 'auto');
+        return;
+      }
+
+      row++;
+    }
+
+    framework.toast('Unable to find instance [$instanceHashCode]',
+        title: 'Error');
+  }
+
+  bool get _isClassSelectedAndInstancesReady =>
+      tableStack.first.model.hasSelection &&
+      tableStack.length == 2 &&
+      tableStack.last.model.data.isNotEmpty;
+
+  void selectClassInstance(String className, int instanceHashCode) {
+    // Remove selection in class list.
+    tableStack.first.clearSelection();
+    // TODO(terry): Better solution is to await a Table event that tells us.
+    Timer.periodic(const Duration(milliseconds: 100), (Timer timer) {
+      if (!tableStack.first.model.hasSelection) {
+        // Wait until the class list has no selection.
+        timer.cancel();
+      }
+    });
+
+    // Select the class (don't record this select in memory history). The
+    // memoryPath will be added by NavigationState.inboundSelect - see below.
+    _selectClass(className, record: false);
+
+    // TODO(terry): Better solution is to await a Table event that tells us.
+    Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
+      // Wait until the class has been selected, 2 lists (class and instances
+      // for the class exist) and the instances list has data.
+      if (_isClassSelectedAndInstancesReady) {
+        timer.cancel();
+
+        await _selectInstanceByHashCode(instanceHashCode);
+      }
+    });
+  }
+
+  void selectClassAndInstanceInField(
+    String className,
+    String field,
+    int instanceHashCode,
+  ) async {
+    fromMemoryHover = true;
+
+    // Remove selection in class list.
+    tableStack.first.clearSelection();
+    // TODO(terry): Better solution is to await a Table event that tells us.
+    Timer.periodic(const Duration(milliseconds: 100), (Timer timer) {
+      if (!tableStack.first.model.hasSelection) {
+        // Wait until the class list has no selection.
+        timer.cancel();
+      }
+    });
+
+    // Select the class (don't record this select in memory history). The
+    // memoryPath will be added by NavigationState.inboundSelect - see below.
+    _selectClass(className, record: false);
+
+    // TODO(terry): Better solution is to await a Table event that tells us.
+    Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
+      // Wait until the class has been selected, 2 lists (class and instances
+      // for the class exist) and the instances list has data.
+      if (_isClassSelectedAndInstancesReady) {
+        timer.cancel();
+
+        final int rowToSelect =
+            await _selectInstanceInFieldHashCode(field, instanceHashCode);
+        if (rowToSelect != -1) {
+          // Found the instance that refs the object (hashCode passed). Mark the
+          // field name (fieldReference).  When the next instance memory path is
+          // added (in select) the field ill be stored in the NavigationState.
+          memoryPath.fieldReference = field;
+        }
+
+        // Wait for instance table, element 1, to have registered the selection.
+        // TODO(terry): Better solution is to await a Table event that tells us.
+        Timer.periodic(const Duration(milliseconds: 100), (Timer timer) async {
+          if (tableStack.length == 2 &&
+              tableStack.elementAt(1).model.hasSelection) {
+            timer.cancel();
+
+            // Done simulating all user UI actions as we navigate via hover thru
+            // classes, instances and fields.
+            fromMemoryHover = false;
+          }
+        });
+      }
+    });
+  }
+
+  void _pushNextTable(
+    Table<dynamic> current,
+    Table<dynamic> next, [
+    InboundsTree inboundTree,
+  ]) {
     // Remove any tables to the right of current from the DOM and the stack.
     while (tableStack.length > 1 && tableStack.last != current) {
+      // TODO(terry): Hacky need to manage tables better.
+      if (tableStack.length == 2) {
+        _inboundTree = null;
+      }
       tableStack.removeLast()
         ..element.element.remove()
         ..dispose();
@@ -190,10 +560,12 @@
     // Push the new table on to the stack and to the right of current.
     if (next != null) {
       final bool isFirst = tableStack.isEmpty;
-
       tableStack.addLast(next);
       tableContainer.add(next.element);
 
+      // TODO(terry): Hacky need to manage tables better.
+      if (inboundTree != null) _inboundTree = inboundTree;
+
       if (!isFirst) {
         next.element.clazz('margin-left');
       }
@@ -213,15 +585,14 @@
 
     resetAccumulatorsButton.disabled = true;
     tableStack.first.element.display = null;
-    final Spinner spinner =
-        tableStack.first.element.add(Spinner()..clazz('padded'));
+    final Spinner spinner = tableStack.first.element.add(Spinner.centered());
 
     try {
       final List<ClassHeapDetailStats> heapStats =
           await memoryController.resetAllocationProfile();
-      tableStack.first.setRows(heapStats);
+      tableStack.first.model.setRows(heapStats);
       _updateStatus(heapStats);
-      spinner.element.remove();
+      spinner.remove();
     } catch (e) {
       framework.toast('Reset failed ${e.toString()}', title: 'Error');
     } finally {
@@ -229,30 +600,74 @@
     }
   }
 
-  Future<void> _loadAllocationProfile({bool reset = false}) async {
+  final List<String> _knownSnapshotClasses = [];
+
+  List<String> getKnownSnapshotClasses() {
+    if (_knownSnapshotClasses.isEmpty) {
+      final List<ClassHeapDetailStats> classesData =
+          tableStack.first.model.data;
+      for (ClassHeapDetailStats stat in classesData) {
+        _knownSnapshotClasses.add(stat.classRef.name);
+      }
+    }
+
+    return _knownSnapshotClasses;
+  }
+
+  Future<void> _search() async {
+    ga.select(ga.memory, ga.search);
+
+    // Subsequent snapshots will reset heapPopupList to empty.
+    if (heapPopupList.isEmpty) {
+      // Only fetch once between snapshots.
+      heapPopupList.setList(getKnownSnapshotClasses());
+    }
+
+    if (!vmSearchField.isVisible) {
+      vmSearchField.element.style.visibility = 'visible';
+      vmSearchField.element.focus();
+      heapAutoCompletePopup.show();
+    } else {
+      heapAutoCompletePopup.matcher.finish(false); // Cancel popup auto-complete
+    }
+  }
+
+  Future<void> _loadAllocationProfile({
+    bool reset = false,
+    bool memoryExperiment = false,
+  }) async {
     ga.select(ga.memory, ga.snapshot);
 
+    _memoryExperiment = memoryExperiment;
+
     memoryChart.plotSnapshot();
 
+    // Empty the popup list - we'll repopulated from new snapshot.
+    heapPopupList.setList([]);
+
     vmMemorySnapshotButton.disabled = true;
     tableStack.first.element.display = null;
-    final Spinner spinner =
-        tableStack.first.element.add(Spinner()..clazz('padded'));
+    final Spinner spinner = tableStack.first.element.add(Spinner.centered());
 
     try {
       final List<ClassHeapDetailStats> heapStats =
           await memoryController.getAllocationProfile();
-      tableStack.first.setRows(heapStats);
+
+      // Reset known snapshot classes, just changed.
+      _knownSnapshotClasses.clear();
+
+      tableStack.first.model.setRows(heapStats);
       _updateStatus(heapStats);
-      spinner.element.remove();
+      spinner.remove();
     } catch (e) {
       framework.toast('Snapshot failed ${e.toString()}', title: 'Error');
     } finally {
       vmMemorySnapshotButton.disabled = false;
+      vmMemorySearchButton.disabled = false;
     }
   }
 
-  Future<Null> _gcNow() async {
+  Future<void> _gcNow() async {
     ga.select(ga.memory, ga.gC);
 
     gcNowButton.disabled = true;
@@ -286,7 +701,7 @@
   }
 
   // VM Service has stopped (disconnected).
-  void serviceDisconnet() {
+  void serviceDisconnect() {
     pauseButton.disabled = true;
     resumeButton.disabled = true;
 
@@ -298,97 +713,384 @@
     memoryChart.disabled = true;
   }
 
-  void _removeInstanceView() {
+  void removeInstanceView() {
     if (tableContainer.element.children.length == 3) {
       tableContainer.element.children.removeLast();
     }
   }
 
   Table<ClassHeapDetailStats> _createHeapStatsTableView() {
-    final Table<ClassHeapDetailStats> table =
-        Table<ClassHeapDetailStats>.virtual()
-          ..element.display = 'none'
-          ..element.clazz('memory-table');
+    final table = Table<ClassHeapDetailStats>.virtual()
+      ..element.display = 'none'
+      ..element.clazz('memory-table');
 
-    table.addColumn(MemoryColumnSize());
-    table.addColumn(MemoryColumnInstanceCount());
-    table.addColumn(MemoryColumnInstanceAccumulatedCount());
-    table.addColumn(MemoryColumnClassName());
+    table.model
+      ..addColumn(MemoryColumnSize())
+      ..addColumn(MemoryColumnInstanceCount())
+      ..addColumn(MemoryColumnInstanceAccumulatedCount())
+      ..addColumn(MemoryColumnClassName());
 
-    table.setSortColumn(table.columns.first);
+    table.model.sortColumn = table.model.columns.first;
 
-    table.onSelect.listen((ClassHeapDetailStats row) async {
+    table.model.onSelect.listen((ClassHeapDetailStats row) async {
       ga.select(ga.memory, ga.inspectClass);
-
       // User selected a new class from the list of classes so the instance view
       // which would be the third child needs to be removed.
-      _removeInstanceView();
+      removeInstanceView();
 
-      final Table<InstanceSummary> newTable =
-          row == null ? null : await _createInstanceListTableView(row);
-      _pushNextTable(table, newTable);
+      final InboundsTree inboundTree =
+          row == null ? null : await displayInboundReferences(row);
+      final TreeTable<InboundsTreeNode> tree = inboundTree.referencesTable;
+      _pushNextTable(table, tree, inboundTree);
     });
 
     return table;
   }
 
-  Future<Table<InstanceSummary>> _createInstanceListTableView(
+  Future<InboundsTree> displayInboundReferences(
       ClassHeapDetailStats row) async {
-    final Table<InstanceSummary> table = new Table<InstanceSummary>.virtual()
-      ..element.clazz('memory-table');
+    final treeData = InboundsTreeData()..data = InboundsTreeNode.root();
 
-    try {
-      final List<InstanceSummary> instanceRows =
-          await memoryController.getInstances(
-        row.classRef.id,
-        row.classRef.name,
-        row.instancesCurrent,
-      );
+    final List<InstanceSummary> instanceRows =
+        await memoryController.getInstances(
+      row.classRef.id,
+      row.classRef.name,
+      row.instancesCurrent,
+    );
 
-      table.addColumn(new MemoryColumnSimple<InstanceSummary>(
-          '${instanceRows.length} Instances of ${row.classRef.name}',
-          (InstanceSummary row) => row.objectRef));
-
-      table.setRows(instanceRows);
-    } catch (e) {
-      framework.toast(
-        'Problem fetching instances of ${row.classRef.name}: $e',
-        title: 'Error',
-      );
+    for (var instance in instanceRows) {
+      final instanceNode = InboundsTreeNode.instance(instance);
+      treeData.data.addChild(instanceNode);
+      // Place holder to lazily compute next child when parent node is expanded.
+      // Place holder to lazily compute next child when parent node is expanded.
+      instanceNode.addChild(InboundsTreeNode.empty());
     }
 
-    table.onSelect.listen((InstanceSummary row) async {
-      ga.select(ga.memory, ga.inspectInstance);
+    final inboundsTreeTable = InboundsTree(this, treeData, row.classRef.name);
+    return inboundsTreeTable..update();
+  }
 
-      // User selected a new instance from the list of class instances so the
-      // instance view which would be the third child needs to be removed.
-      _removeInstanceView();
+  Future<String> computeInboundReference(
+    String objectRef,
+    InboundsTreeNode instanceNode,
+  ) async {
+    final refs = await getInboundReferences(objectRef, 1000);
 
-      Instance instance;
-      try {
-        instance = await memoryController.getObject(row.objectRef);
-      } catch (e) {
-        instance = null; // Signal a problem
-      } finally {
-        tableContainer.add(_createInstanceView(
-          instance != null
-              ? row.objectRef
-              : 'Unable to fetch instance ${row.objectRef}',
-          row.className,
-        ));
+    String instanceHashCode;
+    if (isMemoryExperiment) {
+      // TODO(terry): Expensive need better VMService identity for objectRef.
+      // Get hashCode identity object id changes but hashCode is our identity.
+      final hashCodeResult = await evaluate(objectRef, 'hashCode');
+      instanceHashCode = hashCodeResult?.valueAsString;
+    }
 
-        tableContainer.element.scrollTo(<String, dynamic>{
-          'left': tableContainer.element.scrollWidth,
-          'top': 0,
-          'behavior': 'smooth',
-        });
+    final List<ClassHeapDetailStats> allClasses = tableStack.first.model.data;
 
-        // Allow inspection of the memory object.
-        memoryDataView.showFields(instance != null ? instance.fields : []);
+    computeInboundRefs(allClasses, refs, (
+      String referenceName,
+      String owningAllocator,
+      bool owningAllocatorIsAbstract,
+    ) async {
+      if (!owningAllocatorIsAbstract && owningAllocator.isNotEmpty) {
+        final inboundNode =
+            InboundsTreeNode(owningAllocator, referenceName, instanceHashCode);
+        instanceNode.addChild(inboundNode);
+        if (_memoryExperiment) inboundNode.addChild(InboundsTreeNode.empty());
       }
     });
 
-    return table;
+    return instanceHashCode;
+  }
+
+  Future<InstanceSummary> findLostObjectRef(
+    String classRef,
+    int instanceHashCode,
+  ) async {
+    final classDetails = findClassDetails(classRef);
+    if (classDetails != null) {
+      final List<InstanceSummary> instances =
+          await memoryController.getInstances(
+        classDetails.classRef.id,
+        classDetails.classRef.name,
+        classDetails.instancesCurrent,
+      );
+      for (var instance in instances) {
+        final InstanceRef eval = await evaluate(instance.objectRef, 'hashCode');
+        final int evalResult = int.parse(eval?.valueAsString);
+        if (evalResult == instanceHashCode) {
+          // Found the instance.
+          return instance;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  Future<Instance> getInstance(String objectRef) async {
+    Instance instance;
+    try {
+      final dynamic theObject = await memoryController.getObject(objectRef);
+      if (theObject is Instance) {
+        instance = theObject;
+      } else if (theObject is Sentinel) {
+        instance = null;
+        // TODO(terry): Tracking Sentinel's to be removed.
+        framework.toast('Sentinel $objectRef', title: 'Warning');
+      }
+    } catch (e) {
+      // Log this problem not sure how it can really happen.
+      ga.error('Memory select (getInstance): $e', false);
+
+      instance = null; // Signal a problem
+    }
+
+    return instance;
+  }
+
+  void updateInstancesTree() {
+    _inboundTree.update();
+  }
+
+  void select(InboundsTreeNode rowNode) async {
+    ga.select(ga.memory, ga.inspectInstance);
+
+    // User selected a new instance from the list of class instances so the
+    // instance view which would be the third child needs to be removed.
+    removeInstanceView();
+
+    if (rowNode?.instance == null) return;
+
+    Instance instance = await getInstance(rowNode.instance.objectRef);
+    if (instance == null) {
+      // TODO(terry): Eliminate for eval
+      // Eval objectRef ids have changed re-fetch objectRef ids.
+      final newInstance = await findLostObjectRef(
+        rowNode.instance.classRef,
+        int.parse(rowNode.instanceHashCode),
+      );
+
+      framework.toast(
+        'Re-computed ${rowNode.instance.objectRef} -> ${newInstance.objectRef}',
+        title: 'Message',
+      );
+
+      // Update to the new objectRef id.
+      rowNode.setInstance(newInstance, rowNode.instanceHashCode, true);
+
+      instance = await getInstance(rowNode.instance.objectRef);
+
+      _inboundTree.update();
+
+      // Re-computing could cause instance in TableTree to move (change row).
+      // Find it and select it again.
+      _selectInstanceByObjectRef(rowNode.instance.objectRef);
+    }
+
+    tableContainer.add(_createInstanceView(
+      instance != null
+          ? rowNode.instance.objectRef
+          : 'Unable to fetch instance ${rowNode.name}',
+      rowNode.instance.className,
+    ));
+
+    tableContainer.element.scrollTo(<String, dynamic>{
+      'left': tableContainer.element.scrollWidth,
+      'top': 0,
+      'behavior': 'smooth',
+    });
+
+    // Allow inspection of the memory object.
+    memoryDataView.showFields(instance != null ? instance.fields : []);
+  }
+
+  // TD element used to simulate hover state when hover card is visible. When
+  // not null the mouse is actively in the hover card.
+  CoreElement _tdCellHover;
+
+  // InstanceSummary of the visible hover card.
+  HoverCell<InstanceSummary> _currentHoverSummary;
+
+  // This is the listener for the hover card (hoverPopup's) onMouseOver, it's
+  // designed to keep the hover state (background-color for the TD same as the
+  // CSS :hover) as the mouse slides to the hover card. It gives the appearance
+  // that hover is still active in the TD.
+  void _mouseInHover(html.MouseEvent evt) {
+    final CoreElement cell = _currentHoverSummary?.cell;
+
+    if (cell != null) _tdCellHover = cell;
+
+    // Simulate the :hover when the mouse in hover card.
+    _tdCellHover?.clazz('allocation-hover', removeOthers: true);
+    _tdCellHover?.clazz('left');
+  }
+
+  // This is the listener for the hover card (hoverPopup's) onMouseLeave, it's
+  // designed to end the hover state (background-color for the TD same as the
+  // CSS :hover) as the mouse slides out of the hover card.  It gives the
+  // appearance that the hover is not active.
+  void _mouseOutHover(html.MouseEvent evt) {
+    // Done simulating hover, hover card is closing.  Reset to CSS handling the
+    // :hover for the allocation class.
+    _tdCellHover?.clazz('allocation', removeOthers: true);
+    _tdCellHover?.clazz('left');
+
+    if (_tdCellHover != null) _tdCellHover = null;
+
+    _currentHoverSummary = null;
+
+    // We're really leaving hover so close it.
+    hoverPopup.clear(); // Remove all children.
+    hoverPopup.display = 'none';
+  }
+
+  void _closeHover(HoverCell<InstanceSummary> newCurrent) {
+    // We're really leaving hover so close it.
+    hoverPopup.clear(); // Remove all children.
+    hoverPopup.display = 'none';
+
+    _currentHoverSummary = newCurrent;
+  }
+
+  void _maybeCloseHover() {
+    final String hoverToClose = _currentHoverSummary?.data?.objectRef;
+    Timer(const Duration(milliseconds: 50), () {
+      if (_tdCellHover == null &&
+          hoverToClose == _currentHoverSummary?.data?.objectRef) {
+        // We're really leaving hover so close it.
+        _closeHover(null);
+      }
+    });
+  }
+
+  static const String dataHashCode = 'data-hashcode';
+  static const String dataOwningClass = 'data-owning-class';
+  static const String dataRef = 'data-ref';
+
+  void hoverInstanceAllocations(HoverCellData<InstanceSummary> data) async {
+    final HoverCell<InstanceSummary> hover = data;
+    if (hover.cell == null) {
+      // Hover out of the cell.
+      _maybeCloseHover();
+      return;
+    }
+
+    // Hover in the cell.
+    if (hover.data != _currentHoverSummary?.data) {
+      // Selecting a different instance then what's current.
+      _closeHover(hover);
+    }
+
+    // Entering Hover again?
+    if (hoverPopup.element.children.isNotEmpty) return;
+
+    final CoreElement ulElem = ul();
+    final refs = await getInboundReferences(hover.data.objectRef, 1000);
+
+    if (refs == null) {
+      framework.toast(
+        'Instance ${hover.data.objectRef} - Sentinel/Expired.',
+      );
+      return;
+    }
+
+    ulElem.add(li(c: 'allocation-li-title')
+      ..add([
+        span(text: 'Allocated', c: 'allocated-by-class-title'),
+        span(text: 'Referenced', c: 'ref-by-title')
+      ]));
+
+    final List<ClassHeapDetailStats> allClasses = tableStack.first.model.data;
+
+    computeInboundRefs(
+      allClasses,
+      refs,
+      (
+        String referenceName,
+        String owningAllocator,
+        bool owningAllocatorIsAbstract,
+      ) async {
+        // Callback function to build each item in the hover card.
+        final classAllocation = owningAllocatorIsAbstract
+            ? 'allocation-abstract allocated-by-class'
+            : 'allocated-by-class';
+
+        final fieldAllocation =
+            owningAllocatorIsAbstract ? 'allocation-abstract ref-by' : 'ref-by';
+
+        final CoreElement liElem = li(c: 'allocation-li')
+          ..add([
+            span(
+              text: 'class $owningAllocator',
+              c: classAllocation,
+            ),
+            span(
+              text: 'field $referenceName',
+              c: fieldAllocation,
+            ),
+          ]);
+        if (owningAllocatorIsAbstract) {
+          // Mark as grayed/italic
+          liElem.clazz('li-allocation-abstract');
+        }
+        if (!owningAllocatorIsAbstract && owningAllocator.isNotEmpty) {
+          // TODO(terry): Expensive need better VMService identity for objectRef.
+          // Get hashCode identity object id changes but hashCode is our identity.
+          final hashCodeResult =
+              await evaluate(hover.data.objectRef, 'hashCode');
+
+          liElem.setAttribute(dataHashCode, hashCodeResult?.valueAsString);
+          liElem.setAttribute(dataOwningClass, owningAllocator);
+          liElem.setAttribute(dataRef, referenceName);
+        }
+        liElem.onClick.listen((evt) {
+          final html.Element e = evt.currentTarget;
+
+          String className = e.getAttribute(dataOwningClass);
+          if (className == null || className.isEmpty) {
+            className = e.parent.getAttribute(dataOwningClass);
+          }
+          String refName = e.getAttribute(dataRef);
+          if (refName == null || refName.isEmpty) {
+            refName = e.parent.getAttribute(dataRef);
+          }
+          String objectHashCode = e.getAttribute(dataHashCode);
+          if (objectHashCode == null || objectHashCode.isEmpty) {
+            objectHashCode = e.parent.getAttribute(dataHashCode);
+          }
+          final int instanceHashCode = int.parse(objectHashCode);
+
+          // Done with the hover - close it down.
+          _closeHover(null);
+
+          // Make sure its a known class (not abstract).
+          if (className.isNotEmpty &&
+              refName.isNotEmpty &&
+              instanceHashCode != null) {
+            // Display just the instances of classes with ref
+            selectClassAndInstanceInField(className, refName, instanceHashCode);
+          }
+        });
+        ulElem.add(liElem);
+      },
+    );
+
+    if (hover.cell != null && hover.cell.hasClass('allocation')) {
+      // Hover over
+      final int top = hover.cell.top + 10;
+      final int left = hover.cell.left + 21;
+
+      hoverPopup.clear(); // TODO(terry): Workaround multiple ULs?
+
+      hoverPopup.add(ulElem);
+
+      // Display the popup.
+      hoverPopup
+        ..display = 'block'
+        ..element.style.top = '${top}px'
+        ..element.style.left = '${left}px'
+        ..element.style.height = '';
+    }
   }
 
   CoreElement _createInstanceView(String objectRef, String className) {
@@ -408,29 +1110,17 @@
         return value.name;
       }
 
-      final Instance ref = value;
+      final InstanceRef ref = value;
 
-      if (ref.valueAsString != null && !ref.valueAsStringIsTruncated) {
+      if (ref?.valueAsString != null && !ref.valueAsStringIsTruncated) {
         return ref.valueAsString;
       } else {
-        final dynamic result = await serviceManager.service.invoke(
-          _debuggerState.isolateRef.id,
-          ref.id,
-          'toString',
-          <String>[],
-          disableBreakpoints: true,
-        );
-
-        if (result is ErrorRef) {
-          return '${result.kind} ${result.message}';
-        } else if (result is InstanceRef) {
-          final String str = await _retrieveFullStringValue(result);
-          return str;
-        } else {
-          // TODO: Improve the return value for this case.
-          return null;
-        }
+        // Shouldn't happen but want to check - log to analytics.
+        ga.error(
+            'Memory _createInstanceView: UNKNOWN BoundField $objectRef', false);
       }
+
+      return null;
     };
 
     memoryDataView = MemoryDataView(memoryController, describer);
@@ -447,26 +1137,6 @@
       ]);
   }
 
-  // TODO(terry): Move to common file shared by debugger and memory.
-  Future<String> _retrieveFullStringValue(InstanceRef stringRef) async {
-    if (stringRef.valueAsStringIsTruncated != true) {
-      return stringRef.valueAsString;
-    }
-
-    final dynamic result = await serviceManager.service.getObject(
-      _debuggerState.isolateRef.id,
-      stringRef.id,
-      offset: 0,
-      count: stringRef.length,
-    );
-    if (result is Instance) {
-      final Instance obj = result;
-      return obj.valueAsString;
-    } else {
-      return '${stringRef.valueAsString}...';
-    }
-  }
-
   void _updateStatus(List<ClassHeapDetailStats> data) {
     if (data == null) {
       classCountStatus.element.text = '';
@@ -479,5 +1149,168 @@
       }
       objectCountStatus.element.text = '${nf.format(objectCount)} objects';
     }
+    experimentStatus.element.text =
+        isMemoryExperiment ? 'Experiment' : 'Memory';
+  }
+}
+
+/// Path consists of:
+///    Class selected (from Class list):
+///      _className
+///      _hashCode = empty
+///      field = empty
+///
+///   Instance selected (from Instance list):
+///      _className
+///      _hashCode [hashCode of instance]
+///      field = empty
+///
+///   Hover (from inboundReferences) parent allocations:
+///      _className [class name of parent class that allocated object]
+///      _hashCode [hashCode of instance]
+///      field [field of parent class that has ref]
+class NavigationState {
+  NavigationState._() : _className = '';
+
+  NavigationState.classSelect(this._className);
+
+  NavigationState.instanceSelect(this._className, this._hashCode);
+
+  // data attribute names.
+  static const String dataIndex = 'data-index';
+  static const String dataClass = 'data-class';
+  static const String dataField = 'data-field';
+  static const String dataHashCode = 'data-hashcode';
+
+  String field = '';
+
+  String get className => _className;
+  final String _className;
+
+  int get instanceHashCode => _hashCode;
+  int _hashCode;
+
+  bool get isClass =>
+      _className.isNotEmpty && field.isEmpty && _hashCode == null;
+
+  bool get isInstance =>
+      _className.isNotEmpty && field.isEmpty && _hashCode != null;
+
+  bool get isInbound =>
+      _className.isNotEmpty && field.isNotEmpty && _hashCode != null;
+
+  // Create a span with all information to navigate through the class list and
+  // instance list. The span element will look like:
+  //
+  //    <span class=N data-index=# data-class=N data-field=N data-hashcode=N>
+  //      class[.field]
+  //    </span>
+  //
+  // where:
+  //    class=N is the css class for styling
+  //    index=# is the index of this Navigation link in the NavigationPath list
+  //    data-class=N is the class selected in the memory class list
+  //    data-field=N if specified, references previous history hashcode (object)
+  //    data-hashcode=N if specified, object referenced in this data-class field
+  CoreElement link(int index, [bool last = false]) {
+    final String spanText = field.isNotEmpty
+        ? '$className.$field'
+        : isInstance ? '$className (instance)' : className;
+
+    final CoreElement spanElem =
+        span(text: spanText, c: last ? 'history-link-last' : 'history-link');
+
+    spanElem.setAttribute(dataIndex, '$index');
+    spanElem.setAttribute(dataClass, className);
+    if (field.isNotEmpty) spanElem.setAttribute(dataField, field);
+    if (instanceHashCode != null) {
+      spanElem.setAttribute(dataHashCode, instanceHashCode.toString());
+    }
+
+    return spanElem;
+  }
+
+  CoreElement get separator => span(text: '>', c: 'history-separator');
+}
+
+// Used to manage all memory navigation from user clicks or hover card
+// navigation so user can visually understand the relationship of the current
+// memory object being displayed.
+class NavigationPath {
+  final List<NavigationState> _path = [];
+
+  // Global field name next add if state object isInstance then store the field
+  // name in the state.
+  String _inboundFieldName = '';
+
+  set fieldReference(String field) => _inboundFieldName = field;
+
+  bool get isEmpty => _path.isEmpty;
+
+  bool get isNotEmpty => _path.isNotEmpty;
+
+  void add(NavigationState state) {
+    if (state.isInbound) {
+      throw Exception('Inbound use not valid here.');
+    }
+
+    // If adding a state and the global inbound is set, then record this field
+    // with the state.
+    if (state.isInstance && _inboundFieldName.isNotEmpty) {
+      state.field = _inboundFieldName;
+    }
+
+    _inboundFieldName = '';
+
+    if (_path.isNotEmpty) {
+      final lastState = _path.last;
+      // if last state in path and same state we're to push, ignore - class
+      // being set by a click in history navigation.
+      if (lastState.isClass &&
+          state.isClass &&
+          lastState.className == state.className) return;
+    }
+
+    _path.add(state);
+  }
+
+  NavigationState get(int index) => _path[index];
+
+  void remove(NavigationState stateToRemove) {
+    for (int row = 0; row < _path.length; row++) {
+      final NavigationState state = _path[row];
+      if (stateToRemove == state) {
+        assert(state.instanceHashCode == stateToRemove.instanceHashCode &&
+            state.className == stateToRemove.className &&
+            state.field == stateToRemove.field);
+        _path.removeRange(row, _path.length);
+        return;
+      }
+    }
+  }
+
+  /// Is the last item in the path an inBound NavigationState.
+  bool get isLastInBound => _path.isNotEmpty ? _path.last.isInbound : false;
+
+  bool get isLastInstance => _path.isNotEmpty ? _path.last.isInstance : false;
+
+  // Display all the NavigationStates in our _path as UI links.
+  void displayPathsAsLinks(
+    CoreElement parent, {
+    void Function(CoreElement) clickHandler,
+  }) {
+    for (int index = 0; index < _path.length; index++) {
+      final NavigationState state = _path[index];
+      final bool lastLink = _path.length - 1 == index; // Last item in path?
+      final CoreElement link = state.link(index, lastLink);
+      if (clickHandler != null) {
+        link.click(() {
+          final CoreElement element = link;
+          clickHandler(element);
+        });
+      }
+      parent.add(link);
+      if (!lastLink) parent.add(state.separator);
+    }
   }
 }
diff --git a/devtools/lib/src/memory/memory_chart.dart b/devtools/lib/src/memory/memory_chart.dart
index dbdc439..797c8a6 100644
--- a/devtools/lib/src/memory/memory_chart.dart
+++ b/devtools/lib/src/memory/memory_chart.dart
@@ -18,8 +18,7 @@
     element.id = _memoryGraph;
     element.style
       ..boxSizing = 'content-box' // border-box causes right/left border cut.
-      ..height = '${FramesBarChart.chartHeight}px'
-      ..width = '100%';
+      ..height = '${FramesBarChart.chartHeight}px';
 
     _memoryController.onMemory.listen((MemoryTracker memoryTracker) {
       if (_memoryController.memoryTracker.hasConnection) {
diff --git a/devtools/lib/src/memory/memory_controller.dart b/devtools/lib/src/memory/memory_controller.dart
index b86b9ba..db3506b 100644
--- a/devtools/lib/src/memory/memory_controller.dart
+++ b/devtools/lib/src/memory/memory_controller.dart
@@ -3,11 +3,12 @@
 // found in the LICENSE file.
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../globals.dart';
 import '../vm_service_wrapper.dart';
 import 'memory_protocol.dart';
+import 'memory_service.dart';
 
 /// This class contains the business logic for [memory.dart].
 ///
@@ -21,13 +22,14 @@
 
   final StreamController<MemoryTracker> _memoryTrackerController =
       StreamController<MemoryTracker>.broadcast();
+
   Stream<MemoryTracker> get onMemory => _memoryTrackerController.stream;
 
-  final StreamController<void> _disconnectController =
-      StreamController<void>.broadcast();
   Stream<void> get onDisconnect => _disconnectController.stream;
+  final _disconnectController = StreamController<void>.broadcast();
 
   MemoryTracker _memoryTracker;
+
   MemoryTracker get memoryTracker => _memoryTracker;
 
   bool get hasStarted => _memoryTracker != null;
@@ -42,7 +44,7 @@
     _memoryTracker = MemoryTracker(service);
     _memoryTracker.start();
 
-    _memoryTracker.onChange.listen((Null _) {
+    _memoryTracker.onChange.listen((_) {
       _memoryTrackerController.add(_memoryTracker);
     });
   }
@@ -51,7 +53,7 @@
     _memoryTracker?.stop();
     _memoryTrackerController.add(_memoryTracker);
 
-    _disconnectController.add(Null);
+    _disconnectController.add(null);
     hasStopped = true;
   }
 
@@ -73,65 +75,76 @@
   // 'reset': true to reset the object allocation accumulators
   Future<List<ClassHeapDetailStats>> getAllocationProfile(
       {bool reset = false}) async {
-    final Map resetArg = reset ? {'reset': 'true'} : {};
-
-    final Response response = await serviceManager.service.callMethod(
-      '_getAllocationProfile',
-      isolateId: _isolateId,
-      args: resetArg,
+    final AllocationProfile allocationProfile =
+        await serviceManager.service.getAllocationProfile(
+      _isolateId,
+      reset: reset,
     );
-
-    final List<dynamic> members = response.json['members'];
-
-    final List<ClassHeapDetailStats> heapStats = members
-        .cast<Map<String, dynamic>>()
-        .map((Map<String, dynamic> d) => ClassHeapDetailStats(d))
+    return allocationProfile.members
+        .map((ClassHeapStats stats) => ClassHeapDetailStats(stats.json))
         .where((ClassHeapDetailStats stats) {
       return stats.instancesCurrent > 0 || stats.instancesAccumulated > 0;
     }).toList();
-
-    return heapStats;
   }
 
   Future<List<InstanceSummary>> getInstances(
       String classRef, String className, int maxInstances) async {
-    final List<InstanceSummary> result = [];
-
     // TODO(terry): Expose as a stream to reduce stall when querying for 1000s
     // TODO(terry): of instances.
-    final Map params = {
-      'classId': classRef,
-      'limit': maxInstances,
-    };
-    final Response response = await serviceManager.service.callMethod(
-      '_getInstances',
-      isolateId: _isolateId,
-      args: params,
+    final InstanceSet instanceSet = await serviceManager.service.getInstances(
+      _isolateId,
+      classRef,
+      maxInstances,
+      classId: classRef,
     );
 
-    final List instances = response.json['samples'];
-
-    for (Map instance in instances) {
-      final String objectRef = instance['id'];
-      result.add(InstanceSummary(classRef, className, objectRef));
-    }
-
-    return result;
+    return instanceSet.instances
+        .map((ObjRef ref) => InstanceSummary(classRef, className, ref.id))
+        .toList();
   }
 
-  Future<Instance> getObject(String objectRef) async =>
+  Future<dynamic> getObject(String objectRef) async =>
       await serviceManager.service.getObject(
         _isolateId,
         objectRef,
       );
 
   Future<void> gc() async {
-    await serviceManager.service.callMethod(
-      '_getAllocationProfile',
-      isolateId: _isolateId,
-      args: {
-        'gc': 'full',
-      },
+    await serviceManager.service.getAllocationProfile(
+      _isolateId,
+      gc: true,
     );
   }
+
+  // Temporary hack to allow accessing private fields(e.g., _extra) using eval
+  // of '_extra.hashCode' to fetch the hashCode of the object of that field.
+  // Used to find the object which allocated/references the object being viewed.
+  Future<bool> matchObject(
+      String objectRef, String fieldName, int instanceHashCode) async {
+    final dynamic object = await getObject(objectRef);
+    if (object is Instance) {
+      final Instance instance = object;
+      final List<BoundField> fields = instance.fields;
+      for (var field in fields) {
+        if (field.decl.name == fieldName) {
+          final InstanceRef ref = field.value;
+
+          if (ref == null) continue;
+
+          final evalResult = await evaluate(ref.id, 'hashCode');
+          final int objHashCode = int.parse(evalResult?.valueAsString);
+          if (objHashCode == instanceHashCode) {
+            return true;
+          }
+        }
+      }
+    }
+
+    if (object is Sentinel) {
+      // TODO(terry): Need more graceful handling of sentinels.
+      print('Trying to matchObject with a Sentinel $objectRef');
+    }
+
+    return false;
+  }
 }
diff --git a/devtools/lib/src/memory/memory_data_view.dart b/devtools/lib/src/memory/memory_data_view.dart
index d6679f1..89b19e9 100644
--- a/devtools/lib/src/memory/memory_data_view.dart
+++ b/devtools/lib/src/memory/memory_data_view.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../memory/memory_controller.dart';
 import '../ui/custom.dart';
@@ -85,7 +85,7 @@
     });
   }
 
-  MemoryController _memoryController;
+  final MemoryController _memoryController;
   SelectableTree<BoundField> _items;
 
   List<BoundField> get items => _items.items;
@@ -117,39 +117,44 @@
     final BoundField field = item;
 
     if (field.value != null && field.value is InstanceRef) {
-      final Instance instance =
-          await _memoryController.getObject(field.value.id);
-      switch (field.value.kind) {
-        case InstanceKind.kPlainInstance:
-          return instance.fields;
-        case InstanceKind.kList:
-          final List<BoundField> result = [];
+      final dynamic object = await _memoryController.getObject(field.value.id);
+      if (object is Instance) {
+        final Instance instance = object;
+        switch (field.value.kind) {
+          case InstanceKind.kPlainInstance:
+            return instance.fields;
+          case InstanceKind.kList:
+            final List<BoundField> result = [];
 
-          int index = 0;
-          for (dynamic value in instance.elements) {
-            result.add(BoundField()
-              ..decl = (FieldRef()..name = '[$index]')
-              ..value = value);
-            index++;
-          }
-          return result;
-        case InstanceKind.kMap:
-          final List<BoundField> result = [];
-          for (dynamic value in instance.associations) {
-            result.add(BoundField()
-              // TODO(terry): Need to handle nested objects for keys/values.
-              ..decl = (FieldRef()..name = '[\'${value.key.valueAsString}\']')
-              ..value = value.value.valueAsString);
-          }
-          return result;
-        case InstanceKind.kStackTrace:
-          // TODO(terry): Handle StackTrace type.
-          break;
-        case InstanceKind.kClosure:
-          // TODO(terry): Handle Closure type.
-          break;
-        // TODO(terry): Do we need to handle WeakProperty, Type, TypeParameter,
-        // TODO(terry): TypeDef or BoundedType?
+            int index = 0;
+            for (dynamic value in instance.elements) {
+              result.add(BoundField()
+                ..decl = (FieldRef()..name = '[$index]')
+                ..value = value);
+              index++;
+            }
+            return result;
+          case InstanceKind.kMap:
+            final List<BoundField> result = [];
+            for (dynamic value in instance.associations) {
+              result.add(BoundField()
+                // TODO(terry): Need to handle nested objects for keys/values.
+                ..decl = (FieldRef()..name = '[\'${value.key.valueAsString}\']')
+                ..value = value.value.valueAsString);
+            }
+            return result;
+          case InstanceKind.kStackTrace:
+            // TODO(terry): Handle StackTrace type.
+            break;
+          case InstanceKind.kClosure:
+            // TODO(terry): Handle Closure type.
+            break;
+          // TODO(terry): Do we need to handle WeakProperty, Type, TypeParameter,
+          // TODO(terry): TypeDef or BoundedType?
+        }
+      } else if (object is Sentinel) {
+        // TODO(terry): Need to handle more gracefully.
+        print('ERROR: Sentinel encountered ${field.value.id}.');
       }
     }
 
diff --git a/devtools/lib/src/memory/memory_detail.dart b/devtools/lib/src/memory/memory_detail.dart
index 5abfd59..62eedd2 100644
--- a/devtools/lib/src/memory/memory_detail.dart
+++ b/devtools/lib/src/memory/memory_detail.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import '../tables.dart';
+import '../table_data.dart';
 import 'memory_protocol.dart';
 
 class MemoryRow {
@@ -16,14 +16,14 @@
   String toString() => name;
 }
 
-class MemoryColumnClassName extends Column<ClassHeapDetailStats> {
-  MemoryColumnClassName() : super('Class', wide: true);
+class MemoryColumnClassName extends ColumnData<ClassHeapDetailStats> {
+  MemoryColumnClassName() : super.wide('Class');
 
   @override
-  dynamic getValue(ClassHeapDetailStats item) => item.classRef.name;
+  dynamic getValue(ClassHeapDetailStats dataObject) => dataObject.classRef.name;
 }
 
-class MemoryColumnSize extends Column<ClassHeapDetailStats> {
+class MemoryColumnSize extends ColumnData<ClassHeapDetailStats> {
   MemoryColumnSize() : super('Size');
 
   @override
@@ -32,53 +32,64 @@
   //String get cssClass => 'monospace';
 
   @override
-  dynamic getValue(ClassHeapDetailStats item) => item.bytesCurrent;
+  dynamic getValue(ClassHeapDetailStats dataObject) => dataObject.bytesCurrent;
 
   @override
   String render(dynamic value) {
     if (value < 1024) {
-      return ' ${Column.fastIntl(value)}';
+      return ' ${ColumnData.fastIntl(value)}';
     } else {
-      return ' ${Column.fastIntl(value ~/ 1024)}k';
+      return ' ${ColumnData.fastIntl(value ~/ 1024)}k';
     }
   }
 }
 
-class MemoryColumnInstanceCount extends Column<ClassHeapDetailStats> {
+class MemoryColumnInstanceCount extends ColumnData<ClassHeapDetailStats> {
   MemoryColumnInstanceCount() : super('Count');
 
   @override
   bool get numeric => true;
 
   @override
-  dynamic getValue(ClassHeapDetailStats item) => item.instancesCurrent;
+  dynamic getValue(ClassHeapDetailStats dataObject) =>
+      dataObject.instancesCurrent;
 
   @override
-  String render(dynamic value) => Column.fastIntl(value);
+  String render(dynamic value) => ColumnData.fastIntl(value);
 }
 
 class MemoryColumnInstanceAccumulatedCount
-    extends Column<ClassHeapDetailStats> {
+    extends ColumnData<ClassHeapDetailStats> {
   MemoryColumnInstanceAccumulatedCount() : super('Accumulator');
 
   @override
   bool get numeric => true;
 
   @override
-  dynamic getValue(ClassHeapDetailStats item) => item.instancesAccumulated;
+  dynamic getValue(ClassHeapDetailStats dataObject) =>
+      dataObject.instancesAccumulated;
 
   @override
-  String render(dynamic value) => Column.fastIntl(value);
+  String render(dynamic value) => ColumnData.fastIntl(value);
 }
 
-class MemoryColumnSimple<T> extends Column<T> {
-  MemoryColumnSimple(String name, this.getter, {bool wide = false})
-      : super(name, wide: wide);
+class MemoryColumnSimple<T> extends ColumnData<T> {
+  MemoryColumnSimple(String name, this.getter,
+      {bool wide = false,
+      bool usesHtml = false,
+      bool hover = false,
+      String cssClass})
+      : super(
+          name,
+          usesHtml: usesHtml,
+          cssClass: cssClass,
+          hover: hover,
+        );
 
   String Function(T) getter;
 
   @override
-  String getValue(T item) => getter(item);
+  String getValue(T dataObject) => getter(dataObject);
 }
 
 //  void _loadHeapSnapshot() {
diff --git a/devtools/lib/src/memory/memory_inbounds.dart b/devtools/lib/src/memory/memory_inbounds.dart
new file mode 100644
index 0000000..4221655
--- /dev/null
+++ b/devtools/lib/src/memory/memory_inbounds.dart
@@ -0,0 +1,391 @@
+// Copyright 2019 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 'dart:async';
+
+import 'package:vm_service/vm_service.dart';
+
+import '../table_data.dart';
+import '../tables.dart';
+import '../trees.dart';
+import '../ui/custom.dart';
+import '../ui/elements.dart';
+
+import 'memory.dart';
+import 'memory_protocol.dart';
+import 'memory_service.dart';
+
+class InboundsTree extends InstanceRefsView {
+  InboundsTree(
+    this._memoryScreen,
+    InboundsTreeData inboundsTree,
+    String className,
+  ) : super(inboundsTree) {
+    flex();
+    layoutVertical();
+
+    _init(className);
+  }
+
+  final MemoryScreen _memoryScreen;
+
+  TreeTable<InboundsTreeNode> referencesTable;
+
+  Spinner spinner;
+
+  void _init(String className) {
+    final title =
+        '${inboundsTree.data.children.length} Instances of $className';
+
+    final classNameColumn = ClassNameColumn(title)
+      ..onNodeExpanded.listen((inboundNode) async {
+        // TODO(terry): Fix need to support simultaneous expansions.
+        if (spinner != null) return;
+
+        if (inboundNode.children.length == 1 &&
+            inboundNode.children[0].isEmpty) {
+          inboundNode.children.removeLast();
+          // Make sure it's a known class (not abstract).
+          if (!inboundNode.isEmpty) {
+            spinner = Spinner.centered();
+            referencesTable.element.add(spinner);
+
+            if (inboundNode.instanceHashCode == null &&
+                inboundNode.instance != null) {
+              // Need the hashCode.  It's slow - do it when needed.
+              // TODO(terry): Needs to be used with real snapshot.
+              final String instanceHashCode =
+                  await _memoryScreen.computeInboundReference(
+                inboundNode.instance.objectRef,
+                inboundNode,
+              );
+              inboundNode.instanceHashCode = instanceHashCode;
+            }
+
+            final instanceHashCode = _memoryScreen.isMemoryExperiment
+                ? int.parse(inboundNode.instanceHashCode)
+                : -1;
+
+            final ClassHeapDetailStats classStats =
+                _memoryScreen.findClass(inboundNode.name);
+
+            if (_memoryScreen.isMemoryExperiment) {
+              // All instances of a class.
+              final List<InstanceSummary> instances =
+                  await _memoryScreen.findInstances(classStats);
+              int instanceIndex = 1;
+              for (InstanceSummary instance in instances) {
+                // Give feedback on what is happening node name appended with
+                // ' (N of NNN)' instances of total instances being processed.
+                inboundNode.working(instanceIndex++, instances.length);
+                _memoryScreen.updateInstancesTree();
+
+                // Found the instance.
+                final refs =
+                    await getInboundReferences(instance.objectRef, 1000);
+
+                // TODO(terry): Temporary workaround since evaluate fails on expressions
+                // TODO(terry): accessing a private field e.g., _extra.hashcode.
+                if (await _memoryScreen.memoryController.matchObject(
+                  instance.objectRef,
+                  inboundNode.fieldName,
+                  instanceHashCode,
+                )) {
+                  // TODO(terry): Expensive need better VMService identity for objectRef.
+                  // Get hashCode identity object id changes but hashCode is our identity.
+                  InstanceRef hashCodeResult;
+
+                  hashCodeResult = await evaluate(
+                    instance.objectRef,
+                    'hashCode',
+                  );
+
+                  // Record we have a real instance too.
+                  inboundNode.setInstance(
+                    instance,
+                    hashCodeResult?.valueAsString,
+                  );
+
+                  final List<ClassHeapDetailStats> allClasses =
+                      _memoryScreen.tableStack.first.model.data;
+
+                  computeInboundRefs(
+                    allClasses,
+                    refs,
+                    (
+                      String referenceName,
+                      String owningAllocator,
+                      bool owningAllocatorIsAbstract,
+                    ) async {
+                      if (!owningAllocatorIsAbstract &&
+                          owningAllocator.isNotEmpty) {
+                        final newRefNode = InboundsTreeNode(
+                          owningAllocator,
+                          referenceName,
+                          hashCodeResult?.valueAsString,
+                        );
+                        inboundNode.addChild(newRefNode);
+                        newRefNode.addChild(InboundsTreeNode.empty());
+                      }
+                    },
+                  );
+                  break;
+                }
+              }
+            }
+
+            spinner.remove();
+            // TODO(terry): Make spinner local using as a sentry.
+            spinner = null;
+          }
+        }
+
+        referencesTable.model.expandNode(inboundNode);
+      })
+      ..onNodeCollapsed.listen(
+          (inboundNode) => referencesTable.model.collapseNode(inboundNode));
+
+    referencesTable = TreeTable<InboundsTreeNode>.virtual()
+      ..element.clazz('memory-table');
+
+    referencesTable.model
+      ..addColumn(classNameColumn)
+      ..addColumn(FieldNameColumn())
+      ..setRows(<InboundsTreeNode>[]);
+
+    referencesTable.model.onSelect.listen(_memoryScreen.select);
+
+    add(referencesTable.element);
+  }
+
+  @override
+  void rebuildView() {
+    final InboundsTreeData providerData = inboundsTree;
+
+    final List<InboundsTreeNode> rows = providerData.data.root.children.cast();
+    // TODO(terry): Work around bug if children have a parent (which they do
+    // TODO(terry): the TreeTable won't render.
+    for (InboundsTreeNode row in rows) row.parent = null;
+
+    referencesTable.model.setRows(rows);
+  }
+
+  @override
+  void reset() => referencesTable.model.setRows(<InboundsTreeNode>[]);
+}
+
+class InboundsTreeData {
+  InboundsTreeData();
+
+  InboundsTreeData.test() {
+    final treeNode00 = InboundsTreeNode('class_0_0', 'field_0');
+    final treeNode01 = InboundsTreeNode('class_0_1', 'field_1');
+    final treeNode02 = InboundsTreeNode('class_0_2', 'field_2');
+    final treeNode03 = InboundsTreeNode('class_0_3', 'field_3');
+    final treeNode04 = InboundsTreeNode('class_0_4', 'field_4');
+    final treeNode05 = InboundsTreeNode('class_0_5', 'field_5');
+
+    final treeNode10 = InboundsTreeNode('class_1', 'field_a');
+    final treeNode11 = InboundsTreeNode('class_1_1', 'field_b');
+    final treeNode12 = InboundsTreeNode('class_1_2', 'field_c');
+    final treeNode13 = InboundsTreeNode('class_1_3', 'field_d');
+    final treeNode14 = InboundsTreeNode('class_4_4', 'field_e');
+    final treeNode15 = InboundsTreeNode('class_1_5', 'field_f');
+
+    final terryStuff = InboundsTreeNode(
+        'TerryStuff allocated TerryExtra', 'extra [object/14752]');
+    final shrineAppState1 = InboundsTreeNode('_ShrineAppState', '_stuff');
+    final shrineAppState2 = InboundsTreeNode('_ShrineAppState', '_stuff2');
+    final statefulElement1 = InboundsTreeNode('StatefulElement', 'state');
+    final hashmapEntry1 = InboundsTreeNode('_HashMapEntry', '_key');
+    final singleChildrenObjectElement1 =
+        InboundsTreeNode('SingleChildrenObjectElement', '_child');
+    final statefulElement2 = InboundsTreeNode('StatefulElement', 'state');
+    final hashmapEntry2 = InboundsTreeNode('_HashMapEntry', '_key');
+    final singleChildrenObjectElement2 =
+        InboundsTreeNode('SingleChildrenObjectElement', '_child');
+
+    data = InboundsTreeNode.root()
+      ..addChild(treeNode00
+        ..addChild(treeNode01)
+        ..addChild(treeNode02
+          ..addChild(treeNode03)
+          ..addChild(treeNode04)
+          ..addChild(treeNode05)))
+      ..addChild(treeNode10
+        ..addChild(treeNode11)
+        ..addChild(treeNode12)
+        ..addChild(treeNode13)
+        ..addChild(treeNode14)
+        ..addChild(treeNode15))
+      ..addChild(terryStuff
+        ..addChild(shrineAppState1
+          ..addChild(statefulElement1
+            ..addChild(hashmapEntry1..addChild(singleChildrenObjectElement1))))
+        ..addChild(shrineAppState2
+          ..addChild(statefulElement2
+            ..addChild(
+                hashmapEntry2..addChild(singleChildrenObjectElement2)))));
+  }
+
+  InboundsTreeNode data;
+}
+
+class InboundsTreeNode extends TreeNode<InboundsTreeNode> {
+  InboundsTreeNode(this._name, this.fieldName, [this.instanceHashCode]);
+
+  InboundsTreeNode.instance(this._instance, [this.instanceHashCode])
+      : _name = _instance.objectRef,
+        fieldName = '';
+
+  InboundsTreeNode.root()
+      : _name = 'Instances',
+        fieldName = '',
+        instanceHashCode = null;
+
+  InboundsTreeNode.empty()
+      : _name = null,
+        fieldName = null,
+        instanceHashCode = null;
+
+  String get name => _name;
+
+  String _name;
+
+  InstanceSummary get instance => _instance;
+
+  /// Replaces the node's [instance], [instanceHashCode] and [name].  This is
+  /// can happen as a result of the objectRef id changing (as known by the VM).
+  /// It's matched to the propery objectRef (instance) by comparing hashCodes.
+  ///
+  /// [isNew] signals the objectRef (e.g., objects/123) changed to something
+  /// different (e.g., objects/245).
+  void setInstance(
+    InstanceSummary theInstance,
+    String hashCode, [
+    bool isNew = false,
+  ]) {
+    _instance = theInstance;
+    instanceHashCode = hashCode;
+    _name = _name.split(' ')[0]; // Throw away instance objectRef name.
+
+    _name = (isNew && !isInboundEntry)
+        ? _instance.objectRef
+        : '$name (${instance.objectRef})';
+  }
+
+  void working(int index, int total) {
+    _name = _name.split(' ')[0]; // Throw away instance objectRef name.
+    _name = '$name ($index of $total)';
+  }
+
+  InstanceSummary _instance;
+
+  final String fieldName;
+
+  bool get isInboundEntry => fieldName?.isNotEmpty;
+
+  String instanceHashCode;
+
+  bool get isEmpty =>
+      _name == null && fieldName == null && instanceHashCode == null;
+}
+
+abstract class InstanceRefsView extends CoreElement {
+  InstanceRefsView(this.inboundsTree) : super('div', classes: 'memory-table');
+
+  final InboundsTreeData inboundsTree;
+
+  bool viewNeedsRebuild = false;
+
+  void rebuildView();
+
+  void reset();
+
+  void update({bool showLoadingSpinner = false}) async {
+    if (inboundsTree == null) return;
+
+    // Update the view if it is visible. Otherwise, mark the view as needing a
+    // rebuild.
+    if (!isHidden) {
+      if (showLoadingSpinner) {
+        final spinner = Spinner.centered();
+        add(spinner);
+
+        // Awaiting this future ensures the spinner pops up in between switching
+        // table views. Without this, the UI is laggy and the spinner never
+        // appears.
+        await Future.delayed(const Duration(microseconds: 1));
+
+        rebuildView();
+        spinner.remove();
+      } else {
+        rebuildView();
+      }
+    } else {
+      viewNeedsRebuild = true;
+    }
+  }
+
+  void show() {
+    hidden(false);
+    if (viewNeedsRebuild) {
+      viewNeedsRebuild = false;
+      update(showLoadingSpinner: true);
+    }
+  }
+
+  void hide() => hidden(true);
+}
+
+class ClassNameColumn extends TreeColumnData<InboundsTreeNode> {
+  ClassNameColumn(String title) : super(title);
+
+  static const maxClassNameLength = 75;
+
+  @override
+  dynamic getValue(InboundsTreeNode dataObject) => dataObject.name;
+
+  @override
+  String getDisplayValue(InboundsTreeNode dataObject) {
+    final String name = dataObject.name;
+    if (name.length > maxClassNameLength) {
+      return name.substring(0, maxClassNameLength) + '...';
+    }
+    return name;
+  }
+
+  @override
+  bool get supportsSorting => true;
+
+  @override
+  String getTooltip(InboundsTreeNode dataObject) =>
+      '${dataObject.name} . ${dataObject.fieldName}';
+}
+
+//class FieldNameColumn extends TreeColumn<InboundsTreeNode> {
+class FieldNameColumn extends ColumnData<InboundsTreeNode> {
+  FieldNameColumn() : super('Field Reference');
+
+  static const maxFieldNameLength = 25;
+
+  @override
+  dynamic getValue(InboundsTreeNode dataObject) => dataObject.fieldName;
+
+  @override
+  String getDisplayValue(InboundsTreeNode dataObject) {
+    final String fieldName = dataObject.fieldName;
+    if (fieldName.length > maxFieldNameLength) {
+      return fieldName.substring(0, maxFieldNameLength) + '...';
+    }
+    return fieldName;
+  }
+
+  @override
+  bool get supportsSorting => false;
+
+  @override
+  String getTooltip(InboundsTreeNode dataObject) =>
+      '${dataObject.fieldName} OF ${dataObject.name}';
+}
diff --git a/devtools/lib/src/memory/memory_plotly.dart b/devtools/lib/src/memory/memory_plotly.dart
index 4075602..8bcf24c 100644
--- a/devtools/lib/src/memory/memory_plotly.dart
+++ b/devtools/lib/src/memory/memory_plotly.dart
@@ -277,7 +277,7 @@
   // Resetting to live view, it's an autoscale back to full view.
   void _doubleClick(DataEvent data) => _memoryChart.resume();
 
-  void plotMemory([createEventTimeline = false]) {
+  void plotMemory([bool createEventTimeline = false]) {
     final List<Data> memoryTraces = createMemoryTraces();
 
     if (createEventTimeline) {
@@ -422,7 +422,7 @@
   final String _eventBgColorCss = colorToCss(eventBgColor);
 
   final String _domName;
-  dynamic _chart;
+  final dynamic _chart;
 
   // Trace index within the traces passed to addEventTimelineTo
   int resetTraceIndex;
diff --git a/devtools/lib/src/memory/memory_protocol.dart b/devtools/lib/src/memory/memory_protocol.dart
index ff23a18..c66e0dd 100644
--- a/devtools/lib/src/memory/memory_protocol.dart
+++ b/devtools/lib/src/memory/memory_protocol.dart
@@ -5,9 +5,11 @@
 import 'dart:async';
 import 'dart:math' as math;
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
+import '../globals.dart';
 import '../vm_service_wrapper.dart';
+import 'heap_space.dart';
 
 class MemoryTracker {
   MemoryTracker(this.service);
@@ -16,8 +18,6 @@
 
   VmServiceWrapper service;
   Timer _pollingTimer;
-  final StreamController<Null> _changeController =
-      StreamController<Null>.broadcast();
 
   final List<HeapSample> samples = <HeapSample>[];
   final Map<String, List<HeapSpace>> isolateHeaps = <String, List<HeapSpace>>{};
@@ -26,10 +26,13 @@
 
   bool get hasConnection => service != null;
 
-  Stream<Null> get onChange => _changeController.stream;
+  Stream<void> get onChange => _changeController.stream;
+  final _changeController = StreamController<void>.broadcast();
 
   int get currentCapacity => samples.last.capacity;
+
   int get currentUsed => samples.last.used;
+
   int get currentExternal => samples.last.external;
 
   void start() {
@@ -54,7 +57,7 @@
   }
 
   // TODO(terry): Discuss need a record/stop record for memory?  Unless expensive probably not.
-  Future<Null> _pollMemory() async {
+  Future<void> _pollMemory() async {
     if (!hasConnection) {
       return;
     }
@@ -144,10 +147,9 @@
 
 // Heap Statistics
 
-// More detailed/simpler then the ClassHeapStats (for handling accumulators to
-// detect leaks and simpler then drilling into new/old which should be hidden
-// with a new protocol).
+// Wrapper for ClassHeapStats.
 //
+// Pre VM Service Protocol 3.18:
 // {
 //   type: ClassHeapStats,
 //   class: {type: @Class, fixedId: true, id: classes/5, name: Class},
@@ -156,11 +158,28 @@
 //   promotedInstances: 0,
 //   promotedBytes: 0
 // }
+//
+// VM Service Protocol 3.18 and later:
+// {
+//   type: ClassHeapStats,
+//   class: {type: @Class, fixedId: true, id: classes/5, name: Class},
+//   accumulatedSize: 809536
+//   bytesCurrent: 809536
+//   instancesAccumulated: 3892
+//   instancesCurrent: 3892
+// }
 class ClassHeapDetailStats {
   ClassHeapDetailStats(this.json) {
     classRef = ClassRef.parse(json['class']);
-    _update(json['new']);
-    _update(json['old']);
+    if (serviceManager.service.protocolVersionLessThan(major: 3, minor: 18)) {
+      _update(json['new']);
+      _update(json['old']);
+    } else {
+      instancesCurrent = json['instancesCurrent'];
+      instancesAccumulated = json['instancesAccumulated'];
+      bytesCurrent = json['bytesCurrent'];
+      bytesAccumulated = json['bytesAccumulated'];
+    }
   }
 
   static const int ALLOCATED_BEFORE_GC = 0;
@@ -191,37 +210,27 @@
   }
 
   @override
-  String toString() =>
-      '[ClassHeapStats type: $type, class: ${classRef.name}, count: $instancesCurrent, bytes: $bytesCurrent]';
+  String toString() => '[ClassHeapStats type: $type, class: ${classRef.name}, '
+      'count: $instancesCurrent, bytes: $bytesCurrent]';
 }
 
 class InstanceSummary {
-  InstanceSummary(this._classRef, this._className, this._objectRef);
+  InstanceSummary(this.classRef, this.className, this.objectRef);
 
-  String get classRef => _classRef;
-  String _classRef;
-
-  String get objectRef => _objectRef;
-  String _objectRef;
-
-  String get className => _className;
-  String _className;
+  final String classRef;
+  final String className;
+  final String objectRef;
 
   @override
   String toString() => '[InstanceSummary id: $objectRef, class: $classRef]';
 }
 
 class InstanceData {
-  InstanceData(this._instance, this._name, this._value);
+  InstanceData(this.instance, this.name, this.value);
 
-  InstanceSummary _instance;
-  InstanceSummary get instance => _instance;
-
-  String get name => _name;
-  String _name;
-
-  dynamic get value => _value;
-  dynamic _value;
+  final InstanceSummary instance;
+  final String name;
+  final dynamic value;
 
   @override
   String toString() => '[InstanceData name: $name, value: $value]';
diff --git a/devtools/lib/src/memory/memory_service.dart b/devtools/lib/src/memory/memory_service.dart
new file mode 100644
index 0000000..0cad0d6
--- /dev/null
+++ b/devtools/lib/src/memory/memory_service.dart
@@ -0,0 +1,245 @@
+// Copyright 2019 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:vm_service/vm_service.dart';
+
+import '../globals.dart';
+import 'memory_protocol.dart';
+
+// TODO(terry): This file prints out fatal errors.  Unable to use ga.error
+// TODO(terry): because of dart:js usage.  Look at abstracting errors to a log
+// TODO(terry): and fatal errors are eventually sent to analytics.
+
+String get _isolateId => serviceManager.isolateManager.selectedIsolate.id;
+
+Future<InstanceRef> evaluate(String objectRef, String expression) async {
+  final dynamic result =
+      await serviceManager.service.evaluate(_isolateId, objectRef, expression);
+  switch (result.runtimeType) {
+    case InstanceRef:
+      return InstanceRef.parse(result.json);
+      break;
+    case ErrorRef:
+      return null;
+    default:
+      print('ERROR Memory evaluate: Unknown type ${result.runtimeType}.');
+  }
+
+  return null;
+}
+
+Future<InboundReferences> getInboundReferences(
+    String objectRef, int maxInstances) async {
+  // TODO(terry): Expose as a stream to reduce stall when querying for 1000s of
+  // instances.
+  final Response response = await serviceManager.service
+      .getInboundReferences(_isolateId, objectRef, maxInstances);
+
+  if (response.type == 'Sentinel') return null;
+
+  return InboundReferences(response.json);
+}
+
+class InboundReferences extends Response {
+  InboundReferences(Map<String, dynamic> json) {
+    elements = json['references']
+        .map<InboundReference>((rmap) => InboundReference.parse(rmap))
+        .toList();
+  }
+
+  Iterable<InboundReference> elements;
+}
+
+class InboundReference extends Response {
+  InboundReference._fromJson(Map<String, dynamic> json) {
+    //source = json['source']
+    parentField = createServiceObject(json['parentField'], ['FieldRef']);
+    parentListIndex = json['parentListIndex'];
+    parentWordOffset = json['_parentWordOffset'];
+  }
+
+  static InboundReference parse(Map<String, dynamic> json) {
+    return json == null ? null : new InboundReference._fromJson(json);
+  }
+
+  dynamic parentField;
+
+  int parentListIndex;
+
+  int parentWordOffset;
+
+  bool get isFieldRef => parentField.runtimeType == FieldRef;
+
+  FieldRef get fieldRef => isFieldRef ? parentField as FieldRef : null;
+
+  bool get isClassRef => parentField.runtimeType == ClassRef;
+
+  ClassRef get classRef => isFieldRef ? parentField as ClassRef : null;
+
+  bool get isFuncRef => parentField.runtimeType == FuncRef;
+
+  FuncRef get funcRef => isFuncRef ? parentField as FuncRef : null;
+
+  bool get isNullVal => parentField.runtimeType == NullVal;
+
+  bool get isNullValRef => parentField.runtimeType == NullValRef;
+
+  NullVal get nullVal => isInstanceRef ? parentField as NullVal : null;
+
+  bool get isInstance => parentField.runtimeType == Instance;
+
+  Instance get instance => isInstance ? parentField as Instance : null;
+
+  bool get isInstanceRef => parentField.runtimeType == InstanceRef;
+
+  InstanceRef get instanceRef =>
+      isInstanceRef ? parentField as InstanceRef : null;
+
+  bool get isLibrary => parentField.runtimeType == Library;
+
+  bool get isLibraryRef => parentField.runtimeType == LibraryRef;
+
+  Library get library => isLibrary ? parentField as Library : null;
+
+  bool get isObj => parentField.runtimeType == Obj;
+
+  bool get isObjRef => parentField.runtimeType == ObjRef;
+
+  Obj get obj => isObj ? parentField as Obj : null;
+
+  bool get isSentinel => parentField.runtimeType == Sentinel;
+}
+
+typedef BuildInboundEntry = void Function(
+  String referenceName,
+  /* Field that owns reference to allocated memory */
+  String owningAllocator,
+  /* Parent class that allocated memory. */
+  bool owningAllocatorIsAbstract,
+  /* is owning class abstract */
+);
+
+ClassHeapDetailStats _searchClass(
+  List<ClassHeapDetailStats> allClasses,
+  String className,
+) =>
+    allClasses.firstWhere((dynamic stat) => stat.classRef.name == className,
+        orElse: () => null);
+
+// Compute the inboundRefs, who allocated the class/which field owns the ref.
+void computeInboundRefs(
+  List<ClassHeapDetailStats> allClasses,
+  InboundReferences refs,
+  BuildInboundEntry buildCallback,
+) {
+  final Iterable<InboundReference> elements = refs?.elements ?? [];
+  for (InboundReference element in elements) {
+    // Could be a reference to an evaluate so this isn't known.
+
+    // Looks like an object created from an evaluate, ignore it.
+    if (element.parentField == null && element.json == null) continue;
+
+    // TODO(terry): Verify looks like internal class (maybe to C code).
+    if (element.parentField.owner != null &&
+        element.parentField.owner.name.contains('&')) continue;
+
+    String referenceName;
+    String owningAllocator; // Class or library that allocated.
+    bool owningAllocatorIsAbstract;
+
+    switch (element.parentField.runtimeType.toString()) {
+      case 'ClassRef':
+        final ClassRef classRef = element.classRef;
+        owningAllocator = classRef.name;
+        // TODO(terry): Quick way to detect if class is probably abstract-
+        // TODO(terry): Does it exist in the class list table?
+        owningAllocatorIsAbstract =
+            _searchClass(allClasses, owningAllocator) == null;
+        break;
+      case 'FieldRef':
+        final FieldRef fieldRef = element.fieldRef;
+        referenceName = fieldRef.name;
+        switch (fieldRef.owner.runtimeType.toString()) {
+          case 'ClassRef':
+            final ClassRef classRef = ClassRef.parse(fieldRef.owner.json);
+            owningAllocator = classRef.name;
+            // TODO(terry): Quick way to detect if class is probably abstract-
+            // TODO(terry): Does it exist in the class list table?
+            owningAllocatorIsAbstract =
+                _searchClass(allClasses, owningAllocator) == null;
+            break;
+          case 'Library':
+          case 'LibraryRef':
+            final Library library = Library.parse(fieldRef.owner.json);
+            owningAllocator = 'Library ${library?.name ?? ""}';
+            break;
+        }
+        break;
+      case 'FuncRef':
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final FuncRef funcRef = element.funcRef;
+        break;
+      case 'Instance':
+        print(
+          'Error(hoverInstanceAllocations): '
+          ' Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final Instance instance = element.instance;
+        break;
+      case 'InstanceRef':
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final InstanceRef instanceRef = element.instanceRef;
+        break;
+      case 'Library':
+      case 'LibraryRef':
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final Library library = element.library;
+        break;
+      case 'NullVal':
+      case 'NullValRef':
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final NullVal nullValue = element.nullVal;
+        break;
+      case 'Obj':
+      case 'ObjRef':
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled ${element.parentField.runtimeType}',
+        );
+        // TODO(terry): TBD
+        // final Obj obj = element.obj;
+        break;
+      default:
+        print(
+          'Error(hoverInstanceAllocations): '
+          'Unhandled inbound ${element.parentField.runtimeType}',
+        );
+    }
+
+    // call the build UI callback.
+    if (buildCallback != null)
+      buildCallback(
+        referenceName,
+        owningAllocator,
+        owningAllocatorIsAbstract,
+      );
+  }
+}
diff --git a/devtools/lib/src/message_manager.dart b/devtools/lib/src/message_manager.dart
new file mode 100644
index 0000000..57fe4a5
--- /dev/null
+++ b/devtools/lib/src/message_manager.dart
@@ -0,0 +1,113 @@
+// Copyright 2019 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 'dart:async';
+
+import 'ui/elements.dart';
+import 'ui/primer.dart';
+
+/// Used as a screen id for messages that do not pertain to a specific screen.
+const generalId = 'general';
+
+class MessageManager {
+  MessageManager();
+
+  final _container = CoreElement.from(queryId('messages-container'));
+
+  /// Maps screen ids to their respective messages.
+  ///
+  /// Messages that do not pertain to a specific screen will be stored under the
+  /// key [_generalId].
+  final Map<String, Set<Message>> _messages = {};
+
+  final List<String> _dismissedMessageIds = [];
+
+  void showMessagesForScreen(String screenId) {
+    _messages[screenId]?.forEach(_showMessage);
+  }
+
+  void _showMessage(Message message) {
+    if (_dismissedMessageIds.contains(message.id)) return;
+    _container.add(message.flash);
+  }
+
+  void removeAll() {
+    _container.clear();
+    // Remove all error messages.
+    _messages[generalId]
+        ?.removeWhere((m) => m.messageType == MessageType.error);
+  }
+
+  void addMessage(Message message, String screenId) {
+    message.onDismiss.listen((_message) {
+      if (_message.id != null) {
+        _dismissedMessageIds.add(_message.id);
+      }
+      _messages[screenId]?.remove(_message);
+    });
+
+    // ignore: prefer_collection_literals
+    _messages.putIfAbsent(screenId, () => Set()).add(message);
+    _showMessage(message);
+  }
+}
+
+class Message {
+  Message(
+    this.messageType, {
+    this.id,
+    this.message,
+    this.title,
+    this.children,
+  }) {
+    _buildFlash();
+  }
+
+  final MessageType messageType;
+
+  final String id;
+
+  final String message;
+
+  final String title;
+
+  final List<CoreElement> children;
+
+  final PFlash flash = PFlash();
+
+  final StreamController<Message> _dismissController =
+      StreamController<Message>.broadcast();
+
+  Stream<Message> get onDismiss => _dismissController.stream;
+
+  void _buildFlash() {
+    if (messageType == MessageType.warning) {
+      flash.warning();
+    } else if (messageType == MessageType.error) {
+      flash.error();
+    }
+
+    flash.addClose().click(() {
+      flash.element.remove();
+      _dismissController.add(this);
+    });
+
+    if (title != null) {
+      flash.add(label(text: title));
+    }
+    if (message != null) {
+      for (String text in message.split('\n\n')) {
+        flash.add(div(text: text));
+      }
+    }
+    if (children != null) {
+      children.forEach(flash.add);
+    }
+  }
+}
+
+enum MessageType {
+  info,
+  warning,
+  error,
+}
diff --git a/devtools/lib/src/messages.dart b/devtools/lib/src/messages.dart
new file mode 100644
index 0000000..add2fe0
--- /dev/null
+++ b/devtools/lib/src/messages.dart
@@ -0,0 +1,73 @@
+// Copyright 2019 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 'message_manager.dart';
+import 'ui/elements.dart';
+
+final trackWidgetCreationWarning = Message(
+  MessageType.warning,
+  id: 'trackWidgetCreationWarningId',
+  children: <CoreElement>[
+    div()
+      ..add(span(text: 'The '))
+      ..add(a(
+          text: 'widget creation tracking feature',
+          href: _trackWidgetCreationDocsUrl,
+          target: '_blank;'))
+      ..add(span(text: ' is not enabled. '))
+      ..add(span(text: '''This feature allows the Flutter inspector to present 
+the widget tree in a manner similar to how the UI was defined in your source
+code. Without it, the tree of nodes in the widget tree are much deeper, and it
+can be more difficult to understand how the runtime widget hierarchy corresponds
+to your application\’s UI.''')),
+    div(text: '''To fix this, relaunch your application by running 
+'flutter run --track-widget-creation' (or run your application from VS Code or
+IntelliJ).'''),
+  ],
+);
+
+const _trackWidgetCreationDocsUrl =
+    'https://flutter.dev/docs/development/tools/devtools/inspector#track-widget-creation';
+
+final debugWarning = Message(
+  MessageType.warning,
+  id: 'debugWarningId',
+  children: <CoreElement>[
+    div(
+        text: 'You are running your app in debug mode. Debug mode frame '
+            'rendering times are not indicative of release performance.'),
+    div()
+      ..add(span(
+          text: '''Relaunch your application with the '--profile' argument, or 
+'''))
+      ..add(a(
+          text: 'relaunch in profile mode from VS Code or IntelliJ',
+          href: _runInProfileModeDocsUrl,
+          target: '_blank;'))
+      ..add(span(text: '.')),
+  ],
+);
+
+const String _runInProfileModeDocsUrl =
+    'https://flutter.dev/docs/testing/ui-performance#run-in-profile-mode';
+
+final profileGranularityWarning = Message(
+  MessageType.warning,
+  id: 'highSamplingRateWarning',
+  children: [
+    div(
+        text: 'You are opting in to a high CPU sampling rate. This may affect '
+            'the performance of your application.'),
+    div()
+      ..add(span(text: 'Please read our '))
+      ..add(a(
+          text: 'documentation',
+          href: _profileGranularityDocsUrl,
+          target: '_blank;'))
+      ..add(span(
+          text: ' to understand the trade-offs associated with this setting.'))
+  ],
+);
+
+const String _profileGranularityDocsUrl =
+    'https://flutter.dev/docs/development/tools/devtools/performance#profile-granularity';
diff --git a/devtools/lib/src/model/model.dart b/devtools/lib/src/model/model.dart
index 5b09086..a3a6397 100644
--- a/devtools/lib/src/model/model.dart
+++ b/devtools/lib/src/model/model.dart
@@ -12,16 +12,18 @@
 import 'dart:convert';
 import 'dart:js' as js;
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../debugger/debugger.dart';
 import '../framework/framework.dart';
 import '../globals.dart';
 import '../logging/logging.dart';
+import '../logging/logging_controller.dart';
 import '../main.dart';
 
 class App {
   App(this.framework) {
+    _register<void>('devToolsReady', devToolsReady);
     _register<void>('echo', echo);
     _register<void>('switchPage', switchPage);
     _register<String>('currentPageId', currentPageId);
@@ -54,9 +56,8 @@
         'debugger.getConsoleContents', debuggerGetConsoleContents);
   }
 
-  static void register(PerfToolFramework framework) {
-    final App app = App(framework);
-    app._bind();
+  static App register(PerfToolFramework framework) {
+    return App(framework).._bind();
   }
 
   final PerfToolFramework framework;
@@ -79,6 +80,10 @@
     js.context['devtools'] = binding;
   }
 
+  Future<void> devToolsReady(dynamic message) async {
+    _sendNotification('app.devToolsReady', message);
+  }
+
   Future<void> echo(dynamic message) async {
     _sendNotification('app.echo', message);
   }
@@ -106,17 +111,17 @@
 
   Future<void> logsClearLogs([dynamic _]) async {
     final LoggingScreen screen = framework.getScreen('logging');
-    screen.loggingTable.setRows(<LogData>[]);
+    screen.controller.loggingTableModel.setRows(<LogData>[]);
   }
 
   Future<int> logsLogCount([dynamic _]) async {
     final LoggingScreen screen = framework.getScreen('logging');
-    return screen.loggingTable.rowCount;
+    return screen.controller.loggingTableModel.rowCount;
   }
 
   Future<String> debuggerGetState([dynamic _]) async {
     final DebuggerScreen screen = framework.getScreen('debugger');
-    return screen.debuggerState.isPaused ? 'paused' : 'running';
+    return screen.debuggerState.isPaused.value ? 'paused' : 'running';
   }
 
   Future<String> debuggerGetConsoleContents([dynamic _]) async {
@@ -157,9 +162,9 @@
 
   Future<List<String>> debuggerGetBreakpoints([dynamic _]) async {
     final DebuggerScreen screen = framework.getScreen('debugger');
-    return screen.debuggerState.breakpoints.map((Breakpoint breakpoint) {
-      return breakpoint.id;
-    }).toList();
+    return screen.debuggerState.breakpoints.value
+        .map((breakpoint) => breakpoint.id)
+        .toList();
   }
 
   Future<bool> debuggerSupportsScripts([dynamic _]) async {
@@ -241,6 +246,7 @@
     if (params != null) {
       map['params'] = params;
     }
+    // TODO(terry): Shouldn't print to console by default.
     print('[${jsonEncode(map)}]');
   }
 
@@ -251,6 +257,7 @@
     if (result != null) {
       map['result'] = result;
     }
+    // TODO(terry): Shouldn't print to console by default.
     print('[${jsonEncode(map)}]');
   }
 
@@ -262,6 +269,7 @@
         'stackTrace': stackTrace.toString(),
       },
     };
+    // TODO(terry): Better error message to user and log to GA too?
     print('[${jsonEncode(map)}]');
   }
 
diff --git a/devtools/lib/src/performance/performance.dart b/devtools/lib/src/performance/performance.dart
deleted file mode 100644
index fe44e1c..0000000
--- a/devtools/lib/src/performance/performance.dart
+++ /dev/null
@@ -1,399 +0,0 @@
-// Copyright 2018 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 'dart:async';
-import 'dart:math' as math;
-
-import 'package:vm_service_lib/vm_service_lib.dart';
-
-import '../charts/charts.dart';
-import '../framework/framework.dart';
-import '../globals.dart';
-import '../tables.dart';
-import '../ui/elements.dart';
-import '../ui/primer.dart';
-import '../utils.dart';
-import '../vm_service_wrapper.dart';
-
-class PerformanceScreen extends Screen {
-  PerformanceScreen()
-      : super(
-            name: 'Performance',
-            id: 'performance',
-            iconClass: 'octicon-dashboard') {
-    sampleCountStatus = StatusItem();
-    addStatusItem(sampleCountStatus);
-
-    sampleFreqStatus = StatusItem();
-    addStatusItem(sampleFreqStatus);
-  }
-
-  StatusItem sampleCountStatus;
-  StatusItem sampleFreqStatus;
-
-  PButton loadSnapshotButton;
-  PButton resetButton;
-  CoreElement progressElement;
-  Table<PerfData> perfTable;
-
-  CpuChart cpuChart;
-  SetStateMixin cpuChartStateMixin = SetStateMixin();
-  CpuTracker cpuTracker;
-
-  @override
-  CoreElement createContent(Framework framework) {
-    final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
-
-    screenDiv.add(<CoreElement>[
-      createLiveChartArea(),
-      div(c: 'section'),
-      div(c: 'section')
-        ..add(<CoreElement>[
-          form()
-            ..layoutHorizontal()
-            ..clazz('align-items-center')
-            ..add(<CoreElement>[
-              loadSnapshotButton = PButton('Load snapshot')
-                ..small()
-                ..primary()
-                ..click(_loadSnapshot),
-              progressElement = span(c: 'margin-left text-gray')..flex(),
-              resetButton = PButton('Reset VM counters')
-                ..small()
-                ..click(_reset),
-            ])
-        ]),
-      _createTableView()..clazz('section'),
-    ]);
-
-    _updateStatus(null);
-
-    serviceManager.isolateManager.onSelectedIsolateChanged.listen((_) {
-      _handleIsolateChanged();
-    });
-
-    serviceManager.onConnectionAvailable.listen(_handleConnectionStart);
-    if (serviceManager.hasConnection) {
-      _handleConnectionStart(serviceManager.service);
-    }
-    serviceManager.onConnectionClosed.listen(_handleConnectionStop);
-
-    return screenDiv;
-  }
-
-  void _handleIsolateChanged() {
-    // TODO(devoncarew): Reset UI
-  }
-
-  String get _isolateId => serviceManager.isolateManager.selectedIsolate.id;
-
-  void _loadSnapshot() {
-    loadSnapshotButton.disabled = true;
-
-    progressElement.text = 'Loading snapshot…';
-
-    serviceManager.service
-        .getCpuProfile(_isolateId, 'UserVM')
-        .then((CpuProfile profile) async {
-      final _CalcProfile calc = _CalcProfile(profile);
-      await calc.calc();
-      _updateStatus(profile);
-    }).catchError((dynamic e) {
-      framework.showError('Error loading snapshot', e);
-    }).whenComplete(() {
-      loadSnapshotButton.disabled = false;
-      progressElement.text = '';
-    });
-  }
-
-  CoreElement createLiveChartArea() {
-    final CoreElement container = div(c: 'section perf-chart table-border')
-      ..layoutVertical();
-    cpuChart = CpuChart(container);
-    cpuChart.disabled = true;
-    return container;
-  }
-
-  void _reset() {
-    resetButton.disabled = true;
-
-    serviceManager.service.clearCpuProfile(_isolateId).then((_) {
-      framework.toast('VM counters reset.');
-    }).catchError((dynamic e) {
-      framework.showError('Error resetting counters', e);
-    }).whenComplete(() {
-      resetButton.disabled = false;
-    });
-  }
-
-  CoreElement _createTableView() {
-    perfTable = Table<PerfData>.virtual();
-
-    perfTable.addColumn(PerfColumnInclusive());
-    perfTable.addColumn(PerfColumnSelf());
-    perfTable.addColumn(PerfColumnMethodName());
-
-    perfTable.setSortColumn(perfTable.columns.first);
-
-    perfTable.setRows(<PerfData>[]);
-
-    perfTable.onSelect.listen((PerfData data) {
-      // TODO(devoncarew): Handle item selection
-      print(data);
-    });
-
-    return perfTable.element;
-  }
-
-  void _updateStatus(CpuProfile profile) {
-    if (profile == null) {
-      sampleCountStatus.element.text = '';
-      sampleFreqStatus.element.text = '';
-    } else {
-      final Duration timeSpan = Duration(seconds: profile.timeSpan.round());
-      String s = timeSpan.toString();
-      s = s.substring(0, s.length - 7);
-      sampleCountStatus.element.text =
-          '${nf.format(profile.sampleCount)} samples over $s';
-      sampleFreqStatus.element.text =
-          '${profile.stackDepth} frames per sample @ ${profile.samplePeriod}Hz';
-
-      _process(profile);
-    }
-  }
-
-  void _process(CpuProfile profile) {
-    perfTable.setRows(
-        List<PerfData>.from(profile.functions.where((ProfileFunction f) {
-      return f.inclusiveTicks > 0 || f.exclusiveTicks > 0;
-    }).map<PerfData>((ProfileFunction f) {
-      final int count = math.max(1, profile.sampleCount);
-      return PerfData(
-        f.kind,
-        escape(funcRefName(f.function)),
-        f.exclusiveTicks / count,
-        f.inclusiveTicks / count,
-      );
-    })));
-  }
-
-  void _handleConnectionStart(VmServiceWrapper service) {
-    cpuChart.disabled = false;
-
-    cpuTracker = CpuTracker(service);
-    cpuTracker.start();
-
-    cpuTracker.onChange.listen((Null _) {
-      cpuChartStateMixin.setState(() {
-        cpuChart.updateFrom(cpuTracker);
-      });
-    });
-  }
-
-  void _handleConnectionStop(dynamic event) {
-    cpuChart.disabled = true;
-
-    cpuTracker?.stop();
-  }
-}
-
-class CpuChart extends LineChart<CpuTracker> {
-  CpuChart(CoreElement parent) : super(parent, classes: 'perf-chart') {
-    usageLabel = parent.add(div(c: 'perf-label'));
-    usageLabel.element.style.right = '0';
-  }
-
-  CoreElement usageLabel;
-
-  @override
-  void update(CpuTracker data) {
-    if (data.samples.isEmpty || dim == null) {
-      return;
-    }
-
-    // display the cpu usage
-    usageLabel.text = '${data._lastValue}%';
-
-    // re-render the svg
-    final int hRange = CpuTracker.kMaxGraphTime.inSeconds;
-    const int vRange = 100;
-
-    chartElement.setInnerHtml('''
-<svg viewBox="0 0 0 0 ${dim.x} ${LineChart.fixedHeight}">
-<polyline
-    fill="none"
-    stroke="#0074d9"
-    stroke-width="3"
-    points="${createPoints(data.samples, hRange, vRange)}"/>
-</svg>
-''');
-  }
-
-  String createPoints(List<int> samples, int hRange, int vRange) {
-    // 0,120 20,60 40,80 60,20
-    final List<String> coords = <String>[];
-    int pos = 0;
-    for (int i = samples.length - 1; i >= 0; i--) {
-      final int x = dim.x - (pos * dim.x ~/ hRange);
-      final int y = dim.y - (samples[i] * dim.y ~/ vRange);
-      coords.add('$x,$y');
-      pos++;
-    }
-    return coords.join(' ');
-  }
-}
-
-class CpuTracker {
-  CpuTracker(this.service);
-
-  static const Duration kMaxGraphTime = Duration(minutes: 1);
-  static const Duration kUpdateDelay = Duration(seconds: 1);
-
-  VmServiceWrapper service;
-  Timer _pollingTimer;
-  final StreamController<Null> _changeController =
-      StreamController<Null>.broadcast();
-  List<int> samples = <int>[];
-
-  bool get hasConnection => service != null;
-
-  Stream<Null> get onChange => _changeController.stream;
-
-  void start() {
-    _pollingTimer = Timer(const Duration(milliseconds: 100), _pollCpu);
-  }
-
-  void _pollCpu() {
-    if (!hasConnection) {
-      return;
-    }
-
-    // TODO(devoncarew): Poll the VM for the CPU load.
-
-    _pollingTimer = Timer(kUpdateDelay, _pollCpu);
-  }
-
-  void stop() {
-    _pollingTimer?.cancel();
-    service = null;
-  }
-
-  int get _lastValue => samples.isEmpty ? null : samples.last;
-
-  // ignore: unused_element
-  void _addSample(int sample) {
-    samples.add(sample);
-
-    while (samples.length > (kMaxGraphTime.inSeconds + 2)) {
-      samples.removeAt(0);
-    }
-
-    _changeController.add(null);
-  }
-}
-
-class PerfData {
-  PerfData(this.kind, this.name, this.self, this.inclusive);
-
-  final String kind;
-  final String name;
-  final double self;
-  final double inclusive;
-
-  @override
-  String toString() => '[$kind] $name';
-}
-
-class PerfColumnInclusive extends Column<PerfData> {
-  PerfColumnInclusive() : super('Total');
-
-  @override
-  bool get numeric => true;
-
-  @override
-  dynamic getValue(PerfData item) => item.inclusive;
-
-  @override
-  String render(dynamic value) => percent2(value);
-}
-
-class PerfColumnSelf extends Column<PerfData> {
-  PerfColumnSelf() : super('Self');
-
-  @override
-  bool get numeric => true;
-
-  @override
-  dynamic getValue(PerfData item) => item.self;
-
-  @override
-  String render(dynamic value) => percent2(value);
-}
-
-class PerfColumnMethodName extends Column<PerfData> {
-  PerfColumnMethodName() : super('Method', wide: true);
-
-  @override
-  bool get usesHtml => true;
-
-  @override
-  dynamic getValue(PerfData item) {
-    if (item.kind == 'Dart') {
-      return item.name;
-    }
-    return '${item.name} <span class="function-kind ${item.kind}">${item.kind}</span>';
-  }
-}
-
-class _CalcProfile {
-  _CalcProfile(this.profile);
-
-  final CpuProfile profile;
-
-  Future<void> calc() async {
-    // TODO(devoncarew): Implement code to parse trie graph
-    //profile.exclusiveCodeTrie;
-
-//    tries['exclusiveCodeTrie'] =
-//      new Uint32List.fromList(profile['exclusiveCodeTrie']);
-//    tries['inclusiveCodeTrie'] =
-//      new Uint32List.fromList(profile['inclusiveCodeTrie']);
-//    tries['exclusiveFunctionTrie'] =
-//      new Uint32List.fromList(profile['exclusiveFunctionTrie']);
-//    tries['inclusiveFunctionTrie'] =
-//      new Uint32List.fromList(profile['inclusiveFunctionTrie']);
-  }
-}
-
-/*
-// Process code table.
-for (var codeRegion in profile['codes']) {
-  if (needToUpdate()) {
-    await signal(count * 100.0 / length);
-  }
-  Code code = codeRegion['code'];
-  assert(code != null);
-  codes.add(new ProfileCode.fromMap(this, code, codeRegion));
-}
-// Process function table.
-for (var profileFunction in profile['functions']) {
-  if (needToUpdate()) {
-    await signal(count * 100 / length);
-  }
-  ServiceFunction function = profileFunction['function'];
-  assert(function != null);
-  functions.add(
-      new ProfileFunction.fromMap(this, function, profileFunction));
-}
-
-tries['exclusiveCodeTrie'] =
-    new Uint32List.fromList(profile['exclusiveCodeTrie']);
-tries['inclusiveCodeTrie'] =
-    new Uint32List.fromList(profile['inclusiveCodeTrie']);
-tries['exclusiveFunctionTrie'] =
-    new Uint32List.fromList(profile['exclusiveFunctionTrie']);
-tries['inclusiveFunctionTrie'] =
-    new Uint32List.fromList(profile['inclusiveFunctionTrie']);
-
-*/
diff --git a/devtools/lib/src/performance/performance_controller.dart b/devtools/lib/src/performance/performance_controller.dart
new file mode 100644
index 0000000..bf2abd6
--- /dev/null
+++ b/devtools/lib/src/performance/performance_controller.dart
@@ -0,0 +1,54 @@
+// Copyright 2019 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 'dart:async';
+
+import '../profiler/cpu_profile_model.dart';
+import '../profiler/cpu_profile_service.dart';
+import '../profiler/cpu_profile_transformer.dart';
+import '../utils.dart';
+
+class PerformanceController {
+  final CpuProfilerService cpuProfilerService = CpuProfilerService();
+
+  final CpuProfileTransformer cpuProfileTransformer = CpuProfileTransformer();
+
+  /// Processed cpu profile data from the recorded performance profile.
+  CpuProfileData cpuProfileData;
+
+  Timer timer;
+
+  bool get recording => _recording;
+
+  bool _recording = false;
+
+  final int _profileStartMicros = 0;
+
+  Future<void> startRecording() async {
+    await reset();
+    _recording = true;
+
+    // TODO(kenzie): once [getVMTimelineMicros] is available, we can get the
+    // current timestamp here and set [_profileStartMicros] equal to it. We will
+    // use [_profileStartMicros] for [startMicros] in the [getCpuProfile]
+    // request. For backwards compatibility, we will let start micros default to
+    // 0.
+  }
+
+  Future<void> stopRecording() async {
+    _recording = false;
+
+    cpuProfileData = await cpuProfilerService.getCpuProfile(
+      startMicros: _profileStartMicros,
+      // Using [maxJsInt] as [extentMicros] for the getCpuProfile requests will
+      // give us all cpu samples we have available
+      extentMicros: maxJsInt,
+    );
+  }
+
+  Future<void> reset() async {
+    cpuProfileData = null;
+    await cpuProfilerService.clearCpuProfile();
+  }
+}
diff --git a/devtools/lib/src/performance/performance_screen.dart b/devtools/lib/src/performance/performance_screen.dart
new file mode 100644
index 0000000..ff4cc6a
--- /dev/null
+++ b/devtools/lib/src/performance/performance_screen.dart
@@ -0,0 +1,205 @@
+// Copyright 2018 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:meta/meta.dart';
+
+import '../framework/framework.dart';
+import '../profiler/cpu_profile_flame_chart.dart';
+import '../profiler/cpu_profile_tables.dart';
+import '../profiler/cpu_profiler.dart';
+import '../ui/custom.dart';
+import '../ui/elements.dart';
+import '../ui/material_icons.dart';
+import '../ui/primer.dart';
+import '../ui/ui_utils.dart';
+import '../ui/vm_flag_elements.dart';
+import 'performance_controller.dart';
+
+const performanceScreenId = 'performance';
+
+class PerformanceScreen extends Screen {
+  PerformanceScreen({bool disabled, String disabledTooltip})
+      : super(
+          name: 'Performance',
+          id: 'performance',
+          iconClass: 'octicon-dashboard',
+          disabled: disabled,
+          disabledTooltip: disabledTooltip,
+        );
+
+  final PerformanceController _performanceController = PerformanceController();
+
+  PButton _startRecordingButton;
+
+  PButton _stopRecordingButton;
+
+  PButton _clearButton;
+
+  ProfileGranularitySelector _profileGranularitySelector;
+
+  CoreElement _recordingInstructions;
+
+  CoreElement _recordingStatus;
+
+  CoreElement _recordingStatusMessage;
+
+  CpuProfilerTabNav _tabNav;
+
+  _CpuProfiler _cpuProfiler;
+
+  @override
+  CoreElement createContent(Framework framework) {
+    final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
+
+    // Initialize screen content.
+    _initContent();
+
+    screenDiv.add([
+      div(c: 'section')
+        ..layoutHorizontal()
+        ..add([
+          div(c: 'btn-group')
+            ..add([
+              _startRecordingButton,
+              _stopRecordingButton,
+            ]),
+          _clearButton,
+          div()..flex(),
+          _profileGranularitySelector.selector,
+        ]),
+      div(c: 'section')
+        ..layoutVertical()
+        ..flex()
+        ..add([
+          _tabNav.element..hidden(true),
+          div(c: 'profiler-container section-border')
+            ..add([
+              _cpuProfiler..hidden(true),
+              _recordingInstructions,
+              _recordingStatus..hidden(true)
+            ]),
+        ]),
+    ]);
+
+    maybeAddDebugMessage(framework, performanceScreenId);
+
+    return screenDiv;
+  }
+
+  void _initContent() {
+    _startRecordingButton = PButton.icon('Record', recordPrimary)
+      ..small()
+      ..primary()
+      ..click(() async => await _startRecording());
+
+    _stopRecordingButton = PButton.icon('Stop', stop)
+      ..small()
+      ..clazz('margin-left')
+      ..disabled = true
+      ..click(() async => await _stopRecording());
+
+    _clearButton = PButton.icon('Clear', clearIcon)
+      ..small()
+      ..clazz('margin-left')
+      ..setAttribute('title', 'Clear timeline')
+      ..click(_clear);
+
+    _profileGranularitySelector = ProfileGranularitySelector(framework);
+
+    _recordingInstructions = createRecordingInstructions(
+        recordingGoal: 'to start recording a CPU profile.');
+
+    _recordingStatus = div(c: 'center-in-parent')
+      ..layoutVertical()
+      ..flex()
+      ..add([
+        _recordingStatusMessage = div(c: 'recording-status-message'),
+        Spinner.centered(classes: ['recording-spinner']),
+      ]);
+
+    _cpuProfiler = _CpuProfiler(
+      _performanceController,
+      () => _performanceController.cpuProfileData,
+    );
+
+    _tabNav = CpuProfilerTabNav(
+      _cpuProfiler,
+      CpuProfilerTabOrder(
+        first: CpuProfilerViewType.callTree,
+        second: CpuProfilerViewType.bottomUp,
+        third: CpuProfilerViewType.flameChart,
+      ),
+    );
+  }
+
+  @override
+  void entering() {
+    _profileGranularitySelector.setGranularity();
+  }
+
+  Future<void> _startRecording() async {
+    await _performanceController.startRecording();
+    _updateCpuProfilerVisibility(hidden: true);
+    _updateButtonStates();
+    _recordingInstructions.hidden(true);
+    _recordingStatusMessage.text = 'Recording profile';
+    _recordingStatus.hidden(false);
+  }
+
+  Future<void> _stopRecording() async {
+    _recordingStatusMessage.text = 'Processing profile';
+    await _performanceController.stopRecording();
+    _recordingStatus.hidden(true);
+    _updateCpuProfilerVisibility(hidden: false);
+    _updateButtonStates();
+    await _cpuProfiler.update();
+  }
+
+  void _clear() {
+    _performanceController.reset();
+    _updateCpuProfilerVisibility(hidden: true);
+    _recordingInstructions.hidden(false);
+  }
+
+  void _updateButtonStates() {
+    _startRecordingButton.disabled = _performanceController.recording;
+    _clearButton.disabled = _performanceController.recording;
+    _stopRecordingButton.disabled = !_performanceController.recording;
+  }
+
+  void _updateCpuProfilerVisibility({@required bool hidden}) {
+    _tabNav.element.hidden(hidden);
+    _cpuProfiler.hidden(hidden);
+  }
+}
+
+class _CpuProfiler extends CpuProfiler {
+  _CpuProfiler(
+    this._performanceController,
+    CpuProfileDataProvider profileDataProvider,
+  ) : super(
+          CpuFlameChart(profileDataProvider),
+          CpuCallTree(profileDataProvider),
+          CpuBottomUp(profileDataProvider),
+          defaultView: CpuProfilerViewType.callTree,
+        );
+
+  final PerformanceController _performanceController;
+
+  @override
+  Future<void> prepareCpuProfile() async {
+    _performanceController.cpuProfileTransformer
+        .processData(_performanceController.cpuProfileData);
+  }
+
+  @override
+  bool maybeShowMessageOnUpdate() {
+    if (_performanceController.cpuProfileData == null ||
+        _performanceController.cpuProfileData.profileMetaData.sampleCount ==
+            0) {
+      showMessage(div(text: 'No CPU samples recorded.'));
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/devtools/lib/src/popup.dart b/devtools/lib/src/popup.dart
new file mode 100644
index 0000000..71267c0
--- /dev/null
+++ b/devtools/lib/src/popup.dart
@@ -0,0 +1,644 @@
+// Copyright 2019 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 'dart:async';
+import 'dart:html' as html;
+
+import 'ui/custom.dart';
+import 'ui/elements.dart';
+
+/// Overview
+/// --------
+/// Popup with auto-complete. UI parts visible to the user consists of two parts:
+///   1. Textfield (keyboard input)
+///   2. Popup list that appears below the textfield
+///
+/// In DevTools, auto-complete is exposed as search (find) with as a magnifying
+/// glass button e.g.,
+///
+///    ----
+///   | O  |
+///   |  \ |
+///    ----
+///
+/// Clicking on the search button or using the shortcut key CTRL+f will make the
+/// textfield visible and set focus, to it, for keyboard input. Directly below
+/// the textfield a popup list will appear displaying all possible values (e.g.,
+/// list of all classes in the user's application after a memory snapshot).
+///
+/// Typing characters, in the textfield, filters the contents of the popup list
+/// to only show entries that match the characters typed.  The matching
+/// characters are bolded for each entry in the filtered list. Special chars:
+///   - page up/down and arrow up/down - navigate up/down the list.
+///   - ESC - cancel autocomplete
+///   - ENTER - process the selected item in the popup list.
+///   - click - process the item clicked on in the popup list.
+///
+/// Using
+/// -----
+/// To create an auto-complete you'll need to:
+///
+///   1. create a textfield e.g.,
+///
+///         textField = CoreElement('input', classes: 'auto-text')
+///
+///   2. create a PopupListView to hold your list of items to filter e.g.,
+///
+///         heapPopupList = PopupListView<String>();
+///
+///   3. create a PopupAutoCompleteView this class manages the matcher, binding
+///      the textfield to the popup list and visibility of the textfield and
+///      popup list (calling show or hide) e.g.,
+///
+///       popupAutoComplete = PopupAutoCompleteView(
+///         heapPopupList,    // popuplist used to populate
+///         screenDiv,        // container to display popup (parent)
+///         vmSearchField,    // textfield
+///         _searchForClass,  // Callback when user selects item in autocomplete
+///       );
+///
+///       popupAutoComplete.show();
+///
+/// Priming the data
+/// ----------------
+///     if (heapPopupList.isEmpty) {
+///       // Only fetch if data has changed.
+///       heapPopupList.setList(allItemsKnown());
+///     }
+///
+///     if (!textField.isVisible) {
+///       textField.element.style.visibility = 'visible';
+///       textField.element.focus();
+///
+///       popupAutoComplete.show();
+///     } else {
+///       heapPopup.matcher.finish(false); // Cancel the popup auto-complete
+///                                        // finish is _searchForClass callback
+///     }
+///   }
+///
+/// Processing popup selection
+/// --------------------------
+///   // Finish callback from search class selected (auto-complete).
+///   void _searchForClass([bool cancel]) {
+///     if (cancel) {
+///       popupAutoComplete.matcher.reset();
+///       heapPopupList.reset();
+///     } else {
+///       // Highlighted class is the class to select.
+///       final String classSelected = heapPopupList.highlightedItem;
+///
+///       final List<ClassHeapDetailStats> classesData = tableStack.first.data;
+///       int row = 0;
+///       for (ClassHeapDetailStats stat in classesData) {
+///         if (stat.classRef.name == classSelected) {
+///           tableStack.first.selectByIndex(row, scrollBehavior: 'auto');
+///         }
+///         row++;
+///       }
+///     }
+///
+///     // Done with the popup.
+///     popupAutoComplete.hide();
+///   }
+
+enum ListDirection {
+  pageUp,
+  pageDown,
+  home,
+  end,
+}
+
+/// Keycode definitions.
+const int DOM_VK_RETURN = 13;
+const int DOM_VK_ESCAPE = 27;
+const int DOM_VK_PAGE_UP = 33;
+const int DOM_VK_PAGE_DOWN = 34;
+const int DOM_VK_END = 35;
+const int DOM_VK_HOME = 36;
+const int DOM_VK_UP = 38;
+const int DOM_VK_DOWN = 40;
+
+typedef FinishFunction = void Function([bool cancel]);
+
+// This is the class that has the entire list to display for auto-complete.
+class PopupListView<T> implements CoreElementView {
+  PopupListView() {
+    items = SelectableList<T>()
+      ..flex()
+      ..clazz('popup-items-list');
+    items.setRenderer((T item) {
+      // Renderer for the list show matching characters.
+      final String name = item.toString();
+
+      // Case insensitive matching.
+      final matchingName = name.toLowerCase();
+
+      CoreElement element;
+      if (_popupAutoCompleteView.matcher != null &&
+          _popupAutoCompleteView.matcher.active) {
+        // InputElement's need to fetch the value not text/textContent property.
+        // The value and text are different, all nodes have a text. It the text
+        // content of the node itself along with its descendants. However, input
+        // elements have a value property - its the input data of the input
+        // element. Input elements may have a text/textContent but it is always
+        // empty because they are void elements.
+        final html.InputElement inputElement = _popupAutoCompleteView
+            .matcher.textField.element as html.InputElement;
+        // Case insensitive matching.
+        final String matchPart = inputElement.value.toLowerCase();
+
+        // Compute the matched characters to be bolded.
+        final int startIndex = matchingName.lastIndexOf(matchPart);
+        final String firstPart = name.substring(0, startIndex);
+        final int endBoldIndex = startIndex + matchPart.length;
+        final String boldPart = name.substring(startIndex, endBoldIndex);
+        final String endPart = name.substring(endBoldIndex);
+
+        // Construct the HTML with the bold tag and ensure that the HTML
+        // constructed is safe from attacks e.g., XSS, etc.
+        final String safeElement = html.Element.html(
+                '<div>$firstPart<strong class="strong-match">$boldPart</strong>$endPart</div>')
+            .innerHtml;
+        element = li(html: safeElement, c: 'list-item');
+      } else {
+        element = li(text: name, c: 'list-item');
+      }
+
+      return element;
+    });
+  }
+
+  PopupAutoCompleteView _popupAutoCompleteView;
+
+  set setPopupAutoCompleteView(PopupAutoCompleteView pacView) {
+    _popupAutoCompleteView = pacView;
+  }
+
+  void reset() {
+    highlightedItem = null;
+  }
+
+  void scrollAndHighlight(
+    int row,
+    int topPosition, {
+    bool top = false,
+    bool bottom = false,
+  }) {
+    // TODO(terry): this fixed a RangeError, but investigate why this method is
+    // called when the list is empty.
+    if (itemsAsList.isEmpty) return;
+
+    // Highlight this row.
+    highlightedItem = itemsAsList[row];
+
+    final CoreElement newElement = items.renderer(highlightedItem);
+
+    items.setReplace(row, highlightedItem);
+
+    if (topPosition != -1) element.scrollTop = topPosition;
+
+    newElement?.scrollIntoView(top: top, bottom: bottom);
+  }
+
+  /// Returns the row number of item to make visible.
+  int page(ListDirection direction, [int startRow = 0]) {
+    final int listHeight = element.element.clientHeight;
+    final int itemHeight = items.element.children[0].clientHeight;
+    final int itemsVis = (listHeight / itemHeight).truncate() - 1;
+
+    int childToScrollTo;
+    switch (direction) {
+      case ListDirection.pageDown:
+        int itemIndex = startRow + itemsVis;
+        if (itemIndex > items.items.length - 1) {
+          itemIndex = items.items.length - 1;
+        }
+        childToScrollTo = itemIndex;
+        final int scrollPosition = startRow > 0 ? startRow * itemHeight : 0;
+        scrollAndHighlight(childToScrollTo, scrollPosition, top: true);
+        break;
+      case ListDirection.pageUp:
+        int itemIndex = startRow - itemsVis;
+        if (itemIndex < 0) itemIndex = 0;
+        childToScrollTo = itemIndex;
+        final int scrollPosition =
+            childToScrollTo > 0 ? childToScrollTo * itemHeight : 0;
+        scrollAndHighlight(childToScrollTo, scrollPosition, top: true);
+        break;
+      case ListDirection.home:
+        childToScrollTo = 0;
+        scrollAndHighlight(childToScrollTo, childToScrollTo);
+        break;
+      case ListDirection.end:
+        childToScrollTo = items.items.length - 1;
+        final int scrollPosition =
+            childToScrollTo > 0 ? (childToScrollTo - itemsVis) * itemHeight : 0;
+        scrollAndHighlight(childToScrollTo, scrollPosition);
+        break;
+    }
+
+    return childToScrollTo;
+  }
+
+  SelectableList<T> items;
+  T highlightedItem;
+
+  List<T> get itemsAsList => items.items;
+
+  bool get isEmpty => items.items.isEmpty;
+
+  @override
+  CoreElement get element => items;
+
+  bool get itemsHadClicked => items.hadClicked;
+
+  Stream<html.KeyboardEvent> get onKeyDown => items.onKeyDown;
+
+  Stream<T> get onSelectionChanged => items.onSelectionChanged;
+
+  Stream<void> get onScriptsChanged => items.onItemsChanged;
+
+  void setList(
+    List<T> theItems, {
+    T select,
+  }) {
+    theItems.sort((T item1, T item2) {
+      return item1.toString().compareTo(item2.toString());
+    });
+
+    T selection;
+    if (select != null) {
+      selection = select;
+    }
+
+    items.setItems(theItems,
+        selection: selection, scrollSelectionIntoView: true);
+  }
+
+  void clearList() => items.clearItems();
+}
+
+// View manages Input element (popupTextField) / popup list displayed _listView.
+// show() displays popup list directly below the Input element (popupTextfield).
+class PopupAutoCompleteView extends CoreElement {
+  PopupAutoCompleteView(
+    this._listView,
+    this._containerElement,
+    this._popupTextfield,
+    this._completeAction,
+  ) : super('div', classes: 'popup-view') {
+    _initialize();
+  }
+
+  // Mimic used when the textField should mimic another field's background-color
+  // and color.
+  PopupAutoCompleteView.mimic(
+    this._listView,
+    this._containerElement,
+    this._popupTextfield,
+    this._completeAction,
+    this._elementToMimic,
+  ) : super('div', classes: 'popup-view') {
+    _initialize();
+  }
+
+  void _initialize() {
+    // Setup backpointer from PopupListView to PopupAutoCompleteView (to access
+    // the AutoCompleteMatcher _matcher that's created here.
+    _listView.setPopupAutoCompleteView = this;
+
+    // Hookup listener for selection changes in the popup list (clicking an item
+    // in the list).
+    _hookupListeners();
+
+    // Hookup focus/blur/keyboard events in the textfield.
+    _popupTextfield
+      ..focus(() {
+        // Activate popup auto-complete.
+        _matcher ??= AutoCompleteMatcher();
+        if (!matcher.active) {
+          matcher.start('', _listView, _popupTextfield, _completeAction);
+        }
+      })
+      ..blur(() {
+        Timer(const Duration(milliseconds: 200),
+            () => matcher?.finish(true)); // Hide/clear the popup.
+      })
+      ..onKeyUp.listen((html.KeyboardEvent e) {
+        switch (e.keyCode) {
+          case DOM_VK_RETURN:
+          case DOM_VK_ESCAPE:
+          case DOM_VK_PAGE_UP:
+          case DOM_VK_PAGE_DOWN:
+          case DOM_VK_END:
+          case DOM_VK_HOME:
+          case DOM_VK_UP:
+          case DOM_VK_DOWN:
+            return;
+          default:
+            if (e.ctrlKey || e.key == 'Control') {
+              // CTRL key is down (a shortcut key) - this isn't for the matcher.
+              e.preventDefault();
+            } else {
+              final html.InputElement inputElement = _popupTextfield.element;
+              final String value = inputElement.value.trim();
+              matcher.displayMatchingItems(value);
+            }
+        }
+      });
+  }
+
+  // View of all items to display during auto-complete, this list will be pruned
+  // during auto-complete matching.
+  final PopupListView _listView;
+
+  // Container to display input element that accepts keyboard input during auto-
+  // complete and the _listView (popup) is displayed in this container too.
+  final CoreElement
+      _containerElement; // Top div area container of _sourcePathDiv
+
+  // When creating or making visible the input element use this element to mimic
+  // the auto-complete input element background color and text color.
+  CoreElement _elementToMimic;
+
+  // Input element field to display while popup is active for keyboard input and
+  // _listView navigation (page up/down, arrow up/down, escape, etc.)
+  final CoreElement _popupTextfield;
+
+  // This is where all the incremental filter is done.
+  AutoCompleteMatcher get matcher => _matcher;
+  AutoCompleteMatcher _matcher;
+
+  // Callback to user code to process an item selected (click or ENTER to
+  // process the selected item).
+  final FinishFunction _completeAction;
+
+  CoreElement get popupTextfield => _popupTextfield;
+
+  bool get isPoppedUp => _poppedUp;
+  bool _poppedUp = false;
+
+  /// Handle explicit clicking in the popupList.
+  void _hookupListeners() {
+    _listView.onSelectionChanged.listen((classSelected) async {
+      if (_listView.itemsHadClicked && matcher != null && matcher.active) {
+        // User clicked in the list while matcher was active.
+        if (_listView.itemsHadClicked) {
+          _listView.highlightedItem = classSelected;
+          matcher?.finish(false);
+        }
+
+        matcher.reset();
+      }
+    });
+  }
+
+  void show() {
+    _poppedUp = true;
+
+    add(_listView);
+
+    _matcher.selectFirstItem();
+
+    final html.Rectangle r = _containerElement.element.getBoundingClientRect();
+
+    int nameHeight;
+    if (_elementToMimic == null) {
+      nameHeight =
+          _popupTextfield.element.getBoundingClientRect().height.round();
+    } else {
+      nameHeight =
+          _elementToMimic.element.getBoundingClientRect().height.round();
+    }
+
+    final textFieldClientRect = _popupTextfield.element.getBoundingClientRect();
+    final leftPosition = (textFieldClientRect.left).round();
+    element.style
+      ..top = '${r.top + nameHeight}px'
+      ..left = '${leftPosition}px'
+      ..display = 'inline';
+
+    matcher.displayMatchingItems('');
+  }
+
+  void hide() {
+    element.style.display = 'none'; // Hide PopupView
+
+    // Hide textField and reset it's value.
+    final html.InputElement inputElement = _popupTextfield.element;
+    inputElement.value = '';
+    inputElement.style.visibility = 'hidden';
+
+    _poppedUp = false;
+  }
+
+  PopupListView get listView => _listView;
+}
+
+/// This class handles all the incremental matching as keys are types as well as
+/// navigation through the popup list e.g., pageUp, arrow up/down, etc.
+class AutoCompleteMatcher<T> {
+  AutoCompleteMatcher();
+
+  PopupListView get listView => _listView;
+  PopupListView _listView;
+
+  CoreElement get textField => _textField;
+  CoreElement _textField; // Input element for keyboard input.
+
+  T _original;
+
+  int _originalScrollTop;
+
+  Map<String, List<T>> matchingState = {};
+
+  String _lastMatchingChars;
+
+  String get lastMatchingChars => _lastMatchingChars;
+
+  // Current Row via matching and navigation (up/down ARROW, up/down PAGE, HOME
+  // and END.
+  int _selectRow = -1;
+
+  StreamSubscription _subscription;
+
+  bool get active => _subscription != null;
+
+  FinishFunction _finishCallback;
+
+  void finish([bool cancel = false]) {
+    if (_finishCallback != null) _finishCallback(cancel);
+  }
+
+  void start(T revert, PopupListView<T> listView, CoreElement textfield,
+      [FinishFunction finishCallback]) {
+    _listView = listView;
+    _textField = textfield;
+
+    if (finishCallback != null) _finishCallback = finishCallback;
+
+    _startMatching(revert, true);
+
+    // Start handling user's keystrokes to show matching list of files.
+    _subscription ??= _textField.onKeyDown.listen((html.KeyboardEvent e) {
+      bool preventDefault = true;
+      switch (e.keyCode) {
+        case DOM_VK_RETURN:
+          finish();
+          reset();
+          _listView.reset();
+          break;
+        case DOM_VK_ESCAPE:
+          cancel();
+          preventDefault = false;
+          break;
+        case DOM_VK_PAGE_UP:
+          _selectRow = _listView.page(ListDirection.pageUp, _selectRow);
+          break;
+        case DOM_VK_PAGE_DOWN:
+          _selectRow = _listView.page(ListDirection.pageDown, _selectRow);
+          break;
+        case DOM_VK_END:
+          _selectRow = _listView.page(ListDirection.end);
+          break;
+        case DOM_VK_HOME:
+          _selectRow = _listView.page(ListDirection.home);
+          break;
+        case DOM_VK_UP:
+          // Set selection one item up.
+          if (_selectRow > 0) {
+            _selectRow -= 1;
+            _listView.scrollAndHighlight(_selectRow, -1);
+          }
+          break;
+        case DOM_VK_DOWN:
+          // Set selection one item down.
+          if (_selectRow < listView.itemsAsList.length - 1) {
+            _selectRow += 1;
+            _listView.scrollAndHighlight(_selectRow, -1);
+          }
+          break;
+        default:
+          // All other keys do normal processing.
+          preventDefault = false;
+      }
+      if (preventDefault) e.preventDefault();
+    });
+  }
+
+  void cancel() {
+    revert();
+    finish();
+  }
+
+  void selectFirstItem() {
+    _selectRow = 0;
+    _listView.scrollAndHighlight(_selectRow, -1);
+  }
+
+  // Finished matching - throw away all matching states.
+  void reset() {
+    String selected;
+
+    if (listView.items.hadClicked) {
+      // Matcher was active but user clicked.  So remember the item clicked on -
+      // is the currently selected.
+      selected = listView.items.selectedItem();
+    } else {
+      // Use the item we've highlighted from match navigation.
+      selected = listView.highlightedItem;
+    }
+
+    if (_subscription != null) {
+      // No more event routing until user has clicked again the the textField.
+      _subscription.cancel();
+      _subscription = null;
+    }
+
+    // Remember the whole set of items
+    final List<T> originals = matchingState[''];
+
+    _listView.setList(
+      originals,
+      select: selected,
+    );
+
+    // Lose all other intermediate matches - we're done.
+    matchingState.clear();
+    matchingState.putIfAbsent('', () => originals);
+
+    (_textField.element as html.InputElement).value = '';
+
+    listView.highlightedItem = null;
+
+    _selectRow = -1;
+  }
+
+  int rowPosition(int row) {
+    final int itemHeight = listView.items.element.children[0].clientHeight;
+    return row * itemHeight;
+  }
+
+  /// Revert list and selection back to before the matcher (first click in the
+  /// textField).
+  void revert() {
+    reset();
+    _listView.setList(
+      matchingState[''],
+      select: _original,
+    );
+
+    if (_original != null) {
+      if (listView.items.selectedItem() != null) {
+        listView.element.scrollTop = _originalScrollTop;
+      }
+    }
+  }
+
+  void _startMatching(T original, [bool initialize = false]) {
+    _original = original;
+    _originalScrollTop = _listView.element.scrollTop;
+
+    final html.InputElement element = _textField.element;
+    if (initialize || element.value.isEmpty) {
+      // Save all the scripts.
+      matchingState.putIfAbsent('', () => listView.itemsAsList);
+    }
+  }
+
+  /// Show the list of files matching the set of keystrokes typed.
+  void displayMatchingItems(String charsToMatch) {
+    String previousMatch = '';
+
+    final charsMatchLen = charsToMatch.length;
+    if (charsMatchLen > 0) {
+      previousMatch = charsToMatch.substring(0, charsMatchLen - 1);
+    }
+
+    List<T> lastMatchingItems = matchingState[previousMatch];
+    lastMatchingItems ??= matchingState[''];
+
+    final List<T> matchingItems = lastMatchingItems
+        .where((T item) =>
+            item
+                .toString()
+                // Case insensitive matching.
+                .toLowerCase()
+                .lastIndexOf('${charsToMatch.toLowerCase()}') >=
+            0)
+        .toList();
+
+    matchingState.putIfAbsent(charsToMatch, () => matchingItems);
+
+    listView.clearList();
+    listView.setList(matchingItems);
+
+    selectFirstItem();
+
+    listView.items.scrollTop = 0;
+
+    _lastMatchingChars = charsToMatch;
+  }
+}
diff --git a/devtools/lib/src/profiler/cpu_profile_flame_chart.dart b/devtools/lib/src/profiler/cpu_profile_flame_chart.dart
new file mode 100644
index 0000000..b478e98
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profile_flame_chart.dart
@@ -0,0 +1,204 @@
+// Copyright 2019 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 'dart:math' as math;
+
+import 'package:meta/meta.dart';
+
+import '../charts/flame_chart_canvas.dart';
+import '../ui/colors.dart';
+import '../ui/elements.dart';
+import '../ui/fake_flutter/dart_ui/dart_ui.dart';
+import '../ui/fake_flutter/fake_flutter.dart';
+import '../ui/flutter_html_shim.dart';
+import '../ui/theme.dart';
+import 'cpu_profile_model.dart';
+import 'cpu_profiler.dart';
+
+class CpuFlameChart extends CpuProfilerView {
+  CpuFlameChart(CpuProfileDataProvider profileDataProvider)
+      : super(CpuProfilerViewType.flameChart, profileDataProvider) {
+    stackFrameDetails = div(c: 'event-details-heading stack-frame-details')
+      ..element.style.backgroundColor = colorToCss(stackFrameDetailsBackground)
+      ..hidden(true);
+
+    add(stackFrameDetails);
+  }
+
+  static const String stackFrameDetailsDefaultText =
+      '[No stack frame selected]';
+
+  static const stackFrameDetailsBackground = ThemedColor(
+    Color(0xFFF6F6F6),
+    Color(0xFF202124),
+  );
+
+  CpuFlameChartCanvas canvas;
+
+  CoreElement stackFrameDetails;
+
+  @override
+  void rebuildView() {
+    final CpuProfileData data = profileDataProvider();
+    canvas = CpuFlameChartCanvas(
+      data: data,
+      width: element.clientWidth.toDouble(),
+      height: math.max(
+        // Subtract [rowHeightWithPadding] to account for timeline at the top of
+        // the flame chart.
+        element.clientHeight - rowHeightWithPadding,
+        // Add 1 to account for a row of padding at the bottom of the chart.
+        (data.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
+      ),
+    );
+
+    canvas.onNodeSelected.listen((node) {
+      assert(node.data is CpuStackFrame);
+      stackFrameDetails.text = node.data.toString();
+    });
+
+    add(canvas.element);
+
+    stackFrameDetails
+      ..text = stackFrameDetailsDefaultText
+      ..hidden(false);
+  }
+
+  @override
+  void update({bool showLoadingSpinner = false}) {
+    reset();
+    super.update(showLoadingSpinner: showLoadingSpinner);
+  }
+
+  void updateForContainerResize() {
+    if (canvas == null) {
+      return;
+    }
+
+    final data = profileDataProvider();
+
+    // Only update the canvas if the flame chart is visible and has data.
+    // Otherwise, mark the canvas as needing a rebuild.
+    if (!isHidden && data != null) {
+      // We need to rebuild the canvas with a new content size so that the
+      // canvas is always at least as tall as the container it is in. This
+      // ensures that the grid lines in the chart will extend all the way to the
+      // bottom of the container.
+      canvas.forceRebuildForSize(
+        canvas.widthWithInsets,
+        math.max(
+          // Subtract [rowHeightWithPadding] to account for the size of
+          // [stackFrameDetails] section at the bottom of the chart.
+          element.scrollHeight.toDouble() - rowHeightWithPadding,
+          // Add 1 to account for a row of padding at the bottom of the chart.
+          (data.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
+        ),
+      );
+    } else {
+      viewNeedsRebuild = true;
+    }
+  }
+
+  @override
+  void reset() {
+    if (canvas?.element?.element != null) {
+      canvas.element.element.remove();
+    }
+    canvas = null;
+
+    stackFrameDetails.text = stackFrameDetailsDefaultText;
+    stackFrameDetails.hidden(true);
+  }
+}
+
+class CpuFlameChartCanvas extends FlameChartCanvas<CpuProfileData> {
+  CpuFlameChartCanvas({
+    @required CpuProfileData data,
+    @required double width,
+    @required double height,
+  }) : super(
+          data: data,
+          duration: data.profileMetaData.time.duration,
+          width: width,
+          height: height,
+          classes: 'cpu-flame-chart',
+        );
+
+  static const stackFramePadding = 1;
+
+  int _colorOffset = 0;
+
+  @override
+  double get calculatedWidth => rows[0].nodes[0].rect.right - sideInset;
+
+  @override
+  void initRows() {
+    for (int i = 0; i < data.cpuProfileRoot.depth; i++) {
+      rows.add(FlameChartRow(nodes: [], index: i));
+    }
+
+    final totalWidth = width - 2 * sideInset;
+
+    final Map<String, double> stackFrameLefts = {};
+
+    double leftForStackFrame(CpuStackFrame stackFrame) {
+      final CpuStackFrame parent = stackFrame.parent;
+      double left;
+      if (parent == null) {
+        left = sideInset;
+      } else {
+        final stackFrameIndex = stackFrame.index;
+        if (stackFrameIndex == 0) {
+          // This is the first child of parent. [left] should equal the left
+          // value of [stackFrame]'s parent.
+          left = stackFrameLefts[parent.id];
+        } else {
+          assert(stackFrameIndex != -1);
+          // [stackFrame] is not the first child of its parent. [left] should
+          // equal the right value of its previous sibling.
+          final CpuStackFrame previous = parent.children[stackFrameIndex - 1];
+          left = stackFrameLefts[previous.id] +
+              (totalWidth * previous.totalTimeRatio);
+        }
+      }
+      stackFrameLefts[stackFrame.id] = left;
+      return left;
+    }
+
+    void createChartNodes(CpuStackFrame stackFrame, int row) {
+      final double width =
+          totalWidth * stackFrame.totalTimeRatio - stackFramePadding;
+      final left = leftForStackFrame(stackFrame);
+      final top = row * rowHeightWithPadding + topOffset;
+      final backgroundColor = _colorForStackFrame(stackFrame);
+
+      final node = FlameChartNode<CpuStackFrame>(
+        Rect.fromLTRB(left, top, left + width, top + rowHeight),
+        backgroundColor,
+        Colors.black,
+        Colors.black,
+        stackFrame,
+        (_) => stackFrame.name,
+      );
+
+      rows[row].nodes.add(node);
+
+      for (CpuStackFrame child in stackFrame.children) {
+        createChartNodes(
+          child,
+          row + 1,
+        );
+      }
+    }
+
+    createChartNodes(data.cpuProfileRoot, 0);
+  }
+
+  // TODO(kenzie): base colors on categories (Widget, Render, Layer, User code,
+  // etc.)
+  Color _colorForStackFrame(CpuStackFrame stackFrame) {
+    final color = uiColorPalette[_colorOffset % uiColorPalette.length];
+    _colorOffset++;
+    return color;
+  }
+}
diff --git a/devtools/lib/src/profiler/cpu_profile_model.dart b/devtools/lib/src/profiler/cpu_profile_model.dart
new file mode 100644
index 0000000..34e5b38
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profile_model.dart
@@ -0,0 +1,305 @@
+// Copyright 2019 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 'dart:collection';
+import 'dart:convert';
+
+import 'package:meta/meta.dart';
+
+import '../timeline/timeline_model.dart';
+import '../trees.dart';
+import '../utils.dart';
+
+/// Data model for DevTools CPU profile.
+class CpuProfileData {
+  CpuProfileData._({
+    @required this.stackFramesJson,
+    @required this.stackTraceEvents,
+    @required this.profileMetaData,
+  }) {
+    _cpuProfileRoot = CpuStackFrame(
+      id: 'cpuProfile',
+      name: 'all',
+      category: 'Dart',
+      url: '',
+      profileMetaData: profileMetaData,
+    );
+  }
+
+  static CpuProfileData parse(Map<String, dynamic> json) {
+    return CpuProfileData._(
+      stackFramesJson: jsonDecode(jsonEncode(json[stackFramesKey] ?? {})),
+      stackTraceEvents:
+          (json[traceEventsKey] ?? []).cast<Map<String, dynamic>>(),
+      profileMetaData: CpuProfileMetaData(
+        sampleCount: json[sampleCountKey],
+        samplePeriod: json[samplePeriodKey],
+        stackDepth: json[stackDepthKey],
+        time: (json[timeOriginKey] != null && json[timeExtentKey] != null)
+            ? (TimeRange()
+              ..start = Duration(microseconds: json[timeOriginKey])
+              ..end = Duration(
+                  microseconds: json[timeOriginKey] + json[timeExtentKey]))
+            : null,
+      ),
+    );
+  }
+
+  static CpuProfileData subProfile(
+    CpuProfileData superProfile,
+    TimeRange subTimeRange,
+  ) {
+    // Each trace event in [subTraceEvents] will have the leaf stack frame id
+    // for a cpu sample within [subTimeRange].
+    final subTraceEvents = superProfile.stackTraceEvents
+        .where((trace) => subTimeRange
+            .contains(Duration(microseconds: trace[TraceEvent.timestampKey])))
+        .toList();
+
+    // Use a SplayTreeMap so that map iteration will be in sorted key order.
+    final SplayTreeMap<String, Map<String, dynamic>> subStackFramesJson =
+        SplayTreeMap(stackFrameIdCompare);
+    for (Map<String, dynamic> traceEvent in subTraceEvents) {
+      // Add leaf frame.
+      final String leafId = traceEvent[stackFrameIdKey];
+      final Map<String, dynamic> leafFrameJson =
+          superProfile.stackFramesJson[leafId];
+      subStackFramesJson[leafId] = leafFrameJson;
+
+      // Add leaf frame's ancestors.
+      String parentId = leafFrameJson[parentIdKey];
+      while (parentId != null) {
+        final parentFrameJson = superProfile.stackFramesJson[parentId];
+        subStackFramesJson[parentId] = parentFrameJson;
+        parentId = parentFrameJson[parentIdKey];
+      }
+    }
+
+    return CpuProfileData._(
+      stackFramesJson: subStackFramesJson,
+      stackTraceEvents: subTraceEvents,
+      profileMetaData: CpuProfileMetaData(
+        sampleCount: subTraceEvents.length,
+        samplePeriod: superProfile.profileMetaData.samplePeriod,
+        stackDepth: superProfile.profileMetaData.stackDepth,
+        time: subTimeRange,
+      ),
+    );
+  }
+
+  // Key fields from the VM response JSON.
+  static const nameKey = 'name';
+  static const categoryKey = 'category';
+  static const parentIdKey = 'parent';
+  static const stackFrameIdKey = 'sf';
+  static const resolvedUrlKey = 'resolvedUrl';
+  static const stackFramesKey = 'stackFrames';
+  static const traceEventsKey = 'traceEvents';
+  static const sampleCountKey = 'sampleCount';
+  static const stackDepthKey = 'stackDepth';
+  static const samplePeriodKey = 'samplePeriod';
+  static const timeOriginKey = 'timeOriginMicros';
+  static const timeExtentKey = 'timeExtentMicros';
+
+  /// Marks whether this data has already been processed.
+  bool processed = false;
+
+  final Map<String, dynamic> stackFramesJson;
+
+  /// Trace events associated with the last stackFrame in each sample (i.e. the
+  /// leaves of the [CpuStackFrame] objects).
+  ///
+  /// The trace event will contain a field 'sf' that contains the id of the leaf
+  /// stack frame.
+  final List<Map<String, dynamic>> stackTraceEvents;
+
+  final CpuProfileMetaData profileMetaData;
+
+  CpuStackFrame get cpuProfileRoot => _cpuProfileRoot;
+
+  CpuStackFrame _cpuProfileRoot;
+
+  Map<String, CpuStackFrame> stackFrames = {};
+
+  Map<String, dynamic> get json => {
+        'type': '_CpuProfileTimeline',
+        samplePeriodKey: profileMetaData.samplePeriod,
+        sampleCountKey: profileMetaData.sampleCount,
+        stackDepthKey: profileMetaData.stackDepth,
+        timeOriginKey: profileMetaData.time.start.inMicroseconds,
+        timeExtentKey: profileMetaData.time.duration.inMicroseconds,
+        stackFramesKey: stackFramesJson,
+        traceEventsKey: stackTraceEvents,
+      };
+}
+
+class CpuProfileMetaData {
+  CpuProfileMetaData({
+    @required this.sampleCount,
+    @required this.samplePeriod,
+    @required this.stackDepth,
+    @required this.time,
+  });
+
+  final int sampleCount;
+
+  final int samplePeriod;
+
+  final int stackDepth;
+
+  final TimeRange time;
+}
+
+class CpuStackFrame extends TreeNode<CpuStackFrame> {
+  CpuStackFrame({
+    @required this.id,
+    @required this.name,
+    @required this.category,
+    @required this.url,
+    @required this.profileMetaData,
+  });
+
+  final String id;
+
+  final String name;
+
+  final String category;
+
+  final String url;
+
+  final CpuProfileMetaData profileMetaData;
+
+  /// How many cpu samples for which this frame is a leaf.
+  int exclusiveSampleCount = 0;
+
+  int get inclusiveSampleCount =>
+      _inclusiveSampleCount ?? _calculateInclusiveSampleCount();
+
+  /// How many cpu samples this frame is included in.
+  int _inclusiveSampleCount;
+  set inclusiveSampleCount(int count) => _inclusiveSampleCount = count;
+
+  double get totalTimeRatio =>
+      _totalTimeRatio ??= inclusiveSampleCount / profileMetaData.sampleCount;
+
+  double _totalTimeRatio;
+
+  Duration get totalTime => _totalTime ??= Duration(
+      microseconds:
+          (totalTimeRatio * profileMetaData.time.duration.inMicroseconds)
+              .round());
+
+  Duration _totalTime;
+
+  double get selfTimeRatio =>
+      _selfTimeRatio ??= exclusiveSampleCount / profileMetaData.sampleCount;
+
+  double _selfTimeRatio;
+
+  Duration get selfTime => _selfTime ??= Duration(
+      microseconds:
+          (selfTimeRatio * profileMetaData.time.duration.inMicroseconds)
+              .round());
+
+  Duration _selfTime;
+
+  /// Returns the number of cpu samples this stack frame is a part of.
+  ///
+  /// This will be equal to the number of leaf nodes under this stack frame.
+  int _calculateInclusiveSampleCount() {
+    int count = exclusiveSampleCount;
+    for (CpuStackFrame child in children) {
+      count += child.inclusiveSampleCount;
+    }
+    _inclusiveSampleCount = count;
+    return _inclusiveSampleCount;
+  }
+
+  CpuStackFrame shallowCopy({bool resetInclusiveSampleCount = false}) {
+    final copy = CpuStackFrame(
+      id: id,
+      name: name,
+      category: category,
+      url: url,
+      profileMetaData: profileMetaData,
+    )
+      ..exclusiveSampleCount = exclusiveSampleCount
+      ..inclusiveSampleCount =
+          resetInclusiveSampleCount ? null : inclusiveSampleCount;
+    return copy;
+  }
+
+  /// Returns a deep copy from this stack frame down to the leaves of the tree.
+  ///
+  /// The returned copy stack frame will have a null parent.
+  CpuStackFrame deepCopy() {
+    final copy = shallowCopy();
+    for (CpuStackFrame child in children) {
+      copy.addChild(child.deepCopy());
+    }
+    return copy;
+  }
+
+  /// Whether [this] stack frame matches another stack frame [other].
+  ///
+  /// Two stack frames are said to be matching if they share the following
+  /// properties.
+  bool matches(CpuStackFrame other) =>
+      name == other.name && url == other.url && category == other.category;
+
+  void _format(StringBuffer buf, String indent) {
+    buf.writeln('$indent$name - children: ${children.length} - excl: '
+            '$exclusiveSampleCount - incl: $inclusiveSampleCount'
+        .trimRight());
+    for (CpuStackFrame child in children) {
+      child._format(buf, '  $indent');
+    }
+  }
+
+  @visibleForTesting
+  String toStringDeep() {
+    final buf = StringBuffer();
+    _format(buf, '  ');
+    return buf.toString();
+  }
+
+  @override
+  String toString() {
+    final buf = StringBuffer();
+    buf.write('$name ');
+    if (totalTime != null) {
+      // TODO(kenzie): use a number of fractionDigits that better matches the
+      // resolution of the stack frame.
+      buf.write('- ${msText(totalTime, fractionDigits: 2)} ');
+    }
+    buf.write('($inclusiveSampleCount ');
+    buf.write(inclusiveSampleCount == 1 ? 'sample' : 'samples');
+    buf.write(', ${percent2(totalTimeRatio)})');
+    return buf.toString();
+  }
+}
+
+@visibleForTesting
+int stackFrameIdCompare(String a, String b) {
+  // Stack frame ids are structured as 140225212960768-24 (iOS) or -784070656-24
+  // (Android). We need to compare the number after the last dash to maintain
+  // the correct order.
+  const dash = '-';
+  final aDashIndex = a.lastIndexOf(dash);
+  final bDashIndex = b.lastIndexOf(dash);
+  try {
+    final int aId = int.parse(a.substring(aDashIndex + 1));
+    final int bId = int.parse(b.substring(bDashIndex + 1));
+    return aId.compareTo(bId);
+  } catch (e) {
+    String error = 'invalid stack frame ';
+    if (aDashIndex == -1 && bDashIndex != -1) {
+      error += 'id [$a]';
+    } else if (aDashIndex != -1 && bDashIndex == -1) {
+      error += 'id [$b]';
+    } else {
+      error += 'ids [$a, $b]';
+    }
+    throw error;
+  }
+}
diff --git a/devtools/lib/src/profiler/cpu_profile_service.dart b/devtools/lib/src/profiler/cpu_profile_service.dart
new file mode 100644
index 0000000..18a7abb
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profile_service.dart
@@ -0,0 +1,35 @@
+// Copyright 2019 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:meta/meta.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../globals.dart';
+import '../profiler/cpu_profile_model.dart';
+
+/// Default period at which the VM will collect CPU samples.
+///
+/// This value is applied to the profile_period VM flag.
+const int defaultSamplePeriod = 250;
+
+/// Manages interactions between the Cpu Profiler and the VmService.
+class CpuProfilerService {
+  Future<CpuProfileData> getCpuProfile({
+    @required int startMicros,
+    @required int extentMicros,
+  }) async {
+    final Response response =
+        await serviceManager.service.getCpuProfileTimeline(
+      serviceManager.isolateManager.selectedIsolate.id,
+      startMicros,
+      extentMicros,
+    );
+    return CpuProfileData.parse(response.json);
+  }
+
+  Future<Success> clearCpuProfile() async {
+    return serviceManager.service
+        .clearCpuProfile(serviceManager.isolateManager.selectedIsolate.id);
+  }
+}
diff --git a/devtools/lib/src/profiler/cpu_profile_tables.dart b/devtools/lib/src/profiler/cpu_profile_tables.dart
new file mode 100644
index 0000000..38d62d0
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profile_tables.dart
@@ -0,0 +1,194 @@
+// Copyright 2019 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 '../table_data.dart';
+import '../tables.dart';
+import '../url_utils.dart';
+import '../utils.dart';
+import 'cpu_profile_model.dart';
+import 'cpu_profile_transformer.dart';
+import 'cpu_profiler.dart';
+
+const _timeColumnWidthPx = 145;
+
+class CpuCallTree extends CpuProfilerView {
+  CpuCallTree(CpuProfileDataProvider profileDataProvider)
+      : super(CpuProfilerViewType.callTree, profileDataProvider) {
+    flex();
+    layoutVertical();
+
+    _init();
+  }
+
+  TreeTable<CpuStackFrame> callTreeTable;
+
+  void _init() {
+    final methodNameColumn = MethodNameColumn()
+      ..onNodeExpanded
+          .listen((stackFrame) => callTreeTable.model.expandNode(stackFrame))
+      ..onNodeCollapsed
+          .listen((stackFrame) => callTreeTable.model.collapseNode(stackFrame));
+
+    callTreeTable = TreeTable<CpuStackFrame>.virtual();
+    callTreeTable.model
+      ..addColumn(TotalTimeColumn())
+      ..addColumn(SelfTimeColumn())
+      ..addColumn(methodNameColumn)
+      ..addColumn(SourceColumn());
+    callTreeTable.model
+      ..sortColumn = callTreeTable.model.columns.first
+      ..setRows(<CpuStackFrame>[]);
+    add(callTreeTable.element);
+  }
+
+  @override
+  void rebuildView() {
+    final CpuProfileData data = profileDataProvider();
+    final CpuStackFrame root = data.cpuProfileRoot.deepCopy();
+
+    // Expand the root stack frame to start.
+    final rows = <CpuStackFrame>[
+      root..expand(),
+      ...root.children.cast(),
+    ];
+    callTreeTable.model.setRows(rows);
+  }
+
+  @override
+  void reset() => callTreeTable.model.setRows(<CpuStackFrame>[]);
+}
+
+class CpuBottomUp extends CpuProfilerView {
+  CpuBottomUp(CpuProfileDataProvider profileDataProvider)
+      : super(CpuProfilerViewType.bottomUp, profileDataProvider) {
+    flex();
+    layoutVertical();
+    _init();
+  }
+
+  TreeTable<CpuStackFrame> bottomUpTable;
+
+  void _init() {
+    final methodNameColumn = MethodNameColumn()
+      ..onNodeExpanded
+          .listen((stackFrame) => bottomUpTable.model.expandNode(stackFrame))
+      ..onNodeCollapsed
+          .listen((stackFrame) => bottomUpTable.model.collapseNode(stackFrame));
+    final selfTimeColumn = SelfTimeColumn();
+
+    bottomUpTable = TreeTable<CpuStackFrame>.virtual();
+    bottomUpTable.model
+      ..addColumn(TotalTimeColumn())
+      ..addColumn(selfTimeColumn)
+      ..addColumn(methodNameColumn)
+      ..addColumn(SourceColumn())
+      ..sortColumn = selfTimeColumn
+      ..setRows(<CpuStackFrame>[]);
+    add(bottomUpTable.element);
+  }
+
+  @override
+  void rebuildView() {
+    final CpuProfileData data = profileDataProvider();
+    final List<CpuStackFrame> bottomUpRoots =
+        BottomUpProfileTransformer().processData(data.cpuProfileRoot);
+    bottomUpTable.model.setRows(bottomUpRoots);
+  }
+
+  @override
+  void reset() => bottomUpTable.model.setRows(<CpuStackFrame>[]);
+}
+
+class SelfTimeColumn extends ColumnData<CpuStackFrame> {
+  SelfTimeColumn() : super('Self Time', fixedWidthPx: _timeColumnWidthPx);
+
+  @override
+  bool get numeric => true;
+
+  @override
+  int compare(CpuStackFrame a, CpuStackFrame b) {
+    final int result = super.compare(a, b);
+    if (result == 0) {
+      return a.name.compareTo(b.name);
+    }
+    return result;
+  }
+
+  @override
+  dynamic getValue(CpuStackFrame dataObject) =>
+      dataObject.selfTime.inMicroseconds;
+
+  @override
+  String getDisplayValue(CpuStackFrame dataObject) {
+    return '${msText(dataObject.selfTime, fractionDigits: 2)} '
+        '(${percent2(dataObject.selfTimeRatio)})';
+  }
+}
+
+class TotalTimeColumn extends ColumnData<CpuStackFrame> {
+  TotalTimeColumn() : super('Total Time', fixedWidthPx: _timeColumnWidthPx);
+
+  @override
+  bool get numeric => true;
+
+  @override
+  int compare(CpuStackFrame a, CpuStackFrame b) {
+    final int result = super.compare(a, b);
+    if (result == 0) {
+      return a.name.compareTo(b.name);
+    }
+    return result;
+  }
+
+  @override
+  dynamic getValue(CpuStackFrame dataObject) =>
+      dataObject.totalTime.inMicroseconds;
+
+  @override
+  String getDisplayValue(CpuStackFrame dataObject) {
+    return '${msText(dataObject.totalTime, fractionDigits: 2)} '
+        '(${percent2(dataObject.totalTimeRatio)})';
+  }
+}
+
+class MethodNameColumn extends TreeColumnData<CpuStackFrame> {
+  MethodNameColumn() : super('Method');
+
+  static const maxMethodNameLength = 75;
+
+  @override
+  dynamic getValue(CpuStackFrame dataObject) => dataObject.name;
+
+  @override
+  String getDisplayValue(CpuStackFrame dataObject) {
+    if (dataObject.name.length > maxMethodNameLength) {
+      return dataObject.name.substring(0, maxMethodNameLength) + '...';
+    }
+    return dataObject.name;
+  }
+
+  @override
+  bool get supportsSorting => true;
+
+  @override
+  String getTooltip(CpuStackFrame dataObject) => dataObject.name;
+}
+
+// TODO(kenzie): make these urls clickable once we can jump to source.
+class SourceColumn extends ColumnData<CpuStackFrame> {
+  SourceColumn() : super('Source', alignment: ColumnAlignment.right);
+
+  @override
+  dynamic getValue(CpuStackFrame dataObject) => dataObject.url;
+
+  @override
+  dynamic getDisplayValue(CpuStackFrame dataObject) {
+    return getSimplePackageUrl(dataObject.url);
+  }
+
+  @override
+  String getTooltip(CpuStackFrame dataObject) => dataObject.url;
+
+  @override
+  bool get supportsSorting => true;
+}
diff --git a/devtools/lib/src/profiler/cpu_profile_transformer.dart b/devtools/lib/src/profiler/cpu_profile_transformer.dart
new file mode 100644
index 0000000..8004d4b
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profile_transformer.dart
@@ -0,0 +1,180 @@
+// Copyright 2019 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:meta/meta.dart';
+
+import '../utils.dart';
+import 'cpu_profile_model.dart';
+
+/// Process for composing [CpuProfileData] into a structured tree of
+/// [CpuStackFrame]'s.
+class CpuProfileTransformer {
+  void processData(CpuProfileData cpuProfileData) {
+    // Do not process this data if it has already been processed.
+    if (cpuProfileData.processed) return;
+
+    cpuProfileData.stackFramesJson.forEach((k, v) {
+      final stackFrame = CpuStackFrame(
+        id: k,
+        name: getSimpleStackFrameName(v[CpuProfileData.nameKey]),
+        category: v[CpuProfileData.categoryKey],
+        // If the user is on a version of Flutter where resolvedUrl is not
+        // included in the response, this will be null. If the frame is a native
+        // frame, the this will be the empty string.
+        url: v[CpuProfileData.resolvedUrlKey] ?? '',
+        profileMetaData: cpuProfileData.profileMetaData,
+      );
+      _processStackFrame(
+        stackFrame,
+        cpuProfileData.stackFrames[v[CpuProfileData.parentIdKey]],
+        cpuProfileData,
+      );
+    });
+    _setExclusiveSampleCounts(cpuProfileData);
+
+    cpuProfileData.processed = true;
+
+    assert(
+      cpuProfileData.profileMetaData.sampleCount ==
+          cpuProfileData.cpuProfileRoot.inclusiveSampleCount,
+      'SampleCount from response (${cpuProfileData.profileMetaData.sampleCount})'
+      ' != sample count from root '
+      '(${cpuProfileData.cpuProfileRoot.inclusiveSampleCount})',
+    );
+  }
+
+  void _processStackFrame(
+    CpuStackFrame stackFrame,
+    CpuStackFrame parent,
+    CpuProfileData cpuProfileData,
+  ) {
+    cpuProfileData.stackFrames[stackFrame.id] = stackFrame;
+
+    if (parent == null) {
+      // [stackFrame] is the root of a new cpu sample. Add it as a child of
+      // [cpuProfile].
+      cpuProfileData.cpuProfileRoot.addChild(stackFrame);
+    } else {
+      parent.addChild(stackFrame);
+    }
+  }
+
+  void _setExclusiveSampleCounts(CpuProfileData cpuProfileData) {
+    for (Map<String, dynamic> traceEvent in cpuProfileData.stackTraceEvents) {
+      final leafId = traceEvent[CpuProfileData.stackFrameIdKey];
+      assert(
+        cpuProfileData.stackFrames[leafId] != null,
+        'No StackFrame found for id $leafId. If you see this assertion, please '
+        'export the timeline trace and send to kenzieschmoll@google.com. Note: '
+        'you must export the timeline immediately after the AssertionError is '
+        'thrown.',
+      );
+      cpuProfileData.stackFrames[leafId]?.exclusiveSampleCount++;
+    }
+  }
+}
+
+/// Process for converting a [CpuStackFrame] into a bottom-up representation of
+/// the CPU profile.
+class BottomUpProfileTransformer {
+  List<CpuStackFrame> processData(CpuStackFrame stackFrame) {
+    final List<CpuStackFrame> bottomUpRoots = getRoots(stackFrame, null, []);
+
+    // Set the bottom up sample counts for each sample.
+    bottomUpRoots.forEach(cascadeSampleCounts);
+
+    // Merge samples when possible starting at the root (the leaf node of the
+    // original CPU sample).
+    mergeProfileRoots(bottomUpRoots);
+
+    return bottomUpRoots;
+  }
+
+  /// Returns the roots for a bottom up representation of a CpuStackFrame node.
+  ///
+  /// Each root is a leaf from the original CpuStackFrame tree, and its children
+  /// will be the reverse call stack of the original sample. The stack frames
+  /// returned will not be merged to combine common roots, and the sample counts
+  /// will not reflect the bottom up sample counts. These steps will occur later
+  /// in the bottom-up conversion process.
+  @visibleForTesting
+  List<CpuStackFrame> getRoots(
+    CpuStackFrame node,
+    CpuStackFrame currentBottomUpRoot,
+    List<CpuStackFrame> bottomUpRoots,
+  ) {
+    final copy = node.shallowCopy(resetInclusiveSampleCount: true);
+
+    if (currentBottomUpRoot != null) {
+      copy.addChild(currentBottomUpRoot.deepCopy());
+    }
+
+    // [copy] is the new root of the bottom up call stack.
+    currentBottomUpRoot = copy;
+
+    if (node.exclusiveSampleCount > 0) {
+      // This node is a leaf node, meaning it is a bottom up root.
+      bottomUpRoots.add(currentBottomUpRoot);
+    }
+    for (CpuStackFrame child in node.children) {
+      getRoots(child, currentBottomUpRoot, bottomUpRoots);
+    }
+    return bottomUpRoots;
+  }
+
+  /// Sets sample counts of [stackFrame] and all children to
+  /// [exclusiveSampleCount].
+  ///
+  /// This is necessary for the transformation of a [CpuStackFrame] to its
+  /// bottom-up representation. This is an intermediate step between
+  /// [getRoots] and [mergeProfileRoots].
+  @visibleForTesting
+  void cascadeSampleCounts(CpuStackFrame stackFrame) {
+    stackFrame.inclusiveSampleCount = stackFrame.exclusiveSampleCount;
+    for (CpuStackFrame child in stackFrame.children) {
+      child.exclusiveSampleCount = stackFrame.exclusiveSampleCount;
+      cascadeSampleCounts(child);
+    }
+  }
+}
+
+/// Merges CPU profile roots that share a common call stack (starting at the
+/// root).
+///
+/// Ex. C               C                     C
+///      -> B             -> B        -->      -> B
+///          -> A             -> D                 -> A
+///                                                -> D
+///
+/// At the time this method is called, we assume we have a list of roots with
+/// accurate inclusive/exclusive sample counts.
+void mergeProfileRoots(List<CpuStackFrame> roots) {
+  // Loop through a copy of [roots] so that we can remove nodes from [roots]
+  // once we have merged them.
+  final List<CpuStackFrame> rootsCopy = List.from(roots);
+  for (CpuStackFrame root in rootsCopy) {
+    if (!roots.contains(root)) {
+      // We have already merged [root] and removed it from [roots]. Do not
+      // attempt to merge again.
+      continue;
+    }
+
+    final matchingRoots =
+        roots.where((other) => other.matches(root) && other != root).toList();
+    if (matchingRoots.isEmpty) {
+      continue;
+    }
+
+    for (CpuStackFrame match in matchingRoots) {
+      match.children.forEach(root.addChild);
+      root.exclusiveSampleCount += match.exclusiveSampleCount;
+      root.inclusiveSampleCount += match.inclusiveSampleCount;
+      roots.remove(match);
+      mergeProfileRoots(root.children);
+    }
+  }
+
+  for (CpuStackFrame root in roots) {
+    root.index = roots.indexOf(root);
+  }
+}
diff --git a/devtools/lib/src/profiler/cpu_profiler.dart b/devtools/lib/src/profiler/cpu_profiler.dart
new file mode 100644
index 0000000..5874f50
--- /dev/null
+++ b/devtools/lib/src/profiler/cpu_profiler.dart
@@ -0,0 +1,277 @@
+// Copyright 2019 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:meta/meta.dart';
+
+import '../tables.dart';
+import '../ui/custom.dart';
+import '../ui/elements.dart';
+import '../ui/primer.dart';
+import 'cpu_profile_flame_chart.dart';
+import 'cpu_profile_model.dart';
+import 'cpu_profile_tables.dart';
+
+abstract class CpuProfiler extends CoreElement {
+  CpuProfiler(
+    this.flameChart,
+    this.callTree,
+    this.bottomUp, {
+    this.defaultView = CpuProfilerViewType.flameChart,
+  }) : super('div') {
+    layoutVertical();
+    flex();
+
+    add(views = [
+      flameChart,
+      callTree,
+      bottomUp,
+    ]);
+
+    // Hide views that are not the default view.
+    for (CpuProfilerView v in views.where((view) => view.type != defaultView)) {
+      v.hide();
+    }
+    _selectedViewType = defaultView;
+  }
+
+  CpuFlameChart flameChart;
+
+  CpuBottomUp bottomUp;
+
+  CpuCallTree callTree;
+
+  CpuProfilerViewType defaultView;
+
+  List<CpuProfilerView> views;
+
+  CpuProfilerViewType _selectedViewType;
+
+  bool showingMessage = false;
+
+  void showView(CpuProfilerViewType showType) {
+    _selectedViewType = showType;
+
+    // If we are showing a message, we do not want to show any other views.
+    if (showingMessage) return;
+
+    CpuProfilerView viewToShow;
+    for (CpuProfilerView view in views) {
+      if (view.type == showType) {
+        viewToShow = view;
+      } else {
+        view.hide();
+      }
+    }
+
+    // Show the view after hiding the others.
+    viewToShow.show();
+  }
+
+  void hideAll() {
+    for (CpuProfilerView view in views) {
+      view.hide();
+    }
+  }
+
+  Future<void> update() async {
+    reset();
+
+    final Spinner spinner = Spinner.centered();
+    try {
+      add(spinner);
+
+      await prepareCpuProfile();
+
+      final showingMessage = maybeShowMessageOnUpdate();
+      if (showingMessage) return;
+
+      for (CpuProfilerView view in views) {
+        view.update();
+      }
+
+      // Ensure we are showing the selected profiler view.
+      showView(_selectedViewType);
+    } catch (e) {
+      showMessage(div(text: 'Error retrieving CPU profile: ${e.toString()}'));
+    } finally {
+      spinner.remove();
+    }
+  }
+
+  void reset() {
+    for (CpuProfilerView view in views) {
+      view.reset();
+    }
+    _removeMessage();
+  }
+
+  Future<void> prepareCpuProfile();
+
+  /// Returns true if we are showing a message instead of the profile.
+  bool maybeShowMessageOnUpdate();
+
+  void showMessage(CoreElement message) {
+    hideAll();
+    showingMessage = true;
+    add(message
+      ..id = 'cpu-profiler-message'
+      ..clazz('centered-single-line-message'));
+  }
+
+  void _removeMessage() {
+    element.children.removeWhere((e) => e.id == 'cpu-profiler-message');
+    showingMessage = false;
+  }
+}
+
+typedef CpuProfileDataProvider = CpuProfileData Function();
+
+abstract class CpuProfilerView extends CoreElement {
+  CpuProfilerView(this.type, this.profileDataProvider)
+      : super('div', classes: 'fill-section');
+
+  final CpuProfilerViewType type;
+
+  final CpuProfileDataProvider profileDataProvider;
+
+  bool viewNeedsRebuild = false;
+
+  void rebuildView();
+
+  void reset();
+
+  void update({bool showLoadingSpinner = false}) async {
+    if (profileDataProvider() == null) return;
+
+    // Update the view if it is visible. Otherwise, mark the view as needing a
+    // rebuild.
+    if (!isHidden) {
+      if (showLoadingSpinner) {
+        final Spinner spinner = Spinner.centered();
+        add(spinner);
+
+        // Awaiting this future ensures the spinner pops up in between switching
+        // profiler views. Without this, the UI is laggy and the spinner never
+        // appears.
+        await Future.delayed(const Duration(microseconds: 1));
+
+        rebuildView();
+        spinner.remove();
+      } else {
+        rebuildView();
+      }
+    } else {
+      viewNeedsRebuild = true;
+    }
+  }
+
+  void show() {
+    hidden(false);
+    if (viewNeedsRebuild) {
+      viewNeedsRebuild = false;
+      update(showLoadingSpinner: true);
+    }
+  }
+
+  void hide() => hidden(true);
+}
+
+enum CpuProfilerViewType {
+  flameChart,
+  bottomUp,
+  callTree,
+}
+
+class CpuProfilerTabNav {
+  CpuProfilerTabNav(this.cpuProfiler, this.tabOrder) {
+    _init();
+  }
+
+  final CpuProfiler cpuProfiler;
+
+  final CpuProfilerTabOrder tabOrder;
+
+  final TreeTableToolbar<CpuStackFrame> treeTableToolbar = TreeTableToolbar();
+
+  PTabNav get element => _tabNav;
+
+  PTabNav _tabNav;
+
+  PTabNavTab selectedTab;
+
+  void _init() {
+    final tabs = [
+      CpuProfilerTab(
+        'CPU Flame Chart',
+        CpuProfilerViewType.flameChart,
+      ),
+      CpuProfilerTab(
+        'Call Tree',
+        CpuProfilerViewType.callTree,
+      ),
+      CpuProfilerTab(
+        'Bottom Up',
+        CpuProfilerViewType.bottomUp,
+      )
+    ];
+
+    _tabNav = PTabNav(<CpuProfilerTab>[
+      selectedTab = tabs.firstWhere((tab) => tab.type == tabOrder.first),
+      tabs.firstWhere((tab) => tab.type == tabOrder.second),
+      tabs.firstWhere((tab) => tab.type == tabOrder.third),
+    ])
+      ..element.style.borderBottom = '0'
+      ..layoutHorizontal()
+      ..add([
+        div()..flex(),
+        treeTableToolbar,
+      ]);
+
+    _updateToolbarForSelection(selectedTab);
+
+    _tabNav.onTabSelected.listen((PTabNavTab tab) {
+      // Return early if this tab is already selected.
+      if (tab == selectedTab) {
+        return;
+      }
+      selectedTab = tab;
+      _updateToolbarForSelection(selectedTab);
+      cpuProfiler.showView((tab as CpuProfilerTab).type);
+    });
+  }
+
+  void _updateToolbarForSelection(CpuProfilerTab selectedTab) {
+    switch (selectedTab.type) {
+      case CpuProfilerViewType.flameChart:
+        treeTableToolbar.hidden(true);
+        break;
+      case CpuProfilerViewType.callTree:
+        treeTableToolbar.hidden(false);
+        treeTableToolbar.treeTable = cpuProfiler.callTree.callTreeTable;
+        break;
+      case CpuProfilerViewType.bottomUp:
+        treeTableToolbar.hidden(false);
+        treeTableToolbar.treeTable = cpuProfiler.bottomUp.bottomUpTable;
+        break;
+    }
+  }
+}
+
+class CpuProfilerTab extends PTabNavTab {
+  CpuProfilerTab(String name, this.type) : super(name);
+
+  final CpuProfilerViewType type;
+}
+
+class CpuProfilerTabOrder {
+  CpuProfilerTabOrder({
+    @required this.first,
+    @required this.second,
+    @required this.third,
+  });
+  final CpuProfilerViewType first;
+
+  final CpuProfilerViewType second;
+
+  final CpuProfilerViewType third;
+}
diff --git a/devtools/lib/src/service.dart b/devtools/lib/src/service.dart
index 8d204e3..541650a 100644
--- a/devtools/lib/src/service.dart
+++ b/devtools/lib/src/service.dart
@@ -6,13 +6,51 @@
 import 'dart:html' hide Event;
 import 'dart:typed_data';
 
+import 'package:sse/client/sse_client.dart';
+import 'package:vm_service/utils.dart';
+
 import 'vm_service_wrapper.dart';
 
-Future<VmServiceWrapper> connect(Uri uri, Completer<Null> finishedCompleter) {
-  final WebSocket ws = WebSocket(uri.toString());
+void _connectWithSse(
+  Uri uri,
+  Completer<VmServiceWrapper> connectedCompleter,
+  Completer<void> finishedCompleter,
+) {
+  uri = uri.scheme == 'sse'
+      ? uri.replace(scheme: 'http')
+      : uri.replace(scheme: 'https');
+  final client = SseClient('$uri');
+  final Stream<String> stream = client.stream.asBroadcastStream();
+  client.onOpen.listen((_) {
+    final service = VmServiceWrapper.fromNewVmService(
+      stream,
+      client.sink.add,
+    );
 
-  final Completer<VmServiceWrapper> connectedCompleter =
-      Completer<VmServiceWrapper>();
+    client.sink.done.whenComplete(() {
+      finishedCompleter.complete();
+      service.dispose();
+    });
+
+    connectedCompleter.complete(service);
+  });
+
+  stream.drain().catchError((error) {
+    if (!connectedCompleter.isCompleted) {
+      connectedCompleter.completeError(error);
+    }
+  });
+}
+
+void _connectWithWebSocket(
+  Uri uri,
+  Completer<VmServiceWrapper> connectedCompleter,
+  Completer<void> finishedCompleter,
+) {
+  // Map the URI (which may be Observatory web app) to a WebSocket URI for
+  // the VM service.
+  uri = convertToWebSocketUrl(serviceProtocolUrl: uri);
+  final ws = WebSocket(uri.toString());
 
   ws.onOpen.listen((_) {
     final Stream<dynamic> inStream =
@@ -21,7 +59,7 @@
       if (e.data is String) {
         return e.data;
       } else {
-        final FileReader fileReader = FileReader();
+        final fileReader = FileReader();
         fileReader.readAsArrayBuffer(e.data);
         return fileReader.onLoadEnd.first.then<ByteData>((ProgressEvent _) {
           final Uint8List list = fileReader.result;
@@ -30,9 +68,9 @@
       }
     });
 
-    final VmServiceWrapper service = VmServiceWrapper.fromNewVmService(
+    final service = VmServiceWrapper.fromNewVmService(
       inStream,
-      (String message) => ws.send(message),
+      ws.send,
     );
 
     ws.onClose.listen((_) {
@@ -44,12 +82,19 @@
   });
 
   ws.onError.listen((dynamic e) {
-    //_logger.fine('Unable to connect to observatory, port ${port}', e);
     if (!connectedCompleter.isCompleted) {
       connectedCompleter.completeError(e);
     }
   });
+}
 
+Future<VmServiceWrapper> connect(Uri uri, Completer<void> finishedCompleter) {
+  final connectedCompleter = Completer<VmServiceWrapper>();
+  if (uri.scheme == 'sse' || uri.scheme == 'sses') {
+    _connectWithSse(uri, connectedCompleter, finishedCompleter);
+  } else {
+    _connectWithWebSocket(uri, connectedCompleter, finishedCompleter);
+  }
   return connectedCompleter.future;
 }
 
diff --git a/devtools/lib/src/service_extensions.dart b/devtools/lib/src/service_extensions.dart
index d108fc6..bbb1102 100644
--- a/devtools/lib/src/service_extensions.dart
+++ b/devtools/lib/src/service_extensions.dart
@@ -10,31 +10,72 @@
 import 'ui/icons.dart';
 
 // Each service extension needs to be added to [_extensionDescriptions].
-class ToggleableServiceExtensionDescription<T> {
-  const ToggleableServiceExtensionDescription._({
-    this.extension,
-    this.description,
+class ToggleableServiceExtensionDescription<T>
+    extends ServiceExtensionDescription {
+  ToggleableServiceExtensionDescription._({
+    Icon icon,
+    @required String extension,
+    @required String description,
+    @required T enabledValue,
+    @required T disabledValue,
+    @required String enabledTooltip,
+    @required String disabledTooltip,
+    @required String gaScreenName,
+    @required String gaItem,
+  }) : super(
+          extension: extension,
+          description: description,
+          icon: icon,
+          values: [enabledValue, disabledValue],
+          tooltips: [enabledTooltip, disabledTooltip],
+          gaScreenName: gaScreenName,
+          gaItem: gaItem,
+        );
+
+  static const enabledValueIndex = 0;
+
+  static const disabledValueIndex = 1;
+
+  T get enabledValue => values[enabledValueIndex];
+
+  T get disabledValue => values[disabledValueIndex];
+
+  String get enabledTooltip => tooltips[enabledValueIndex];
+
+  String get disabledTooltip => tooltips[disabledValueIndex];
+}
+
+class ServiceExtensionDescription<T> {
+  ServiceExtensionDescription({
     this.icon,
-    this.enabledValue,
-    this.disabledValue,
-    this.enabledTooltip,
-    this.disabledTooltip,
+    List<String> displayValues,
+    @required this.extension,
+    @required this.description,
+    @required this.values,
+    @required this.tooltips,
     @required this.gaScreenName,
     @required this.gaItem,
-  });
+  }) : displayValues =
+            displayValues ?? values.map((v) => v.toString()).toList();
 
   final String extension;
+
   final String description;
+
   final Icon icon;
-  final T enabledValue;
-  final T disabledValue;
-  final String enabledTooltip;
-  final String disabledTooltip;
+
+  final List<T> values;
+
+  final List<String> displayValues;
+
+  final List<String> tooltips;
+
   final String gaScreenName; // Analytics screen (screen name where item lives).
+
   final String gaItem; // Analytics item name (toggleable item's name).
 }
 
-const debugAllowBanner = ToggleableServiceExtensionDescription<bool>._(
+final debugAllowBanner = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.debugAllowBanner',
   description: 'Debug Banner',
   icon: FlutterIcons.debugBanner,
@@ -46,7 +87,7 @@
   gaItem: ga.debugBanner,
 );
 
-const debugPaint = ToggleableServiceExtensionDescription<bool>._(
+final debugPaint = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.debugPaint',
   description: 'Debug Paint',
   icon: FlutterIcons.debugPaint,
@@ -58,7 +99,7 @@
   gaItem: ga.debugPaint,
 );
 
-const debugPaintBaselines = ToggleableServiceExtensionDescription<bool>._(
+final debugPaintBaselines = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.debugPaintBaselinesEnabled',
   description: 'Paint Baselines',
   icon: FlutterIcons.text,
@@ -70,7 +111,7 @@
   gaItem: ga.paintBaseline,
 );
 
-const performanceOverlay = ToggleableServiceExtensionDescription<bool>._(
+final performanceOverlay = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.showPerformanceOverlay',
   description: 'Performance Overlay',
   icon: FlutterIcons.performanceOverlay,
@@ -82,7 +123,7 @@
   gaItem: ga.performanceOverlay,
 );
 
-const profileWidgetBuilds = ToggleableServiceExtensionDescription<bool>._(
+final profileWidgetBuilds = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.profileWidgetBuilds',
   description: 'Track Widget Rebuilds',
   icon: FlutterIcons.greyProgr,
@@ -94,7 +135,7 @@
   gaItem: ga.trackRebuilds,
 );
 
-const repaintRainbow = ToggleableServiceExtensionDescription<bool>._(
+final repaintRainbow = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.repaintRainbow',
   description: 'Repaint Rainbow',
   icon: FlutterIcons.repaintRainbow,
@@ -106,7 +147,7 @@
   gaItem: ga.repaintRainbow,
 );
 
-const slowAnimations = ToggleableServiceExtensionDescription<num>._(
+final slowAnimations = ToggleableServiceExtensionDescription<num>._(
   extension: 'ext.flutter.timeDilation',
   description: 'Slow Animations',
   icon: FlutterIcons.history,
@@ -118,19 +159,18 @@
   gaItem: ga.slowAnimation,
 );
 
-const togglePlatformMode = ToggleableServiceExtensionDescription<String>._(
+final togglePlatformMode = ServiceExtensionDescription<String>(
   extension: 'ext.flutter.platformOverride',
-  description: 'iOS',
+  description: 'Override target platform',
   icon: FlutterIcons.phone,
-  enabledValue: 'iOS',
-  disabledValue: 'android',
-  enabledTooltip: 'Toggle iOS Platform',
-  disabledTooltip: 'Toggle iOS Platform',
+  values: ['iOS', 'android', 'fuchsia'],
+  displayValues: ['Platform: iOS', 'Platform: Android', 'Platform: Fuchsia'],
+  tooltips: ['Override Target Platform'],
   gaScreenName: ga.inspector,
-  gaItem: ga.toggleIoS,
+  gaItem: ga.togglePlatform,
 );
 
-const toggleSelectWidgetMode = ToggleableServiceExtensionDescription<bool>._(
+final toggleSelectWidgetMode = ToggleableServiceExtensionDescription<bool>._(
   extension: 'ext.flutter.inspector.show',
   description: 'Select Widget Mode',
   icon: FlutterIcons.locate,
@@ -142,11 +182,23 @@
   gaItem: ga.selectWidgetMode,
 );
 
+final structuredErrors = ToggleableServiceExtensionDescription<bool>._(
+  extension: 'ext.flutter.inspector.structuredErrors',
+  description: 'Show structured errors',
+  icon: FlutterIcons.redError,
+  enabledValue: true,
+  disabledValue: false,
+  enabledTooltip: 'Disable structured errors for Flutter framework issues',
+  disabledTooltip: 'Show structured errors for Flutter framework issues',
+  gaScreenName: ga.logging,
+  gaItem: ga.structuredErrors,
+);
+
 // This extension should never be displayed as a button so does not need a
 // ServiceExtensionDescription object.
 const String didSendFirstFrameEvent = 'ext.flutter.didSendFirstFrameEvent';
 
-const List<ToggleableServiceExtensionDescription> _extensionDescriptions = [
+final List<ServiceExtensionDescription> _extensionDescriptions = [
   debugPaint,
   debugPaintBaselines,
   repaintRainbow,
@@ -156,10 +208,11 @@
   toggleSelectWidgetMode,
   togglePlatformMode,
   slowAnimations,
+  structuredErrors,
 ];
 
-final Map<String, ToggleableServiceExtensionDescription>
-    toggleableExtensionsWhitelist = Map.fromIterable(
+final Map<String, ServiceExtensionDescription> serviceExtensionsWhitelist =
+    Map.fromIterable(
   _extensionDescriptions,
   key: (extension) => extension.extension,
   value: (extension) => extension,
diff --git a/devtools/lib/src/service_manager.dart b/devtools/lib/src/service_manager.dart
index d778c4e..9a9b907 100644
--- a/devtools/lib/src/service_manager.dart
+++ b/devtools/lib/src/service_manager.dart
@@ -6,7 +6,7 @@
 
 import 'package:meta/meta.dart';
 import 'package:pedantic/pedantic.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import 'connected_app.dart';
 import 'eval_on_dart_library.dart';
@@ -26,14 +26,10 @@
     _serviceExtensionManager = serviceExtensionManager;
   }
 
-  final StreamController<Null> _stateController =
-      StreamController<Null>.broadcast();
   final StreamController<VmServiceWrapper> _connectionAvailableController =
       StreamController<VmServiceWrapper>.broadcast();
-  final StreamController<Null> _connectionClosedController =
-      StreamController<Null>.broadcast();
 
-  final Completer<Null> serviceAvailable = Completer();
+  final serviceAvailable = Completer<void>();
 
   VmServiceCapabilities _serviceCapabilities;
 
@@ -69,12 +65,14 @@
 
   bool get hasConnection => service != null;
 
-  Stream<Null> get onStateChange => _stateController.stream;
+  Stream<void> get onStateChange => _stateController.stream;
+  final _stateController = StreamController<void>.broadcast();
 
   Stream<VmServiceWrapper> get onConnectionAvailable =>
       _connectionAvailableController.stream;
 
-  Stream<Null> get onConnectionClosed => _connectionClosedController.stream;
+  Stream<void> get onConnectionClosed => _connectionClosedController.stream;
+  final _connectionClosedController = StreamController<void>.broadcast();
 
   /// Call a service that is registered by exactly one client.
   Future<Response> callService(
@@ -120,7 +118,7 @@
     VmServiceWrapper service, {
     @required Future<void> onClosed,
   }) async {
-    const kServiceStream = '_Service';
+    final serviceStreamName = await service.serviceStreamName;
 
     final vm = await service.getVM();
     this.vm = vm;
@@ -134,7 +132,7 @@
 
     connectedApp = ConnectedApp();
 
-    service.onEvent(kServiceStream).listen((e) {
+    void handleServiceEvent(Event e) {
       if (e.kind == EventKind.kServiceRegistered) {
         if (!_registeredMethodsForService.containsKey(e.service)) {
           _registeredMethodsForService[e.service] = [e.method];
@@ -145,7 +143,9 @@
           _registeredMethodsForService[e.service].add(e.method);
         }
       }
-    });
+    }
+
+    service.onEvent(serviceStreamName).listen(handleServiceEvent);
 
     _isolateManager._service = service;
     _serviceExtensionManager._service = service;
@@ -169,11 +169,12 @@
       EventStreams.kGC,
       EventStreams.kTimeline,
       EventStreams.kExtension,
-      kServiceStream,
+      serviceStreamName
     ];
 
-    // The following streams are not yet supported by Flutter Web.
-    if (!await connectedApp.isFlutterWebApp) {
+    // The following streams are not yet supported by Flutter Web / Dart web
+    // apps.
+    if (!await connectedApp.isDartWebApp) {
       streamIds.addAll(['_Graph', '_Logging', EventStreams.kLogging]);
     }
 
@@ -181,10 +182,11 @@
       try {
         await service.streamListen(id);
       } catch (e) {
-        // Don't complain about '_Logging' or 'Logging' events (newer VMs don't
-        // support '_Logging' and older VMs don't support 'Logging').
         // TODO(devoncarew): Remove this check on or after approx. Oct 1 2019.
-        if (!id.endsWith('Logging')) {
+        if (id.endsWith('Logging')) {
+          // Don't complain about '_Logging' or 'Logging' events (new VMs don't
+          // have the private names, and older ones don't have the public ones).
+        } else {
           print("Service client stream not supported: '$id'\n  $e");
         }
       }
@@ -231,7 +233,7 @@
   final StreamController<IsolateRef> _selectedIsolateController =
       StreamController<IsolateRef>.broadcast();
 
-  Completer<Null> selectedIsolateAvailable = Completer();
+  var selectedIsolateAvailable = Completer<void>();
 
   List<LibraryRef> selectedIsolateLibraries;
 
@@ -380,7 +382,7 @@
   // ignore: prefer_collection_literals
   final Set<String> _pendingServiceExtensions = Set<String>();
 
-  Completer<Null> extensionStatesUpdated = Completer();
+  var extensionStatesUpdated = Completer<void>();
 
   Future<void> _handleExtensionEvent(Event event) async {
     switch (event.extensionKind) {
@@ -393,10 +395,17 @@
         final String valueFromJson =
             event.json['extensionData']['value'].toString();
 
-        final extension = extensions.toggleableExtensionsWhitelist[name];
+        final extension = extensions.serviceExtensionsWhitelist[name];
         if (extension != null) {
           final dynamic value = _getExtensionValueFromJson(name, valueFromJson);
-          final bool enabled = value == extension.enabledValue;
+
+          final enabled =
+              extension is extensions.ToggleableServiceExtensionDescription
+                  ? value == extension.enabledValue
+                  // For extensions that have more than two states
+                  // (enabled / disabled), we will always consider them to be
+                  // enabled with the current value.
+                  : true;
 
           await setServiceExtensionState(
             name,
@@ -410,7 +419,7 @@
 
   dynamic _getExtensionValueFromJson(String name, String valueFromJson) {
     final expectedValueType =
-        extensions.toggleableExtensionsWhitelist[name].enabledValue.runtimeType;
+        extensions.serviceExtensionsWhitelist[name].values.first.runtimeType;
     switch (expectedValueType) {
       case bool:
         return valueFromJson == 'true' ? true : false;
@@ -499,23 +508,25 @@
     _serviceExtensions.add(name);
     streamController.add(true);
 
-    // Set any extensions that are already enabled on the device. This will
-    // enable extension states in DevTools on page refresh or initial start.
-    await _restoreExtensionFromDevice(name);
-
-    // Restore any previously enabled states by calling their service extension.
-    // This will restore extension states on the device after a hot restart.
     if (_enabledServiceExtensions.containsKey(name)) {
+      // Restore any previously enabled states by calling their service
+      // extension. This will restore extension states on the device after a hot
+      // restart. [_enabledServiceExtensions] will be empty on page refresh or
+      // initial start.
       await _callServiceExtension(name, _enabledServiceExtensions[name].value);
+    } else {
+      // Set any extensions that are already enabled on the device. This will
+      // enable extension states in DevTools on page refresh or initial start.
+      await _restoreExtensionFromDevice(name);
     }
   }
 
   Future<void> _restoreExtensionFromDevice(String name) async {
-    if (!extensions.toggleableExtensionsWhitelist.containsKey(name)) {
+    if (!extensions.serviceExtensionsWhitelist.containsKey(name)) {
       return;
     }
     final expectedValueType =
-        extensions.toggleableExtensionsWhitelist[name].enabledValue.runtimeType;
+        extensions.serviceExtensionsWhitelist[name].values.first.runtimeType;
 
     try {
       final response = await _service.callServiceExtension(
@@ -552,7 +563,13 @@
   }
 
   Future<void> _maybeRestoreExtension(String name, dynamic value) async {
-    if (value == extensions.toggleableExtensionsWhitelist[name].enabledValue) {
+    final extensionDescription = extensions.serviceExtensionsWhitelist[name];
+    if (extensionDescription
+        is extensions.ToggleableServiceExtensionDescription) {
+      if (value == extensionDescription.enabledValue) {
+        await setServiceExtensionState(name, true, value, callExtension: false);
+      }
+    } else {
       await setServiceExtensionState(name, true, value, callExtension: false);
     }
   }
@@ -589,6 +606,7 @@
   void resetAvailableExtensions() {
     extensionStatesUpdated = Completer();
     _firstFrameEventReceived = false;
+    _pendingServiceExtensions.clear();
     _serviceExtensions.clear();
     _serviceExtensionController
         .forEach((String name, StreamController<bool> stream) {
diff --git a/devtools/lib/src/settings/settings_controller.dart b/devtools/lib/src/settings/settings_controller.dart
new file mode 100644
index 0000000..308d575
--- /dev/null
+++ b/devtools/lib/src/settings/settings_controller.dart
@@ -0,0 +1,24 @@
+// Copyright 2019 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:vm_service/vm_service.dart';
+
+import '../globals.dart';
+
+class SettingsController {
+  SettingsController({this.onFlagListChange, this.onSdkVersionChange});
+
+  final Function(FlagList) onFlagListChange;
+  final Function(String) onSdkVersionChange;
+
+  Future<String> _getSdkVersion() async {
+    final isAnyFlutterApp = await serviceManager.connectedApp.isAnyFlutterApp;
+    return '${isAnyFlutterApp ? 'Flutter' : 'Dart'} SDK Version: ${serviceManager.sdkVersion}';
+  }
+
+  Future<void> entering() async {
+    onFlagListChange(await serviceManager.service.getFlagList());
+    onSdkVersionChange(await _getSdkVersion());
+  }
+}
diff --git a/devtools/lib/src/settings/settings_screen.dart b/devtools/lib/src/settings/settings_screen.dart
new file mode 100644
index 0000000..59b0913
--- /dev/null
+++ b/devtools/lib/src/settings/settings_screen.dart
@@ -0,0 +1,81 @@
+// Copyright 2019 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:devtools/src/settings/settings_controller.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../framework/framework.dart';
+import '../ui/analytics_platform.dart' as ga_platform;
+import '../ui/elements.dart';
+
+class FlagDetailsUI extends CoreElement {
+  FlagDetailsUI(Flag flag) : super('div', classes: 'flag-details-container') {
+    final flagDescription = div(c: 'flag-details-descriptions-container')
+      ..add(<CoreElement>[
+        CoreElement('h3', classes: 'flag-name', text: flag.name),
+        span(c: 'flag-description', text: flag.comment),
+      ]);
+
+    final flagValues = div(c: 'flag-details-values-container')
+      ..layoutVertical()
+      ..add(<CoreElement>[
+        span(c: 'flag-value', text: flag.valueAsString),
+        span(c: 'flag-modified', text: flag.modified ? 'modified' : 'default')
+      ]);
+
+    add(<CoreElement>[
+      flagDescription,
+      flagValues,
+    ]);
+  }
+}
+
+class SettingsScreen extends Screen {
+  SettingsScreen()
+      : super(
+          name: '',
+          id: 'settings',
+          iconClass: 'octicon-gear masthead-item action-button active',
+          showTab: false,
+        ) {
+    _flagList = div(c: 'flag-list-container')
+      ..layoutVertical()
+      ..add(
+        CoreElement('h2', text: 'Dart VM Flag List'),
+      );
+    _sdkVersion = div(c: 'sdk-version-container')..flex(2);
+
+    _controller = SettingsController(
+      onFlagListChange: (FlagList flagList) {
+        _flagList.add(flagList.flags.map((flag) => FlagDetailsUI(flag)));
+      },
+      onSdkVersionChange: (String sdkVersion) {
+        _sdkVersion.add(h2(text: sdkVersion));
+      },
+    );
+  }
+
+  CoreElement _container;
+  CoreElement _flagList;
+  CoreElement _screenDiv;
+  CoreElement _sdkVersion;
+
+  SettingsController _controller;
+
+  @override
+  CoreElement createContent(Framework framework) {
+    ga_platform.setupDimensions();
+    this.framework = framework;
+    _screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
+    _container = div(c: 'settings-container');
+    _screenDiv
+      ..clear()
+      ..add(_container..clear());
+    _container.add(_sdkVersion);
+    _container.add(_flagList);
+
+    _controller.entering();
+    return _screenDiv;
+  }
+}
diff --git a/devtools/lib/src/table_data.dart b/devtools/lib/src/table_data.dart
new file mode 100644
index 0000000..ab947aa
--- /dev/null
+++ b/devtools/lib/src/table_data.dart
@@ -0,0 +1,482 @@
+// Copyright 2019 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 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import 'trees.dart';
+import 'ui/fake_flutter/fake_flutter.dart';
+import 'utils.dart';
+
+class HoverCellData<T> {
+  HoverCellData(this.data);
+
+  final T data;
+}
+
+abstract class TableDataClient<T> {
+  void rebuildTable();
+
+  /// Override to update data after setting rows.
+  void onSetRows();
+
+  void setState(VoidCallback modifyState);
+
+  /// The way the columns are sorted have changed.
+  ///
+  /// Update the UI to reflect the new state.
+  void onColumnSortChanged(ColumnData<T> column, SortOrder sortDirection);
+
+  /// Selects by index. Note: This is index of the row as it's rendered
+  /// and not necessarily for rows[] since it may be being rendered in reverse.
+  /// This way, +1 will always move down the visible table.
+  /// scrollBehaviour is a string as defined for the HTML scrollTo() method
+  /// https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo (eg.
+  /// `smooth`, `instance`, `auto`).
+  void selectByIndex(
+    int newIndex, {
+    bool keepVisible = true,
+    String scrollBehavior = 'smooth',
+  });
+
+  void scrollToIndex(int rowIndex, {String scrollBehavior = 'smooth'});
+
+  void clearSelection();
+}
+
+class TableData<T> extends Object {
+  void setState(VoidCallback modifyState) {
+    client?.setState(modifyState);
+  }
+
+  TableDataClient<T> client;
+
+  bool _hasPendingRebuild = false;
+
+  List<ColumnData<T>> get columns => _columns;
+
+  final List<ColumnData<T>> _columns = <ColumnData<T>>[];
+
+  List<T> data = [];
+
+  int get rowCount => data?.length ?? 0;
+
+  ColumnData<T> _sortColumn;
+
+  SortOrder _sortDirection;
+
+  set sortColumn(ColumnData<T> column) {
+    _sortColumn = column;
+    _sortDirection =
+        column.numeric ? SortOrder.descending : SortOrder.ascending;
+  }
+
+  @protected
+  final StreamController<T> selectController = StreamController<T>.broadcast();
+
+  final StreamController<HoverCellData<T>> selectElementController =
+      StreamController<HoverCellData<T>>.broadcast();
+
+  @mustCallSuper
+  void dispose() {
+    client = null;
+  }
+
+  Stream<T> get onSelect => selectController.stream;
+
+  Stream<void> get onRowsChanged => _rowsChangedController.stream;
+  final _rowsChangedController = StreamController<void>.broadcast();
+
+  Stream<HoverCellData<T>> get onCellHover => selectElementController.stream;
+
+  void addColumn(ColumnData<T> column) {
+    _columns.add(column);
+  }
+
+  void setRows(List<T> data) {
+    // If the selected object is no longer valid, clear the selection and
+    // scroll to the top.
+    if (!data.contains(_selectedObject)) {
+      if (rowCount > 0) {
+        client?.scrollToIndex(0, scrollBehavior: 'auto');
+      }
+      client?.clearSelection();
+    }
+
+    // Copy the list, so that changes to it don't affect us.
+    this.data = data.toList();
+
+    _rowsChangedController.add(null);
+
+    client?.onSetRows();
+
+    if (_sortColumn == null) {
+      final ColumnData<T> column = _columns.firstWhere(
+          (ColumnData<T> c) => c.supportsSorting,
+          orElse: () => null);
+      if (column != null) {
+        sortColumn = column;
+      }
+    }
+
+    if (_sortColumn != null) {
+      _doSort();
+    }
+
+    scheduleRebuild();
+  }
+
+  void scrollTo(T row, {String scrollBehavior = 'smooth'}) {
+    final index = data.indexOf(row);
+    if (index == -1) {
+      return;
+    }
+    if (_hasPendingRebuild) {
+      // Wait for the content to be rendered before we scroll otherwise we may
+      // not be able to scroll far enough.
+      setState(() {
+        // We assume the index is still valid. Alternately we search for the
+        // index again. The one thing we should absolutely not do is call the
+        // scrollTo helper method again as there is a significant risk scrollTo
+        // would never be called if items are added to the table every frame.
+        client?.scrollToIndex(index, scrollBehavior: scrollBehavior);
+      });
+      return;
+    }
+    client?.scrollToIndex(index, scrollBehavior: scrollBehavior);
+  }
+
+  void scheduleRebuild() {
+    if (!_hasPendingRebuild) {
+      // Set a flag to ensure we don't schedule rebuilds if there's already one
+      // in the queue.
+      _hasPendingRebuild = true;
+
+      setState(() {
+        _hasPendingRebuild = false;
+        client?.rebuildTable();
+      });
+    }
+  }
+
+  void _doSort() {
+    final ColumnData<T> column = _sortColumn;
+    final int direction = _sortDirection == SortOrder.ascending ? 1 : -1;
+
+    client?.onColumnSortChanged(column, _sortDirection);
+
+    _sortData(column, direction);
+  }
+
+  void _sortData(ColumnData column, int direction) {
+    data.sort((T a, T b) => _compareData(a, b, column, direction));
+  }
+
+  int _compareData(T a, T b, ColumnData column, int direction) {
+    return column.compare(a, b) * direction;
+  }
+
+  T get selectedObject => _selectedObject;
+  T _selectedObject;
+
+  int get selectedObjectIndex => _selectedObjectIndex;
+  int _selectedObjectIndex;
+
+  void setSelection(T object, int index) {
+    assert(index == null || data[index] == object);
+    _selectedObjectIndex = index;
+    if (selectedObject != object) {
+      _selectedObject = object;
+      selectController.add(object);
+    }
+  }
+
+  bool get hasSelection => _selectedObject != null;
+
+  void onColumnClicked(ColumnData<T> column) {
+    if (!column.supportsSorting) {
+      return;
+    }
+
+    if (_sortColumn == column) {
+      _sortDirection = _sortDirection == SortOrder.ascending
+          ? SortOrder.descending
+          : SortOrder.ascending;
+    } else {
+      sortColumn = column;
+    }
+
+    _doSort();
+    scheduleRebuild();
+  }
+
+  /// Override this method if the table should handle left key strokes.
+  void handleLeftKey() {}
+
+  /// Override this method if the table should handle right key strokes.
+  void handleRightKey() {}
+}
+
+/// Data for a tree table where nodes in the tree can be expanded and
+/// collapsed.
+///
+/// A TreeTableData must have exactly one column that is a [TreeColumnData].
+class TreeTableData<T extends TreeNode<T>> extends TableData<T> {
+  @override
+  void addColumn(ColumnData<T> column) {
+    // Avoid adding multiple TreeColumnData columns.
+    assert(column is! TreeColumnData<T> ||
+        _columns.where((c) => c is TreeColumnData).isEmpty);
+    super.addColumn(column);
+  }
+
+  @override
+  void handleLeftKey() {
+    if (_selectedObject != null) {
+      if (_selectedObject.isExpanded) {
+        // Collapse node and preserve selection.
+        collapseNode(_selectedObject);
+      } else {
+        // Select the node's parent.
+        final parentIndex = data.indexOf(_selectedObject.parent);
+        if (parentIndex != null && parentIndex != -1) {
+          client?.selectByIndex(parentIndex);
+        }
+      }
+    }
+  }
+
+  @override
+  void handleRightKey() {
+    if (_selectedObject != null) {
+      if (_selectedObject.isExpanded) {
+        // Select the node's first child.
+        final firstChildIndex = data.indexOf(_selectedObject.children.first);
+        client?.selectByIndex(firstChildIndex);
+      } else if (_selectedObject.isExpandable) {
+        // Expand node and preserve selection.
+        expandNode(_selectedObject);
+      } else {
+        // The node is not expandable. Select the next node in range.
+        final nextIndex = data.indexOf(_selectedObject) + 1;
+        if (nextIndex != data.length) {
+          client?.selectByIndex(nextIndex);
+        }
+      }
+    }
+  }
+
+  @override
+  void _sortData(ColumnData column, int direction) {
+    final List<T> sortedData = [];
+
+    void _addToSortedData(T dataObject) {
+      sortedData.add(dataObject);
+      if (dataObject.isExpanded) {
+        dataObject.children
+          ..sort((T a, T b) => _compareData(a, b, column, direction))
+          ..forEach(_addToSortedData);
+      }
+    }
+
+    data.where((dataObject) => dataObject.level == 0).toList()
+      ..sort((T a, T b) => _compareData(a, b, column, direction))
+      ..forEach(_addToSortedData);
+
+    data = sortedData;
+  }
+
+  void collapseNode(T dataObject) {
+    void cascadingRemove(T _dataObject) {
+      if (!data.contains(_dataObject)) return;
+      data.remove(_dataObject);
+      (_dataObject.children).forEach(cascadingRemove);
+    }
+
+    assert(data.contains(dataObject));
+    dataObject.children.forEach(cascadingRemove);
+    dataObject.collapse();
+
+    _selectedObject ??= dataObject;
+    setRows(data);
+  }
+
+  void expandNode(T dataObject) {
+    void expand(T node) {
+      assert(data.contains(node));
+      int insertIndex = data.indexOf(node) + 1;
+      for (T child in node.children) {
+        data.insert(insertIndex, child);
+        if (child.isExpanded) {
+          expand(child);
+        }
+        insertIndex++;
+      }
+      node.expand();
+    }
+
+    expand(dataObject);
+
+    _selectedObject ??= dataObject;
+    setRows(data);
+  }
+
+  void expandAll() {
+    // Store visited nodes so that we do not expand the same root multiple
+    // times.
+    final visited = <T>{};
+    for (T dataObject in data) {
+      final root = dataObject.root;
+      if (!visited.contains(root)) {
+        root.expandCascading();
+        visited.add(root);
+      }
+    }
+
+    setRows(data);
+  }
+
+  void collapseAll() {
+    // Store visited nodes so that we do not expand the same root multiple
+    // times.
+    final visited = <T>{};
+    for (T dataObject in data) {
+      final root = dataObject.root;
+      if (!visited.contains(root)) {
+        root.collapseCascading();
+        visited.add(root);
+      }
+    }
+
+    setRows(data);
+  }
+}
+
+abstract class ColumnData<T> {
+  ColumnData(
+    this.title, {
+    this.alignment = ColumnAlignment.left,
+    this.fixedWidthPx,
+    this.percentWidth,
+    this.usesHtml = false,
+    this.hover = false,
+    this.cssClass,
+  }) {
+    if (percentWidth != null) {
+      percentWidth.clamp(0, 100);
+    }
+  }
+
+  ColumnData.wide(
+    this.title, {
+    this.alignment = ColumnAlignment.left,
+    this.usesHtml = false,
+    this.hover = false,
+    this.cssClass,
+  }) : percentWidth = defaultWideColumnPercentWidth;
+
+  static const defaultWideColumnPercentWidth = 100;
+
+  final String title;
+
+  // TODO(jacobr): remove this field from the data before porting to FlutterWeb.
+  final String cssClass;
+
+  /// Width of the column expressed as a fixed number of pixels.
+  ///
+  /// If both [fixedWidthPx] and [percentWidth] are specified, [fixedWidthPx]
+  /// will be used.
+  int fixedWidthPx;
+
+  /// Width of the column expressed as a percent value between 0 and 100.
+  ///
+  /// TODO(jacobr): make this a double.
+  int percentWidth;
+
+  final ColumnAlignment alignment;
+
+  final bool usesHtml;
+
+  final bool hover;
+
+  bool get numeric => false;
+
+  bool get supportsSorting => numeric;
+
+  int compare(T a, T b) {
+    final Comparable valueA = getValue(a);
+    final Comparable valueB = getValue(b);
+    return valueA.compareTo(valueB);
+  }
+
+  /// Get the cell's value from the given [dataObject].
+  dynamic getValue(T dataObject);
+
+  /// Get the cell's display value from the given [dataObject].
+  dynamic getDisplayValue(T dataObject) => getValue(dataObject);
+
+  /// Get the cell's tooltip value from the given [dataObject].
+  String getTooltip(T dataObject) => '';
+
+  /// Given a value from [getValue], render it to a String.
+  String render(dynamic value) {
+    if (value is num) {
+      return fastIntl(value);
+    }
+    return value.toString();
+  }
+
+  static String fastIntl(num value) {
+    if (value is int && value < 1000) {
+      return value.toString();
+    } else {
+      return nf.format(value);
+    }
+  }
+
+  @override
+  String toString() => title;
+}
+
+abstract class TreeColumnData<T extends TreeNode<T>> extends ColumnData<T> {
+  TreeColumnData(
+    String title, {
+    int fixedWidthPx,
+    int percentWidth,
+  }) : super(title, fixedWidthPx: fixedWidthPx, percentWidth: percentWidth);
+
+  static const treeToggleWidth = 14;
+
+  final StreamController<T> nodeExpandedController =
+      StreamController<T>.broadcast();
+
+  Stream<T> get onNodeExpanded => nodeExpandedController.stream;
+
+  final StreamController<T> nodeCollapsedController =
+      StreamController<T>.broadcast();
+
+  Stream<T> get onNodeCollapsed => nodeCollapsedController.stream;
+
+  int getNodeIndentPx(T dataObject) {
+    int indentWidth = dataObject.level * treeToggleWidth;
+    if (!dataObject.isExpandable) {
+      // If the object is not expandable, we need to increase the width of our
+      // spacer to account for the missing tree toggle.
+      indentWidth += TreeColumnData.treeToggleWidth;
+    }
+    return indentWidth;
+  }
+}
+
+enum ColumnAlignment {
+  left,
+  right,
+  center,
+}
+
+enum SortOrder {
+  ascending,
+  descending,
+}
diff --git a/devtools/lib/src/tables.dart b/devtools/lib/src/tables.dart
index 0b3ebf7..15c55c1 100644
--- a/devtools/lib/src/tables.dart
+++ b/devtools/lib/src/tables.dart
@@ -2,46 +2,98 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
 import 'dart:html';
 
+import 'package:devtools/src/ui/primer.dart';
 import 'package:meta/meta.dart';
 
 import 'framework/framework.dart';
+import 'table_data.dart';
+import 'trees.dart';
+import 'ui/custom.dart';
 import 'ui/elements.dart';
-import 'utils.dart';
 
-class Table<T> extends Object with SetStateMixin {
-  Table()
-      : element = div(a: 'flex', c: 'overflow-y table-border'),
-        _isVirtual = false {
-    _init();
-  }
+class HoverCell<T> extends HoverCellData<T> {
+  HoverCell(this.cell, T data) : super(data);
 
-  Table.virtual({this.rowHeight = 29.0})
-      : element = div(a: 'flex', c: 'overflow-y table-border table-virtual'),
-        _isVirtual = true {
-    _init();
+  final CoreElement cell;
+}
+
+class Table<T> with SetStateMixin implements TableDataClient<T> {
+  factory Table() => Table._(TableData<T>(), null, false);
+
+  factory Table.virtual({double rowHeight = 29.0}) =>
+      Table._(TableData<T>(), rowHeight, true);
+
+  Table._(
+    this.model,
+    this.rowHeight,
+    this.isVirtual, {
+    bool overflowAuto = false,
+  }) : assert(model.client == null) {
+    model.client = this;
+    element = isVirtual
+        ? div(
+            a: 'flex',
+            c: '${overflowAuto ? 'overflow-auto' : 'overflow-y'} '
+                'table-border table-virtual',
+          )
+        : div(a: 'flex', c: 'overflow-y table-border');
+    element.add(_table);
+
+    // Handle key events.
+    _table.onKeyDown.listen((KeyboardEvent e) {
+      int indexOffset;
+      // TODO(dantup): PgUp/PgDown/Home/End?
+      switch (e.keyCode) {
+        case KeyCode.UP:
+          indexOffset = -1;
+          break;
+        case KeyCode.DOWN:
+          indexOffset = 1;
+          break;
+        case KeyCode.LEFT:
+          model.handleLeftKey();
+          break;
+        case KeyCode.RIGHT:
+          model.handleRightKey();
+          break;
+        default:
+          break;
+      }
+
+      if (indexOffset == null) {
+        return;
+      }
+
+      e.preventDefault();
+
+      // Get the index of the currently selected row.
+      final int currentIndex = model.selectedObjectIndex;
+      // Offset it, or select index 0 if there was no prior selection.
+      int newIndex = currentIndex == null ? 0 : (currentIndex + indexOffset);
+      // Clamp to the first/last row.
+      final int maxRowIndex = (model.data?.length ?? 1) - 1;
+      newIndex = newIndex.clamp(0, maxRowIndex);
+
+      selectByIndex(newIndex);
+    });
 
     _spacerBeforeVisibleRows = CoreElement('tr');
     _spacerAfterVisibleRows = CoreElement('tr');
 
-    element.onScroll.listen((_) => _scheduleRebuild());
+    element.onScroll.listen((_) => model.scheduleRebuild());
   }
 
-  final CoreElement element;
-  final bool _isVirtual;
+  final double rowHeight;
+  final bool isVirtual;
 
-  double rowHeight;
-  bool _hasPendingRebuild = false;
+  void dispose() {
+    model?.dispose();
+  }
 
-  List<Column<T>> columns = <Column<T>>[];
-  List<T> data;
-
-  int get rowCount => data?.length ?? 0;
-
-  Column<T> _sortColumn;
-  SortOrder _sortDirection;
+  final TableData<T> model;
+  CoreElement element;
 
   final CoreElement _table = CoreElement('table')
     ..clazz('full-width')
@@ -54,84 +106,34 @@
   final CoreElement _dummyRowToForceAlternatingColor = CoreElement('tr')
     ..display = 'none';
 
-  final Map<Column<T>, CoreElement> _spanForColumn = <Column<T>, CoreElement>{};
+  final Map<ColumnData<T>, CoreElement> _spanForColumn =
+      <ColumnData<T>, CoreElement>{};
   final Map<Element, T> _dataForRow = <Element, T>{};
   final Map<int, CoreElement> _rowForIndex = <int, CoreElement>{};
 
-  final StreamController<T> _selectController = StreamController<T>.broadcast();
-  final StreamController<Null> _rowsChangedController =
-      StreamController.broadcast();
-
-  void _init() {
-    element.add(_table);
-
-    // handle hey events
-    _table.onKeyDown.listen((KeyboardEvent e) {
-      int indexOffset;
-      // TODO(dantup): PgUp/PgDown/Home/End?
-      if (e.keyCode == KeyCode.UP) {
-        indexOffset = -1;
-      } else if (e.keyCode == KeyCode.DOWN) {
-        indexOffset = 1;
-      }
-
-      if (indexOffset == null) {
-        return;
-      }
-
-      e.preventDefault();
-
-      // Get the index of the currently selected row.
-      final int currentIndex = _selectedObjectIndex;
-      // Offset it, or select index 0 if there was no prior selection.
-      int newIndex = currentIndex == null ? 0 : (currentIndex + indexOffset);
-      // Clamp to the first/last row.
-      final int maxRowIndex = (data?.length ?? 1) - 1;
-      newIndex = newIndex.clamp(0, maxRowIndex);
-
-      selectByIndex(newIndex);
-    });
+  ColumnRenderer<T> getColumnRenderer(ColumnData<T> columnModel) {
+    return ColumnRenderer(columnModel);
   }
 
-  void dispose() {}
-
-  Stream<T> get onSelect => _selectController.stream;
-
-  Stream<Null> get onRowsChanged => _rowsChangedController.stream;
-
-  void addColumn(Column<T> column) {
-    columns.add(column);
-  }
-
-  void setRows(List<T> data) {
-    // If the selected object is no longer valid, clear the selection and
-    // scroll to the top.
-    if (!data.contains(_selectedObject)) {
-      if (rowCount > 0) {
-        _scrollToIndex(0, scrollBehavior: 'auto');
-      }
-      _clearSelection();
-    }
-
-    // Copy the list, so that changes to it don't affect us.
-    this.data = data.toList();
-
-    _rowsChangedController.add(null);
-
+  @override
+  void onSetRows() {
     if (_thead == null) {
       _thead = CoreElement('thead')
         ..add(tr()
-          ..add(columns.map((Column<T> column) {
+          ..add(model.columns.map((ColumnData<T> column) {
             final CoreElement s = span(
                 text: column.title,
                 c: 'interactable${column.supportsSorting ? ' sortable' : ''}');
-            s.click(() => _columnClicked(column));
+            s.click(() => model.onColumnClicked(column));
             _spanForColumn[column] = s;
+
             final CoreElement header =
-                th(c: 'sticky-top ${column.numeric ? 'right' : 'left'}')
+                th(c: 'sticky-top ${getColumnRenderer(column).alignmentCss}')
                   ..add(s);
-            if (column.wide) {
-              header.clazz('wide');
+            if (column.fixedWidthPx != null) {
+              header.element.style.width = '${column.fixedWidthPx}px';
+            } else if (column.percentWidth != null) {
+              header.element.style.width = '${column.percentWidth}%';
             }
             return header;
           })));
@@ -143,100 +145,35 @@
       _tbody = CoreElement('tbody', classes: 'selectable');
       _table.add(_tbody);
     }
-
-    if (_sortColumn == null) {
-      final Column<T> column = columns
-          .firstWhere((Column<T> c) => c.supportsSorting, orElse: () => null);
-      if (column != null) {
-        setSortColumn(column);
-      }
-    }
-
-    if (_sortColumn != null) {
-      _doSort();
-    }
-
-    _scheduleRebuild();
   }
 
-  void scrollTo(T row, {String scrollBehavior = 'smooth'}) {
-    final index = data.indexOf(row);
-    if (index == -1) {
-      return;
-    }
-    if (_hasPendingRebuild) {
-      // Wait for the content to be rendered before we scroll otherwise we may
-      // not be able to scroll far enough.
-      setState(() {
-        // We assume the index is still valid. Alternately we search for the
-        // index again. The one thing we should absolutely not do is call the
-        // scrollTo helper method again as there is a significant risk scrollTo
-        // would never be called if items are added to the table every frame.
-        _scrollToIndex(index, scrollBehavior: scrollBehavior);
-      });
-      return;
-    }
-    _scrollToIndex(index, scrollBehavior: scrollBehavior);
-  }
-
-  void _scheduleRebuild() {
-    if (!_hasPendingRebuild) {
-      // Set a flag to ensure we don't schedule rebuilds if there's already one
-      // in the queue.
-      _hasPendingRebuild = true;
-
-      setState(() {
-        _hasPendingRebuild = false;
-        _rebuildTable();
-      });
-    }
-  }
-
-  void _doSort() {
-    final Column<T> column = _sortColumn;
-    final bool numeric = column.numeric;
-    final int direction = _sortDirection == SortOrder.ascending ? 1 : -1;
+  @override
+  void onColumnSortChanged(ColumnData<T> column, SortOrder sortDirection) {
+    // Update the UI to reflect the new column sort order.
+    // The base class will sort the actual data.
 
     // update the sort arrows
-    for (Column<T> c in columns) {
+    for (ColumnData<T> c in model.columns) {
       final CoreElement s = _spanForColumn[c];
-      if (c == _sortColumn) {
-        s.toggleClass('up', _sortDirection == SortOrder.ascending);
-        s.toggleClass('down', _sortDirection != SortOrder.ascending);
+      if (c == column) {
+        s.toggleClass('up', sortDirection == SortOrder.ascending);
+        s.toggleClass('down', sortDirection != SortOrder.ascending);
       } else {
         s.toggleClass('up', false);
         s.toggleClass('down', false);
       }
     }
-
-    data.sort((T a, T b) {
-      if (numeric) {
-        final num one = column.getValue(a);
-        final num two = column.getValue(b);
-        if (one == two) {
-          return 0;
-        }
-        if (_sortDirection == SortOrder.ascending) {
-          return one > two ? 1 : -1;
-        } else {
-          return one > two ? -1 : 1;
-        }
-      } else {
-        final String one = column.render(column.getValue(a));
-        final String two = column.render(column.getValue(b));
-        return one.compareTo(two) * direction;
-      }
-    });
   }
 
-  void _rebuildTable() {
+  @override
+  void rebuildTable() {
     // If we've never had any data set, we don't need to (and can't - since all
     // the elements aren't created) rebuild.
-    if (data == null) {
+    if (model.data == null) {
       return;
     }
 
-    if (_isVirtual) {
+    if (isVirtual) {
       _rebuildVirtualTable();
     } else {
       _rebuildStaticTable();
@@ -250,6 +187,8 @@
     // need to build rows that are already in the table, just add and remove the
     // deltas.
 
+    final data = model.data;
+
     int firstRenderedRowInclusive = 0;
     int lastRenderedRowExclusive = data?.length ?? 0;
 
@@ -293,19 +232,22 @@
     }
 
     currentRowIndex = _buildTableRows(
-        firstRenderedRowInclusive: firstRenderedRowInclusive,
-        lastRenderedRowExclusive: lastRenderedRowExclusive,
-        currentRowIndex: currentRowIndex);
+      firstRenderedRowInclusive: firstRenderedRowInclusive,
+      lastRenderedRowExclusive: lastRenderedRowExclusive,
+      currentRowIndex: currentRowIndex,
+    );
 
     // Remove any additional rows that we had left that we didn't reuse above.
     if (currentRowIndex > 0 &&
         currentRowIndex < _tbody.element.children.length) {
-      // removeRange doesn't work in dart:html (UnimplementedError) so we have
-      // to remove them one at a time.
-      while (_tbody.element.children.length >= currentRowIndex) {
-        _tbody.element.children.removeLast();
-      }
+      _tbody.element.children.removeWhere((e) {
+        final rowElementsInUse =
+            _rowForIndex.values.map((CoreElement el) => el.element).toList();
+        return !rowElementsInUse.contains(e) &&
+            e != _spacerBeforeVisibleRows.element;
+      });
     }
+
     // Set the "after" spacer to the correct height to keep the scroll size
     // correct for the number of rows to come after.
     final double spacerAfterHeight =
@@ -317,7 +259,7 @@
   void _rebuildStaticTable() {
     _buildTableRows(
         firstRenderedRowInclusive: 0,
-        lastRenderedRowExclusive: data?.length ?? 0);
+        lastRenderedRowExclusive: model.data?.length ?? 0);
   }
 
   int _buildTableRows({
@@ -343,7 +285,7 @@
     for (int index = firstRenderedRowInclusive;
         index < lastRenderedRowExclusive;
         index++) {
-      final T dataObject = data[index];
+      final T dataObject = model.data[index];
       final bool isReusableRow =
           currentRowIndex < _tbody.element.children.length;
       // Reuse a row if one already exists in the table.
@@ -362,13 +304,23 @@
         _select(row, _dataForRow[row], index);
       }
 
+      void hoverCell(CoreElement row, CoreElement cell, int rowIndex) {
+        _selectCoreElement(cell, _dataForRow[row.element], rowIndex);
+      }
+
       // We also keep a lookup to get the row for the index of index to allow
       // easy changing of the selected row with keyboard (which needs to offset
       // the selected index).
       _rowForIndex[index] = tableRow;
 
       if (!isReusableRow) {
-        tableRow.click(() => selectRow(tableRow.element, index));
+        tableRow.click(() {
+          final rowElement = tableRow.element;
+          final dataForRow = _dataForRow[rowElement];
+          selectRow(rowElement, model.data.indexOf(dataForRow));
+          // TODO(kenzie): we should do less work on selection.
+          model.scheduleRebuild();
+        });
       }
 
       if (rowHeight != null) {
@@ -377,7 +329,7 @@
       }
 
       int currentColumnIndex = 0;
-      for (Column<T> column in columns) {
+      for (ColumnData<T> column in model.columns) {
         final bool isReusableColumn =
             currentColumnIndex < tableRow.element.children.length;
         // Reuse or create a cell.
@@ -387,6 +339,15 @@
 
         currentColumnIndex++;
 
+        if (column.hover) {
+          if (tableCell.over != hoverCell) {
+            tableCell.over(() => hoverCell(tableRow, tableCell, index));
+          }
+          if (tableCell.leave != hoverCell) {
+            tableCell.leave(() => hoverCell(tableRow, null, index));
+          }
+        }
+
         // TODO(dantup): Should we make CoreElement expose ClassList instead of
         // having flat strings?
         tableCell.element.classes.clear();
@@ -394,11 +355,10 @@
           column.cssClass.split(' ').forEach(tableCell.clazz);
         }
 
-        if (column.numeric) {
-          tableCell.clazz('right');
-        }
+        final columnRenderer = getColumnRenderer(column);
+        tableCell.clazz(columnRenderer.alignmentCss);
 
-        column.renderToElement(tableCell, dataObject);
+        columnRenderer.renderToElement(tableCell, dataObject);
 
         if (!isReusableColumn) {
           tableRow.add(tableCell);
@@ -406,8 +366,8 @@
       }
 
       // If this row represents our selected object, highlight it.
-      if (dataObject == _selectedObject) {
-        _select(tableRow.element, _selectedObject, index);
+      if (dataObject == model.selectedObject) {
+        _select(tableRow.element, model.selectedObject, index);
       } else {
         // Otherwise, ensure it's not marked as selected (the previous data
         // shown in this row may have been selected).
@@ -421,10 +381,6 @@
     return currentRowIndex;
   }
 
-  T _selectedObject;
-
-  int _selectedObjectIndex;
-
   void _select(Element row, T object, int index) {
     if (_tbody != null) {
       for (Element row in _tbody.element.querySelectorAll('.selected')) {
@@ -436,12 +392,11 @@
       row.classes.add('selected');
     }
 
-    if (_selectedObject != object) {
-      _selectController.add(object);
-    }
+    model.setSelection(object, index);
+  }
 
-    _selectedObject = object;
-    _selectedObjectIndex = index;
+  void _selectCoreElement(CoreElement coreElement, T object, int index) {
+    model.selectElementController.add(HoverCell<T>(coreElement, object));
   }
 
   /// Selects by index. Note: This is index of the row as it's rendered
@@ -450,19 +405,23 @@
   /// scrollBehaviour is a string as defined for the HTML scrollTo() method
   /// https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo (eg.
   /// `smooth`, `instance`, `auto`).
-  @visibleForTesting
-  void selectByIndex(int newIndex,
-      {bool keepVisible = true, String scrollBehavior = 'smooth'}) {
+  @override
+  void selectByIndex(
+    int newIndex, {
+    bool keepVisible = true,
+    String scrollBehavior = 'smooth',
+  }) {
     final CoreElement row = _rowForIndex[newIndex];
-    final T dataObject = data[newIndex];
+    final T dataObject = model.data[newIndex];
     _select(row?.element, dataObject, newIndex);
 
     if (keepVisible) {
-      _scrollToIndex(newIndex, scrollBehavior: scrollBehavior);
+      scrollToIndex(newIndex, scrollBehavior: scrollBehavior);
     }
   }
 
-  void _scrollToIndex(int rowIndex, {String scrollBehavior = 'smooth'}) {
+  @override
+  void scrollToIndex(int rowIndex, {String scrollBehavior = 'smooth'}) {
     final double rowOffsetPixels = _rowOffset(rowIndex);
     final int visibleStartOffsetPixels = element.scrollTop;
     final int visibleEndOffsetPixels = element.scrollTop + element.offsetHeight;
@@ -483,7 +442,7 @@
         .clamp(0, element.scrollHeight);
 
     element.element.scrollTo(<String, dynamic>{
-      'left': 0,
+      'left': element.element.scrollLeft,
       'top': newScrollTop,
       'behavior': scrollBehavior,
     });
@@ -493,79 +452,117 @@
     return (rowIndex * rowHeight) + _thead.offsetHeight;
   }
 
-  void _clearSelection() => _select(null, null, null);
-
-  void setSortColumn(Column<T> column) {
-    _sortColumn = column;
-    _sortDirection =
-        column.numeric ? SortOrder.descending : SortOrder.ascending;
-  }
-
-  void _columnClicked(Column<T> column) {
-    if (!column.supportsSorting) {
-      return;
-    }
-
-    if (_sortColumn == column) {
-      _sortDirection = _sortDirection == SortOrder.ascending
-          ? SortOrder.descending
-          : SortOrder.ascending;
-    } else {
-      setSortColumn(column);
-    }
-
-    _doSort();
-    _scheduleRebuild();
-  }
+  @override
+  void clearSelection() => _select(null, null, null);
 }
 
-abstract class Column<T> {
-  Column(this.title, {this.wide = false});
+class ColumnRenderer<T> {
+  ColumnRenderer(this.model);
 
-  final String title;
-  final bool wide;
+  final ColumnData<T> model;
 
-  String get cssClass => null;
-
-  bool get numeric => false;
-
-  bool get supportsSorting => numeric;
-
-  bool get usesHtml => false;
-
-  /// Get the cell's value from the given [item].
-  dynamic getValue(T item);
-
-  /// Given a value from [getValue], render it to a String.
-  String render(dynamic value) {
-    if (numeric) {
-      return fastIntl(value);
-    }
-    return value.toString();
-  }
-
-  static String fastIntl(num value) {
-    if (value is int && value < 1000) {
-      return value.toString();
-    } else {
-      return nf.format(value);
-    }
-  }
-
-  @override
-  String toString() => title;
+  String get alignmentCss => _getAlignmentCss(model.alignment);
 
   void renderToElement(CoreElement cell, T dataObject) {
-    final String content = render(getValue(dataObject));
-    if (usesHtml) {
+    final String content = model.render(model.getDisplayValue(dataObject));
+    if (model.usesHtml) {
       cell.setInnerHtml(content);
     } else {
       cell.text = content;
     }
+    cell.tooltip = model.getTooltip(dataObject);
+  }
+
+  String _getAlignmentCss(ColumnAlignment alignment) {
+    switch (alignment) {
+      case ColumnAlignment.left:
+        return 'left';
+      case ColumnAlignment.right:
+        return 'right';
+      case ColumnAlignment.center:
+        return 'center';
+      default:
+        throw Exception('Invalid column alignment: $alignment');
+    }
   }
 }
 
-enum SortOrder {
-  ascending,
-  descending,
+class TreeColumnRenderer<T extends TreeNode<T>> extends ColumnRenderer<T> {
+  TreeColumnRenderer(TreeColumnData<T> model) : super(model);
+
+  @override
+  TreeColumnData<T> get model => super.model;
+
+  @override
+  void renderToElement(CoreElement cell, T dataObject) {
+    final container = div()
+      ..layoutHorizontal()
+      ..flex()
+      // Add spacer to beginning of element that reflects tree structure.
+      ..add(div()
+        ..element.style.minWidth = '${model.getNodeIndentPx(dataObject)}px');
+
+    if (dataObject.isExpandable) {
+      final TreeToggle treeToggle = TreeToggle(forceOpen: dataObject.isExpanded)
+        ..onOpen.listen((isOpen) {
+          if (isOpen) {
+            model.nodeExpandedController.add(dataObject);
+          } else {
+            model.nodeCollapsedController.add(dataObject);
+          }
+        });
+      container.add(treeToggle);
+    }
+
+    container.add(div(text: model.render(model.getDisplayValue(dataObject))));
+
+    cell
+      ..clear()
+      ..add(container)
+      ..tooltip = model.getTooltip(dataObject);
+  }
+}
+
+class TreeTable<T extends TreeNode<T>> extends Table<T> {
+  factory TreeTable() => TreeTable._(TreeTableData<T>(), null, false);
+
+  factory TreeTable.virtual({double rowHeight = 29.0}) =>
+      TreeTable._(TreeTableData<T>(), rowHeight, true);
+
+  TreeTable._(TreeTableData<T> model, double rowHeight, bool isVirtual)
+      : super._(model, rowHeight, isVirtual, overflowAuto: true);
+
+  @override
+  TreeTableData<T> get model => super.model;
+
+  @override
+  ColumnRenderer<T> getColumnRenderer(ColumnData<T> columnModel) {
+    return columnModel is TreeColumnData<T>
+        ? TreeColumnRenderer(columnModel)
+        : ColumnRenderer(columnModel);
+  }
+}
+
+class TreeTableToolbar<T extends TreeNode<T>> extends CoreElement {
+  TreeTableToolbar() : super('div') {
+    add(div(c: 'btn-group')
+      ..add([
+        PButton('Expand all')
+          ..small()
+          ..click(_expandAll),
+        PButton('Collapse all')
+          ..small()
+          ..click(_collapseAll),
+      ]));
+  }
+
+  TreeTable<T> treeTable;
+
+  void _expandAll() {
+    treeTable.model.expandAll();
+  }
+
+  void _collapseAll() {
+    treeTable.model.collapseAll();
+  }
 }
diff --git a/devtools/lib/src/timeline/README.md b/devtools/lib/src/timeline/README.md
new file mode 100644
index 0000000..ad49e06
--- /dev/null
+++ b/devtools/lib/src/timeline/README.md
@@ -0,0 +1,23 @@
+## Timeline Code Architecture
+
+<img src="timeline_architecture.png" width="800" />
+
+### View
+
+The view has no awareness of the timeline data directly. It has access to the timeline data (frames, selected event,
+selected frame, cpu profile, etc.) through the `TimelineController`. The view receives updates from the
+`TimelineController` via streams.
+
+### Model
+
+`TimelineData` stores the current data for the DevTools timeline. Its main components include `frames`, `selectedFrame`,
+`selectedEvent`, `cpuProfileData`, and `traceEvents`. This data is maintained by `TimelineController`.
+
+### Controller
+
+The controller manages `TimelineData` and communicates with the view to give and receive data updates. It manages data
+processing via protocols `TimelineProtocol` (protocol for processing trace events and composing them into
+`TimelineEvent`s and `TimelineFrame`s) and `CpuProfileProtocol` (protocol for processing `CpuProfileData` and composing
+it into a structured tree of `CpuStackFrame`s). The controller also communicates with `TimelineService`, which manages
+interactions between the Timeline and the VmService. `TimelineController` has no dependency on `dart:html`, making it
+easily testable.
diff --git a/devtools/lib/src/timeline/cpu_bottom_up.dart b/devtools/lib/src/timeline/cpu_bottom_up.dart
deleted file mode 100644
index 95ff2c8..0000000
--- a/devtools/lib/src/timeline/cpu_bottom_up.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2019 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 '../ui/elements.dart';
-
-class CpuBottomUp extends CoreElement {
-  CpuBottomUp() : super('div', classes: 'ui-details-section') {
-    flex();
-    layoutVertical();
-
-    add(div(text: 'Bottom up view coming soon', c: 'message'));
-  }
-}
diff --git a/devtools/lib/src/timeline/cpu_call_tree.dart b/devtools/lib/src/timeline/cpu_call_tree.dart
deleted file mode 100644
index 8265553..0000000
--- a/devtools/lib/src/timeline/cpu_call_tree.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2019 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 '../ui/elements.dart';
-
-class CpuCallTree extends CoreElement {
-  CpuCallTree() : super('div', classes: 'ui-details-section') {
-    flex();
-    layoutVertical();
-
-    add(div(text: 'Call tree view coming soon', c: 'message'));
-  }
-}
diff --git a/devtools/lib/src/timeline/cpu_profile_protocol.dart b/devtools/lib/src/timeline/cpu_profile_protocol.dart
deleted file mode 100644
index f6103ce..0000000
--- a/devtools/lib/src/timeline/cpu_profile_protocol.dart
+++ /dev/null
@@ -1,204 +0,0 @@
-// Copyright 2019 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 'dart:math';
-
-import 'package:meta/meta.dart';
-import 'package:vm_service_lib/vm_service_lib.dart' show Response;
-
-import '../utils.dart';
-
-class CpuProfileData {
-  CpuProfileData(this.cpuProfileResponse, this.duration)
-      : sampleCount = cpuProfileResponse.json['sampleCount'],
-        samplePeriod = cpuProfileResponse.json['samplePeriod'],
-        stackFramesJson = cpuProfileResponse.json['stackFrames'],
-        stackTraceEvents = cpuProfileResponse.json['traceEvents'] {
-    _processStackFrames(cpuProfileResponse);
-    _setExclusiveSampleCounts();
-
-    assert(
-      sampleCount == cpuProfileRoot.inclusiveSampleCount,
-      'SampleCount from response ($sampleCount) != sample count from root'
-      ' (${cpuProfileRoot.inclusiveSampleCount})',
-    );
-  }
-
-  // Key fields from the response JSON.
-  static const name = 'name';
-  static const category = 'category';
-  static const parentId = 'parent';
-  static const stackFrameId = 'sf';
-  static const resolvedUrl = 'resolvedUrl';
-
-  final Response cpuProfileResponse;
-  final Duration duration;
-  final int sampleCount;
-  final int samplePeriod;
-  final Map<String, dynamic> stackFramesJson;
-
-  /// Trace events associated with the last stackFrame in each sample (i.e. the
-  /// leaves of the [CpuStackFrame] objects).
-  ///
-  /// The trace event will contain a field 'sf' that contains the id of the leaf
-  /// stack frame.
-  final List<dynamic> stackTraceEvents;
-
-  final cpuProfileRoot = CpuStackFrame(
-    id: 'cpuProfile',
-    name: 'all',
-    category: 'Dart',
-    url: 'root',
-  );
-
-  Map<String, CpuStackFrame> stackFrames = {};
-
-  void _processStackFrames(Response response) {
-    stackFramesJson.forEach((k, v) {
-      final stackFrame = CpuStackFrame(
-        id: k,
-        name: v[name],
-        category: v[category],
-        // If the user is on a version of Flutter where resolvedUrl is not
-        // included in the response, this will be null. If the frame is a native
-        // frame, the this will be the empty string.
-        url: v[resolvedUrl],
-      );
-      _processStackFrame(stackFrame, stackFrames[v[parentId]]);
-    });
-  }
-
-  void _processStackFrame(CpuStackFrame stackFrame, CpuStackFrame parent) {
-    stackFrames[stackFrame.id] = stackFrame;
-
-    if (parent == null) {
-      // [stackFrame] is the root of a new cpu sample. Add it as a child of
-      // [cpuProfile].
-      cpuProfileRoot.addChild(stackFrame);
-    } else {
-      parent.addChild(stackFrame);
-    }
-  }
-
-  void _setExclusiveSampleCounts() {
-    for (Map<String, dynamic> traceEvent in stackTraceEvents) {
-      final leafId = traceEvent[stackFrameId];
-      assert(
-        stackFrames[leafId] != null,
-        'No StackFrame found for id $leafId. If you see this assertion, please '
-        'export the timeline trace and send to kenzieschmoll@google.com. Note: '
-        'you must export the timeline immediately after the AssertionError is '
-        'thrown.',
-      );
-      stackFrames[leafId]?.exclusiveSampleCount++;
-    }
-  }
-}
-
-class CpuStackFrame {
-  CpuStackFrame({
-    @required this.id,
-    @required this.name,
-    @required this.category,
-    @required this.url,
-  });
-
-  final String id;
-  final String name;
-  final String category;
-  final String url;
-
-  CpuStackFrame parent;
-  List<CpuStackFrame> children = [];
-
-  /// Index in [parent.children].
-  int index = -1;
-
-  /// How many cpu samples for which this frame is a leaf.
-  int exclusiveSampleCount = 0;
-
-  /// Depth of this CpuStackFrame tree, including [this].
-  ///
-  /// We assume that CpuStackFrame nodes are not modified after the first time
-  /// [depth] is accessed. We would need to clear the cache if this was
-  /// supported.
-  int get depth {
-    if (_depth != 0) {
-      return _depth;
-    }
-    for (CpuStackFrame child in children) {
-      _depth = max(_depth, child.depth);
-    }
-    return _depth = _depth + 1;
-  }
-
-  int _depth = 0;
-
-  int get inclusiveSampleCount =>
-      _inclusiveSampleCount ?? calculateInclusiveSampleCount();
-
-  /// How many cpu samples this frame is included in.
-  int _inclusiveSampleCount;
-
-  double get cpuConsumptionRatio => _cpuConsumptionRatio ??=
-      inclusiveSampleCount / getRoot().inclusiveSampleCount;
-
-  double _cpuConsumptionRatio;
-
-  void addChild(CpuStackFrame child) {
-    children.add(child);
-    child.parent = this;
-    child.index = children.length - 1;
-  }
-
-  CpuStackFrame getRoot() {
-    CpuStackFrame root = this;
-    while (root.parent != null) {
-      root = root.parent;
-    }
-    return root;
-  }
-
-  /// Returns the number of cpu samples this stack frame is a part of.
-  ///
-  /// This will be equal to the number of leaf nodes under this stack frame.
-  int calculateInclusiveSampleCount() {
-    int count = exclusiveSampleCount;
-    for (CpuStackFrame child in children) {
-      count += child.inclusiveSampleCount;
-    }
-    _inclusiveSampleCount = count;
-    return _inclusiveSampleCount;
-  }
-
-  void _format(StringBuffer buf, String indent) {
-    buf.writeln(
-        '$indent$id - children: ${children.length} - exclusiveSampleCount: '
-        '$exclusiveSampleCount');
-    for (CpuStackFrame child in children) {
-      child._format(buf, '  $indent');
-    }
-  }
-
-  @visibleForTesting
-  String toStringDeep() {
-    final buf = StringBuffer();
-    _format(buf, '  ');
-    return buf.toString();
-  }
-
-  @override
-  String toString({Duration duration}) {
-    final buf = StringBuffer();
-    buf.write('$name ');
-    if (duration != null) {
-      // TODO(kenzie): use a number of fractionDigits that better matches the
-      // resolution of the stack frame.
-      buf.write('- ${msText(duration, fractionDigits: 2)} ');
-    }
-    buf.write('($inclusiveSampleCount ');
-    buf.write(inclusiveSampleCount == 1 ? 'sample' : 'samples');
-    buf.write(', ${percent2(cpuConsumptionRatio)})');
-    return buf.toString();
-  }
-}
diff --git a/devtools/lib/src/timeline/event_details.dart b/devtools/lib/src/timeline/event_details.dart
index 865c452..7e7e0ee 100644
--- a/devtools/lib/src/timeline/event_details.dart
+++ b/devtools/lib/src/timeline/event_details.dart
@@ -1,47 +1,29 @@
 // Copyright 2019 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 'dart:async';
 import 'dart:html' as html;
-import 'dart:math' as math;
 
 import 'package:js/js.dart';
-import 'package:vm_service_lib/vm_service_lib.dart' hide TimelineEvent;
 
 import '../globals.dart';
-import '../ui/custom.dart';
+import '../profiler/cpu_profile_flame_chart.dart';
+import '../profiler/cpu_profile_tables.dart';
+import '../profiler/cpu_profiler.dart';
+import '../ui/colors.dart';
 import '../ui/elements.dart';
 import '../ui/fake_flutter/dart_ui/dart_ui.dart';
 import '../ui/flutter_html_shim.dart';
-import '../ui/primer.dart';
 import '../ui/theme.dart';
 import '../utils.dart';
-import 'cpu_bottom_up.dart';
-import 'cpu_call_tree.dart';
-import 'cpu_profile_protocol.dart';
-import 'flame_chart_canvas.dart';
-import 'frame_flame_chart.dart';
-import 'timeline.dart';
 import 'timeline_controller.dart';
-import 'timeline_protocol.dart';
-
-/// StreamController that handles loading a CPU profile from snapshot.
-final StreamController<TimelineSnapshot> _loadProfileSnapshotController =
-    StreamController<TimelineSnapshot>.broadcast();
-
-/// Stream for CPU profile snapshot loads.
-Stream<TimelineSnapshot> get _onLoadProfileSnapshot =>
-    _loadProfileSnapshotController.stream;
 
 class EventDetails extends CoreElement {
-  EventDetails() : super('div') {
+  EventDetails(this.timelineController) : super('div') {
     flex();
     layoutVertical();
 
     _initContent();
-
-    assert(tabNav != null);
-    assert(content != null);
+    _initListeners();
 
     // The size of the event details section will change as the splitter is
     // is moved. Observe resizing so that we can rebuild the flame chart canvas
@@ -50,35 +32,14 @@
     // https://github.com/dart-lang/html/issues/104 is fixed.
     final observer =
         html.ResizeObserver(allowInterop((List<dynamic> entries, _) {
-      _details.uiEventDetails.flameChart.updateForContainerResize();
+      cpuProfiler.flameChart.updateForContainerResize();
     }));
     observer.observe(element);
 
-    add([tabNav, content]);
+    assert(tabNav != null);
+    assert(content != null);
 
-    onLoadTimelineSnapshot.listen((snapshot) {
-      clearCurrentSnapshot();
-      if (snapshot.hasCpuProfile) {
-        _loadProfileSnapshotController.add(snapshot);
-      }
-    });
-
-    _onLoadProfileSnapshot.listen((snapshot) {
-      attribute('hidden', false);
-      _setTitleText(
-        snapshot.selectedEvent.name,
-        Duration(
-            microseconds: snapshot.selectedEvent.time.duration.inMicroseconds),
-      );
-      _title.element.style
-        ..backgroundColor = colorToCss(mainUiColor)
-        ..color = colorToCss(Colors.black);
-      _details.attribute('hidden', false);
-      _details.gpuEventDetails.attribute('hidden', true);
-      _details.uiEventDetails.attribute('hidden', false);
-
-      _details.uiEventDetails.flameChart._loadFromSnapshot(snapshot);
-    });
+    add([tabNav.element, content]);
   }
 
   static const defaultTitleText = '[No event selected]';
@@ -88,424 +49,159 @@
     Color(0xFF2D2E31), // Material Dark Grey 900+2
   );
 
-  PTabNav tabNav;
+  final TimelineController timelineController;
 
-  PTabNavTab _selectedTab;
+  CpuProfilerTabNav tabNav;
 
   CoreElement content;
 
-  TimelineEvent get event => _event;
-
-  TimelineEvent _event;
-
   CoreElement _title;
 
-  _Details _details;
+  _CpuProfiler cpuProfiler;
 
-  CpuProfileData get cpuProfileData =>
-      _details.uiEventDetails.flameChart.cpuProfileData;
+  CoreElement gpuEventDetails;
+
+  Color titleBackgroundColor = defaultTitleBackground;
+
+  Color titleTextColor = contrastForeground;
 
   void _initContent() {
     _title = div(text: defaultTitleText, c: 'event-details-heading');
     _title.element.style
-      ..color = colorToCss(contrastForeground)
-      ..backgroundColor = colorToCss(defaultTitleBackground);
-    _details = _Details()..attribute('hidden');
+      ..color = colorToCss(titleTextColor)
+      ..backgroundColor = colorToCss(titleBackgroundColor);
+
+    final details = div(c: 'event-details')
+      ..layoutVertical()
+      ..flex()
+      ..add([
+        cpuProfiler = _CpuProfiler(
+          timelineController,
+          () => timelineController.timelineData.cpuProfileData,
+        )..hidden(true),
+        // TODO(kenzie): eventually we should show something in this area that
+        // is useful for GPU events as well (tips, links to docs, etc).
+        gpuEventDetails = div(
+          text: 'CPU profiling is not available for GPU events.',
+          c: 'centered-single-line-message',
+        )..hidden(true),
+      ]);
 
     content = div(c: 'event-details-section section-border')
       ..flex()
-      ..add(<CoreElement>[_title, _details]);
+      ..add(<CoreElement>[_title, details]);
 
-    _initTabNav();
+    tabNav = CpuProfilerTabNav(
+      cpuProfiler,
+      CpuProfilerTabOrder(
+        first: CpuProfilerViewType.flameChart,
+        second: CpuProfilerViewType.callTree,
+        third: CpuProfilerViewType.bottomUp,
+      ),
+    );
   }
 
-  void _initTabNav() {
-    final flameChartTab = EventDetailsTabNavTab(
-      'CPU Flame Chart (preview)',
-      EventDetailsTabType.flameChart,
-    );
-    final bottomUpTab = EventDetailsTabNavTab(
-      'Bottom Up',
-      EventDetailsTabType.bottomUp,
-    );
-    final callTreeTab = EventDetailsTabNavTab(
-      'Call Tree',
-      EventDetailsTabType.callTree,
-    );
+  void _initListeners() {
+    timelineController.onSelectedFrame.listen((_) => reset());
 
-    tabNav = PTabNav(<EventDetailsTabNavTab>[
-      flameChartTab,
-      bottomUpTab,
-      callTreeTab,
-    ])
-      ..element.style.borderBottom = '0';
+    timelineController.onSelectedTimelineEvent
+        .listen((_) async => await update());
 
-    _selectedTab = flameChartTab;
-
-    tabNav.onTabSelected.listen((PTabNavTab tab) {
-      // Return early if this tab is already selected.
-      if (tab == _selectedTab) {
-        return;
-      }
-      _selectedTab = tab;
-
-      assert(tab is EventDetailsTabNavTab);
-      switch ((tab as EventDetailsTabNavTab).type) {
-        case EventDetailsTabType.flameChart:
-          _details.uiEventDetails.showTab(EventDetailsTabType.flameChart);
-          break;
-        case EventDetailsTabType.bottomUp:
-          _details.uiEventDetails.showTab(EventDetailsTabType.bottomUp);
-          break;
-        case EventDetailsTabType.callTree:
-          _details.uiEventDetails.showTab(EventDetailsTabType.callTree);
-          break;
+    timelineController.onLoadOfflineData.listen((_) async {
+      if (timelineController.timelineData.selectedEvent != null) {
+        titleTextColor = Colors.black;
+        titleBackgroundColor = mainUiColor;
+        await update();
       }
     });
   }
 
-  void _setTitleText(String name, Duration duration) {
-    _title.text = '$name - ${msText(duration)}';
-  }
+  Future<void> update({bool hide = false}) async {
+    final selectedEvent = timelineController.timelineData?.selectedEvent;
 
-  Future<void> update(FrameFlameChartItem item) async {
-    _event = item.event;
-
-    _setTitleText(_event.name, _event.time.duration);
+    _title.text = selectedEvent != null
+        ? '${selectedEvent.name} - ${msText(selectedEvent.time.duration)}'
+        : defaultTitleText;
     _title.element.style
-      ..backgroundColor = colorToCss(item.backgroundColor)
-      ..color = colorToCss(item.defaultTextColor);
+      ..backgroundColor = colorToCss(titleBackgroundColor)
+      ..color = colorToCss(titleTextColor);
 
-    await _details.update(item.event);
-  }
+    hidden(hide);
+    gpuEventDetails.hidden(selectedEvent?.isUiEvent ?? true);
+    cpuProfiler.hidden(selectedEvent?.isGpuEvent ?? true);
 
-  void reset() {
-    _title.text = defaultTitleText;
-    _title.element.style
-      ..color = colorToCss(contrastForeground)
-      ..backgroundColor = colorToCss(defaultTitleBackground);
-    _details.reset();
-  }
-
-  void clearCurrentSnapshot() {
-    _details.uiEventDetails.flameChart.snapshot = null;
-  }
-}
-
-class _Details extends CoreElement {
-  _Details() : super('div', classes: 'event-details') {
-    layoutVertical();
-    flex();
-
-    uiEventDetails = _UiEventDetails()..attribute('hidden');
-
-    // TODO(kenzie): eventually we should show something in this area that is
-    // useful for GPU events as well (tips, links to docs, etc).
-    gpuEventDetails = div(
-      text: 'CPU profiling is not available for GPU events.',
-      c: 'message',
-    )..attribute('hidden');
-
-    add(uiEventDetails);
-    add(gpuEventDetails);
-  }
-
-  CoreElement gpuEventDetails;
-
-  _UiEventDetails uiEventDetails;
-
-  Future<void> update(TimelineEvent event) async {
-    attribute('hidden', false);
-    gpuEventDetails.attribute('hidden', event.isUiEvent);
-    uiEventDetails.attribute('hidden', event.isGpuEvent);
-
-    if (event.isUiEvent) {
-      await uiEventDetails.update(event);
+    if (selectedEvent != null && selectedEvent.isUiEvent) {
+      await cpuProfiler.update();
     }
   }
 
-  void reset() {
-    gpuEventDetails.attribute('hidden', true);
-    uiEventDetails.attribute('hidden', true);
-    uiEventDetails.reset();
+  void reset({bool hide = false}) {
+    titleTextColor = contrastForeground;
+    titleBackgroundColor = defaultTitleBackground;
+    update(hide: hide);
   }
 }
 
-class _UiEventDetails extends CoreElement {
-  _UiEventDetails() : super('div') {
-    layoutVertical();
-    flex();
+class _CpuProfiler extends CpuProfiler {
+  _CpuProfiler(
+    this._timelineController,
+    CpuProfileDataProvider profileDataProvider,
+  ) : super(
+          CpuFlameChart(profileDataProvider),
+          CpuCallTree(profileDataProvider),
+          CpuBottomUp(profileDataProvider),
+        );
 
-    add([
-      flameChart = _CpuFlameChart(),
-      bottomUp = CpuBottomUp()..attribute('hidden', true),
-      callTree = CpuCallTree()..attribute('hidden', true),
-    ]);
-  }
+  final TimelineController _timelineController;
 
-  _CpuFlameChart flameChart;
-
-  CpuBottomUp bottomUp;
-
-  CpuCallTree callTree;
-
-  TimelineEvent event;
-
-  void showTab(EventDetailsTabType tabType) {
-    switch (tabType) {
-      case EventDetailsTabType.flameChart:
-        flameChart.show();
-        bottomUp.attribute('hidden', true);
-        callTree.attribute('hidden', true);
-        break;
-      case EventDetailsTabType.bottomUp:
-        flameChart.attribute('hidden', true);
-        bottomUp.attribute('hidden', false);
-        callTree.attribute('hidden', true);
-        break;
-      case EventDetailsTabType.callTree:
-        flameChart.attribute('hidden', true);
-        bottomUp.attribute('hidden', true);
-        callTree.attribute('hidden', false);
-        break;
+  @override
+  Future<void> prepareCpuProfile() async {
+    // Fetch a profile if we are not loading from offline.
+    if (!offlineMode || _timelineController.offlineTimelineData == null) {
+      await _timelineController.getCpuProfileForSelectedEvent();
     }
   }
 
-  Future<void> update(TimelineEvent event) async {
-    if (event == this.event) {
-      return;
-    }
-    reset();
-    this.event = event;
-
-    // TODO(kenzie): update call tree and bottom up views here once they are
-    // implemented.
-    await flameChart.update(event);
-  }
-
-  void reset() {
-    // TODO(kenzie): reset call tree and bottom up views here once they are
-    // implemented.
-    flameChart.reset();
-  }
-}
-
-class _CpuFlameChart extends CoreElement {
-  _CpuFlameChart() : super('div', classes: 'ui-details-section') {
-    stackFrameDetails = div(c: 'event-details-heading stack-frame-details')
-      ..element.style.backgroundColor = colorToCss(stackFrameDetailsBackground)
-      ..attribute('hidden', true);
-
-    add(stackFrameDetails);
-  }
-
-  static const String stackFrameDetailsDefaultText =
-      '[No stack frame selected]';
-
-  static const stackFrameDetailsBackground = ThemedColor(
-    Color(0xFFF6F6F6),
-    Color(0xFF202124),
-  );
-
-  FlameChartCanvas canvas;
-
-  CoreElement stackFrameDetails;
-
-  CpuProfileData cpuProfileData;
-
-  TimelineEvent event;
-
-  /// Stores the latest timeline snapshot.
-  ///
-  /// This will be null if a cpu profile was not loaded from snapshot.
-  TimelineSnapshot snapshot;
-
-  bool canvasNeedsRebuild = false;
-
-  bool showingMessage = false;
-
-  Future<void> _getCpuProfile() async {
-    final Response response =
-        await serviceManager.service.getCpuProfileTimeline(
-      serviceManager.isolateManager.selectedIsolate.id,
-      event.time.start.inMicroseconds,
-      event.time.duration.inMicroseconds,
-    );
-
-    cpuProfileData = CpuProfileData(
-      response,
-      event.time.duration,
-    );
-  }
-
-  void _drawFlameChart() {
-    if (cpuProfileData.stackFrames.isEmpty) {
-      final snapshotModeMessage = div()
+  @override
+  bool maybeShowMessageOnUpdate() {
+    if (offlineMode &&
+        _timelineController.timelineData.selectedEvent !=
+            _timelineController.offlineTimelineData?.selectedEvent) {
+      final offlineModeMessage = div()
         ..add(span(
             text:
                 'CPU profiling is not yet available for snapshots. You can only'
                 ' view '));
-      if (snapshot != null && snapshot.hasCpuProfile) {
-        snapshotModeMessage
+      if (_timelineController.offlineTimelineData?.cpuProfileData != null) {
+        offlineModeMessage
           ..add(span(text: 'the '))
           ..add(span(text: 'CPU profile', c: 'message-action')
-            ..click(() => _loadProfileSnapshotController.add(snapshot)))
+            ..click(
+                () => _timelineController.restoreCpuProfileFromOfflineData()))
           ..add(span(text: ' included in the snapshot.'));
       } else {
-        snapshotModeMessage.add(span(
+        offlineModeMessage.add(span(
             text:
                 'a CPU profile if it is included in the imported snapshot file.'));
       }
-
-      _updateChartWithMessage(snapshotMode
-          ? snapshotModeMessage
-          : div(
-              text: 'CPU profile unavailable for time range'
-                  ' [${event.time.start.inMicroseconds} -'
-                  ' ${event.time.end.inMicroseconds}]'));
-      return;
+      showMessage(offlineModeMessage);
+      return true;
     }
 
-    canvas = FlameChartCanvas(
-      data: cpuProfileData,
-      flameChartWidth: element.clientWidth,
-      flameChartHeight: math.max(
-        // Subtract [rowHeightWithPadding] to account for timeline at the top of
-        // the flame chart.
-        element.clientHeight - rowHeightWithPadding,
-        // Add 1 to account for a row of padding at the bottom of the chart.
-        (cpuProfileData.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
-      ),
-    );
-
-    canvas.onStackFrameSelected.listen((CpuStackFrame stackFrame) {
-      final frameDuration = Duration(
-          microseconds: (stackFrame.cpuConsumptionRatio *
-                  event.time.duration.inMicroseconds)
-              .round());
-      stackFrameDetails.text = stackFrame.toString(duration: frameDuration);
-    });
-
-    add(canvas.element);
-
-    if (!showingMessage) {
-      stackFrameDetails
-        ..text = stackFrameDetailsDefaultText
-        ..attribute('hidden', false);
+    if (_timelineController.timelineData.cpuProfileData.stackFrames.isEmpty) {
+      final frameOffset =
+          _timelineController.timelineData.selectedFrame.time.start;
+      final startTime =
+          _timelineController.timelineData.selectedEvent.time.start -
+              frameOffset;
+      final endTime =
+          _timelineController.timelineData.selectedEvent.time.end - frameOffset;
+      showMessage(div(
+          text: 'CPU profile unavailable for time range'
+              ' [${msText(startTime, fractionDigits: 2)} -'
+              ' ${msText(endTime, fractionDigits: 2)}]'));
+      return true;
     }
+    return false;
   }
-
-  void _updateChartWithMessage(CoreElement message) {
-    showingMessage = true;
-    add(message
-      ..id = 'flame-chart-message'
-      ..clazz('message'));
-    stackFrameDetails.attribute('hidden', true);
-  }
-
-  Future<void> update(TimelineEvent event) async {
-    reset();
-    this.event = event;
-
-    // Update the canvas if the flame chart is visible. Otherwise, mark the
-    // canvas as needing a rebuild.
-    if (!isHidden) {
-      final Spinner spinner = Spinner()..clazz('cpu-profile-spinner');
-      add(spinner);
-
-      try {
-        await _getCpuProfile();
-        _drawFlameChart();
-      } on AssertionError catch (e) {
-        _updateChartWithMessage(div(text: e.toString()));
-      } catch (e) {
-        _updateChartWithMessage(
-            div(text: 'Error retrieving CPU profile: ${e.toString()}'));
-      }
-
-      spinner.element.remove();
-    } else {
-      canvasNeedsRebuild = true;
-    }
-  }
-
-  void updateForContainerResize() {
-    if (canvas == null) {
-      return;
-    }
-
-    // Only update the canvas if the flame chart is visible and has data.
-    // Otherwise, mark the canvas as needing a rebuild.
-    if (!isHidden && cpuProfileData != null) {
-      // We need to rebuild the canvas with a new content size so that the
-      // canvas is always at least as tall as the container it is in. This
-      // ensures that the grid lines in the chart will extend all the way to the
-      // bottom of the container.
-      canvas.forceRebuildForSize(
-        canvas.flameChartWidthWithInsets,
-        math.max(
-          // Subtract [rowHeightWithPadding] to account for the size of
-          // [stackFrameDetails] section at the bottom of the chart.
-          element.scrollHeight.toDouble() - rowHeightWithPadding,
-          // Add 1 to account for a row of padding at the bottom of the chart.
-          (cpuProfileData.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
-        ),
-      );
-    } else {
-      canvasNeedsRebuild = true;
-    }
-  }
-
-  Future<void> show() async {
-    attribute('hidden', false);
-
-    if (canvasNeedsRebuild) {
-      assert(event != null);
-      canvasNeedsRebuild = false;
-      await update(event);
-    }
-  }
-
-  void reset() {
-    if (canvas?.element?.element != null) {
-      canvas.element.element.remove();
-    }
-    canvas = null;
-
-    stackFrameDetails.text = stackFrameDetailsDefaultText;
-    stackFrameDetails.attribute('hidden', true);
-
-    _removeMessage();
-
-    cpuProfileData = null;
-  }
-
-  void _removeMessage() {
-    element.children.removeWhere((e) => e.id == 'flame-chart-message');
-    showingMessage = false;
-  }
-
-  void _loadFromSnapshot(TimelineSnapshot snapshot) {
-    _removeMessage();
-    this.snapshot = snapshot;
-    event = snapshot.selectedEvent;
-    cpuProfileData = CpuProfileData(
-      Response.parse(snapshot.cpuProfile),
-      Duration(
-          microseconds: snapshot.selectedEvent.time.duration.inMicroseconds),
-    );
-    _drawFlameChart();
-  }
-}
-
-enum EventDetailsTabType {
-  flameChart,
-  bottomUp,
-  callTree,
-}
-
-class EventDetailsTabNavTab extends PTabNavTab {
-  EventDetailsTabNavTab(String name, this.type) : super(name);
-
-  final EventDetailsTabType type;
 }
diff --git a/devtools/lib/src/timeline/flame_chart.dart b/devtools/lib/src/timeline/flame_chart.dart
deleted file mode 100644
index 5931a26..0000000
--- a/devtools/lib/src/timeline/flame_chart.dart
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright 2019 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 'dart:async';
-import 'dart:html';
-import 'dart:math' as math;
-
-import 'package:meta/meta.dart';
-
-import '../ui/drag_scroll.dart';
-import '../ui/elements.dart';
-import '../ui/fake_flutter/dart_ui/dart_ui.dart';
-import '../ui/flutter_html_shim.dart';
-import '../ui/theme.dart';
-import 'timeline.dart';
-
-// TODO(kenzie): delete this file once frame_flame_chart is ported to canvas.
-
-const selectedFlameChartItemColor =
-    ThemedColor(mainUiColorSelectedLight, mainUiColorSelectedLight);
-
-/// Flame chart superclass that houses zooming, scrolling, and selection logic,
-/// among other data handling methods like `update` and `reset`.
-///
-/// Any subclass of [FlameChart] must override [render] - the method responsible
-/// for drawing the flame chart. Additional method overrides may be necessary.
-class FlameChart<T> extends CoreElement {
-  FlameChart({
-    @required Stream<FlameChartItem> onSelectedFlameChartItem,
-    @required DragScroll dragScroll,
-    String classes,
-    this.flameChartInset = 0,
-  }) : super('div', classes: classes) {
-    flex();
-    layoutVertical();
-
-    dragScroll.enableDragScrolling(this);
-
-    element.onMouseWheel.listen(_handleMouseWheel);
-
-    onSelectedFlameChartItem.listen(_selectItem);
-  }
-
-  final int flameChartInset;
-
-  static const padding = 2;
-  static const int rowHeight = 25;
-
-  /// All flame chart items currently drawn on the chart.
-  final List<FlameChartItem> chartItems = [];
-
-  /// Maximum scroll delta allowed for scrollwheel based zooming.
-  ///
-  /// This isn't really needed but is a reasonable for safety in case we
-  /// aren't handling some mouse based scroll wheel behavior well, etc.
-  final num maxScrollWheelDelta = 20;
-
-  /// Maximum zoom level we should allow.
-  ///
-  /// Arbitrary large number to accommodate spacing for some of the shortest
-  /// events when zoomed in to [_maxZoomLevel].
-  final _maxZoomLevel = 150;
-  final _minZoomLevel = 1;
-  num zoomLevel = 1;
-
-  num get _zoomMultiplier => zoomLevel * 0.003;
-
-  // The DOM doesn't allow floating point scroll offsets so we track a
-  // theoretical floating point scroll offset corresponding to the current
-  // scroll offset to reduce floating point error when zooming.
-  num floatingPointScrollLeft = 0;
-
-  FlameChartItem selectedItem;
-
-  T data;
-
-  /// Method responsible for drawing the flame chart.
-  ///
-  /// This method is REQUIRED to be overridden by a subclass - otherwise, the
-  /// chart will be blank.
-  void render() {}
-
-  void update(T _data) {
-    data = _data;
-    reset();
-
-    if (_data != null) {
-      render();
-    }
-  }
-
-  void reset() {
-    clear();
-    element.scrollLeft = 0;
-    element.scrollTop = 0;
-    zoomLevel = 1;
-    chartItems.clear();
-  }
-
-  void addItemToFlameChart(FlameChartItem item, CoreElement container) {
-    chartItems.add(item);
-    container.element.append(item.element);
-  }
-
-  num getFlameChartWidth() {
-    num maxRight = 0;
-    for (FlameChartItem item in chartItems) {
-      if ((item.currentLeft + item.currentWidth) > maxRight) {
-        maxRight = item.currentLeft + item.currentWidth;
-      }
-    }
-    // Subtract [beginningInset] to account for spacing at the beginning of the
-    // chart.
-    return maxRight - flameChartInset;
-  }
-
-  void _selectItem(FlameChartItem item) {
-    // Unselect the previously selected item.
-    selectedItem?.setSelected(false);
-
-    // Select the new item.
-    item.setSelected(true);
-    selectedItem = item;
-  }
-
-  void _handleMouseWheel(WheelEvent e) {
-    e.preventDefault();
-
-    if (e.deltaY.abs() >= e.deltaX.abs()) {
-      final mouseX = e.client.x - element.getBoundingClientRect().left;
-      _zoom(e.deltaY, mouseX);
-    } else {
-      // Manually perform horizontal scrolling.
-      element.scrollLeft += e.deltaX.round();
-    }
-  }
-
-  void _zoom(num deltaY, num mouseX) {
-    assert(data != null);
-
-    deltaY = deltaY.clamp(-maxScrollWheelDelta, maxScrollWheelDelta);
-    num newZoomLevel = zoomLevel + deltaY * _zoomMultiplier;
-    newZoomLevel = newZoomLevel.clamp(_minZoomLevel, _maxZoomLevel);
-
-    if (newZoomLevel == zoomLevel) return;
-    // Store current scroll values for re-calculating scroll location on zoom.
-    num lastScrollLeft = element.scrollLeft;
-    // Test whether the scroll offset has changed by more than rounding error
-    // since the last time an exact scroll offset was calculated.
-    if ((floatingPointScrollLeft - lastScrollLeft).abs() < 0.5) {
-      lastScrollLeft = floatingPointScrollLeft;
-    }
-    // Position in the zoomable coordinate space that we want to keep fixed.
-    final num fixedX = mouseX + lastScrollLeft - flameChartInset;
-    // Calculate and set our new horizontal scroll position.
-    if (fixedX >= 0) {
-      floatingPointScrollLeft =
-          fixedX * newZoomLevel / zoomLevel + flameChartInset - mouseX;
-    } else {
-      // No need to transform as we are in the fixed portion of the window.
-      floatingPointScrollLeft = lastScrollLeft;
-    }
-    zoomLevel = newZoomLevel;
-
-    updateChartForZoom();
-  }
-
-  void updateChartForZoom() {
-    for (FlameChartItem item in chartItems) {
-      item.updateHorizontalPosition(zoom: zoomLevel);
-    }
-  }
-}
-
-class FlameChartItem {
-  FlameChartItem({
-    @required this.startingLeft,
-    @required this.startingWidth,
-    @required this.top,
-    @required this.backgroundColor,
-    @required this.defaultTextColor,
-    @required this.selectedTextColor,
-    this.flameChartInset = 0,
-  }) {
-    element = Element.div()..className = 'flame-chart-item';
-    _labelWrapper = Element.div()..className = 'flame-chart-item-label-wrapper';
-
-    itemLabel = Element.span()
-      ..className = 'flame-chart-item-label'
-      ..style.color = colorToCss(defaultTextColor);
-    _labelWrapper.append(itemLabel);
-    element.append(_labelWrapper);
-
-    element.style
-      ..background = colorToCss(backgroundColor)
-      ..top = '${top}px';
-    updateHorizontalPosition(zoom: 1);
-
-    setText();
-    setOnClick();
-  }
-
-  /// Pixels of padding to place on the right side of the label to ensure label
-  /// text does not get too close to the right hand size of each div.
-  static const labelPaddingRight = 4;
-
-  static const selectedBorderColor = ThemedColor(
-    Color(0x5A1B1F23),
-    Color(0x5A1B1F23),
-  );
-
-  /// Left value for the flame chart item at zoom level 1.
-  final num startingLeft;
-
-  /// Width value for the flame chart item at zoom level 1;
-  final num startingWidth;
-
-  /// Top position for the flame chart item.
-  final num top;
-
-  final Color backgroundColor;
-
-  final Color defaultTextColor;
-
-  final Color selectedTextColor;
-
-  /// Inset for the start/end of the flame chart.
-  final int flameChartInset;
-
-  Element element;
-  Element itemLabel;
-  Element _labelWrapper;
-
-  num currentLeft;
-  num currentWidth;
-
-  // This method should be overridden by the subclass.
-  void setText() {}
-
-  // TODO(kenzie): set a global click listener instead of setting one per item.
-  // This method should be overridden by the subclass.
-  void setOnClick() {}
-
-  void updateHorizontalPosition({@required num zoom}) {
-    // Do not round these values. Rounding the left could cause us to have
-    // inaccurately placed events on the chart. Rounding the width could cause
-    // us to lose very small events if the width rounds to zero.
-    final newLeft = flameChartInset + startingLeft * zoom;
-    final newWidth = startingWidth * zoom;
-
-    element.style.left = '${newLeft}px';
-    if (startingWidth != null) {
-      element.style.width = '${newWidth}px';
-      _labelWrapper.style.maxWidth =
-          '${math.max(0, newWidth - labelPaddingRight)}px';
-    }
-    currentLeft = newLeft;
-    currentWidth = newWidth;
-  }
-
-  void setSelected(bool selected) {
-    element.style
-      ..backgroundColor =
-          colorToCss(selected ? selectedFlameChartItemColor : backgroundColor)
-      ..border = selected ? '1px solid' : 'none'
-      ..borderColor = colorToCss(selectedBorderColor);
-    itemLabel.style.color =
-        colorToCss(selected ? selectedTextColor : defaultTextColor);
-  }
-}
diff --git a/devtools/lib/src/timeline/frame_flame_chart.dart b/devtools/lib/src/timeline/frame_flame_chart.dart
deleted file mode 100644
index 9db5613..0000000
--- a/devtools/lib/src/timeline/frame_flame_chart.dart
+++ /dev/null
@@ -1,445 +0,0 @@
-// Copyright 2019 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 'dart:async';
-import 'dart:math' as math;
-
-import '../ui/drag_scroll.dart';
-import '../ui/elements.dart';
-import '../ui/fake_flutter/dart_ui/dart_ui.dart';
-import '../ui/flutter_html_shim.dart';
-import '../ui/theme.dart';
-import '../utils.dart';
-import 'flame_chart.dart';
-import 'timeline.dart';
-import 'timeline_protocol.dart';
-
-// TODO(kenzie): port all of this code to use flame_chart_canvas.dart.
-
-// Light Blue 50: 200-400 (light mode) - see https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
-// Blue Material Dark: 200-400 (dark mode) - see https://standards.google/guidelines/google-material/color/dark-theme.html#style.
-final uiColorPalette = [
-  const ThemedColor(mainUiColorLight, mainUiColorDark),
-  const ThemedColor(Color(0xFF4FC3F7), Color(0xFF8AB4F7)),
-  const ThemedColor(Color(0xFF29B6F6), Color(0xFF669CF6)),
-];
-
-// Light Blue 50: 700-900 (light mode) - see https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
-// Blue Material Dark: 500-700 (dark mode) - see https://standards.google/guidelines/google-material/color/dark-theme.html#style.
-final gpuColorPalette = [
-  const ThemedColor(mainGpuColorLight, mainGpuColorDark),
-  const ThemedColor(Color(0xFF0277BD), Color(0xFF1966D2)),
-  const ThemedColor(Color(0xFF01579B), Color(0xFF1859BD)),
-];
-
-final StreamController<FrameFlameChartItem>
-    _selectedFrameFlameChartItemController =
-    StreamController<FrameFlameChartItem>.broadcast();
-
-Stream<FrameFlameChartItem> get onSelectedFrameFlameChartItem =>
-    _selectedFrameFlameChartItemController.stream;
-
-final DragScroll _dragScroll = DragScroll();
-
-const _flameChartInset = 70;
-
-class FrameFlameChart extends FlameChart<TimelineFrame> {
-  FrameFlameChart()
-      : super(
-          onSelectedFlameChartItem: onSelectedFrameFlameChartItem,
-          dragScroll: _dragScroll,
-          classes: 'section-border flame-chart-container',
-          flameChartInset: _flameChartInset,
-        );
-
-  static const int sectionSpacing = 15;
-
-  TimelineGrid _timelineGrid;
-  CoreElement _flameChart;
-  CoreElement _timelineBackground;
-  CoreElement _uiSection;
-  CoreElement _gpuSection;
-
-  int _uiColorOffset = 0;
-  int _gpuColorOffset = 0;
-
-  Color nextUiColor() {
-    final color = uiColorPalette[_uiColorOffset % uiColorPalette.length];
-    _uiColorOffset++;
-    return color;
-  }
-
-  Color nextGpuColor() {
-    final color = gpuColorPalette[_gpuColorOffset % gpuColorPalette.length];
-    _gpuColorOffset++;
-    return color;
-  }
-
-  @override
-  void reset() {
-    super.reset();
-    _uiColorOffset = 0;
-    _gpuColorOffset = 0;
-  }
-
-  @override
-  void render() {
-    final TimelineFrame frame = data;
-
-    /// Pixels per microsecond in order to fit the entire frame in view.
-    ///
-    /// Subtract 2 * [sectionLabelOffset] to account for extra space at the
-    /// beginning/end of the chart.
-    final double pxPerMicro = (element.clientWidth - 2 * flameChartInset) /
-        frame.time.duration.inMicroseconds;
-
-    final int frameStartOffset = frame.time.start.inMicroseconds;
-    final uiSectionHeight =
-        frame.uiEventFlow.depth * FlameChart.rowHeight + sectionSpacing;
-    final gpuSectionHeight = frame.gpuEventFlow.depth * FlameChart.rowHeight;
-    final flameChartHeight =
-        2 * FlameChart.rowHeight + uiSectionHeight + gpuSectionHeight;
-
-    void drawSubtree(
-      TimelineEvent event,
-      int row,
-      CoreElement section, {
-      bool includeDuration = false,
-    }) {
-      // Do not round these values. Rounding the left could cause us to have
-      // inaccurately placed events on the chart. Rounding the width could cause
-      // us to lose very small events if the width rounds to zero.
-      final double startPx =
-          (event.time.start.inMicroseconds - frameStartOffset) * pxPerMicro;
-      final double endPx =
-          (event.time.end.inMicroseconds - frameStartOffset) * pxPerMicro;
-
-      _drawFlameChartItem(
-        event,
-        startPx,
-        endPx - startPx,
-        row * FlameChart.rowHeight + FlameChart.padding,
-        section,
-        includeDuration: includeDuration,
-      );
-
-      for (TimelineEvent child in event.children) {
-        drawSubtree(child, row + 1, section);
-      }
-    }
-
-    void drawTimelineBackground() {
-      _timelineBackground = div(c: 'timeline-background')
-        ..element.style.height = '${FlameChart.rowHeight}px';
-      add(_timelineBackground);
-    }
-
-    void drawUiEvents() {
-      _uiSection = div(c: 'flame-chart-section ui');
-      _flameChart.add(_uiSection);
-
-      _uiSection.element.style
-        ..height = '${uiSectionHeight}px'
-        ..top = '${FlameChart.rowHeight}px';
-
-      final sectionTitle =
-          div(text: 'UI', c: 'flame-chart-item flame-chart-title');
-      sectionTitle.element.style
-        ..background = colorToCss(mainUiColor)
-        ..left = '${FlameChart.padding}px'
-        ..top = '${FlameChart.padding}px';
-      _uiSection.add(sectionTitle);
-
-      drawSubtree(
-        frame.uiEventFlow,
-        0,
-        _uiSection,
-        includeDuration: true,
-      );
-    }
-
-    void drawGpuEvents() {
-      _gpuSection = div(c: 'flame-chart-section');
-      _flameChart.add(_gpuSection);
-
-      _gpuSection.element.style
-        ..height = '${gpuSectionHeight}px'
-        ..top = '${FlameChart.rowHeight + uiSectionHeight}px';
-
-      final sectionTitle =
-          div(text: 'GPU', c: 'flame-chart-item flame-chart-title');
-      sectionTitle.element.style
-        ..background = colorToCss(mainGpuColor)
-        ..left = '${FlameChart.padding}px'
-        ..top = '${FlameChart.padding}px';
-      _gpuSection.add(sectionTitle);
-
-      drawSubtree(
-        frame.gpuEventFlow,
-        0,
-        _gpuSection,
-        includeDuration: true,
-      );
-    }
-
-    void drawTimelineGrid() {
-      _timelineGrid = TimelineGrid(
-        frame.time.duration,
-        getFlameChartWidth(),
-      );
-      _timelineGrid.element.style.height = '${flameChartHeight}px';
-      add(_timelineGrid);
-    }
-
-    _flameChart = div(c: 'flame-chart')
-      ..flex()
-      ..layoutVertical();
-    _flameChart.element.style.height = '${flameChartHeight}px';
-    add(_flameChart);
-
-    drawTimelineBackground();
-    drawUiEvents();
-    drawGpuEvents();
-    drawTimelineGrid();
-
-    _setSectionWidths();
-  }
-
-  void _drawFlameChartItem(
-    TimelineEvent event,
-    num left,
-    num width,
-    num top,
-    CoreElement section, {
-    bool includeDuration = false,
-  }) {
-    final item = FrameFlameChartItem(
-      event,
-      left,
-      width,
-      top,
-      event.isUiEvent ? nextUiColor() : nextGpuColor(),
-      event.isUiEvent ? Colors.black : contrastForegroundWhite,
-      Colors.black,
-      includeDuration: includeDuration,
-    );
-    addItemToFlameChart(item, section);
-  }
-
-  void _setSectionWidths() {
-    // Add 2 * [flameChartInset] to account for spacing at the beginning and end
-    // of the chart.
-    final width = getFlameChartWidth() + 2 * flameChartInset;
-    _flameChart.element.style.width = '${width}px';
-    _timelineBackground.element.style.width = '${width}px';
-    _uiSection.element.style.width = '${width}px';
-    _gpuSection.element.style.width = '${width}px';
-  }
-
-  @override
-  void updateChartForZoom() {
-    super.updateChartForZoom();
-
-    _setSectionWidths();
-    _timelineGrid.updateForZoom(zoomLevel, getFlameChartWidth());
-
-    element.scrollLeft = math.max(0, floatingPointScrollLeft.round());
-  }
-}
-
-class FrameFlameChartItem extends FlameChartItem {
-  FrameFlameChartItem(
-    this._event,
-    num startingLeft,
-    num startingWidth,
-    num top,
-    Color backgroundColor,
-    Color defaultTextColor,
-    Color selectedTextColor, {
-    this.includeDuration = false,
-  }) : super(
-          startingLeft: startingLeft,
-          startingWidth: startingWidth,
-          top: top,
-          backgroundColor: backgroundColor,
-          defaultTextColor: defaultTextColor,
-          selectedTextColor: selectedTextColor,
-          flameChartInset: _flameChartInset,
-        );
-
-  TimelineEvent get event => _event;
-  final TimelineEvent _event;
-  final bool includeDuration;
-
-  @override
-  void setText() {
-    final durationText = msText(event.time.duration);
-
-    String title = _event.name;
-    element.title = '$title ($durationText)';
-
-    if (includeDuration) {
-      title = '$title ($durationText)';
-    }
-
-    itemLabel.text = title;
-  }
-
-  @override
-  void setOnClick() {
-    element.onClick.listen((e) {
-      // Prevent clicks when the chart was being dragged.
-      if (!_dragScroll.wasDragged) {
-        _selectedFrameFlameChartItemController.add(this);
-      }
-    });
-  }
-}
-
-class TimelineGrid extends CoreElement {
-  TimelineGrid(this._frameDuration, this._flameChartWidth)
-      : super('div', classes: 'flame-chart-grid') {
-    flex();
-    layoutHorizontal();
-    _initializeGrid(baseGridInterval);
-  }
-
-  static const baseGridInterval = 150;
-
-  final Duration _frameDuration;
-
-  num _zoomLevel = 1;
-
-  num _flameChartWidth;
-
-  num get _flameChartWidthWithInsets => _flameChartWidth + 2 * _flameChartInset;
-
-  final List<TimelineGridItem> _gridItems = [];
-
-  void _initializeGrid(num interval) {
-    // Draw the first grid item since it will have a different width than the
-    // rest.
-    final gridItem =
-        TimelineGridItem(0, _flameChartInset, const Duration(microseconds: 0));
-    _gridItems.add(gridItem);
-    add(gridItem);
-
-    num left = _flameChartInset;
-
-    while (left + interval < _flameChartWidthWithInsets) {
-      // TODO(kenzie): Instead of calculating timestamp based on position, track
-      // timestamp var and increment it by time interval represented by each
-      // grid item. See comment on https://github.com/flutter/devtools/pull/325.
-      final Duration timestamp =
-          Duration(microseconds: getTimestampForPosition(left + interval));
-      final gridItem = TimelineGridItem(left, interval, timestamp);
-
-      _gridItems.add(gridItem);
-      add(gridItem);
-
-      left += interval;
-    }
-  }
-
-  /// Returns the timestamp rounded to the nearest microsecond for the
-  /// x-position.
-  int getTimestampForPosition(num gridItemEnd) {
-    return ((gridItemEnd - _flameChartInset) /
-            _flameChartWidth *
-            _frameDuration.inMicroseconds)
-        .round();
-  }
-
-  void updateForZoom(num newZoomLevel, num newFlameChartWidth) {
-    if (_zoomLevel == newZoomLevel) {
-      return;
-    }
-
-    _flameChartWidth = newFlameChartWidth;
-    element.style.width = '${_flameChartWidthWithInsets}px';
-
-    final log2ZoomLevel = log2(_zoomLevel);
-    final log2NewZoomLevel = log2(newZoomLevel);
-
-    final gridZoomFactor = math.pow(2, log2NewZoomLevel);
-    final gridIntervalPx = baseGridInterval / gridZoomFactor;
-
-    /// The physical pixel width of the grid interval at [newZoomLevel].
-    final zoomedGridIntervalPx = gridIntervalPx * newZoomLevel;
-
-    // TODO(kenzie): add tests for grid drawing and zooming logic.
-    if (log2NewZoomLevel == log2ZoomLevel) {
-      // Don't modify the first grid item. This item will have a fixed left of
-      // 0, width of [flameChartInset], and timestamp of '0.0 ms'.
-      for (int i = 1; i < _gridItems.length; i++) {
-        final currentItem = _gridItems[i];
-
-        final newLeft = _flameChartInset + zoomedGridIntervalPx * (i - 1);
-        currentItem.setPosition(newLeft, zoomedGridIntervalPx);
-      }
-    } else {
-      clear();
-      _gridItems.clear();
-      _initializeGrid(zoomedGridIntervalPx);
-    }
-
-    _zoomLevel = newZoomLevel;
-  }
-}
-
-/// Describes a single item in the frame chart's timeline grid.
-///
-/// A single item consists of a line and a timestamp describing the location
-/// in the overall timeline [TimelineGrid].
-class TimelineGridItem extends CoreElement {
-  TimelineGridItem(this.currentLeft, this.currentWidth, this.timestamp)
-      : super('div', classes: 'flame-chart-grid-item') {
-    _initGridItem();
-  }
-
-  static const gridLineWidth = 1;
-  static const timestampPadding = 4;
-
-  final Duration timestamp;
-  num currentLeft;
-  num currentWidth;
-
-  /// The timestamp label for this grid item.
-  CoreElement timestampLabel;
-
-  /// The line for this grid item.
-  CoreElement gridLine;
-
-  void _initGridItem() {
-    gridLine = div(c: 'grid-line');
-    add(gridLine);
-
-    timestampLabel = div(c: 'timestamp')
-      ..element.style.color = colorToCss(contrastForeground);
-    // TODO(kenzie): add more advanced logic for rounding the timestamps. See
-    // https://github.com/flutter/devtools/issues/329.
-    timestampLabel.text = msText(
-      timestamp,
-      fractionDigits: timestamp.inMicroseconds == 0 ? 1 : 3,
-    );
-    add(timestampLabel);
-
-    setPosition(currentLeft, currentWidth);
-  }
-
-  void setPosition(num left, num width) {
-    currentLeft = left;
-    currentWidth = width;
-
-    element.style
-      ..left = '${left}px'
-      ..width = '${width}px';
-
-    // Update [gridLine] position.
-    gridLine.element.style.left = '${width - gridLineWidth}px';
-
-    // Update [timestampLabel] position.
-    timestampLabel.element.style.width = '${width - 2 * timestampPadding}px';
-  }
-}
diff --git a/devtools/lib/src/timeline/frames_bar_chart.dart b/devtools/lib/src/timeline/frames_bar_chart.dart
index 6b3b92c..67ef2a0 100644
--- a/devtools/lib/src/timeline/frames_bar_chart.dart
+++ b/devtools/lib/src/timeline/frames_bar_chart.dart
@@ -10,11 +10,11 @@
 import '../ui/plotly.dart';
 import 'frames_bar_plotly.dart';
 import 'timeline_controller.dart';
-import 'timeline_protocol.dart';
+import 'timeline_model.dart';
 
 class FramesBarChart extends CoreElement with SetStateMixin {
-  FramesBarChart(TimelineController timelineController)
-      : super('div', classes: 'timeline-frames') {
+  FramesBarChart(this.timelineController)
+      : super('div', classes: 'timeline-frames section section-border') {
     // No frame around component, so data spikes can appear to go through the
     // roof (highest horizontal line is 100 ms).
     layoutHorizontal();
@@ -44,6 +44,8 @@
   static const int maxFrames = 500;
   static const topPadding = 2;
 
+  final TimelineController timelineController;
+
   TimelineFrame selectedFrame;
   PlotlyDivGraph frameUIgraph;
   bool _createdPlot = false;
@@ -54,19 +56,8 @@
   Stream<TimelineFrame> get onSelectedFrame => _selectedFrameController.stream;
 
   void setSelected(TimelineFrame frame) {
-    if (selectedFrame == frame) {
-      return;
-    }
-
     selectedFrame = frame;
     _selectedFrameController.add(frame);
-
-    ga.selectFrame(
-      ga.timeline,
-      ga.timelineFrame,
-      frame.gpuDuration,
-      frame.uiDuration,
-    );
   }
 }
 
@@ -144,7 +135,13 @@
 
       if (_frames.containsKey(xPosition)) {
         final TimelineFrame timelineFrame = _frames[xPosition];
-        framesBarChart.setSelected(timelineFrame);
+        timelineController.selectFrame(timelineFrame);
+        ga.selectFrame(
+          ga.timeline,
+          ga.timelineFrame,
+          timelineFrame.gpuDuration,
+          timelineFrame.uiDuration,
+        );
       }
     }
   }
diff --git a/devtools/lib/src/timeline/frames_bar_plotly.dart b/devtools/lib/src/timeline/frames_bar_plotly.dart
index e0bbcfe..af0c6cb 100644
--- a/devtools/lib/src/timeline/frames_bar_plotly.dart
+++ b/devtools/lib/src/timeline/frames_bar_plotly.dart
@@ -4,7 +4,7 @@
 
 import 'package:js/js_util.dart';
 
-import '../timeline/timeline.dart';
+import '../ui/colors.dart';
 import '../ui/flutter_html_shim.dart';
 import '../ui/plotly.dart';
 import '../ui/theme.dart';
diff --git a/devtools/lib/src/timeline/timeline.dart b/devtools/lib/src/timeline/timeline.dart
deleted file mode 100644
index 65b83aa..0000000
--- a/devtools/lib/src/timeline/timeline.dart
+++ /dev/null
@@ -1,465 +0,0 @@
-// Copyright 2018 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 'dart:convert';
-
-import 'package:split/split.dart' as split;
-import 'package:vm_service_lib/vm_service_lib.dart' hide TimelineEvent;
-
-import '../framework/framework.dart';
-import '../globals.dart';
-import '../ui/analytics.dart' as ga;
-import '../ui/analytics_platform.dart' as ga_platform;
-import '../ui/elements.dart';
-import '../ui/fake_flutter/dart_ui/dart_ui.dart';
-import '../ui/icons.dart';
-import '../ui/material_icons.dart';
-import '../ui/primer.dart';
-import '../ui/theme.dart';
-import '../ui/ui_utils.dart';
-import '../vm_service_wrapper.dart';
-import 'event_details.dart';
-import 'frame_flame_chart.dart';
-import 'frames_bar_chart.dart';
-import 'timeline_controller.dart';
-import 'timeline_protocol.dart';
-
-// Light mode is Light Blue 50 palette and Dark mode is Blue 50 palette.
-// https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
-const mainUiColorLight = Color(0xFF81D4FA); // Light Blue 50 - 200
-const mainUiColorSelectedLight = Color(0xFFD4D7DA); // Lighter grey.
-
-const mainGpuColorLight = Color(0xFF0288D1); // Light Blue 50 - 700
-const mainGpuColorSelectedLight = Color(0xFFB5B5B5); // Darker grey.
-
-const mainUiColorDark = Color(0xFF9EBEF9); // Blue 200 Material Dark
-const mainUiColorSelectedDark = Colors.white;
-
-const mainGpuColorDark = Color(0xFF1A73E8); // Blue 600 Material Dark
-const mainGpuColorSelectedDark = Color(0xFFC9C9C9); // Grey.
-
-const mainUiColor = ThemedColor(mainUiColorLight, mainUiColorDark);
-const mainGpuColor = ThemedColor(mainGpuColorLight, mainGpuColorDark);
-
-const Color selectedUiColor =
-    ThemedColor(mainUiColorSelectedLight, mainUiColorSelectedDark);
-const Color selectedGpuColor =
-    ThemedColor(mainGpuColorSelectedLight, mainGpuColorSelectedDark);
-
-// Light is Red @ .2 opacity, Dark is Red 200 Material Dark @ .2 opacity.
-const Color jankGlowInside = ThemedColor(Color(0x66FF0000), Color(0x66F29C99));
-// Light is Red @ .5 opacity, Dark is Red 600 Material Dark @ .6 opacity.
-const Color jankGlowEdge = ThemedColor(Color(0x80FF0000), Color(0x99CE191C));
-
-// Red 50 - 400 is light at 1/2 opacity, Dark Red 500 Material Dark.
-const Color highwater16msColor = mainUiColorSelectedLight;
-
-const Color hoverTextHighContrastColor = Colors.white;
-const Color hoverTextColor = Colors.black;
-
-// TODO(devoncarew): show the Skia picture (gpu drawing commands) for a frame
-
-// TODO(devoncarew): show the list of widgets re-drawn during a frame
-
-// TODO(devoncarew): display whether running in debug or profile
-
-// TODO(devoncarew): Have a timeline view thumbnail overview.
-
-// TODO(devoncarew): Switch to showing all timeline events, but highlighting the
-// area associated with the selected frame.
-
-const Icon _exportTimelineIcon = MaterialIcon(
-  'file_download',
-  defaultButtonIconColor,
-  fontSize: 32,
-  iconWidth: 18,
-);
-
-const Icon _clear = MaterialIcon('block', defaultButtonIconColor);
-
-const Icon _exitSnapshotModeIcon =
-    MaterialIcon('clear', defaultButtonIconColor);
-
-class TimelineScreen extends Screen {
-  TimelineScreen({bool disabled, String disabledTooltip})
-      : super(
-          name: 'Timeline',
-          id: timelineScreenId,
-          iconClass: 'octicon-pulse',
-          disabled: disabled,
-          disabledTooltip: disabledTooltip,
-        ) {
-    onLoadTimelineSnapshot.listen((snapshot) {
-      _updateButtonStates();
-      _clearTimeline();
-    });
-  }
-
-  TimelineController timelineController = TimelineController();
-
-  FramesBarChart framesBarChart;
-
-  FrameFlameChart flameChart;
-
-  EventDetails eventDetails;
-
-  PButton pauseButton;
-
-  PButton resumeButton;
-
-  PButton clearButton;
-
-  PButton exportButton;
-
-  PButton exitSnapshotModeButton;
-
-  CoreElement upperButtonSection;
-
-  CoreElement debugButtonSection;
-
-  bool _manuallyPaused = false;
-
-  split.Splitter splitter;
-
-  bool splitterConfigured = false;
-
-  @override
-  CoreElement createContent(Framework framework) {
-    ga_platform.setupDimensions();
-
-    final CoreElement screenDiv = div()..layoutVertical();
-
-    pauseButton = PButton.icon('Pause recording', FlutterIcons.pause_white_2x)
-      ..small()
-      ..primary()
-      ..click(_pauseRecording);
-
-    resumeButton =
-        PButton.icon('Resume recording', FlutterIcons.resume_black_disabled_2x)
-          ..small()
-          ..clazz('margin-left')
-          ..disabled = true
-          ..click(_resumeRecording);
-
-    exportButton = PButton.icon('', _exportTimelineIcon)
-      ..small()
-      ..clazz('margin-left')
-      ..setAttribute('title', 'Export timeline')
-      ..click(_exportTimeline);
-
-    clearButton = PButton.icon('Clear', _clear)
-      ..small()
-      ..clazz('margin-left')
-      ..setAttribute('title', 'Clear timeline')
-      ..click(_clearTimeline);
-
-    exitSnapshotModeButton = PButton.icon(
-      'Exit snapshot mode',
-      _exitSnapshotModeIcon,
-    )
-      ..small()
-      ..setAttribute('title', 'Exit snapshot mode to connect to a VM Service.')
-      ..setAttribute('hidden', 'true')
-      ..click(_exitSnapshotMode);
-
-    upperButtonSection = div(c: 'section')
-      ..layoutHorizontal()
-      ..add(<CoreElement>[
-        div(c: 'btn-group')
-          ..add([
-            pauseButton,
-            resumeButton,
-          ]),
-        clearButton,
-        exitSnapshotModeButton,
-        div()..flex(),
-        debugButtonSection = div(c: 'btn-group'),
-        exportButton,
-      ]);
-
-    _maybeAddDebugButtons();
-
-    screenDiv.add(<CoreElement>[
-      upperButtonSection,
-      div(c: 'section section-border')
-        ..add(framesBarChart = FramesBarChart(timelineController)),
-      div(c: 'section')
-        ..layoutVertical()
-        ..flex()
-        ..add(<CoreElement>[
-          flameChart = FrameFlameChart()..attribute('hidden'),
-          eventDetails = EventDetails()..attribute('hidden'),
-        ]),
-    ]);
-
-    serviceManager.onConnectionAvailable.listen(_handleConnectionStart);
-    if (serviceManager.hasConnection) {
-      _handleConnectionStart(serviceManager.service);
-    }
-    serviceManager.onConnectionClosed.listen(_handleConnectionStop);
-
-    framesBarChart.onSelectedFrame.listen((TimelineFrame frame) {
-      if (frame != null && timelineController.hasStarted) {
-        flameChart.attribute('hidden', frame == null);
-        eventDetails.attribute('hidden', frame == null);
-
-        if (debugTimeline && frame != null) {
-          final buf = StringBuffer();
-          buf.writeln('UI timeline event for frame ${frame.id}:');
-          frame.uiEventFlow.format(buf, '  ');
-          buf.writeln('\nUI trace for frame ${frame.id}');
-          frame.uiEventFlow.writeTraceToBuffer(buf);
-          buf.writeln('\nGPU timeline event frame ${frame.id}:');
-          frame.gpuEventFlow.format(buf, '  ');
-          buf.writeln('\nGPU trace for frame ${frame.id}');
-          frame.gpuEventFlow.writeTraceToBuffer(buf);
-          print(buf.toString());
-        }
-
-        flameChart.update(frame);
-        eventDetails.reset();
-
-        _configureSplitter();
-      }
-    });
-
-    onSelectedFrameFlameChartItem.listen((FrameFlameChartItem item) async {
-      final TimelineEvent event = item.event;
-      ga.select(
-        ga.timeline,
-        event.isGpuEvent ? ga.timelineFlameGpu : ga.timelineFlameUi,
-        event.time.duration.inMicroseconds, // No inMilliseconds loses precision
-      );
-
-      await eventDetails.update(item);
-    });
-
-    maybeShowDebugWarning(framework);
-
-    return screenDiv;
-  }
-
-  void _configureSplitter() {
-    // Configure the flame chart / event details splitter if we haven't
-    // already.
-    if (!splitterConfigured) {
-      splitter = split.flexSplit(
-        [flameChart.element, eventDetails.element],
-        horizontal: false,
-        gutterSize: defaultSplitterWidth,
-        sizes: [75, 25],
-        minSize: [60, 160],
-      );
-      splitterConfigured = true;
-    }
-  }
-
-  void _destroySplitter() {
-    if (splitterConfigured) {
-      splitter.destroy();
-      splitterConfigured = false;
-    }
-  }
-
-  @override
-  void entering() {
-    _updateListeningState();
-    _updateButtonStates();
-  }
-
-  @override
-  void exiting() {
-    framework.clearMessages();
-    _updateListeningState();
-    _updateButtonStates();
-  }
-
-  void _exitSnapshotMode() {
-    // This needs to be called first because [framework.exitSnapshotMode()] will
-    // remove all elements from the dom if we are not connected to an app.
-    // Performing operations from [_clearTimeline()] on elements that have been
-    // removed will throw exceptions, so we need to maintain this order.
-    _clearTimeline();
-    eventDetails.clearCurrentSnapshot();
-    timelineController.exitSnapshotMode();
-    // This needs to be called before we update the button states because it
-    // changes the value of [snapshotMode], which the button states depend on.
-    framework.exitSnapshotMode();
-    _updateButtonStates();
-    _destroySplitter();
-  }
-
-  void _handleConnectionStart(VmServiceWrapper service) {
-    serviceManager.service.setFlag('profile_period', '50');
-    serviceManager.service.onEvent('Timeline').listen((Event event) {
-      final List<dynamic> list = event.json['timelineEvents'];
-      final List<Map<String, dynamic>> events =
-          list.cast<Map<String, dynamic>>();
-
-      if (!snapshotMode && !_manuallyPaused && !timelineController.paused) {
-        for (Map<String, dynamic> json in events) {
-          final TraceEvent e = TraceEvent(json);
-          timelineController.timelineData?.processTraceEvent(e);
-        }
-      }
-    });
-  }
-
-  void _handleConnectionStop(dynamic event) {
-    timelineController = null;
-  }
-
-  void _pauseRecording() {
-    _manuallyPaused = true;
-    timelineController.pause();
-    ga.select(ga.timeline, ga.pause);
-    _updateButtonStates();
-    _updateListeningState();
-  }
-
-  void _resumeRecording() {
-    _manuallyPaused = false;
-    timelineController.resume();
-    ga.select(ga.timeline, ga.resume);
-    _updateButtonStates();
-    _updateListeningState();
-  }
-
-  void _updateButtonStates() {
-    pauseButton
-      ..disabled = _manuallyPaused
-      ..attribute('hidden', snapshotMode);
-    resumeButton
-      ..disabled = !_manuallyPaused
-      ..attribute('hidden', snapshotMode);
-    clearButton.attribute('hidden', snapshotMode);
-    exportButton.attribute('hidden', snapshotMode);
-    exitSnapshotModeButton.attribute('hidden', !snapshotMode);
-  }
-
-  void _updateListeningState() async {
-    await serviceManager.serviceAvailable.future;
-
-    final bool shouldBeRunning = !_manuallyPaused && isCurrentScreen;
-    final bool isRunning = !timelineController.paused;
-
-    if (shouldBeRunning && isRunning && !timelineController.hasStarted) {
-      await timelineController.startTimeline();
-    }
-
-    if (shouldBeRunning && !isRunning) {
-      timelineController.resume();
-
-      await serviceManager.service
-          .setVMTimelineFlags(<String>['GC', 'Dart', 'Embedder']);
-    } else if (!shouldBeRunning && isRunning) {
-      // TODO(devoncarew): turn off the events
-      await serviceManager.service.setVMTimelineFlags(<String>[]);
-      timelineController.pause();
-    }
-  }
-
-  void _clearTimeline() {
-    timelineTraceEvents.clear();
-    debugHandledTraceEvents.clear();
-    debugFrameTracking.clear();
-    framesBarChart.frameUIgraph.reset();
-    flameChart.attribute('hidden', true);
-    eventDetails.attribute('hidden', true);
-    eventDetails.reset();
-    _destroySplitter();
-  }
-
-  void _exportTimeline() {
-    // TODO(kenzie): add analytics for this. It would be helpful to know how
-    // complex the problems are that users are trying to solve.
-    final snapshot = TimelineSnapshot.from(
-      timelineTraceEvents,
-      eventDetails.cpuProfileData?.cpuProfileResponse?.json,
-      eventDetails.event,
-    );
-    final now = DateTime.now();
-    final timestamp =
-        '${now.year}_${now.month}_${now.day}-${now.microsecondsSinceEpoch}';
-    downloadFile(snapshot.encodedJson, 'timeline_$timestamp.json');
-  }
-
-  /// Adds a button to the timeline that will dump debug information to text
-  /// files and download them. This will only appear if the [debugTimeline] flag
-  /// is true.
-  void _maybeAddDebugButtons() {
-    if (debugTimeline) {
-      debugButtonSection.add(PButton('Debug dump timeline')
-        ..small()
-        ..click(() {
-          // Trace event json in the order we handled the events.
-          final handledTraceEventsJson = {
-            'traceEvents': debugHandledTraceEvents
-          };
-          downloadFile(
-            jsonEncode(handledTraceEventsJson),
-            'handled_trace_output.json',
-          );
-
-          // Significant events in the frame tracking process.
-          downloadFile(
-            debugFrameTracking.toString(),
-            'frame_tracking_output.txt',
-          );
-
-          // Current status of our frame tracking elements (i.e. pendingEvents,
-          // pendingFrames).
-          final buf = StringBuffer();
-          buf.writeln('Pending events: '
-              '${timelineController.timelineData.pendingEvents.length}');
-          for (TimelineEvent event
-              in timelineController.timelineData.pendingEvents) {
-            event.format(buf, '    ');
-            buf.writeln();
-          }
-          buf.writeln('\nPending frames: '
-              '${timelineController.timelineData.pendingFrames.length}');
-          for (TimelineFrame frame
-              in timelineController.timelineData.pendingFrames.values) {
-            buf.writeln('${frame.toString()}');
-          }
-          if (timelineController
-                  .timelineData.currentEventNodes[TimelineEventType.ui.index] !=
-              null) {
-            buf.writeln('\nCurrent UI event node:');
-            timelineController
-                .timelineData.currentEventNodes[TimelineEventType.ui.index]
-                .format(buf, '   ');
-          }
-          if (timelineController.timelineData
-                  .currentEventNodes[TimelineEventType.gpu.index] !=
-              null) {
-            buf.writeln('\n Current GPU event node:');
-            timelineController
-                .timelineData.currentEventNodes[TimelineEventType.gpu.index]
-                .format(buf, '   ');
-          }
-          if (timelineController
-              .timelineData.heaps[TimelineEventType.ui.index].isNotEmpty) {
-            buf.writeln('\nUI heap');
-            for (TraceEventWrapper wrapper in timelineController
-                .timelineData.heaps[TimelineEventType.ui.index]
-                .toList()) {
-              buf.writeln(wrapper.event.json.toString());
-            }
-          }
-          if (timelineController
-              .timelineData.heaps[TimelineEventType.gpu.index].isNotEmpty) {
-            buf.writeln('\nGPU heap');
-            for (TraceEventWrapper wrapper in timelineController
-                .timelineData.heaps[TimelineEventType.gpu.index]
-                .toList()) {
-              buf.writeln(wrapper.event.json.toString());
-            }
-          }
-          downloadFile(buf.toString(), 'pending_frame_tracking_status.txt');
-        }));
-    }
-  }
-}
diff --git a/devtools/lib/src/timeline/timeline_architecture.png b/devtools/lib/src/timeline/timeline_architecture.png
new file mode 100644
index 0000000..a6776c6
--- /dev/null
+++ b/devtools/lib/src/timeline/timeline_architecture.png
Binary files differ
diff --git a/devtools/lib/src/timeline/timeline_controller.dart b/devtools/lib/src/timeline/timeline_controller.dart
index 65013dc..f3a22c3 100644
--- a/devtools/lib/src/timeline/timeline_controller.dart
+++ b/devtools/lib/src/timeline/timeline_controller.dart
@@ -2,38 +2,98 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 import 'dart:async';
-import 'dart:convert';
 
-import 'package:vm_service_lib/vm_service_lib.dart' hide TimelineEvent;
-
-import '../globals.dart';
+import '../profiler/cpu_profile_model.dart';
+import '../profiler/cpu_profile_service.dart';
+import '../profiler/cpu_profile_transformer.dart';
+import 'timeline_model.dart';
 import 'timeline_protocol.dart';
+import 'timeline_service.dart';
 
 const String timelineScreenId = 'timeline';
 
-final loadTimelineSnapshotController =
-    StreamController<TimelineSnapshot>.broadcast();
-
-Stream<TimelineSnapshot> get onLoadTimelineSnapshot =>
-    loadTimelineSnapshotController.stream;
-
-/// This class contains the business logic for [timeline.dart].
+/// This class contains the business logic for [timeline_screen.dart].
+///
+/// The controller manages the timeline data model and communicates with the
+/// view to give and receive data updates. It also manages data processing via
+/// protocols [TimelineProtocol] and [CpuProfileTransformer], and it communicates
+/// with [TimelineService].
 ///
 /// This class must not have direct dependencies on dart:html. This allows tests
 /// of the complicated logic in this class to run on the VM and will help
 /// simplify porting this code to work with Hummingbird.
 class TimelineController {
   TimelineController() {
-    onLoadTimelineSnapshot.listen(_loadTimelineFromSnapshot);
+    timelineService = TimelineService(this);
   }
 
-  final StreamController<TimelineFrame> _frameAddedController =
+  /// Stream controller that notifies a frame was added to the timeline.
+  ///
+  /// Subscribers to this stream will be responsible for updating the UI for the
+  /// new value of [timelineData.frames].
+  final StreamController<TimelineFrame> frameAddedController =
       StreamController<TimelineFrame>.broadcast();
-  Stream<TimelineFrame> get onFrameAdded => _frameAddedController.stream;
 
-  TimelineData _timelineData;
+  /// Stream controller that notifies a frame was selected.
+  ///
+  /// Subscribers to this stream will be responsible for updating the UI for the
+  /// new value of [timelineData.selectedFrame].
+  final StreamController<TimelineFrame> _selectedFrameController =
+      StreamController<TimelineFrame>.broadcast();
 
-  TimelineData get timelineData => _timelineData;
+  /// Stream controller that notifies a timeline event was selected.
+  ///
+  /// Subscribers to this stream will be responsible for updating the UI for the
+  /// new value of [timelineData.selectedEvent]. We send the
+  /// [FrameFlameChartItem] so that we can persist the colors through to the
+  /// event details view.
+  final StreamController<TimelineEvent> _selectedTimelineEventController =
+      StreamController<TimelineEvent>.broadcast();
+
+  /// Stream controller that notifies that offline data was loaded into the
+  /// timeline.
+  ///
+  /// Subscribers to this stream will be responsible for updating the UI for the
+  /// new value of [timelineData].
+  final StreamController<OfflineTimelineData> _loadOfflineDataController =
+      StreamController<OfflineTimelineData>.broadcast();
+
+  /// Stream controller that notifies the timeline screen when a non-fatal error
+  /// should be logged for the timeline.
+  final _nonFatalErrorController = StreamController<String>.broadcast();
+
+  Stream<TimelineFrame> get onFrameAdded => frameAddedController.stream;
+
+  Stream<TimelineFrame> get onSelectedFrame => _selectedFrameController.stream;
+
+  Stream<TimelineEvent> get onSelectedTimelineEvent =>
+      _selectedTimelineEventController.stream;
+
+  Stream<OfflineTimelineData> get onLoadOfflineData =>
+      _loadOfflineDataController.stream;
+
+  Stream<String> get onNonFatalError => _nonFatalErrorController.stream;
+
+  TimelineData timelineData;
+
+  OfflineTimelineData offlineTimelineData;
+
+  TimelineService timelineService;
+
+  TimelineProtocol timelineProtocol;
+
+  CpuProfileTransformer cpuProfileTransformer = CpuProfileTransformer();
+
+  CpuProfilerService cpuProfilerService = CpuProfilerService();
+
+  TimelineMode timelineMode = TimelineMode.frameBased;
+
+  /// Whether the timeline has been manually paused via the Pause button.
+  bool manuallyPaused = false;
+
+  /// Whether the timeline is being recorded, which will only occur when the
+  /// timeline is not in display-by-frame mode.
+  bool recording = false;
 
   bool get hasStarted => timelineData != null;
 
@@ -41,61 +101,108 @@
 
   bool _paused = false;
 
-  void pause() {
+  void pause({bool manual = false}) {
+    manuallyPaused = manual;
     _paused = true;
   }
 
   void resume() {
+    manuallyPaused = false;
     _paused = false;
   }
 
-  Future<void> startTimeline() async {
-    await serviceManager.serviceAvailable.future;
-    await serviceManager.service
-        .setVMTimelineFlags(<String>['GC', 'Dart', 'Embedder']);
-    await serviceManager.service.clearVMTimeline();
-
-    final Response response = await serviceManager.service.getVMTimeline();
-    final List<dynamic> list = response.json['traceEvents'];
-    final List<Map<String, dynamic>> traceEvents =
-        list.cast<Map<String, dynamic>>();
-
-    final List<TraceEvent> events = traceEvents
-        .map((Map<String, dynamic> event) => TraceEvent(event))
-        .where((TraceEvent event) {
-      return event.name == 'thread_name';
-    }).toList();
-
-    // TODO(kenzie): Remove this logic once ui/gpu distinction changes are
-    // available in the engine.
-    int uiThreadId;
-    int gpuThreadId;
-    for (TraceEvent event in events) {
-      // iOS - 'io.flutter.1.ui', Android - '1.ui'.
-      if (event.args['name'].contains('1.ui')) {
-        uiThreadId = event.threadId;
-      }
-      // iOS - 'io.flutter.1.gpu', Android - '1.gpu'.
-      if (event.args['name'].contains('1.gpu')) {
-        gpuThreadId = event.threadId;
-      }
-    }
-
-    final TimelineData timelineData = TimelineData(
-      uiThreadId: uiThreadId,
-      gpuThreadId: gpuThreadId,
-    );
-
-    timelineData.onFrameCompleted.listen((frame) {
-      _frameAddedController.add(frame);
-    });
-
-    _timelineData = timelineData;
+  void startRecording() {
+    // TODO(kenzie): kick off timeline recording here.
+    recording = true;
   }
 
-  void _loadTimelineFromSnapshot(TimelineSnapshot snapshot) {
+  void stopRecording() {
+    // TODO(kenzie): kick off trace event processing here.
+    recording = false;
+  }
+
+  void selectFrame(TimelineFrame frame) {
+    if (frame == null || timelineData.selectedFrame == frame || !hasStarted) {
+      return;
+    }
+    timelineData.selectedFrame = frame;
+    timelineData.selectedEvent = null;
+    timelineData.cpuProfileData = null;
+    _selectedFrameController.add(frame);
+
+    if (debugTimeline && frame != null) {
+      final buf = StringBuffer();
+      buf.writeln('UI timeline event for frame ${frame.id}:');
+      frame.uiEventFlow.format(buf, '  ');
+      buf.writeln('\nUI trace for frame ${frame.id}');
+      frame.uiEventFlow.writeTraceToBuffer(buf);
+      buf.writeln('\nGPU timeline event frame ${frame.id}:');
+      frame.gpuEventFlow.format(buf, '  ');
+      buf.writeln('\nGPU trace for frame ${frame.id}');
+      frame.gpuEventFlow.writeTraceToBuffer(buf);
+      print(buf.toString());
+    }
+  }
+
+  void selectTimelineEvent(TimelineEvent event) {
+    if (timelineData.selectedEvent == event) {
+      return;
+    }
+    timelineData.selectedEvent = event;
+    _selectedTimelineEventController.add(event);
+  }
+
+  void addFrame(TimelineFrame frame) {
+    timelineData.frames.add(frame);
+    frameAddedController.add(frame);
+  }
+
+  Future<void> getCpuProfileForSelectedEvent() async {
+    if (!timelineData.selectedEvent.isUiEvent) return;
+
+    assert(timelineData.selectedEvent.frameId == timelineData.selectedFrame.id);
+
+    timelineData.selectedFrame.cpuProfileData ??=
+        await cpuProfilerService.getCpuProfile(
+      startMicros:
+          timelineData.selectedFrame.uiEventFlow.time.start.inMicroseconds,
+      extentMicros:
+          timelineData.selectedFrame.uiEventFlow.time.duration.inMicroseconds,
+    );
+
+    timelineData.cpuProfileData = CpuProfileData.subProfile(
+      timelineData.selectedFrame.cpuProfileData,
+      timelineData.selectedEvent.time,
+    );
+    cpuProfileTransformer.processData(timelineData.cpuProfileData);
+  }
+
+  void restoreCpuProfileFromOfflineData() {
+    if (offlineTimelineData == null) {
+      return;
+    }
+    timelineData = offlineTimelineData.copy();
+    _loadOfflineDataController.add(offlineTimelineData);
+  }
+
+  void recordTrace(Map<String, dynamic> trace) {
+    timelineData.traceEvents.add(trace);
+  }
+
+  void recordTraceForTimelineEvent(TimelineEvent event) {
+    recordTrace(event.beginTraceEventJson);
+    event.children.forEach(recordTraceForTimelineEvent);
+    if (event.endTraceEventJson != null) {
+      recordTrace(event.endTraceEventJson);
+    }
+  }
+
+  void loadOfflineData(OfflineTimelineData offlineData) {
+    timelineData = offlineData.copy();
+    offlineTimelineData = offlineData.copy();
+
     final traceEvents =
-        snapshot.traceEvents.map((trace) => TraceEvent(trace)).toList();
+        offlineData.traceEvents.map((trace) => TraceEvent(trace)).toList();
 
     // TODO(kenzie): once each trace event has a ui/gpu distinction bit added to
     // the trace, we will not need to infer thread ids. Since we control the
@@ -103,132 +210,37 @@
     final uiThreadId = traceEvents.first.threadId;
     final gpuThreadId = traceEvents.last.threadId;
 
-    final TimelineData timelineData = TimelineData(
+    timelineProtocol = TimelineProtocol(
+      timelineController: this,
       uiThreadId: uiThreadId,
       gpuThreadId: gpuThreadId,
     );
 
-    timelineData.onFrameCompleted.listen((frame) {
-      _frameAddedController.add(frame);
-    });
-
-    _timelineData = timelineData;
-
     for (TraceEvent event in traceEvents) {
-      timelineData.processTraceEvent(event, immediate: true);
+      timelineProtocol.processTraceEvent(event, immediate: true);
     }
     // Make a final call to [maybeAddPendingEvents] so that we complete the
     // processing for every frame in the snapshot.
-    timelineData.maybeAddPendingEvents();
-  }
+    timelineProtocol.maybeAddPendingEvents();
 
-  void exitSnapshotMode() {
-    // If the timeline controller had previously been started, restart it
-    // because [_timelineData] has changed since we entered snapshot mode.
-    if (hasStarted) {
-      startTimeline();
+    if (timelineData.cpuProfileData != null) {
+      cpuProfileTransformer.processData(timelineData.cpuProfileData);
     }
+
+    _loadOfflineDataController.add(offlineData);
+  }
+
+  void exitOfflineMode() {
+    timelineData.clear();
+    offlineTimelineData = null;
+  }
+
+  void logNonFatalError(String message) {
+    _nonFatalErrorController.add(message);
   }
 }
 
-class TimelineSnapshot {
-  TimelineSnapshot._(
-    this.traceEvents,
-    this.cpuProfile,
-    this.selectedEvent,
-  );
-
-  static TimelineSnapshot from(
-    List<Map<String, dynamic>> traceEvents,
-    Map<String, dynamic> cpuProfile,
-    TimelineEvent selectedEvent,
-  ) {
-    final _traceEvents = traceEvents ?? [];
-    final _cpuProfile = cpuProfile ?? {};
-    final _selectedEvent = selectedEvent != null
-        ? TimelineEventSnapshot(
-            selectedEvent.name,
-            selectedEvent.time.start.inMicroseconds,
-            selectedEvent.time.duration.inMicroseconds,
-          )
-        : null;
-    return TimelineSnapshot._(_traceEvents, _cpuProfile, _selectedEvent);
-  }
-
-  static TimelineSnapshot parse(Map<String, dynamic> json) {
-    final List<dynamic> traceEvents =
-        (json[traceEventsKey] ?? []).cast<Map<String, dynamic>>();
-    final Map<String, dynamic> cpuProfile = json[cpuProfileKey] ?? {};
-    final Map<String, dynamic> selectedEventData = json[selectedEventKey];
-    final selectedEvent = selectedEventData.isNotEmpty
-        ? TimelineEventSnapshot(
-            selectedEventData[TimelineEventSnapshot.eventNameKey],
-            selectedEventData[TimelineEventSnapshot.eventStartTimeKey],
-            selectedEventData[TimelineEventSnapshot.eventDurationKey],
-          )
-        : null;
-
-    return TimelineSnapshot._(traceEvents, cpuProfile, selectedEvent);
-  }
-
-  static const traceEventsKey = 'traceEvents';
-  static const cpuProfileKey = 'cpuProfile';
-  static const selectedEventKey = 'selectedEvent';
-  static const devToolsScreenKey = 'dartDevToolsScreen';
-
-  final List<Map<String, dynamic>> traceEvents;
-
-  final Map<String, dynamic> cpuProfile;
-
-  final TimelineEventSnapshot selectedEvent;
-
-  String get encodedJson {
-    final json = {
-      traceEventsKey: traceEvents,
-      cpuProfileKey: cpuProfile,
-      selectedEventKey: selectedEvent?.json ?? {},
-      devToolsScreenKey: timelineScreenId,
-    };
-    return jsonEncode(json);
-  }
-
-  bool get isEmpty => traceEvents.isEmpty && cpuProfile.isEmpty;
-
-  bool get hasCpuProfile => cpuProfile.isNotEmpty && selectedEvent != null;
-}
-
-/// Wrapper class for [TimelineEvent] that only includes information we need for
-/// importing and exporting snapshots.
-///
-/// * name
-/// * start time
-/// * duration
-///
-/// We extend TimelineEvent so that our CPU profiler code requiring a selected
-/// timeline event will work as it does when we are not loading from a snapshot.
-class TimelineEventSnapshot extends TimelineEvent {
-  TimelineEventSnapshot(String name, int startMicros, int durationMicros)
-      : super(TraceEventWrapper(
-          TraceEvent({
-            'name': name,
-            'ts': startMicros,
-            'dur': durationMicros,
-            'args': {'type': 'ui'},
-          }),
-          0, // 0 is an arbitrary value for [TraceEventWrapper.timeReceived].
-        )) {
-    time.end = Duration(microseconds: startMicros + durationMicros);
-  }
-
-  static const eventNameKey = 'name';
-  static const eventStartTimeKey = 'startMicros';
-  static const eventDurationKey = 'durationMicros';
-
-  Map<String, dynamic> get json {
-    return {
-      eventNameKey: name,
-      eventStartTimeKey: time.start.inMicroseconds,
-      eventDurationKey: time.duration.inMicroseconds,
-    };
-  }
+enum TimelineMode {
+  frameBased,
+  full,
 }
diff --git a/devtools/lib/src/timeline/timeline_flame_chart.dart b/devtools/lib/src/timeline/timeline_flame_chart.dart
new file mode 100644
index 0000000..74c0ff7
--- /dev/null
+++ b/devtools/lib/src/timeline/timeline_flame_chart.dart
@@ -0,0 +1,153 @@
+// Copyright 2019 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 'dart:math' as math;
+
+import 'package:meta/meta.dart';
+
+import '../charts/flame_chart_canvas.dart';
+import '../ui/colors.dart';
+import '../ui/fake_flutter/dart_ui/dart_ui.dart';
+import '../ui/theme.dart';
+import 'timeline_model.dart';
+
+class TimelineFlameChartCanvas extends FlameChartCanvas<TimelineFrame> {
+  TimelineFlameChartCanvas({
+    @required TimelineFrame data,
+    @required double width,
+    @required double height,
+  }) : super(
+          data: data,
+          duration: data.time.duration,
+          width: width,
+          height: height,
+        );
+
+  static const double sectionSpacing = 15.0;
+
+  int get gpuSectionStartRow => data.uiEventFlow.depth;
+
+  @override
+  void initRows() {
+    rows = List.generate(
+      data.uiEventFlow.depth + data.gpuEventFlow.depth,
+      (i) => FlameChartRow(nodes: [], index: i),
+    );
+
+    final totalWidth = width - 2 * sideInset;
+
+    final int frameStartOffset = data.time.start.inMicroseconds;
+
+    double getTopForRow(int row) {
+      // This accounts for the section spacing between the UI events and the GPU
+      // events.
+      final additionalPadding =
+          row >= gpuSectionStartRow ? sectionSpacing : 0.0;
+      return (row * rowHeightWithPadding + topOffset + additionalPadding)
+          .toDouble();
+    }
+
+    // Add UI section label.
+    const uiLabelWidth = 22.0 + rowPadding;
+    final uiLabelTop = getTopForRow(0);
+    final uiLabelBottom = uiLabelTop + rowHeight;
+    final uiSectionLabel = FlameChartNode<TimelineEvent>(
+      Rect.fromLTRB(rowPadding, uiLabelTop, uiLabelWidth, uiLabelBottom),
+      mainUiColor,
+      Colors.black,
+      Colors.black,
+      null,
+      (_) => 'UI',
+    );
+    rows[0].nodes.add(uiSectionLabel);
+
+    // Add GPU section label.
+    const gpuLabelWidth = 40.0 + rowPadding;
+    final gpuLabelTop = getTopForRow(gpuSectionStartRow);
+    final gpuLabelBottom = gpuLabelTop + rowHeight;
+    final gpuSectionLabel = FlameChartNode<TimelineEvent>(
+      Rect.fromLTRB(rowPadding, gpuLabelTop, gpuLabelWidth, gpuLabelBottom),
+      mainGpuColor,
+      Colors.white,
+      Colors.white,
+      null,
+      (_) => 'GPU',
+    );
+    rows[gpuSectionStartRow].nodes.add(gpuSectionLabel);
+
+    void createChartNodes(TimelineEvent event, int row) {
+      // Pixels per microsecond in order to fit the entire frame in view.
+      final double pxPerMicro = totalWidth / data.time.duration.inMicroseconds;
+
+      // Do not round these values. Rounding the left could cause us to have
+      // inaccurately placed events on the chart. Rounding the width could cause
+      // us to lose very small events if the width rounds to zero.
+      final double left =
+          (event.time.start.inMicroseconds - frameStartOffset) * pxPerMicro +
+              sideInset;
+      final double right =
+          (event.time.end.inMicroseconds - frameStartOffset) * pxPerMicro +
+              sideInset;
+      final top = getTopForRow(row);
+      final backgroundColor =
+          event.isUiEvent ? _nextUiColor() : _nextGpuColor();
+
+      final node = FlameChartNode<TimelineEvent>(
+        Rect.fromLTRB(left, top, right, top + rowHeight),
+        backgroundColor,
+        event.isUiEvent
+            ? ThemedColor.fromSingleColor(Colors.black)
+            : ThemedColor.fromSingleColor(contrastForegroundWhite),
+        Colors.black,
+        event,
+        (_) => event.name,
+      );
+
+      rows[row].nodes.add(node);
+
+      for (TimelineEvent child in event.children) {
+        createChartNodes(
+          child,
+          row + 1,
+        );
+      }
+    }
+
+    createChartNodes(data.uiEventFlow, 0);
+    createChartNodes(data.gpuEventFlow, gpuSectionStartRow);
+  }
+
+  @override
+  double get calculatedWidth {
+    // The farthest right node in the graph will either be the root UI event or
+    // the root GPU event.
+    return math.max(rows[gpuSectionStartRow].nodes.last.rect.right,
+            rows[gpuSectionStartRow].nodes.last.rect.right) -
+        sideInset;
+  }
+
+  @override
+  double relativeYPosition(double absoluteY) {
+    final row = (absoluteY - topOffset) ~/ rowHeightWithPadding;
+    if (row >= gpuSectionStartRow) {
+      return absoluteY - topOffset - sectionSpacing;
+    }
+    return absoluteY - topOffset;
+  }
+
+  int _uiColorOffset = 0;
+
+  int _gpuColorOffset = 0;
+
+  Color _nextUiColor() {
+    final color = uiColorPalette[_uiColorOffset % uiColorPalette.length];
+    _uiColorOffset++;
+    return color;
+  }
+
+  Color _nextGpuColor() {
+    final color = gpuColorPalette[_gpuColorOffset % gpuColorPalette.length];
+    _gpuColorOffset++;
+    return color;
+  }
+}
diff --git a/devtools/lib/src/timeline/timeline_model.dart b/devtools/lib/src/timeline/timeline_model.dart
new file mode 100644
index 0000000..a4ddf77
--- /dev/null
+++ b/devtools/lib/src/timeline/timeline_model.dart
@@ -0,0 +1,603 @@
+// Copyright 2019 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:meta/meta.dart';
+
+import '../profiler/cpu_profile_model.dart';
+import '../trees.dart';
+import '../utils.dart';
+import 'timeline_controller.dart';
+
+/// Data model for DevTools Timeline.
+class TimelineData {
+  TimelineData({
+    List<Map<String, dynamic>> traceEvents,
+    List<TimelineFrame> frames,
+    this.selectedFrame,
+    this.selectedEvent,
+    this.cpuProfileData,
+  })  : traceEvents = traceEvents ?? [],
+        frames = frames ?? [];
+
+  static const traceEventsKey = 'traceEvents';
+  static const cpuProfileKey = 'cpuProfile';
+  static const selectedEventKey = 'selectedEvent';
+  static const devToolsScreenKey = 'dartDevToolsScreen';
+
+  /// List that will store trace events in the order we process them.
+  ///
+  /// These events are scrubbed so that bad data from the engine does not hinder
+  /// event processing or trace viewing. When the export timeline button is
+  /// clicked, this will be part of the output.
+  List<Map<String, dynamic>> traceEvents = [];
+
+  /// All frames currently visible in the timeline.
+  List<TimelineFrame> frames = [];
+
+  TimelineFrame selectedFrame;
+
+  TimelineEvent selectedEvent;
+
+  CpuProfileData cpuProfileData;
+
+  Map<String, dynamic> get json => {
+        traceEventsKey: traceEvents,
+        cpuProfileKey: cpuProfileData?.json ?? {},
+        selectedEventKey: selectedEvent?.json ?? {},
+        devToolsScreenKey: timelineScreenId,
+      };
+
+  void clear() {
+    traceEvents.clear();
+    frames.clear();
+    selectedFrame = null;
+    selectedEvent = null;
+    cpuProfileData = null;
+  }
+}
+
+class OfflineTimelineData extends TimelineData {
+  OfflineTimelineData._({
+    List<Map<String, dynamic>> traceEvents,
+    List<TimelineFrame> frames,
+    TimelineFrame selectedFrame,
+    TimelineEvent selectedEvent,
+    CpuProfileData cpuProfileData,
+  }) : super(
+          traceEvents: traceEvents,
+          frames: frames,
+          selectedFrame: selectedFrame,
+          selectedEvent: selectedEvent,
+          cpuProfileData: cpuProfileData,
+        );
+
+  static OfflineTimelineData parse(Map<String, dynamic> json) {
+    final List<dynamic> traceEvents =
+        (json[TimelineData.traceEventsKey] ?? []).cast<Map<String, dynamic>>();
+
+    final Map<String, dynamic> cpuProfileJson =
+        json[TimelineData.cpuProfileKey] ?? {};
+    final CpuProfileData cpuProfileData =
+        cpuProfileJson.isNotEmpty ? CpuProfileData.parse(cpuProfileJson) : null;
+
+    final Map<String, dynamic> selectedEventJson =
+        json[TimelineData.selectedEventKey] ?? {};
+    final OfflineTimelineEvent selectedEvent = selectedEventJson.isNotEmpty
+        ? OfflineTimelineEvent(
+            selectedEventJson[TimelineEvent.eventNameKey],
+            selectedEventJson[TimelineEvent.eventTypeKey],
+            selectedEventJson[TimelineEvent.eventStartTimeKey],
+            selectedEventJson[TimelineEvent.eventDurationKey],
+          )
+        : null;
+
+    return OfflineTimelineData._(
+      traceEvents: traceEvents,
+      selectedEvent: selectedEvent,
+      cpuProfileData: cpuProfileData,
+    );
+  }
+
+  bool get isEmpty => traceEvents.isEmpty;
+
+  /// Creates a new instance of [OfflineTimelineData] with references to the
+  /// same objects contained in this instance ([traceEvents], [frames],
+  /// [selectedFrame], [selectedEvent], [cpuProfileData]).
+  ///
+  /// This is not a deep copy. We are not modifying the before-mentioned
+  /// objects, only pointing our reference variables at different objects.
+  /// Therefore, we do not need to store a copy of all these objects (and the
+  /// objects they contain) in memory.
+  OfflineTimelineData copy() {
+    return OfflineTimelineData._(
+      traceEvents: traceEvents,
+      frames: frames,
+      selectedFrame: selectedFrame,
+      selectedEvent: selectedEvent,
+      cpuProfileData: cpuProfileData,
+    );
+  }
+}
+
+/// Wrapper class for [TimelineEvent] that only includes information we need for
+/// importing and exporting snapshots.
+///
+/// * name
+/// * start time
+/// * duration
+///
+/// We extend TimelineEvent so that our CPU profiler code requiring a selected
+/// timeline event will work as it does when we are not loading from offline.
+class OfflineTimelineEvent extends TimelineEvent {
+  OfflineTimelineEvent(
+      String name, String eventType, int startMicros, int durationMicros)
+      : super(TraceEventWrapper(
+          TraceEvent({
+            TraceEvent.nameKey: name,
+            TraceEvent.timestampKey: startMicros,
+            TraceEvent.durationKey: durationMicros,
+            TraceEvent.argsKey: {TraceEvent.typeKey: 'ui'},
+          }),
+          0, // 0 is an arbitrary value for [TraceEventWrapper.timeReceived].
+        )) {
+    time.end = Duration(microseconds: startMicros + durationMicros);
+    type = eventType == TimelineEventType.ui.toString()
+        ? TimelineEventType.ui
+        : TimelineEventType.gpu;
+  }
+}
+
+/// Data describing a single frame.
+///
+/// Each TimelineFrame should have 2 distinct pieces of data:
+/// * [uiEventFlow] : flow of events showing the UI work for the frame.
+/// * [gpuEventFlow] : flow of events showing the GPU work for the frame.
+class TimelineFrame {
+  TimelineFrame(this.id);
+
+  // TODO(kenzie): we should query the device for targetFps at some point.
+  static const targetFps = 60.0;
+
+  static const targetMaxDuration = 1000.0 / targetFps;
+
+  final String id;
+
+  /// Marks whether this frame has been added to the timeline.
+  ///
+  /// This should only be set once.
+  bool get addedToTimeline => _addedToTimeline;
+
+  bool _addedToTimeline;
+
+  set addedToTimeline(bool v) {
+    assert(_addedToTimeline == null);
+    _addedToTimeline = v;
+  }
+
+  /// Event flows for the UI and GPU work for the frame.
+  final List<TimelineEvent> eventFlows = List.generate(2, (_) => null);
+
+  /// Flow of events describing the UI work for the frame.
+  TimelineEvent get uiEventFlow => eventFlows[TimelineEventType.ui.index];
+
+  /// Flow of events describing the GPU work for the frame.
+  TimelineEvent get gpuEventFlow => eventFlows[TimelineEventType.gpu.index];
+
+  /// Whether the frame is ready for the timeline.
+  ///
+  /// A frame is ready once it has both required event flows as well as
+  /// [_pipelineItemStartTime] and [_pipelineItemEndTime].
+  bool get isReadyForTimeline {
+    return uiEventFlow != null &&
+        gpuEventFlow != null &&
+        pipelineItemTime.start?.inMicroseconds != null &&
+        pipelineItemTime.end?.inMicroseconds != null;
+  }
+
+  // Stores frame start time, end time, and duration.
+  final time = TimeRange();
+
+  /// Pipeline item time range in micros.
+  ///
+  /// This stores the start and end times for the pipeline item event for this
+  /// frame. We use this value to determine whether a TimelineEvent fits within
+  /// the frame's time boundaries.
+  final pipelineItemTime = TimeRange(singleAssignment: false);
+
+  TraceEvent pipelineItemStartTrace;
+
+  TraceEvent pipelineItemEndTrace;
+
+  bool get isWellFormed =>
+      pipelineItemTime.start?.inMicroseconds != null &&
+      pipelineItemTime.end?.inMicroseconds != null;
+
+  int get uiDuration =>
+      uiEventFlow != null ? uiEventFlow.time.duration.inMicroseconds : null;
+
+  double get uiDurationMs => uiDuration != null ? uiDuration / 1000 : null;
+
+  int get gpuDuration =>
+      gpuEventFlow != null ? gpuEventFlow.time.duration.inMicroseconds : null;
+
+  double get gpuDurationMs => gpuDuration != null ? gpuDuration / 1000 : null;
+
+  CpuProfileData cpuProfileData;
+
+  void setEventFlow(TimelineEvent event, {TimelineEventType type}) {
+    type ??= event?.type;
+    if (type == TimelineEventType.ui) {
+      time.start = event?.time?.start;
+    }
+    if (type == TimelineEventType.gpu) {
+      time.end = event?.time?.end;
+    }
+    eventFlows[type.index] = event;
+    event?.frameId = id;
+  }
+
+  @override
+  String toString() {
+    return 'Frame $id - $time, ui: ${uiEventFlow.time}, '
+        'gpu: ${gpuEventFlow.time}';
+  }
+}
+
+enum TimelineEventType {
+  ui,
+  gpu,
+  unknown,
+}
+
+class TimelineEvent extends TreeNode<TimelineEvent> {
+  TimelineEvent(TraceEventWrapper firstTraceEvent)
+      : traceEvents = [firstTraceEvent],
+        type = firstTraceEvent.event.type {
+    time.start = Duration(microseconds: firstTraceEvent.event.timestampMicros);
+  }
+
+  static const eventNameKey = 'name';
+  static const eventTypeKey = 'type';
+  static const eventStartTimeKey = 'startMicros';
+  static const eventDurationKey = 'durationMicros';
+
+  /// Trace events associated with this [TimelineEvent].
+  ///
+  /// There will either be one entry in the list (for DurationComplete events)
+  /// or two (one for the associated DurationBegin event and one for the
+  /// associated DurationEnd event).
+  final List<TraceEventWrapper> traceEvents;
+
+  TimelineEventType type;
+
+  TimeRange time = TimeRange();
+
+  String get frameId => _frameId ?? root._frameId;
+
+  String _frameId;
+
+  set frameId(String id) => _frameId = id;
+
+  String get name => traceEvents.first.event.name;
+
+  Map<String, dynamic> get beginTraceEventJson => traceEvents.first.json;
+
+  Map<String, dynamic> get endTraceEventJson =>
+      traceEvents.length > 1 ? traceEvents.last.json : null;
+
+  bool get isUiEvent => type == TimelineEventType.ui;
+
+  bool get isGpuEvent => type == TimelineEventType.gpu;
+
+  bool get isUiEventFlow => containsChildWithCondition(
+      (TimelineEvent event) => event.name.contains('Engine::BeginFrame'));
+
+  bool get isGpuEventFlow => containsChildWithCondition(
+      (TimelineEvent event) => event.name.contains('PipelineConsume'));
+
+  void maybeRemoveDuplicate() {
+    void _maybeRemoveDuplicate({@required TimelineEvent parent}) {
+      if (parent.children.length == 1 &&
+          // [parent]'s DurationBegin trace is equal to that of its only child.
+          collectionEquals(
+            parent.beginTraceEventJson,
+            parent.children.first.beginTraceEventJson,
+          ) &&
+          // [parent]'s DurationEnd trace is equal to that of its only child.
+          collectionEquals(
+            parent.endTraceEventJson,
+            parent.children.first.endTraceEventJson,
+          )) {
+        parent.removeChild(children.first);
+      }
+    }
+
+    // Remove [this] event's child if it is a duplicate of [this].
+    if (children.isNotEmpty) {
+      _maybeRemoveDuplicate(parent: this);
+    }
+    // Remove [this] event if it is a duplicate of [parent].
+    if (parent != null) {
+      _maybeRemoveDuplicate(parent: parent);
+    }
+  }
+
+  void removeChild(TimelineEvent childToRemove) {
+    assert(children.contains(childToRemove));
+    final List<TimelineEvent> newChildren = List.from(childToRemove.children);
+    newChildren.forEach(_addChild);
+    children.remove(childToRemove);
+  }
+
+  @override
+  void addChild(TimelineEvent child) {
+    // Places the child in it's correct position amongst the other children.
+    void _putChildInTree(TimelineEvent root) {
+      // [root] is a leaf. Add child here.
+      if (root.children.isEmpty) {
+        root._addChild(child);
+        return;
+      }
+
+      final _children = root.children.toList();
+
+      // If [child] is the parent of some or all of the members in [_children],
+      // those members will need to be reordered in the tree.
+      final childrenToReorder = [];
+      for (TimelineEvent otherChild in _children) {
+        if (child.couldBeParentOf(otherChild)) {
+          childrenToReorder.add(otherChild);
+        }
+      }
+
+      if (childrenToReorder.isNotEmpty) {
+        root._addChild(child);
+
+        for (TimelineEvent otherChild in childrenToReorder) {
+          // Link [otherChild] with its correct parent [child].
+          child._addChild(otherChild);
+
+          // Unlink [otherChild] from its incorrect parent [root].
+          root.children.remove(otherChild);
+        }
+        return;
+      }
+
+      // Check if a member of [_children] is the parent of [child]. If multiple
+      // children in [_children] share a timestamp, they both could be the
+      // parent of [child]. We reverse [_children] so that we will pick the last
+      // received candidate as the new parent of [child].
+      for (TimelineEvent otherChild in _children.reversed) {
+        if (otherChild.couldBeParentOf(child)) {
+          // Recurse on [otherChild]'s subtree.
+          _putChildInTree(otherChild);
+          return;
+        }
+      }
+
+      // If we have not returned at this point, [child] belongs in
+      // [root.children].
+      root._addChild(child);
+    }
+
+    _putChildInTree(this);
+  }
+
+  void _addChild(TimelineEvent child) {
+    assert(!children.contains(child));
+    children.add(child);
+    child.parent = this;
+  }
+
+  bool couldBeParentOf(TimelineEvent e) {
+    final startTime = time.start.inMicroseconds;
+    final endTime = time.end?.inMicroseconds;
+    final eStartTime = e.time.start.inMicroseconds;
+    final eEndTime = e.time.end?.inMicroseconds;
+
+    if (endTime != null && eEndTime != null) {
+      if (startTime == eStartTime && endTime == eEndTime) {
+        return traceEvents.first.id < e.traceEvents.first.id;
+      }
+      return startTime <= eStartTime && endTime >= eEndTime;
+    } else if (endTime != null) {
+      // We don't use >= to compare [endTime] and [e.startTime] here because we
+      // don't want to falsely make [this] the parent of [e]. We do not know
+      // [e.endTime], meaning [e] could start at [endTime] and end later than
+      // [endTime] (unless e has a duration of 0). In this case, [this] would
+      // not be the parent of [e].
+      return startTime <= eStartTime && endTime > eStartTime;
+    } else if (startTime == eStartTime) {
+      return traceEvents.first.id < e.traceEvents.first.id;
+    } else {
+      return startTime < eStartTime;
+    }
+  }
+
+  void format(StringBuffer buf, String indent) {
+    buf.writeln('$indent$name $time');
+    for (TimelineEvent child in children) {
+      child.format(buf, '  $indent');
+    }
+  }
+
+  void formatFromRoot(StringBuffer buf, String indent) {
+    root.format(buf, indent);
+  }
+
+  void writeTraceToBuffer(StringBuffer buf) {
+    buf.writeln(beginTraceEventJson);
+    for (TimelineEvent child in children) {
+      child.writeTraceToBuffer(buf);
+    }
+    if (endTraceEventJson != null) {
+      buf.writeln(endTraceEventJson);
+    }
+  }
+
+  Map<String, dynamic> get json {
+    return {
+      eventNameKey: name,
+      eventTypeKey: type.toString(),
+      eventStartTimeKey: time.start.inMicroseconds,
+      eventDurationKey: time.duration.inMicroseconds,
+    };
+  }
+
+  @visibleForTesting
+  TimelineEvent deepCopy() {
+    final copy = TimelineEvent(traceEvents.first);
+    copy.time.end = time.end;
+    copy.parent = parent;
+    for (TimelineEvent child in children) {
+      copy._addChild(child.deepCopy());
+    }
+    return copy;
+  }
+
+  // TODO(kenzie): use DiagnosticableTreeMixin instead.
+  @override
+  String toString() {
+    final buf = StringBuffer();
+    format(buf, '  ');
+    return buf.toString();
+  }
+}
+
+// TODO(devoncarew): Upstream this class to the service protocol library.
+
+/// A single timeline event.
+class TraceEvent {
+  /// Creates a timeline event given JSON-encoded event data.
+  TraceEvent(this.json)
+      : name = json[nameKey],
+        category = json[categoryKey],
+        phase = json[phaseKey],
+        processId = json[processIdKey],
+        threadId = json[threadIdKey],
+        duration = json[durationKey],
+        timestampMicros = json[timestampKey],
+        args = json[argsKey];
+
+  static const nameKey = 'name';
+  static const categoryKey = 'cat';
+  static const phaseKey = 'ph';
+  static const processIdKey = 'pid';
+  static const threadIdKey = 'tid';
+  static const durationKey = 'dur';
+  static const timestampKey = 'ts';
+  static const argsKey = 'args';
+  static const typeKey = 'type';
+  static const idKey = 'id';
+  static const scopeKey = 'scope';
+
+  /// The original event JSON.
+  final Map<String, dynamic> json;
+
+  /// The name of the event.
+  ///
+  /// Corresponds to the "name" field in the JSON event.
+  final String name;
+
+  /// Event category. Events with different names may share the same category.
+  ///
+  /// Corresponds to the "cat" field in the JSON event.
+  final String category;
+
+  /// For a given long lasting event, denotes the phase of the event, such as
+  /// "B" for "event began", and "E" for "event ended".
+  ///
+  /// Corresponds to the "ph" field in the JSON event.
+  final String phase;
+
+  /// ID of process that emitted the event.
+  ///
+  /// Corresponds to the "pid" field in the JSON event.
+  final int processId;
+
+  /// ID of thread that issues the event.
+  ///
+  /// Corresponds to the "tid" field in the JSON event.
+  final int threadId;
+
+  /// Each async event has an additional required parameter id. We consider the
+  /// events with the same category and id as events from the same event tree.
+  dynamic get id => json[idKey];
+
+  /// An optional scope string can be specified to avoid id conflicts, in which
+  /// case we consider events with the same category, scope, and id as events
+  /// from the same event tree.
+  String get scope => json[scopeKey];
+
+  /// The duration of the event, in microseconds.
+  ///
+  /// Note, some events are reported with duration. Others are reported as a
+  /// pair of begin/end events.
+  ///
+  /// Corresponds to the "dur" field in the JSON event.
+  final int duration;
+
+  /// Time passed since tracing was enabled, in microseconds.
+  final int timestampMicros;
+
+  /// Arbitrary data attached to the event.
+  final Map<String, dynamic> args;
+
+  String get asyncUID {
+    if (scope == null) {
+      return '$category:$id';
+    } else {
+      return '$category:$scope:$id';
+    }
+  }
+
+  TimelineEventType _type;
+
+  TimelineEventType get type {
+    if (_type == null) {
+      if (args[typeKey] == 'ui') {
+        _type = TimelineEventType.ui;
+      } else if (args[typeKey] == 'gpu') {
+        _type = TimelineEventType.gpu;
+      } else {
+        _type = TimelineEventType.unknown;
+      }
+    }
+    return _type;
+  }
+
+  set type(TimelineEventType t) => _type = t;
+
+  bool get isUiEvent => type == TimelineEventType.ui;
+
+  bool get isGpuEvent => type == TimelineEventType.gpu;
+
+  @override
+  String toString() => '$type event [$idKey: $id] [$phaseKey: $phase] '
+      '$name - [$timestampKey: $timestampMicros] [$durationKey: $duration]';
+}
+
+int _traceEventWrapperId = 0;
+
+class TraceEventWrapper implements Comparable<TraceEventWrapper> {
+  TraceEventWrapper(this.event, this.timeReceived)
+      : id = _traceEventWrapperId++;
+  final TraceEvent event;
+
+  final num timeReceived;
+
+  final int id;
+
+  Map<String, dynamic> get json => event.json;
+
+  bool processed = false;
+
+  @override
+  int compareTo(TraceEventWrapper other) {
+    // Order events based on their timestamps. If the events share a timestamp,
+    // order them in the order we received them.
+    final compare =
+        event.timestampMicros.compareTo(other.event.timestampMicros);
+    return compare != 0 ? compare : id.compareTo(other.id);
+  }
+}
diff --git a/devtools/lib/src/timeline/timeline_protocol.dart b/devtools/lib/src/timeline/timeline_protocol.dart
index 63b6394..4b58bab 100644
--- a/devtools/lib/src/timeline/timeline_protocol.dart
+++ b/devtools/lib/src/timeline/timeline_protocol.dart
@@ -2,13 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
 import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
 
 import '../utils.dart';
+import 'timeline_controller.dart';
+import 'timeline_model.dart';
 
 // For documentation, see the Chrome "Trace Event Format" document:
 // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU.
@@ -34,19 +35,6 @@
 /// be able to dump this buffer to a downloadable text file.
 StringBuffer debugFrameTracking = StringBuffer();
 
-/// List that will store trace events in the order we process them.
-///
-/// These events are scrubbed so that bad data from the engine does not hinder
-/// event processing or trace viewing. When the export timeline button is
-/// clicked, this will be part of the output.
-List<Map<String, dynamic>> timelineTraceEvents = [];
-
-enum TimelineEventType {
-  ui,
-  gpu,
-  unknown,
-}
-
 /// Epsilon in micros used for determining it an event fits within a given frame
 /// boundary.
 ///
@@ -58,8 +46,14 @@
 /// Delay in ms for processing trace events.
 const Duration traceEventDelay = Duration(milliseconds: 1000);
 
-class TimelineData {
-  TimelineData({this.uiThreadId, this.gpuThreadId});
+/// Protocol for processing trace events and composing them into
+/// [TimelineEvents] and [TimelineFrames].
+class TimelineProtocol {
+  TimelineProtocol({
+    @required this.uiThreadId,
+    @required this.gpuThreadId,
+    @required this.timelineController,
+  });
 
   static const durationBeginPhase = 'B';
   static const durationEndPhase = 'E';
@@ -71,16 +65,12 @@
   //  and frame ids are available in the engine.
   final int uiThreadId;
   final int gpuThreadId;
-
-  final StreamController<TimelineFrame> _frameCompleteController =
-      StreamController<TimelineFrame>.broadcast();
-
-  Stream<TimelineFrame> get onFrameCompleted => _frameCompleteController.stream;
+  final TimelineController timelineController;
 
   /// Frames we are in the process of assembling.
   ///
   /// Once frames are ready, we will remove them from this Map and add them to
-  /// [_frameCompleteController].
+  /// the timeline.
   final Map<String, TimelineFrame> pendingFrames = <String, TimelineFrame>{};
 
   /// Events we have collected and are waiting to add to their respective
@@ -168,7 +158,7 @@
     if (currentEventNodes[event.type.index] != null &&
         event.timestampMicros <
             currentEventNodes[event.type.index]
-                .getRoot()
+                .root
                 .time
                 .start
                 .inMicroseconds) {
@@ -397,8 +387,8 @@
     final current = currentEventNodes[event.type.index];
     if (current != null) {
       if (current
-          .containsChildWithCondition((TimelineEvent e) => collectionEquals(
-                e.beginTraceEventJson,
+          .containsChildWithCondition((TimelineEvent event) => collectionEquals(
+                event.beginTraceEventJson,
                 timelineEvent.beginTraceEventJson,
               ))) {
         // This is a duplicate DurationComplete event. Return early.
@@ -504,14 +494,16 @@
       }
 
       // Record the trace events for this timeline frame.
-      timelineTraceEvents.add(frame.pipelineItemStartTrace.json);
-      frame.uiEventFlow.recordTrace();
-      frame.gpuEventFlow.recordTrace();
-      timelineTraceEvents.add(frame.pipelineItemEndTrace.json);
+      timelineController.recordTrace(frame.pipelineItemStartTrace.json);
+      timelineController.recordTraceForTimelineEvent(frame.uiEventFlow);
+      timelineController.recordTraceForTimelineEvent(frame.gpuEventFlow);
+      timelineController.recordTrace(frame.pipelineItemEndTrace.json);
 
-      _frameCompleteController.add(frame);
+      timelineController.addFrame(frame);
       pendingFrames.remove(frame.id);
       frame.addedToTimeline = true;
+
+      // TODO(kenzie): add cpu profile pre-fetching here when the app is idle.
     }
   }
 
@@ -604,475 +596,3 @@
         (event.isGpuEvent || event.isUiEvent);
   }
 }
-
-/// Data describing a single frame.
-///
-/// Each TimelineFrame should have 2 distinct pieces of data:
-/// * [uiEventFlow] : flow of events showing the UI work for the frame.
-/// * [gpuEventFlow] : flow of events showing the GPU work for the frame.
-class TimelineFrame {
-  TimelineFrame(this.id);
-
-  final String id;
-
-  // TODO(kenzie): we should query the device for targetFps at some point.
-  static const targetFps = 60.0;
-  static const targetMaxDuration = 1000.0 / targetFps;
-
-  /// Marks whether this frame has been added to the timeline.
-  ///
-  /// This should only be set once.
-  bool get addedToTimeline => _addedToTimeline;
-  bool _addedToTimeline;
-
-  set addedToTimeline(v) {
-    assert(_addedToTimeline == null);
-    _addedToTimeline = v;
-  }
-
-  /// Event flows for the UI and GPU work for the frame.
-  final List<TimelineEvent> eventFlows = List.generate(2, (_) => null);
-
-  /// Flow of events describing the UI work for the frame.
-  TimelineEvent get uiEventFlow => eventFlows[TimelineEventType.ui.index];
-
-  /// Flow of events describing the GPU work for the frame.
-  TimelineEvent get gpuEventFlow => eventFlows[TimelineEventType.gpu.index];
-
-  /// Whether the frame is ready for the timeline.
-  ///
-  /// A frame is ready once it has both required event flows as well as
-  /// [_pipelineItemStartTime] and [_pipelineItemEndTime].
-  bool get isReadyForTimeline {
-    return uiEventFlow != null &&
-        gpuEventFlow != null &&
-        pipelineItemTime.start?.inMicroseconds != null &&
-        pipelineItemTime.end?.inMicroseconds != null;
-  }
-
-  // Stores frame start time, end time, and duration.
-  final time = TimeRange();
-
-  /// Pipeline item time range in micros.
-  ///
-  /// This stores the start and end times for the pipeline item event for this
-  /// frame. We use this value to determine whether a TimelineEvent fits within
-  /// the frame's time boundaries.
-  final pipelineItemTime = TimeRange(singleAssignment: false);
-
-  TraceEvent pipelineItemStartTrace;
-
-  TraceEvent pipelineItemEndTrace;
-
-  bool get isWellFormed =>
-      pipelineItemTime.start?.inMicroseconds != null &&
-      pipelineItemTime.end?.inMicroseconds != null;
-
-  int get uiDuration =>
-      uiEventFlow != null ? uiEventFlow.time.duration.inMicroseconds : null;
-
-  double get uiDurationMs => uiDuration != null ? uiDuration / 1000 : null;
-
-  int get gpuDuration =>
-      gpuEventFlow != null ? gpuEventFlow.time.duration.inMicroseconds : null;
-
-  double get gpuDurationMs => gpuDuration != null ? gpuDuration / 1000 : null;
-
-  void setEventFlow(TimelineEvent event, {TimelineEventType type}) {
-    type ??= event?.type;
-
-    eventFlows[type.index] = event;
-
-    if (type == TimelineEventType.ui) {
-      time.start = event?.time?.start;
-    }
-    if (type == TimelineEventType.gpu) {
-      time.end = event?.time?.end;
-    }
-  }
-
-  @override
-  String toString() {
-    return 'Frame $id - $time, ui: ${uiEventFlow.time}, '
-        'gpu: ${gpuEventFlow.time}';
-  }
-}
-
-class TimelineEvent {
-  TimelineEvent(TraceEventWrapper firstTraceEvent)
-      : traceEvents = [firstTraceEvent],
-        type = firstTraceEvent.event.type {
-    time.start = Duration(microseconds: firstTraceEvent.event.timestampMicros);
-  }
-
-  /// Trace events associated with this [TimelineEvent].
-  ///
-  /// There will either be one entry in the list (for DurationComplete events)
-  /// or two (one for the associated DurationBegin event and one for the
-  /// associated DurationEnd event).
-  final List<TraceEventWrapper> traceEvents;
-
-  @visibleForTesting
-  TimelineEventType type;
-
-  TimeRange time = TimeRange();
-
-  TimelineEvent parent;
-
-  List<TimelineEvent> children = [];
-
-  String get name => traceEvents.first.event.name;
-
-  Map<String, dynamic> get beginTraceEventJson => traceEvents.first.json;
-
-  Map<String, dynamic> get endTraceEventJson =>
-      traceEvents.length > 1 ? traceEvents.last.json : null;
-
-  bool get isUiEvent => type == TimelineEventType.ui;
-
-  bool get isGpuEvent => type == TimelineEventType.gpu;
-
-  bool get isUiEventFlow => containsChildWithCondition(
-      (TimelineEvent event) => event.name.contains('Engine::BeginFrame'));
-
-  bool get isGpuEventFlow => containsChildWithCondition(
-      (TimelineEvent event) => event.name.contains('PipelineConsume'));
-
-  /// Depth of this TimelineEvent tree, including [this].
-  ///
-  /// We assume that TimelineEvent nodes are not modified after the first time
-  /// [depth] is accessed. We would need to clear the cache if this was
-  /// supported.
-  int get depth {
-    if (_depth != 0) {
-      return _depth;
-    }
-    for (TimelineEvent child in children) {
-      _depth = max(_depth, child.depth);
-    }
-    return _depth = _depth + 1;
-  }
-
-  int _depth = 0;
-
-  TimelineEvent getRoot() {
-    TimelineEvent root = this;
-    while (root.parent != null) {
-      root = root.parent;
-    }
-    return root;
-  }
-
-  bool containsChildWithCondition(bool condition(TimelineEvent _)) {
-    bool _containsChildWithCondition(
-      TimelineEvent root,
-      bool condition(TimelineEvent _),
-    ) {
-      if (condition(root)) {
-        return true;
-      }
-      for (TimelineEvent newRoot in root.children) {
-        if (_containsChildWithCondition(newRoot, condition)) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    return _containsChildWithCondition(this, condition);
-  }
-
-  void maybeRemoveDuplicate() {
-    void _maybeRemoveDuplicate({@required TimelineEvent parent}) {
-      if (parent.children.length == 1 &&
-          // [parent]'s DurationBegin trace is equal to that of its only child.
-          collectionEquals(
-            parent.beginTraceEventJson,
-            parent.children.first.beginTraceEventJson,
-          ) &&
-          // [parent]'s DurationEnd trace is equal to that of its only child.
-          collectionEquals(
-            parent.endTraceEventJson,
-            parent.children.first.endTraceEventJson,
-          )) {
-        parent.removeChild(children.first);
-      }
-    }
-
-    // Remove [this] event's child if it is a duplicate of [this].
-    if (children.isNotEmpty) {
-      _maybeRemoveDuplicate(parent: this);
-    }
-    // Remove [this] event if it is a duplicate of [parent].
-    if (parent != null) {
-      _maybeRemoveDuplicate(parent: parent);
-    }
-  }
-
-  void removeChild(TimelineEvent childToRemove) {
-    assert(children.contains(childToRemove));
-    final List<TimelineEvent> newChildren = List.from(childToRemove.children);
-    newChildren.forEach(_addChild);
-    children.remove(childToRemove);
-  }
-
-  void addChild(TimelineEvent child) {
-    // Places the child in it's correct position amongst the other children.
-    void _putChildInTree(TimelineEvent root) {
-      // [root] is a leaf. Add child here.
-      if (root.children.isEmpty) {
-        root._addChild(child);
-        return;
-      }
-
-      final _children = root.children.toList();
-
-      // If [child] is the parent of some or all of the members in [_children],
-      // those members will need to be reordered in the tree.
-      final childrenToReorder = [];
-      for (TimelineEvent otherChild in _children) {
-        if (child.couldBeParentOf(otherChild)) {
-          childrenToReorder.add(otherChild);
-        }
-      }
-
-      if (childrenToReorder.isNotEmpty) {
-        root._addChild(child);
-
-        for (TimelineEvent otherChild in childrenToReorder) {
-          // Link [otherChild] with its correct parent [child].
-          child._addChild(otherChild);
-
-          // Unlink [otherChild] from its incorrect parent [root].
-          root.children.remove(otherChild);
-        }
-        return;
-      }
-
-      // Check if a member of [_children] is the parent of [child]. If multiple
-      // children in [_children] share a timestamp, they both could be the
-      // parent of [child]. We reverse [_children] so that we will pick the last
-      // received candidate as the new parent of [child].
-      for (TimelineEvent otherChild in _children.reversed) {
-        if (otherChild.couldBeParentOf(child)) {
-          // Recurse on [otherChild]'s subtree.
-          _putChildInTree(otherChild);
-          return;
-        }
-      }
-
-      // If we have not returned at this point, [child] belongs in
-      // [root.children].
-      root._addChild(child);
-    }
-
-    _putChildInTree(this);
-  }
-
-  void _addChild(TimelineEvent child) {
-    assert(!children.contains(child));
-    children.add(child);
-    child.parent = this;
-  }
-
-  bool couldBeParentOf(TimelineEvent e) {
-    final startTime = time.start.inMicroseconds;
-    final endTime = time.end?.inMicroseconds;
-    final eStartTime = e.time.start.inMicroseconds;
-    final eEndTime = e.time.end?.inMicroseconds;
-
-    if (endTime != null && eEndTime != null) {
-      if (startTime == eStartTime && endTime == eEndTime) {
-        return traceEvents.first.id < e.traceEvents.first.id;
-      }
-      return startTime <= eStartTime && endTime >= eEndTime;
-    } else if (endTime != null) {
-      // We don't use >= to compare [endTime] and [e.startTime] here because we
-      // don't want to falsely make [this] the parent of [e]. We do not know
-      // [e.endTime], meaning [e] could start at [endTime] and end later than
-      // [endTime] (unless e has a duration of 0). In this case, [this] would
-      // not be the parent of [e].
-      return startTime <= eStartTime && endTime > eStartTime;
-    } else if (startTime == eStartTime) {
-      return traceEvents.first.id < e.traceEvents.first.id;
-    } else {
-      return startTime < eStartTime;
-    }
-  }
-
-  void format(StringBuffer buf, String indent) {
-    buf.writeln('$indent$name $time');
-    for (TimelineEvent child in children) {
-      child.format(buf, '  $indent');
-    }
-  }
-
-  void formatFromRoot(StringBuffer buf, String indent) {
-    getRoot().format(buf, indent);
-  }
-
-  void writeTraceToBuffer(StringBuffer buf) {
-    buf.writeln(beginTraceEventJson);
-    for (TimelineEvent child in children) {
-      child.writeTraceToBuffer(buf);
-    }
-    if (endTraceEventJson != null) {
-      buf.writeln(endTraceEventJson);
-    }
-  }
-
-  void recordTrace() {
-    timelineTraceEvents.add(beginTraceEventJson);
-    for (TimelineEvent event in children) {
-      event.recordTrace();
-    }
-    if (endTraceEventJson != null) {
-      timelineTraceEvents.add(endTraceEventJson);
-    }
-  }
-
-  @visibleForTesting
-  TimelineEvent deepCopy() {
-    final copy = TimelineEvent(traceEvents.first);
-    copy.time.end = time.end;
-    copy.parent = parent;
-    for (TimelineEvent child in children) {
-      copy._addChild(child.deepCopy());
-    }
-    return copy;
-  }
-
-  // TODO(kenzie): use DiagnosticableTreeMixin instead.
-  @override
-  String toString() {
-    final buf = StringBuffer();
-    format(buf, '  ');
-    return buf.toString();
-  }
-}
-
-// TODO(devoncarew): Upstream this class to the service protocol library.
-
-/// A single timeline event.
-class TraceEvent {
-  /// Creates a timeline event given JSON-encoded event data.
-  TraceEvent(this.json)
-      : name = json['name'],
-        category = json['cat'],
-        phase = json['ph'],
-        processId = json['pid'],
-        threadId = json['tid'],
-        duration = json['dur'],
-        timestampMicros = json['ts'],
-        args = json['args'];
-
-  /// The original event JSON.
-  final Map<String, dynamic> json;
-
-  /// The name of the event.
-  ///
-  /// Corresponds to the "name" field in the JSON event.
-  final String name;
-
-  /// Event category. Events with different names may share the same category.
-  ///
-  /// Corresponds to the "cat" field in the JSON event.
-  final String category;
-
-  /// For a given long lasting event, denotes the phase of the event, such as
-  /// "B" for "event began", and "E" for "event ended".
-  ///
-  /// Corresponds to the "ph" field in the JSON event.
-  final String phase;
-
-  /// ID of process that emitted the event.
-  ///
-  /// Corresponds to the "pid" field in the JSON event.
-  final int processId;
-
-  /// ID of thread that issues the event.
-  ///
-  /// Corresponds to the "tid" field in the JSON event.
-  final int threadId;
-
-  /// Each async event has an additional required parameter id. We consider the
-  /// events with the same category and id as events from the same event tree.
-  dynamic get id => json['id'];
-
-  /// An optional scope string can be specified to avoid id conflicts, in which
-  /// case we consider events with the same category, scope, and id as events
-  /// from the same event tree.
-  String get scope => json['scope'];
-
-  /// The duration of the event, in microseconds.
-  ///
-  /// Note, some events are reported with duration. Others are reported as a
-  /// pair of begin/end events.
-  ///
-  /// Corresponds to the "dur" field in the JSON event.
-  final int duration;
-
-  /// Time passed since tracing was enabled, in microseconds.
-  final int timestampMicros;
-
-  /// Arbitrary data attached to the event.
-  final Map<String, dynamic> args;
-
-  String get asyncUID {
-    if (scope == null) {
-      return '$category:$id';
-    } else {
-      return '$category:$scope:$id';
-    }
-  }
-
-  TimelineEventType _type;
-
-  TimelineEventType get type {
-    if (_type == null) {
-      if (args['type'] == 'ui') {
-        _type = TimelineEventType.ui;
-      } else if (args['type'] == 'gpu') {
-        _type = TimelineEventType.gpu;
-      } else {
-        _type = TimelineEventType.unknown;
-      }
-    }
-    return _type;
-  }
-
-  set type(TimelineEventType t) => _type = t;
-
-  bool get isUiEvent => type == TimelineEventType.ui;
-
-  bool get isGpuEvent => type == TimelineEventType.gpu;
-
-  @override
-  String toString() => '$type event [id: $id] [cat: $category] [ph: $phase] '
-      '$name - [timestamp: $timestampMicros] [duration: $duration]';
-}
-
-int _traceEventWrapperId = 0;
-
-class TraceEventWrapper implements Comparable<TraceEventWrapper> {
-  TraceEventWrapper(this.event, this.timeReceived)
-      : id = _traceEventWrapperId++;
-  final TraceEvent event;
-
-  final num timeReceived;
-
-  final int id;
-
-  Map<String, dynamic> get json => event.json;
-
-  bool processed = false;
-
-  @override
-  int compareTo(TraceEventWrapper other) {
-    // Order events based on their timestamps. If the events share a timestamp,
-    // order them in the order we received them.
-    final compare =
-        event.timestampMicros.compareTo(other.event.timestampMicros);
-    return compare != 0 ? compare : id.compareTo(other.id);
-  }
-}
diff --git a/devtools/lib/src/timeline/timeline_screen.dart b/devtools/lib/src/timeline/timeline_screen.dart
new file mode 100644
index 0000000..d22f7e7
--- /dev/null
+++ b/devtools/lib/src/timeline/timeline_screen.dart
@@ -0,0 +1,573 @@
+// Copyright 2018 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 'dart:convert';
+import 'dart:html' as html;
+import 'dart:math' as math;
+
+import 'package:js/js.dart';
+import 'package:meta/meta.dart';
+import 'package:split/split.dart' as split;
+
+import '../charts/flame_chart_canvas.dart';
+import '../framework/framework.dart';
+import '../globals.dart';
+import '../service_extensions.dart';
+import '../ui/analytics.dart' as ga;
+import '../ui/analytics_platform.dart' as ga_platform;
+import '../ui/custom.dart';
+import '../ui/elements.dart';
+import '../ui/icons.dart';
+import '../ui/material_icons.dart';
+import '../ui/primer.dart';
+import '../ui/service_extension_elements.dart';
+import '../ui/ui_utils.dart';
+import '../ui/vm_flag_elements.dart';
+import 'event_details.dart';
+import 'frames_bar_chart.dart';
+import 'timeline_controller.dart';
+import 'timeline_flame_chart.dart';
+import 'timeline_model.dart';
+import 'timeline_protocol.dart';
+
+// TODO(devoncarew): show the Skia picture (gpu drawing commands) for a frame
+
+// TODO(devoncarew): show the list of widgets re-drawn during a frame
+
+// TODO(devoncarew): display whether running in debug or profile
+
+// TODO(devoncarew): Have a timeline view thumbnail overview.
+
+// TODO(devoncarew): Switch to showing all timeline events, but highlighting the
+// area associated with the selected frame.
+
+const enableMultiModeTimeline = false;
+
+class TimelineScreen extends Screen {
+  TimelineScreen({bool disabled, String disabledTooltip})
+      : super(
+          name: 'Timeline',
+          id: timelineScreenId,
+          iconClass: 'octicon-pulse',
+          disabled: disabled,
+          disabledTooltip: disabledTooltip,
+        );
+
+  TimelineController timelineController = TimelineController();
+
+  FramesBarChart framesBarChart;
+
+  CoreElement flameChartContainer;
+
+  TimelineFlameChartCanvas flameChartCanvas;
+
+  EventDetails eventDetails;
+
+  PButton pauseButton;
+
+  PButton resumeButton;
+
+  PButton _startRecordingButton;
+
+  PButton _stopRecordingButton;
+
+  PButton clearButton;
+
+  PButton exportButton;
+
+  PButton exitOfflineModeButton;
+
+  ServiceExtensionButton performanceOverlayButton;
+
+  ProfileGranularitySelector _profileGranularitySelector;
+
+  CoreElement _frameBasedTimelineSettingContainer;
+
+  CoreElement _frameBasedTimelineCheckbox;
+
+  CoreElement _recordingInstructions;
+
+  CoreElement _recordingStatus;
+
+  CoreElement _recordingStatusMessage;
+
+  CoreElement upperButtonSection;
+
+  CoreElement debugButtonSection;
+
+  split.Splitter splitter;
+
+  bool splitterConfigured = false;
+
+  @override
+  CoreElement createContent(Framework framework) {
+    ga_platform.setupDimensions();
+
+    final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
+
+    pauseButton = PButton.icon('Pause recording', FlutterIcons.pause_white_2x)
+      ..small()
+      ..primary()
+      ..hidden(timelineController.timelineMode == TimelineMode.full)
+      ..click(_pauseFrameRecording);
+
+    resumeButton =
+        PButton.icon('Resume recording', FlutterIcons.resume_black_disabled_2x)
+          ..small()
+          ..clazz('margin-left')
+          ..disabled = timelineController.manuallyPaused
+          ..hidden(timelineController.timelineMode == TimelineMode.full)
+          ..click(_resumeFrameRecording);
+
+    _startRecordingButton = PButton.icon('Record', recordPrimary)
+      ..small()
+      ..primary()
+      ..hidden(timelineController.timelineMode == TimelineMode.frameBased)
+      ..click(_startRecording);
+
+    _stopRecordingButton = PButton.icon('Stop', stop)
+      ..small()
+      ..clazz('margin-left')
+      ..disabled = !timelineController.recording
+      ..hidden(timelineController.timelineMode == TimelineMode.frameBased)
+      ..click(_stopRecording);
+
+    _recordingInstructions = createRecordingInstructions(
+        recordingGoal: 'to start recording timeline trace.');
+
+    _recordingStatus = div(c: 'center-in-parent')
+      ..layoutVertical()
+      ..flex()
+      ..add([
+        _recordingStatusMessage = div(c: 'recording-status-message'),
+        Spinner.centered(classes: ['recording-spinner']),
+      ]);
+
+    exportButton = PButton.icon('Export', exportIcon)
+      ..small()
+      ..clazz('margin-left')
+      ..setAttribute('title', 'Export timeline')
+      ..click(_exportTimeline);
+
+    clearButton = PButton.icon('Clear', clearIcon)
+      ..small()
+      ..clazz('margin-left')
+      ..setAttribute('title', 'Clear timeline')
+      ..click(clearTimeline);
+
+    exitOfflineModeButton = PButton.icon(
+      'Exit offline mode',
+      exitIcon,
+    )
+      ..small()
+      ..setAttribute('title', 'Exit offline mode to connect to a VM Service.')
+      ..setAttribute('hidden', 'true')
+      ..click(_exitOfflineMode);
+
+    performanceOverlayButton = ServiceExtensionButton(performanceOverlay);
+
+    _profileGranularitySelector = ProfileGranularitySelector(framework);
+
+    _frameBasedTimelineCheckbox = CoreElement('input', classes: 'checkbox')
+      ..setAttribute('type', 'checkbox');
+    final html.InputElement checkbox = _frameBasedTimelineCheckbox.element;
+    checkbox
+      ..checked = timelineController.timelineMode == TimelineMode.frameBased
+      ..onChange.listen((_) => _setTimelineMode(
+          timelineMode:
+              checkbox.checked ? TimelineMode.frameBased : TimelineMode.full));
+
+    _frameBasedTimelineSettingContainer = div(c: 'checkbox-container')
+      ..layoutHorizontal()
+      ..add([
+        _frameBasedTimelineCheckbox,
+        div(text: 'Show frames', c: 'checkbox-text')
+      ]);
+
+    // TODO(kenzie): once [enableMultiModeTimeline] is enabled by default,
+    // adjust collapsible-xxx CSS classes to account for timeline mode checkbox.
+    upperButtonSection = div(c: 'section')
+      ..layoutHorizontal()
+      ..add(<CoreElement>[
+        div(c: 'btn-group collapsible-885')
+          ..add([
+            pauseButton,
+            resumeButton,
+            _startRecordingButton,
+            _stopRecordingButton,
+          ]),
+        div(c: 'btn-group collapsible-685')..add(clearButton),
+        exitOfflineModeButton,
+        div()..flex(),
+        debugButtonSection = div(c: 'btn-group'),
+        if (enableMultiModeTimeline) _frameBasedTimelineSettingContainer,
+        _profileGranularitySelector.selector..clazz('margin-left'),
+        div(c: 'btn-group collapsible-685 margin-left')
+          ..add(performanceOverlayButton.button),
+        div(c: 'btn-group collapsible-685')..add(exportButton),
+      ]);
+
+    _maybeAddDebugButtons();
+
+    screenDiv.add(<CoreElement>[
+      upperButtonSection,
+      framesBarChart = FramesBarChart(timelineController),
+      div(c: 'section')
+        ..layoutVertical()
+        ..flex()
+        ..add(<CoreElement>[
+          flameChartContainer =
+              div(c: 'timeline-flame-chart-container section-border')
+                ..flex()
+                ..layoutVertical()
+                ..hidden(true),
+          eventDetails = EventDetails(timelineController)..hidden(true),
+        ]),
+    ]);
+
+    maybeAddDebugMessage(framework, timelineScreenId);
+
+    return screenDiv;
+  }
+
+  @override
+  void onContentAttached() {
+    timelineController.onSelectedFrame.listen((_) {
+      flameChartContainer
+        ..clear()
+        ..hidden(false);
+      final TimelineFrame frame = timelineController.timelineData.selectedFrame;
+      flameChartCanvas = TimelineFlameChartCanvas(
+        data: frame,
+        width: flameChartContainer.element.clientWidth.toDouble(),
+        height: math.max(
+          // Subtract [rowHeightWithPadding] to account for timeline at the top of
+          // the flame chart.
+          flameChartContainer.element.clientHeight.toDouble(),
+          // Add 1 to account for a row of padding at the bottom of the chart.
+          (frame.uiEventFlow.depth + frame.gpuEventFlow.depth + 1) *
+                  rowHeightWithPadding +
+              TimelineFlameChartCanvas.sectionSpacing,
+        ),
+      );
+      flameChartCanvas.onNodeSelected.listen((node) {
+        eventDetails.titleBackgroundColor = node.backgroundColor;
+        eventDetails.titleTextColor = node.textColor;
+        timelineController.selectTimelineEvent(node.data);
+      });
+      flameChartContainer.add(flameChartCanvas.element);
+
+      _configureSplitter();
+    });
+
+    timelineController.onLoadOfflineData.listen((_) {
+      framesBarChart.hidden(false);
+      flameChartContainer..hidden(true);
+      _destroySplitter();
+    });
+
+    timelineController.onNonFatalError.listen((message) {
+      ga.error(message, false);
+    });
+
+    // The size of [flameChartContainer] will change as the splitter moved.
+    // Observe resizing so that we can rebuild the flame chart canvas as
+    // necessary.
+    // TODO(kenzie): clean this code up when
+    // https://github.com/dart-lang/html/issues/104 is fixed.
+    final observer =
+        html.ResizeObserver(allowInterop((List<dynamic> entries, _) {
+      // TODO(kenzie): observe resizing for recordedTimeline as well. Recorded
+      // timeline will not have a selected frame.
+      if (flameChartCanvas == null ||
+          timelineController.timelineMode == TimelineMode.full) {
+        return;
+      }
+
+      flameChartCanvas.forceRebuildForSize(
+        flameChartCanvas.widthWithInsets,
+        math.max(
+          // Subtract [rowHeightWithPadding] to account for the size of
+          // [stackFrameDetails] section at the bottom of the chart.
+          flameChartContainer.element.scrollHeight.toDouble(),
+          // Add 1 to account for a row of padding at the bottom of the chart.
+          (timelineController.timelineData.selectedFrame.uiEventFlow.depth +
+                      timelineController
+                          .timelineData.selectedFrame.gpuEventFlow.depth +
+                      1) *
+                  rowHeightWithPadding +
+              TimelineFlameChartCanvas.sectionSpacing,
+        ),
+      );
+    }));
+    observer.observe(flameChartContainer.element);
+  }
+
+  void _configureSplitter() {
+    // Configure the flame chart / event details splitter if we haven't
+    // already.
+    if (!splitterConfigured) {
+      // TODO(jacobr): we need to tweak this layout so there is more room to
+      // display this UI. On typical devices, the space available is very
+      // limited making the UI harder to use than it would be otherwise.
+      splitter = split.flexSplit(
+        [flameChartContainer.element, eventDetails.element],
+        horizontal: false,
+        gutterSize: defaultSplitterWidth,
+        sizes: [75, 25],
+        minSize: [50, 50],
+      );
+      splitterConfigured = true;
+    }
+  }
+
+  void _destroySplitter() {
+    if (splitterConfigured) {
+      splitter.destroy();
+      splitterConfigured = false;
+    }
+  }
+
+  @override
+  void entering() {
+    _updateListeningState();
+    _updateButtonStates();
+    _profileGranularitySelector.setGranularity();
+  }
+
+  @override
+  void exiting() {
+    _updateListeningState();
+    _updateButtonStates();
+  }
+
+  void _exitOfflineMode() {
+    // This needs to be called first because [framework.exitOfflineMode()] will
+    // remove all elements from the dom if we are not connected to an app.
+    // Performing operations from [_clearTimeline()] on elements that have been
+    // removed will throw exceptions, so we need to maintain this order.
+    clearTimeline();
+    eventDetails.reset(hide: true);
+    timelineController.exitOfflineMode();
+    // This needs to be called before we update the button states because it
+    // changes the value of [offlineMode], which the button states depend on.
+    framework.exitOfflineMode();
+    // Revert to the previously selected mode on offline exit.
+    _setTimelineMode(timelineMode: timelineController.timelineMode);
+    _updateButtonStates();
+  }
+
+  Future<void> _pauseFrameRecording() async {
+    assert(timelineController.timelineMode == TimelineMode.frameBased);
+    timelineController.pause(manual: true);
+    ga.select(ga.timeline, ga.pause);
+    _updateButtonStates();
+    await _updateListeningState();
+  }
+
+  Future<void> _resumeFrameRecording() async {
+    assert(timelineController.timelineMode == TimelineMode.frameBased);
+    timelineController.resume();
+    ga.select(ga.timeline, ga.resume);
+    _updateButtonStates();
+    await _updateListeningState();
+  }
+
+  void _startRecording() {
+    assert(timelineController.timelineMode == TimelineMode.full);
+    timelineController.startRecording();
+    _recordingInstructions.hidden(true);
+    _recordingStatusMessage.text = 'Recording timeline trace';
+    _recordingStatus.hidden(false);
+    _updateButtonStates();
+  }
+
+  void _stopRecording() {
+    assert(timelineController.timelineMode == TimelineMode.full);
+    _recordingStatusMessage.text = 'Processing timeline trace';
+    timelineController.stopRecording();
+    _recordingStatus.hidden(true);
+    _updateButtonStates();
+  }
+
+  void _setTimelineMode({@required TimelineMode timelineMode}) {
+    // TODO(kenzie): the two modes should be aware of one another and we should
+    // share data. For simplicity, we will start by having each mode be aware of
+    // only its own data and clearing on mode switch.
+    timelineController.timelineData.clear();
+
+    timelineController.timelineMode = timelineMode;
+    _updateButtonStates();
+
+    // Update visibility and then reset - the order matters here.
+    framesBarChart
+      ..hidden(timelineMode == TimelineMode.full)
+      ..frameUIgraph.reset();
+
+    flameChartCanvas = null;
+    flameChartContainer
+      ..clear()
+      ..hidden(timelineMode == TimelineMode.frameBased);
+    if (timelineMode == TimelineMode.full) {
+      flameChartContainer.add([
+        _recordingInstructions..hidden(false),
+        _recordingStatus..hidden(true),
+      ]);
+    }
+
+    eventDetails.reset(hide: timelineMode == TimelineMode.frameBased);
+
+    if (timelineMode == TimelineMode.frameBased) {
+      _destroySplitter();
+    } else {
+      _configureSplitter();
+    }
+  }
+
+  void _updateButtonStates() {
+    pauseButton
+      ..disabled = timelineController.manuallyPaused
+      ..hidden(
+          offlineMode || timelineController.timelineMode == TimelineMode.full);
+    resumeButton
+      ..disabled = !timelineController.manuallyPaused
+      ..hidden(
+          offlineMode || timelineController.timelineMode == TimelineMode.full);
+    _startRecordingButton
+      ..disabled = timelineController.recording
+      ..hidden(offlineMode ||
+          timelineController.timelineMode == TimelineMode.frameBased);
+    _stopRecordingButton
+      ..disabled = !timelineController.recording
+      ..hidden(offlineMode ||
+          timelineController.timelineMode == TimelineMode.frameBased);
+    _frameBasedTimelineCheckbox.disabled = timelineController.recording;
+
+    // TODO(kenzie): support loading offline data in both modes.
+    _frameBasedTimelineSettingContainer.hidden(offlineMode);
+
+    clearButton
+      ..disabled = timelineController.recording
+      ..hidden(offlineMode);
+    exportButton
+      ..disabled = timelineController.recording
+      ..hidden(offlineMode);
+    performanceOverlayButton.button.hidden(offlineMode);
+    _profileGranularitySelector.selector.hidden(offlineMode);
+    exitOfflineModeButton.hidden(!offlineMode);
+  }
+
+  Future<void> _updateListeningState() async {
+    final bool shouldBeRunning =
+        !timelineController.manuallyPaused && !offlineMode && isCurrentScreen;
+    final bool isRunning = !timelineController.paused;
+    await timelineController.timelineService.updateListeningState(
+      shouldBeRunning: shouldBeRunning,
+      isRunning: isRunning,
+    );
+  }
+
+  void clearTimeline() {
+    timelineController.timelineData?.clear();
+    flameChartContainer
+        .hidden(timelineController.timelineMode == TimelineMode.frameBased);
+    flameChartCanvas = null;
+    eventDetails.reset(
+        hide: timelineController.timelineMode == TimelineMode.frameBased);
+
+    switch (timelineController.timelineMode) {
+      case TimelineMode.frameBased:
+        debugHandledTraceEvents.clear();
+        debugFrameTracking.clear();
+        framesBarChart.frameUIgraph.reset();
+        _destroySplitter();
+        break;
+      case TimelineMode.full:
+        _recordingInstructions.hidden(false);
+    }
+  }
+
+  void _exportTimeline() {
+    // TODO(kenzie): add analytics for this. It would be helpful to know how
+    // complex the problems are that users are trying to solve.
+    final String encodedTimelineData =
+        jsonEncode(timelineController.timelineData.json);
+    final now = DateTime.now();
+    final timestamp =
+        '${now.year}_${now.month}_${now.day}-${now.microsecondsSinceEpoch}';
+    downloadFile(encodedTimelineData, 'timeline_$timestamp.json');
+  }
+
+  /// Adds a button to the timeline that will dump debug information to text
+  /// files and download them. This will only appear if the [debugTimeline] flag
+  /// is true.
+  void _maybeAddDebugButtons() {
+    if (debugTimeline) {
+      debugButtonSection.add(PButton('Debug dump timeline')
+        ..small()
+        ..click(() {
+          // Trace event json in the order we handled the events.
+          final handledTraceEventsJson = {
+            'traceEvents': debugHandledTraceEvents
+          };
+          downloadFile(
+            jsonEncode(handledTraceEventsJson),
+            'handled_trace_output.json',
+          );
+
+          // Significant events in the frame tracking process.
+          downloadFile(
+            debugFrameTracking.toString(),
+            'frame_tracking_output.txt',
+          );
+
+          final timelineProtocol = timelineController.timelineProtocol;
+
+          // Current status of our frame tracking elements (i.e. pendingEvents,
+          // pendingFrames).
+          final buf = StringBuffer();
+          buf.writeln('Pending events: '
+              '${timelineProtocol.pendingEvents.length}');
+          for (TimelineEvent event in timelineProtocol.pendingEvents) {
+            event.format(buf, '    ');
+            buf.writeln();
+          }
+          buf.writeln('\nPending frames: '
+              '${timelineProtocol.pendingFrames.length}');
+          for (TimelineFrame frame in timelineProtocol.pendingFrames.values) {
+            buf.writeln('${frame.toString()}');
+          }
+          if (timelineProtocol.currentEventNodes[TimelineEventType.ui.index] !=
+              null) {
+            buf.writeln('\nCurrent UI event node:');
+            timelineProtocol.currentEventNodes[TimelineEventType.ui.index]
+                .format(buf, '   ');
+          }
+          if (timelineProtocol.currentEventNodes[TimelineEventType.gpu.index] !=
+              null) {
+            buf.writeln('\n Current GPU event node:');
+            timelineProtocol.currentEventNodes[TimelineEventType.gpu.index]
+                .format(buf, '   ');
+          }
+          if (timelineProtocol.heaps[TimelineEventType.ui.index].isNotEmpty) {
+            buf.writeln('\nUI heap');
+            for (TraceEventWrapper wrapper in timelineProtocol
+                .heaps[TimelineEventType.ui.index]
+                .toList()) {
+              buf.writeln(wrapper.event.json.toString());
+            }
+          }
+          if (timelineProtocol.heaps[TimelineEventType.gpu.index].isNotEmpty) {
+            buf.writeln('\nGPU heap');
+            for (TraceEventWrapper wrapper in timelineProtocol
+                .heaps[TimelineEventType.gpu.index]
+                .toList()) {
+              buf.writeln(wrapper.event.json.toString());
+            }
+          }
+          downloadFile(buf.toString(), 'pending_frame_tracking_status.txt');
+        }));
+    }
+  }
+}
diff --git a/devtools/lib/src/timeline/timeline_service.dart b/devtools/lib/src/timeline/timeline_service.dart
new file mode 100644
index 0000000..e21b036
--- /dev/null
+++ b/devtools/lib/src/timeline/timeline_service.dart
@@ -0,0 +1,132 @@
+// Copyright 2019 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:meta/meta.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../config_specific/allowed_error.dart';
+import '../globals.dart';
+import '../profiler/cpu_profile_service.dart';
+import '../vm_service_wrapper.dart';
+import 'timeline_controller.dart';
+import 'timeline_model.dart';
+import 'timeline_protocol.dart';
+
+/// Manages interactions between the Timeline and the VmService.
+class TimelineService {
+  TimelineService(this.timelineController) {
+    _initListeners();
+  }
+
+  final TimelineController timelineController;
+
+  void _initListeners() async {
+    serviceManager.onConnectionAvailable.listen(_handleConnectionStart);
+    // Do not start the timeline for Dart web apps.
+    if (serviceManager.hasConnection &&
+        !await serviceManager.connectedApp.isDartWebApp) {
+      _handleConnectionStart(serviceManager.service);
+    }
+    serviceManager.onConnectionClosed.listen(_handleConnectionStop);
+  }
+
+  void _handleConnectionStart(VmServiceWrapper service) {
+    allowedError(serviceManager.service
+        .setFlag('profile_period', '$defaultSamplePeriod'));
+    serviceManager.service.onEvent('Timeline').listen((Event event) {
+      final List<dynamic> list = event.json['timelineEvents'];
+      final List<Map<String, dynamic>> events =
+          list.cast<Map<String, dynamic>>();
+
+      if (!offlineMode &&
+          !timelineController.manuallyPaused &&
+          !timelineController.paused) {
+        for (Map<String, dynamic> json in events) {
+          final TraceEvent e = TraceEvent(json);
+          timelineController.timelineProtocol?.processTraceEvent(e);
+        }
+      }
+    });
+  }
+
+  void _handleConnectionStop(dynamic event) {
+    // TODO(kenzie): investigate if we need to do anything here.
+  }
+
+  Future<void> startTimeline() async {
+    timelineController.timelineData = TimelineData();
+
+    await serviceManager.serviceAvailable.future;
+    await allowedError(serviceManager.service
+        .setVMTimelineFlags(<String>['GC', 'Dart', 'Embedder']));
+    await allowedError(serviceManager.service.clearVMTimeline());
+
+    final Timeline timeline = await serviceManager.service.getVMTimeline();
+    final List<dynamic> list = timeline.json['traceEvents'];
+    final List<Map<String, dynamic>> traceEvents =
+        list.cast<Map<String, dynamic>>();
+
+    final List<TraceEvent> events = traceEvents
+        .map((Map<String, dynamic> event) => TraceEvent(event))
+        .where((TraceEvent event) {
+      return event.name == 'thread_name';
+    }).toList();
+
+    // TODO(kenzie): Remove this logic once ui/gpu distinction changes are
+    // available in the engine.
+    int uiThreadId;
+    int gpuThreadId;
+
+    // Store the thread names for debugging purposes. If [uiThreadId] or
+    // [gpuThreadId] are null, we will print all the thread names we received
+    // to console.
+    final threadNames = [];
+
+    for (TraceEvent event in events) {
+      final name = event.args['name'];
+      threadNames.add(name);
+
+      // iOS: "io.flutter.1.ui (12652)", Android: "1.ui (12652)",
+      // Dream (g3): "io.flutter.ui (12652)"
+      if (name.contains('.ui')) {
+        uiThreadId = event.threadId;
+      }
+      // iOS: "io.flutter.1.gpu (12651)", Android: "1.gpu (12651)",
+      // Dream (g3): "io.flutter.gpu (12651)"
+      if (name.contains('.gpu')) {
+        gpuThreadId = event.threadId;
+      }
+    }
+
+    if (uiThreadId == null || gpuThreadId == null) {
+      timelineController.logNonFatalError(
+          'Could not find UI thread and / or GPU thread from names: '
+          '$threadNames');
+    }
+
+    timelineController.timelineProtocol = TimelineProtocol(
+      uiThreadId: uiThreadId,
+      gpuThreadId: gpuThreadId,
+      timelineController: timelineController,
+    );
+  }
+
+  Future<void> updateListeningState({
+    @required bool shouldBeRunning,
+    @required bool isRunning,
+  }) async {
+    await serviceManager.serviceAvailable.future;
+    if (shouldBeRunning && isRunning && !timelineController.hasStarted) {
+      await startTimeline();
+    } else if (shouldBeRunning && !isRunning) {
+      timelineController.resume();
+      await allowedError(serviceManager.service
+          .setVMTimelineFlags(<String>['GC', 'Dart', 'Embedder']));
+    } else if (!shouldBeRunning && isRunning) {
+      // TODO(devoncarew): turn off the events
+      timelineController.pause();
+      await allowedError(serviceManager.service.setVMTimelineFlags(<String>[]));
+    }
+  }
+}
diff --git a/devtools/lib/src/trees.dart b/devtools/lib/src/trees.dart
new file mode 100644
index 0000000..18c71cb
--- /dev/null
+++ b/devtools/lib/src/trees.dart
@@ -0,0 +1,148 @@
+// Copyright 2019 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 'dart:collection';
+import 'dart:math';
+
+/// Non-UI specific tree code should live in this file.
+///
+/// This file does not have direct dependencies on dart:html and therefore
+/// allows for testing to be done on the VM.
+
+// TODO(kenzie): look into consolidating logic between this file and
+// ui/trees.dart, which houses generic tree types vs the base classes in this
+// file.
+
+class TreeNode<T extends TreeNode<T>> {
+  T parent;
+
+  final List<T> children = [];
+
+  /// Index in [parent.children].
+  int index = -1;
+
+  /// Depth of this tree, including [this].
+  ///
+  /// We assume that TreeNodes are not modified after the first time [depth] is
+  /// accessed. We would need to clear the cache before accessing, otherwise.
+  int get depth {
+    if (_depth != 0) {
+      return _depth;
+    }
+    for (T child in children) {
+      _depth = max(_depth, child.depth);
+    }
+    return _depth = _depth + 1;
+  }
+
+  int _depth = 0;
+
+  T get root {
+    if (_root != null) {
+      return _root;
+    }
+
+    // Store nodes we have visited so we can cache the root value for each one
+    // once we find the root.
+    final visited = {this};
+
+    T root = this;
+    while (root.parent != null) {
+      visited.add(root);
+      root = root.parent;
+    }
+
+    // Set [_root] for all nodes we visited.
+    for (T node in visited) {
+      node._root = root;
+    }
+
+    return root;
+  }
+
+  T _root;
+
+  /// The level (0-based) of this tree node in the tree.
+  int get level {
+    if (_level != null) {
+      return _level;
+    }
+    int level = 0;
+    T current = this;
+    while (current.parent != null) {
+      current = current.parent;
+      level++;
+    }
+    return _level = level;
+  }
+
+  int _level;
+
+  /// Whether the tree table node is expandable.
+  bool get isExpandable => children.isNotEmpty;
+
+  /// Whether the node is currently expanded in the tree table.
+  bool get isExpanded => _isExpanded;
+  bool _isExpanded = false;
+
+  void expand() {
+    _isExpanded = true;
+  }
+
+  void collapse() {
+    _isExpanded = false;
+  }
+
+  void addChild(T child) {
+    children.add(child);
+    child.parent = this;
+    child.index = children.length - 1;
+  }
+
+  /// Expands this node and all of its children (cascading).
+  void expandCascading() {
+    breadthFirstTraversal<T>(this, action: (T node) {
+      node.expand();
+    });
+  }
+
+  /// Collapses this node and all of its children (cascading).
+  void collapseCascading() {
+    breadthFirstTraversal<T>(this, action: (T node) {
+      node.collapse();
+    });
+  }
+
+  bool containsChildWithCondition(bool condition(T node)) {
+    final T childWithCondition = breadthFirstTraversal<T>(
+      this,
+      returnCondition: condition,
+    );
+    return childWithCondition != null;
+  }
+}
+
+/// Traverses a tree in breadth-first order.
+///
+/// [returnCondition] specifies the condition for which we should stop
+/// traversing the tree. For example, if we are calling this method to perform
+/// BFS, [returnCondition] would specify when we have found the node we are
+/// searching for. [action] specifies an action that we will execute on each
+/// node. For example, if we need to traverse a tree and change a property on
+/// every single node, we would do this through the [action] param.
+T breadthFirstTraversal<T extends TreeNode<T>>(T root,
+    {bool returnCondition(T node), void action(T node)}) {
+  final queue = Queue.of([root]);
+  while (queue.isNotEmpty) {
+    final node = queue.removeFirst();
+    if (returnCondition != null && returnCondition(node)) {
+      return node;
+    }
+    if (action != null) {
+      action(node);
+    }
+    node.children.forEach(queue.add);
+  }
+  return null;
+}
diff --git a/devtools/lib/src/ui/analytics.dart b/devtools/lib/src/ui/analytics.dart
index 4f8855d..75a4b50 100644
--- a/devtools/lib/src/ui/analytics.dart
+++ b/devtools/lib/src/ui/analytics.dart
@@ -9,7 +9,7 @@
 
 import 'package:devtools/devtools.dart' as devtools show version;
 import 'package:js/js.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 import '../eval_on_dart_library.dart';
 import '../globals.dart';
@@ -43,6 +43,10 @@
 const String devToolsChromeOS = 'CrOS'; // Chrome OS
 // Dimension6 devToolsVersion
 
+// Dimension7 ideLaunched
+const String ideLaunchedQuery = 'ide'; // '&ide=' query parameter
+const String ideLaunchedCLI = 'CLI'; // Command Line Interface
+
 @JS('gtagsEnabled')
 external bool isGtagsEnabled();
 
@@ -66,6 +70,7 @@
     String devtools_platform, // dimension4 linux/android/mac/windows
     String devtools_chrome, // dimension5 Chrome version #
     String devtools_version, // dimension6 DevTools version #
+    String ide_launched, // dimension7 Devtools launched (CLI, VSCode, Android)
 
     int gpu_duration,
     int ui_duration,
@@ -96,6 +101,7 @@
   external String get devtools_platform;
   external String get devtools_chrome;
   external String get devtools_version;
+  external String get ide_launched;
 
   // Custom metrics:
   external int get gpu_duration;
@@ -114,6 +120,7 @@
     String devtools_platform, // dimension4 linux/android/mac/windows
     String devtools_chrome, // dimension5 Chrome version #
     String devtools_version, // dimension6 DevTools version #
+    String ide_launched, // dimension7 IDE launched DevTools
   });
 
   @override
@@ -128,6 +135,7 @@
   external String get devtools_platform;
   external String get devtools_chrome;
   external String get devtools_version;
+  external String get ide_launched;
 }
 
 void screen(
@@ -135,16 +143,19 @@
   int value = 0,
 ]) {
   GTag.event(
-      screenName,
-      GtagEventDevTools(
-          event_category: screenViewEvent,
-          value: value,
-          user_app: userAppType,
-          user_build: userBuildType,
-          user_platform: userPlatformType,
-          devtools_platform: devtoolsPlatformType,
-          devtools_chrome: devtoolsChrome,
-          devtools_version: devtoolsVersion));
+    screenName,
+    GtagEventDevTools(
+      event_category: screenViewEvent,
+      value: value,
+      user_app: userAppType,
+      user_build: userBuildType,
+      user_platform: userPlatformType,
+      devtools_platform: devtoolsPlatformType,
+      devtools_chrome: devtoolsChrome,
+      devtools_version: devtoolsVersion,
+      ide_launched: ideLaunched,
+    ),
+  );
 }
 
 void select(
@@ -153,17 +164,20 @@
   int value = 0,
 ]) {
   GTag.event(
-      screenName,
-      GtagEventDevTools(
-          event_category: selectEvent,
-          event_label: selectedItem,
-          value: value,
-          user_app: userAppType,
-          user_build: userBuildType,
-          user_platform: userPlatformType,
-          devtools_platform: devtoolsPlatformType,
-          devtools_chrome: devtoolsChrome,
-          devtools_version: devtoolsVersion));
+    screenName,
+    GtagEventDevTools(
+      event_category: selectEvent,
+      event_label: selectedItem,
+      value: value,
+      user_app: userAppType,
+      user_build: userBuildType,
+      user_platform: userPlatformType,
+      devtools_platform: devtoolsPlatformType,
+      devtools_chrome: devtoolsChrome,
+      devtools_version: devtoolsVersion,
+      ide_launched: ideLaunched,
+    ),
+  );
 }
 
 // Used only for Timeline Frame selection.
@@ -174,18 +188,21 @@
   int uiDuration, // Custom metric
 ]) {
   GTag.event(
-      screenName,
-      GtagEventDevTools(
-          event_category: selectEvent,
-          event_label: selectedItem,
-          gpu_duration: gpuDuration,
-          ui_duration: uiDuration,
-          user_app: userAppType,
-          user_build: userBuildType,
-          user_platform: userPlatformType,
-          devtools_platform: devtoolsPlatformType,
-          devtools_chrome: devtoolsChrome,
-          devtools_version: devtoolsVersion));
+    screenName,
+    GtagEventDevTools(
+      event_category: selectEvent,
+      event_label: selectedItem,
+      gpu_duration: gpuDuration,
+      ui_duration: uiDuration,
+      user_app: userAppType,
+      user_build: userBuildType,
+      user_platform: userPlatformType,
+      devtools_platform: devtoolsPlatformType,
+      devtools_chrome: devtoolsChrome,
+      devtools_version: devtoolsVersion,
+      ide_launched: ideLaunched,
+    ),
+  );
 }
 
 String _lastGaError;
@@ -198,7 +215,8 @@
   if (_lastGaError == errorMessage) return;
   _lastGaError = errorMessage;
 
-  GTag.exception(GtagExceptionDevTools(
+  GTag.exception(
+    GtagExceptionDevTools(
       description: errorMessage,
       fatal: fatal,
       user_app: userAppType,
@@ -206,7 +224,10 @@
       user_platform: userPlatformType,
       devtools_platform: devtoolsPlatformType,
       devtools_chrome: devtoolsChrome,
-      devtools_version: devtoolsVersion));
+      devtools_version: devtoolsVersion,
+      ide_launched: ideLaunched,
+    ),
+  );
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -224,6 +245,8 @@
 
 const String devtoolsVersion = devtools.version; //dimension6 n.n.n
 
+String _ideLaunched = ''; // dimension7 IDE launched DevTools (VSCode, CLI, ...)
+
 String get userAppType => _userAppType;
 set userAppType(String __userAppType) {
   _userAppType = __userAppType;
@@ -249,6 +272,11 @@
   _devtoolsChrome = __devtoolsChrome;
 }
 
+String get ideLaunched => _ideLaunched;
+set ideLaunched(String __ideLaunched) {
+  _ideLaunched = __ideLaunched;
+}
+
 bool _analyticsComputed = false;
 bool get isDimensionsComputed => _analyticsComputed;
 void dimensionsComputed() {
@@ -264,7 +292,7 @@
   final isProfile = await serviceManager.connectedApp.isProfileBuild;
   final isAnyFlutterApp = await serviceManager.connectedApp.isAnyFlutterApp;
 
-  if (isFlutter) {
+  if (isFlutter && !isProfile) {
     // Compute the Flutter platform for the user's running application.
     final VmService vmService = serviceManager.service;
     final io = EvalOnDartLibrary(['dart:io'], vmService);
diff --git a/devtools/lib/src/ui/analytics_constants.dart b/devtools/lib/src/ui/analytics_constants.dart
index 9e976f2..b825252 100644
--- a/devtools/lib/src/ui/analytics_constants.dart
+++ b/devtools/lib/src/ui/analytics_constants.dart
@@ -42,7 +42,7 @@
 const String repaintRainbow = 'repaintRainbow';
 const String debugBanner = 'debugBanner';
 const String trackRebuilds = 'trackRebuilds';
-const String toggleIoS = 'iOS';
+const String togglePlatform = 'togglePlatform';
 const String selectWidgetMode = 'selectWidgetMode';
 
 // Timeline UX actions:
@@ -51,6 +51,7 @@
 const String timelineFlameUi = 'flameUI'; // Selected a UI flame
 
 // Memory UX actions:
+const String search = 'search';
 const String snapshot = 'snapshot';
 const String reset = 'reset';
 const String gC = 'gc';
@@ -69,3 +70,4 @@
 
 // Logging UX actions:
 const String clearLogs = 'clearLogs';
+const String structuredErrors = 'structuredErrors';
diff --git a/devtools/lib/src/ui/analytics_platform.dart b/devtools/lib/src/ui/analytics_platform.dart
index d72aaaf..8282e0b 100644
--- a/devtools/lib/src/ui/analytics_platform.dart
+++ b/devtools/lib/src/ui/analytics_platform.dart
@@ -61,6 +61,17 @@
   }
 }
 
+// Look at the query parameters '&ide=' and record in GA.
+void computeDevToolsQueryParams() {
+  ga.ideLaunched = ga.ideLaunchedCLI; // Default is Command Line launch.
+
+  final Uri uri = Uri.parse(html.window.location.toString());
+  final ideValue = uri.queryParameters[ga.ideLaunchedQuery];
+  if (ideValue != null) {
+    ga.ideLaunched = ideValue;
+  }
+}
+
 bool _computing = false;
 
 int _stillWaiting = 0;
@@ -101,6 +112,7 @@
     // available before first GA event sent.
     await ga.computeUserApplicationCustomGTagData();
     computeDevToolsCustomGTagsData();
+    computeDevToolsQueryParams();
     ga.dimensionsComputed();
   }
 }
diff --git a/devtools/lib/src/ui/colors.dart b/devtools/lib/src/ui/colors.dart
new file mode 100644
index 0000000..b52031d
--- /dev/null
+++ b/devtools/lib/src/ui/colors.dart
@@ -0,0 +1,77 @@
+// Copyright 2019 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 'fake_flutter/dart_ui/dart_ui.dart';
+import 'theme.dart';
+
+/// This file holds color constants that are used throughout DevTools.
+// TODO(kenzie): move colors from other pages to this file for consistency.
+
+/// Mark: Timeline / CPU profiler.
+///
+/// Light mode is Light Blue 50 palette and Dark mode is Blue 50 palette.
+/// https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
+const mainUiColor = ThemedColor(mainUiColorLight, mainUiColorDark);
+const mainGpuColor = ThemedColor(mainGpuColorLight, mainGpuColorDark);
+
+const mainUiColorLight = Color(0xFF81D4FA); // Light Blue 50 - 200
+const mainGpuColorLight = Color(0xFF0288D1); // Light Blue 50 - 700
+
+const mainUiColorSelectedLight = Color(0xFFD4D7DA); // Lighter grey.
+const mainGpuColorSelectedLight = Color(0xFFB5B5B5); // Darker grey.
+
+const mainUiColorDark = Color(0xFF9EBEF9); // Blue 200 Material Dark
+const mainGpuColorDark = Color(0xFF1A73E8); // Blue 600 Material Dark
+
+const mainUiColorSelectedDark = Colors.white;
+const mainGpuColorSelectedDark = Color(0xFFC9C9C9); // Grey.
+
+const Color selectedUiColor = ThemedColor(
+  mainUiColorSelectedLight,
+  mainUiColorSelectedDark,
+);
+const Color selectedGpuColor = ThemedColor(
+  mainGpuColorSelectedLight,
+  mainGpuColorSelectedDark,
+);
+
+// Light Blue 50: 200-400 (light mode) - see https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
+// Blue Material Dark: 200-400 (dark mode) - see https://standards.google/guidelines/google-material/color/dark-theme.html#style.
+final uiColorPalette = [
+  const ThemedColor(mainUiColorLight, mainUiColorDark),
+  const ThemedColor(Color(0xFF4FC3F7), Color(0xFF8AB4F7)),
+  const ThemedColor(Color(0xFF29B6F6), Color(0xFF669CF6)),
+];
+
+// Light Blue 50: 700-900 (light mode) - see https://material.io/design/color/the-color-system.html#tools-for-picking-colors.
+// Blue Material Dark: 500-700 (dark mode) - see https://standards.google/guidelines/google-material/color/dark-theme.html#style.
+final gpuColorPalette = [
+  const ThemedColor(mainGpuColorLight, mainGpuColorDark),
+  const ThemedColor(Color(0xFF0277BD), Color(0xFF1966D2)),
+  const ThemedColor(Color(0xFF01579B), Color(0xFF1859BD)),
+];
+
+const selectedFlameChartItemColor = ThemedColor(
+  mainUiColorSelectedLight,
+  mainUiColorSelectedLight,
+);
+
+// Light is Red @ .2 opacity, Dark is Red 200 Material Dark @ .2 opacity.
+const Color jankGlowInside = ThemedColor(
+  Color(0x66FF0000),
+  Color(0x66F29C99),
+);
+
+// Light is Red @ .5 opacity, Dark is Red 600 Material Dark @ .6 opacity.
+const Color jankGlowEdge = ThemedColor(
+  Color(0x80FF0000),
+  Color(0x99CE191C),
+);
+
+// Red 50 - 400 is light at 1/2 opacity, Dark Red 500 Material Dark.
+const Color highwater16msColor = mainUiColorSelectedLight;
+
+const Color hoverTextHighContrastColor = Colors.white;
+
+const Color hoverTextColor = Colors.black;
diff --git a/devtools/lib/src/ui/custom.dart b/devtools/lib/src/ui/custom.dart
index b9a9c08..7cb60e5 100644
--- a/devtools/lib/src/ui/custom.dart
+++ b/devtools/lib/src/ui/custom.dart
@@ -43,9 +43,15 @@
 }
 
 class Spinner extends CoreElement {
-  Spinner() : super('div') {
+  Spinner({List<String> classes = const []}) : super('div') {
     clazz('spinner');
+    classes.forEach(clazz);
   }
+
+  static Spinner centered({List<String> classes = const []}) =>
+      Spinner(classes: ['centered']..addAll(classes));
+
+  void remove() => element.remove();
 }
 
 typedef ListRenderer<T> = CoreElement Function(T item);
@@ -326,14 +332,18 @@
   }
 }
 
+// TODO(kenzie): wrap this element in a larger div to increase tap target.
 class TreeToggle extends CoreElement {
-  TreeToggle({bool empty = false})
+  TreeToggle({bool empty = false, bool forceOpen = false})
       : super('div', classes: 'tree-toggle octicon') {
     if (!empty) {
       click(toggle);
-    }
-    if (!empty) {
-      clazz('octicon-triangle-right');
+      if (forceOpen) {
+        _isOpen = true;
+        clazz('octicon-triangle-down');
+      } else {
+        clazz('octicon-triangle-right');
+      }
     }
   }
 
diff --git a/devtools/lib/src/ui/drag_scroll.dart b/devtools/lib/src/ui/drag_scroll.dart
index e3bdc27..c8949f8 100644
--- a/devtools/lib/src/ui/drag_scroll.dart
+++ b/devtools/lib/src/ui/drag_scroll.dart
@@ -2,12 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:html';
 
 import '../ui/elements.dart';
 
 class DragScroll {
-  /// Whether the element was dragged on the previous click.
+  /// Whether the element was dragged on the previous click or touch.
   bool wasDragged = false;
 
   // This callback can optionally be set to perform additional actions on a
@@ -17,29 +18,35 @@
 
   set onVerticalScroll(VoidCallback callback) => _onVerticalScroll = callback;
 
+  StreamSubscription<MouseEvent> _mouseMoveListener;
+
+  StreamSubscription<MouseEvent> _mouseUpListener;
+
+  StreamSubscription<TouchEvent> _touchMoveListener;
+
+  StreamSubscription<TouchEvent> _touchEndListener;
+
   void enableDragScrolling(CoreElement element) {
     final dragged = element.element;
+    _handleMouseDrags(dragged);
+    _handleTouchDrags(dragged);
+  }
 
+  void _handleMouseDrags(Element dragged) {
     num lastX;
     num lastY;
-    bool clicked = false;
 
     dragged.onMouseDown.listen((event) {
       final MouseEvent m = event;
-      clicked = true;
-      wasDragged = false;
 
+      wasDragged = false;
       lastX = m.client.x;
       lastY = m.client.y;
 
       m.preventDefault();
-    });
 
-    window.onMouseUp.listen((event) => clicked = false);
-
-    window.onMouseMove.listen((event) {
-      final MouseEvent m = event;
-      if (clicked) {
+      _mouseMoveListener = window.onMouseMove.listen((event) {
+        final MouseEvent m = event;
         final num newX = m.client.x;
         final num newY = m.client.y;
 
@@ -57,7 +64,61 @@
         lastY = newY;
 
         wasDragged = true;
-      }
+      });
+
+      _mouseUpListener = window.onMouseUp.listen((event) {
+        _mouseUpListener.cancel();
+        _mouseMoveListener.cancel();
+      });
+    });
+  }
+
+  void _handleTouchDrags(Element dragged) {
+    num lastX;
+    num lastY;
+
+    dragged.onTouchStart.listen((event) {
+      final TouchEvent t = event;
+      // If there are multiple touches, always use the first.
+      final Touch touch = t.touches.first;
+
+      wasDragged = false;
+      lastX = touch.client.x;
+      lastY = touch.client.y;
+
+      t.preventDefault();
+
+      _touchMoveListener = window.onTouchMove.listen((event) {
+        final TouchEvent t = event;
+        // If there are multiple touches, always use the first.
+        final Touch touch = t.touches.first;
+
+        final num newX = touch.client.x;
+        final num newY = touch.client.y;
+
+        final num deltaX = lastX - newX;
+        final num deltaY = lastY - newY;
+
+        dragged.scrollLeft += deltaX.round();
+        dragged.scrollTop += deltaY.round();
+
+        if (_onVerticalScroll != null && deltaY.round() != 0) {
+          _onVerticalScroll();
+        }
+
+        lastX = newX;
+        lastY = newY;
+
+        wasDragged = true;
+      });
+
+      _touchEndListener = window.onTouchEnd.listen((event) {
+        final TouchEvent t = event;
+        if (t.touches.isEmpty) {
+          _touchEndListener.cancel();
+          _touchMoveListener.cancel();
+        }
+      });
     });
   }
 }
diff --git a/devtools/lib/src/ui/elements.dart b/devtools/lib/src/ui/elements.dart
index e9bb029..5c204f2 100644
--- a/devtools/lib/src/ui/elements.dart
+++ b/devtools/lib/src/ui/elements.dart
@@ -153,6 +153,8 @@
     element.text = value;
   }
 
+  bool get isVisible => element.style.visibility == 'visible';
+
   /// Add the given child to this element's list of children. [child] must be
   /// either a `CoreElement` or an `Element`.
   dynamic add(dynamic child) {
@@ -270,11 +272,27 @@
     element.style.height = value;
   }
 
+  int get top => element.getBoundingClientRect().top.round();
+  int get left => element.getBoundingClientRect().left.round();
+
   Stream<MouseEvent> get onClick => element.onClick.where((_) => !disabled);
 
+  Stream<TouchEvent> get onTouchStart =>
+      element.onTouchStart.where((_) => !disabled);
+
+  Stream<TouchEvent> get onTouchMove =>
+      element.onTouchMove.where((_) => !disabled);
+
+  Stream<TouchEvent> get onTouchEnd =>
+      element.onTouchEnd.where((_) => !disabled);
+
   Stream<Event> get onFocus => element.onFocus.where((_) => !disabled);
 
   Stream<Event> get onBlur => element.onBlur.where((_) => !disabled);
+  Stream<MouseEvent> get onMouseOver =>
+      element.onMouseOver.where((_) => !disabled);
+  Stream<MouseEvent> get onMouseLeave =>
+      element.onMouseLeave.where((_) => !disabled);
 
   Stream<Event> get onScroll => element.onScroll;
 
@@ -322,6 +340,22 @@
     });
   }
 
+  /// Subscribe to the [blur] event stream with a no-arg handler.
+  StreamSubscription<Event> over(void handle()) {
+    return onMouseOver.listen((Event e) {
+      e.stopImmediatePropagation();
+      handle();
+    });
+  }
+
+  /// Subscribe to the [blur] event stream with a no-arg handler.
+  StreamSubscription<Event> leave(void handle()) {
+    return onMouseLeave.listen((Event e) {
+      e.stopImmediatePropagation();
+      handle();
+    });
+  }
+
   void clear() => element.children.clear();
 
   void scrollIntoView({bool bottom = false, bool top = false}) {
diff --git a/devtools/lib/src/ui/fake_flutter/change_notifier.dart b/devtools/lib/src/ui/fake_flutter/change_notifier.dart
new file mode 100644
index 0000000..056af4a
--- /dev/null
+++ b/devtools/lib/src/ui/fake_flutter/change_notifier.dart
@@ -0,0 +1,221 @@
+// Copyright 2019 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.
+
+part of 'fake_flutter.dart';
+
+/// An object that maintains a list of listeners.
+///
+/// The listeners are typically used to notify clients that the object has been
+/// updated.
+///
+/// There are two variants of this interface:
+///
+///  * [ValueListenable], an interface that augments the [Listenable] interface
+///    with the concept of a _current value_.
+///
+///  * [Animation], an interface that augments the [ValueListenable] interface
+///    to add the concept of direction (forward or reverse).
+///
+/// Many classes in the Flutter API use or implement these interfaces. The
+/// following subclasses are especially relevant:
+///
+///  * [ChangeNotifier], which can be subclassed or mixed in to create objects
+///    that implement the [Listenable] interface.
+///
+///  * [ValueNotifier], which implements the [ValueListenable] interface with
+///    a mutable value that triggers the notifications when modified.
+///
+/// The terms "notify clients", "send notifications", "trigger notifications",
+/// and "fire notifications" are used interchangeably.
+///
+/// See also:
+///
+///  * [AnimatedBuilder], a widget that uses a builder callback to rebuild
+///    whenever a given [Listenable] triggers its notifications. This widget is
+///    commonly used with [Animation] subclasses, wherein its name. It is a
+///    subclass of [AnimatedWidget], which can be used to create widgets that
+///    are driven from a [Listenable].
+///  * [ValueListenableBuilder], a widget that uses a builder callback to
+///    rebuild whenever a [ValueListenable] object triggers its notifications,
+///    providing the builder with the value of the object.
+///  * [InheritedNotifier], an abstract superclass for widgets that use a
+///    [Listenable]'s notifications to trigger rebuilds in descendant widgets
+///    that declare a dependency on them, using the [InheritedWidget] mechanism.
+///  * [new Listenable.merge], which creates a [Listenable] that triggers
+///    notifications whenever any of a list of other [Listenable]s trigger their
+///    notifications.
+abstract class Listenable {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Listenable();
+
+  /// Register a closure to be called when the object notifies its listeners.
+  void addListener(VoidCallback listener);
+
+  /// Remove a previously registered closure from the list of closures that the
+  /// object notifies.
+  void removeListener(VoidCallback listener);
+}
+
+/// An interface for subclasses of [Listenable] that expose a [value].
+///
+/// This interface is implemented by [ValueNotifier<T>] and [Animation<T>], and
+/// allows other APIs to accept either of those implementations interchangeably.
+abstract class ValueListenable<T> extends Listenable {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const ValueListenable();
+
+  /// The current value of the object. When the value changes, the callbacks
+  /// registered with [addListener] will be invoked.
+  T get value;
+}
+
+/// A class that can be extended or mixed in that provides a change notification
+/// API using [VoidCallback] for notifications.
+///
+/// [ChangeNotifier] is optimized for small numbers (one or two) of listeners.
+/// It is O(N) for adding and removing listeners and O(N²) for dispatching
+/// notifications (where N is the number of listeners).
+///
+/// See also:
+///
+///  * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value.
+class ChangeNotifier implements Listenable {
+  var _listeners = <VoidCallback>[];
+
+  bool _debugAssertNotDisposed() {
+    assert(() {
+      if (_listeners == null) {
+        throw FlutterError('A $runtimeType was used after being disposed.\n'
+            'Once you have called dispose() on a $runtimeType, it can no longer be used.');
+      }
+      return true;
+    }());
+    return true;
+  }
+
+  /// Whether any listeners are currently registered.
+  ///
+  /// Clients should not depend on this value for their behavior, because having
+  /// one listener's logic change when another listener happens to start or stop
+  /// listening will lead to extremely hard-to-track bugs. Subclasses might use
+  /// this information to determine whether to do any work when there are no
+  /// listeners, however; for example, resuming a [Stream] when a listener is
+  /// added and pausing it when a listener is removed.
+  ///
+  /// Typically this is used by overriding [addListener], checking if
+  /// [hasListeners] is false before calling `super.addListener()`, and if so,
+  /// starting whatever work is needed to determine when to call
+  /// [notifyListeners]; and similarly, by overriding [removeListener], checking
+  /// if [hasListeners] is false after calling `super.removeListener()`, and if
+  /// so, stopping that same work.
+  @protected
+  bool get hasListeners {
+    assert(_debugAssertNotDisposed());
+    return _listeners.isNotEmpty;
+  }
+
+  /// Register a closure to be called when the object changes.
+  ///
+  /// This method must not be called after [dispose] has been called.
+  @override
+  void addListener(VoidCallback listener) {
+    assert(_debugAssertNotDisposed());
+    _listeners.add(listener);
+  }
+
+  /// Remove a previously registered closure from the list of closures that are
+  /// notified when the object changes.
+  ///
+  /// If the given listener is not registered, the call is ignored.
+  ///
+  /// This method must not be called after [dispose] has been called.
+  ///
+  /// If a listener had been added twice, and is removed once during an
+  /// iteration (i.e. in response to a notification), it will still be called
+  /// again. If, on the other hand, it is removed as many times as it was
+  /// registered, then it will no longer be called. This odd behavior is the
+  /// result of the [ChangeNotifier] not being able to determine which listener
+  /// is being removed, since they are identical, and therefore conservatively
+  /// still calling all the listeners when it knows that any are still
+  /// registered.
+  ///
+  /// This surprising behavior can be unexpectedly observed when registering a
+  /// listener on two separate objects which are both forwarding all
+  /// registrations to a common upstream object.
+  @override
+  void removeListener(VoidCallback listener) {
+    assert(_debugAssertNotDisposed());
+    _listeners.remove(listener);
+  }
+
+  /// Discards any resources used by the object. After this is called, the
+  /// object is not in a usable state and should be discarded (calls to
+  /// [addListener] and [removeListener] will throw after the object is
+  /// disposed).
+  ///
+  /// This method should only be called by the object's owner.
+  @mustCallSuper
+  void dispose() {
+    assert(_debugAssertNotDisposed());
+    _listeners = null;
+  }
+
+  /// Call all the registered listeners.
+  ///
+  /// Call this method whenever the object changes, to notify any clients the
+  /// object may have. Listeners that are added during this iteration will not
+  /// be visited. Listeners that are removed during this iteration will not be
+  /// visited after they are removed.
+  ///
+  /// Exceptions thrown by listeners will be caught and reported using
+  /// [FlutterError.reportError].
+  ///
+  /// This method must not be called after [dispose] has been called.
+  ///
+  /// Surprising behavior can result when reentrantly removing a listener (i.e.
+  /// in response to a notification) that has been registered multiple times.
+  /// See the discussion at [removeListener].
+  @protected
+  @visibleForTesting
+  void notifyListeners() {
+    assert(_debugAssertNotDisposed());
+    if (_listeners != null) {
+      final List<VoidCallback> localListeners =
+          List<VoidCallback>.from(_listeners);
+      for (VoidCallback listener in localListeners) {
+        try {
+          if (_listeners.contains(listener)) listener();
+        } catch (exception) {
+          debugPrint('Error while dispatching notifications for $runtimeType.'
+              ' $exception');
+        }
+      }
+    }
+  }
+}
+
+/// A [ChangeNotifier] that holds a single value.
+///
+/// When [value] is replaced, this class notifies its listeners.
+class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
+  /// Creates a [ChangeNotifier] that wraps this value.
+  ValueNotifier(this._value);
+
+  /// The current value stored in this notifier.
+  ///
+  /// When the value is replaced, this class notifies its listeners.
+  @override
+  T get value => _value;
+  T _value;
+  set value(T newValue) {
+    if (_value == newValue) return;
+    _value = newValue;
+    notifyListeners();
+  }
+
+  @override
+  String toString() => '${describeIdentity(this)}($value)';
+}
diff --git a/devtools/lib/src/ui/fake_flutter/diagnosticable.dart b/devtools/lib/src/ui/fake_flutter/diagnosticable.dart
index 735367d..8534670 100644
--- a/devtools/lib/src/ui/fake_flutter/diagnosticable.dart
+++ b/devtools/lib/src/ui/fake_flutter/diagnosticable.dart
@@ -4,8 +4,6 @@
 
 // This is a direct copy of
 // /packages/flutter/lib/src/foundation/diagnostics.dart
-// with a couple of tweaks to support error levels that haven't yet landed in
-// the core Flutter repo but should land soon.
 part of 'fake_flutter.dart';
 
 // Examples can assume:
@@ -60,23 +58,14 @@
 
   /// Diagnostics that provide a hint about best practices.
   ///
-  /// For example, a diagnostic providing a hint on  on how to fix an overflow
-  /// error.
+  /// For example, a diagnostic describing best practices for fixing an error.
   hint,
 
-  /// Diagnostics that provide a hint for how to fix a problem.
+  /// Diagnostics that summarize other diagnostics present.
   ///
-  /// For example, a diagnostic providing advice for how to fix an overflow
-  /// error.
-  fix,
-
-  /// Diagnostics that describe a contract.
-  ///
-  /// For example, a diagnostic describing the constraints applying to layout or
-  /// invariants that must remain true to correctly compose objects.
-  contract,
-
-  violation,
+  /// For example, use this level for a short one or two line summary
+  /// describing other diagnostics present.
+  summary,
 
   /// Diagnostics that indicate errors or unexpected conditions.
   ///
@@ -98,6 +87,9 @@
 ///  * [DiagnosticsNode.toStringDeep], which dumps text art trees for these
 ///    styles.
 enum DiagnosticsTreeStyle {
+  /// A style that does not display the tree, for release mode.
+  none,
+
   /// Sparse style for displaying trees.
   ///
   /// See also:
@@ -156,13 +148,16 @@
   /// Render the tree on a single line without showing children.
   singleLine,
 
-  /// Render the tree on a single line without showing children acting like the
-  /// line is a header.
-  headerLine,
-
-  /// Render the tree on a single line with the name and value on separate
-  /// lines.
-  indentedSingleLine,
+  /// Render the tree using a style appropriate for properties that are part
+  /// of an error message.
+  ///
+  /// The name is placed on one line with the value and properties placed on
+  /// the following line.
+  ///
+  /// See also:
+  ///  * [singleLine], which displays the same information but keeps the
+  ///    property and value on the same line.
+  errorProperty,
 
   /// Render only the immediate properties of a node instead of the full tree.
   ///
@@ -174,9 +169,6 @@
 
   /// Render only the children of a node truncating before the tree becomes too
   /// large.
-  ///
-  /// See also:
-  ///  * XXX
   truncateChildren,
 }
 
@@ -208,6 +200,7 @@
     this.afterDescription = '',
     this.beforeProperties = '',
     this.afterProperties = '',
+    this.mandatoryAfterProperties = '',
     this.propertySeparator = '',
     this.bodyIndent = '',
     this.footer = '',
@@ -244,10 +237,6 @@
   /// Prefix to add to the first line to display a child with this style.
   final String prefixLineOne;
 
-  /// Similar to prefixLineOne but applies even when this is the root node in
-  /// the tree.
-  final String beforeName;
-
   /// Suffix to add to end of the first line to make its length match the footer.
   final String suffixLineOne;
 
@@ -299,6 +288,12 @@
   /// properties on one line.
   final bool lineBreakProperties;
 
+  /// Text added immediately before the name of the node.
+  ///
+  /// See [errorTextConfiguration] for an example of using this to achieve a
+  /// custom line art style.
+  final String beforeName;
+
   /// Text added immediately after the name of the node.
   ///
   /// See [transitionTextConfiguration] for an example of using a value other
@@ -306,7 +301,7 @@
   final String afterName;
 
   /// Text to add immediately after the description line of a node with
-  /// properties and/or children if he node has a body.
+  /// properties and/or children if the node has a body.
   final String afterDescriptionIfBody;
 
   /// Text to add immediately after the description line of a node with
@@ -325,6 +320,13 @@
   /// See documentation for [beforeProperties].
   final String afterProperties;
 
+  /// Mandatory string to add after the properties of a node regardless of
+  /// whether the node has any properties.
+  ///
+  /// See [headerLineTextConfiguration] for an example of using this field to
+  /// add a colon at the end of the header line.
+  final String mandatoryAfterProperties;
+
   /// Property separator to add between properties.
   ///
   /// See [singleLineTextConfiguration] for an example of using this field
@@ -394,7 +396,7 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.sparse]
+///  * [DiagnosticsTreeStyle.sparse], uses this style for ASCII art display.
 final TextTreeConfiguration sparseTextConfiguration = TextTreeConfiguration(
   prefixLineOne: '├─',
   prefixOtherLines: ' ',
@@ -472,7 +474,7 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.dense]
+///  * [DiagnosticsTreeStyle.dense], uses this style for ASCII art display.
 final TextTreeConfiguration denseTextConfiguration = TextTreeConfiguration(
   propertySeparator: ', ',
   beforeProperties: '(',
@@ -511,7 +513,7 @@
 ///
 /// /// See also:
 ///
-///  * [DiagnosticsTreeStyle.transition]
+///  * [DiagnosticsTreeStyle.transition], uses this style for ASCII art display.
 final TextTreeConfiguration transitionTextConfiguration = TextTreeConfiguration(
   prefixLineOne: '╞═╦══ ',
   prefixLastChildLineOne: '╘═╦══ ',
@@ -579,10 +581,9 @@
 /// ════════════════════════════════════════════════════════════════
 /// ```
 ///
-/// /// See also:
+/// See also:
 ///
-///  * [DiagnosticsTreeStyle.error]
-// TODO(jacobr): cleanup this style to create nice flower boxes in other cases
+///  * [DiagnosticsTreeStyle.error], uses this style for ASCII art display.
 final TextTreeConfiguration errorTextConfiguration = TextTreeConfiguration(
   prefixLineOne: '╞═╦',
   prefixLastChildLineOne: '╘═╦',
@@ -623,7 +624,7 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.whitespace]
+///  * [DiagnosticsTreeStyle.whitespace], uses this style for ASCII art display.
 final TextTreeConfiguration whitespaceTextConfiguration = TextTreeConfiguration(
   prefixLineOne: '',
   prefixLastChildLineOne: '',
@@ -640,8 +641,7 @@
   isBlankLineBetweenPropertiesAndChildren: false,
 );
 
-/// Whitespace only configuration where children are consistently indented
-/// two spaces.
+/// Whitespace only configuration where children are not indented.
 ///
 /// Use this style when indentation is not needed to disambiguate parents from
 /// children as in the case of a [DiagnosticsStackTrace].
@@ -659,7 +659,7 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.flat]
+///  * [DiagnosticsTreeStyle.flat], uses this style for ASCII art display.
 final TextTreeConfiguration flatTextConfiguration = TextTreeConfiguration(
   prefixLineOne: '',
   prefixLastChildLineOne: '',
@@ -683,7 +683,7 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.singleLine]
+///  * [DiagnosticsTreeStyle.singleLine], uses this style for ASCII art display.
 final TextTreeConfiguration singleLineTextConfiguration = TextTreeConfiguration(
   propertySeparator: ', ',
   beforeProperties: '(',
@@ -695,40 +695,14 @@
   lineBreakProperties: false,
   addBlankLineIfNoChildren: false,
   showChildren: false,
-  propertyPrefixIfChildren: '',
-  propertyPrefixNoChildren: '',
+  propertyPrefixIfChildren: '  ',
+  propertyPrefixNoChildren: '  ',
   linkCharacter: '',
   prefixOtherLinesRootNode: '',
 );
 
-/// Render a node as a single line omitting children styling the node like it is
-/// a header describing content following it in the tree even though the node is
-/// not actually the parent of the content following it.
-///
-/// Example:
-/// `<name>: <description>(<property1>, <property2>, ..., <propertyN>):`
-///
-/// See also:
-///
-///  * [DiagnosticsTreeStyle.headerLine]
-final TextTreeConfiguration headerLineTextConfiguration = TextTreeConfiguration(
-  propertySeparator: ', ',
-  beforeProperties: '(',
-  afterProperties: ')',
-  prefixLineOne: '',
-  prefixOtherLines: '',
-  prefixLastChildLineOne: '',
-  lineBreak: '',
-  lineBreakProperties: false,
-  addBlankLineIfNoChildren: false,
-  showChildren: false,
-  propertyPrefixIfChildren: '',
-  propertyPrefixNoChildren: '',
-  linkCharacter: '',
-  prefixOtherLinesRootNode: '',
-);
-
-/// Render a node as a single line omitting children.
+/// Render the name on a line followed by the body and properties on the next
+/// line omitting the children.
 ///
 /// Example:
 /// ```
@@ -738,8 +712,9 @@
 ///
 /// See also:
 ///
-///  * [DiagnosticsTreeStyle.indentedSingleLine]
-final TextTreeConfiguration singleLineTextConfigurationIndented =
+///  * [DiagnosticsTreeStyle.errorProperty], uses this style for ASCII art
+///    display.
+final TextTreeConfiguration errorPropertyTextConfiguration =
     TextTreeConfiguration(
   propertySeparator: ', ',
   beforeProperties: '(',
@@ -747,16 +722,16 @@
   prefixLineOne: '',
   prefixOtherLines: '',
   prefixLastChildLineOne: '',
-  lineBreak: '',
+  lineBreak: '\n',
   lineBreakProperties: false,
   addBlankLineIfNoChildren: false,
   showChildren: false,
-  propertyPrefixIfChildren: '',
-  propertyPrefixNoChildren: '',
+  propertyPrefixIfChildren: '  ',
+  propertyPrefixNoChildren: '  ',
   linkCharacter: '',
   prefixOtherLinesRootNode: '',
-  afterName:
-      '\n  ', // This is the difference between this text configuration and the typical single line text configuration. TODO(jacobr): verify this is robust.
+  afterName: ':',
+  isNameOnOwnLine: true,
 );
 
 /// Render a node on multiple lines omitting children.
@@ -787,6 +762,8 @@
   showChildren: false,
 );
 
+enum _WordWrapParseMode { inSpace, inWord, atBreak }
+
 /// Builder that builds a String with specified prefixes for the first and
 /// subsequent lines.
 ///
@@ -795,7 +772,11 @@
 /// prefixed by [prefixLineOne] and subsequent lines prefixed by
 /// [prefixOtherLines].
 class _PrefixedStringBuilder {
-  _PrefixedStringBuilder(this.prefixLineOne, this.prefixOtherLines);
+  _PrefixedStringBuilder(
+      {@required this.prefixLineOne,
+      @required String prefixOtherLines,
+      this.wrapWidth})
+      : _prefixOtherLines = prefixOtherLines;
 
   /// Prefix to add to the first line.
   final String prefixLineOne;
@@ -804,108 +785,272 @@
   ///
   /// The prefix can be modified while the string is being built in which case
   /// subsequent lines will be added with the modified prefix.
-  String prefixOtherLines;
-
-  final StringBuffer _buffer = StringBuffer();
-  bool _hasMultipleLines = false;
-  int _lineIndex = 0;
-
-  int get _currentLineLength {
-    return (_hasMultipleLines ? prefixOtherLines : prefixLineOne).length +
-        _lineIndex;
+  String get prefixOtherLines => _nextPrefixOtherLines ?? _prefixOtherLines;
+  String _prefixOtherLines;
+  set prefixOtherLines(String prefix) {
+    _prefixOtherLines = prefix;
+    _nextPrefixOtherLines = null;
   }
 
+  String _nextPrefixOtherLines;
+  void incrementPrefixOtherLines(String suffix,
+      {@required bool updateCurrentLine}) {
+    if (_currentLine.isEmpty || updateCurrentLine) {
+      _prefixOtherLines = prefixOtherLines + suffix;
+      _nextPrefixOtherLines = null;
+    } else {
+      _nextPrefixOtherLines = prefixOtherLines + suffix;
+    }
+  }
+
+  final int wrapWidth;
+
+  /// Buffer containing lines that have already been completely laid out.
+  final StringBuffer _buffer = StringBuffer();
+
+  /// Buffer containing the current line that has not yet been wrapped.
+  final StringBuffer _currentLine = StringBuffer();
+
+  /// List of pairs of integers indicating the start and end of each block of
+  /// text within _currentLine that can be wrapped.
+  final List<int> _wrappableRanges = <int>[];
+
   /// Whether the string being built already has more than 1 line.
-  bool get hasMultipleLines => _hasMultipleLines;
+  bool get requiresMultipleLines =>
+      _numLines > 1 ||
+      (_numLines == 1 && _currentLine.isNotEmpty) ||
+      (_currentLine.length + _getCurrentPrefix(true).length > wrapWidth);
+
+  bool get isCurrentLineEmpty => _currentLine.isEmpty;
+
+  int _numLines = 0;
+
+  void _finalizeLine(bool addTrailingLineBreak) {
+    final bool firstLine = _buffer.isEmpty;
+    final String text = _currentLine.toString();
+    _currentLine.clear();
+
+    if (_wrappableRanges.isEmpty) {
+      // Fast path. There were no wrappable spans of text.
+      _writeLine(
+        text,
+        includeLineBreak: addTrailingLineBreak,
+        firstLine: firstLine,
+      );
+      return;
+    }
+    final Iterable<String> lines = _wordWrapLine(
+      text,
+      _wrappableRanges,
+      wrapWidth,
+      startOffset: firstLine ? prefixLineOne.length : _prefixOtherLines.length,
+      otherLineOffset:
+          firstLine ? _prefixOtherLines.length : _prefixOtherLines.length,
+    );
+    int i = 0;
+    final int length = lines.length;
+    for (String line in lines) {
+      i++;
+      _writeLine(
+        line,
+        includeLineBreak: addTrailingLineBreak || i < length,
+        firstLine: firstLine,
+      );
+    }
+    _wrappableRanges.clear();
+  }
+
+  /// Wraps the given string at the given width.
+  ///
+  /// Wrapping occurs at space characters (U+0020).
+  ///
+  /// This is not suitable for use with arbitrary Unicode text. For example, it
+  /// doesn't implement UAX #14, can't handle ideographic text, doesn't hyphenate,
+  /// and so forth. It is only intended for formatting error messages.
+  ///
+  /// This method wraps a sequence of text where only some spans of text can be
+  /// used as wrap boundaries.
+  static Iterable<String> _wordWrapLine(
+      String message, List<int> wrapRanges, int width,
+      {int startOffset = 0, int otherLineOffset = 0}) sync* {
+    if (message.length + startOffset < width) {
+      // Nothing to do. The line doesn't wrap.
+      yield message;
+      return;
+    }
+    int startForLengthCalculations = -startOffset;
+    bool addPrefix = false;
+    int index = 0;
+    _WordWrapParseMode mode = _WordWrapParseMode.inSpace;
+    int lastWordStart;
+    int lastWordEnd;
+    int start = 0;
+
+    int currentChunk = 0;
+
+    // This helper is called with increasing indexes.
+    bool noWrap(int index) {
+      while (true) {
+        if (currentChunk >= wrapRanges.length) return true;
+
+        if (index < wrapRanges[currentChunk + 1]) break; // Found nearest chunk.
+        currentChunk += 2;
+      }
+      return index < wrapRanges[currentChunk];
+    }
+
+    while (true) {
+      switch (mode) {
+        case _WordWrapParseMode
+            .inSpace: // at start of break point (or start of line); can't break until next break
+          while ((index < message.length) && (message[index] == ' '))
+            index += 1;
+          lastWordStart = index;
+          mode = _WordWrapParseMode.inWord;
+          break;
+        case _WordWrapParseMode
+            .inWord: // looking for a good break point. Treat all text
+          while ((index < message.length) &&
+              (message[index] != ' ' || noWrap(index))) index += 1;
+          mode = _WordWrapParseMode.atBreak;
+          break;
+        case _WordWrapParseMode.atBreak: // at start of break point
+          if ((index - startForLengthCalculations > width) ||
+              (index == message.length)) {
+            // we are over the width line, so break
+            if ((index - startForLengthCalculations <= width) ||
+                (lastWordEnd == null)) {
+              // we should use this point, because either it doesn't actually go over the
+              // end (last line), or it does, but there was no earlier break point
+              lastWordEnd = index;
+            }
+            final String line = message.substring(start, lastWordEnd);
+            yield line;
+            addPrefix = true;
+            if (lastWordEnd >= message.length) return;
+            // just yielded a line
+            if (lastWordEnd == index) {
+              // we broke at current position
+              // eat all the wrappable spaces, then set our start point
+              // Even if some of the spaces are not wrappable that is ok.
+              while ((index < message.length) && (message[index] == ' '))
+                index += 1;
+              start = index;
+              mode = _WordWrapParseMode.inWord;
+            } else {
+              // we broke at the previous break point, and we're at the start of a new one
+              assert(lastWordStart > lastWordEnd);
+              start = lastWordStart;
+              mode = _WordWrapParseMode.atBreak;
+            }
+            startForLengthCalculations = start - otherLineOffset;
+            assert(addPrefix);
+            lastWordEnd = null;
+          } else {
+            // save this break point, we're not yet over the line width
+            lastWordEnd = index;
+            // skip to the end of this break point
+            mode = _WordWrapParseMode.inSpace;
+          }
+          break;
+      }
+    }
+  }
 
   /// Write text ensuring the specified prefixes for the first and subsequent
   /// lines.
-  void write(String s) {
+  ///
+  /// If [allowWrap] is true, the text may be wrapped to stay within the
+  /// allow `wrapWidth`.
+  void write(String s, {bool allowWrap = false}) {
     if (s.isEmpty) return;
 
-    if (s == '\n') {
-      // Edge case to avoid adding trailing whitespace when the caller did
-      // not explicitly add trailing whitespace.
-      if (_buffer.isEmpty) {
-        _buffer.write(prefixLineOne.trimRight());
-      } else if (_lineIndex == 0) {
-        _buffer.write(prefixOtherLines.trimRight());
-        _hasMultipleLines = true;
+    final List<String> lines = s.split('\n');
+    for (int i = 0; i < lines.length; i += 1) {
+      if (i > 0) {
+        _finalizeLine(true);
+        _updatePrefix();
       }
-      _buffer.write('\n');
-      _lineIndex = 0;
-      return;
-    }
-
-    if (_buffer.isEmpty) {
-      _buffer.write(prefixLineOne);
-    } else if (_lineIndex == 0) {
-      _buffer.write(prefixOtherLines);
-      _hasMultipleLines = true;
-    }
-    bool lineTerminated = false;
-
-    if (s.endsWith('\n')) {
-      s = s.substring(0, s.length - 1);
-      lineTerminated = true;
-    }
-
-    final List<String> parts = s.split('\n');
-    _buffer.write(parts[0]);
-    for (int i = 1; i < parts.length; ++i) {
-      _buffer..write('\n')..write(prefixOtherLines)..write(parts[i]);
-    }
-
-    if (lineTerminated) _buffer.write('\n');
-
-    if (lineTerminated) {
-      _lineIndex = 0;
-    } else {
-      _lineIndex += parts.last.length;
+      final String line = lines[i];
+      if (line.isNotEmpty) {
+        if (allowWrap && wrapWidth != null) {
+          final int wrapStart = _currentLine.length;
+          final int wrapEnd = wrapStart + line.length;
+          if (_wrappableRanges.isNotEmpty &&
+              _wrappableRanges.last == wrapStart) {
+            // Extend last range.
+            _wrappableRanges.last = wrapEnd;
+          } else {
+            _wrappableRanges..add(wrapStart)..add(wrapEnd);
+          }
+        }
+        _currentLine.write(line);
+      }
     }
   }
 
-  /// Write text assuming the text already obeys the specified prefixes for the
-  /// first and subsequent lines.
-  void writeRaw(String text) {
-    if (text.isEmpty) return;
-    _buffer.write(text);
-    final int lastLineBreakIndex = text.lastIndexOf('\n');
-    if (lastLineBreakIndex != -1) {
-      _lineIndex = text.length - lastLineBreakIndex - 1;
-    } else {
-      _lineIndex += text.length;
+  void _updatePrefix() {
+    if (_nextPrefixOtherLines != null) {
+      _prefixOtherLines = _nextPrefixOtherLines;
+      _nextPrefixOtherLines = null;
     }
   }
 
-  /// Write a line assuming the line obeys the specified prefixes. Ensures that
+  void _writeLine(
+    String line, {
+    @required bool includeLineBreak,
+    @required bool firstLine,
+  }) {
+    line = '${_getCurrentPrefix(firstLine)}$line';
+    _buffer.write(line.trimRight());
+    if (includeLineBreak) _buffer.write('\n');
+    _numLines++;
+  }
+
+  String _getCurrentPrefix(bool firstLine) {
+    return _buffer.isEmpty
+        ? prefixLineOne
+        : (firstLine ? _prefixOtherLines : _prefixOtherLines);
+  }
+
+  /// Write lines assuming the lines obey the specified prefixes. Ensures that
   /// a newline is added if one is not present.
-  /// The same as [writeRaw] except a newline is added at the end of [line] if
-  /// one is not already present.
-  ///
-  /// A new line is not added if the input string already contains a newline.
-  void writeRawLine(String line) {
-    if (line.isEmpty) return;
-    _buffer.write(line);
-    if (!line.endsWith('\n')) _buffer.write('\n');
-    _lineIndex = 0;
+  void writeRawLines(String lines) {
+    if (lines.isEmpty) return;
+
+    if (_currentLine.isNotEmpty) {
+      _finalizeLine(true);
+    }
+    assert(_currentLine.isEmpty);
+
+    _buffer.write(lines);
+    if (!lines.endsWith('\n')) _buffer.write('\n');
+    _numLines++;
+    _updatePrefix();
   }
 
-  void writeStretched(String text, int lineLength) {
+  /// Finishes the current line with a stretched version of text.
+  void writeStretched(String text, int targetLineLength) {
     write(text);
-    final int targetLength = lineLength - _currentLineLength;
+    final int currentLineLength =
+        _currentLine.length + _getCurrentPrefix(_buffer.isEmpty).length;
+    assert(_currentLine.length > 0);
+    final int targetLength = targetLineLength - currentLineLength;
     if (targetLength > 0) {
       assert(text.isNotEmpty);
       final String lastChar = text[text.length - 1];
       assert(lastChar != '\n');
-      if (_currentLineLength < targetLength) {
-        writeRaw(lastChar * targetLength);
-      }
+      _currentLine.write(lastChar * targetLength);
     }
+    // Mark the entire line as not wrappable.
+    _wrappableRanges.clear();
   }
 
-  @override
-  String toString() => _buffer.toString();
+  String build() {
+    if (_currentLine.isNotEmpty) _finalizeLine(false);
+
+    return _buffer.toString();
+  }
 }
 
 class _NoDefaultValue {
@@ -915,68 +1060,82 @@
 /// Marker object indicating that a [DiagnosticsNode] has no default value.
 const _NoDefaultValue kNoDefaultValue = _NoDefaultValue();
 
-// XXX remove this terminal color logic.
-enum TerminalColor {
-  red,
-  green,
-  blue,
-  cyan,
-  yellow,
-  magenta,
-  grey,
+bool _isSingleLine(DiagnosticsTreeStyle style) {
+  return style == DiagnosticsTreeStyle.singleLine;
 }
 
-class TextRenderer {
-  static const String bold = '\u001B[1m';
-  static const String resetAll = '\u001B[0m';
-  static const String resetColor = '\u001B[39m';
-  static const String resetBold = '\u001B[22m';
-  static const String clear = '\u001B[2J\u001B[H';
+/// Renderer that creates ascii art representations of trees of
+/// [DiagnosticsNode] objects.
+///
+/// See also:
+///
+/// * [DiagnosticsNode.toStringDeep], which uses a [TextRender] to return a
+///    string representation of this node and its descendants.
+class TextTreeRenderer {
+  /// Creates a [TextTreeRenderer] object with the given arguments specifying
+  /// how the tree is rendered.
+  ///
+  /// Lines are wrapped to at the maximum of [wrapWidth] and the current indent
+  /// plus [wrapWidthProperties] characters. This ensures that wrapping does not
+  /// become too excessive when displaying very deep trees and that wrapping
+  /// only occurs at the overall [wrapWidth] when the tree is not very indented.
+  /// If [maxDescendentsTruncatableNode] is specified, [DiagnosticsNode] objects
+  /// with `allowTruncate` set to `true` are truncated after including
+  /// [maxDescendentsTruncatableNode] descendants of the node to be truncated.
+  TextTreeRenderer({
+    DiagnosticLevel minLevel = DiagnosticLevel.debug,
+    int wrapWidth = 100,
+    int wrapWidthProperties = 65,
+    int maxDescendentsTruncatableNode = -1,
+  })  : assert(minLevel != null),
+        _minLevel = minLevel,
+        _wrapWidth = wrapWidth,
+        _wrapWidthProperties = wrapWidthProperties,
+        _maxDescendentsTruncatableNode = maxDescendentsTruncatableNode;
 
-  static const String red = '\u001b[31m';
-  static const String green = '\u001b[32m';
-  static const String blue = '\u001b[34m';
-  static const String cyan = '\u001b[36m';
-  static const String magenta = '\u001b[35m';
-  static const String yellow = '\u001b[33m';
-  static const String grey = '\u001b[1;30m';
+  final int _wrapWidth;
+  final int _wrapWidthProperties;
+  final DiagnosticLevel _minLevel;
+  final int _maxDescendentsTruncatableNode;
 
-  static const Map<TerminalColor, String> _colorMap = <TerminalColor, String>{
-    TerminalColor.red: red,
-    TerminalColor.green: green,
-    TerminalColor.blue: blue,
-    TerminalColor.cyan: cyan,
-    TerminalColor.magenta: magenta,
-    TerminalColor.yellow: yellow,
-    TerminalColor.grey: grey,
-  };
+  /// Text configuration to use to connect this node to a `child`.
+  ///
+  /// The singleLine styles are special cased because the connection from the
+  /// parent to the child should be consistent with the parent's style as the
+  /// single line style does not provide any meaningful style for how children
+  /// should be connected to their parents.
+  TextTreeConfiguration _childTextConfiguration(
+    DiagnosticsNode child,
+    TextTreeConfiguration textStyle,
+  ) {
+    final DiagnosticsTreeStyle childStyle = child?.style;
+    return (_isSingleLine(childStyle) ||
+            childStyle == DiagnosticsTreeStyle.errorProperty)
+        ? textStyle
+        : child.textTreeConfiguration;
+  }
 
-  static String colorCode(TerminalColor color) => _colorMap[color];
-
-  String currentColor;
-  bool showColor;
-
-  static String renderToString(
+  /// Renders a [node] to a String.
+  String render(
     DiagnosticsNode node, {
     String prefixLineOne = '',
     String prefixOtherLines,
     TextTreeConfiguration parentConfiguration,
-    DiagnosticLevel minLevel = DiagnosticLevel.debug,
-    @required int wrapWidth,
-    @required int wrapWidthProperties,
   }) {
-    assert(minLevel != null);
+    if (kReleaseMode) {
+      return '';
+    }
+    final bool isSingleLine = _isSingleLine(node.style) &&
+        parentConfiguration?.lineBreakProperties != true;
     prefixOtherLines ??= prefixLineOne;
     if (node.linePrefix != null) {
-      // TODO(jacobr): should it apply to line 1 or not??
       prefixLineOne += node.linePrefix;
       prefixOtherLines += node.linePrefix;
     }
 
     final TextTreeConfiguration config = node.textTreeConfiguration;
-    if (prefixOtherLines.isEmpty) {
+    if (prefixOtherLines.isEmpty)
       prefixOtherLines += config.prefixOtherLinesRootNode;
-    }
 
     if (node.style == DiagnosticsTreeStyle.truncateChildren) {
       // This style is different enough that it isn't worthwhile to reuse the
@@ -1015,49 +1174,103 @@
       return information.toString();
     }
     final _PrefixedStringBuilder builder = _PrefixedStringBuilder(
-      prefixLineOne,
-      prefixOtherLines,
+      prefixLineOne: prefixLineOne,
+      prefixOtherLines: prefixOtherLines,
+      wrapWidth:
+          math.max(_wrapWidth, prefixOtherLines.length + _wrapWidthProperties),
     );
 
-    final List<DiagnosticsNode> children = node.getChildren();
+    List<DiagnosticsNode> children = node.getChildren();
 
-    final String description =
+    String description =
         node.toDescription(parentConfiguration: parentConfiguration);
     if (config.beforeName.isNotEmpty) {
       builder.write(config.beforeName);
     }
+    final bool wrapName = !isSingleLine && node.allowNameWrap;
+    final bool wrapDescription = !isSingleLine && node.allowWrap;
+    final bool uppercaseTitle = node.style == DiagnosticsTreeStyle.error;
+    String name = node.name;
+    if (uppercaseTitle) {
+      name = name?.toUpperCase();
+    }
     if (description == null || description.isEmpty) {
-      if (node.showName && node.name != null) builder.write(node.name);
+      if (node.showName && name != null)
+        builder.write(name, allowWrap: wrapName);
     } else {
-      if (node.name != null && node.name.isNotEmpty && node.showName) {
-        builder.write(node.name);
-        if (node.showSeparator) builder.write(config.afterName);
+      bool includeName = false;
+      if (name != null && name.isNotEmpty && node.showName) {
+        includeName = true;
+        builder.write(name, allowWrap: wrapName);
+        if (node.showSeparator)
+          builder.write(config.afterName, allowWrap: wrapName);
 
         builder.write(
-            config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' ');
-        if (description.contains('\n') &&
-            node.style == DiagnosticsTreeStyle.singleLine) {
-          builder.prefixOtherLines += '  ';
-        }
+          config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' ',
+          allowWrap: wrapName,
+        );
       }
-      builder.prefixOtherLines += children.isEmpty
-          ? config.propertyPrefixNoChildren
-          : config.propertyPrefixIfChildren;
-      builder.write(description);
+      if (!isSingleLine &&
+          builder.requiresMultipleLines &&
+          !builder.isCurrentLineEmpty) {
+        // Make sure there is a break between the current line and next one if
+        // there is not one already.
+        builder.write('\n');
+      }
+      if (includeName) {
+        builder.incrementPrefixOtherLines(
+          children.isEmpty
+              ? config.propertyPrefixNoChildren
+              : config.propertyPrefixIfChildren,
+          updateCurrentLine: true,
+        );
+      }
+
+      if (uppercaseTitle) {
+        description = description.toUpperCase();
+      }
+      builder.write(description.trimRight(), allowWrap: wrapDescription);
+
+      if (!includeName) {
+        builder.incrementPrefixOtherLines(
+          children.isEmpty
+              ? config.propertyPrefixNoChildren
+              : config.propertyPrefixIfChildren,
+          updateCurrentLine: false,
+        );
+      }
     }
     if (config.suffixLineOne.isNotEmpty) {
-      builder.writeStretched(config.suffixLineOne, wrapWidth);
+      builder.writeStretched(config.suffixLineOne, builder.wrapWidth);
     }
 
-    final List<DiagnosticsNode> properties = node
+    final Iterable<DiagnosticsNode> propertiesIterable = node
         .getProperties()
-        .where((DiagnosticsNode n) => !n.isFiltered(minLevel))
-        .toList();
+        .where((DiagnosticsNode n) => !n.isFiltered(_minLevel));
+    List<DiagnosticsNode> properties;
+    if (_maxDescendentsTruncatableNode >= 0 && node.allowTruncate) {
+      if (propertiesIterable.length < _maxDescendentsTruncatableNode) {
+        properties =
+            propertiesIterable.take(_maxDescendentsTruncatableNode).toList();
+        properties.add(DiagnosticsNode.message('...'));
+      } else {
+        properties = propertiesIterable.toList();
+      }
+      if (_maxDescendentsTruncatableNode < children.length) {
+        children = children.take(_maxDescendentsTruncatableNode).toList();
+        children.add(DiagnosticsNode.message('...'));
+      }
+    } else {
+      properties = propertiesIterable.toList();
+    }
 
-    builder.write(config.afterDescription);
-    if (properties.isNotEmpty ||
-        children.isNotEmpty ||
-        node.emptyBodyDescription != null) {
+    // If the node does not show a separator and there is no description then
+    // we should not place a separator between the name and the value.
+    // Essentially in this case the properties are treated a bit like a value.
+    if ((properties.isNotEmpty ||
+            children.isNotEmpty ||
+            node.emptyBodyDescription != null) &&
+        (node.showSeparator || description?.isNotEmpty == true)) {
       builder.write(config.afterDescriptionIfBody);
     }
 
@@ -1065,7 +1278,8 @@
 
     if (properties.isNotEmpty) builder.write(config.beforeProperties);
 
-    builder.prefixOtherLines += config.bodyIndent;
+    builder.incrementPrefixOtherLines(config.bodyIndent,
+        updateCurrentLine: false);
 
     if (node.emptyBodyDescription != null &&
         properties.isEmpty &&
@@ -1079,54 +1293,51 @@
       final DiagnosticsNode property = properties[i];
       if (i > 0) builder.write(config.propertySeparator);
 
-      if (property.style != DiagnosticsTreeStyle.singleLine) {
-        final TextTreeConfiguration propertyStyle =
-            property.textTreeConfiguration;
-        builder.writeRaw(renderToString(
+      final TextTreeConfiguration propertyStyle =
+          property.textTreeConfiguration;
+      if (_isSingleLine(property.style)) {
+        // We have to treat single line properties slightly differently to deal
+        // with cases where a single line properties output may not have single
+        // linebreak.
+        final String propertyRender = render(
+          property,
+          prefixLineOne: '${propertyStyle.prefixLineOne}',
+          prefixOtherLines:
+              '${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}',
+          parentConfiguration: config,
+        );
+        final List<String> propertyLines = propertyRender.split('\n');
+        if (propertyLines.length == 1 && !config.lineBreakProperties) {
+          builder.write(propertyLines.first);
+        } else {
+          builder.write(propertyRender, allowWrap: false);
+          if (!propertyRender.endsWith('\n')) builder.write('\n');
+        }
+      } else {
+        final String propertyRender = render(
           property,
           prefixLineOne:
               '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}',
           prefixOtherLines:
-              '${builder.prefixOtherLines}${propertyStyle.linkCharacter}${propertyStyle.prefixOtherLines}',
+              '${builder.prefixOtherLines}${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}',
           parentConfiguration: config,
-          minLevel: minLevel,
-          wrapWidth: wrapWidth,
-          wrapWidthProperties: wrapWidthProperties,
-        ));
-        continue;
+        );
+        builder.writeRawLines(propertyRender);
       }
-      assert(property.style == DiagnosticsTreeStyle.singleLine);
-      final String message =
-          property.toString(parentConfiguration: config, minLevel: minLevel);
-      if (!config.lineBreakProperties ||
-          message.length < wrapWidth ||
-          !property.allowWrap) {
-        builder.write(message);
-      } else {
-        // debugWordWrap doesn't handle line breaks within the text being
-        // wrapped so we must call it on each line.
-        final List<String> lines = message.split('\n');
-        for (int j = 0; j < lines.length; ++j) {
-          final String line = lines[j];
-          if (j > 0) builder.write(config.lineBreak);
-          builder.write(
-              debugWordWrap(line, wrapWidthProperties, wrapIndent: '  ')
-                  .join('\n'));
-        }
-      }
-      if (config.lineBreakProperties) builder.write(config.lineBreak);
     }
     if (properties.isNotEmpty) builder.write(config.afterProperties);
 
+    builder.write(config.mandatoryAfterProperties);
+
     if (!config.lineBreakProperties) builder.write(config.lineBreak);
 
-    final String prefixChildren = '$prefixOtherLines${config.bodyIndent}';
-
+    final String prefixChildren = '${config.bodyIndent}';
+    final String prefixChildrenRaw = '$prefixOtherLines$prefixChildren';
     if (children.isEmpty &&
         config.addBlankLineIfNoChildren &&
-        builder.hasMultipleLines) {
-      final String prefix = prefixChildren.trimRight();
-      if (prefix.isNotEmpty) builder.writeRaw('$prefix${config.lineBreak}');
+        builder.requiresMultipleLines &&
+        builder.prefixOtherLines.trimRight().isNotEmpty) {
+      builder.write(config.lineBreak);
     }
 
     if (children.isNotEmpty && config.showChildren) {
@@ -1137,53 +1348,58 @@
         builder.write(config.lineBreak);
       }
 
+      builder.prefixOtherLines = prefixOtherLines;
+
       for (int i = 0; i < children.length; i++) {
         final DiagnosticsNode child = children[i];
         assert(child != null);
         final TextTreeConfiguration childConfig =
-            node._childTextConfiguration(child, config);
+            _childTextConfiguration(child, config);
         if (i == children.length - 1) {
           final String lastChildPrefixLineOne =
-              '$prefixChildren${childConfig.prefixLastChildLineOne}';
-          builder.writeRawLine(renderToString(
+              '$prefixChildrenRaw${childConfig.prefixLastChildLineOne}';
+          final String childPrefixOtherLines =
+              '$prefixChildrenRaw${childConfig.childLinkSpace}${childConfig.prefixOtherLines}';
+          builder.writeRawLines(render(
             child,
             prefixLineOne: lastChildPrefixLineOne,
-            prefixOtherLines:
-                '$prefixChildren${childConfig.childLinkSpace}${childConfig.prefixOtherLines}',
+            prefixOtherLines: childPrefixOtherLines,
             parentConfiguration: config,
-            minLevel: minLevel,
-            wrapWidth: wrapWidth,
-            wrapWidthProperties: wrapWidthProperties,
           ));
           if (childConfig.footer.isNotEmpty) {
-            builder.writeRaw(
-                '$prefixChildren${childConfig.childLinkSpace}${childConfig.footer}');
+            builder.prefixOtherLines = prefixChildrenRaw;
+            builder.write('${childConfig.childLinkSpace}${childConfig.footer}');
             if (childConfig.manditoryFooter.isNotEmpty) {
-              builder.writeStretched(config.manditoryFooter, wrapWidth);
+              builder.writeStretched(
+                childConfig.manditoryFooter,
+                math.max(builder.wrapWidth,
+                    _wrapWidthProperties + childPrefixOtherLines.length),
+              );
             }
             builder.write(config.lineBreak);
           }
         } else {
           final TextTreeConfiguration nextChildStyle =
-              node._childTextConfiguration(children[i + 1], config);
+              _childTextConfiguration(children[i + 1], config);
           final String childPrefixLineOne =
-              '$prefixChildren${childConfig.prefixLineOne}';
+              '$prefixChildrenRaw${childConfig.prefixLineOne}';
           final String childPrefixOtherLines =
-              '$prefixChildren${nextChildStyle.linkCharacter}${childConfig.prefixOtherLines}';
-          builder.writeRawLine(renderToString(
+              '$prefixChildrenRaw${nextChildStyle.linkCharacter}${childConfig.prefixOtherLines}';
+          builder.writeRawLines(render(
             child,
             prefixLineOne: childPrefixLineOne,
             prefixOtherLines: childPrefixOtherLines,
             parentConfiguration: config,
-            minLevel: minLevel,
-            wrapWidth: wrapWidth,
-            wrapWidthProperties: wrapWidthProperties,
           ));
           if (childConfig.footer.isNotEmpty) {
-            builder.writeRaw(
-                '$prefixChildren${nextChildStyle.linkCharacter}${childConfig.footer}');
+            builder.prefixOtherLines = prefixChildrenRaw;
+            builder.write('${childConfig.linkCharacter}${childConfig.footer}');
             if (childConfig.manditoryFooter.isNotEmpty) {
-              builder.writeStretched(config.manditoryFooter, wrapWidth);
+              builder.writeStretched(
+                childConfig.manditoryFooter,
+                math.max(builder.wrapWidth,
+                    _wrapWidthProperties + childPrefixOtherLines.length),
+              );
             }
             builder.write(config.lineBreak);
           }
@@ -1191,19 +1407,23 @@
       }
     }
     if (parentConfiguration == null && config.manditoryFooter.isNotEmpty) {
-      builder.writeStretched(config.manditoryFooter, wrapWidth);
+      builder.writeStretched(config.manditoryFooter, builder.wrapWidth);
+      builder.write(config.lineBreak);
     }
-    return builder.toString();
+    return builder.build();
   }
 }
 
 /// Defines diagnostics data for a [value].
 ///
-/// [DiagnosticsNode] provides a high quality multi-line string dump via
-/// [toStringDeep]. The core members are the [name], [toDescription],
-/// [getProperties], [value], and [getChildren]. All other members exist
-/// typically to provide hints for how [toStringDeep] and debugging tools should
-/// format output.
+/// For debug and profile modes, [DiagnosticsNode] provides a high quality
+/// multi-line string dump via [toStringDeep]. The core members are the [name],
+/// [toDescription], [getProperties], [value], and [getChildren]. All other
+/// members exist typically to provide hints for how [toStringDeep] and
+/// debugging tools should format output.
+///
+/// In release mode, far less information is retained and some information may
+/// not print at all.
 abstract class DiagnosticsNode {
   /// Initializes the object.
   ///
@@ -1220,8 +1440,11 @@
         // A name ending with ':' indicates that the user forgot that the ':' will
         // be automatically added for them when generating descriptions of the
         // property.
-        assert(name == null || !name.endsWith(':'),
-            'Names of diagnostic nodes must not end with colons.');
+        assert(
+            name == null || !name.endsWith(':'),
+            'Names of diagnostic nodes must not end with colons.\n'
+            'name:\n'
+            '  "$name"');
 
   /// Diagnostics containing just a string `message` and not a concrete name or
   /// value.
@@ -1275,8 +1498,9 @@
   /// than `minLevel`.
   ///
   /// If `minLevel` is [DiagnosticLevel.hidden] no diagnostics will be filtered.
-  /// If `minLevel` is [DiagnosticsLevel.off] all diagnostics will be filtered.
-  bool isFiltered(DiagnosticLevel minLevel) => level.index < minLevel.index;
+  /// If `minLevel` is [DiagnosticLevel.off] all diagnostics will be filtered.
+  bool isFiltered(DiagnosticLevel minLevel) =>
+      kReleaseMode || level.index < minLevel.index;
 
   /// Priority level of the diagnostic used to control which diagnostics should
   /// be shown and filtered.
@@ -1287,7 +1511,8 @@
   /// the value returned here but other factors also influence it. For example,
   /// whether an exception is thrown computing a property value
   /// [DiagnosticLevel.error] is returned.
-  DiagnosticLevel get level => DiagnosticLevel.info;
+  DiagnosticLevel get level =>
+      kReleaseMode ? DiagnosticLevel.hidden : DiagnosticLevel.info;
 
   /// Whether the name of the property should be shown when showing the default
   /// view of the tree.
@@ -1296,6 +1521,7 @@
   /// will make the name self-evident.
   final bool showName;
 
+  /// Prefix to include at the start of each line
   final String linePrefix;
 
   /// Description to show if the node has no displayed properties or children.
@@ -1308,7 +1534,13 @@
   final DiagnosticsTreeStyle style;
 
   /// Whether to wrap text on onto multiple lines or not.
-  bool get allowWrap => true;
+  bool get allowWrap => false;
+
+  /// Whether to wrap the name onto multiple lines or not.
+  bool get allowNameWrap => false;
+
+  /// Whether to allow truncation when displaying the node and its children.
+  bool get allowTruncate => false;
 
   /// Properties of this [DiagnosticsNode].
   ///
@@ -1325,7 +1557,8 @@
 
   String get _separator => showSeparator ? ':' : '';
 
-  /// Serialize the node excluding its descendants to a JSON map.
+  /// Serialize the node to a JSON map according to the configuration provided
+  /// in the [DiagnosticsSerializationDelegate].
   ///
   /// Subclasses should override if they have additional properties that are
   /// useful for the GUI tools that consume this JSON.
@@ -1336,28 +1569,66 @@
   ///    by this method and interactive tree views in the Flutter IntelliJ
   ///    plugin.
   @mustCallSuper
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> data = <String, Object>{
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    if (kReleaseMode) {
+      return <String, Object>{};
+    }
+    final bool hasChildren = getChildren().isNotEmpty;
+    return <String, Object>{
       'description': toDescription(),
       'type': runtimeType.toString(),
+      if (name != null) 'name': name,
+      if (!showSeparator) 'showSeparator': showSeparator,
+      if (level != DiagnosticLevel.info) 'level': describeEnum(level),
+      if (showName == false) 'showName': showName,
+      if (emptyBodyDescription != null)
+        'emptyBodyDescription': emptyBodyDescription,
+      if (style != DiagnosticsTreeStyle.sparse) 'style': describeEnum(style),
+      if (allowTruncate) 'allowTruncate': allowTruncate,
+      if (hasChildren) 'hasChildren': hasChildren,
+      if (linePrefix?.isNotEmpty == true) 'linePrefix': linePrefix,
+      if (!allowWrap) 'allowWrap': allowWrap,
+      if (allowNameWrap) 'allowNameWrap': allowNameWrap,
+      ...delegate.additionalNodeProperties(this),
+      if (delegate.includeProperties)
+        'properties': toJsonList(
+          delegate.filterProperties(getProperties(), this),
+          this,
+          delegate,
+        ),
+      if (delegate.subtreeDepth > 0)
+        'children': toJsonList(
+          delegate.filterChildren(getChildren(), this),
+          this,
+          delegate,
+        ),
     };
-    if (name != null) data['name'] = name;
+  }
 
-    if (!showSeparator) data['showSeparator'] = showSeparator;
-    if (level != DiagnosticLevel.info) data['level'] = describeEnum(level);
-    if (showName == false) data['showName'] = showName;
-    if (emptyBodyDescription != null) {
-      data['emptyBodyDescription'] = emptyBodyDescription;
+  /// Serializes a [List] of [DiagnosticsNode]s to a JSON list according to
+  /// the configuration provided by the [DiagnosticsSerializationDelegate].
+  ///
+  /// The provided `nodes` may be properties or children of the `parent`
+  /// [DiagnosticsNode].
+  static List<Map<String, Object>> toJsonList(
+    List<DiagnosticsNode> nodes,
+    DiagnosticsNode parent,
+    DiagnosticsSerializationDelegate delegate,
+  ) {
+    bool truncated = false;
+    if (nodes == null) return const <Map<String, Object>>[];
+    final int originalNodeCount = nodes.length;
+    nodes = delegate.truncateNodesList(nodes, parent);
+    if (nodes.length != originalNodeCount) {
+      nodes.add(DiagnosticsNode.message('...'));
+      truncated = true;
     }
-    if (style != DiagnosticsTreeStyle.sparse) {
-      data['style'] = describeEnum(style);
-    }
-
-    final bool hasChildren = getChildren().isNotEmpty;
-    if (hasChildren) data['hasChildren'] = hasChildren;
-
-    if (linePrefix?.isNotEmpty == true) data['linePrefix'] = linePrefix;
-    return data;
+    final List<Map<String, Object>> json =
+        nodes.map<Map<String, Object>>((DiagnosticsNode node) {
+      return node.toJsonMap(delegate.delegateForNode(node));
+    }).toList();
+    if (truncated) json.last['truncated'] = true;
+    return json;
   }
 
   /// Returns a string representation of this diagnostic that is compatible with
@@ -1374,12 +1645,14 @@
     TextTreeConfiguration parentConfiguration,
     DiagnosticLevel minLevel = DiagnosticLevel.info,
   }) {
+    if (kReleaseMode) {
+      return super.toString();
+    }
     assert(style != null);
     assert(minLevel != null);
-    if (style == DiagnosticsTreeStyle.singleLine) {
+    if (_isSingleLine(style))
       return toStringDeep(
           parentConfiguration: parentConfiguration, minLevel: minLevel);
-    }
 
     final String description =
         toDescription(parentConfiguration: parentConfiguration);
@@ -1397,6 +1670,8 @@
   TextTreeConfiguration get textTreeConfiguration {
     assert(style != null);
     switch (style) {
+      case DiagnosticsTreeStyle.none:
+        return null;
       case DiagnosticsTreeStyle.dense:
         return denseTextConfiguration;
       case DiagnosticsTreeStyle.sparse:
@@ -1409,37 +1684,22 @@
         return transitionTextConfiguration;
       case DiagnosticsTreeStyle.singleLine:
         return singleLineTextConfiguration;
-      case DiagnosticsTreeStyle.headerLine:
-        return headerLineTextConfiguration;
-      case DiagnosticsTreeStyle.indentedSingleLine:
-        return singleLineTextConfigurationIndented;
+      case DiagnosticsTreeStyle.errorProperty:
+        return errorPropertyTextConfiguration;
       case DiagnosticsTreeStyle.shallow:
         return shallowTextConfiguration;
       case DiagnosticsTreeStyle.error:
         return errorTextConfiguration;
       case DiagnosticsTreeStyle.truncateChildren:
-        return whitespaceTextConfiguration; // XXX inacurate. fix.
+        // Truncate children doesn't really need its own text style as the
+        // rendering is quite custom.
+        return whitespaceTextConfiguration;
       case DiagnosticsTreeStyle.flat:
         return flatTextConfiguration;
     }
     return null;
   }
 
-  /// Text configuration to use to connect this node to a `child`.
-  ///
-  /// The singleLine style is special cased because the connection from the
-  /// parent to the child should be consistent with the parent's style as the
-  /// single line style does not provide any meaningful style for how children
-  /// should be connected to their parents.
-  TextTreeConfiguration _childTextConfiguration(
-    DiagnosticsNode child,
-    TextTreeConfiguration textStyle,
-  ) {
-    return (child != null && child.style != DiagnosticsTreeStyle.singleLine)
-        ? child.textTreeConfiguration
-        : textStyle;
-  }
-
   /// Returns a string representation of this node and its descendants.
   ///
   /// `prefixLineOne` will be added to the front of the first line of the
@@ -1455,8 +1715,7 @@
   ///
   /// See also:
   ///
-  ///  * [toString], for a brief description of the [value] but not its children.
-  ///  * [toStringShallow], for a detailed description of the [value] but not its
+  ///  * [toString], for a brief description of the [value] but not its
   ///    children.
   String toStringDeep({
     String prefixLineOne = '',
@@ -1464,14 +1723,18 @@
     TextTreeConfiguration parentConfiguration,
     DiagnosticLevel minLevel = DiagnosticLevel.debug,
   }) {
-    return TextRenderer.renderToString(
+    if (kReleaseMode) {
+      return '';
+    }
+    return TextTreeRenderer(
+      minLevel: minLevel,
+      wrapWidth: 65,
+      wrapWidthProperties: 65,
+    ).render(
       this,
       prefixLineOne: prefixLineOne,
       prefixOtherLines: prefixOtherLines,
       parentConfiguration: parentConfiguration,
-      minLevel: minLevel,
-      wrapWidth: 100,
-      wrapWidthProperties: 65,
     );
   }
 }
@@ -1515,41 +1778,13 @@
   MessageProperty(
     String name,
     String message, {
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(name != null),
         assert(message != null),
+        assert(style != null),
         assert(level != null),
-        super(name, null, description: message, level: level);
-}
-
-class UrlProperty extends DiagnosticsProperty<String> {
-  /// Create a diagnostics property for describing urls.
-  ///
-  /// The [showName], [quoted], and [level] arguments must not be null.
-  UrlProperty(
-    String name, {
-    @required String url,
-    String tooltip,
-    Object defaultValue = kNoDefaultValue,
-    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
-    DiagnosticLevel level = DiagnosticLevel.info,
-  })  : assert(showName != null),
-        assert(level != null),
-        super(
-          name,
-          url,
-          defaultValue: defaultValue,
-          tooltip: tooltip,
-          style: style,
-          level: level,
-        );
-
-  @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
-    json['isUrl'] = true;
-    return json;
-  }
+        super(name, null, description: message, style: style, level: level);
 }
 
 /// Property which encloses its string [value] in quotes.
@@ -1561,7 +1796,7 @@
 class StringProperty extends DiagnosticsProperty<String> {
   /// Create a diagnostics property for strings.
   ///
-  /// The [showName], [quoted], and [level] arguments must not be null.
+  /// The [showName], [quoted], [style], and [level] arguments must not be null.
   StringProperty(
     String name,
     String value, {
@@ -1571,9 +1806,11 @@
     Object defaultValue = kNoDefaultValue,
     this.quoted = true,
     String ifEmpty,
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(showName != null),
         assert(quoted != null),
+        assert(style != null),
         assert(level != null),
         super(
           name,
@@ -1583,6 +1820,7 @@
           tooltip: tooltip,
           showName: showName,
           ifEmpty: ifEmpty,
+          style: style,
           level: level,
         );
 
@@ -1590,8 +1828,8 @@
   final bool quoted;
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final Map<String, Object> json = super.toJsonMap(delegate);
     json['quoted'] = quoted;
     return json;
   }
@@ -1618,32 +1856,6 @@
   }
 }
 
-class LinkProperty extends DiagnosticsProperty<String> {
-  /// Create a diagnostics property for url links.
-  ///
-  /// The [showName], [quoted], and [level] arguments must not be null.
-  LinkProperty(
-    String name, {
-    String url,
-    bool showName = true,
-    DiagnosticLevel level = DiagnosticLevel.info,
-  })  : assert(showName != null),
-        assert(level != null),
-        super(
-          name,
-          url,
-          showName: showName,
-          level: level,
-        );
-
-  @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
-    json['url'] = true;
-    return json;
-  }
-}
-
 abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> {
   _NumProperty(
     String name,
@@ -1653,6 +1865,7 @@
     bool showName = true,
     Object defaultValue = kNoDefaultValue,
     String tooltip,
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   }) : super(
           name,
@@ -1662,6 +1875,7 @@
           defaultValue: defaultValue,
           tooltip: tooltip,
           level: level,
+          style: style,
         );
 
   _NumProperty.lazy(
@@ -1672,6 +1886,7 @@
     bool showName = true,
     Object defaultValue = kNoDefaultValue,
     String tooltip,
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   }) : super.lazy(
           name,
@@ -1680,12 +1895,13 @@
           showName: showName,
           defaultValue: defaultValue,
           tooltip: tooltip,
+          style: style,
           level: level,
         );
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final Map<String, Object> json = super.toJsonMap(delegate);
     if (unit != null) json['unit'] = unit;
 
     json['numberToString'] = numberToString();
@@ -1716,7 +1932,7 @@
 class DoubleProperty extends _NumProperty<double> {
   /// If specified, [unit] describes the unit for the [value] (e.g. px).
   ///
-  /// The [showName] and [level] arguments must not be null.
+  /// The [showName], [style], and [level] arguments must not be null.
   DoubleProperty(
     String name,
     double value, {
@@ -1725,8 +1941,10 @@
     String tooltip,
     Object defaultValue = kNoDefaultValue,
     bool showName = true,
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(showName != null),
+        assert(style != null),
         assert(level != null),
         super(
           name,
@@ -1736,6 +1954,7 @@
           tooltip: tooltip,
           defaultValue: defaultValue,
           showName: showName,
+          style: style,
           level: level,
         );
 
@@ -1768,7 +1987,7 @@
         );
 
   @override
-  String numberToString() => value?.toStringAsFixed(1);
+  String numberToString() => debugFormatDouble(value);
 }
 
 /// An int valued property with an optional unit the value is measured in.
@@ -1777,7 +1996,7 @@
 class IntProperty extends _NumProperty<int> {
   /// Create a diagnostics property for integers.
   ///
-  /// The [showName] and [level] arguments must not be null.
+  /// The [showName], [style], and [level] arguments must not be null.
   IntProperty(
     String name,
     int value, {
@@ -1785,9 +2004,11 @@
     bool showName = true,
     String unit,
     Object defaultValue = kNoDefaultValue,
+    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(showName != null),
         assert(level != null),
+        assert(style != null),
         super(
           name,
           value,
@@ -1912,8 +2133,8 @@
         );
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final Map<String, Object> json = super.toJsonMap(delegate);
     if (ifTrue != null) json['ifTrue'] = ifTrue;
     if (ifFalse != null) json['ifFalse'] = ifFalse;
 
@@ -2023,7 +2244,7 @@
       return '[${value.join(', ')}]';
     }
 
-    return value.join(style == DiagnosticsTreeStyle.singleLine ? ', ' : '\n');
+    return value.join(_isSingleLine(style) ? ', ' : '\n');
   }
 
   /// Priority level of the diagnostic used to control which diagnostics should
@@ -2043,8 +2264,8 @@
   }
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final Map<String, Object> json = super.toJsonMap(delegate);
     if (value != null) {
       json['values'] =
           value.map<String>((T value) => value.toString()).toList();
@@ -2093,7 +2314,7 @@
 /// The [ifPresent] and [ifNull] strings describe the property [value] when it
 /// is non-null and null respectively. If one of [ifPresent] or [ifNull] is
 /// omitted, that is taken to mean that [level] should be
-/// [DiagnosticsLevel.hidden] when [value] is non-null or null respectively.
+/// [DiagnosticLevel.hidden] when [value] is non-null or null respectively.
 ///
 /// This kind of diagnostics property is typically used for values mostly opaque
 /// values, like closures, where presenting the actual object is of dubious
@@ -2152,7 +2373,7 @@
   /// Description to use if the property [value] is not null.
   ///
   /// If the property [value] is not null and [ifPresent] is null, the
-  /// [level] for the property is [DiagnosticsLevel.hidden] and the description
+  /// [level] for the property is [DiagnosticLevel.hidden] and the description
   /// from superclass is used.
   final String ifPresent;
 
@@ -2191,8 +2412,8 @@
   }
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final Map<String, Object> json = super.toJsonMap(delegate);
     if (ifPresent != null) json['ifPresent'] = ifPresent;
     return json;
   }
@@ -2230,13 +2451,13 @@
     this.ifEmpty,
     bool showName = true,
     bool showSeparator = true,
-    bool showSeperatorAfter = false,
     this.defaultValue = kNoDefaultValue,
     this.tooltip,
     this.missingIfNull = false,
     String linePrefix,
     this.expandableValue = false,
     this.allowWrap = true,
+    this.allowNameWrap = true,
     DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(showName != null),
@@ -2282,6 +2503,7 @@
     this.missingIfNull = false,
     this.expandableValue = false,
     this.allowWrap = true,
+    this.allowNameWrap = true,
     DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
     DiagnosticLevel level = DiagnosticLevel.info,
   })  : assert(showName != null),
@@ -2305,17 +2527,38 @@
 
   final String _description;
 
+  /// Whether to expose properties and children of the value as properties and
+  /// children.
   final bool expandableValue;
 
   @override
   final bool allowWrap;
 
   @override
-  Map<String, Object> toJsonMap() {
-    final Map<String, Object> json = super.toJsonMap();
-    if (defaultValue != kNoDefaultValue) {
-      json['defaultValue'] = defaultValue.toString();
+  final bool allowNameWrap;
+
+  @override
+  Map<String, Object> toJsonMap(DiagnosticsSerializationDelegate delegate) {
+    final T v = value;
+    List<Map<String, Object>> properties;
+    if (delegate.expandPropertyValues &&
+        delegate.includeProperties &&
+        v is Diagnosticable &&
+        getProperties().isEmpty) {
+      // Exclude children for expanded nodes to avoid cycles.
+      delegate = delegate.copyWith(subtreeDepth: 0, includeProperties: false);
+      properties = DiagnosticsNode.toJsonList(
+        delegate.filterProperties(v.toDiagnosticsNode().getProperties(), this),
+        this,
+        delegate,
+      );
     }
+    final Map<String, Object> json = super.toJsonMap(delegate);
+    if (properties != null) {
+      json['properties'] = properties;
+    }
+    if (defaultValue != kNoDefaultValue)
+      json['defaultValue'] = defaultValue.toString();
     if (ifEmpty != null) json['ifEmpty'] = ifEmpty;
     if (ifNull != null) json['ifNull'] = ifNull;
     if (tooltip != null) json['tooltip'] = tooltip;
@@ -2323,10 +2566,10 @@
     if (exception != null) json['exception'] = exception.toString();
     json['propertyType'] = propertyType.toString();
     json['defaultLevel'] = describeEnum(_defaultLevel);
-    if (T is Diagnosticable || T is DiagnosticsNode) {
+    if (value is Diagnosticable || value is DiagnosticsNode)
       json['isDiagnosticableValue'] = true;
-    }
-    if (!allowWrap) json['allowWrap'] = allowWrap;
+    if (value is num || value is String || value is bool || value == null)
+      json['value'] = value;
     return json;
   }
 
@@ -2455,7 +2698,7 @@
   /// [defaultValue] has type [T] or is [kNoDefaultValue].
   final Object defaultValue;
 
-  DiagnosticLevel _defaultLevel;
+  final DiagnosticLevel _defaultLevel;
 
   /// Priority level of the diagnostic used to control which diagnostics should
   /// be shown and filtered.
@@ -2476,9 +2719,8 @@
     if (value == null && missingIfNull) return DiagnosticLevel.warning;
 
     // Use a low level when the value matches the default value.
-    if (defaultValue != kNoDefaultValue && value == defaultValue) {
+    if (defaultValue != kNoDefaultValue && value == defaultValue)
       return DiagnosticLevel.fine;
-    }
 
     return _defaultLevel;
   }
@@ -2535,7 +2777,11 @@
 
   DiagnosticPropertiesBuilder _cachedBuilder;
 
-  DiagnosticPropertiesBuilder get _builder {
+  /// Retrieve the [DiagnosticPropertiesBuilder] of current node.
+  ///
+  /// It will cache the result to prevent duplicate operation.
+  DiagnosticPropertiesBuilder get builder {
+    if (kReleaseMode) return null;
     if (_cachedBuilder == null) {
       _cachedBuilder = DiagnosticPropertiesBuilder();
       value?.debugFillProperties(_cachedBuilder);
@@ -2545,14 +2791,18 @@
 
   @override
   DiagnosticsTreeStyle get style {
-    return super.style ?? _builder.defaultDiagnosticsTreeStyle;
+    return kReleaseMode
+        ? DiagnosticsTreeStyle.none
+        : super.style ?? builder.defaultDiagnosticsTreeStyle;
   }
 
   @override
-  String get emptyBodyDescription => _builder.emptyBodyDescription;
+  String get emptyBodyDescription =>
+      kReleaseMode ? '' : builder.emptyBodyDescription;
 
   @override
-  List<DiagnosticsNode> getProperties() => _builder.properties;
+  List<DiagnosticsNode> getProperties() =>
+      kReleaseMode ? const <DiagnosticsNode>[] : builder.properties;
 
   @override
   List<DiagnosticsNode> getChildren() {
@@ -2561,16 +2811,17 @@
 
   @override
   String toDescription({TextTreeConfiguration parentConfiguration}) {
+    if (kReleaseMode) {
+      return '';
+    }
     return value.toStringShort();
   }
-
-  @override
-  DiagnosticLevel get level => value.debugDiagnosticLevel;
 }
 
 /// [DiagnosticsNode] for an instance of [DiagnosticableTree].
-class _DiagnosticableTreeNode extends DiagnosticableNode<DiagnosticableTree> {
-  _DiagnosticableTreeNode({
+class DiagnosticableTreeNode extends DiagnosticableNode<DiagnosticableTree> {
+  /// Creates a [DiagnosticableTreeNode].
+  DiagnosticableTreeNode({
     String name,
     @required DiagnosticableTree value,
     @required DiagnosticsTreeStyle style,
@@ -2633,13 +2884,22 @@
 /// Builder to accumulate properties and configuration used to assemble a
 /// [DiagnosticsNode] from a [Diagnosticable] object.
 class DiagnosticPropertiesBuilder {
+  /// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to
+  /// an empty array.
+  DiagnosticPropertiesBuilder() : properties = <DiagnosticsNode>[];
+
+  /// Creates a [DiagnosticPropertiesBuilder] with a given [properties].
+  DiagnosticPropertiesBuilder.fromProperties(this.properties);
+
   /// Add a property to the list of properties.
   void add(DiagnosticsNode property) {
-    properties.add(property);
+    if (!kReleaseMode) {
+      properties.add(property);
+    }
   }
 
   /// List of properties accumulated so far.
-  final List<DiagnosticsNode> properties = <DiagnosticsNode>[];
+  final List<DiagnosticsNode> properties;
 
   /// Default style to use for the [DiagnosticsNode] if no style is specified.
   DiagnosticsTreeStyle defaultDiagnosticsTreeStyle =
@@ -2664,23 +2924,54 @@
 ///
 ///  * [DiagnosticableTree], which extends this class to also describe the
 ///    children of a tree structured object.
-///  * [Diagnosticable.debugFillProperties], which lists best practices
-///    for specifying the properties of a [DiagnosticNode]. The most common use
+///  * [DiagnosticableMixin], which provides the implementation for
+///    [Diagnosticable], and can be used to add diagnostics to classes which
+///    already have a base class.
+///  * [DiagnosticableMixin.debugFillProperties], which lists best practices
+///    for specifying the properties of a [DiagnosticsNode]. The most common use
 ///    case is to override [debugFillProperties] defining custom properties for
-///    a subclass of [TreeDiagnosticsMixin] using the existing
+///    a subclass of [DiagnosticableTreeMixin] using the existing
 ///    [DiagnosticsProperty] subclasses.
 ///  * [DiagnosticableTree.debugDescribeChildren], which lists best practices
-///    for describing the children of a [DiagnosticNode]. Typically the base
+///    for describing the children of a [DiagnosticsNode]. Typically the base
 ///    class already describes the children of a node properly or a node has
 ///    no children.
 ///  * [DiagnosticsProperty], which should be used to create leaf diagnostic
-///    nodes without properties or children. There are many [DiagnosticProperty]
-///    subclasses to handle common use cases.
-abstract class Diagnosticable {
+///    nodes without properties or children. There are many
+///    [DiagnosticsProperty] subclasses to handle common use cases.
+abstract class Diagnosticable with DiagnosticableMixin {
   /// Abstract const constructor. This constructor enables subclasses to provide
   /// const constructors so that they can be used in const expressions.
   const Diagnosticable();
+}
 
+/// A mixin class that provides the implementation for [Diagnosticable].
+///
+/// This mixin can be used to add diagnostics to a class which already has an
+/// base class.
+///
+/// The string debug representation is generated from the intermediate
+/// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is
+/// also used by debugging tools displaying interactive trees of objects and
+/// properties.
+///
+/// See also:
+///
+///  * [debugFillProperties], which lists best practices for specifying the
+///    properties of a [DiagnosticsNode]. The most common use case is to
+///    override [debugFillProperties] defining custom properties for a subclass
+///    of [DiagnosticableTreeMixin] using the existing [DiagnosticsProperty]
+///    subclasses.
+///  * [DiagnosticableTree], which extends this class to also describe the
+///    children of a tree structured object.
+///  * [DiagnosticableTree.debugDescribeChildren], which lists best practices
+///    for describing the children of a [DiagnosticsNode]. Typically the base
+///    class already describes the children of a node properly or a node has
+///    no children.
+///  * [DiagnosticsProperty], which should be used to create leaf diagnostic
+///    nodes without properties or children. There are many
+///    [DiagnosticsProperty] subclasses to handle common use cases.
+mixin DiagnosticableMixin {
   /// A brief description of this object, usually just the [runtimeType] and the
   /// [hashCode].
   ///
@@ -2691,12 +2982,17 @@
 
   @override
   String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
-    return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine)
-        .toString(minLevel: minLevel);
+    String fullString;
+    assert(() {
+      fullString = toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine)
+          .toString(minLevel: minLevel);
+      return true;
+    }());
+    return fullString ?? toStringShort();
   }
 
   /// Returns a debug representation of the object that is used by debugging
-  /// tools and by [toStringDeep].
+  /// tools and by [DiagnosticsNode.toStringDeep].
   ///
   /// Leave [name] as null if there is not a meaningful description of the
   /// relationship between the this node and its parent.
@@ -2775,6 +3071,10 @@
   ///  * [ObjectFlagProperty], which provides terse descriptions of whether a
   ///    property value is present or not. For example, whether an `onClick`
   ///    callback is specified or an animation is in progress.
+  ///  * [ColorProperty], which must be used if the property value is
+  ///    a [Color] or one of its subclasses.
+  ///  * [IconDataProperty], which must be used if the property value
+  ///    is of type [IconData].
   ///
   /// If none of these subclasses apply, use the [DiagnosticsProperty]
   /// constructor or in rare cases create your own [DiagnosticsProperty]
@@ -2918,8 +3218,6 @@
   @protected
   @mustCallSuper
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {}
-
-  DiagnosticLevel get debugDiagnosticLevel => DiagnosticLevel.info;
 }
 
 /// A base class for providing string and [DiagnosticsNode] debug
@@ -2947,7 +3245,7 @@
   ///
   /// `joiner` specifies the string which is place between each part obtained
   /// from [debugFillProperties]. Passing a string such as `'\n '` will result
-  /// in a multiline string that indents the properties of the object below its
+  /// in a multi-line string that indents the properties of the object below its
   /// name (as per [toString]).
   ///
   /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included
@@ -2961,6 +3259,9 @@
     String joiner = ', ',
     DiagnosticLevel minLevel = DiagnosticLevel.debug,
   }) {
+    if (kReleaseMode) {
+      return toString();
+    }
     final StringBuffer result = StringBuffer();
     result.write(toString());
     result.write(joiner);
@@ -3008,7 +3309,7 @@
 
   @override
   DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) {
-    return _DiagnosticableTreeNode(
+    return DiagnosticableTreeNode(
       name: name,
       value: this,
       style: style,
@@ -3051,6 +3352,9 @@
     String joiner = ', ',
     DiagnosticLevel minLevel = DiagnosticLevel.debug,
   }) {
+    if (kReleaseMode) {
+      return toString();
+    }
     final StringBuffer result = StringBuffer();
     result.write(toStringShort());
     result.write(joiner);
@@ -3081,7 +3385,7 @@
 
   @override
   DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) {
-    return _DiagnosticableTreeNode(
+    return DiagnosticableTreeNode(
       name: name,
       value: this,
       style: style,
@@ -3093,17 +3397,15 @@
 
   @override
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {}
-
-  @override
-  DiagnosticLevel get debugDiagnosticLevel => DiagnosticLevel.info;
 }
 
-/// Use this class to create a Diagnostic that exists purely to provide a
-/// container for other diagnostics.
+/// [DiagnosticsNode] that exists mainly to provide a container for other
+/// diagnostics that typically lacks a meaningful value of its own.
 ///
-/// For example, use this diagnostic to nest a link and message diagnostic
-/// inside a hint.
+/// This class is typically used for displaying complex nested error messages.
 class DiagnosticsBlock extends DiagnosticsNode {
+  /// Creates a diagnostic with properties specified by [properties] and
+  /// children specified by [children].
   DiagnosticsBlock({
     String name,
     DiagnosticsTreeStyle style = DiagnosticsTreeStyle.whitespace,
@@ -3113,6 +3415,7 @@
     this.value,
     String description,
     this.level = DiagnosticLevel.info,
+    this.allowTruncate = false,
     List<DiagnosticsNode> children = const <DiagnosticsNode>[],
     List<DiagnosticsNode> properties = const <DiagnosticsNode>[],
   })  : _description = description,
@@ -3136,6 +3439,9 @@
   final Object value;
 
   @override
+  final bool allowTruncate;
+
+  @override
   List<DiagnosticsNode> getChildren() => _children;
 
   @override
@@ -3145,3 +3451,167 @@
   String toDescription({TextTreeConfiguration parentConfiguration}) =>
       _description;
 }
+
+/// A delegate that configures how a hierarchy of [DiagnosticsNode]s should be
+/// serialized.
+///
+/// Implement this class in a subclass to fully configure how [DiagnosticsNode]s
+/// get serialized.
+abstract class DiagnosticsSerializationDelegate {
+  /// Creates a simple [DiagnosticsSerializationDelegate] that controls the
+  /// [subtreeDepth] and whether to [includeProperties].
+  ///
+  /// For additional configuration options, extend
+  /// [DiagnosticsSerializationDelegate] and provide custom implementations
+  /// for the methods of this class.
+  const factory DiagnosticsSerializationDelegate({
+    int subtreeDepth,
+    bool includeProperties,
+  }) = _DefaultDiagnosticsSerializationDelegate;
+
+  /// Returns a serializable map of additional information that will be included
+  /// in the serialization of the given [DiagnosticsNode].
+  ///
+  /// This method is called for every [DiagnosticsNode] that's included in
+  /// the serialization.
+  Map<String, Object> additionalNodeProperties(DiagnosticsNode node);
+
+  /// Filters the list of [DiagnosticsNode]s that will be included as children
+  /// for the given `owner` node.
+  ///
+  /// The callback may return a subset of the children in the provided list
+  /// or replace the entire list with new child nodes.
+  ///
+  /// See also:
+  ///
+  ///  * [subtreeDepth], which controls how many levels of children will be
+  ///    included in the serialization.
+  List<DiagnosticsNode> filterChildren(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner);
+
+  /// Filters the list of [DiagnosticsNode]s that will be included as properties
+  /// for the given `owner` node.
+  ///
+  /// The callback may return a subset of the properties in the provided list
+  /// or replace the entire list with new property nodes.
+  ///
+  /// By default, `nodes` is returned as-is.
+  ///
+  /// See also:
+  ///
+  ///  * [includeProperties], which controls whether properties will be included
+  ///    at all.
+  List<DiagnosticsNode> filterProperties(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner);
+
+  /// Truncates the given list of [DiagnosticsNode] that will be added to the
+  /// serialization as children or properties of the `owner` node.
+  ///
+  /// The method must return a subset of the provided nodes and may
+  /// not replace any nodes. While [filterProperties] and [filterChildren]
+  /// completely hide a node from the serialization, truncating a node will
+  /// leave a hint in the serialization that there were additional nodes in the
+  /// result that are not included in the current serialization.
+  ///
+  /// By default, `nodes` is returned as-is.
+  List<DiagnosticsNode> truncateNodesList(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner);
+
+  /// Returns the [DiagnosticsSerializationDelegate] to be used
+  /// for adding the provided [DiagnosticsNode] to the serialization.
+  ///
+  /// By default, this will return a copy of this delegate, which has the
+  /// [subtreeDepth] reduced by one.
+  ///
+  /// This is called for nodes that will be added to the serialization as
+  /// property or child of another node. It may return the same delegate if no
+  /// changes to it are necessary.
+  DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node);
+
+  /// Controls how many levels of children will be included in the serialized
+  /// hierarchy of [DiagnosticsNode]s.
+  ///
+  /// Defaults to zero.
+  ///
+  /// See also:
+  ///
+  ///  * [filterChildren], which provides a way to filter the children that
+  ///    will be included.
+  int get subtreeDepth;
+
+  /// Whether to include the properties of a [DiagnosticsNode] in the
+  /// serialization.
+  ///
+  /// Defaults to false.
+  ///
+  /// See also:
+  ///
+  ///  * [filterProperties], which provides a way to filter the properties that
+  ///    will be included.
+  bool get includeProperties;
+
+  /// Whether properties that have a [Diagnosticable] as value should be
+  /// expanded.
+  bool get expandPropertyValues;
+
+  /// Creates a copy of this [DiagnosticsSerializationDelegate] with the
+  /// provided values.
+  DiagnosticsSerializationDelegate copyWith({
+    int subtreeDepth,
+    bool includeProperties,
+  });
+}
+
+class _DefaultDiagnosticsSerializationDelegate
+    implements DiagnosticsSerializationDelegate {
+  const _DefaultDiagnosticsSerializationDelegate({
+    this.includeProperties = false,
+    this.subtreeDepth = 0,
+  });
+
+  @override
+  Map<String, Object> additionalNodeProperties(DiagnosticsNode node) {
+    return const <String, Object>{};
+  }
+
+  @override
+  DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) {
+    return subtreeDepth > 0 ? copyWith(subtreeDepth: subtreeDepth - 1) : this;
+  }
+
+  @override
+  bool get expandPropertyValues => false;
+
+  @override
+  List<DiagnosticsNode> filterChildren(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
+    return nodes;
+  }
+
+  @override
+  List<DiagnosticsNode> filterProperties(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
+    return nodes;
+  }
+
+  @override
+  final bool includeProperties;
+
+  @override
+  final int subtreeDepth;
+
+  @override
+  List<DiagnosticsNode> truncateNodesList(
+      List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
+    return nodes;
+  }
+
+  @override
+  DiagnosticsSerializationDelegate copyWith(
+      {int subtreeDepth, bool includeProperties}) {
+    return _DefaultDiagnosticsSerializationDelegate(
+      subtreeDepth: subtreeDepth ?? this.subtreeDepth,
+      includeProperties: includeProperties ?? this.includeProperties,
+    );
+  }
+}
diff --git a/devtools/lib/src/ui/fake_flutter/fake_flutter.dart b/devtools/lib/src/ui/fake_flutter/fake_flutter.dart
index 164cf82..64a3c8e 100644
--- a/devtools/lib/src/ui/fake_flutter/fake_flutter.dart
+++ b/devtools/lib/src/ui/fake_flutter/fake_flutter.dart
@@ -15,6 +15,7 @@
 
 import 'dart:async';
 import 'dart:collection';
+import 'dart:math' as math;
 
 import 'package:meta/meta.dart';
 
@@ -26,6 +27,7 @@
 export 'dart_ui/dart_ui.dart' hide TextStyle;
 
 part 'assertions.dart';
+part 'change_notifier.dart';
 part 'diagnosticable.dart';
 part 'foundation.dart';
 part 'print.dart';
diff --git a/devtools/lib/src/ui/fake_flutter/foundation.dart b/devtools/lib/src/ui/fake_flutter/foundation.dart
index 1e334e2..df4ad67 100644
--- a/devtools/lib/src/ui/fake_flutter/foundation.dart
+++ b/devtools/lib/src/ui/fake_flutter/foundation.dart
@@ -8,3 +8,23 @@
 
 /// The signature of [State.setState] functions.
 typedef StateSetter = void Function(VoidCallback fn);
+
+const kReleaseMode = false;
+
+/// Configure [debugFormatDouble] using [num.toStringAsPrecision].
+///
+/// Defaults to null, which uses the default logic of [debugFormatDouble].
+int debugDoublePrecision;
+
+/// Formats a double to have standard formatting.
+///
+/// This behavior can be overriden by [debugDoublePrecision].
+String debugFormatDouble(double value) {
+  if (value == null) {
+    return 'null';
+  }
+  if (debugDoublePrecision != null) {
+    return value.toStringAsPrecision(debugDoublePrecision);
+  }
+  return value.toStringAsFixed(1);
+}
diff --git a/devtools/lib/src/ui/fake_flutter/print.dart b/devtools/lib/src/ui/fake_flutter/print.dart
index 2f51ea6..4e7ac2a 100644
--- a/devtools/lib/src/ui/fake_flutter/print.dart
+++ b/devtools/lib/src/ui/fake_flutter/print.dart
@@ -96,7 +96,6 @@
     _debugPrintCompleter?.future ?? Future<void>.value();
 
 final RegExp _indentPattern = RegExp('^ *(?:[-+*] |[0-9]+[.):] )?');
-enum _WordWrapParseMode { inSpace, inWord, atBreak }
 
 /// Wraps the given string at the given width.
 ///
diff --git a/devtools/lib/src/ui/icons.dart b/devtools/lib/src/ui/icons.dart
index dcd26c9..caab719 100644
--- a/devtools/lib/src/ui/icons.dart
+++ b/devtools/lib/src/ui/icons.dart
@@ -13,11 +13,11 @@
 /// of code that uses icons can run on the Dart VM.
 library icons;
 
-import 'package:devtools/src/ui/material_icons.dart';
-import 'package:devtools/src/ui/theme.dart';
 import 'package:meta/meta.dart';
 
 import 'fake_flutter/fake_flutter.dart';
+import 'material_icons.dart';
+import 'theme.dart';
 
 abstract class Icon {
   const Icon();
@@ -126,6 +126,8 @@
   static const Icon redProgress = UrlIcon('/icons/perf/red_progress.gif');
   static const Icon yellowProgress = UrlIcon('/icons/perf/yellow_progress.gif');
 
+  static const Icon redError = UrlIcon('/icons/perf/RedExcl.png');
+
   // Icons matching IntelliJ core icons.
   static const Icon locate = UrlIcon('/icons/general/locate.png');
   static const Icon forceRefresh = UrlIcon('/icons/actions/forceRefresh.svg');
@@ -167,6 +169,8 @@
   static const UrlIcon lightbulb_2x =
       UrlIcon('/icons/general/lightbulb_outline@2x.png');
 
+  static const UrlIcon allocation = UrlIcon('/icons/memory/alloc_icon.png');
+  static const UrlIcon search = UrlIcon('/icons/memory/ic_search.png');
   static const UrlIcon snapshot = UrlIcon('/icons/memory/snapshot_color.png');
   static const UrlIcon resetAccumulators =
       UrlIcon('/icons/memory/reset_icon.png', invertDark: true);
@@ -248,7 +252,7 @@
 }
 
 class IconKind {
-  const IconKind(this.name, this.icon, [abstractIcon])
+  const IconKind(this.name, this.icon, [Icon abstractIcon])
       : abstractIcon = abstractIcon ?? icon;
 
   static const IconKind classIcon = IconKind(
diff --git a/devtools/lib/src/ui/material_icons.dart b/devtools/lib/src/ui/material_icons.dart
index 5387ca2..01244f2 100644
--- a/devtools/lib/src/ui/material_icons.dart
+++ b/devtools/lib/src/ui/material_icons.dart
@@ -8,6 +8,19 @@
 import 'icons.dart';
 import 'theme.dart';
 
+const Icon clearIcon = MaterialIcon('block', defaultButtonIconColor);
+
+const Icon exitIcon = MaterialIcon('clear', defaultButtonIconColor);
+
+const Icon exportIcon = MaterialIcon('file_download', defaultButtonIconColor);
+
+const Icon recordPrimary =
+    MaterialIcon('fiber_manual_record', defaultPrimaryButtonIconColor);
+
+const Icon record = MaterialIcon('fiber_manual_record', defaultButtonIconColor);
+
+const Icon stop = MaterialIcon('stop', defaultButtonIconColor);
+
 /// Class for icons consistent with
 /// https://docs.flutter.io/flutter/material/Icons-class.html
 class MaterialIcon extends Icon {
@@ -29,6 +42,7 @@
 
 class FlutterMaterialIcons {
   FlutterMaterialIcons._();
+
   static final Map<String, MaterialIcon> _iconCache = {};
 
   static Icon getIconForCodePoint(int charCode) {
diff --git a/devtools/lib/src/ui/service_extension_elements.dart b/devtools/lib/src/ui/service_extension_elements.dart
new file mode 100644
index 0000000..b89cbe2
--- /dev/null
+++ b/devtools/lib/src/ui/service_extension_elements.dart
@@ -0,0 +1,292 @@
+// Copyright 2019 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 'dart:html' as html;
+
+import '../globals.dart';
+import '../service_extensions.dart';
+import '../service_manager.dart' show ServiceExtensionState;
+import '../service_registrations.dart';
+import '../utils.dart';
+import 'analytics.dart' as ga;
+import 'elements.dart';
+import 'primer.dart';
+
+List<CoreElement> getServiceExtensionElements() {
+  return [
+    div(c: 'btn-group collapsible-1200 nowrap margin-left')
+      ..add(<CoreElement>[
+        ServiceExtensionButton(performanceOverlay).button,
+        ServiceExtensionButton(slowAnimations).button,
+      ]),
+    div(c: 'btn-group collapsible-1200 nowrap margin-left')
+      ..add(<CoreElement>[
+        ServiceExtensionButton(debugPaint).button,
+        ServiceExtensionButton(debugPaintBaselines).button,
+      ]),
+    div(c: 'btn-group collapsible-1400 nowrap margin-left')
+      ..add(<CoreElement>[
+        ServiceExtensionButton(repaintRainbow).button,
+        ServiceExtensionButton(debugAllowBanner).button,
+      ]),
+    div(c: 'btn-group nowrap margin-left')
+      ..add(TogglePlatformSelector().selector)
+  ];
+}
+
+/// Checkbox that stays synced with the value of a service extension.
+///
+/// Service extensions can be found in [service_extensions.dart].
+///
+/// See also:
+/// * ServiceExtensionButton, which provides the same functionality but uses
+///   a button instead of a button. In general, using a button makes the UI
+///   more compact but the checkbox makes the current state of the UI clearer.
+class ServiceExtensionCheckbox {
+  ServiceExtensionCheckbox(this.extensionDescription)
+      : element = CoreElement('label') {
+    final checkbox = CoreElement('input')..setAttribute('type', 'checkbox');
+    _checkboxElement = checkbox.element;
+
+    element.add(<CoreElement>[
+      checkbox,
+      span(text: ' ${extensionDescription.description}'),
+    ]);
+
+    final extensionName = extensionDescription.extension;
+
+    // Disable button for unavailable service extensions.
+    checkbox.disabled = !serviceManager.serviceExtensionManager
+        .isServiceExtensionAvailable(extensionName);
+    serviceManager.serviceExtensionManager.hasServiceExtension(
+        extensionName, (available) => checkbox.disabled = !available);
+
+    _checkboxElement.onChange.listen((_) {
+      ga.select(extensionDescription.gaScreenName, extensionDescription.gaItem);
+
+      final bool selected = _checkboxElement.checked;
+      serviceManager.serviceExtensionManager.setServiceExtensionState(
+        extensionDescription.extension,
+        selected,
+        selected
+            ? extensionDescription.enabledValue
+            : extensionDescription.disabledValue,
+      );
+    });
+    _updateState();
+  }
+
+  final ToggleableServiceExtensionDescription extensionDescription;
+  final CoreElement element;
+  html.InputElement _checkboxElement;
+
+  void _updateState() {
+    serviceManager.serviceExtensionManager
+        .getServiceExtensionState(extensionDescription.extension, (state) {
+      final extensionEnabled = state.value == extensionDescription.enabledValue;
+      _checkboxElement.checked = extensionEnabled;
+      // We display the tooltips in the reverse order they show up in
+      // ServiceExtensionButton as for a checkbox it makes more sense to show
+      // a tooltip for the current value instead of for the value clicking on
+      // the button would switch to.
+      element.tooltip = extensionEnabled
+          ? extensionDescription.disabledTooltip
+          : extensionDescription.enabledTooltip;
+    });
+  }
+}
+
+/// Button that calls a service extension.
+///
+/// Service extensions can be found in [service_extensions.dart].
+///
+/// See also:
+/// * ServiceExtensionCheckbox, which provides the same functionality but
+///   uses a checkbox instead of a button. In general, using a checkbox makes
+///   the state of the UI clearer but requires more space.
+class ServiceExtensionButton {
+  ServiceExtensionButton(this.extensionDescription)
+      : button = PButton.icon(
+          extensionDescription.description,
+          extensionDescription.icon,
+          title: extensionDescription.disabledTooltip,
+        )..small() {
+    final extensionName = extensionDescription.extension;
+
+    // Disable button for unavailable service extensions.
+    button.disabled = !serviceManager.serviceExtensionManager
+        .isServiceExtensionAvailable(extensionName);
+    serviceManager.serviceExtensionManager.hasServiceExtension(
+        extensionName, (available) => button.disabled = !available);
+
+    button.click(() => _click());
+
+    _updateState();
+  }
+
+  final ToggleableServiceExtensionDescription extensionDescription;
+  final PButton button;
+
+  void _click() {
+    ga.select(extensionDescription.gaScreenName, extensionDescription.gaItem);
+
+    final bool wasSelected = button.element.classes.contains('selected');
+    serviceManager.serviceExtensionManager.setServiceExtensionState(
+      extensionDescription.extension,
+      !wasSelected,
+      wasSelected
+          ? extensionDescription.disabledValue
+          : extensionDescription.enabledValue,
+    );
+  }
+
+  void _updateState() {
+    serviceManager.serviceExtensionManager
+        .getServiceExtensionState(extensionDescription.extension, (state) {
+      final extensionEnabled = state.value == extensionDescription.enabledValue;
+      button.toggleClass('selected', extensionEnabled);
+      button.tooltip = extensionEnabled
+          ? extensionDescription.enabledTooltip
+          : extensionDescription.disabledTooltip;
+    });
+  }
+}
+
+/// Button that calls a registered service from flutter_tools. Registered
+/// services can be found in [service_registrations.dart].
+class RegisteredServiceExtensionButton {
+  RegisteredServiceExtensionButton(
+    this.serviceDescription,
+    this.action,
+    this.errorAction,
+  ) {
+    button = PButton.icon(
+      serviceDescription.title,
+      serviceDescription.icon,
+      title: serviceDescription.title,
+    )
+      ..small()
+      ..hidden(true);
+
+    // Only show the button if the device supports the given service.
+    serviceManager.hasRegisteredService(
+      serviceDescription.service,
+      (registered) {
+        button.hidden(!registered);
+      },
+    );
+
+    button.click(() => _click());
+  }
+
+  final RegisteredServiceDescription serviceDescription;
+  final VoidAsyncFunction action;
+  final VoidFunctionWithArg errorAction;
+  PButton button;
+
+  void _click() async {
+    try {
+      button.disabled = true;
+      await action();
+    } catch (e) {
+      errorAction(e);
+    } finally {
+      button.disabled = false;
+    }
+  }
+}
+
+/// Dropdown selector that calls a service extension.
+///
+/// Service extensions can be found in [service_extensions.dart].
+class ServiceExtensionSelector {
+  ServiceExtensionSelector(this.extensionDescription) : selector = PSelect() {
+    selector
+      ..small()
+      ..clazz('button-bar-dropdown')
+      ..change(_handleSelect)
+      ..tooltip = extensionDescription.tooltips.first ??
+          extensionDescription.description;
+
+    final extensionName = extensionDescription.extension;
+
+    // Disable selector for unavailable service extensions.
+    selector.disabled = !serviceManager.serviceExtensionManager
+        .isServiceExtensionAvailable(extensionName);
+    serviceManager.serviceExtensionManager.hasServiceExtension(
+        extensionName, (available) => selector.disabled = !available);
+
+    addOptions();
+    updateState();
+  }
+
+  final ServiceExtensionDescription extensionDescription;
+
+  final PSelect selector;
+
+  String _selectedValue;
+
+  void _handleSelect() {
+    if (selector.value == _selectedValue) return;
+
+    ga.select(extensionDescription.gaScreenName, extensionDescription.gaItem);
+
+    final extensionValue = extensionDescription
+        .values[extensionDescription.displayValues.indexOf(selector.value)];
+
+    serviceManager.serviceExtensionManager.setServiceExtensionState(
+      extensionDescription.extension,
+      true,
+      extensionValue,
+    );
+
+    _selectedValue = selector.value;
+  }
+
+  void addOptions() {
+    extensionDescription.displayValues.forEach(selector.option);
+  }
+
+  void updateState() {
+    // Select option whose state is already enabled.
+    serviceManager.serviceExtensionManager
+        .getServiceExtensionState(extensionDescription.extension, (state) {
+      updateSelection(state);
+    });
+  }
+
+  void updateSelection(ServiceExtensionState state) {
+    if (state.value != null) {
+      final selectedIndex = extensionDescription.values.indexOf(state.value);
+      selector.selectedIndex = selectedIndex;
+      _selectedValue = extensionDescription.displayValues[selectedIndex];
+    }
+  }
+}
+
+class TogglePlatformSelector extends ServiceExtensionSelector {
+  TogglePlatformSelector() : super(togglePlatformMode);
+
+  static const fuchsia = 'Fuchsia';
+
+  @override
+  void addOptions() {
+    extensionDescription.displayValues
+        .where((displayValue) => !displayValue.contains(fuchsia))
+        .forEach(selector.option);
+  }
+
+  @override
+  void updateState() {
+    // Select option whose state is already enabled.
+    serviceManager.serviceExtensionManager
+        .getServiceExtensionState(extensionDescription.extension, (state) {
+      if (state.value == fuchsia.toLowerCase()) {
+        selector.option(extensionDescription.displayValues
+            .firstWhere((displayValue) => displayValue.contains(fuchsia)));
+      }
+      updateSelection(state);
+    });
+  }
+}
diff --git a/devtools/lib/src/ui/theme.dart b/devtools/lib/src/ui/theme.dart
index c005c60..f3f42b1 100644
--- a/devtools/lib/src/ui/theme.dart
+++ b/devtools/lib/src/ui/theme.dart
@@ -45,6 +45,8 @@
   Color(0xFF89B5F8),
 );
 
+const defaultPrimaryButtonIconColor = defaultBackground;
+
 /// Color that behaves differently depending on whether a light or dark theme
 /// is used.
 ///
@@ -54,6 +56,8 @@
 class ThemedColor implements Color {
   const ThemedColor(this._light, this._dark);
 
+  static ThemedColor fromSingleColor(Color color) => ThemedColor(color, color);
+
   final Color _light;
   final Color _dark;
 
diff --git a/devtools/lib/src/ui/ui_utils.dart b/devtools/lib/src/ui/ui_utils.dart
index 8624d89..19810ef 100644
--- a/devtools/lib/src/ui/ui_utils.dart
+++ b/devtools/lib/src/ui/ui_utils.dart
@@ -9,78 +9,14 @@
 
 import '../framework/framework.dart';
 import '../globals.dart';
-import '../service_extensions.dart';
-import '../service_registrations.dart';
-import '../service_registrations.dart' as registrations;
-import '../utils.dart';
-import 'analytics.dart' as ga;
+import '../messages.dart';
 import 'elements.dart';
 import 'environment.dart' as environment;
 import 'fake_flutter/dart_ui/dart_ui.dart';
 import 'html_icon_renderer.dart';
 import 'material_icons.dart';
-import 'primer.dart';
 
 const int defaultSplitterWidth = 10;
-const String runInProfileModeDocsUrl =
-    'https://flutter.dev/docs/testing/ui-performance#run-in-profile-mode';
-
-CoreElement createExtensionCheckBox(
-    ToggleableServiceExtensionDescription extensionDescription) {
-  final extensionName = extensionDescription.extension;
-  final CoreElement input = checkbox();
-
-  serviceManager.serviceExtensionManager.hasServiceExtension(
-      extensionName, (available) => input.disabled = !available);
-
-  serviceManager.serviceExtensionManager.getServiceExtensionState(
-    extensionName,
-    (state) {
-      final InputElement e = input.element;
-      e.checked = state.value;
-    },
-  );
-
-  input.element.onChange.listen((_) {
-    final InputElement e = input.element;
-    serviceManager.serviceExtensionManager
-        .setServiceExtensionState(extensionName, e.checked, e.checked);
-  });
-  final inputLabel = label();
-  if (extensionDescription.icon != null) {
-    inputLabel.add(createIconElement(extensionDescription.icon));
-  }
-  inputLabel.add(span(text: extensionName));
-
-  final outerDiv = div(c: 'form-checkbox')
-    ..add(CoreElement('label')..add([input, inputLabel]));
-  input.setAttribute('title', extensionDescription.disabledTooltip);
-  return outerDiv;
-}
-
-List<CoreElement> getServiceExtensionButtons() {
-  return [
-    div(c: 'btn-group collapsible-1200 nowrap margin-left')
-      ..add(<CoreElement>[
-        ServiceExtensionButton(performanceOverlay).button,
-        ServiceExtensionButton(togglePlatformMode).button,
-      ]),
-    div(c: 'btn-group collapsible-1200 nowrap margin-left')
-      ..add(<CoreElement>[
-        ServiceExtensionButton(debugPaint).button,
-        ServiceExtensionButton(debugPaintBaselines).button,
-      ]),
-    div(c: 'btn-group collapsible-1200 nowrap margin-left')
-      ..add(<CoreElement>[
-        ServiceExtensionButton(slowAnimations).button,
-      ]),
-    div(c: 'btn-group collapsible-1400 nowrap margin-left')
-      ..add(<CoreElement>[
-        ServiceExtensionButton(repaintRainbow).button,
-        ServiceExtensionButton(debugAllowBanner).button,
-      ]),
-  ];
-}
 
 StatusItem createLinkStatusItem(
   CoreElement textElement, {
@@ -105,158 +41,12 @@
   return StatusItem()..element.add(element);
 }
 
-CoreElement createHotReloadRestartGroup(Framework framework) {
-  return div(c: 'btn-group')
-    ..add([
-      createHotReloadButton(framework),
-      createHotRestartButton(framework),
-    ]);
-}
-
-CoreElement createHotReloadButton(Framework framework) {
-  final action = () async {
-    await serviceManager.performHotReload();
-  };
-  final errorAction = (e) {
-    framework.showError('Error performing hot reload', e);
-  };
-  return RegisteredServiceExtensionButton(
-    registrations.hotReload,
-    action,
-    errorAction,
-  ).button;
-}
-
-// TODO: move this button out of timeline if we decide to make a global button bar.
-CoreElement createHotRestartButton(Framework framework) {
-  final action = () async {
-    await serviceManager.performHotRestart();
-  };
-  final errorAction = (e) {
-    framework.showError('Error performing hot restart', e);
-  };
-
-  return RegisteredServiceExtensionButton(
-    registrations.hotRestart,
-    action,
-    errorAction,
-  ).button;
-}
-
-Future<void> maybeShowDebugWarning(Framework framework) async {
-  if (!snapshotMode &&
+Future<void> maybeAddDebugMessage(Framework framework, String screenId) async {
+  if (!offlineMode &&
       serviceManager.connectedApp != null &&
+      await serviceManager.connectedApp.isFlutterApp &&
       !await serviceManager.connectedApp.isProfileBuild) {
-    framework.showWarning(children: <CoreElement>[
-      div(
-          text: 'You are running your app in debug mode. Debug mode frame '
-              'rendering times are not indicative of release performance.'),
-      div()
-        ..add(span(
-            text:
-                '''Relaunch your application with the '--profile' argument, or '''))
-        ..add(a(
-            text: 'relaunch in profile mode from VS Code or IntelliJ',
-            href: runInProfileModeDocsUrl,
-            target: '_blank;'))
-        ..add(span(text: '.')),
-    ]);
-  }
-}
-
-/// Button that calls a service extension. Service extensions can be found in
-/// [service_extensions.dart].
-class ServiceExtensionButton {
-  ServiceExtensionButton(this.extensionDescription) {
-    button = PButton.icon(
-      extensionDescription.description,
-      extensionDescription.icon,
-      title: extensionDescription.disabledTooltip,
-    )..small();
-
-    final extensionName = extensionDescription.extension;
-
-    // Disable button for unavailable service extensions.
-    button.disabled = !serviceManager.serviceExtensionManager
-        .isServiceExtensionAvailable(extensionName);
-    serviceManager.serviceExtensionManager.hasServiceExtension(
-        extensionName, (available) => button.disabled = !available);
-
-    button.click(() => _click());
-
-    _updateState();
-  }
-
-  final ToggleableServiceExtensionDescription extensionDescription;
-  PButton button;
-
-  void _click() {
-    ga.select(extensionDescription.gaScreenName, extensionDescription.gaItem);
-
-    final bool wasSelected = button.element.classes.contains('selected');
-    serviceManager.serviceExtensionManager.setServiceExtensionState(
-      extensionDescription.extension,
-      !wasSelected,
-      wasSelected
-          ? extensionDescription.disabledValue
-          : extensionDescription.enabledValue,
-    );
-  }
-
-  void _updateState() {
-    // Select button whose state is already enabled.
-    serviceManager.serviceExtensionManager
-        .getServiceExtensionState(extensionDescription.extension, (state) {
-      final extensionEnabled = state.value == extensionDescription.enabledValue;
-      button.toggleClass('selected', extensionEnabled);
-      button.tooltip = extensionEnabled
-          ? extensionDescription.enabledTooltip
-          : extensionDescription.disabledTooltip;
-    });
-  }
-}
-
-/// Button that calls a registered service from flutter_tools. Registered
-/// services can be found in [service_registrations.dart].
-class RegisteredServiceExtensionButton {
-  RegisteredServiceExtensionButton(
-    this.serviceDescription,
-    this.action,
-    this.errorAction,
-  ) {
-    button = PButton.icon(
-      serviceDescription.title,
-      serviceDescription.icon,
-      title: serviceDescription.title,
-    )
-      ..small()
-      ..hidden(true);
-
-    // Only show the button if the device supports the given service.
-    serviceManager.hasRegisteredService(
-      serviceDescription.service,
-      (registered) {
-        button.hidden(!registered);
-      },
-    );
-
-    button.click(() => _click());
-  }
-
-  final RegisteredServiceDescription serviceDescription;
-  final VoidAsyncFunction action;
-  final VoidFunctionWithArg errorAction;
-  PButton button;
-
-  void _click() async {
-    try {
-      button.disabled = true;
-      await action();
-    } catch (e) {
-      errorAction(e);
-    } finally {
-      button.disabled = false;
-    }
+    framework.showMessage(message: debugWarning, screenId: screenId);
   }
 }
 
@@ -269,11 +59,7 @@
 Set<String> _lookupHiddenPages() {
   final queryString = window.location.search;
   if (queryString == null || queryString.length <= 1) {
-    // TODO(dantup): Remove this ignore, change to `{}` and bump SDK requirements
-    // in pubspec.yaml (devtools + devtools_server) once Flutter stable includes
-    // Dart SDK >= v2.2.
-    // ignore: prefer_collection_literals
-    return Set();
+    return {};
   }
   final qsParams = Uri.splitQueryString(queryString.substring(1));
   return (qsParams['hide'] ?? '').split(',').toSet();
@@ -322,3 +108,27 @@
   element.click();
   element.remove();
 }
+
+CoreElement createRecordingInstructions({@required String recordingGoal}) {
+  return div(c: 'center-in-parent recording-instruction-container')
+    ..layoutVertical()
+    ..flex()
+    ..add([
+      div(c: 'recording-instruction-message')
+        ..layoutHorizontal()
+        ..flex()
+        ..add([
+          div(text: 'Click the record button '),
+          createIconElement(record),
+          div(text: recordingGoal)
+        ]),
+      div(c: 'recording-instruction-message')
+        ..layoutHorizontal()
+        ..flex()
+        ..add([
+          div(text: 'Click the stop button '),
+          createIconElement(stop),
+          div(text: 'to end the recording.')
+        ]),
+    ]);
+}
diff --git a/devtools/lib/src/ui/viewport_canvas.dart b/devtools/lib/src/ui/viewport_canvas.dart
index 3b11ab2..9d8ea0e 100644
--- a/devtools/lib/src/ui/viewport_canvas.dart
+++ b/devtools/lib/src/ui/viewport_canvas.dart
@@ -184,11 +184,31 @@
       // async so we risk flickering UI if we don't render with a buffer.
       rebuild(force: false);
     });
+
     if (_onTap != null) {
       _content.onClick.listen((e) {
         _onTap(_clientToGlobal(e.client));
       });
+
+      // We do not have a simple `onTouch` method to use, so we use
+      // `onTouchStart`, `onTouchMove`, and `onTouchEnd` to accomplish the same
+      // end as well as disambiguate between touch-drags and touches.
+      _content.onTouchStart.listen((e) {
+        // If there are multiple touches, always use the first.
+        _activeTouch = e.touches.first;
+      });
+      _content.onTouchMove.listen((_) {
+        _wasDraggedByTouch = true;
+      });
+      _content.onTouchEnd.listen((_) {
+        if (!_wasDraggedByTouch && _activeTouch != null) {
+          _onTap(_clientToGlobal(_activeTouch.client));
+        }
+        _wasDraggedByTouch = false;
+        _activeTouch = null;
+      });
     }
+
     _content.element.onMouseLeave.listen((_) {
       _currentMouseHover = null;
       if (_onMouseLeave != null) {
@@ -203,6 +223,10 @@
 
   Point _currentMouseHover;
 
+  Touch _activeTouch;
+
+  bool _wasDraggedByTouch = false;
+
   /// Id used to help debug what was rendered as part of the current frame.
   int _frameId = 0;
 
diff --git a/devtools/lib/src/ui/vm_flag_elements.dart b/devtools/lib/src/ui/vm_flag_elements.dart
new file mode 100644
index 0000000..113f6e6
--- /dev/null
+++ b/devtools/lib/src/ui/vm_flag_elements.dart
@@ -0,0 +1,61 @@
+// Copyright 2019 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 'dart:async';
+
+import '../config_specific/allowed_error.dart';
+import '../framework/framework.dart';
+import '../globals.dart';
+import '../messages.dart';
+import 'primer.dart';
+
+// TODO(kenzie): handle the multi-client case for this selector.
+
+class ProfileGranularitySelector {
+  ProfileGranularitySelector(this.framework) {
+    selector = PSelect()
+      ..small()
+      ..clazz('button-bar-dropdown')
+      ..change(_handleSelect)
+      ..tooltip = 'Granularity of CPU profiling. For a finer-grained profile, '
+          'choose "Profile granularity: high". Please read our documentation to'
+          ' understand the trade-offs associated with this setting.'
+      ..option('Profile granularity: low', value: lowGranularityValue)
+      ..option('Profile granularity: medium', value: mediumGranularityValue)
+      ..option('Profile granularity: high', value: highGranularityValue);
+
+    // Select medium granularity (250 μs) as the default.
+    selector.selectedIndex = mediumGranularityIndex;
+  }
+
+  static const profilePeriodFlagName = 'profile_period';
+
+  static const lowGranularityValue = '1000';
+
+  static const mediumGranularityValue = '250';
+
+  static const highGranularityValue = '50';
+
+  static const mediumGranularityIndex = 1;
+
+  final Framework framework;
+
+  PSelect selector;
+
+  String _selectedValue;
+
+  Future<void> setGranularity() async {
+    return allowedError(
+        serviceManager.service.setFlag(profilePeriodFlagName, selector.value));
+  }
+
+  void _handleSelect() async {
+    if (selector.value == _selectedValue) return;
+    await setGranularity();
+    _selectedValue = selector.value;
+
+    if (selector.value == highGranularityValue) {
+      framework.showMessage(message: profileGranularityWarning);
+    }
+  }
+}
diff --git a/devtools/lib/src/url_utils.dart b/devtools/lib/src/url_utils.dart
new file mode 100644
index 0000000..1bb509e
--- /dev/null
+++ b/devtools/lib/src/url_utils.dart
@@ -0,0 +1,38 @@
+// Copyright 2019 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.
+
+/// Returns a simplified version of a package url, replacing "path/to/flutter"
+/// with "package:".
+///
+/// This is a bit of a hack, as we have file paths instead of the explicit
+/// "package:uris" we'd like to have. This will be problematic for a use case
+/// such as "packages/my_package/src/utils/packages/flutter/".
+String getSimplePackageUrl(String url) {
+  const newPackagePrefix = 'package:';
+  const originalPackagePrefix = 'packages/';
+
+  const flutterPrefix = 'packages/flutter/';
+  const flutterWebPrefix = 'packages/flutter_web/';
+  final flutterPrefixIndex = url.indexOf(flutterPrefix);
+  final flutterWebPrefixIndex = url.indexOf(flutterWebPrefix);
+
+  if (flutterPrefixIndex != -1) {
+    return newPackagePrefix +
+        url.substring(flutterPrefixIndex + originalPackagePrefix.length);
+  } else if (flutterWebPrefixIndex != -1) {
+    return newPackagePrefix +
+        url.substring(flutterWebPrefixIndex + originalPackagePrefix.length);
+  }
+  return url;
+}
+
+/// Returns a trimmed vm service uri without any trailing characters.
+///
+/// For example, given a [value] of http://127.0.0.1:60667/72K34Xmq0X0=/#/vm,
+/// this method will return the URI http://127.0.0.1:60667/72K34Xmq0X0=/.
+Uri getNormalizedTrimmedUri(String value) {
+  final uri = Uri.parse(value.trim()).removeFragment();
+  if (uri.path.endsWith('/')) return uri;
+  return uri.replace(path: uri.path);
+}
diff --git a/devtools/lib/src/utils.dart b/devtools/lib/src/utils.dart
index d6c7c50..7095a0f 100644
--- a/devtools/lib/src/utils.dart
+++ b/devtools/lib/src/utils.dart
@@ -8,7 +8,7 @@
 
 import 'package:collection/collection.dart';
 import 'package:intl/intl.dart';
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:vm_service/vm_service.dart';
 
 bool collectionEquals(e1, e2) => const DeepCollectionEquality().equals(e1, e2);
 
@@ -20,6 +20,9 @@
 sodales suscipit risus. Nullam consequat sit amet turpis vitae facilisis. Integer sit amet tempus arcu.
 ''';
 
+// 2^52 is the max int for dart2js.
+final int maxJsInt = pow(2, 52) as int;
+
 String getLoremText([int paragraphCount = 1]) {
   String str = '';
   for (int i = 0; i < paragraphCount; i++) {
@@ -50,8 +53,6 @@
 
 final NumberFormat nf = NumberFormat.decimalPattern();
 
-String percent(double d) => '${(d * 100).toStringAsFixed(1)}%';
-
 String percent2(double d) => '${(d * 100).toStringAsFixed(2)}%';
 
 String printMb(num bytes, [int fractionDigits = 1]) {
@@ -100,7 +101,7 @@
   } else if (ref.owner is ClassRef) {
     return '${ref.owner.name}.${ref.name}';
   } else if (ref.owner is FuncRef) {
-    return '${funcRefName(ref.owner)}.${ref.name}';
+    return '${funcRefName(ref.owner as FuncRef)}.${ref.name}';
   } else {
     return ref.name;
   }
@@ -145,15 +146,29 @@
   return originalText.substring(0, i);
 }
 
-/// Returns a trimmed vm service uri without any trailing characters.
+/// Whether a given code unit is a letter (A-Z or a-z).
+bool isLetter(int codeUnit) =>
+    (codeUnit >= 65 && codeUnit <= 90) || (codeUnit >= 97 && codeUnit <= 122);
+
+/// Returns a simplified version of a StackFrame name.
 ///
-/// For example, given a [value] of http://127.0.0.1:60667/72K34Xmq0X0=/#/vm,
-/// this method will return the URI http://127.0.0.1:60667/72K34Xmq0X0=/.
-Uri getTrimmedUri(String value) {
-  final uri = Uri.parse(value.trim());
-  return uri
-      .removeFragment()
-      .replace(path: uri.path.endsWith('/') ? uri.path : '${uri.path}/');
+/// Given an input such as
+/// `_WidgetsFlutterBinding&BindingBase&GestureBinding.handleBeginFrame`, this
+/// method will strip off all the leading class names and return
+/// `GestureBinding.handleBeginFrame`.
+///
+/// See (https://github.com/dart-lang/sdk/issues/36999).
+String getSimpleStackFrameName(String name) {
+  final newName = name.replaceAll('<anonymous closure>', '<closure>');
+
+  // If the class name contains a space, then it is not a valid Dart name. We
+  // throw out simplified names with spaces to prevent simplifying C++ class
+  // signatures, where the '&' char signifies a reference variable - not
+  // appended class names.
+  if (newName.contains(' ')) {
+    return newName;
+  }
+  return newName.split('&').last;
 }
 
 class Property<T> {
@@ -234,20 +249,20 @@
   static String getStringMember(Map<String, Object> json, String memberName) {
     // TODO(jacobr): should we handle non-string values with a reasonable
     // toString differently?
-    return json[memberName];
+    return json[memberName] as String;
   }
 
   static int getIntMember(Map<String, Object> json, String memberName) {
-    return json[memberName] ?? -1;
+    return json[memberName] as int ?? -1;
   }
 
   static List<String> getValues(Map<String, Object> json, String member) {
-    final List<Object> values = json[member];
+    final List<dynamic> values = json[member] as List;
     if (values == null || values.isEmpty) {
       return const [];
     }
 
-    return values.toList();
+    return values.cast();
   }
 
   static bool hasJsonData(String data) {
@@ -363,6 +378,8 @@
 
   Duration _end;
 
+  bool contains(Duration target) => target >= start && target <= end;
+
   set end(Duration value) {
     if (singleAssignment) {
       assert(_end == null);
diff --git a/devtools/lib/src/vm_service_wrapper.dart b/devtools/lib/src/vm_service_wrapper.dart
index e26d1fb..f65ff6f 100644
--- a/devtools/lib/src/vm_service_wrapper.dart
+++ b/devtools/lib/src/vm_service_wrapper.dart
@@ -4,7 +4,8 @@
 
 import 'dart:async';
 
-import 'package:vm_service_lib/vm_service_lib.dart';
+import 'package:meta/meta.dart';
+import 'package:vm_service/vm_service.dart';
 
 class VmServiceWrapper implements VmService {
   VmServiceWrapper(
@@ -24,14 +25,11 @@
   }
 
   VmService _vmService;
+  Version _protocolVersion;
   final bool trackFutures;
   final Map<String, Future<Success>> _activeStreams = {};
 
-  // TODO(dantup): Remove this ignore, change to `{}` and bump SDK requirements
-  // in pubspec.yaml (devtools + devtools_server) once Flutter stable includes
-  // Dart SDK >= v2.2.
-  // ignore: prefer_collection_literals
-  final Set<TrackedFuture<Object>> activeFutures = Set();
+  final Set<TrackedFuture<Object>> activeFutures = {};
   Completer<bool> _allFuturesCompleter = Completer<bool>()
     // Mark the future as completed by default so if we don't track any
     // futures but someone tries to wait on [allFuturesCompleted] they don't
@@ -95,24 +93,23 @@
         ));
   }
 
-  @override
-  Future<Success> clearCpuProfile(String isolateId) {
-    return _trackFuture(
-        'clearCpuProfile', _vmService.clearCpuProfile(isolateId));
+  Future<Success> clearCpuProfile(String isolateId) async {
+    final response = await _trackFuture('clearCpuProfile',
+        callMethod('_clearCpuProfile', isolateId: isolateId));
+    return response as Success;
   }
 
   @override
-  Future<Success> clearVMTimeline() {
+  Future<Success> clearVMTimeline() async {
+    if (await isProtocolVersionLessThan(major: 3, minor: 19)) {
+      final response =
+          await _trackFuture('clearVMTimeline', callMethod('_clearVMTimeline'));
+      return response as Success;
+    }
     return _trackFuture('clearVMTimeline', _vmService.clearVMTimeline());
   }
 
   @override
-  Future<Success> collectAllGarbage(String isolateId) {
-    return _trackFuture(
-        'collectAllGarbage', _vmService.collectAllGarbage(isolateId));
-  }
-
-  @override
   void dispose() => _vmService.dispose();
 
   @override
@@ -156,17 +153,27 @@
   @override
   Future<AllocationProfile> getAllocationProfile(
     String isolateId, {
-    String gc,
     bool reset,
-  }) {
+    bool gc,
+  }) async {
+    if (await isProtocolVersionLessThan(major: 3, minor: 18)) {
+      final Map<String, dynamic> args = {};
+      if (gc != null && gc) {
+        args['gc'] = 'full';
+      }
+      if (reset != null && reset) {
+        args['reset'] = reset;
+      }
+      final response = await _trackFuture(
+        'getAllocationProfile',
+        callMethod('_getAllocationProfile', isolateId: isolateId, args: args),
+      );
+      return AllocationProfile.parse(response.json);
+    }
     return _trackFuture(
-        'getAllocationProfile', _vmService.getAllocationProfile(isolateId));
-  }
-
-  @override
-  Future<CpuProfile> getCpuProfile(String isolateId, String tags) {
-    return _trackFuture(
-        'getCpuProfile', _vmService.getCpuProfile(isolateId, tags));
+      'getAllocationProfile',
+      _vmService.getAllocationProfile(isolateId, reset: reset, gc: gc),
+    );
   }
 
   // TODO(kenzie): keep track of all private methods we are currently using to
@@ -191,9 +198,27 @@
       _trackFuture('getFlagList', _vmService.getFlagList());
 
   @override
-  Future<ObjRef> getInstances(String isolateId, String classId, int limit) {
+  Future<InstanceSet> getInstances(
+    String isolateId,
+    String objectId,
+    int limit, {
+    String classId,
+  }) async {
+    if (await isProtocolVersionLessThan(major: 3, minor: 20)) {
+      final response = await _trackFuture(
+        'getInstances',
+        callMethod('_getInstances', args: {
+          'isolateId': isolateId,
+          'classId': classId,
+          'limit': limit,
+        }),
+      );
+      return InstanceSet.parse(response.json);
+    }
     return _trackFuture(
-        'getInstances', _vmService.getInstances(isolateId, classId, limit));
+      'getInstances',
+      _vmService.getInstances(isolateId, objectId, limit),
+    );
   }
 
   @override
@@ -246,8 +271,32 @@
   Future<VM> getVM() => _trackFuture('getVM', _vmService.getVM());
 
   @override
-  Future<Response> getVMTimeline() =>
-      _trackFuture('getVMTimeline', _vmService.getVMTimeline());
+  Future<Timeline> getVMTimeline({
+    int timeOriginMicros,
+    int timeExtentMicros,
+  }) async {
+    if (await isProtocolVersionLessThan(major: 3, minor: 19)) {
+      final Response response =
+          await _trackFuture('getVMTimeline', callMethod('_getVMTimeline'));
+      return Timeline.parse(response.json);
+    }
+    return _trackFuture(
+      'getVMTimeline',
+      _vmService.getVMTimeline(
+        timeOriginMicros: timeOriginMicros,
+        timeExtentMicros: timeExtentMicros,
+      ),
+    );
+  }
+
+  @override
+  Future<TimelineFlags> getVMTimelineFlags() {
+    return _trackFuture('getVMTimelineFlags', _vmService.getVMTimelineFlags());
+  }
+
+  @override
+  Future<Timestamp> getVMTimelineMicros() =>
+      _trackFuture('getVMTimelineMicros', _vmService.getVMTimelineMicros());
 
   @override
   Future<Version> getVersion() =>
@@ -277,6 +326,14 @@
   }
 
   @override
+  Future<Success> requestHeapSnapshot(String isolateId) {
+    return _trackFuture(
+      'requestHeapSnapshot',
+      _vmService.requestHeapSnapshot(isolateId),
+    );
+  }
+
+  @override
   Future<Success> kill(String isolateId) {
     return _trackFuture('kill', _vmService.kill(isolateId));
   }
@@ -309,6 +366,9 @@
   Stream<String> get onSend => _vmService.onSend;
 
   @override
+  Stream<Event> get onServiceEvent => _vmService.onServiceEvent;
+
+  @override
   Stream<Event> get onStderrEvent => _vmService.onStderrEvent;
 
   @override
@@ -318,14 +378,35 @@
   Stream<Event> get onVMEvent => _vmService.onVMEvent;
 
   @override
+  Stream<Event> get onHeapSnapshotEvent => _vmService.onHeapSnapshotEvent;
+
+  @override
   Future<Success> pause(String isolateId) {
     return _trackFuture('pause', _vmService.pause(isolateId));
   }
 
   @override
-  Future<Success> registerService(String service, String alias) {
-    return _trackFuture(
-        'registerService $service', _vmService.registerService(service, alias));
+  Future<Success> registerService(String service, String alias) async {
+    // Handle registerService method name change based on protocol version.
+    final registerServiceMethodName =
+        await isProtocolVersionLessThan(major: 3, minor: 22)
+            ? '_registerService'
+            : 'registerService';
+
+    final response = await _trackFuture(
+      '$registerServiceMethodName $service',
+      callMethod(registerServiceMethodName,
+          args: {'service': service, 'alias': alias}),
+    );
+
+    return response as Success;
+
+    // TODO(dantup): When we no longer need to support clients on older VMs
+    // that don't support public registerService (added in July 2019, VM service
+    // v3.22) we can replace the above with a direct call to vm_service_lib's
+    // registerService (as long as we're pinned to version >= 3.22.0).
+    // return _trackFuture(
+    //     'registerService $service', _vmService.registerService(service, alias));
   }
 
   @override
@@ -359,16 +440,6 @@
   }
 
   @override
-  Future<Success> requestHeapSnapshot(
-    String isolateId,
-    String roots,
-    bool collectGarbage,
-  ) {
-    return _trackFuture('requestHeapSnapshot',
-        _vmService.requestHeapSnapshot(isolateId, roots, collectGarbage));
-  }
-
-  @override
   Future<Success> resume(String isolateId, {String step, int frameIndex}) {
     return _trackFuture('resume',
         _vmService.resume(isolateId, step: step, frameIndex: frameIndex));
@@ -406,9 +477,20 @@
   }
 
   @override
-  Future<Success> setVMTimelineFlags(List<String> recordedStreams) {
+  Future<Success> setVMTimelineFlags(List<String> recordedStreams) async {
+    if (await isProtocolVersionLessThan(major: 3, minor: 19)) {
+      final response = await _trackFuture(
+          'setVMTimelineFlags',
+          callMethod(
+            '_setVMTimelineFlags',
+            args: {'recordedStreams': recordedStreams},
+          ));
+      return response as Success;
+    }
     return _trackFuture(
-        'setVMTimelineFlags', _vmService.setVMTimelineFlags(recordedStreams));
+      'setVMTimelineFlags',
+      _vmService.setVMTimelineFlags(recordedStreams),
+    );
   }
 
   @override
@@ -445,6 +527,31 @@
     activeFutures.clear();
   }
 
+  Future<bool> isProtocolVersionLessThan({
+    @required int major,
+    @required int minor,
+  }) async {
+    _protocolVersion ??= await getVersion();
+    return protocolVersionLessThan(major: major, minor: minor);
+  }
+
+  bool protocolVersionLessThan({
+    @required int major,
+    @required int minor,
+  }) {
+    assert(_protocolVersion != null);
+    return _protocolVersion.major < major ||
+        (_protocolVersion.major == major && _protocolVersion.minor < minor);
+  }
+
+  /// Gets the name of the service stream for the connected VM service. Pre-v3.22
+  /// this was a private API and named _Service and in v3.22 (July 2019) it was
+  /// made public ("Service").
+  Future<String> get serviceStreamName async =>
+      (await isProtocolVersionLessThan(major: 3, minor: 22))
+          ? '_Service'
+          : 'Service';
+
   Future<T> _trackFuture<T>(String name, Future<T> future) {
     if (!trackFutures) {
       return future;
@@ -468,6 +575,30 @@
     );
     return future;
   }
+
+  @override
+  Future getInboundReferences(
+      String isolateId, String targetId, int limit) async {
+    Future future;
+
+    if (await isProtocolVersionLessThan(major: 3, minor: 25)) {
+      future = _vmService.callMethod(
+        '_getInboundReferences',
+        isolateId: isolateId,
+        args: {'targetId': targetId, 'limit': limit},
+      );
+    } else {
+      future = _vmService.getInboundReferences(isolateId, targetId, limit);
+    }
+
+    return _trackFuture('getInboundReferences', future);
+  }
+
+  @override
+  Future<RetainingPath> getRetainingPath(
+          String isolateId, String targetId, int limit) =>
+      _trackFuture('getRetainingPath',
+          _vmService.getRetainingPath(isolateId, targetId, limit));
 }
 
 class TrackedFuture<T> {
diff --git a/devtools/pubspec.yaml b/devtools/pubspec.yaml
index e222963..06ba39e 100644
--- a/devtools/pubspec.yaml
+++ b/devtools/pubspec.yaml
@@ -3,18 +3,18 @@
 
 # Note: when updating this version, please update the corresponding entry in
 # lib/devtools.dart.
-version: 0.1.1
+version: 0.1.6-dev.3
 
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/flutter/devtools
 
 environment:
-  sdk: '>=2.1.2-dev.0.0 <3.0.0'
+  sdk: '>=2.3.0 <3.0.0'
 
 dependencies:
   codemirror: ^0.5.3
   collection: ^1.14.11
-  devtools_server: 0.1.2
+  devtools_server: 0.1.6
 #    path: ../devtools_server
   http: ^0.12.0+1
   intl: ^0.15.0
@@ -23,8 +23,7 @@
   path: ^1.6.0
   pedantic: ^1.7.0
   platform_detect: ^1.3.5
-  rxdart: ^0.21.0
-  vm_service_lib: ^3.17.0+1
+  vm_service: 1.1.1
   # We would use local dependencies for these packages if pub publish allowed it.
   octicons_css:
     ^0.0.1
@@ -33,22 +32,24 @@
     ^0.0.1
 #    path: ../../third_party/packages/polymer_css
   primer_css:
-     ^0.0.2
+    ^0.0.2
 #    path: ../../third_party/packages/primer_css
   split:
-    ^0.0.2
+    0.0.5
 #    path: ../../third_party/packages/split
   plotly_js:
     ^0.0.1
 #    path: ../../third_party/packages/plotly
+  sse: ^2.0.0
 
 dev_dependencies:
+  mockito: ^4.0.0
   build_runner: ^1.3.0
   build_test: ^0.10.0
-  build_web_compilers: ^1.2.0
+  build_web_compilers: '>=1.2.0 <3.0.0'
   matcher: ^0.12.3
   test: ^1.0.0
-  webkit_inspection_protocol: ^0.4.0
+  webkit_inspection_protocol: '>=0.4.0 <0.6.0'
 
 executables:
   devtools:
diff --git a/devtools/web/icons/memory/alloc_icon.png b/devtools/web/icons/memory/alloc_icon.png
new file mode 100644
index 0000000..fb67f42
--- /dev/null
+++ b/devtools/web/icons/memory/alloc_icon.png
Binary files differ
diff --git a/devtools/web/icons/memory/alloc_icon@2x.png b/devtools/web/icons/memory/alloc_icon@2x.png
new file mode 100644
index 0000000..920f20c
--- /dev/null
+++ b/devtools/web/icons/memory/alloc_icon@2x.png
Binary files differ
diff --git a/devtools/web/icons/memory/ic_search.png b/devtools/web/icons/memory/ic_search.png
new file mode 100644
index 0000000..1244fe3
--- /dev/null
+++ b/devtools/web/icons/memory/ic_search.png
Binary files differ
diff --git a/devtools/web/icons/memory/ic_search@2x.png b/devtools/web/icons/memory/ic_search@2x.png
new file mode 100644
index 0000000..425d4bc
--- /dev/null
+++ b/devtools/web/icons/memory/ic_search@2x.png
Binary files differ
diff --git a/devtools/web/main.dart b/devtools/web/main.dart
index 9c47471..7b8794a 100644
--- a/devtools/web/main.dart
+++ b/devtools/web/main.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:html';
+
 import 'package:devtools/src/framework/framework_core.dart';
 import 'package:devtools/src/main.dart';
 import 'package:devtools/src/ui/analytics.dart' as ga;
@@ -11,52 +12,90 @@
 import 'package:platform_detect/platform_detect.dart';
 
 void main() {
-  // Need to catch all Dart exceptions - done via an isolate.
-  runZoned(() {
-    // Initialize the core framework.
-    FrameworkCore.init();
+  // Run in a zone in order to catch all Dart exceptions.
+  runZoned(
+    () {
+      // Initialize the core framework.
+      FrameworkCore.init();
 
-    // Load the web app framework.
-    final PerfToolFramework framework = PerfToolFramework();
+      // Load the web app framework.
+      final PerfToolFramework framework = PerfToolFramework();
 
-    // Show the opt-in dialog for collection analytics?
-    if (ga.isGtagsEnabled() &
-        (!window.localStorage.containsKey(ga_platform.devToolsProperty()) ||
-            window.localStorage[ga_platform.devToolsProperty()].isEmpty)) {
-      framework.showAnalyticsDialog();
-    }
-
-    if (!browser.isChrome) {
-      final browserName =
-          // Edge shows up as IE, so we replace it's name to avoid confusion.
-          browser.isInternetExplorer || browser == Browser.UnknownBrowser
-              ? 'an unsupported browser'
-              : browser.name;
-      framework.disableAppWithError(
-        'ERROR: You are running DevTools on $browserName, '
-            'but DevTools only runs on Chrome.',
-        'Reopen this url in a Chrome browser to use DevTools.',
-      );
-      return;
-    }
-
-    FrameworkCore.initVmService(errorReporter: (String title, dynamic error) {
-      framework.showError(title, error);
-    }).then((bool connected) {
-      if (!connected) {
-        framework.showConnectionDialog();
-        framework.showSnapshotMessage();
-        // Clear the main element so it stops displaying "Loading..."
-        // TODO(jacobr): display a message explaining how to launch a Flutter
-        // application from the command line and connect to it with DevTools.
-        framework.mainElement.clear();
+      // Show the opt-in dialog for collection analytics?
+      try {
+        if (ga.isGtagsEnabled() &
+            (!window.localStorage.containsKey(ga_platform.devToolsProperty()) ||
+                window.localStorage[ga_platform.devToolsProperty()].isEmpty)) {
+          framework.showAnalyticsDialog();
+        }
+      } catch (e) {
+        // If there are errors setting up analytics, write them to the console
+        // but do not prevent DevTools from loading.
+        window.console.error(e);
       }
-    });
 
-    framework.loadScreenFromLocation();
-  }, onError: (error, stack) {
-    // Report exceptions with DevTools to GA, any user's Flutter app exceptions
-    // are not collected.
-    ga.error('${error.toString()}\n$stack.toString()', true);
-  });
+      if (!browser.isChrome) {
+        final browserName =
+            // Edge shows up as IE, so we replace it's name to avoid confusion.
+            browser.isInternetExplorer || browser == Browser.UnknownBrowser
+                ? 'an unsupported browser'
+                : browser.name;
+        framework.disableAppWithError(
+          'ERROR: You are running DevTools on $browserName, '
+              'but DevTools only runs on Chrome.',
+          'Reopen this url in a Chrome browser to use DevTools.',
+        );
+        return;
+      }
+
+      FrameworkCore.initVmService(errorReporter: (String title, dynamic error) {
+        framework.showError(title, error);
+      }).then((bool connected) {
+        if (!connected) {
+          framework.showConnectionDialog();
+          framework.showSnapshotMessage();
+          // Clear the main element so it stops displaying "Loading..."
+          // TODO(jacobr): display a message explaining how to launch a Flutter
+          // application from the command line and connect to it with DevTools.
+          framework.mainElement.clear();
+        }
+      });
+
+      framework.loadScreenFromLocation();
+    },
+    zoneSpecification: const ZoneSpecification(
+      handleUncaughtError: _handleUncaughtError,
+    ),
+  );
+}
+
+void _handleUncaughtError(
+  Zone self,
+  ZoneDelegate parent,
+  Zone zone,
+  Object error,
+  StackTrace stackTrace,
+) {
+  // TODO(devoncarew): `stackTrace` always seems to be null.
+
+  // Report exceptions with DevTools to GA; user's Flutter app exceptions are
+  // not collected.
+  ga.error('$error\n${stackTrace ?? ''}'.trim(), true);
+
+  final Console console = window.console;
+
+  // Also write them to the console to aid debugging.
+  final errorLines = error.toString().split('\n');
+  console.groupCollapsed(
+      'DevTools exception: [${error.runtimeType}] ${errorLines.first}');
+  console.log(errorLines.skip(1).join('\n'));
+
+  if (stackTrace != null) {
+    if (errorLines.length > 1) {
+      console.log('\n');
+    }
+    console.log(stackTrace.toString().trim());
+  }
+
+  console.groupEnd();
 }
diff --git a/devtools/web/third_party/split/LICENSE b/devtools/web/third_party/split/LICENSE
deleted file mode 100644
index b01128a..0000000
--- a/devtools/web/third_party/split/LICENSE
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (c) 2018 Nathan Cahill
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/devtools/web/third_party/split/README.md b/devtools/web/third_party/split/README.md
deleted file mode 100644
index ca60c28..0000000
--- a/devtools/web/third_party/split/README.md
+++ /dev/null
@@ -1,594 +0,0 @@
-<p align="center">
-<img alt="Split.js" title="Split.js" src="logo.svg" width="430">
-<br><br>
-<a href="https://circleci.com/gh/nathancahill/split"><img src="https://img.shields.io/circleci/project/github/nathancahill/split/master.svg" alt="Build Status"></a>
-<img src="https://img.badgesize.io/https://unpkg.com/split.js/dist/split.min.js?compression=gzip&label=size&v=1.5.9" alt="File Size">
-<img src="https://badge.fury.io/js/split.js.svg" alt="npm version">
-<img src="https://david-dm.org/nathancahill/split/status.svg" alt="Dependencies">
-<img src = "https://opencollective.com/splitjs/backers/badge.svg" alt="Backers on Open Collective"/>
-<img src = "https://opencollective.com/splitjs/sponsors/badge.svg" alt="Sponsors on Open Collective"/>
-</p>
-
-# Split.js
-
-> 2kb unopinionated utility for resizeable split views.
-
--   **Zero Deps**
--   **Tiny:** Weights 2kb gzipped.
--   **Fast:** No overhead or attached window event listeners, uses pure CSS for resizing.
--   **Unopinionated:** Plays nicely with `float`, `flex` and other layouts.
--   **Compatible:** Works great in IE9, and _even loads in IE8_ with polyfills. Early Firefox/Chrome/Safari/Opera supported too.
-
-## Table of Contents
-
--   [Installation](#installation)
--   [Documentation](#documentation)
--   [Important Note](#important-note)
--   [Options](#options)
--   [Examples](#usage-examples)
--   [Saving State](#saving-state)
--   [Flexbox](#flexbox)
--   [API](#api)
--   [CSS](#css)
--   [React](#react)
--   [Browser Support](#browser-support)
--   [Credits](#credits)
--   [License](#license)
-
-## Installation
-
-Yarn:
-
-```bash
-$ yarn add split.js
-```
-
-npm:
-
-```bash
-$ npm install --save split.js
-```
-
-Include with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.github.io/):
-
-```js
-// using ES6 modules
-import Split from 'split.js'
-
-// using CommonJS modules
-var Split = require('split.js')
-```
-
-The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](http://unpkg.com/):
-
-```html
-<script src="https://unpkg.com/split.js/dist/split.min.js"></script>
-```
-
-or [cdnjs](https://cdnjs.com/):
-
-```html
-<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.5.9/split.min.js"></script>
-```
-
-You can find the library on `window.Split`.
-
-## Documentation
-
-```js
-var split = Split(<HTMLElement|selector[]> elements, <options> options?)
-```
-
-| Options        | Type            | Default        | Description                                              |
-| -------------- | --------------- | -------------- | -------------------------------------------------------- |
-| `sizes`        | Array           |                | Initial sizes of each element in percents or CSS values. |
-| `minSize`      | Number or Array | `100`          | Minimum size of each element.                            |
-| `expandToMin`  | Boolean         | `false`        | Grow initial sizes to `minSize`                          |
-| `gutterSize`   | Number          | `10`           | Gutter size in pixels.                                   |
-| `gutterAlign`  | String          | `'center'`     | Gutter alignment between elements.                       |
-| `snapOffset`   | Number          | `30`           | Snap to minimum size offset in pixels.                   |
-| `dragInterval` | Number          | `1`            | Number of pixels to drag.                                |
-| `direction`    | String          | `'horizontal'` | Direction to split: horizontal or vertical.              |
-| `cursor`       | String          | `'col-resize'` | Cursor to display while dragging.                        |
-| `gutter`       | Function        |                | Called to create each gutter element                     |
-| `elementStyle` | Function        |                | Called to set the style of each element.                 |
-| `gutterStyle`  | Function        |                | Called to set the style of the gutter.                   |
-| `onDrag`       | Function        |                | Callback on drag.                                        |
-| `onDragStart`  | Function        |                | Callback on drag start.                                  |
-| `onDragEnd`    | Function        |                | Callback on drag end.                                    |
-
-## Important Note
-
-Split.js does not set CSS beyond the minimum needed to manage the width or height of the elements.
-This is by design. It makes Split.js flexible and useful in many different situations.
-If you create a horizontal split, you are responsible for (likely) floating the elements and the gutter,
-and setting their heights. See the [CSS](#css) section below. If your gutters are not showing up, check the applied CSS styles.
-
-**THIS IS THE #1 QUESTION ABOUT THE LIBRARY**.
-
-## Options
-
-#### sizes
-
-An array of initial sizes of the elements, specified as percentage values. Example: Setting the initial sizes to `25%` and `75%`.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-})
-```
-
-#### minSize. Default: `100`
-
-An array of minimum sizes of the elements, specified as pixel values. Example: Setting the minimum sizes to `100px` and `300px`, respectively.
-
-```js
-Split(['#one', '#two'], {
-    minSize: [100, 300],
-})
-```
-
-If a number is passed instead of an array, all elements are set to the same minimum size:
-
-```js
-Split(['#one', '#two'], {
-    minSize: 100,
-})
-```
-
-#### expandToMin. Default: `false`
-
-When the split is created, if `expandToMin` is `true`, the minSize for each element overrides the percentage value from the `sizes` option.
-Example: The first element (`#one`) is set to 25% width of the parent container. However, it's `minSize` is `300px`. Using `expandToMin: true` means that
-the first element will always load at at least `300px`, even if `25%` were smaller.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-    minSize: [300, 100],
-    expanedToMin: true,
-})
-```
-
-#### gutterSize. Default: `10`
-
-Gutter size in pixels. Example: Setting the gutter size to `20px`.
-
-```js
-Split(['#one', '#two'], {
-    gutterSize: 20,
-})
-```
-
-#### gutterAlign. Default: `'center'`
-
-Possible options are `'start'`, `'end'` and `'center'`. Determines how the gutter aligns between the two elements.
-`'start'` shrinks the first element to fit the gutter, `'end'` shrinks the second element to fit the gutter and `'center'` shrinks both
-elements by the same amount so the gutter sits between. Added in v1.5.3.
-
-Example: move gutter to the side of the second element:
-
-```js
-Split(['#one', '#two'], {
-    gutterAlign: 'end',
-})
-```
-
-#### snapOffset. Default: `30`
-
-Snap to minimum size at this offset in pixels. Example: Set to `0` to disable to snap effect.
-
-```js
-Split(['#one', '#two'], {
-    snapOffset: 0,
-})
-```
-
-#### dragInterval. Default: `1`
-
-Drag this number of pixels at a time. Defaults to `1` for smooth dragging, but can be set to a pixel value to
-give more control over the resulting sizes. Works particularly well when the `gutterSize` is set to the same size.
-Added in v1.5.3. Example: Drag 20px at a time:
-
-```js
-Split(['#one', '#two'], {
-    dragInterval: 20,
-})
-```
-
-#### direction. Default: `'horizontal'`
-
-Direction to split in. Can be `'vertical'` or `'horizontal'`. Determines which CSS properties are applied (ie. width/height) to each element and gutter. Example: split vertically:
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-})
-```
-
-#### cursor. Default: `'col-resize'`
-
-Cursor to show on the gutter (also applied to the body on dragging to prevent flickering). Defaults to `'col-resize'`for `direction: 'horizontal'` and `'row-resize'` for `direction: 'vertical'`:
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-    cursor: 'row-resize',
-})
-```
-
-#### gutter
-
-Optional function called to create each gutter element. The signature looks like this:
-
-```js
-;(index, direction, pairElement) => HTMLElement
-```
-
-Defaults to creating a `div` with `class="gutter gutter-horizontal"` or `class="gutter gutter-vertical"`, depending on the direction. The default gutter function looks like this:
-
-```js
-;(index, direction) => {
-    const gutter = document.createElement('div')
-    gutter.className = `gutter gutter-${direction}`
-    return gutter
-}
-```
-
-The returned element is then inserted into the DOM, and it's width or height are set. This option can be used to clone an existing DOM element, or to create a new element with custom styles.
-
-Returning a falsey value like `null` or `false` will not insert a gutter. This behavior was added in v1.4.1.
-An additional argument, `pairElement`, is passed to the gutter function: this is the DOM element after (to the right or below) the gutter. This argument was added in v1.4.1.
-
-This final argument makes it easy to return the gutter that has already been created, for example, if `split.destroy()` was called with the option to preserve the gutters.
-
-```js
-;(index, direction, pairElement) => pairElement.previousSibling
-```
-
-#### elementStyle
-
-Optional function called setting the CSS style of the elements. The signature looks like this:
-
-```js
-;(dimension, elementSize, gutterSize, index) => Object
-```
-
-Dimension will be a string, `'width'` or `'height'`, and can be used in the return style. `elementSize` is the target percentage value of the element, and `gutterSize` is the target pixel value of the gutter.
-
-It should return an object with CSS properties to apply to the element. For horizontal splits, the return object looks like this:
-
-```js
-{
-    'width': 'calc(50% - 5px)'
-}
-```
-
-A vertical split style would look like this:
-
-```js
-{
-    'height': 'calc(50% - 5px)'
-}
-```
-
-You might use this function if you're using a different layout like flex (see [Flex Layout](#flex-layout)).
-Flex styles for a horizontal split could return an object like this:
-
-```js
-{
-    'flex-basis': 'calc(50% - 5px)'
-}
-```
-
-#### gutterStyle
-
-Optional function called when setting the CSS style of the gutters. The signature looks like this:
-
-```js
-;(dimension, gutterSize, index) => Object
-```
-
-Dimension is a string, either `'width'` or `'height'`, and `gutterSize` is a pixel value representing the width of the gutter.
-
-It should return a similar object as `elementStyle`, an object with CSS properties to apply to the gutter. Since gutters have fixed widths, it will generally look like this:
-
-```js
-{
-    'width': '10px'
-}
-```
-
-Both `elementStyle` and `gutterStyle` are called continously while dragging, so don't do anything besides return the style object in these functions. Both of these functions should be _pure_, returning the same values for the same inputs and not modifying any external state.
-
-#### onDrag, onDragStart, onDragEnd
-
-Callbacks that can be added on drag (fired continously), drag start and drag end. If doing more than basic operations in `onDrag`, add a debounce function to rate limit the callback.
-
-`onDragStart` and `onDragEnd` are passed the initial and final sizes of the split since it's a common pattern to access the sizes this way.
-
-Their function signature looks like this, where `sizes` is an array of percentage values like returned by `getSizes()`:
-
-```js
-sizes => {}
-```
-
-## Usage Examples
-
-Reference HTML for examples. Gutters are inserted automatically:
-
-```html
-<div>
-    <div id="one">content one</div>
-    <div id="two">content two</div>
-    <div id="three">content three</div>
-</div>
-```
-
-A split with two elements, starting at `25%` and `75%` wide, with `200px` minimum width.
-
-```js
-Split(['#one', '#two'], {
-    sizes: [25, 75],
-    minSize: 200,
-})
-```
-
-A split with three elements, starting with even (default) widths and minimum widths set to `100px`, `100px` and `300px`, respectively.
-
-```js
-Split(['#one', '#two', '#three'], {
-    minSize: [100, 100, 300],
-})
-```
-
-A vertical split with two elements.
-
-```js
-Split(['#one', '#two'], {
-    direction: 'vertical',
-})
-```
-
-## Saving State
-
-Use local storage to save the most recent state:
-
-```js
-var sizes = localStorage.getItem('split-sizes')
-
-if (sizes) {
-    sizes = JSON.parse(sizes)
-} else {
-    sizes = [50, 50] // default sizes
-}
-
-var split = Split(['#one', '#two'], {
-    sizes: sizes,
-    onDragEnd: function(sizes) {
-        localStorage.setItem('split-sizes', JSON.stringify(sizes))
-    },
-})
-```
-
-## Flex Layout
-
-Flex layout is supported easily by adding a `display: flex` to the parent element. The `width` or `height` CSS values
-assigned by default by Split.js work well with flex.
-
-```html
-<div id="flex">
-    <div id="flex-1"></div>
-    <div id="flex-2"></div>
-</div>
-```
-
-And CSS style like this:
-
-```css
-#flex {
-    display: flex;
-    flex-direction: row;
-}
-```
-
-For more complicated flex layouts, the `elementStyle` and `gutterStyle` can be used to set flex-basis:
-
-```js
-Split(['#flex-1', '#flex-2'], {
-    elementStyle: function(dimension, size, gutterSize) {
-        return {
-            'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)',
-        }
-    },
-    gutterStyle: function(dimension, gutterSize) {
-        return {
-            'flex-basis': gutterSize + 'px',
-        }
-    },
-})
-```
-
-## API
-
-Split.js returns an instance with a couple of functions. The instance is returned on creation:
-
-```js
-var instance = Split([], ...)
-```
-
-#### `.setSizes([])`
-
-setSizes behaves the same as the `sizes` configuration option, passing an array of percentages. It updates the sizes of the elements in the split. Added in v1.1.0:
-
-```js
-instance.setSizes([25, 75])
-```
-
-#### `.getSizes()`
-
-getSizes returns an array of percents, suitable for using with `setSizes` or creation. Not supported in IE8. Added in v1.1.2:
-
-```js
-instance.getSizes() > [25, 75]
-```
-
-#### `.collapse(index)`
-
-collapse changes the size of element at `index` to it's `minSize`. Every element except the last is collapsed towards the front (left or top). The last is collapsed towards the back. Not supported in IE8. Added in v1.1.0:
-
-```js
-instance.collapse(0)
-```
-
-#### `.destroy(preserveStyles? = false, preserveGutters? = false)`
-
-Destroy the instance. It removes the gutter elements, and the size CSS styles Split.js set. Added in v1.1.1.
-Passing `preserveStyles = true` does not remove the CSS styles. Option added in v1.4.0.
-Passing `preserveGutters = true` does not remove the gutter elements. Option added in v1.4.1.
-
-```js
-instance.destroy()
-```
-
-## CSS
-
-In being non-opionionated, the only CSS Split.js sets is the widths or heights of the elements. Everything else is left up to you. You must set the elements and gutter heights when using horizontal mode. The gutters will not be visible if their height is 0px. Here's some basic CSS to style the gutters with, although it's not required. Both grip images are included in this repo:
-
-```css
-.gutter {
-    background-color: #eee;
-
-    background-repeat: no-repeat;
-    background-position: 50%;
-}
-
-.gutter.gutter-horizontal {
-    background-image: url('grips/vertical.png');
-    cursor: col-resize;
-}
-
-.gutter.gutter-vertical {
-    background-image: url('grips/horizontal.png');
-    cursor: row-resize;
-}
-```
-
-The grip images are small files and can be included with base64 instead:
-
-```css
-.gutter.gutter-vertical {
-    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
-}
-
-.gutter.gutter-horizontal {
-    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
-}
-```
-
-Split.js also works best when the elements are sized using `border-box`. The `split` class would have to be added manually to apply these styles:
-
-```css
-.split {
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box;
-}
-```
-
-And for horizontal splits, make sure the layout allows elements (including gutters) to be displayed side-by-side. Floating the elements is one option:
-
-```css
-.split,
-.gutter.gutter-horizontal {
-    float: left;
-}
-```
-
-If you use floats, set the height of the elements including the gutters. The gutters will not be visible otherwise if the height is set to 0px.
-
-```css
-.split,
-.gutter.gutter-horizontal {
-    height: 300px;
-}
-```
-
-Overflow can be handled as well, to get scrolling within the elements:
-
-```css
-.split {
-    overflow-y: auto;
-    overflow-x: hidden;
-}
-```
-
-## Browser Support
-
-This library uses [CSS calc()](https://developer.mozilla.org/en-US/docs/Web/CSS/calc#AutoCompatibilityTable), [CSS box-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing#AutoCompatibilityTable) and [JS getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#AutoCompatibilityTable). These features are supported in the following browsers:
-
-| <img src="http://i.imgur.com/dJC1GUv.png" width="48px" height="48px" alt="Chrome logo"> | <img src="http://i.imgur.com/o1m5RcQ.png" width="48px" height="48px" alt="Firefox logo"> | <img src="http://i.imgur.com/8h3iz5H.png" width="48px" height="48px" alt="Internet Explorer logo"> | <img src="http://i.imgur.com/iQV4nmJ.png" width="48px" height="48px" alt="Opera logo"> | <img src="http://i.imgur.com/j3tgNKJ.png" width="48px" height="48px" alt="Safari logo"> | [<img src="https://i.imgur.com/29eVTCg.png" height="28px" alt="Sauce Labs">](https://saucelabs.com) |
-| :-------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------- |
-|                                          22+ ✔                                          |                                           6+ ✔                                           |                                                9+ ✔                                                |                                         15+ ✔                                          |                                         6.2+ ✔                                          | Sponsored ✔                                                                                         |
-
-Gracefully falls back in IE 8 and below to only setting the initial widths/heights and not allowing dragging. IE 8 requires polyfills for `Array.isArray()`, `Array.forEach`, `Array.map`, `Array.filter`, `Object.keys()` and `getComputedStyle`. This script from [Polyfill.io](https://polyfill.io/) includes all of these, adding 1.91 kb to the gzipped size.
-
-This is **ONLY NEEDED** if you are supporting **IE8:**
-
-```html
-<script src="///polyfill.io/v2/polyfill.min.js?features=Array.isArray,Array.prototype.forEach,Array.prototype.map,Object.keys,Array.prototype.filter,getComputedStyle"></script>
-```
-
-Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com).
-
-## Credits
-
-### Contributors
-
-This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
-<a href="graphs/contributors"><img src="https://opencollective.com/splitjs/contributors.svg?width=890&button=false" /></a>
-
-### Backers
-
-Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/splitjs#backer)]
-
-<a href="https://opencollective.com/splitjs#backers" target="_blank"><img src="https://opencollective.com/splitjs/backers.svg?width=890"></a>
-
-### Sponsors
-
-Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/splitjs#sponsor)]
-
-[<img src="https://i.imgur.com/29eVTCg.png" height="28px" alt="Sauce Labs">](https://saucelabs.com)
-
-<a href="https://opencollective.com/splitjs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/0/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/1/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/1/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/2/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/2/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/3/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/3/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/4/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/4/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/5/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/5/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/6/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/6/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/7/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/7/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/8/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/8/avatar.svg"></a>
-<a href="https://opencollective.com/splitjs/sponsor/9/website" target="_blank"><img src="https://opencollective.com/splitjs/sponsor/9/avatar.svg"></a>
-
-## License
-
-Copyright (c) 2018 Nathan Cahill
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/devtools/web/widgets.json b/devtools/web/widgets.json
index 4f798fb..6e8f09a 100644
--- a/devtools/web/widgets.json
+++ b/devtools/web/widgets.json
@@ -8,7 +8,7 @@
       "Single-child layout widgets"
     ],
     "description": "A convenience widget that combines common painting, positioning, and sizing widgets.",
-    "link": "https://docs.flutter.io/flutter/widgets/Container-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Container-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-container-1' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker><marker id='arrow-container-2' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker><filter id='shadow-container' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur stdDeviation='4'/></filter><linearGradient id='gradient-container' x1='0' y1='0.2' x2='0.4' y2='0.9'><stop offset='55%' stop-color='#ffffff'/><stop offset='100%' stop-color='#fdccdd'/></linearGradient></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='15' y='25' width='70' height='47.5' rx='10' ry='10' fill='#000000' filter='url(#shadow-container)'/><rect x='15' y='25' width='70' height='47.5' rx='10' ry='10' fill='url(#gradient-container)' stroke-width='5' stroke='#3b75ad'/><rect x='30' y='40' width='40' height='30' fill='#4dd0e1'/><line x1='20' y1='55' x2='27' y2='55' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='73' y1='55' x2='80' y2='55' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='50' y1='30' x2='50' y2='37' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='50' y1='78' x2='50' y2='82' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-container-1)' marker-end='url(#arrow-container-1)'/><line x1='16' y1='17.5' x2='85' y2='17.5' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-container-2)' marker-end='url(#arrow-container-2)'/><line x1='7.5' y1='26' x2='7.5' y2='72' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-container-2)' marker-end='url(#arrow-container-2)'/></svg>"
   },
   {
@@ -20,7 +20,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Row-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Row-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='30' width='80' height='40' fill='#ffffff'/><rect x='15' y='40' width='20' height='20' fill='#4dd0e1'/><rect x='40' y='35' width='30' height='30' fill='#4dd0e1'/></svg>"
   },
   {
@@ -32,7 +32,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Column-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Column-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='40' y='15' width='20' height='20' fill='#4dd0e1'/><rect x='35' y='40' width='30' height='30' fill='#4dd0e1'/></svg>"
   },
   {
@@ -45,7 +45,7 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Image-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Image-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='60' fill='#ffffff'/><image x='22.5' y='22.5' width='55' height='55' xlink:href='/images/owl.jpg'/></svg>"
   },
   {
@@ -56,12 +56,12 @@
       "Text"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/widgets/Text-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Text-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='30' width='60' height='40' fill='#ffffff'/><text x='50' y='60' text-anchor='middle' font-family='Roboto' font-size='25' fill='#3b75ad'>Abc</text></svg>"
   },
   {
     "name": "Icon",
-    "description": "A material design icon.",
+    "description": "A Material Design icon.",
     "categories": [
       "Basics",
       "Assets, Images, and Icons"
@@ -69,45 +69,45 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Icon-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/Icon-class.html",
+    "image": "<img alt='' src='https://flutter.github.io/assets-for-api-docs/assets/widgets/icon.png'>"
   },
   {
     "name": "RaisedButton",
-    "description": "A material design raised button. A raised button consists of a rectangular piece of material that hovers over the interface.",
+    "description": "A Material Design raised button. A raised button consists of a rectangular piece of material that hovers over the interface.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/RaisedButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/RaisedButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VbDh6YmNiYVc3SHM/components_buttons_usage2.png'>"
   },
   {
     "name": "Scaffold",
-    "sample": "Scaffold_index",
-    "description": "Implements the basic material design visual layout structure. This class provides APIs for showing drawers, snack bars, and bottom sheets.",
+    "sample": "Scaffold",
+    "description": "Implements the basic Material Design visual layout structure. This class provides APIs for showing drawers, snack bars, and bottom sheets.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Scaffold-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Scaffold-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0Bx4BSt6jniD7T0hfM01sSmRyTG8/layout_structure_regions_mobile.png'>"
   },
   {
     "name": "Appbar",
-    "sample": "AppBar_index",
-    "description": "A material design app bar. An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar.",
+    "sample": "AppBar",
+    "description": "A Material Design app bar. An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar.",
     "categories": [
       "Basics"
     ],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/AppBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/AppBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VclpfSFpuelBGR1k/components_toolbars.png'>"
   },
   {
@@ -117,7 +117,7 @@
       "Basics"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/material/FlutterLogo-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FlutterLogo-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -129,7 +129,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Stack-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Stack-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='10' width='80' height='80' fill='#ffffff'/><rect x='25' y='25' width='40' height='30' fill='#3b75ad'/><rect x='45' y='35' width='30' height='30' fill='#4dd0e1'/><rect x='40' y='50' width='10' height='10' fill='#f50057'/></svg>"
   },
   {
@@ -139,7 +139,7 @@
       "Basics"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/widgets/Placeholder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Placeholder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -149,39 +149,39 @@
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/BottomNavigationBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWG5nei0wWXpoczA/components_bottom_navigation.png'>"
   },
   {
     "name": "TabBar",
-    "sample": "TabBar_index",
-    "description": "A material design widget that displays a horizontal row of tabs.",
+    "sample": "TabBar",
+    "description": "A Material Design widget that displays a horizontal row of tabs.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TabBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TabBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VaWdBdnhMT3ViXzQ/components_tabs.png'>"
   },
   {
     "name": "TabBarView",
-    "sample": "TabBarView_index",
+    "sample": "TabBarView",
     "description": "A page view that displays the widget which corresponds to the currently selected tab. Typically used in conjunction with a TabBar.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TabBarView-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TabBarView-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VaWdBdnhMT3ViXzQ/components_tabs.png'>"
   },
   {
     "name": "MaterialApp",
-    "description": "A convenience widget that wraps a number of widgets that are commonly required for material design applications.",
+    "description": "A convenience widget that wraps a number of widgets that are commonly required for applications implementing Material Design.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/MaterialApp-class.html",
+    "link": "https://api.flutter.dev/flutter/material/MaterialApp-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0Bx4BSt6jniD7Y1huOXVQdlFPMmM/materialdesign_introduction.png'>"
   },
   {
@@ -191,17 +191,17 @@
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/WidgetsApp-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/WidgetsApp-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Drawer",
-    "description": "A material design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application.",
+    "description": "A Material Design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application.",
     "categories": [],
     "subcategories": [
       "App structure and navigation"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Drawer-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Drawer-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VaDhWUXJTTng4ZGs/patterns_navigation_drawer.png'>"
   },
   {
@@ -211,39 +211,49 @@
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/FloatingActionButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FloatingActionButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VN20tOXJoUjVxQjg/components_buttons_fab.png'>"
   },
   {
     "name": "FlatButton",
-    "description": "A flat button is a section printed on a Material widget that reacts to touches by filling with color.",
+    "description": "A flat button is a section printed on a Material Components widget that reacts to touches by filling with color.",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/FlatButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/FlatButton-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VNDg3V3ZjU2hsNGc/components_buttons_usage3.png'>"
   },
   {
     "name": "IconButton",
-    "sample": "IconButton_index",
+    "sample": "IconButton",
     "description": "An icon button is a picture printed on a Material widget that reacts to touches by filling with color (ink).",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/IconButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/IconButton-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B_udO5B8pzrzdXVuTlBoOTBjcU0/components_buttons_other1.png'>"
   },
   {
+    "name": "DropdownButton",
+    "description": "Shows the currently selected item and an arrow that opens a menu for selecting another item.",
+    "categories": [],
+    "subcategories": [
+      "Buttons"
+    ],
+    "link": "https://api.flutter.dev/flutter/material/DropdownButton-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
     "name": "PopupMenuButton",
-    "sample": "PopupMenuButton_index",
+    "sample": "PopupMenuButton",
     "description": "Displays a menu when pressed and calls onSelected when the menu is dismissed because an item was selected.",
     "categories": [],
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/PopupMenuButton-class.html",
+    "link": "https://api.flutter.dev/flutter/material/PopupMenuButton-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_11/assets/0B7WCemMG6e0VakJ6a0F2MFJaaDQ/components_menus.png'>"
   },
   {
@@ -253,7 +263,7 @@
     "subcategories": [
       "Buttons"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ButtonBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ButtonBar-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -263,7 +273,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/TextField-class.html",
+    "link": "https://api.flutter.dev/flutter/material/TextField-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VbURLNTM0N0R6eUE/components_text_fields.png'>"
   },
   {
@@ -273,7 +283,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Checkbox-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Checkbox-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7T2xGbGo0cUlPVG8/components_switches_check1.png'>"
   },
   {
@@ -283,7 +293,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Radio-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Radio-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7Z1NaaXh2ZkpDRkE/components_switches_radio1.png'>"
   },
   {
@@ -293,7 +303,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Switch-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Switch-class.html",
     "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_9/0Bx4BSt6jniD7NDg4aGIzVXYxVEE/components_switches_switch1.png'>"
   },
   {
@@ -303,7 +313,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Slider-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Slider-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VTmJrQUYzajFIclE/components_sliders.png'>"
   },
   {
@@ -313,7 +323,7 @@
     "subcategories": [
       "Input and selections"
     ],
-    "link": "https://docs.flutter.io/flutter/material/showDatePicker.html",
+    "link": "https://api.flutter.dev/flutter/material/showDatePicker.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VY2h4WElGdEhPb2c/components_pickers.png'>"
   },
   {
@@ -323,7 +333,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/SimpleDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/material/SimpleDialog-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VVGNnN3NvMGdoQTg/components_dialogs.png'>"
   },
   {
@@ -333,7 +343,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/AlertDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/material/AlertDialog-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0Bzhp5Z4wHba3TzFHYVlrbWF2bnM/components_alerts_1.png'>"
   },
   {
@@ -343,7 +353,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/BottomSheet-class.html",
+    "link": "https://api.flutter.dev/flutter/material/BottomSheet-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VVWZzZ1FIN09XWGc/components_bottom_sheets.png'>"
   },
   {
@@ -353,7 +363,7 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ExpansionPanel-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ExpansionPanel-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VOXF3eEJ3azZMSjg/components_expansion_panels.png'>"
   },
   {
@@ -363,17 +373,17 @@
     "subcategories": [
       "Dialogs, alerts, and panels"
     ],
-    "link": "https://docs.flutter.io/flutter/material/SnackBar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/SnackBar-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VSjZkendtc19iZ2M/components_snackbars.png'>"
   },
   {
     "name": "Chip",
-    "description": "A material design chip. Chips represent complex entities in small blocks, such as a contact.",
+    "description": "A Material Design chip. Chips represent complex entities in small blocks, such as a contact.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Chip-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Chip-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VM1VORGxxWUx5U0E/components_chips.png'>"
   },
   {
@@ -383,7 +393,7 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Tooltip-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Tooltip-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VZ1JKMzJFcmhOWkk/components_tooltips.png'>"
   },
   {
@@ -393,38 +403,38 @@
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/DataTable-class.html",
+    "link": "https://api.flutter.dev/flutter/material/DataTable-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWTJHMmJZdWZ5LU0/components_data_tables.png'>"
   },
   {
     "name": "Card",
-    "description": "A material design card. A card has slightly rounded corners and a shadow.",
+    "description": "A Material Design card. A card has slightly rounded corners and a shadow.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Card-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Card-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VR0ptbC1RV1NLNlk/components_cards.png'>"
   },
   {
     "name": "LinearProgressIndicator",
-    "description": "Progress and activity indicators are visual indications of an app loading content. The LinearProgressIndicator widget implements this component. In addition you may also use the CircularProgressIndicator widget.",
+    "description": "A material design linear progress indicator, also known as a progress bar.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/material/LinearProgressIndicator-class.html",
+    "link": "https://api.flutter.dev/flutter/material/LinearProgressIndicator-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VWkJiRjRLbzRNS3M/components_progress_and_activity.png'>"
   },
   {
-    "name": "GridView",
-    "description": "A grid list consists of a repeated pattern of cells arrayed in a vertical and horizontal layout. The GridView widget implements this component.",
+    "name": "CircularProgressIndicator",
+    "description": "A material design circular progress indicator, which spins to indicate that the application is busy.",
     "categories": [],
     "subcategories": [
       "Information displays"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
-    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VandQYXpNMG9aQUk/components_grid_lists.png'>"
+    "link": "https://api.flutter.dev/flutter/material/CircularProgressIndicator-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ListTile",
@@ -433,17 +443,17 @@
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/ListTile-class.html",
+    "link": "https://api.flutter.dev/flutter/material/ListTile-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0Bx4BSt6jniD7UUw0bzVwU2lMUHc/components_lists_keylines_single10.png'>"
   },
   {
     "name": "Stepper",
-    "description": "A material stepper widget that displays progress through a sequence of steps.",
+    "description": "A Material Design stepper widget that displays progress through a sequence of steps.",
     "categories": [],
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Stepper-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Stepper-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VTndyUnNCR2tQREE/components_steppers.png'>"
   },
   {
@@ -453,17 +463,27 @@
     "subcategories": [
       "Layout"
     ],
-    "link": "https://docs.flutter.io/flutter/material/Divider-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Divider-class.html",
     "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VUVlmbHM1Q013RU0/components_dividers.png'>"
   },
   {
+    "name": "CupertinoActionSheet",
+    "description": "An iOS-style modal bottom action sheet to choose an option among many.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoActionSheet-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-action-sheet.png'>"
+  },
+  {
     "name": "CupertinoActivityIndicator",
     "description": "An iOS-style activity indicator. Displays a circular 'spinner'.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoActivityIndicator-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoActivityIndicator-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-activity-indicator.png'>"
   },
   {
@@ -473,8 +493,8 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoAlertDialog-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoAlertDialog-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-alert-dialog.png'>"
   },
   {
     "name": "CupertinoButton",
@@ -483,17 +503,27 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoButton-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoButton-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-button.png'>"
   },
   {
+    "name": "CupertinoDatePicker",
+    "description": "An iOS-style date or date and time picker.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDatePicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-date-picker.png'>"
+  },
+  {
     "name": "CupertinoDialog",
     "description": "An iOS-style dialog.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoDialog-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDialog-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-dialog.png'>"
   },
   {
@@ -503,17 +533,87 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoDialogAction-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoDialogAction-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-dialog-action.png'>"
+  },
+  {
+    "name": "CupertinoFullscreenDialogTransition",
+    "description": "An iOS-style transition used for summoning fullscreen dialogs.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoFullscreenDialogTransition-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-fullscreen-dialog-transition.png'>"
+  },
+  {
+    "name": "CupertinoPageScaffold",
+    "description": "Basic iOS style page layout structure. Positions a navigation bar and content on a background.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPageScaffold-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-page-scaffold.png'>"
+  },
+  {
+    "name": "CupertinoPageTransition",
+    "description": "Provides an iOS-style page transition animation.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPageTransition-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-page-transition.png'>"
+  },
+  {
+    "name": "CupertinoPicker",
+    "description": "An iOS-style picker control. Used to select an item in a short list.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-picker.png'>"
+  },
+  {
+    "name": "CupertinoPopupSurface",
+    "description": "Rounded rectangle surface that looks like an iOS popup surface, such as an alert dialog or action sheet.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoPopupSurface-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
+    "name": "CupertinoScrollbar",
+    "description": "An iOS-style scrollbar that indicates which portion of a scrollable widget is currently visible.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoScrollbar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-scrollbar.png'>"
+  },
+  {
+    "name": "CupertinoSegmentedControl",
+    "description": "An iOS-style segmented control. Used to select mutually exclusive options in a horizontal list.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSegmentedControl-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-segmented-control.png'>"
+  },
+  {
     "name": "CupertinoSlider",
     "description": "Used to select from a range of values.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoSlider-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSlider-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-slider.png'>"
   },
   {
@@ -523,28 +623,68 @@
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html",
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoSwitch-class.html",
     "image": "<img alt='' src='/images/widget-catalog/cupertino-switch.png'>"
   },
   {
-    "name": "CupertinoPageTransition",
-    "description": "Provides an iOS-style page transition animation.",
+    "name": "CupertinoNavigationBar",
+    "description": "An iOS-style top navigation bar. Typically used with CupertinoPageScaffold.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoPageTransition-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoNavigationBar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-nav-bar.png'>"
   },
   {
-    "name": "CupertinoFullscreenDialogTransition",
-    "description": "An iOS-style transition used for summoning fullscreen dialogs.",
+    "name": "CupertinoTabBar",
+    "description": "An iOS-style bottom tab bar. Typically used with CupertinoTabScaffold.",
     "categories": [
       "Cupertino (iOS-style widgets)"
     ],
     "subcategories": [],
-    "link": "https://docs.flutter.io/flutter/cupertino/CupertinoFullscreenDialogTransition-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabBar-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-bar.png'>"
+  },
+  {
+    "name": "CupertinoTabScaffold",
+    "description": "Tabbed iOS app structure. Positions a tab bar on top of tabs of content.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabScaffold-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-scaffold.png'>"
+  },
+  {
+    "name": "CupertinoTabView",
+    "description": "Root content of a tab that supports parallel navigation between tabs. Typically used with CupertinoTabScaffold.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTabView-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-tab-view.png'>"
+  },
+  {
+    "name": "CupertinoTextField",
+    "description": "An iOS-style text field.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTextField-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-textfield.png'>"
+  },
+  {
+    "name": "CupertinoTimerPicker",
+    "description": "An iOS-style countdown timer picker.",
+    "categories": [
+      "Cupertino (iOS-style widgets)"
+    ],
+    "subcategories": [],
+    "link": "https://api.flutter.dev/flutter/cupertino/CupertinoTimerPicker-class.html",
+    "image": "<img alt='' src='/images/widget-catalog/cupertino-timer-picker.png'>"
   },
   {
     "name": "Padding",
@@ -555,7 +695,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Padding-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Padding-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-padding' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='25' y='30' width='50' height='30' fill='#4dd0e1'/><line x1='13' y1='45' x2='22' y2='45' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='78' y1='45' x2='87' y2='45' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='50' y1='23' x2='50' y2='27' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/><line x1='50' y1='63' x2='50' y2='77' stroke='#f50057' stroke-width='2' marker-start='url(#arrow-padding)' marker-end='url(#arrow-padding)'/></svg>"
   },
   {
@@ -566,7 +706,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Center-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Center-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-center' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='25' y='35' width='50' height='30' fill='#4dd0e1'/><line x1='10' y1='50' x2='22' y2='50' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='90' y1='50' x2='78' y2='50' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='50' y1='20' x2='50' y2='32' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/><line x1='50' y1='80' x2='50' y2='68' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-center)'/></svg>"
   },
   {
@@ -577,7 +717,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Align-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Align-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-align' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#f50057'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='20' width='80' height='60' fill='#ffffff'/><rect x='15' y='50' width='50' height='30' fill='#4dd0e1'/><line x1='10' y1='65' x2='12' y2='65' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/><line x1='90' y1='65' x2='68' y2='65' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/><line x1='40' y1='20' x2='40' y2='47' stroke='#f50057' stroke-width='2' marker-end='url(#arrow-align)'/></svg>"
   },
   {
@@ -588,7 +728,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FittedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FittedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -599,7 +739,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AspectRatio-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AspectRatio-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-aspect-ratio' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='35' width='50' height='30' fill='#ffffff'/><rect x='27.5' y='37.5' width='45' height='25' fill='#4dd0e1'/><line x1='31' y1='41' x2='69' y2='59' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-aspect-ratio)' marker-end='url(#arrow-aspect-ratio)'/></svg>"
   },
   {
@@ -610,7 +750,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ConstrainedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ConstrainedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-constrained-box' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='30' width='50' height='40' fill='#ffffff'/><rect x='22.5' y='32.5' width='45' height='35' fill='#4dd0e1'/><path d='M63 23 v 40' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M77 23 v 54' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M13 63 h 50' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M13 77 h 64' stroke='#ffffff' stroke-width='1' stroke-linecap='round' stroke-dasharray='0.5 3.1' fill='transparent'/><path d='M50 25 L60 25' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M90 25 L80 25' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M15 50 L15 60' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/><path d='M15 90 L15 80' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-constrained-box)'/></svg>"
   },
   {
@@ -621,7 +761,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Baseline-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Baseline-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-baseline' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='50' fill='#ffffff'/><rect x='20' y='45' width='60' height='30' fill='#4dd0e1'/><line x1='85' y1='20' x2='85' y2='66' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-baseline)'/><line x1='15' y1='20' x2='15' y2='66' stroke='#ffffff' stroke-width='2' marker-end='url(#arrow-baseline)'/><line x1='10' y1='70' x2='90' y2='70' stroke='#ffffff' stroke-width='2'/><text x='50' y='69' text-anchor='middle' font-family='Roboto' font-size='25' fill='#3b75ad'>Abc </text></svg>"
   },
   {
@@ -632,7 +772,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FractionallySizedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FractionallySizedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -643,7 +783,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IntrinsicHeight-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IntrinsicHeight-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -654,7 +794,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IntrinsicWidth-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IntrinsicWidth-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -665,7 +805,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LimitedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LimitedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -676,7 +816,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Offstage-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Offstage-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -687,7 +827,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/OverflowBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/OverflowBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -698,7 +838,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><marker id='arrow-sized-box' orient='auto-start-reverse' viewBox='0 0 1 1' markerWidth='3' markerHeight='3' refX='0.5' refY='0.5'><path d='M 1 0.5 L 0.5 0 L 0.5 1 z' fill='#ffffff'/></marker></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='35' width='50' height='30' fill='#ffffff'/><rect x='27.5' y='37.5' width='45' height='25' fill='#4dd0e1'/><line x1='28' y1='30' x2='72' y2='30' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-sized-box)' marker-end='url(#arrow-sized-box)'/><line x1='20' y1='38' x2='20' y2='62' stroke='#ffffff' stroke-width='2' marker-start='url(#arrow-sized-box)' marker-end='url(#arrow-sized-box)'/></svg>"
   },
   {
@@ -709,18 +849,19 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizedOverflowBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizedOverflowBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Transform",
     "description": "A widget that applies a transformation before painting its child.",
     "categories": [
+      "Painting and effects"
     ],
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Transform-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Transform-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='17.5' y='42.5' width='50' height='30' fill='#ffffff'/><rect x='20' y='45' width='45' height='25' fill='#3b75ad'/><rect x='20' y='45' width='45' height='25' fill='#4dd0e1' transform='translate(15 -5) rotate(-23) skewX(-10)'/></svg>"
   },
   {
@@ -731,7 +872,7 @@
     "subcategories": [
       "Single-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomSingleChildLayout-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomSingleChildLayout-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -742,19 +883,21 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IndexedStack-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IndexedStack-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "GridView",
-    "description": "A scrollable, 2D array of widgets.",
+    "description": "A grid list consists of a repeated pattern of cells arrayed in a vertical and horizontal layout. The GridView widget implements this component.",
     "categories": [
+      "Scrolling"
     ],
     "subcategories": [
+      "Information displays",
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/GridView-class.html",
+    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VandQYXpNMG9aQUk/components_grid_lists.png'>"
   },
   {
     "name": "Flow",
@@ -764,7 +907,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Flow-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Flow-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -775,7 +918,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Table-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Table-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -786,7 +929,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Wrap-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Wrap-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -797,22 +940,10 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListBody-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ListBody-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
-    "name": "ListView",
-    "sample": "ListView_index",
-    "description": "A scrollable, linear list of widgets. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.",
-    "categories": [
-    ],
-    "subcategories": [
-      "Multi-child layout widgets"
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListView-class.html",
-    "image": "<svg viewBox='0 0 100 100'><filter id='inset-shadow-block' x='-50%' y='-50%' width='200%' height='200%'><feComponentTransfer in='SourceAlpha'><feFuncA type='table' tableValues='1 0' /></feComponentTransfer><feGaussianBlur stdDeviation='2' result='Blur'/><feFlood flood-color='#666666' result='color'/><feComposite in2='Blur' operator='in'/><feComposite in2='SourceAlpha' operator='in' /><feMerge><feMergeNode /></feMerge></filter><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='35' y='10' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='30' width='30' height='20' fill='#4dd0e1'/><rect x='35' y='55' width='30' height='5' fill='#4dd0e1'/><rect x='35' y='65' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='85' width='30' height='5' fill='#4dd0e1'/><rect x='30' y='10' width='40' height='80' fill='#ffffff' filter='url(#inset-shadow-block)'/></svg>"
-  },
-  {
     "name": "CustomMultiChildLayout",
     "description": "A widget that uses a delegate to size and position multiple children.",
     "categories": [
@@ -820,7 +951,7 @@
     "subcategories": [
       "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomMultiChildLayout-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomMultiChildLayout-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -829,9 +960,9 @@
     "categories": [
     ],
     "subcategories": [
-      "Layout helpers"
+      "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LayoutBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -842,7 +973,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RichText-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RichText-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -853,7 +984,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DefaultTextStyle-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DefaultTextStyle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -864,7 +995,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RawImage-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RawImage-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -875,7 +1006,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/services/AssetBundle-class.html",
+    "link": "https://api.flutter.dev/flutter/services/AssetBundle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -886,7 +1017,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Form-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Form-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -897,7 +1028,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FormField-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FormField-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -908,7 +1039,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RawKeyboardListener-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RawKeyboardListener-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -919,7 +1050,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedContainer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -930,7 +1061,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedCrossFade-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedCrossFade-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -942,7 +1073,7 @@
     "subcategories": [
       "Routing"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Hero-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Hero-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -953,7 +1084,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -964,7 +1095,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Draggable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Draggable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -975,7 +1106,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/LongPressDraggable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/LongPressDraggable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -986,7 +1117,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/GestureDetector-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/GestureDetector-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -997,7 +1128,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DragTarget-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DragTarget-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1008,7 +1139,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Dismissible-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Dismissible-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1019,7 +1150,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/IgnorePointer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/IgnorePointer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1030,7 +1161,7 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AbsorbPointer-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AbsorbPointer-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1041,7 +1172,7 @@
     "subcategories": [
       "Routing"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Navigator-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Navigator-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1052,7 +1183,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/material/Theme-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Theme-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1063,40 +1194,31 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/MediaQuery-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/MediaQuery-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ListView",
+    "sample": "ListView",
     "description": "A scrollable, linear list of widgets. ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
+      "Multi-child layout widgets"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ListView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+    "link": "https://api.flutter.dev/flutter/widgets/ListView-class.html",
+    "image": "<svg viewBox='0 0 100 100'><filter id='inset-shadow-block' x='-50%' y='-50%' width='200%' height='200%'><feComponentTransfer in='SourceAlpha'><feFuncA type='table' tableValues='1 0' /></feComponentTransfer><feGaussianBlur stdDeviation='2' result='Blur'/><feFlood flood-color='#666666' result='color'/><feComposite in2='Blur' operator='in'/><feComposite in2='SourceAlpha' operator='in' /><feMerge><feMergeNode /></feMerge></filter><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='40' height='80' fill='#ffffff'/><rect x='35' y='10' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='30' width='30' height='20' fill='#4dd0e1'/><rect x='35' y='55' width='30' height='5' fill='#4dd0e1'/><rect x='35' y='65' width='30' height='15' fill='#4dd0e1'/><rect x='35' y='85' width='30' height='5' fill='#4dd0e1'/><rect x='30' y='10' width='40' height='80' fill='#ffffff' filter='url(#inset-shadow-block)'/></svg>"
   },
   {
     "name": "NestedScrollView",
-    "description": "",
+    "description": "A scrolling view inside of which can be nested other scrolling views, with their scroll positions being intrinsically linked.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/NestedScrollView-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
-  },
-  {
-    "name": "GridView",
-    "description": "A scrollable, 2D array of widgets. The most commonly used grid layouts are GridView.count, which creates a layout with a fixed number of tiles in the cross axis, and GridView.extent, which creates a layout with tiles that have a maximum cross-axis extent. A custom SliverGridDelegate can produce an arbitrary 2D arrangement of children, including arrangements that are unaligned or overlapping.",
-    "categories": [
-      "Scrolling"
-    ],
-    "subcategories": [
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/GridView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/NestedScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1107,7 +1229,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SingleChildScrollView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1119,18 +1241,18 @@
     "subcategories": [
       "Touch interactions"
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Scrollable-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Scrollable-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "Scrollbar",
-    "description": "A material design scrollbar. A scrollbar indicates which portion of a Scrollable widget is actually visible.",
+    "description": "A Material Design scrollbar. A scrollbar indicates which portion of a Scrollable widget is actually visible.",
     "categories": [
       "Scrolling"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/material/Scrollbar-class.html",
+    "link": "https://api.flutter.dev/flutter/material/Scrollbar-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1141,7 +1263,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomScrollView-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomScrollView-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1152,7 +1274,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/NotificationListener-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/NotificationListener-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1163,10 +1285,21 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ScrollConfiguration-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ScrollConfiguration-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
+    "name": "RefreshIndicator",
+    "description": "A Material Design pull-to-refresh wrapper for scrollables.",
+    "categories": [
+      "Scrolling"
+    ],
+    "subcategories": [
+    ],
+    "link": "https://api.flutter.dev/flutter/material/RefreshIndicator-class.html",
+    "image": "<img alt='' src='https://storage.googleapis.com/material-design/publish/material_v_12/assets/0B7WCemMG6e0VS2kzSmZwNnNKQVk/patterns-swipe-to-refresh.png'>"
+  },
+  {
     "name": "Opacity",
     "description": "A widget that makes its child partially transparent.",
     "categories": [
@@ -1174,21 +1307,10 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Opacity-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Opacity-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='10' y='10' width='60' height='60' fill='#3b75ad'/><rect x='20' y='30' width='70' height='50' fill='#ffffff' opacity='0.8'/></svg>"
   },
   {
-    "name": "Transform",
-    "description": "A widget that applies a transformation before painting its child.",
-    "categories": [
-      "Painting and effects"
-    ],
-    "subcategories": [
-    ],
-    "link": "https://docs.flutter.io/flutter/widgets/Transform-class.html",
-    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
-  },
-  {
     "name": "DecoratedBox",
     "description": "A widget that paints a Decoration either before or after its child paints.",
     "categories": [
@@ -1196,7 +1318,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DecoratedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DecoratedBox-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><filter id='shadow-decorated-box' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur stdDeviation='4'/></filter><linearGradient id='gradient-decorated-box' x1='0' y1='0' x2='0.5' y2='1'><stop offset='50%' stop-color='#ffffff'/><stop offset='100%' stop-color='#f50057'/></linearGradient></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='25' y='25' width='50' height='50' rx='10' ry='10' fill='#000000' filter='url(#shadow-decorated-box)'/><rect x='25' y='25' width='50' height='50' rx='10' ry='10' fill='url(#gradient-decorated-box)' stroke-width='5' stroke='#4dd0e1'/></svg>"
   },
   {
@@ -1207,7 +1329,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FractionalTranslation-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FractionalTranslation-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1218,7 +1340,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RotatedBox-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RotatedBox-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1229,7 +1351,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipOval-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipOval-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><clipPath id='clip-oval'><circle cx='40' cy='50' r='30'/></clipPath></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' opacity='0.05'/><circle cx='40' cy='50' r='30' fill='#ffffff'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' clip-path='url(#clip-oval)'/></svg>"
   },
   {
@@ -1240,7 +1362,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipPath-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipPath-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1251,7 +1373,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ClipRect-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ClipRect-class.html",
     "image": "<svg viewBox='0 0 100 100'><defs><clipPath id='clip-rect'><rect x='10' y='20' width='60' height='60'/></clipPath></defs><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' opacity='0.05'/><rect x='10' y='20' width='60' height='60' fill='#ffffff'/><rect x='30' y='10' width='60' height='50' fill='#4dd0e1' clip-path='url(#clip-rect)'/></svg>"
   },
   {
@@ -1262,7 +1384,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/CustomPaint-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/CustomPaint-class.html",
     "image": "<svg viewBox='0 0 100 100'><rect x='0' y='0' width='100' height='100' fill='#3949ab'/><rect x='20' y='20' width='60' height='60' fill='#ffffff'/><ellipse cx='40' cy='35' rx='10' ry='10' stroke-width='5' stroke='#4dd0e1' fill='transparent'/><ellipse cx='20' cy='71' rx='15' ry='20' stroke-width='5' stroke='#4dd0e1' fill='transparent' transform='rotate(-25)'/></svg>"
   },
   {
@@ -1273,7 +1395,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/BackdropFilter-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/BackdropFilter-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1284,7 +1406,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/Semantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/Semantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1295,18 +1417,18 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/MergeSemantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/MergeSemantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "ExcludeSemantics",
-    "description": "A widget that drops all the semantics of its descendants. This can be used to hide subwidgets that would otherwise be reported but that would only be confusing. For example, the material library's Chip widget hides the avatar since it is redundant with the chip label.",
+    "description": "A widget that drops all the semantics of its descendants. This can be used to hide subwidgets that would otherwise be reported but that would only be confusing. For example, the Material Components Chip widget hides the avatar since it is redundant with the chip label.",
     "categories": [
       "Accessibility"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ExcludeSemantics-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ExcludeSemantics-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1317,7 +1439,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1328,7 +1450,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/StreamBuilder-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1339,7 +1461,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/DecoratedBoxTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/DecoratedBoxTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1350,7 +1472,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/FadeTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/FadeTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1361,7 +1483,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/PositionedTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/PositionedTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1372,7 +1494,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/RotationTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/RotationTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1383,7 +1505,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/ScaleTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/ScaleTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1394,7 +1516,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SizeTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SizeTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1405,7 +1527,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/SlideTransition-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/SlideTransition-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1416,19 +1538,19 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedDefaultTextStyle-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedDefaultTextStyle-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
     "name": "AnimatedListState",
-    "sample": "AnimatedListState_index",
+    "sample": "AnimatedListState",
     "description": "The state for a scrolling container that animates items when they are inserted or removed.",
     "categories": [
       "Animation and Motion"
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedListState-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1439,7 +1561,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedModalBarrier-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedModalBarrier-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1450,7 +1572,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedOpacity-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedOpacity-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1461,7 +1583,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedPhysicalModel-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedPhysicalModel-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1472,7 +1594,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedPositioned-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1483,7 +1605,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedSize-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1494,7 +1616,7 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedWidget-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedWidget-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
   },
   {
@@ -1505,7 +1627,40 @@
     ],
     "subcategories": [
     ],
-    "link": "https://docs.flutter.io/flutter/widgets/AnimatedWidgetBaseState-class.html",
+    "link": "https://api.flutter.dev/flutter/widgets/AnimatedWidgetBaseState-class.html",
     "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "Expanded",
+    "categories": [
+    ],
+    "subcategories": [
+      "Multi-child layout widgets"
+    ],
+    "description": "A widget that expands a child of a Row, Column, or Flex.",
+    "link": "https://api.flutter.dev/flutter/widgets/Expanded-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "PageView",
+    "description": "A scrollable list that works page by page.",
+    "categories": [
+      "Scrolling"
+    ],
+    "subcategories": [
+    ],
+    "link": "https://api.flutter.dev/flutter/widgets/PageView-class.html",
+    "image": "<img alt='' src='/images/catalog-widget-placeholder.png'>"
+  },
+  {
+    "name": "SliverAppBar",
+    "description": "A material design app bar that integrates with a CustomScrollView.",
+    "categories": [
+    ],
+    "subcategories": [
+      "App structure and navigation"
+    ],
+    "link": "https://api.flutter.dev/flutter/material/SliverAppBar-class.html",
+    "image": "<img alt='' src='https://material-design.storage.googleapis.com/publish/material_v_9/0B7WCemMG6e0VclpfSFpuelBGR1k/components_toolbars.png'>"
   }
 ]
\ No newline at end of file
diff --git a/devtools_server/BUILD.gn b/devtools_server/BUILD.gn
index eee737d..1aac9fa 100644
--- a/devtools_server/BUILD.gn
+++ b/devtools_server/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for devtools_server-0.1.2
+# This file is generated by importer.py for devtools_server-0.1.6
 
 import("//build/dart/dart_library.gni")
 
@@ -17,9 +17,8 @@
     "//third_party/dart-pkg/pub/shelf",
     "//third_party/dart-pkg/pub/args",
     "//third_party/dart-pkg/pub/meta",
-    "//third_party/dart-pkg/pub/vm_service_lib",
+    "//third_party/dart-pkg/pub/vm_service",
     "//third_party/dart-pkg/pub/path",
     "//third_party/dart-pkg/pub/browser_launcher",
-    "//third_party/dart-pkg/pub/webkit_inspection_protocol",
   ]
 }
diff --git a/devtools_server/CHANGELOG.md b/devtools_server/CHANGELOG.md
index 670ef88..982628f 100644
--- a/devtools_server/CHANGELOG.md
+++ b/devtools_server/CHANGELOG.md
@@ -1,3 +1,13 @@
+## 0.1.6
+
+- Rev to using the latest version of `package:vm_service` (1.1.1).
+
+## 0.1.4
+- The `launchDevTools` service will now register with VMs using public APIs when available, falling back to private APIs otherwise.
+
+## 0.1.3
+- vm_service_lib dependency has been pinned to 3.21.0
+
 ## 0.1.2
 - The `launchDevTools` service will now return well-formed errors if it fails to
   launch the browser for any reason.
diff --git a/rxdart/example/flutter/github_search/github_search.iml b/devtools_server/devtools_server.iml
old mode 100755
new mode 100644
similarity index 65%
rename from rxdart/example/flutter/github_search/github_search.iml
rename to devtools_server/devtools_server.iml
index d4f3e6e..ae9af97
--- a/rxdart/example/flutter/github_search/github_search.iml
+++ b/devtools_server/devtools_server.iml
@@ -1,15 +1,15 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<module type="FLUTTER_MODULE_TYPE" version="4">

-  <component name="NewModuleRootManager" inherit-compiler-output="true">

-    <exclude-output />

-    <content url="file://$MODULE_DIR$">

-      <excludeFolder url="file://$MODULE_DIR$/.idea" />

-      <excludeFolder url="file://$MODULE_DIR$/.pub" />

-      <excludeFolder url="file://$MODULE_DIR$/build" />

-      <excludeFolder url="file://$MODULE_DIR$/packages" />

-    </content>

-    <orderEntry type="sourceFolder" forTests="false" />

-    <orderEntry type="library" name="Dart Packages" level="project" />

-    <orderEntry type="library" name="Dart SDK" level="application" />

-  </component>

+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+    <orderEntry type="library" name="Dart Packages" level="project" />
+  </component>
 </module>
\ No newline at end of file
diff --git a/devtools_server/lib/src/server.dart b/devtools_server/lib/src/server.dart
index fe087ba..ae5593e 100644
--- a/devtools_server/lib/src/server.dart
+++ b/devtools_server/lib/src/server.dart
@@ -14,8 +14,8 @@
 import 'package:shelf/shelf.dart' as shelf;
 import 'package:shelf/shelf_io.dart' as shelf;
 import 'package:shelf_static/shelf_static.dart';
-import 'package:vm_service_lib/utils.dart';
-import 'package:vm_service_lib/vm_service_lib.dart' hide Isolate;
+import 'package:vm_service/utils.dart';
+import 'package:vm_service/vm_service.dart' hide Isolate;
 
 const argHelp = 'help';
 const argMachine = 'machine';
@@ -254,7 +254,13 @@
       }
     });
 
-    await service.registerService(launchDevToolsService, 'DevTools Server');
+    // Handle registerService method name change based on protocol version.
+    final registerServiceMethodName =
+        isVersionLessThan(await service.getVersion(), major: 3, minor: 22)
+            ? '_registerService'
+            : 'registerService';
+    await service.callMethod(registerServiceMethodName,
+        args: {'service': launchDevToolsService, 'alias': 'DevTools Server'});
 
     printOutput(
       'Successfully registered launchDevTools service',
@@ -276,23 +282,23 @@
   }
 }
 
+// TODO(dantup): This method was adapted from devtools and should be upstreamed
+// in some form into vm_service_lib.
+bool isVersionLessThan(
+  Version version, {
+  @required int major,
+  @required int minor,
+}) {
+  assert(version != null);
+  return version.major < major ||
+      (version.major == major && version.minor < minor);
+}
+
 final bool _isChromeOS = new File('/dev/.cros_milestone').existsSync();
 
 bool _isAccessibleToChromeOSNativeBrowser(Uri uri) {
-  // TODO(dantup): Change to Set literal when supported.
-  const tunneledPorts = {
-    8000: true,
-    8008: true,
-    8080: true,
-    8085: true,
-    8888: true,
-    9005: true,
-    3000: true,
-    4200: true,
-    5000: true,
-  };
-
-  return uri != null && uri.hasPort && tunneledPorts[uri.port] == true;
+  const tunneledPorts = {8000, 8008, 8080, 8085, 8888, 9005, 3000, 4200, 5000};
+  return uri != null && uri.hasPort && tunneledPorts.contains(uri.port);
 }
 
 Future<VmService> _connectToVmService(Uri uri) async {
diff --git a/devtools_server/pubspec.yaml b/devtools_server/pubspec.yaml
index ae27c13..41c2e45 100644
--- a/devtools_server/pubspec.yaml
+++ b/devtools_server/pubspec.yaml
@@ -1,21 +1,20 @@
 name: devtools_server
 description: A server that supports Dart DevTools.
 
-version: 0.1.2
+version: 0.1.6
 
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/flutter/devtools
 
 environment:
-  sdk: '>=2.1.2-dev.0.0 <3.0.0'
+  sdk: '>=2.3.0 <3.0.0'
 
 dependencies:
   args: ^1.5.1
   browser_launcher: ^0.1.2
   meta: ^1.1.0
   path: ^1.6.0
-  pedantic: ^1.4.0
+  pedantic: ^1.7.0
   shelf: ^0.7.4
   shelf_static: ^0.2.8
-  vm_service_lib: ^3.15.1+1
-  webkit_inspection_protocol: ^0.4.0
+  vm_service: 1.1.1
diff --git a/dwds/BUILD.gn b/dwds/BUILD.gn
index 327a762..1fb5ead 100644
--- a/dwds/BUILD.gn
+++ b/dwds/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for dwds-0.5.0
+# This file is generated by importer.py for dwds-0.5.2
 
 import("//build/dart/dart_library.gni")
 
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 45aa9a8..7cf2ca8 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,3 +1,15 @@
+## 0.5.2
+
+- Fix issue where certain required fields of VM service protocol objects were
+  null.
+- Properly display `Closure` names in the debug view.
+
+## 0.5.1
+
+- Fix an issue where missing source maps would cause a crash. A warning will
+  now be logged to the console instead.
+- Depend on the latest `package:webkit_inspection_protocol`.
+
 ## 0.5.0
 
 - Fix an issue where we source map paths were not normalized.
diff --git a/dwds/debug_extension/CONTRIBUTING.md b/dwds/debug_extension/CONTRIBUTING.md
new file mode 100644
index 0000000..7ca2f97
--- /dev/null
+++ b/dwds/debug_extension/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+## Building
+
+- With dart2js:
+
+```
+pub run build_runner build web -o build -r
+```
+
+- With DDC:
+
+```
+pub run build_runner build web -o build
+```
+
+## Deployment
+
+- Update the version number in `web/manifest.json`.
+- Zip `web/manifest.json`, `web/background.js`, and `dart.png`.
+- Upload the `.zip` to the Chrome Web Store.
+
+## [For Googlers] Local Development
+
+The developer key is needed for local development and testing. Add one of the whitelisted keys to `web/manifest.json`.
\ No newline at end of file
diff --git a/dwds/debug_extension/README.md b/dwds/debug_extension/README.md
new file mode 100644
index 0000000..239308d
--- /dev/null
+++ b/dwds/debug_extension/README.md
@@ -0,0 +1,9 @@
+A Chrome extension for enabling Dart application debugging over an SSE connection with package:dwds.
+
+## Installation
+
+The extension is available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/dart-debug-extension/eljbmlghnomdjgdjmbdekegdkbabckhm).
+
+## Usage
+
+The extension requires disabling 'Throttle expensive background timers' in `chrome://flags` to ensure proper performance.
\ No newline at end of file
diff --git a/dwds/debug_extension/build.yaml b/dwds/debug_extension/build.yaml
new file mode 100644
index 0000000..5f4e18d
--- /dev/null
+++ b/dwds/debug_extension/build.yaml
@@ -0,0 +1,22 @@
+targets:
+  $default:
+    builders:
+      build_web_compilers|entrypoint:
+        options:
+          dart2js_args:
+            - -O4 --csp
+        generate_for:
+          - web/background.dart
+      extension|client_js_copy_builder:
+        enabled: true
+      
+builders:
+  client_js_copy_builder:
+    import: "tool/copy_builder.dart"
+    builder_factories:
+        - copyBuilder
+    build_extensions:
+      web/background.dart.js:
+        - web/background.js
+    auto_apply: none
+    build_to: source
diff --git a/dwds/debug_extension/tool/copy_builder.dart b/dwds/debug_extension/tool/copy_builder.dart
new file mode 100644
index 0000000..8ecc701
--- /dev/null
+++ b/dwds/debug_extension/tool/copy_builder.dart
@@ -0,0 +1,31 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. 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:build/build.dart';
+
+/// Factory for the build script.
+Builder copyBuilder(_) => _CopyBuilder();
+
+/// Copies the [_backgroundJsId] file to [_backgroundJsCopyId].
+class _CopyBuilder extends Builder {
+  @override
+  Map<String, List<String>> get buildExtensions => {
+        _backgroundJsId.path: [_backgroundJsCopyId.path]
+      };
+
+  @override
+  void build(BuildStep buildStep) {
+    if (buildStep.inputId == _backgroundJsId) {
+      buildStep.writeAsString(
+          _backgroundJsCopyId, buildStep.readAsString(_backgroundJsId));
+      return;
+    } else {
+      throw StateError(
+          'Unexpected input for `CopyBuilder` expected only $_backgroundJsId');
+    }
+  }
+}
+
+final _backgroundJsId = AssetId('extension', 'web/background.dart.js');
+final _backgroundJsCopyId = AssetId('extension', 'web/background.js');
diff --git a/dwds/debug_extension/web/background.dart b/dwds/debug_extension/web/background.dart
index 138227b..4368653 100644
--- a/dwds/debug_extension/web/background.dart
+++ b/dwds/debug_extension/web/background.dart
@@ -20,7 +20,7 @@
 // GENERATE:
 // pub run build_runner build web -o build -r
 void main() {
-  addListener(allowInterop((e) {
+  var startDebug = allowInterop((_) {
     var query = QueryInfo(active: true, currentWindow: true);
     Tab currentTab;
 
@@ -55,7 +55,15 @@
     queryTabs(query, allowInterop((List tabs) {
       callback(List.from(tabs));
     }));
-  }));
+  });
+  addListener(startDebug);
+
+  // For testing only.
+  onFakeClick = allowInterop(() {
+    startDebug(null);
+  });
+
+  isDartDebugExtension = true;
 }
 
 // Starts an SSE client.
@@ -64,22 +72,11 @@
 // and sends an [ExtensionEvent].
 Future<void> startSseClient(
     hostname, port, appId, instanceId, currentTab) async {
+  // Specifies whether the debugger is attached.
+  //
+  // A debugger is detached if it is closed by user or the target is closed.
+  var attached = true;
   var client = SseClient('http://$hostname:$port/\$debug');
-  await client.onOpen.first;
-  client.sink.add(jsonEncode(serializers.serialize(DevToolsRequest((b) => b
-    ..appId = appId as String
-    ..instanceId = instanceId as String
-    ..tabUrl = currentTab.url as String))));
-  sendCommand(Debuggee(tabId: currentTab.id), 'Runtime.enable', EmptyParam(),
-      allowInterop((e) {}));
-
-  // Notifies the backend of debugger events.
-  addDebuggerListener(
-      allowInterop((Debuggee source, String method, Object params) {
-    client.sink.add(jsonEncode(serializers.serialize(ExtensionEvent((b) => b
-      ..params = jsonEncode(json.decode(stringify(params)))
-      ..method = jsonEncode(method)))));
-  }));
 
   client.stream.listen((data) {
     var message = serializers.deserialize(jsonDecode(data));
@@ -95,9 +92,54 @@
               ..result = stringify(e)))));
       }));
     }
-  }, onError: (e) {
+  }, onDone: () {
+    attached = false;
     client.close();
+    return;
+  }, onError: (_) {
+    alert('Lost app connection.');
+    detach(Debuggee(tabId: currentTab.id), allowInterop(() {}));
   }, cancelOnError: true);
+
+  await client.onOpen.first;
+  client.sink.add(jsonEncode(serializers.serialize(DevToolsRequest((b) => b
+    ..appId = appId as String
+    ..instanceId = instanceId as String
+    ..tabUrl = currentTab.url as String))));
+
+  sendCommand(Debuggee(tabId: currentTab.id), 'Runtime.enable', EmptyParam(),
+      allowInterop((e) {}));
+
+  // Notifies the backend of debugger events.
+  //
+  // The listener of the `currentTab` receives events from all tabs.
+  // We want to forward an event only if it originates from `currentTab`.
+  // We know that if `source.tabId` and `currentTab.id` are the same.
+  addDebuggerListener(
+      allowInterop((Debuggee source, String method, Object params) {
+    if (source.tabId == currentTab.id && attached) {
+      client.sink.add(jsonEncode(serializers.serialize(ExtensionEvent((b) => b
+        ..params = jsonEncode(json.decode(stringify(params)))
+        ..method = jsonEncode(method)))));
+    }
+  }));
+
+  onDetachAddListener(allowInterop((Debuggee source, DetachReason reason) {
+    if (attached) {
+      if (source.tabId == currentTab.id) {
+        if (reason.toString() == 'canceled_by_user') {
+          alert('Debugger detached.'
+              'Click the extension to relaunch DevTools.');
+        } else if (reason.toString() == 'target_closed') {
+          alert('Debugger detached because a Dart app tab'
+              'using the debugger is closed.');
+        }
+      }
+      attached = false;
+      client.close();
+      return;
+    }
+  }));
 }
 
 @JS('chrome.browserAction.onClicked.addListener')
@@ -117,6 +159,9 @@
 @JS('chrome.debugger.onEvent.addListener')
 external dynamic addDebuggerListener(Function callback);
 
+@JS('chrome.debugger.onDetach.addListener')
+external dynamic onDetachAddListener(Function callback);
+
 @JS('chrome.tabs.query')
 external List<Tab> queryTabs(QueryInfo queryInfo, Function callback);
 
@@ -136,6 +181,13 @@
 
 @JS()
 @anonymous
+class RemoveInfo {
+  external int get windowId;
+  external bool get isWindowClosing;
+}
+
+@JS()
+@anonymous
 class Debuggee {
   external dynamic get tabId;
   external String get extensionId;
@@ -183,3 +235,18 @@
   external String get scriptId;
   external factory ScriptIdParam({String scriptId});
 }
+
+@JS()
+@anonymous
+class DetachReason {}
+
+/// For testing only.
+//
+/// An automated click on the extension icon is not supported by WebDriver.
+/// We initiate a fake click from the `debug_extension_test`
+/// after the extension is loaded.
+@JS('fakeClick')
+external set onFakeClick(void Function() f);
+
+@JS('window.isDartDebugExtension')
+external set isDartDebugExtension(_);
diff --git a/dwds/debug_extension/web/manifest.json b/dwds/debug_extension/web/manifest.json
index a76dd13..f1d1aaf 100644
--- a/dwds/debug_extension/web/manifest.json
+++ b/dwds/debug_extension/web/manifest.json
@@ -1,6 +1,6 @@
 {
-    "name": "sample",
-    "version": "1.0",
+    "name": "Dart Debug Extension",
+    "version": "1.3",
     "minimum_chrome_version": "10.0",
     "devtools_page": "devtools.html",
     "manifest_version": 2,
@@ -13,7 +13,7 @@
     ],
     "background": {
         "scripts": [
-            "background.dart.js"
+            "background.js"
         ]
     }
 }
\ No newline at end of file
diff --git a/dwds/lib/data/connect_request.g.dart b/dwds/lib/data/connect_request.g.dart
index 125a3ba..f44dea8 100644
--- a/dwds/lib/data/connect_request.g.dart
+++ b/dwds/lib/data/connect_request.g.dart
@@ -17,7 +17,7 @@
   final String wireName = 'ConnectRequest';
 
   @override
-  Iterable serialize(Serializers serializers, ConnectRequest object,
+  Iterable<Object> serialize(Serializers serializers, ConnectRequest object,
       {FullType specifiedType = FullType.unspecified}) {
     final result = <Object>[
       'appId',
@@ -32,7 +32,8 @@
   }
 
   @override
-  ConnectRequest deserialize(Serializers serializers, Iterable serialized,
+  ConnectRequest deserialize(
+      Serializers serializers, Iterable<Object> serialized,
       {FullType specifiedType = FullType.unspecified}) {
     final result = new ConnectRequestBuilder();
 
diff --git a/dwds/lib/data/isolate_events.g.dart b/dwds/lib/data/isolate_events.g.dart
index 0c06cb4..ae75eba 100644
--- a/dwds/lib/data/isolate_events.g.dart
+++ b/dwds/lib/data/isolate_events.g.dart
@@ -17,7 +17,7 @@
   final String wireName = 'IsolateExit';
 
   @override
-  Iterable serialize(Serializers serializers, IsolateExit object,
+  Iterable<Object> serialize(Serializers serializers, IsolateExit object,
       {FullType specifiedType = FullType.unspecified}) {
     final result = <Object>[
       'appId',
@@ -32,7 +32,7 @@
   }
 
   @override
-  IsolateExit deserialize(Serializers serializers, Iterable serialized,
+  IsolateExit deserialize(Serializers serializers, Iterable<Object> serialized,
       {FullType specifiedType = FullType.unspecified}) {
     final result = new IsolateExitBuilder();
 
@@ -64,7 +64,7 @@
   final String wireName = 'IsolateStart';
 
   @override
-  Iterable serialize(Serializers serializers, IsolateStart object,
+  Iterable<Object> serialize(Serializers serializers, IsolateStart object,
       {FullType specifiedType = FullType.unspecified}) {
     final result = <Object>[
       'appId',
@@ -79,7 +79,7 @@
   }
 
   @override
-  IsolateStart deserialize(Serializers serializers, Iterable serialized,
+  IsolateStart deserialize(Serializers serializers, Iterable<Object> serialized,
       {FullType specifiedType = FullType.unspecified}) {
     final result = new IsolateStartBuilder();
 
diff --git a/dwds/lib/data/run_request.g.dart b/dwds/lib/data/run_request.g.dart
index a7e1062..008cc44 100644
--- a/dwds/lib/data/run_request.g.dart
+++ b/dwds/lib/data/run_request.g.dart
@@ -15,13 +15,13 @@
   final String wireName = 'RunRequest';
 
   @override
-  Iterable serialize(Serializers serializers, RunRequest object,
+  Iterable<Object> serialize(Serializers serializers, RunRequest object,
       {FullType specifiedType = FullType.unspecified}) {
     return <Object>[];
   }
 
   @override
-  RunRequest deserialize(Serializers serializers, Iterable serialized,
+  RunRequest deserialize(Serializers serializers, Iterable<Object> serialized,
       {FullType specifiedType = FullType.unspecified}) {
     return new RunRequestBuilder().build();
   }
diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart
index a124117..ee4cc8b 100644
--- a/dwds/lib/dwds.dart
+++ b/dwds/lib/dwds.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 
 import 'package:build_daemon/data/build_status.dart';
+import 'package:dwds/src/utilities/shared.dart';
 import 'package:logging/logging.dart';
 import 'package:meta/meta.dart';
 import 'package:shelf/shelf.dart';
@@ -24,6 +25,7 @@
 typedef LogWriter = void Function(Level, String);
 typedef ConnectionProvider = Future<ChromeConnection> Function();
 enum ReloadConfiguration { none, hotReload, hotRestart, liveReload }
+enum ModuleStrategy { requireJS, legacy }
 
 /// The Dart Web Debug Service.
 class Dwds {
@@ -58,6 +60,7 @@
     LogWriter logWriter,
     bool verbose,
     bool enableDebugExtension,
+    ModuleStrategy moduleStrategy,
   }) async {
     hostname ??= 'localhost';
     reloadConfiguration ??= ReloadConfiguration.none;
@@ -66,6 +69,7 @@
     serveDevTools = serveDevTools || enableDebugExtension;
     logWriter ??= (level, message) => print(message);
     verbose ??= false;
+    globalModuleStrategy = moduleStrategy ?? ModuleStrategy.requireJS;
     var assetHandler = AssetHandler(
       assetServerPort,
       applicationTarget,
diff --git a/dwds/lib/src/debugging/dart_scope.dart b/dwds/lib/src/debugging/dart_scope.dart
new file mode 100644
index 0000000..67feb7a
--- /dev/null
+++ b/dwds/lib/src/debugging/dart_scope.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. 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:dwds/src/utilities/shared.dart';
+
+import '../utilities/objects.dart';
+import 'debugger.dart';
+
+/// Find the visible Dart properties from a JS Scope Chain, coming from the
+/// scopeChain attribute of a Chrome CallFrame corresponding to [callFrameId].
+///
+/// See
+/// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#type-CallFrame
+///
+/// The [scopeList] is a List of Maps corresponding to Chrome Scope objects.
+/// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#type-Scope
+Future<List<Property>> visibleProperties(
+    {List<Map<String, dynamic>> scopeList,
+    Debugger debugger,
+    String callFrameId}) async {
+  // We skip the global and the outer library scope and assume everything before
+  // that is a method scope.
+  var numberOfMethods = scopeList.length - 2;
+  if (numberOfMethods <= 0) return [];
+  var methodScopes = scopeList.sublist(0, numberOfMethods);
+  var propertyLists = methodScopes
+      .map((scope) async =>
+          await debugger.getProperties(scope['object']['objectId'] as String))
+      .toList();
+  var allProperties = [for (var list in propertyLists) ...await list];
+  var existingThis =
+      allProperties.firstWhere((x) => x.name == 'this', orElse: () => null);
+  if (existingThis == null) {
+    var syntheticThis = await _findMissingThis(callFrameId, debugger);
+    if (syntheticThis != null) {
+      allProperties.add(syntheticThis);
+    }
+  }
+  return allProperties;
+}
+
+/// Find the `this` in scope if it wasn't in the provided data from Chrome.
+///
+/// If we were not given a `this` value in the Chrome scopes that might mean
+/// we're in a nested closure, or we might be a top-level function. Find it by evaluating
+/// code in the JS frame. If it's null/undefined or is a Dart library scope, then
+/// return null. Otherwise make a property for `this` and return it.
+Future<Property> _findMissingThis(String callFrameId, Debugger debugger) async {
+  // If 'this' is a library return null, otherwise
+  // return 'this'.
+  final findCurrent = '''
+        (function (THIS) { 
+           if (THIS === window) { return null; }
+           let dart = $loadModule('dart_sdk').dart
+           let libs = dart.getLibraries().map(dart.getLibrary);
+           for (let lib of libs) {
+             if (lib === THIS) {
+                return null;
+            }
+            } return THIS; })(this)''';
+
+  var actualThis =
+      await debugger.evaluateJsOnCallFrame(callFrameId, findCurrent);
+  return (actualThis.type == 'undefined')
+      ? null
+      : Property({'name': 'this', 'value': actualThis});
+}
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart
index 2f752bc..c739f2a 100644
--- a/dwds/lib/src/debugging/debugger.dart
+++ b/dwds/lib/src/debugging/debugger.dart
@@ -4,15 +4,19 @@
 
 import 'dart:async';
 
-import 'package:dwds/src/debugging/remote_debugger.dart';
+import 'package:dwds/src/utilities/shared.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+import '../../dwds.dart' show LogWriter;
+import '../handlers/asset_handler.dart';
 import '../services/chrome_proxy_service.dart';
 import '../utilities/dart_uri.dart';
 import '../utilities/domain.dart';
 import '../utilities/objects.dart';
+import 'dart_scope.dart';
 import 'location.dart';
+import 'remote_debugger.dart';
 import 'sources.dart';
 
 /// Converts from ExceptionPauseMode strings to [PauseState] enums.
@@ -26,13 +30,13 @@
 };
 
 class Debugger extends Domain {
-  final RemoteDebugger _remoteDebugger;
-
   final AssetHandler _assetHandler;
-  final StreamNotify _streamNotify;
+  final LogWriter _logWriter;
+  final RemoteDebugger _remoteDebugger;
 
   /// The root URI from which the application is served.
   final String _root;
+  final StreamNotify _streamNotify;
 
   Debugger._(
     this._assetHandler,
@@ -41,6 +45,7 @@
     AppInspectorProvider provider,
     // TODO(401) - Remove.
     this._root,
+    this._logWriter,
   )   : _breakpoints = _Breakpoints(provider),
         super(provider);
 
@@ -56,6 +61,15 @@
 
   Stack _pausedStack;
 
+  /// The JS frames corresponding to [_pausedStack].
+  ///
+  /// The most important thing here is that frames are identified by
+  /// frameIndex in the Dart API, but by frame Id in Chrome, so we need
+  /// to keep the JS frames and their Ids around.
+  // TODO(alanknight): It would be nice to keep these as CallFrame instances,
+  // but they don't map enough of the data yet.
+  List<Map<String, dynamic>> _pausedJsStack;
+
   bool _isStepping = false;
 
   Future<Success> pause() async {
@@ -125,11 +139,13 @@
   }
 
   static Future<Debugger> create(
-      AssetHandler assetHandler,
-      RemoteDebugger remoteDebugger,
-      StreamNotify streamNotify,
-      AppInspectorProvider appInspectorProvider,
-      String root) async {
+    AssetHandler assetHandler,
+    RemoteDebugger remoteDebugger,
+    StreamNotify streamNotify,
+    AppInspectorProvider appInspectorProvider,
+    String root,
+    LogWriter logWriter,
+  ) async {
     var debugger = Debugger._(
       assetHandler,
       remoteDebugger,
@@ -137,13 +153,14 @@
       appInspectorProvider,
       // TODO(401) - Remove.
       root,
+      logWriter,
     );
     await debugger._initialize();
     return debugger;
   }
 
   Future<Null> _initialize() async {
-    sources = Sources(_assetHandler, _remoteDebugger);
+    sources = Sources(_assetHandler, _remoteDebugger, _logWriter);
     // We must add a listener before enabling the debugger otherwise we will
     // miss events.
     // Allow a null debugger/connection for unit tests.
@@ -280,8 +297,9 @@
       if (dartFrame != null) {
         dartFrame.code.name = functionName.isEmpty ? '<closure>' : functionName;
         dartFrame.index = index++;
-        dartFrame.vars =
-            await _variablesFor(frame['scopeChain'] as List<dynamic>);
+        dartFrame.vars = await variablesFor(
+            frame['scopeChain'] as List<dynamic>,
+            frame['callFrameId'] as String);
         dartFrames.add(dartFrame);
       }
     }
@@ -289,25 +307,34 @@
   }
 
   /// The variables visible in a frame in Dart protocol [BoundVariable] form.
-  Future<List<BoundVariable>> _variablesFor(List<dynamic> scopeChain) async {
-    // TODO: Much better logic for which frames to use. This is probably just
-    // the dynamically visible variables, so we should omit library scope.
-    return [
-      for (var scope in scopeChain.take(2)) ...await _boundVariables(scope)
-    ];
+  Future<List<BoundVariable>> variablesFor(
+      List<dynamic> scopeChain, String callFrameId) async {
+    // TODO(alanknight): Can these be moved to dart_scope.dart?
+    var properties = await visibleProperties(
+        scopeList: scopeChain.cast<Map<String, dynamic>>().toList(),
+        debugger: this,
+        callFrameId: callFrameId);
+    var boundVariables = await Future.wait(
+        properties.map((property) async => await _boundVariable(property)));
+    boundVariables = boundVariables.where((bv) => bv != null).toList();
+    boundVariables.sort((a, b) => a.name.compareTo(b.name));
+    return boundVariables;
   }
 
-  /// The [BoundVariable]s visible in a v8 'scope' object as found in the
-  /// 'scopeChain' field of the 'callFrames' in a DebuggerPausedEvent.
-  Future<Iterable<BoundVariable>> _boundVariables(dynamic scope) async {
-    var properties = await getProperties(scope['object']['objectId'] as String);
+  Future<BoundVariable> _boundVariable(Property property) async {
     // We return one level of properties from this object. Sub-properties are
     // another round trip.
-    var refs = properties
-        .map<Future<BoundVariable>>((property) async => BoundVariable()
-          ..name = property.name
-          ..value = await inspector.instanceRefFor(property.value));
-    return Future.wait(refs);
+    var instanceRef =
+        await inspector.instanceHelper.instanceRefFor(property.value);
+    // Skip null instance refs, which we get for weird objects, e.g.
+    // properties that are getter/setter pairs.
+    // TODO(alanknight): Handle these properly.
+    if (instanceRef == null) return null;
+    return BoundVariable()
+      ..name = property.name
+      ..value = instanceRef
+      // TODO(grouma) - Provide an actual token position.
+      ..declarationTokenPos = -1;
   }
 
   /// Calls the Chrome Runtime.getProperties API for the object with [id].
@@ -345,7 +372,9 @@
     var script =
         await inspector?.scriptRefFor(bestLocation.dartLocation.uri.serverPath);
     return Frame()
-      ..code = (CodeRef()..kind = CodeKind.kDart)
+      ..code = (CodeRef()
+        ..id = createId()
+        ..kind = CodeKind.kDart)
       ..location = (SourceLocation()
         ..tokenPos = bestLocation.tokenPos
         ..script = script)
@@ -377,6 +406,7 @@
     _pausedStack = Stack()
       ..frames = frames
       ..messages = [];
+    _pausedJsStack = jsFrames;
     if (frames.isNotEmpty) event.topFrame = frames.first;
     isolate.pauseEvent = event;
     _streamNotify('Debug', event);
@@ -389,12 +419,42 @@
     var isolate = inspector?.isolate;
     if (isolate == null) return;
     _pausedStack = null;
+    _pausedJsStack = null;
     var event = Event()
       ..kind = EventKind.kResume
       ..isolate = inspector.isolateRef;
     isolate.pauseEvent = event;
     _streamNotify('Debug', event);
   }
+
+  /// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on
+  /// the call frame with index [frameIndex] in the currently saved stack.
+  ///
+  /// If the program is not paused, so there is no current stack, throws a
+  /// [StateError].
+  Future<RemoteObject> evaluateJsOnCallFrameIndex(
+      int frameIndex, String expression) {
+    if (_pausedJsStack == null) {
+      throw StateError(
+          'Cannot evaluate on a call frame when the program is not paused');
+    }
+    return evaluateJsOnCallFrame(
+        _pausedJsStack[frameIndex]['callFrameId'] as String, expression);
+  }
+
+  /// Evaluate [expression] by calling Chrome's Runtime.evaluateOnCallFrame on
+  /// the call frame with id [callFrameId].
+  Future<RemoteObject> evaluateJsOnCallFrame(
+      String callFrameId, String expression) async {
+    // TODO(alanknight): Support a version with arguments if needed.
+    WipResponse result;
+    result = await _remoteDebugger.sendCommand('Debugger.evaluateOnCallFrame',
+        params: {'callFrameId': callFrameId, 'expression': expression});
+    handleErrorIfPresent(result, evalContents: expression, additionalDetails: {
+      'Dart expression': expression,
+    });
+    return RemoteObject(result.result['result'] as Map<String, dynamic>);
+  }
 }
 
 /// Keeps track of the Dart and JS breakpoint Ids that correspond.
diff --git a/dwds/lib/src/debugging/exceptions.dart b/dwds/lib/src/debugging/exceptions.dart
new file mode 100644
index 0000000..c941a6a
--- /dev/null
+++ b/dwds/lib/src/debugging/exceptions.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.import 'dart:async';
+
+import 'package:shelf/shelf.dart' as shelf;
+
+class ScriptNotFound implements Exception {
+  final String scriptPath;
+  final shelf.Response response;
+
+  ScriptNotFound(this.scriptPath, this.response) : super();
+
+  @override
+  String toString() => '''
+Failed to load script at path: $scriptPath
+
+Response status code: ${response.statusCode}
+''';
+}
diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart
index 6175a2a..ac020e7 100644
--- a/dwds/lib/src/debugging/inspector.dart
+++ b/dwds/lib/src/debugging/inspector.dart
@@ -2,18 +2,22 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.import 'dart:async';
 
-import 'dart:math' as math show min;
+import 'dart:io';
 
 import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:path/path.dart' as p;
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+import '../handlers/asset_handler.dart';
 import '../services/chrome_proxy_service.dart';
 import '../utilities/dart_uri.dart';
 import '../utilities/domain.dart';
 import '../utilities/shared.dart';
 import 'debugger.dart';
+import 'exceptions.dart';
+import 'instance.dart';
+import 'metadata.dart';
 
 /// An inspector for a running Dart application contained in the
 /// [WipConnection].
@@ -44,6 +48,7 @@
   final Debugger debugger;
   final Isolate isolate;
   final IsolateRef isolateRef;
+  final InstanceHelper instanceHelper;
 
   /// The root URI from which the application is served.
   final String _root;
@@ -55,6 +60,7 @@
     this._root,
     this._remoteDebugger,
   )   : isolateRef = _toIsolateRef(isolate),
+        instanceHelper = InstanceHelper(debugger, _remoteDebugger),
         super.forInspector();
 
   @override
@@ -103,23 +109,17 @@
   }
 
   /// Get the value of the field named [fieldName] from [receiver].
-  ///
-  /// Note that the returned [RemoteObject] might be for a simple value, and
-  /// [AppInspector._asDartObject] can be used to get the value.
   Future<RemoteObject> loadField(RemoteObject receiver, String fieldName) {
     var load = '''
         function() {
-          return require("dart_sdk").dart.dloadRepl(this, "$fieldName");
+          return $loadModule("dart_sdk").dart.dloadRepl(this, "$fieldName");
         }
         ''';
-    return _callFunctionOn(receiver, load, _marshallArguments([]));
+    return callFunctionOn(receiver, load, _marshallArguments([]));
   }
 
   /// Call a method by name on [receiver], with arguments [positionalArgs] and
   /// [namedArgs].
-  ///
-  /// Note that the returned [RemoteObject] might be for a simple value, and
-  /// [AppInspector._asDartObject] can be used to get the value.
   Future<RemoteObject> sendMessage(RemoteObject receiver, String methodName,
       [List positionalArgs = const [], Map namedArgs = const {}]) async {
     // TODO(alanknight): Support named arguments.
@@ -129,36 +129,20 @@
     var send = '''
         function (positional) {
           if (!(this.__proto__)) { return 'Instance of PlainJavaScriptObject';}
-          return require("dart_sdk").dart.dsendRepl(this, "$methodName", positional);
+          return $loadModule("dart_sdk").dart.dsendRepl(this, "$methodName", positional);
         }
         ''';
     var arguments = _marshallArguments(positionalArgs);
-    var remote = await _callFunctionOn(receiver, send, arguments);
+    var remote = await callFunctionOn(receiver, send, arguments);
     return remote;
   }
 
-  /// Given [remote], if it's a simple type, return the value, otherwise leave
-  /// it as a RemoteObject.
-  // TODO(alanknight): Consider our own RemoteObject class where we could add
-  // methods to do things like this.
-  Object _asDartObject(RemoteObject remote) {
-    if (remote.type == 'object') {
-      if (remote.objectId == null) {
-        return null;
-      } else {
-        return remote;
-      }
-    } else {
-      return remote.value;
-    }
-  }
-
   /// Calls Chrome's Runtime.callFunctionOn method.
   ///
   /// [arguments] is expected to be in the form returned by
   /// [_marshallArguments]. [evalExpression] should be a function definition
   /// that can accept [arguments].
-  Future<RemoteObject> _callFunctionOn(
+  Future<RemoteObject> callFunctionOn(
       RemoteObject receiver, String evalExpression, List arguments) async {
     var result =
         await _remoteDebugger.sendCommand('Runtime.callFunctionOn', params: {
@@ -192,12 +176,6 @@
     }
   }
 
-  /// Call the Dart toString for [receiver].
-  Future<String> toStringOf(RemoteObject receiver) async {
-    var remote = await sendMessage(receiver, 'toString', []);
-    return _asDartObject(remote) as String;
-  }
-
   Future<RemoteObject> evaluate(
       String isolateId, String targetId, String expression,
       {Map<String, String> scope, bool disableBreakpoints}) async {
@@ -277,55 +255,6 @@
     return RemoteObject(result.result['result'] as Map<String, dynamic>);
   }
 
-  /// Create an [InstanceRef] for the given Chrome [remoteObject].
-  Future<InstanceRef> instanceRefFor(RemoteObject remoteObject) async {
-    // If we have a null result, treat it as a reference to null.
-    if (remoteObject == null) {
-      return _primitiveInstance(InstanceKind.kNull, remoteObject);
-    }
-    switch (remoteObject.type) {
-      case 'string':
-        return _primitiveInstance(InstanceKind.kString, remoteObject);
-      case 'number':
-        return _primitiveInstance(InstanceKind.kDouble, remoteObject);
-      case 'boolean':
-        return _primitiveInstance(InstanceKind.kBool, remoteObject);
-      case 'undefined':
-        return _primitiveInstance(InstanceKind.kNull, remoteObject);
-      case 'object':
-        if (_asDartObject(remoteObject) == null) {
-          return _primitiveInstance(InstanceKind.kNull, remoteObject);
-        }
-        var toString = await toStringOf(remoteObject);
-        // TODO: Make the truncation consistent with the VM.
-        var truncated = toString.substring(0, math.min(100, toString.length));
-        return InstanceRef()
-          ..kind = InstanceKind.kPlainInstance
-          ..id = remoteObject.objectId
-          ..valueAsString = toString
-          ..valueAsStringIsTruncated = truncated.length != toString.length
-          // TODO(jakemac): Create a real ClassRef, we need a way of looking
-          // up the library for a given instance to create it though.
-          // https://github.com/dart-lang/sdk/issues/36771.
-          ..classRef = ClassRef();
-      case 'function':
-        var crudeAttemptAtName = remoteObject.description.split('(').first;
-        return InstanceRef()
-          ..kind = InstanceKind.kPlainInstance
-          ..id = remoteObject.objectId
-          ..valueAsString = crudeAttemptAtName
-          ..classRef = ClassRef();
-      default:
-        // Return unsupported types as a String placeholder for now.
-        var unsupported = RemoteObject({
-          'type': 'String',
-          'value':
-              'Unsupported type:${remoteObject.type} (${remoteObject.description})'
-        });
-        return _primitiveInstance(InstanceKind.kString, unsupported);
-    }
-  }
-
   Future<Library> _getLibrary(String isolateId, String objectId) async {
     if (isolateId != isolate.id) return null;
     var libraryRef = _libraryRefs[objectId];
@@ -345,8 +274,11 @@
     if (clazz != null) return clazz;
     var scriptRef = _scriptRefs[objectId];
     if (scriptRef != null) return await _getScript(isolateId, scriptRef);
-    throw UnsupportedError(
-        'Only libraries and classes are supported for getObject');
+    var instance =
+        instanceHelper.instanceFor(RemoteObject({'objectId': objectId}));
+    if (instance != null) return instance;
+    throw UnsupportedError('Only libraries, instances, classes, and scripts '
+        'are supported for getObject');
   }
 
   Future<Library> _constructLibrary(LibraryRef libraryRef) async {
@@ -356,7 +288,9 @@
       ${_getLibrarySnippet(libraryRef.uri)}
       var parts = sdkUtils.getParts('${libraryRef.uri}');
       var result = {'parts' : parts}
-      var classes = Object.values(library)
+      var classes = Object.values(Object.getOwnPropertyDescriptors(library))
+        .filter((p) => 'value' in p)
+        .map((p) => p.value)
         .filter((l) => l && sdkUtils.isType(l));
       var classList = classes.map(function(clazz) {
         var descriptor = {'name': clazz.name};
@@ -401,18 +335,18 @@
         .cast<Map<String, Object>>();
     var classRefs = <ClassRef>[];
     for (var classDescriptor in classDescriptors) {
-      var name = classDescriptor['name'] as String;
-      var classId = '${libraryRef.id}:$name';
+      var classMetaData =
+          ClassMetaData(classDescriptor['name'] as String, libraryRef.id);
       var classRef = ClassRef()
-        ..name = name
-        ..id = classId;
+        ..name = classMetaData.name
+        ..id = classMetaData.id;
       classRefs.add(classRef);
 
       var methodRefs = <FuncRef>[];
       var methodDescriptors =
           classDescriptor['methods'] as Map<String, dynamic>;
       methodDescriptors.forEach((name, descriptor) {
-        var methodId = '$classId:$name';
+        var methodId = '${classMetaData.id}:$name';
         methodRefs.add(FuncRef()
           ..id = methodId
           ..name = name
@@ -426,7 +360,6 @@
       fieldDescriptors.forEach((name, descriptor) {
         fieldRefs.add(FieldRef()
           ..name = name
-          ..declaredType = (InstanceRef()..classRef = ClassRef())
           ..owner = classRef
           ..isConst = descriptor['isConst'] as bool
           ..isFinal = descriptor['isFinal'] as bool
@@ -435,13 +368,13 @@
 
       // TODO: Implement the rest of these
       // https://github.com/dart-lang/webdev/issues/176.
-      _classes[classId] = Class()
+      _classes[classMetaData.id] = Class()
         ..classRef = classRef
         ..fields = fieldRefs
         ..functions = methodRefs
-        ..id = classId
+        ..id = classMetaData.id
         ..library = libraryRef
-        ..name = name
+        ..name = classMetaData.name
         ..interfaces = []
         ..subclasses = [];
     }
@@ -486,7 +419,11 @@
   Future<Script> _getScript(String isolateId, ScriptRef scriptRef) async {
     var libraryId = _scriptIdToLibraryId[scriptRef.id];
     var serverPath = DartUri(scriptRef.uri, _root).serverPath;
-    var script = await _assetHandler(serverPath);
+    var response = await _assetHandler.getRelativeAsset(serverPath);
+    if (response.statusCode != HttpStatus.ok) {
+      throw ScriptNotFound(serverPath, response);
+    }
+    var script = await response.readAsString();
     return Script()
       ..library = _libraryRefs[libraryId]
       ..id = scriptRef.id
@@ -533,7 +470,7 @@
   /// Note this can return a cached result.
   Future<List<LibraryRef>> _getLibraryRefs() async {
     if (_libraryRefs.isNotEmpty) return _libraryRefs.values.toList();
-    var expression = "require('dart_sdk').dart.getLibraries();";
+    var expression = "$loadModule('dart_sdk').dart.getLibraries();";
     var librariesResult = await _remoteDebugger.sendCommand('Runtime.evaluate',
         params: {'expression': expression, 'returnByValue': true});
     handleErrorIfPresent(librariesResult, evalContents: expression);
@@ -551,7 +488,8 @@
 
   /// Runs an eval on the page to compute all existing registered extensions.
   Future<List<String>> _getExtensionRpcs() async {
-    var expression = "require('dart_sdk').developer._extensions.keys.toList();";
+    var expression =
+        "$loadModule('dart_sdk').developer._extensions.keys.toList();";
     var extensionsResult = await _remoteDebugger.sendCommand('Runtime.evaluate',
         params: {'expression': expression, 'returnByValue': true});
     handleErrorIfPresent(extensionsResult, evalContents: expression);
@@ -571,20 +509,9 @@
 /// Dart-specific scheme URIs, and we set `library` the corresponding library.
 String _getLibrarySnippet(String libraryUri) => '''
   var libraryName = '$libraryUri';
-  var sdkUtils = require('dart_sdk').dart;
+  var sdkUtils = $loadModule('dart_sdk').dart;
   var moduleName = sdkUtils.getModuleNames().find(
     (name) => sdkUtils.getModuleLibraries(name)[libraryName]);
   var library = sdkUtils.getModuleLibraries(moduleName)[libraryName];
   if (!library) throw 'cannot find library for ' + libraryName + ' under ' + moduleName;
 ''';
-
-/// Creates an [InstanceRef] for a primitive [RemoteObject].
-InstanceRef _primitiveInstance(String kind, RemoteObject remoteObject) {
-  var classRef = ClassRef()
-    ..id = 'dart:core:${remoteObject.type}'
-    ..name = kind;
-  return InstanceRef()
-    ..valueAsString = '${remoteObject.value}'
-    ..classRef = classRef
-    ..kind = kind;
-}
diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart
new file mode 100644
index 0000000..285dbc5
--- /dev/null
+++ b/dwds/lib/src/debugging/instance.dart
@@ -0,0 +1,193 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.import 'dart:async';
+
+import 'package:dwds/src/utilities/shared.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import '../utilities/objects.dart';
+import 'debugger.dart';
+import 'metadata.dart';
+import 'remote_debugger.dart';
+
+/// Chrome doesn't give us an objectId for a String. So we use the string
+/// as its own ID, with a prefix.
+///
+/// This should not be confused with any
+/// other object Ids, as those will be Chrome objectIds, which are
+/// opaque, but are JSON serialized objects of the form
+/// "{\"injectedScriptId\":1,\"id\":1}".
+const _prefixForStringIds = '#StringInstanceRef#';
+
+/// Creates an [InstanceRef] for a primitive [RemoteObject].
+InstanceRef _primitiveInstance(String kind, RemoteObject remoteObject) {
+  var classRef = ClassRef()
+    // TODO(grouma) - is this ID correct?
+    ..id = 'dart:core:${remoteObject?.type}'
+    ..name = kind;
+  return InstanceRef()
+    ..valueAsString = '${remoteObject?.value}'
+    ..classRef = classRef
+    ..kind = kind;
+}
+
+/// A hard-coded ClassRef for the String class.
+final _classRefForString = ClassRef()
+  ..id = 'dart:core:String'
+  ..name = InstanceKind.kString;
+
+/// A hard-coded ClassRef for the Closure class.
+// TODO(grouma) - orgnaize our static classRefs better.
+final _classRefForClosure = ClassRef()
+  ..name = 'Closure'
+  ..id = createId();
+
+/// Contains a set of methods for getting [Instance]s and [InstanceRef]s.
+class InstanceHelper {
+  final Debugger _debugger;
+  final RemoteDebugger _remoteDebugger;
+
+  InstanceHelper(this._debugger, this._remoteDebugger);
+
+  Future<Instance> _stringInstanceFor(RemoteObject remoteObject) async {
+    var actualString =
+        remoteObject.objectId.substring(_prefixForStringIds.length);
+    return Instance()
+      ..kind = InstanceKind.kString
+      ..classRef = _classRefForString
+      ..valueAsString = actualString
+      ..length = actualString.length;
+  }
+
+  Future<Instance> _closureInstanceFor(RemoteObject remoteObject) async {
+    var functionMetaData =
+        await FunctionMetaData.metaDataFor(_remoteDebugger, remoteObject);
+    var result = Instance()
+      ..kind = InstanceKind.kClosure
+      ..id = remoteObject.objectId
+      ..closureFunction = (FuncRef()
+            ..name = functionMetaData.name
+            ..id = createId()
+          // TODO(grouma) - fill these in.
+          //..owner = ?
+          //..isConst = ?
+          //..isStatic = ?
+          )
+      ..closureContext = ContextRef()
+      ..classRef = _classRefForClosure;
+    return result;
+  }
+
+  /// Create an [Instance] for the given [remoteObject].
+  ///
+  /// Does a remote eval to get instance information. Returns null if there
+  /// isn't a corresponding instance.
+  Future<Instance> instanceFor(RemoteObject remoteObject) async {
+    if (remoteObject.objectId.startsWith(_prefixForStringIds)) {
+      return _stringInstanceFor(remoteObject);
+    }
+    var metaData =
+        await ClassMetaData.metaDataFor(_remoteDebugger, remoteObject);
+    if (metaData.name == 'Function') {
+      return _closureInstanceFor(remoteObject);
+    } else {
+      var classRef = ClassRef()
+        ..id = metaData.id
+        ..name = metaData.name;
+      var properties = await _debugger.getProperties(remoteObject.objectId);
+      var dartProperties = await dartPropertiesFor(properties, remoteObject);
+      var fields = await Future.wait(
+          dartProperties.map<Future<BoundField>>((property) async {
+        var instance = await instanceRefFor(property.value);
+        return BoundField()
+          ..decl = (FieldRef()
+            // TODO(grouma) - Convert JS name to Dart.
+            ..name = property.name
+            ..declaredType = (InstanceRef()
+              ..type = InstanceKind.kType
+              ..classRef = instance.classRef)
+            ..owner = classRef)
+          ..value = instance;
+      }));
+      fields.sort((a, b) => a.decl.name.compareTo(b.decl.name));
+      var result = Instance()
+        ..kind = InstanceKind.kPlainInstance
+        ..id = remoteObject.objectId
+        ..fields = fields
+        ..classRef = classRef;
+      return result;
+    }
+  }
+
+  /// Filter [allJsProperties] and return a list containing only those
+  /// that correspond to Dart fields on the object.
+  Future<List<Property>> dartPropertiesFor(
+      List<Property> allJsProperties, RemoteObject remoteObject) async {
+    // An expression to find the field names from the types, extract both
+    // private (named by symbols) and public (named by strings) and return them
+    // as a comma-separated single string, so we can return it by value and not
+    // need to make multiple round trips.
+    // TODO(alanknight): Handle superclass fields.
+    final fieldNameExpression = '''function() {
+      const sdk_utils = $loadModule("dart_sdk").dart;
+      const fields = sdk_utils.getFields(sdk_utils.getType(this));
+      const privateFields = Object.getOwnPropertySymbols(fields);
+      const nonSymbolNames = privateFields.map(sym => sym.description);
+      const publicFieldNames = Object.getOwnPropertyNames(fields);
+      return nonSymbolNames.concat(publicFieldNames).join(',');
+    }
+    ''';
+    var allNames = (await _debugger.inspector
+            .callFunctionOn(remoteObject, fieldNameExpression, []))
+        .value as String;
+    var names = allNames.split(',');
+    return allJsProperties
+        .where((property) => names.contains(property.name))
+        .toList();
+  }
+
+  /// Create an [InstanceRef] for the given Chrome [remoteObject].
+  Future<InstanceRef> instanceRefFor(RemoteObject remoteObject) async {
+    // If we have a null result, treat it as a reference to null.
+    if (remoteObject == null) {
+      return _primitiveInstance(InstanceKind.kNull, remoteObject);
+    }
+    switch (remoteObject.type) {
+      case 'string':
+        return InstanceRef()
+          // See comment for [_prefixForStringIds].
+          ..id = '$_prefixForStringIds${remoteObject.value}'
+          ..valueAsString = remoteObject.value as String
+          ..classRef = _classRefForString
+          ..kind = InstanceKind.kString;
+      case 'number':
+        return _primitiveInstance(InstanceKind.kDouble, remoteObject);
+      case 'boolean':
+        return _primitiveInstance(InstanceKind.kBool, remoteObject);
+      case 'undefined':
+        return _primitiveInstance(InstanceKind.kNull, remoteObject);
+      case 'object':
+        if (remoteObject.type == 'object' && remoteObject.objectId == null) {
+          return _primitiveInstance(InstanceKind.kNull, remoteObject);
+        }
+        var metaData =
+            await ClassMetaData.metaDataFor(_remoteDebugger, remoteObject);
+        if (metaData == null) return null;
+        return InstanceRef()
+          ..kind = InstanceKind.kPlainInstance
+          ..id = remoteObject.objectId
+          ..classRef = (ClassRef()
+            ..name = metaData.name
+            ..id = metaData.id);
+      case 'function':
+        return InstanceRef()
+          ..kind = InstanceKind.kClosure
+          ..id = remoteObject.objectId
+          ..classRef = _classRefForClosure;
+      default:
+        // Return null for an unsupported type. This is likely a JS construct.
+        return null;
+    }
+  }
+}
diff --git a/dwds/lib/src/debugging/metadata.dart b/dwds/lib/src/debugging/metadata.dart
new file mode 100644
index 0000000..124b52b
--- /dev/null
+++ b/dwds/lib/src/debugging/metadata.dart
@@ -0,0 +1,97 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.import 'dart:async';
+
+import 'package:dwds/src/utilities/shared.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import '../services/chrome_proxy_service.dart';
+import 'remote_debugger.dart';
+
+/// Meta data for a remote Dart class in Chrome.
+class ClassMetaData {
+  final String name;
+  final String libraryId;
+  ClassMetaData(this.name, this.libraryId);
+
+  /// Returns the ID of the class.
+  ///
+  /// Takes the form of 'libraryId:name'.
+  String get id => '$libraryId:$name';
+
+  /// Returns the [ClassMetaData] for the Chrome [remoteObject].
+  ///
+  /// Returns null if the [remoteObject] is not a Dart class.
+  static Future<ClassMetaData> metaDataFor(
+      RemoteDebugger remoteDebugger, RemoteObject remoteObject) async {
+    try {
+      var evalExpression = '''
+      function(arg) {
+        var sdkUtils = $loadModule('dart_sdk').dart;
+        var classObject = sdkUtils.getType(arg);
+        var result = {};
+        result['name'] = classObject.name;
+        result['libraryId'] = sdkUtils.getLibraryUri(classObject);
+        return result;
+      }
+    ''';
+      var arguments = [
+        {'objectId': remoteObject.objectId}
+      ];
+      var result =
+          await remoteDebugger.sendCommand('Runtime.callFunctionOn', params: {
+        'functionDeclaration': evalExpression,
+        'arguments': arguments,
+        'objectId': remoteObject.objectId,
+        'returnByValue': true,
+      });
+      handleErrorIfPresent(
+        result,
+        evalContents: evalExpression,
+      );
+      return ClassMetaData(result.result['result']['value']['name'] as String,
+          result.result['result']['value']['libraryId'] as String);
+    } on ChromeDebugException {
+      return null;
+    }
+  }
+}
+
+/// Meta data for a remote Dart function in Chrome.
+class FunctionMetaData {
+  final String name;
+  FunctionMetaData(this.name);
+
+  /// Returns the [FunctionMetaData] for the Chrome [remoteObject].
+  static Future<FunctionMetaData> metaDataFor(
+      RemoteDebugger remoteDebugger, RemoteObject remoteObject) async {
+    var evalExpression = '''
+      function(remoteObject) {
+        var sdkUtils = $loadModule('dart_sdk').dart;
+        var name = remoteObject.name;
+        if(remoteObject._boundObject) {
+         name = sdkUtils.getType(remoteObject._boundObject).name +
+                 '.' + remoteObject._boundMethod.name;
+        }
+        return name;
+      }
+    ''';
+    var arguments = [
+      {'objectId': remoteObject.objectId}
+    ];
+    var response =
+        await remoteDebugger.sendCommand('Runtime.callFunctionOn', params: {
+      'functionDeclaration': evalExpression,
+      'arguments': arguments,
+      'objectId': remoteObject.objectId,
+      'returnByValue': true,
+    });
+    handleErrorIfPresent(
+      response,
+      evalContents: evalExpression,
+    );
+    var name = response.result['result']['value'] as String;
+    if (name.isEmpty) name = 'Closure';
+    return FunctionMetaData(name);
+  }
+}
diff --git a/dwds/lib/src/debugging/remote_debugger.dart b/dwds/lib/src/debugging/remote_debugger.dart
index 18cd815..bb9cba4 100644
--- a/dwds/lib/src/debugging/remote_debugger.dart
+++ b/dwds/lib/src/debugging/remote_debugger.dart
@@ -28,5 +28,5 @@
   Stream<DebuggerResumedEvent> get onResumed;
   Stream<ScriptParsedEvent> get onScriptParsed;
   Map<String, WipScript> get scripts;
-  Stream<WipConnection> get onClose;
+  Stream<void> get onClose;
 }
diff --git a/dwds/lib/src/debugging/sources.dart b/dwds/lib/src/debugging/sources.dart
index ab7bcba..a5e9b06 100644
--- a/dwds/lib/src/debugging/sources.dart
+++ b/dwds/lib/src/debugging/sources.dart
@@ -3,12 +3,16 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
 
+import 'package:logging/logging.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_maps/source_maps.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
-import '../services/chrome_proxy_service.dart';
+import '../../dwds.dart' show LogWriter;
+import '../handlers/asset_handler.dart';
 import '../utilities/dart_uri.dart';
 import 'location.dart';
 import 'remote_debugger.dart';
@@ -35,10 +39,15 @@
   /// Paths to black box in the Chrome debugger.
   final _pathsToBlackBox = {'/packages/stack_trace/'};
 
+  /// Use `_readAssetOrNull` instead of using this directly, as it handles
+  /// logging unsuccessful responses.
   final AssetHandler _assetHandler;
+
+  final LogWriter _logWriter;
+
   final RemoteDebugger _remoteDebugger;
 
-  Sources(this._assetHandler, this._remoteDebugger);
+  Sources(this._assetHandler, this._remoteDebugger, this._logWriter);
 
   /// Returns all [Location] data for a provided Dart source.
   Set<Location> locationsForDart(String serverPath) =>
@@ -54,10 +63,10 @@
     var script = e.script;
     // TODO(grouma) - This should be configurable.
     await _blackBoxIfNecessary(script);
-    var sourceMapContents = await _sourceMap(script);
-    if (sourceMapContents == null) return;
     _clearCacheFor(script);
     _sourceToScriptId[script.url] = script.scriptId;
+    var sourceMapContents = await _sourceMapOrNull(script);
+    if (sourceMapContents == null) return;
     // This happens to be a [SingleMapping] today in DDC.
     var mapping = parse(sourceMapContents);
     if (mapping is SingleMapping) {
@@ -132,15 +141,40 @@
     _sourceToScriptId.remove(script.url);
   }
 
+  /// Reads an asset at [path] relative to the server root.
+  ///
+  /// Returns `null` and logs the response if the status is anything other than
+  /// [HttpStatus.ok].
+  Future<String> _readAssetOrNull(String path) async {
+    var response = await _assetHandler.getRelativeAsset(path);
+    if (response.statusCode == HttpStatus.ok) {
+      return response.readAsString();
+    }
+    _logWriter(Level.WARNING, '''
+Failed to load asset at path: $path.
+
+Status code: ${response.statusCode}
+
+Headers:
+${const JsonEncoder.withIndent('  ').convert(response.headers)}
+
+Content:
+${await response.readAsString()}
+''');
+    return null;
+  }
+
   /// The source map for a DDC-compiled JS [script].
-  Future<String> _sourceMap(WipScript script) {
+  ///
+  /// Returns `null` and logs if it can't be read.
+  Future<String> _sourceMapOrNull(WipScript script) {
     var sourceMapUrl = script.sourceMapURL;
     if (sourceMapUrl == null || !sourceMapUrl.endsWith('.ddc.js.map')) {
       return null;
     }
     var scriptPath = DartUri(script.url).serverPath;
-    var sourcemapPath = p.join(p.dirname(scriptPath), sourceMapUrl);
-    return _assetHandler(sourcemapPath);
+    var sourcemapPath = p.url.join(p.url.dirname(scriptPath), sourceMapUrl);
+    return _readAssetOrNull(sourcemapPath);
   }
 
   /// Black boxes the Dart SDK and paths in [_pathsToBlackBox].
@@ -148,16 +182,18 @@
     if (script.url.endsWith('dart_sdk.js')) {
       await _blackBoxSdk(script);
     } else if (_pathsToBlackBox.any((path) => script.url.contains(path))) {
-      var lines =
-          (await _assetHandler(DartUri(script.url).serverPath)).split('\n');
+      var content = await _readAssetOrNull(DartUri(script.url).serverPath);
+      if (content == null) return;
+      var lines = content.split('\n');
       await _blackBoxRanges(script.scriptId, [lines.length]);
     }
   }
 
   /// Black boxes the SDK excluding the range which includes exception logic.
   Future<void> _blackBoxSdk(WipScript script) async {
-    var sdkSourceLines =
-        (await _assetHandler(DartUri(script.url).serverPath)).split('\n');
+    var content = await _readAssetOrNull(DartUri(script.url).serverPath);
+    if (content == null) return;
+    var sdkSourceLines = content.split('\n');
     // TODO(grouma) - Find a more robust way to identify this location.
     var throwIndex = sdkSourceLines.indexWhere(
         (line) => line.contains('dart.throw = function throw_(exception) {'));
diff --git a/dwds/lib/src/handlers/asset_handler.dart b/dwds/lib/src/handlers/asset_handler.dart
index ecd08d0..84fd4ef 100644
--- a/dwds/lib/src/handlers/asset_handler.dart
+++ b/dwds/lib/src/handlers/asset_handler.dart
@@ -28,9 +28,6 @@
   ///
   /// For example the path `main.dart` should return the raw text value of that
   /// corresponding file.
-  Future<String> getRelativeAsset(String path) async {
-    var response = await handler(Request(
-        'GET', Uri.parse('http://$_applicationHost:$_applicationPort/$path')));
-    return await response.readAsString();
-  }
+  Future<Response> getRelativeAsset(String path) async => handler(Request(
+      'GET', Uri.parse('http://$_applicationHost:$_applicationPort/$path')));
 }
diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart
index aee205c..a45faa9 100644
--- a/dwds/lib/src/handlers/dev_handler.dart
+++ b/dwds/lib/src/handlers/dev_handler.dart
@@ -59,7 +59,7 @@
     _sub = buildResults.listen(_emitBuildResults);
     _listen();
     if (_extensionBackend != null) {
-      _startExtensionDebugService();
+      _listenForDebugExtension();
     }
   }
 
@@ -116,8 +116,9 @@
       _hostname,
       webkitDebugger,
       appTab.url,
-      _assetHandler.getRelativeAsset,
+      _assetHandler,
       appInstanceId,
+      _logWriter,
       onResponse: _verbose
           ? (response) {
               if (response['error'] == null) return;
@@ -220,7 +221,7 @@
             .sendCommand('Target.createTarget', params: {
           'newWindow': true,
           'url': 'http://${_devTools.hostname}:${_devTools.port}'
-              '/?hide=none&uri=${appServices.debugService.uri}',
+              '/?uri=${appServices.debugService.uri}',
         });
       } else if (message is ConnectRequest) {
         if (appId != null) {
@@ -282,33 +283,55 @@
     return AppDebugServices(debugService, webdevClient);
   }
 
+  void _listenForDebugExtension() async {
+    while (await _extensionBackend.connections.hasNext) {
+      _startExtensionDebugService();
+    }
+  }
+
   /// Starts a [DebugService] for Dart Debug Extension.
   void _startExtensionDebugService() async {
     var _extensionDebugger = await _extensionBackend.extensionDebugger;
     // Waits for a `DevToolsRequest` to be sent from the extension background
     // when the extension is clicked.
-    // TODO(pisong): Handle multiple `devToolsRequest`.
-    await _extensionDebugger.devToolsRequestStream.first;
-    var debugService = await DebugService.start(
-      _hostname,
-      _extensionDebugger,
-      _extensionDebugger.tabUrl,
-      _assetHandler.getRelativeAsset,
-      _extensionDebugger.appId,
-      onResponse: _verbose
-          ? (response) {
-              if (response['error'] == null) return;
-              _logWriter(Level.WARNING,
-                  'VmService proxy responded with an error:\n$response');
-            }
-          : null,
-    );
-    var appServices =
-        await _createAppDebugServices(_extensionDebugger.appId, debugService);
-    await _extensionDebugger.sendCommand('Target.createTarget', params: {
-      'newWindow': true,
-      'url': 'http://${_devTools.hostname}:${_devTools.port}'
-          '/?hide=none&uri=${appServices.debugService.uri}',
+    _extensionDebugger.devToolsRequestStream.listen((devToolsRequest) async {
+      var appServices =
+          await _servicesByAppId.putIfAbsent(devToolsRequest.appId, () async {
+        var debugService = await DebugService.start(
+          _hostname,
+          _extensionDebugger,
+          devToolsRequest.tabUrl,
+          _assetHandler,
+          devToolsRequest.appId,
+          _logWriter,
+          onResponse: _verbose
+              ? (response) {
+                  if (response['error'] == null) return;
+                  _logWriter(Level.WARNING,
+                      'VmService proxy responded with an error:\n$response');
+                }
+              : null,
+          useSse: true,
+        );
+        var appServices =
+            await _createAppDebugServices(devToolsRequest.appId, debugService);
+        unawaited(appServices.chromeProxyService.remoteDebugger.onClose.first
+            .whenComplete(() {
+          appServices.chromeProxyService.destroyIsolate();
+          appServices.close();
+          _servicesByAppId.remove(devToolsRequest.appId);
+          _logWriter(
+              Level.INFO,
+              'Stopped debug service on '
+              'ws://${appServices.debugService.hostname}:${appServices.debugService.port}\n');
+        }));
+        return appServices;
+      });
+      await _extensionDebugger.sendCommand('Target.createTarget', params: {
+        'newWindow': true,
+        'url': 'http://${_devTools.hostname}:${_devTools.port}'
+            '/?uri=${appServices.debugService.uri}',
+      });
     });
   }
 }
diff --git a/dwds/lib/src/handlers/injected_handler.dart b/dwds/lib/src/handlers/injected_handler.dart
index 99164b0..07a3810 100644
--- a/dwds/lib/src/handlers/injected_handler.dart
+++ b/dwds/lib/src/handlers/injected_handler.dart
@@ -7,6 +7,7 @@
 import 'dart:isolate';
 
 import 'package:crypto/crypto.dart';
+import 'package:dwds/src/utilities/shared.dart';
 import 'package:shelf/shelf.dart';
 
 import '../../dwds.dart';
@@ -63,11 +64,10 @@
             body = bodyLines.sublist(0, extensionIndex).join('\n');
             // The line after the marker calls `main`. We prevent `main` from
             // being called and make it runnable through a global variable.
-            var mainFuntion = bodyLines[extensionIndex + 1]
-                .replaceAll('(', '')
-                .replaceAll(')', '')
+            var mainFunction = bodyLines[extensionIndex + 1]
+                .replaceAll('main()', 'main')
                 .trim();
-            body += _injectedClientJs(configuration, appId, mainFuntion,
+            body += _injectedClientJs(configuration, appId, mainFunction,
                 extensionHostname: extensionHostname,
                 extensionPort: extensionPort);
             body += bodyLines.sublist(extensionIndex + 2).join('\n');
@@ -97,6 +97,7 @@
             window.\$dartRunMain = $mainFunction;
             window.\$dartReloadConfiguration = "$configuration";
             window.\$dartLoader.forceLoadModule('$_clientScript');
+            window.\$loadModuleConfig = "$loadModule";
             ''';
   if (extensionPort != null && extensionHostname != null) {
     injectedBody += '''
diff --git a/dwds/lib/src/servers/extension_debugger.dart b/dwds/lib/src/servers/extension_debugger.dart
index ccbf31e..93d8920 100644
--- a/dwds/lib/src/servers/extension_debugger.dart
+++ b/dwds/lib/src/servers/extension_debugger.dart
@@ -10,6 +10,7 @@
 import 'package:dwds/data/extension_request.dart';
 import 'package:dwds/data/serializers.dart';
 import 'package:dwds/src/debugging/remote_debugger.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
 import 'package:sse/server/sse_handler.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
@@ -25,8 +26,6 @@
   final _eventStreams = <String, Stream>{};
   var _completerId = 0;
 
-  String tabUrl;
-  String appId;
   String instanceId;
 
   final _devToolsRequestController = StreamController<DevToolsRequest>();
@@ -36,6 +35,10 @@
   final _notificationController = StreamController<WipEvent>.broadcast();
   Stream<WipEvent> get onNotification => _notificationController.stream;
 
+  final _closeController = StreamController<WipEvent>.broadcast();
+  @override
+  Stream<WipEvent> get onClose => _closeController.stream;
+
   @override
   Stream<ConsoleAPIEvent> get onConsoleAPICalled => eventStream(
       'Runtime.consoleAPICalled', (WipEvent event) => ConsoleAPIEvent(event));
@@ -66,14 +69,12 @@
         };
         _notificationController.sink.add(WipEvent(map));
       } else if (message is DevToolsRequest) {
-        tabUrl = message.tabUrl;
-        appId = message.appId;
         instanceId = message.instanceId;
         _devToolsRequestController.sink.add(message);
       }
     }, onError: (_) {
       close();
-    });
+    }, onDone: close);
     onScriptParsed.listen((event) {
       _scripts[event.script.scriptId] = event.script;
     });
@@ -103,9 +104,11 @@
 
   @override
   void close() {
+    _closeController.add(WipEvent({}));
     sseConnection.sink.close();
     _notificationController.close();
     _devToolsRequestController.close();
+    _closeController.close();
   }
 
   @override
@@ -141,11 +144,20 @@
   Future<WipResponse> stepOver() => sendCommand('Debugger.stepOver');
 
   @override
-  Future<void> enablePage() => throw UnimplementedError();
+  Future<void> enablePage() => sendCommand('Page.enable');
 
   @override
-  Future<RemoteObject> evaluate(String expression) =>
-      throw UnimplementedError();
+  Future<RemoteObject> evaluate(String expression) async {
+    final response = await sendCommand('Runtime.evaluate', params: {
+      'expression': expression,
+    });
+    if (response.result.containsKey('exceptionDetails')) {
+      throw ChromeDebugException(
+          response.result['exceptionDetails'] as Map<String, dynamic>);
+    } else {
+      return RemoteObject(response.result['result'] as Map<String, dynamic>);
+    }
+  }
 
   @override
   Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
@@ -178,9 +190,6 @@
   @override
   Map<String, WipScript> get scripts => UnmodifiableMapView(_scripts);
 
-  @override
-  Stream<WipConnection> get onClose => throw UnimplementedError();
-
   String _pauseStateToString(PauseState state) {
     switch (state) {
       case PauseState.all:
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 24bdcd9..7a94ed1 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -6,19 +6,18 @@
 import 'dart:convert';
 import 'dart:io';
 
-import 'package:dwds/src/debugging/remote_debugger.dart';
-import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:pub_semver/pub_semver.dart' as semver;
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+import '../../dwds.dart' show LogWriter;
 import '../debugging/debugger.dart';
 import '../debugging/inspector.dart';
+import '../debugging/remote_debugger.dart';
+import '../handlers/asset_handler.dart';
+import '../utilities/dart_uri.dart';
 import '../utilities/shared.dart';
 
-/// A handler for application assets, e.g. Dart sources.
-typedef AssetHandler = Future<String> Function(String);
-
 /// Adds [event] to the stream with [streamId] if there is anybody listening
 /// on that stream.
 typedef StreamNotify = void Function(String streamId, Event event);
@@ -30,6 +29,8 @@
 
 /// A proxy from the chrome debug protocol to the dart vm service protocol.
 class ChromeProxyService implements VmServiceInterface {
+  final LogWriter _logWriter;
+
   /// Cache of all existing StreamControllers.
   ///
   /// These are all created through [onEvent].
@@ -63,6 +64,7 @@
     this.uri,
     this._assetHandler,
     this.remoteDebugger,
+    this._logWriter,
   );
 
   static Future<ChromeProxyService> create(
@@ -70,6 +72,7 @@
     String tabUrl,
     AssetHandler assetHandler,
     String appInstanceId,
+    LogWriter logWriter,
   ) async {
     // TODO: What about `architectureBits`, `targetCPU`, `hostCPU` and `pid`?
     final vm = VM()
@@ -77,8 +80,8 @@
       ..name = 'ChromeDebugProxy'
       ..startTime = DateTime.now().millisecondsSinceEpoch
       ..version = Platform.version;
-    var service =
-        ChromeProxyService._(vm, tabUrl, assetHandler, remoteDebugger);
+    var service = ChromeProxyService._(
+        vm, tabUrl, assetHandler, remoteDebugger, logWriter);
     await service._initialize();
     await service.createIsolate();
     return service;
@@ -91,6 +94,7 @@
       _streamNotify,
       appInspectorProvider,
       uri,
+      _logWriter,
     );
   }
 
@@ -188,7 +192,7 @@
     var stringArgs = args.map((k, v) => MapEntry(
         k is String ? k : jsonEncode(k), v is String ? v : jsonEncode(v)));
     var expression = '''
-require("dart_sdk").developer.invokeExtension(
+$loadModule("dart_sdk").developer.invokeExtension(
     "$method", JSON.stringify(${jsonEncode(stringArgs)}));
 ''';
     var response =
@@ -221,7 +225,7 @@
       {Map<String, String> scope, bool disableBreakpoints}) async {
     var remote = await _inspector?.evaluate(isolateId, targetId, expression,
         scope: scope, disableBreakpoints: disableBreakpoints);
-    return _inspector?.instanceRefFor(remote);
+    return _inspector?.instanceHelper?.instanceRefFor(remote);
   }
 
   @override
@@ -307,8 +311,8 @@
   }
 
   @override
-  Future invoke(String isolateId, String targetId, String selector,
-      List<String> argumentIds,
+  Future invoke(
+      String isolateId, String targetId, String selector, List argumentIds,
       {bool disableBreakpoints}) {
     throw UnimplementedError();
   }
@@ -466,7 +470,8 @@
 
   /// Listens for chrome console events and handles the ones we care about.
   void _setUpChromeConsoleListeners(IsolateRef isolateRef) {
-    _consoleSubscription = remoteDebugger.onConsoleAPICalled.listen((event) {
+    _consoleSubscription =
+        remoteDebugger.onConsoleAPICalled.listen((event) async {
       var isolate = _inspector?.isolate;
       if (isolate == null) return;
       if (event.type != 'debug') return;
@@ -496,13 +501,8 @@
           // All inspected objects should be real objects.
           if (event.args[1].type != 'object') break;
 
-          var inspectee = InstanceRef()
-            ..kind = InstanceKind.kPlainInstance
-            ..id = event.args[1].objectId
-            // TODO(jakemac): Create a real ClassRef, we need a way of looking
-            // up the library for a given instance to create it though.
-            // https://github.com/dart-lang/sdk/issues/36771.
-            ..classRef = ClassRef();
+          var inspectee =
+              await _inspector.instanceHelper.instanceRefFor(event.args[1]);
           _streamNotify(
               'Debug',
               Event()
@@ -538,6 +538,11 @@
       String isolateId, String targetId, int limit) {
     throw UnimplementedError();
   }
+
+  @override
+  Future<Success> requestHeapSnapshot(String isolateId) {
+    throw UnimplementedError();
+  }
 }
 
 /// The `type`s of [ConsoleAPIEvent]s that are treated as `stderr` logs.
diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart
index 7be8ede..210a653 100644
--- a/dwds/lib/src/services/debug_service.dart
+++ b/dwds/lib/src/services/debug_service.dart
@@ -8,17 +8,19 @@
 import 'dart:math';
 import 'dart:typed_data';
 
+import 'package:dwds/dwds.dart' show LogWriter;
 import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:pedantic/pedantic.dart';
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf.dart' hide Response;
 import 'package:shelf/shelf_io.dart';
 import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:sse/server/sse_handler.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:web_socket_channel/web_socket_channel.dart';
-import 'package:sse/server/sse_handler.dart';
 
+import '../handlers/asset_handler.dart';
 import '../utilities/shared.dart';
 import 'chrome_proxy_service.dart';
 
@@ -112,14 +114,16 @@
     String hostname,
     RemoteDebugger remoteDebugger,
     String tabUrl,
-    Future<String> Function(String) assetHandler,
-    String appInstanceId, {
+    AssetHandler assetHandler,
+    String appInstanceId,
+    LogWriter logWriter, {
     void Function(Map<String, dynamic>) onRequest,
     void Function(Map<String, dynamic>) onResponse,
     bool useSse,
   }) async {
+    useSse ??= false;
     var chromeProxyService = await ChromeProxyService.create(
-        remoteDebugger, tabUrl, assetHandler, appInstanceId);
+        remoteDebugger, tabUrl, assetHandler, appInstanceId, logWriter);
     var authToken = _makeAuthToken();
     var serviceExtensionRegistry = ServiceExtensionRegistry();
     Handler handler;
diff --git a/dwds/lib/src/utilities/objects.dart b/dwds/lib/src/utilities/objects.dart
index ccbd797..b563235 100644
--- a/dwds/lib/src/utilities/objects.dart
+++ b/dwds/lib/src/utilities/objects.dart
@@ -10,6 +10,7 @@
 /// Represents a property of an object.
 class Property {
   final Map<String, dynamic> _map;
+  RemoteObject _remoteObjectValue;
 
   Property(this._map);
 
@@ -17,14 +18,35 @@
   ///
   /// Useful for getting access to properties of particular types of
   /// RemoteObject.
-  Map<String, dynamic> get rawValue => _map['value'] as Map<String, dynamic>;
+  Object get rawValue => _map == null ? null : _map['value'];
 
   /// Remote object value in case of primitive values or JSON values (if it was
   /// requested). (optional)
-  RemoteObject get value => RemoteObject(rawValue);
+  RemoteObject get value {
+    if (_remoteObjectValue != null) return _remoteObjectValue;
+    if (rawValue == null) return null;
+    var val = _map['value'];
+    if (val is RemoteObject) {
+      _remoteObjectValue = val;
+    } else {
+      _remoteObjectValue = RemoteObject(val as Map<String, dynamic>);
+    }
+    return _remoteObjectValue;
+  }
 
   /// The name of the property
-  String get name => _map['name'] as String;
+  String get name {
+    const prefix = 'Symbol(';
+    var nonSymbol = (rawName.startsWith(prefix))
+        ? rawName.substring(prefix.length, rawName.length - 1)
+        : rawName;
+    return nonSymbol.split('.').last;
+  }
+
+  /// The raw name of the property in JS.
+  ///
+  /// Will be of the form 'Symbol(_actualName)' for private fields.
+  String get rawName => _map['name'] as String;
 
   @override
   String toString() => '$name $value';
diff --git a/dwds/lib/src/utilities/shared.dart b/dwds/lib/src/utilities/shared.dart
index c953671..43e2a6e 100644
--- a/dwds/lib/src/utilities/shared.dart
+++ b/dwds/lib/src/utilities/shared.dart
@@ -4,6 +4,7 @@
 
 import 'dart:io';
 
+import 'package:dwds/dwds.dart' show ModuleStrategy;
 import 'package:vm_service/vm_service.dart';
 
 VMRef toVMRef(VM vm) => VMRef()..name = vm.name;
@@ -31,3 +32,21 @@
   await socket.close();
   return port;
 }
+
+String _fetchModuleStrategy(ModuleStrategy config) {
+  switch (config) {
+    case ModuleStrategy.legacy:
+      return 'dart_library.import';
+    case ModuleStrategy.requireJS:
+      return 'require';
+  }
+  throw StateError('Unreachable code');
+}
+
+String _loadModule;
+
+String get loadModule => _loadModule;
+
+set globalModuleStrategy(ModuleStrategy moduleStrategy) {
+  _loadModule = _fetchModuleStrategy(moduleStrategy);
+}
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 18d3396..6745d68 100644
--- a/dwds/pubspec.yaml
+++ b/dwds/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dwds
-version: 0.5.0
+version: 0.5.2
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/webdev/tree/master/dwds
 description: >-
@@ -14,7 +14,7 @@
   build_daemon: ^2.0.0
   built_value: ^6.7.0
   crypto: ^2.0.6
-  devtools: ^0.1.0
+  devtools: ^0.1.6-dev.3
   http: ^0.12.0
   http_multi_server: ^2.0.0
   logging: ^0.11.3
@@ -28,9 +28,9 @@
   shelf_web_socket: ^0.2.0
   source_maps: ^0.10.0
   sse: ^2.0.2
-  vm_service: 1.1.0
+  vm_service: 1.1.1
   web_socket_channel: ^1.0.0
-  webkit_inspection_protocol: ^0.4.0
+  webkit_inspection_protocol: '>=0.4.0 <0.6.0'
 
 dev_dependencies:
   args: ^1.0.0
@@ -41,7 +41,6 @@
   built_value_generator: ^6.4.0
   graphs: ^0.2.0
   js: ^0.6.1
-  sse: ^2.0.2
   stream_channel: ^2.0.0
   test: ^1.6.0
   uuid: ^2.0.0
diff --git a/dwds/web/client.dart b/dwds/web/client.dart
index e8a3670..a46b061 100644
--- a/dwds/web/client.dart
+++ b/dwds/web/client.dart
@@ -82,8 +82,9 @@
       }
     });
 
-    window.onKeyDown.listen((e) {
-      if (const [
+    window.onKeyDown.listen((Event e) {
+      if (e is KeyboardEvent &&
+          const [
             'd',
             'D',
             '∂', // alt-d output on Mac
@@ -210,7 +211,7 @@
 @JS(r'$dartReloadConfiguration')
 external String get reloadConfiguration;
 
-@JS(r'require')
+@JS(r'$loadModuleConfig')
 external Object Function(String module) get require;
 
 List<K> keys<K, V>(JsMap<K, V> map) {
diff --git a/rxdart/.gitignore b/rxdart/.gitignore
deleted file mode 100755
index e9d4626..0000000
--- a/rxdart/.gitignore
+++ /dev/null
@@ -1,21 +0,0 @@
-# Don’t commit the following directories created by pub.

-.buildlog

-.dart_tool/

-.pub/

-build/

-packages

-*.packages

-.idea/

-web/experimental

-doc

-.dart_tool/

-

-# Or the files created by dart2js.

-*.dart.js

-*.js_

-*.js.deps

-*.js.map

-

-# Include when developing application packages.

-pubspec.lock

-coverage*

diff --git a/rxdart/.travis.yml b/rxdart/.travis.yml
deleted file mode 100755
index 01836a3..0000000
--- a/rxdart/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: dart

-# Speed up builds by using containerization.

-sudo: false

-dart:

-- 2.0.0-dev.69.1

-with_content_shell: false

-before_install:

-  - export DISPLAY=:99.0

-  - sh -e /etc/init.d/xvfb start

-script:

-  - set -e

-  - sh tool/check_format.sh

-  - dartanalyzer --fatal-infos --fatal-warnings ./

-  - pub run test test/rxdart_test.dart

-  - pub get --packages-dir

-  - pub global activate coverage

-  - pub global run coverage:collect_coverage --port=8111 -o coverage.json --resume-isolates --wait-paused &

-  - dart --observe=8111 test/rxdart_test.dart

-  - pub global run coverage:format_coverage --package-root=packages --report-on lib --in coverage.json --out lcov.info --lcov

-after_success:

-  - bash <(curl -s https://codecov.io/bash)

diff --git a/rxdart/BUILD.gn b/rxdart/BUILD.gn
deleted file mode 100644
index 150cffd..0000000
--- a/rxdart/BUILD.gn
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is generated by importer.py for rxdart-0.21.0
-
-import("//build/dart/dart_library.gni")
-
-dart_library("rxdart") {
-  package_name = "rxdart"
-
-  # This parameter is left empty as we don't care about analysis or exporting
-  # these sources outside of the tree.
-  sources = []
-
-  disable_analysis = true
-
-  deps = [
-  ]
-}
diff --git a/rxdart/CHANGELOG.md b/rxdart/CHANGELOG.md
deleted file mode 100755
index 1045c8f..0000000
--- a/rxdart/CHANGELOG.md
+++ /dev/null
@@ -1,292 +0,0 @@
-## 0.21.0

-  * Breaking Change: `BehaviorSubject` now has a separate factory constructor `seeded()`

-  This allows you to seed this Subject with a `null` value.

-  * Breaking Change: `BehaviorSubject` will now emit an `Error`, if the last event was also an `Error`.

-  Before, when an `Error` occurred before a `listen`, the subscriber would not be notified of that `Error`.

-  To refactor, simply change all occurences of `BehaviorSubject(seedValue: value)` to `BehaviorSubject.seeded(value)`

-  * Added the `groupBy` operator

-  * Bugix: `doOnCancel`: will now await the cancel result, if it is a `Future`.

-  * Removed: `bufferWithCount`, `windowWithCount`, `tween`

-  Please use `bufferCount` and `windowCount`, `tween` is removed, because it never was an official Rx spec.

-  * Updated Flutter example to work with the latest Flutter stable.

-

-## 0.20.0

-  * Breaking Change: bufferCount had buggy behavior when using `startBufferEvery` (was `skip` previously)

-  If you were relying on bufferCount with `skip` greater than 1 before, then you may have noticed 

-  erroneous behavior.

-  * Breaking Change: `repeat` is no longer an operator which simply repeats the last emitted event n-times,

-  instead this is now an Observable factory method which takes a StreamFactory and a count parameter.

-  This will cause each repeat cycle to create a fresh Observable sequence.

-  * `mapTo` is a new operator, which works just like `map`, but instead of taking a mapper Function, it takes

-  a single value where each event is mapped to.

-  * Bugfix: switchIfEmpty now correctly calls onDone

-  * combineLatest and zip can now take any amount of Streams:

-    * combineLatest2-9 & zip2-9 functionality unchanged, but now use a new path for construction.

-    * adds combineLatest and zipLatest which allows you to pass through an Iterable<Stream<T>> and a combiner that takes a List<T> when any source emits a change.

-    * adds combineLatestList / zipList which allows you to take in an Iterable<Stream<T>> and emit a Observable<List<T>> with the values. Just a convenience factory if all you want is the list!

-    * Constructors are provided by the Stream implementation directly

-  * Bugfix: Subjects that are transformed will now correctly return a new Observable where isBroadcast is true (was false before)  

-  * Remove deprecated operators which were replaced long ago: `bufferWithCount`, `windowWithCount`, `amb`, `flatMapLatest`

-

-## 0.19.0

-

-  * Breaking Change: Subjects `onCancel` function now returns `void` instead of `Future` to properly comply with the `StreamController` signature.

-  * Bugfix: FlatMap operator properly calls onDone for all cases

-  * Connectable Observable: An observable that can be listened to multiple times, and does not begin emitting values until the `connect` method is called

-  * ValueObservable: A new interface that allows you to get the latest value emitted by an Observable.

-    * Implemented by BehaviorSubject

-    * Convert normal observables into ValueObservables via `publishValue` or `shareValue`

-  * ReplayObservable: A new interface that allows you to get the values emitted by an Observable.

-      * Implemented by ReplaySubject

-      * Convert normal observables into ReplayObservables via `publishReplay` or `shareReplay`  

-

-## 0.18.1

-

-* Add `retryWhen` operator. Thanks to Razvan Lung (@long1eu)! This can be used for custom retry logic.

-

-## 0.18.0

-

-* Breaking Change: remove `retype` method, deprecated as part of Dart 2.

-* Add `flatMapIterable`

-

-## 0.17.0

-

-* Breaking Change: `stream` property on Observable is now private.

-  * Avoids API confusion

-  * Simplifies Subject implementation

-  * Require folks who are overriding the `stream` property to use a `super` constructor instead 

-* Adds proper onPause and onResume handling for `amb`/`race`, `combineLatest`, `concat`, `concat_eager`, `merge`  and `zip`

-* Add `switchLatest` operator

-* Add errors and stacktraces to RetryError class

-* Add `onErrorResume` and `onErrorRetryWith` operators. These allow folks to return a specific stream or value depending on the error that occurred. 

-

-## 0.16.7

-

-* Fix new buffer and window implementation for Flutter + Dart 2

-* Subject now implements the Observable interface

-

-## 0.16.6

-

-* Rework for `buffer` and `window`, allow to schedule using a sampler

-* added `buffer`

-* added `bufferFuture`

-* added `bufferTest`

-* added `bufferTime`

-* added `bufferWhen`

-* added `window`

-* added `windowFuture`

-* added `windowTest`

-* added `windowTime`

-* added `windowWhen`

-* added `onCount` sampler for `buffer` and `window`

-* added `onFuture` sampler for `buffer` and `window`

-* added `onTest` sampler for `buffer` and `window`

-* added `onTime` sampler for `buffer` and `window`

-* added `onStream` sampler for `buffer` and `window`

-

-## 0.16.5

-

-* Renames `amb` to `race`

-* Renames `flatMapLatest` to `switchMap`

-* Renames `bufferWithCount` to `bufferCount`

-* Renames `windowWithCount` to `windowCount`

-

-## 0.16.4

-

-* Adds `bufferTime` transformer.

-* Adds `windowTime` transformer.

-

-## 0.16.3

-

-* Adds `delay` transformer.

-

-## 0.16.2

-

-* Fix added events to `sink` are not processed correctly by `Subjects`.

-

-## 0.16.1

-

-* Fix `dematerialize` method for Dart 2.

-

-## 0.16.0+2

-

-* Add `value` to `BehaviorSubject`. Allows you to get the latest value emitted by the subject if it exists.

-* Add `values` to `ReplayrSubject`. Allows you to get the values stored by the subject if any exists.

-

-## 0.16.0+1

-

-* Update Changelog

-

-## 0.16.0

-

-* **breaks backwards compatibility**, this release only works with Dart SDK >=2.0.0.

-* Removed old `cast` in favour of the now native Stream cast method.

-* Override `retype` to return an `Observable`.

-

-## 0.15.1

-

-* Add `exhaustMap` map to inner observable, ignore other values until that observable completes.

-* Improved code to be dartdevc compatible.

-* Add upper SDK version limit in pubspec

-

-## 0.15.0

-

-* Change `debounce` to emit the last item of the source stream as soon as the source stream completes.

-* Ensure `debounce` does not keep open any addition async timers after it has been cancelled.

-

-## 0.14.0+1

-

-* Change `DoStreamTransformer` to return a `Future` on cancel for api compatibility. 

-

-## 0.14.0

-

-* Add `PublishSubject` (thanks to @pauldemarco)

-* Fix bug with `doOnX` operators where callbacks were fired too often

-

-## 0.13.1

-

-* Fix error with FlatMapLatest where it was not properly cancelled in some scenarios

-* Remove additional async methods on Stream handlers unless they're shown to solve a problem

-

-## 0.13.0

-

-* Remove `call` operator / `StreamTransformer` entirely

-* Important bug fix: Errors thrown within any Stream or Operator will now be properly sent to the `StreamSubscription`.

-* Improve overall handling of errors throughout the library to ensure they're handled correctly

-

-## 0.12.0

-

-* Added doOn* operators in place of `call`.

-* Added `DoStreamTransformer` as a replacement for `CallStreamTransformer`

-* Deprecated `call` and `CallStreamTransformer`. Please use the appropriate `doOnX` operator / transformer.

-* Added `distinctUnique`. Emits items if they've never been emitted before. Same as to Rx#distinct.

-

-## 0.11.0

-

-* !!!Breaking Api Change!!!

-    * Observable.groupBy has been removed in order to be compatible with the next version of the `Stream` class in Dart 1.24.0, which includes this method

-

-## 0.10.2

-

-* BugFix: The new Subject implementation no longer causes infinite loops when used with ng2 async pipes.

-

-## 0.10.1

-

-* Documentation fixes

-

-## 0.10.0

-

-* Api Changes

-  * Observable

-    * Remove all deprecated methods, including:

-      * `observable` factory -- replaced by the constructor `new Observable()`

-      * `combineLatest` -- replaced by Strong-Mode versions `combineLatest2` - `combineLatest9`

-      * `zip` -- replaced by Strong-Mode versions `zip2` - `zip9`

-    * Support `asObservable` conversion from Future-returning methods. e.g. `new Observable.fromIterable([1, 2]).first.asObservable()`

-    * Max and Min now return a Future of the Max or Min value, rather than a stream of increasing or decreasing values.

-    * Add `cast` operator

-    * Remove `ConcatMapStreamTransformer` -- functionality is already supported by `asyncExpand`. Keep the `concatMap` method as an alias.

-  * Subjects

-    * BehaviourSubject has been renamed to BehaviorSubject

-    * The subjects have been rewritten and include far more testing

-    * In keeping with the Rx idea of Subjects, they are broadcast-only

-* Documentation -- extensive documentation has been added to the library with explanations and examples for each Future, Stream & Transformer.

-  * Docs detailing the differences between RxDart and raw Observables.

-  

-## 0.9.0

-

-* Api Changes:

-  * Convert all StreamTransformer factories to proper classes

-    * Ensure these classes can be re-used multiple times

-  * Retry has moved from an operator to a constructor. This is to ensure the stream can be properly re-constructed every time in the correct way.

-  * Streams now properly enforce the single-subscription contract

-* Include example Flutter app. To run it, please follow the instructions in the README.

-

-## 0.8.3+1

-* rename examples map to example

-

-## 0.8.3

-* added concatWith, zipWith, mergeWith, skipUntil

-* cleanup of the examples folder

-* cleanup of examples code

-* added fibonacci example

-* added search GitHub example

-

-## 0.8.2+1

-* moved repo into ReactiveX

-* update readme badges accordingly

-

-## 0.8.2

-* added materialize/dematerialize

-* added range (factory)

-* added timer (factory)

-* added timestamp

-* added concatMap

-

-## 0.8.1

-* added never constructor

-* added error constructor

-* moved code coverage to [codecov.io](https://codecov.io/gh/frankpepermans/rxdart)

-

-## 0.8.0

-* BREAKING: tap is replaced by call(onData)

-* added call, which can take any combination of the following event methods: 

-onCancel, onData, onDone, onError, onListen, onPause, onResume

-

-## 0.7.1+1

-* improved the README file

-

-## 0.7.1

-* added ignoreElements

-* added onErrorResumeNext

-* added onErrorReturn

-* added switchIfEmpty

-* added empty factory constructor

-

-## 0.7.0

-* BREAKING: rename combineXXXLatest and zipXXX to a numbered equivalent,

-for example: combineThreeLatest becomes combineLatest3

-* internal refactoring, expose streams/stream transformers as a separate library

-

-## 0.6.3+4

-* changed ofType to use TypeToken

-

-## 0.6.3+3

-* added ofType

-

-## 0.6.3+2

-* added defaultIfEmpty

-

-## 0.6.3+1

-* changed concat, old concat is now concatEager, new concat behaves as expected

-

-## 0.6.3

-* Added withLatestFrom 

-* Added defer ctr

-(both thanks to [brianegan](https://github.com/brianegan "GitHub link"))

-

-## 0.6.2

-* Added just (thanks to [brianegan](https://github.com/brianegan "GitHub link"))

-* Added groupBy

-* Added amb

-

-## 0.6.1

-* Added concat

-

-## 0.6.0

-* BREAKING: startWith now takes just one parameter instead of an Iterable. To add multiple starting events, please use startWithMany.

-* Added BehaviourSubject and ReplaySubject. These implement StreamController.

-* BehaviourSubject will notify the last added event upon listening.

-* ReplaySubject will notify all past events upon listening.

-* DEPRECATED: zip and combineLatest, use their strong-type-friendly alternatives instead (available as static methods on the Observable class, i.e. Observable.combineThreeLatest, Observable.zipFour, ...)

-

-## 0.5.1

-

-* Added documentation (thanks to [dustinlessard-wf](https://github.com/dustinlessard-wf "GitHub link"))

-* Fix tests breaking due to deprecation of expectAsync

-* Fix tests to satisfy strong mode requirements

-

-## 0.5.0

-

-* As of this version, rxdart depends on SDK v1.21.0, to support the newly added generic method type syntax

-

diff --git a/rxdart/CONTRIBUTING.md b/rxdart/CONTRIBUTING.md
deleted file mode 100755
index 09387b0..0000000
--- a/rxdart/CONTRIBUTING.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Contributing to RxDart

-

-## Create a new issue

-

-The easiest way to get involved is to create a [new issue](https://github.com/ReactiveX/rxdart/issues/new) when you spot a bug, if the documentation is incomplete or out of date, or if you identify an implementation problem.

-

-## General coding guidlines

-

-If you'd like to add a feature or fix a bug, we're more than happy to accept pull requests! We only ask a few things:

-

-  - Ensure your code contains no analyzer errors, e.g.

-    - Code is strong-mode compliant

-    - Code is free of lint errors

-  - Format your code with `dartfmt`

-  - Write tests for all new code paths, consider using the [Stream Matchers](https://pub.dartlang.org/packages/test#stream-matchers) available from the test package.

-  - Write helpful documentation

-  - If you would like to make a bigger / fundamental change to the codebase, please file a lightweight example PR / issue, or contact us in [Gitter](https://gitter.im/ReactiveX/rxdart) so we can discuss the issue.

-

-## Advice when adding a new factory

-

-  - Extend from `Stream` so it can be constructed outside the scope of the Observable class

-  - Add the new `Stream` to the exported `rx_streams` library

-  - Ensure the stream properly enforces the single-subscription contract

-  - Ensure the stream closses properly

-  - Add new tests to `tests/rxdart_test.dart`

-

-## Advice when adding a new operator

-

-  - Extend from the `StreamTransformer` class so it can be used independently

-  - Add the new `StreamTransformer` to the exported `rx_transformers` library

-  - Ensure the `StreamTransformer` can be re-used

-  - Add new tests to `tests/rxdart_test.dart`

diff --git a/rxdart/LICENSE b/rxdart/LICENSE
deleted file mode 100755
index dc45a1c..0000000
--- a/rxdart/LICENSE
+++ /dev/null
@@ -1,11 +0,0 @@
-Licensed under the Apache License, Version 2.0 (the "License"); you

-may not use this file except in compliance with the License. You may

-obtain a copy of the License at

-

-http://www.apache.org/licenses/LICENSE-2.0

-

-Unless required by applicable law or agreed to in writing, software

-distributed under the License is distributed on an "AS IS" BASIS,

-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or

-implied. See the License for the specific language governing permissions

-and limitations under the License.
\ No newline at end of file
diff --git a/rxdart/README.md b/rxdart/README.md
deleted file mode 100755
index 260221e..0000000
--- a/rxdart/README.md
+++ /dev/null
@@ -1,219 +0,0 @@
-# RxDart

-

-[![Build Status](https://travis-ci.org/ReactiveX/rxdart.svg?branch=master)](https://travis-ci.org/ReactiveX/rxdart)

-[![codecov](https://codecov.io/gh/ReactiveX/rxdart/branch/master/graph/badge.svg)](https://codecov.io/gh/ReactiveX/rxdart)

-[![Pub](https://img.shields.io/pub/v/rxdart.svg)](https://pub.dartlang.org/packages/rxdart)

-[![Gitter](https://img.shields.io/gitter/room/ReactiveX/rxdart.svg)](https://gitter.im/ReactiveX/rxdart)

-

-## About

-RxDart is a reactive functional programming library for Google Dart, based on [ReactiveX](http://reactivex.io/).  

-Google Dart comes with a very decent [Streams](https://api.dartlang.org/stable/1.21.1/dart-async/Stream-class.html) API out-of-the-box; rather than attempting to provide an alternative to this API, RxDart adds functionality on top of it.

-

-## Version

-Dart 1.0 is supported until release 0.15.x,

-version 0.16.x is no longer backwards compatible and requires the Dart SDK 2.0

-

-## How To Use RxDart

-

-### For Example: Reading the Konami Code 

-

-```dart

-void main() {

-  const konamiKeyCodes = const <int>[

-    KeyCode.UP,

-    KeyCode.UP,

-    KeyCode.DOWN,

-    KeyCode.DOWN,

-    KeyCode.LEFT,

-    KeyCode.RIGHT,

-    KeyCode.LEFT,

-    KeyCode.RIGHT,

-    KeyCode.B,

-    KeyCode.A];

-

-  final result = querySelector('#result');

-  final keyUp = new Observable<KeyboardEvent>(document.onKeyUp);

-

-  keyUp

-    .map((event) => event.keyCode)

-    .bufferCount(10, 1)

-    .where((lastTenKeyCodes) => const IterableEquality<int>().equals(lastTenKeyCodes, konamiKeyCodes))

-    .listen((_) => result.innerHtml = 'KONAMI!');

-}

-```

-

-## API Overview

-

-### Objects

-

-- [Observable](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable-class.html)

-- [PublishSubject](https://www.dartdocs.org/documentation/rxdart/latest/rx/PublishSubject-class.html)

-- [BehaviorSubject](https://www.dartdocs.org/documentation/rxdart/latest/rx/BehaviorSubject-class.html)

-- [ReplaySubject](https://www.dartdocs.org/documentation/rxdart/latest/rx/ReplaySubject-class.html)

-

-### Observable

-

-RxDart's Observables extends the Stream class. This has two major implications:  

-- All [methods defined on the Stream class](https://api.dartlang.org/stable/1.21.1/dart-async/Stream-class.html#instance-methods) exist on RxDart's Observables as well.

-- All Observables can be passed to any API that expects a Dart Stream as an input.

-- Additional important distinctions are documented as part of the [Observable class](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable-class.html)

-

-Finally, the Observable class & operators are simple wrappers around `Stream` and `StreamTransformer` classes. All underlying implementations can be used free of the Observable class, and are exposed in their own libraries. They are linked to below.

-

-### Instantiation

-

-Generally speaking, creating a new Observable is either done by wrapping a Dart Stream using the top-level constructor `new Observable()`, or by calling a factory method on the Observable class.

-But to better support Dart's strong mode, `combineLatest` and `zip` have been pulled apart into fixed-length constructors. 

-These methods are supplied as static methods, since Dart's factory methods don't support generic types.

-

-###### Usage

-```dart

-var myObservable = new Observable(myStream);

-```

-

-#### Available Factory Methods

-- [concat](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.concat.html) / [ConcatStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/ConcatStream-class.html)

-- [concatEager](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.concat.html) / [ConcatEagerStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/ConcatEagerStream-class.html)

-- [defer](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.defer.html) / [DeferStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/DeferStream-class.html)

-- [error](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.error.html) / [ErrorStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/ErrorStream-class.html)

-- [just](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.just.html)

-- [merge](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.merge.html) / [MergeStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/MergeStream-class.html)

-- [never](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.never.html) / [NeverStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/NeverStream-class.html)

-- [periodic](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.periodic.html)

-- [race](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.race.html) / [RaceStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/RaceStream-class.html)

-- [repeat](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.repeat.html) / [RepeatStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/RepeatStream-class.html)

-- [retry](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.retry.html) / [RetryStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/RetryStream-class.html)

-- [retryWhen](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.retryWhen.html) / [RetryWhenStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/RetryWhenStream-class.html)

-- [switchLatest](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.switchLatest.html) / [SwitchLatestStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/SwitchLatestStream-class.html)

-- [timer](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/Observable.timer.html) / [TimerStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/TimerStream-class.html)

-

-###### Usage

-```dart

-var myObservable = new Observable.merge([myFirstStream, mySecondStream]);

-```

-

-##### Available Static Methods

-- [combineLatest](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/combineLatest2.html) / [CombineLatestStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/CombineLatestStream-class.html) (combineLatest2, combineLatest... combineLatest9) 

-- [range](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/range.html) / [RangeStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/RangeStream-class.html)

-- [tween](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/tween.html) / [TweenStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/TweenStream-class.html)

-- [zip](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/zip2.html) / [ZipStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/ZipStream-class.html) (zip2, zip3, zip4, ..., zip9)

-

-###### Usage

-```dart

-var myObservable = Observable.combineLatest3(

-    myFirstStream, 

-    mySecondStream, 

-    myThirdStream, 

-    (firstData, secondData, thirdData) => print("$firstData $secondData $thirdData"));

-```

-

-### Transformations

-    

-##### Available Methods

-- [buffer](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/buffer.html) / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html)

-- [bufferCount](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/bufferCount.html) / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onCount](https://www.dartdocs.org/documentation/rxdart/latest/rx/onCount.html)

-- [bufferFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/bufferFuture.html) / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/onFuture.html)

-- [bufferTest](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/bufferTest.html) / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onTest](https://www.dartdocs.org/documentation/rxdart/latest/rx/onTest.html)

-- [bufferTime](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/bufferTime.html) / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onTime](https://www.dartdocs.org/documentation/rxdart/latest/rx/onTime.html)

-- [bufferWhen](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/bufferWhen.html) / / [BufferStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/onStream.html)

-- [concatMap](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/concatMap.html) (alias for `asyncExpand`)

-- [concatWith](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/concatWith.html)

-- [debounce](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/debounce.html) / [DebounceStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DebounceStreamTransformer-class.html)

-- [delay](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/delay.html) / [DelayStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DelayStreamTransformer-class.html)

-- [dematerialize](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/dematerialize.html) / [DematerializeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DematerializeStreamTransformer-class.html)

-- [distinctUnique](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/distinctUnique.html) / [DistinctUniqueStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DistinctUniqueStreamTransformer-class.html)

-- [doOnCancel](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnCancel.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnData](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnData.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnDone](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnDone.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnEach](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnEach.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnError](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnError.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnListen](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnListen.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnPause](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnPause.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [doOnResume](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/doOnResume.html) / [DoStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/DoStreamTransformer-class.html)

-- [exhaustMap](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/exhaustMap.html) / [ExhaustMapStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/ExhaustMapStreamTransformer-class.html)

-- [flatMap](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/flatMap.html) / [FlatMapStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/FlatMapStreamTransformer-class.html)

-- [flatMapIterable](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/flatMapIterable.html)

-- [groupBy](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/groupBy.html) / [GroupByStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/GroupByStreamTransformer-class.html)

-- [interval](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/interval.html) / [IntervalStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/IntervalStreamTransformer-class.html)

-- [mapTo](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/mapTo.html) / [MapToStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/MapToStreamTransformer-class.html)

-- [materialize](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/materialize.html) / [MaterializeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/MaterializeStreamTransformer-class.html)

-- [mergeWith](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/mergeWith.html)

-- [max](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/max.html) / [StreamMaxFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/StreamMaxFuture-class.html)

-- [min](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/min.html) / [StreamMinFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/StreamMinFuture-class.html)

-- [onErrorResume](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/onErrorResume.html) / [OnErrorResumeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/OnErrorResumeStreamTransformer-class.html)

-- [onErrorResumeNext](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/onErrorResumeNext.html) / [OnErrorResumeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/OnErrorResumeStreamTransformer-class.html)

-- [onErrorReturn](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/onErrorReturn.html) / [OnErrorResumeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/OnErrorResumeStreamTransformer-class.html)

-- [onErrorReturnWith](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/onErrorReturnWith.html) / [OnErrorResumeStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/OnErrorResumeStreamTransformer-class.html)

-- [sample](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/sample.html) / [SampleStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/SampleStreamTransformer-class.html)

-- [scan](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/scan.html) / [ScanStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/ScanStreamTransformer-class.html)

-- [skipUntil](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/skipUntil.html) / [SkipUntilStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/SkipUntilStreamTransformer-class.html)

-- [startWith](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/startWith.html) / [StartWithStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/StartWithStreamTransformer-class.html)

-- [startWithMany](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/startWithMany.html) / [StartWithManyStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/StartWithManyStreamTransformer-class.html) 

-- [switchMap](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/switchMap.html) / [SwitchMapStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx_transformers/SwitchMapStreamTransformer-class.html)

-- [takeUntil](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/takeUntil.html) / [TakeUntilStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/TakeUntilStreamTransformer-class.html)

-- [timeInterval](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/timeInterval.html) / [TimeIntervalStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/TimeIntervalStreamTransformer-class.html)

-- [timestamp](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/timestamp.html) / [TimestampStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/TimestampStreamTransformer-class.html)

-- [throttle](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/throttle.html) / [ThrottleStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/ThrottleStreamTransformer-class.html)

-- [window](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/window.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/WindowStreamTransformer-class.html)

-- [windowCount](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/windowCount.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onCount](https://www.dartdocs.org/documentation/rxdart/latest/rx/onCount.html)

-- [windowFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/windowFuture.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onFuture](https://www.dartdocs.org/documentation/rxdart/latest/rx/onFuture.html)

-- [windowTest](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/windowTest.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onTest](https://www.dartdocs.org/documentation/rxdart/latest/rx/onTest.html)

-- [windowTime](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/windowTime.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onTime](https://www.dartdocs.org/documentation/rxdart/latest/rx/onTime.html)

-- [windowWhen](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/windowWhen.html) / [WindowStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/BufferStreamTransformer-class.html) / [onStream](https://www.dartdocs.org/documentation/rxdart/latest/rx/onStream.html)

-- [withLatestFrom](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/withLatestFrom.html) / [WithLatestFromStreamTransformer](https://www.dartdocs.org/documentation/rxdart/latest/rx/WithLatestFromStreamTransformer-class.html)

-- [zipWith](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable/zipWith.html)

-

-A full list of all methods and properties including those provided by the Dart Stream API (such as `first`, `asyncMap`, etc), can be seen by examining the [DartDocs](https://www.dartdocs.org/documentation/rxdart/latest/rx/Observable-class.html#instance-methods)

-

-###### Usage

-```Dart

-var myObservable = new Observable(myStream)

-    .bufferCount(5)

-    .distinct();

-```

-

-## Examples

-

-Web and command-line examples can be found in the `example` folder.

-

-### Web Examples

- 

-In order to run the web examples, please follow these steps:

-

-  1. Clone this repo and enter the directory

-  2. Run `pub get`

-  3. Run `pub run build_runner serve example`

-  4. Navigate to [http://localhost:8080/web/](http://localhost:8080/web/) in your browser

-

-### Command Line Examples

-

-In order to run the command line example, please follow these steps:

-

-  1. Clone this repo and enter the directory

-  2. Run `pub get`

-  3. Run `dart example/example.dart 10`

-  

-### Flutter Example

-  

-#### Install Flutter

-

-In order to run the flutter example, you must have Flutter installed. For installation instructions, view the online

-[documentation](https://flutter.io/).

-

-#### Run the app

-

-  1. Open up an Android Emulator, the iOS Simulator, or connect an appropriate mobile device for debugging.

-  2. Open up a terminal

-  3. `cd` into the `example/flutter/github_search` directory

-  4. Run `flutter doctor` to ensure you have all Flutter dependencies working.

-  5. Run `flutter packages get`

-  6. Run `flutter run`

-

-## Notable References

-- [Documentation on the Dart Stream class](https://api.dartlang.org/stable/2.0.0/dart-async/Stream-class.html)

-- [Tutorial on working with Streams in Dart](https://www.dartlang.org/tutorials/language/streams)

-- [ReactiveX (Rx)](http://reactivex.io/)

-

-## Changelog

-

-Refer to the [Changelog](https://github.com/frankpepermans/rxdart/blob/master/CHANGELOG.md) to get all release notes.

diff --git a/rxdart/analysis_options.yaml b/rxdart/analysis_options.yaml
deleted file mode 100755
index 30d0a6f..0000000
--- a/rxdart/analysis_options.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-analyzer:

-  strong-mode:

-    implicit-casts: false

-    implicit-dynamic: false

-  exclude:

-    - 'example/**'

-linter:

-  rules:

-    - always_declare_return_types

-    - annotate_overrides

-    #- avoid_as

-    - avoid_empty_else

-    - avoid_init_to_null

-    - avoid_return_types_on_setters

-    - await_only_futures

-    #- camel_case_types

-    #- constant_identifier_names

-    - cancel_subscriptions

-    - close_sinks

-    - comment_references

-    - control_flow_in_finally

-    - empty_catches

-    - empty_constructor_bodies

-    - empty_statements

-    - hash_and_equals

-    #- implementation_imports

-    - iterable_contains_unrelated_type

-    - library_names

-    - library_prefixes

-    - list_remove_unrelated_type

-    #- non_constant_identifier_names

-    - omit_local_variable_types

-    - one_member_abstracts

-    - package_api_docs

-    - package_names

-    - package_prefixed_library_names

-    - prefer_is_not_empty

-    - slash_for_doc_comments

-    #- sort_constructors_first

-    #- sort_unnamed_constructors_first

-    - super_goes_last

-    - test_types_in_equals

-    - throw_in_finally

-    - type_annotate_public_apis

-    - type_init_formals

-    - unnecessary_brace_in_string_interps

-    - unnecessary_getters_setters

-    - unrelated_type_equality_checks

-    - unawaited_futures

-    - valid_regexps

diff --git a/rxdart/build.yaml b/rxdart/build.yaml
deleted file mode 100755
index f561845..0000000
--- a/rxdart/build.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-targets:

-  $default:

-    builders:

-      build_web_compilers|entrypoint:

-        generate_for: ['example/web/**']

diff --git a/rxdart/codecov.yml b/rxdart/codecov.yml
deleted file mode 100755
index de8be6e..0000000
--- a/rxdart/codecov.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-codecov:

-  notify:

-    require_ci_to_pass: yes

-

-coverage:

-  precision: 2

-  round: down

-  range: "70...100"

-

-  status:

-    project: yes

-    patch: yes

-    changes: no

-

-  notify:

-    gitter:

-      default:

-        url: "https://webhooks.gitter.im/e/13a7391e854a677bad98"

-        threshold: 1%

-

-parsers:

-  gcov:

-    branch_detection:

-      conditional: yes

-      loop: yes

-      method: no

-      macro: no

-

-comment:

-  layout: "header, diff"

-  behavior: default

-  require_changes: no
\ No newline at end of file
diff --git a/rxdart/example/example.dart b/rxdart/example/example.dart
deleted file mode 100755
index 282b23d..0000000
--- a/rxdart/example/example.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-import 'package:rxdart/rxdart.dart';

-

-/// generate n-amount of fibonacci numbers

-///

-/// for example: dart fibonacci.dart 10

-/// outputs:

-/// 1: 1

-/// 2: 1

-/// 3: 2

-/// 4: 3

-/// 5: 5

-/// 6: 8

-/// 7: 13

-/// 8: 21

-/// 9: 34

-/// 10: 55

-/// done!

-void main(List<String> arguments) {

-  // read the command line argument, if none provided, default to 10

-  var n = (arguments.length == 1) ? int.parse(arguments.first) : 10;

-

-  // seed value: this value will be used as the

-  // starting value for the [scan] method

-  const seed = const IndexedPair(1, 1, 0);

-

-  Observable

-          // amount of numbers to compute

-          .range(1, n)

-      // accumulator: computes a new accumulated

-      // value each time a [Stream] event occurs

-      // in this case, the accumulated value is always

-      // the latest Fibonacci number

-      .scan((IndexedPair seq, _, __) => new IndexedPair.next(seq), seed)

-      // finally, print the output

-      .listen(print, onDone: () => print('done!'));

-}

-

-class IndexedPair {

-  final int n1, n2, index;

-

-  const IndexedPair(this.n1, this.n2, this.index);

-

-  factory IndexedPair.next(IndexedPair prev) => new IndexedPair(

-      prev.n2, prev.index <= 1 ? prev.n1 : prev.n1 + prev.n2, prev.index + 1);

-

-  @override

-  String toString() => '$index: $n2';

-}

diff --git a/rxdart/example/flutter/github_search/.gitignore b/rxdart/example/flutter/github_search/.gitignore
deleted file mode 100755
index 5c7f8e5..0000000
--- a/rxdart/example/flutter/github_search/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-.DS_Store

-.atom/

-.idea

-.packages

-.pub/

-build/

-ios/.generated/

-packages

-pubspec.lock

diff --git a/rxdart/example/flutter/github_search/README.md b/rxdart/example/flutter/github_search/README.md
deleted file mode 100755
index 7a5a498..0000000
--- a/rxdart/example/flutter/github_search/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-## Flutter Github Search Example

-

-This example makes use of the bloc pattern [presented by Paolo Soares at DartConf](https://www.youtube.com/watch?v=PLHln7wHgPE).

-

-### Run the example

-

-This example requires Dart 2 be enabled. To run the example:

-

-`flutter run --preview-dart-2`  

diff --git a/rxdart/example/flutter/github_search/analysis_options.yaml b/rxdart/example/flutter/github_search/analysis_options.yaml
deleted file mode 100755
index edfaed0..0000000
--- a/rxdart/example/flutter/github_search/analysis_options.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-analyzer:

-  strong-mode: true
\ No newline at end of file
diff --git a/rxdart/example/flutter/github_search/android/.gitignore b/rxdart/example/flutter/github_search/android/.gitignore
deleted file mode 100755
index 635c65d..0000000
--- a/rxdart/example/flutter/github_search/android/.gitignore
+++ /dev/null
@@ -1,12 +0,0 @@
-*.iml

-.gradle

-/local.properties

-/.idea/workspace.xml

-/.idea/libraries

-.DS_Store

-/build

-/captures

-

-/gradle

-/gradlew

-/gradlew.bat

diff --git a/rxdart/example/flutter/github_search/android/app/build.gradle b/rxdart/example/flutter/github_search/android/app/build.gradle
deleted file mode 100755
index 0fd57d7..0000000
--- a/rxdart/example/flutter/github_search/android/app/build.gradle
+++ /dev/null
@@ -1,45 +0,0 @@
-def localProperties = new Properties()

-def localPropertiesFile = rootProject.file('local.properties')

-if (localPropertiesFile.exists()) {

-    localPropertiesFile.withInputStream { stream ->

-        localProperties.load(stream)

-    }

-}

-

-def flutterRoot = localProperties.getProperty('flutter.sdk')

-if (flutterRoot == null) {

-    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")

-}

-

-apply plugin: 'com.android.application'

-apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

-

-android {

-    compileSdkVersion 27

-

-    lintOptions {

-        disable 'InvalidPackage'

-    }

-

-    defaultConfig {

-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

-    }

-

-    buildTypes {

-        release {

-            // TODO: Add your own signing config for the release build.

-            // Signing with the debug keys for now, so `flutter run --release` works.

-            signingConfig signingConfigs.debug

-        }

-    }

-}

-

-flutter {

-    source '../..'

-}

-

-dependencies {

-    androidTestImplementation 'com.android.support:support-annotations:25.0.0'

-    androidTestImplementation 'com.android.support.test:runner:1.0.1'

-    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'

-}

diff --git a/rxdart/example/flutter/github_search/android/app/src/main/AndroidManifest.xml b/rxdart/example/flutter/github_search/android/app/src/main/AndroidManifest.xml
deleted file mode 100755
index bd224fa..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"

-    package="com.yourcompany.githubSearch"

-    android:versionCode="1"

-    android:versionName="0.0.1">

-

-    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />

-

-    <!-- The INTERNET permission is required for development. Specifically, flutter needs it to communicate with the running application

-         to allow setting breakpoints, to provide hot reload, etc.

-    -->

-    <uses-permission android:name="android.permission.INTERNET"/>

-

-    <!-- io.flutter.app.FlutterApplication is an android.app.Application that

-         calls FlutterMain.startInitialization(this); in its onCreate method.

-         In most cases you can leave this as-is, but you if you want to provide

-         additional functionality it is fine to subclass or reimplement

-         FlutterApplication and put your custom class here. -->

-    <application android:name="io.flutter.app.FlutterApplication" android:label="github_search" android:icon="@mipmap/ic_launcher">

-        <activity android:name=".MainActivity"

-                  android:launchMode="singleTop"

-                  android:theme="@android:style/Theme.Black.NoTitleBar"

-                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"

-                  android:hardwareAccelerated="true"

-                  android:windowSoftInputMode="adjustResize">

-            <intent-filter>

-                <action android:name="android.intent.action.MAIN"/>

-                <category android:name="android.intent.category.LAUNCHER"/>

-            </intent-filter>

-        </activity>

-    </application>

-</manifest>

diff --git a/rxdart/example/flutter/github_search/android/app/src/main/java/com/yourcompany/github_search/MainActivity.java b/rxdart/example/flutter/github_search/android/app/src/main/java/com/yourcompany/github_search/MainActivity.java
deleted file mode 100755
index ab3a9b6..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/java/com/yourcompany/github_search/MainActivity.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.yourcompany.githubSearch;

-

-import android.os.Bundle;

-import io.flutter.app.FlutterActivity;

-

-public class MainActivity extends FlutterActivity {

-    @Override

-    protected void onCreate(Bundle savedInstanceState) {

-        super.onCreate(savedInstanceState);

-    }

-}

-

diff --git a/rxdart/example/flutter/github_search/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/rxdart/example/flutter/github_search/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
deleted file mode 100755
index 374c5d6..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.flutter.plugins;

-

-import io.flutter.plugin.common.PluginRegistry;

-

-/**

- * Generated file. Do not edit.

- */

-public final class GeneratedPluginRegistrant {

-  public static void registerWith(PluginRegistry registry) {

-    if (alreadyRegisteredWith(registry)) {

-      return;

-    }

-  }

-

-  private static boolean alreadyRegisteredWith(PluginRegistry registry) {

-    final String key = GeneratedPluginRegistrant.class.getCanonicalName();

-    if (registry.hasPlugin(key)) {

-      return true;

-    }

-    registry.registrarFor(key);

-    return false;

-  }

-}

diff --git a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100755
index db77bb4..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100755
index 17987b7..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100755
index 09d4391..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100755
index d5f1c8d..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100755
index 4d6372e..0000000
--- a/rxdart/example/flutter/github_search/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/android/build.gradle b/rxdart/example/flutter/github_search/android/build.gradle
deleted file mode 100755
index b2483d3..0000000
--- a/rxdart/example/flutter/github_search/android/build.gradle
+++ /dev/null
@@ -1,29 +0,0 @@
-buildscript {

-    repositories {

-        google()

-        jcenter()

-    }

-

-    dependencies {

-        classpath 'com.android.tools.build:gradle:3.0.1'

-    }

-}

-

-allprojects {

-    repositories {

-        google()

-        jcenter()

-    }

-}

-

-rootProject.buildDir = '../build'

-subprojects {

-    project.buildDir = "${rootProject.buildDir}/${project.name}"

-}

-subprojects {

-    project.evaluationDependsOn(':app')

-}

-

-task clean(type: Delete) {

-    delete rootProject.buildDir

-}

diff --git a/rxdart/example/flutter/github_search/android/gradle.properties b/rxdart/example/flutter/github_search/android/gradle.properties
deleted file mode 100755
index b472197..0000000
--- a/rxdart/example/flutter/github_search/android/gradle.properties
+++ /dev/null
@@ -1 +0,0 @@
-org.gradle.jvmargs=-Xmx1536M

diff --git a/rxdart/example/flutter/github_search/android/settings.gradle b/rxdart/example/flutter/github_search/android/settings.gradle
deleted file mode 100755
index d3db109..0000000
--- a/rxdart/example/flutter/github_search/android/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'

diff --git a/rxdart/example/flutter/github_search/fonts/hind/Hind-Bold.ttf b/rxdart/example/flutter/github_search/fonts/hind/Hind-Bold.ttf
deleted file mode 100755
index e33d27a..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/Hind-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/hind/Hind-Light.ttf b/rxdart/example/flutter/github_search/fonts/hind/Hind-Light.ttf
deleted file mode 100755
index 49154fc..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/Hind-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/hind/Hind-Medium.ttf b/rxdart/example/flutter/github_search/fonts/hind/Hind-Medium.ttf
deleted file mode 100755
index be1f60f..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/Hind-Medium.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/hind/Hind-Regular.ttf b/rxdart/example/flutter/github_search/fonts/hind/Hind-Regular.ttf
deleted file mode 100755
index 5d027b2..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/Hind-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/hind/Hind-Semibold.ttf b/rxdart/example/flutter/github_search/fonts/hind/Hind-Semibold.ttf
deleted file mode 100755
index 0f38131..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/Hind-Semibold.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/hind/METADATA.json b/rxdart/example/flutter/github_search/fonts/hind/METADATA.json
deleted file mode 100755
index 425b06f..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/METADATA.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{

-  "name": "Hind",

-  "designer": "Indian Type Foundry",

-  "license": "OFL",

-  "visibility": "External",

-  "category": "Sans Serif",

-  "size": 79589,

-  "fonts": [

-    {

-      "name": "Hind",

-      "style": "normal",

-      "weight": 700,

-      "filename": "Hind-Bold.ttf",

-      "postScriptName": "Hind-Bold",

-      "fullName": "Hind Bold",

-      "copyright": "Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com)"

-    },

-    {

-      "name": "Hind",

-      "style": "normal",

-      "weight": 300,

-      "filename": "Hind-Light.ttf",

-      "postScriptName": "Hind-Light",

-      "fullName": "Hind Light",

-      "copyright": "Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com)"

-    },

-    {

-      "name": "Hind",

-      "style": "normal",

-      "weight": 500,

-      "filename": "Hind-Medium.ttf",

-      "postScriptName": "Hind-Medium",

-      "fullName": "Hind Medium",

-      "copyright": "Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com)"

-    },

-    {

-      "name": "Hind",

-      "style": "normal",

-      "weight": 400,

-      "filename": "Hind-Regular.ttf",

-      "postScriptName": "Hind-Regular",

-      "fullName": "Hind Regular",

-      "copyright": "Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com)"

-    },

-    {

-      "name": "Hind",

-      "style": "normal",

-      "weight": 600,

-      "filename": "Hind-Semibold.ttf",

-      "postScriptName": "Hind-Semibold",

-      "fullName": "Hind Semibold",

-      "copyright": "Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com)"

-    }

-  ],

-  "subsets": [

-    "latin",

-    "latin-ext",

-    "devanagari",

-    "menu"

-  ],

-  "dateAdded": "2014-06-25"

-}

diff --git a/rxdart/example/flutter/github_search/fonts/hind/OFL.txt b/rxdart/example/flutter/github_search/fonts/hind/OFL.txt
deleted file mode 100755
index 79115a5..0000000
--- a/rxdart/example/flutter/github_search/fonts/hind/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).

-

-This Font Software is licensed under the SIL Open Font License, Version 1.1.

-This license is copied below, and is also available with a FAQ at:

-http://scripts.sil.org/OFL

-

-

------------------------------------------------------------

-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

------------------------------------------------------------

-

-PREAMBLE

-The goals of the Open Font License (OFL) are to stimulate worldwide

-development of collaborative font projects, to support the font creation

-efforts of academic and linguistic communities, and to provide a free and

-open framework in which fonts may be shared and improved in partnership

-with others.

-

-The OFL allows the licensed fonts to be used, studied, modified and

-redistributed freely as long as they are not sold by themselves. The

-fonts, including any derivative works, can be bundled, embedded, 

-redistributed and/or sold with any software provided that any reserved

-names are not used by derivative works. The fonts and derivatives,

-however, cannot be released under any other type of license. The

-requirement for fonts to remain under this license does not apply

-to any document created using the fonts or their derivatives.

-

-DEFINITIONS

-"Font Software" refers to the set of files released by the Copyright

-Holder(s) under this license and clearly marked as such. This may

-include source files, build scripts and documentation.

-

-"Reserved Font Name" refers to any names specified as such after the

-copyright statement(s).

-

-"Original Version" refers to the collection of Font Software components as

-distributed by the Copyright Holder(s).

-

-"Modified Version" refers to any derivative made by adding to, deleting,

-or substituting -- in part or in whole -- any of the components of the

-Original Version, by changing formats or by porting the Font Software to a

-new environment.

-

-"Author" refers to any designer, engineer, programmer, technical

-writer or other person who contributed to the Font Software.

-

-PERMISSION & CONDITIONS

-Permission is hereby granted, free of charge, to any person obtaining

-a copy of the Font Software, to use, study, copy, merge, embed, modify,

-redistribute, and sell modified and unmodified copies of the Font

-Software, subject to the following conditions:

-

-1) Neither the Font Software nor any of its individual components,

-in Original or Modified Versions, may be sold by itself.

-

-2) Original or Modified Versions of the Font Software may be bundled,

-redistributed and/or sold with any software, provided that each copy

-contains the above copyright notice and this license. These can be

-included either as stand-alone text files, human-readable headers or

-in the appropriate machine-readable metadata fields within text or

-binary files as long as those fields can be easily viewed by the user.

-

-3) No Modified Version of the Font Software may use the Reserved Font

-Name(s) unless explicit written permission is granted by the corresponding

-Copyright Holder. This restriction only applies to the primary font name as

-presented to the users.

-

-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

-Software shall not be used to promote, endorse or advertise any

-Modified Version, except to acknowledge the contribution(s) of the

-Copyright Holder(s) and the Author(s) or with their explicit written

-permission.

-

-5) The Font Software, modified or unmodified, in part or in whole,

-must be distributed entirely under this license, and must not be

-distributed under any other license. The requirement for fonts to

-remain under this license does not apply to any document created

-using the Font Software.

-

-TERMINATION

-This license becomes null and void if any of the above conditions are

-not met.

-

-DISCLAIMER

-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

-OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/METADATA.json b/rxdart/example/flutter/github_search/fonts/montserrat/METADATA.json
deleted file mode 100755
index 9bc9706..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/METADATA.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{

-  "name": "Montserrat",

-  "designer": "Julieta Ulanovsky",

-  "license": "OFL",

-  "visibility": "External",

-  "category": "Sans Serif",

-  "size": 14615,

-  "fonts": [

-    {

-      "name": "Montserrat",

-      "style": "normal",

-      "weight": 900,

-      "filename": "Montserrat-Black.ttf",

-      "postScriptName": "Montserrat-Black",

-      "fullName": "Montserrat Black",

-      "copyright": "Copyright \u00a9 2014 by Julieta Ulanovsky. All rights reserved."

-    },

-    {

-      "name": "Montserrat",

-      "style": "normal",

-      "weight": 700,

-      "filename": "Montserrat-Bold.ttf",

-      "postScriptName": "Montserrat-Bold",

-      "fullName": "Montserrat Bold",

-      "copyright": "Copyright \u00a9 2014 by Julieta Ulanovsky. All rights reserved."

-    },

-    {

-      "name": "Montserrat",

-      "style": "normal",

-      "weight": 300,

-      "filename": "Montserrat-Light.ttf",

-      "postScriptName": "Montserrat-Light",

-      "fullName": "Montserrat Light",

-      "copyright": "Copyright \u00a9 2014 by Julieta Ulanovsky. All rights reserved."

-    },

-    {

-      "name": "Montserrat",

-      "style": "normal",

-      "weight": 400,

-      "filename": "Montserrat-Regular.ttf",

-      "postScriptName": "Montserrat-Regular",

-      "fullName": "Montserrat",

-      "copyright": "Copyright \u00a9 2014 by Julieta Ulanovsky. All rights reserved."

-    },

-    {

-      "name": "Montserrat",

-      "style": "normal",

-      "weight": 100,

-      "filename": "Montserrat-Thin.ttf",

-      "postScriptName": "Montserrat-Hairline",

-      "fullName": "Montserrat Hairline",

-      "copyright": "Copyright \u00a9 2014 by Julieta Ulanovsky. All rights reserved."

-    }

-  ],

-  "subsets": [

-    "latin",

-    "menu"

-  ],

-  "dateAdded": "2011-12-13"

-}

diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Black.ttf b/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Black.ttf
deleted file mode 100755
index fc62f02..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Black.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Bold.ttf b/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Bold.ttf
deleted file mode 100755
index 762e1af..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Light.ttf b/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Light.ttf
deleted file mode 100755
index cbeeb77..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Regular.ttf b/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Regular.ttf
deleted file mode 100755
index b436863..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Thin.ttf b/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Thin.ttf
deleted file mode 100755
index 58ba111..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/Montserrat-Thin.ttf
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/fonts/montserrat/OFL.txt b/rxdart/example/flutter/github_search/fonts/montserrat/OFL.txt
deleted file mode 100755
index 119174b..0000000
--- a/rxdart/example/flutter/github_search/fonts/montserrat/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright (c) 2011-2012, Julieta Ulanovsky (julieta.ulanovsky@gmail.com), with Reserved Font Names 'Montserrat'

-

-This Font Software is licensed under the SIL Open Font License, Version 1.1.

-This license is copied below, and is also available with a FAQ at:

-http://scripts.sil.org/OFL

-

-

------------------------------------------------------------

-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

------------------------------------------------------------

-

-PREAMBLE

-The goals of the Open Font License (OFL) are to stimulate worldwide

-development of collaborative font projects, to support the font creation

-efforts of academic and linguistic communities, and to provide a free and

-open framework in which fonts may be shared and improved in partnership

-with others.

-

-The OFL allows the licensed fonts to be used, studied, modified and

-redistributed freely as long as they are not sold by themselves. The

-fonts, including any derivative works, can be bundled, embedded, 

-redistributed and/or sold with any software provided that any reserved

-names are not used by derivative works. The fonts and derivatives,

-however, cannot be released under any other type of license. The

-requirement for fonts to remain under this license does not apply

-to any document created using the fonts or their derivatives.

-

-DEFINITIONS

-"Font Software" refers to the set of files released by the Copyright

-Holder(s) under this license and clearly marked as such. This may

-include source files, build scripts and documentation.

-

-"Reserved Font Name" refers to any names specified as such after the

-copyright statement(s).

-

-"Original Version" refers to the collection of Font Software components as

-distributed by the Copyright Holder(s).

-

-"Modified Version" refers to any derivative made by adding to, deleting,

-or substituting -- in part or in whole -- any of the components of the

-Original Version, by changing formats or by porting the Font Software to a

-new environment.

-

-"Author" refers to any designer, engineer, programmer, technical

-writer or other person who contributed to the Font Software.

-

-PERMISSION & CONDITIONS

-Permission is hereby granted, free of charge, to any person obtaining

-a copy of the Font Software, to use, study, copy, merge, embed, modify,

-redistribute, and sell modified and unmodified copies of the Font

-Software, subject to the following conditions:

-

-1) Neither the Font Software nor any of its individual components,

-in Original or Modified Versions, may be sold by itself.

-

-2) Original or Modified Versions of the Font Software may be bundled,

-redistributed and/or sold with any software, provided that each copy

-contains the above copyright notice and this license. These can be

-included either as stand-alone text files, human-readable headers or

-in the appropriate machine-readable metadata fields within text or

-binary files as long as those fields can be easily viewed by the user.

-

-3) No Modified Version of the Font Software may use the Reserved Font

-Name(s) unless explicit written permission is granted by the corresponding

-Copyright Holder. This restriction only applies to the primary font name as

-presented to the users.

-

-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

-Software shall not be used to promote, endorse or advertise any

-Modified Version, except to acknowledge the contribution(s) of the

-Copyright Holder(s) and the Author(s) or with their explicit written

-permission.

-

-5) The Font Software, modified or unmodified, in part or in whole,

-must be distributed entirely under this license, and must not be

-distributed under any other license. The requirement for fonts to

-remain under this license does not apply to any document created

-using the Font Software.

-

-TERMINATION

-This license becomes null and void if any of the above conditions are

-not met.

-

-DISCLAIMER

-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

-OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/rxdart/example/flutter/github_search/ios/.gitignore b/rxdart/example/flutter/github_search/ios/.gitignore
deleted file mode 100755
index 5cc90d8..0000000
--- a/rxdart/example/flutter/github_search/ios/.gitignore
+++ /dev/null
@@ -1,45 +0,0 @@
-.idea/

-.vagrant/

-.sconsign.dblite

-.svn/

-

-.DS_Store

-*.swp

-profile

-

-DerivedData/

-build/

-Runner/GeneratedPluginRegistrant.h

-GeneratedPluginRegistrant.m

-

-.generated/

-

-*.pbxuser

-*.mode1v3

-*.mode2v3

-*.perspectivev3

-

-!default.pbxuser

-!default.mode1v3

-!default.mode2v3

-!default.perspectivev3

-

-xcuserdata

-

-*.moved-aside

-

-*.pyc

-*sync/

-Icon?

-.tags*

-

-/Flutter/app.flx

-/Flutter/app.zip

-/Flutter/flutter_assets/

-/Flutter/App.framework

-/Flutter/Flutter.framework

-/Flutter/Generated.xcconfig

-/ServiceDefinitions.json

-

-Pods/

-.symlinks/

diff --git a/rxdart/example/flutter/github_search/ios/Flutter/AppFrameworkInfo.plist b/rxdart/example/flutter/github_search/ios/Flutter/AppFrameworkInfo.plist
deleted file mode 100755
index 79f1e34..0000000
--- a/rxdart/example/flutter/github_search/ios/Flutter/AppFrameworkInfo.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

-<plist version="1.0">

-<dict>

-  <key>CFBundleDevelopmentRegion</key>

-  <string>en</string>

-  <key>CFBundleExecutable</key>

-  <string>App</string>

-  <key>CFBundleIdentifier</key>

-  <string>io.flutter.flutter.app</string>

-  <key>CFBundleInfoDictionaryVersion</key>

-  <string>6.0</string>

-  <key>CFBundleName</key>

-  <string>App</string>

-  <key>CFBundlePackageType</key>

-  <string>FMWK</string>

-  <key>CFBundleShortVersionString</key>

-  <string>1.0</string>

-  <key>CFBundleSignature</key>

-  <string>????</string>

-  <key>CFBundleVersion</key>

-  <string>1.0</string>

-  <key>MinimumOSVersion</key>

-  <string>8.0</string>

-</dict>

-</plist>

diff --git a/rxdart/example/flutter/github_search/ios/Flutter/Debug.xcconfig b/rxdart/example/flutter/github_search/ios/Flutter/Debug.xcconfig
deleted file mode 100755
index 0b2d479..0000000
--- a/rxdart/example/flutter/github_search/ios/Flutter/Debug.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"

diff --git a/rxdart/example/flutter/github_search/ios/Flutter/Release.xcconfig b/rxdart/example/flutter/github_search/ios/Flutter/Release.xcconfig
deleted file mode 100755
index 0b2d479..0000000
--- a/rxdart/example/flutter/github_search/ios/Flutter/Release.xcconfig
+++ /dev/null
@@ -1 +0,0 @@
-#include "Generated.xcconfig"

diff --git a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.pbxproj b/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.pbxproj
deleted file mode 100755
index cb09287..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,438 +0,0 @@
-// !$*UTF8*$!

-{

-	archiveVersion = 1;

-	classes = {

-	};

-	objectVersion = 46;

-	objects = {

-

-/* Begin PBXBuildFile section */

-		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };

-		2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };

-		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };

-		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };

-		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };

-		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };

-		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };

-		9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };

-		9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };

-		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };

-		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };

-		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };

-		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };

-		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };

-/* End PBXBuildFile section */

-

-/* Begin PBXCopyFilesBuildPhase section */

-		9705A1C41CF9048500538489 /* Embed Frameworks */ = {

-			isa = PBXCopyFilesBuildPhase;

-			buildActionMask = 2147483647;

-			dstPath = "";

-			dstSubfolderSpec = 10;

-			files = (

-				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,

-				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,

-			);

-			name = "Embed Frameworks";

-			runOnlyForDeploymentPostprocessing = 0;

-		};

-/* End PBXCopyFilesBuildPhase section */

-

-/* Begin PBXFileReference section */

-		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };

-		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };

-		2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };

-		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };

-		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };

-		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };

-		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };

-		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };

-		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };

-		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };

-		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };

-		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };

-		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };

-		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };

-		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };

-		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };

-		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };

-/* End PBXFileReference section */

-

-/* Begin PBXFrameworksBuildPhase section */

-		97C146EB1CF9000F007C117D /* Frameworks */ = {

-			isa = PBXFrameworksBuildPhase;

-			buildActionMask = 2147483647;

-			files = (

-				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,

-				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,

-			);

-			runOnlyForDeploymentPostprocessing = 0;

-		};

-/* End PBXFrameworksBuildPhase section */

-

-/* Begin PBXGroup section */

-		9740EEB11CF90186004384FC /* Flutter */ = {

-			isa = PBXGroup;

-			children = (

-				2D5378251FAA1A9400D5DBA9 /* flutter_assets */,

-				3B80C3931E831B6300D905FE /* App.framework */,

-				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,

-				9740EEBA1CF902C7004384FC /* Flutter.framework */,

-				9740EEB21CF90195004384FC /* Debug.xcconfig */,

-				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,

-				9740EEB31CF90195004384FC /* Generated.xcconfig */,

-			);

-			name = Flutter;

-			sourceTree = "<group>";

-		};

-		97C146E51CF9000F007C117D = {

-			isa = PBXGroup;

-			children = (

-				9740EEB11CF90186004384FC /* Flutter */,

-				97C146F01CF9000F007C117D /* Runner */,

-				97C146EF1CF9000F007C117D /* Products */,

-				CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,

-			);

-			sourceTree = "<group>";

-		};

-		97C146EF1CF9000F007C117D /* Products */ = {

-			isa = PBXGroup;

-			children = (

-				97C146EE1CF9000F007C117D /* Runner.app */,

-			);

-			name = Products;

-			sourceTree = "<group>";

-		};

-		97C146F01CF9000F007C117D /* Runner */ = {

-			isa = PBXGroup;

-			children = (

-				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,

-				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,

-				97C146FA1CF9000F007C117D /* Main.storyboard */,

-				97C146FD1CF9000F007C117D /* Assets.xcassets */,

-				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,

-				97C147021CF9000F007C117D /* Info.plist */,

-				97C146F11CF9000F007C117D /* Supporting Files */,

-				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,

-				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,

-			);

-			path = Runner;

-			sourceTree = "<group>";

-		};

-		97C146F11CF9000F007C117D /* Supporting Files */ = {

-			isa = PBXGroup;

-			children = (

-				97C146F21CF9000F007C117D /* main.m */,

-			);

-			name = "Supporting Files";

-			sourceTree = "<group>";

-		};

-/* End PBXGroup section */

-

-/* Begin PBXNativeTarget section */

-		97C146ED1CF9000F007C117D /* Runner */ = {

-			isa = PBXNativeTarget;

-			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;

-			buildPhases = (

-				9740EEB61CF901F6004384FC /* Run Script */,

-				97C146EA1CF9000F007C117D /* Sources */,

-				97C146EB1CF9000F007C117D /* Frameworks */,

-				97C146EC1CF9000F007C117D /* Resources */,

-				9705A1C41CF9048500538489 /* Embed Frameworks */,

-				3B06AD1E1E4923F5004D2608 /* Thin Binary */,

-			);

-			buildRules = (

-			);

-			dependencies = (

-			);

-			name = Runner;

-			productName = Runner;

-			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;

-			productType = "com.apple.product-type.application";

-		};

-/* End PBXNativeTarget section */

-

-/* Begin PBXProject section */

-		97C146E61CF9000F007C117D /* Project object */ = {

-			isa = PBXProject;

-			attributes = {

-				LastUpgradeCheck = 0910;

-				ORGANIZATIONNAME = "The Chromium Authors";

-				TargetAttributes = {

-					97C146ED1CF9000F007C117D = {

-						CreatedOnToolsVersion = 7.3.1;

-					};

-				};

-			};

-			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;

-			compatibilityVersion = "Xcode 3.2";

-			developmentRegion = English;

-			hasScannedForEncodings = 0;

-			knownRegions = (

-				en,

-				Base,

-			);

-			mainGroup = 97C146E51CF9000F007C117D;

-			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;

-			projectDirPath = "";

-			projectRoot = "";

-			targets = (

-				97C146ED1CF9000F007C117D /* Runner */,

-			);

-		};

-/* End PBXProject section */

-

-/* Begin PBXResourcesBuildPhase section */

-		97C146EC1CF9000F007C117D /* Resources */ = {

-			isa = PBXResourcesBuildPhase;

-			buildActionMask = 2147483647;

-			files = (

-				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,

-				9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,

-				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,

-				9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,

-				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,

-				2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,

-				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,

-			);

-			runOnlyForDeploymentPostprocessing = 0;

-		};

-/* End PBXResourcesBuildPhase section */

-

-/* Begin PBXShellScriptBuildPhase section */

-		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {

-			isa = PBXShellScriptBuildPhase;

-			buildActionMask = 2147483647;

-			files = (

-			);

-			inputPaths = (

-			);

-			name = "Thin Binary";

-			outputPaths = (

-			);

-			runOnlyForDeploymentPostprocessing = 0;

-			shellPath = /bin/sh;

-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";

-		};

-		9740EEB61CF901F6004384FC /* Run Script */ = {

-			isa = PBXShellScriptBuildPhase;

-			buildActionMask = 2147483647;

-			files = (

-			);

-			inputPaths = (

-			);

-			name = "Run Script";

-			outputPaths = (

-			);

-			runOnlyForDeploymentPostprocessing = 0;

-			shellPath = /bin/sh;

-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";

-		};

-/* End PBXShellScriptBuildPhase section */

-

-/* Begin PBXSourcesBuildPhase section */

-		97C146EA1CF9000F007C117D /* Sources */ = {

-			isa = PBXSourcesBuildPhase;

-			buildActionMask = 2147483647;

-			files = (

-				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,

-				97C146F31CF9000F007C117D /* main.m in Sources */,

-				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,

-			);

-			runOnlyForDeploymentPostprocessing = 0;

-		};

-/* End PBXSourcesBuildPhase section */

-

-/* Begin PBXVariantGroup section */

-		97C146FA1CF9000F007C117D /* Main.storyboard */ = {

-			isa = PBXVariantGroup;

-			children = (

-				97C146FB1CF9000F007C117D /* Base */,

-			);

-			name = Main.storyboard;

-			sourceTree = "<group>";

-		};

-		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {

-			isa = PBXVariantGroup;

-			children = (

-				97C147001CF9000F007C117D /* Base */,

-			);

-			name = LaunchScreen.storyboard;

-			sourceTree = "<group>";

-		};

-/* End PBXVariantGroup section */

-

-/* Begin XCBuildConfiguration section */

-		97C147031CF9000F007C117D /* Debug */ = {

-			isa = XCBuildConfiguration;

-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;

-			buildSettings = {

-				ALWAYS_SEARCH_USER_PATHS = NO;

-				CLANG_ANALYZER_NONNULL = YES;

-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";

-				CLANG_CXX_LIBRARY = "libc++";

-				CLANG_ENABLE_MODULES = YES;

-				CLANG_ENABLE_OBJC_ARC = YES;

-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;

-				CLANG_WARN_BOOL_CONVERSION = YES;

-				CLANG_WARN_COMMA = YES;

-				CLANG_WARN_CONSTANT_CONVERSION = YES;

-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;

-				CLANG_WARN_EMPTY_BODY = YES;

-				CLANG_WARN_ENUM_CONVERSION = YES;

-				CLANG_WARN_INFINITE_RECURSION = YES;

-				CLANG_WARN_INT_CONVERSION = YES;

-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;

-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;

-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;

-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;

-				CLANG_WARN_STRICT_PROTOTYPES = YES;

-				CLANG_WARN_SUSPICIOUS_MOVE = YES;

-				CLANG_WARN_UNREACHABLE_CODE = YES;

-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;

-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";

-				COPY_PHASE_STRIP = NO;

-				DEBUG_INFORMATION_FORMAT = dwarf;

-				ENABLE_STRICT_OBJC_MSGSEND = YES;

-				ENABLE_TESTABILITY = YES;

-				GCC_C_LANGUAGE_STANDARD = gnu99;

-				GCC_DYNAMIC_NO_PIC = NO;

-				GCC_NO_COMMON_BLOCKS = YES;

-				GCC_OPTIMIZATION_LEVEL = 0;

-				GCC_PREPROCESSOR_DEFINITIONS = (

-					"DEBUG=1",

-					"$(inherited)",

-				);

-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;

-				GCC_WARN_UNDECLARED_SELECTOR = YES;

-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;

-				GCC_WARN_UNUSED_FUNCTION = YES;

-				GCC_WARN_UNUSED_VARIABLE = YES;

-				IPHONEOS_DEPLOYMENT_TARGET = 8.0;

-				MTL_ENABLE_DEBUG_INFO = YES;

-				ONLY_ACTIVE_ARCH = YES;

-				SDKROOT = iphoneos;

-				TARGETED_DEVICE_FAMILY = "1,2";

-			};

-			name = Debug;

-		};

-		97C147041CF9000F007C117D /* Release */ = {

-			isa = XCBuildConfiguration;

-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;

-			buildSettings = {

-				ALWAYS_SEARCH_USER_PATHS = NO;

-				CLANG_ANALYZER_NONNULL = YES;

-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";

-				CLANG_CXX_LIBRARY = "libc++";

-				CLANG_ENABLE_MODULES = YES;

-				CLANG_ENABLE_OBJC_ARC = YES;

-				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;

-				CLANG_WARN_BOOL_CONVERSION = YES;

-				CLANG_WARN_COMMA = YES;

-				CLANG_WARN_CONSTANT_CONVERSION = YES;

-				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;

-				CLANG_WARN_EMPTY_BODY = YES;

-				CLANG_WARN_ENUM_CONVERSION = YES;

-				CLANG_WARN_INFINITE_RECURSION = YES;

-				CLANG_WARN_INT_CONVERSION = YES;

-				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;

-				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;

-				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;

-				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;

-				CLANG_WARN_STRICT_PROTOTYPES = YES;

-				CLANG_WARN_SUSPICIOUS_MOVE = YES;

-				CLANG_WARN_UNREACHABLE_CODE = YES;

-				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;

-				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";

-				COPY_PHASE_STRIP = NO;

-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";

-				ENABLE_NS_ASSERTIONS = NO;

-				ENABLE_STRICT_OBJC_MSGSEND = YES;

-				GCC_C_LANGUAGE_STANDARD = gnu99;

-				GCC_NO_COMMON_BLOCKS = YES;

-				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

-				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;

-				GCC_WARN_UNDECLARED_SELECTOR = YES;

-				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;

-				GCC_WARN_UNUSED_FUNCTION = YES;

-				GCC_WARN_UNUSED_VARIABLE = YES;

-				IPHONEOS_DEPLOYMENT_TARGET = 8.0;

-				MTL_ENABLE_DEBUG_INFO = NO;

-				SDKROOT = iphoneos;

-				TARGETED_DEVICE_FAMILY = "1,2";

-				VALIDATE_PRODUCT = YES;

-			};

-			name = Release;

-		};

-		97C147061CF9000F007C117D /* Debug */ = {

-			isa = XCBuildConfiguration;

-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;

-			buildSettings = {

-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;

-				CURRENT_PROJECT_VERSION = 1;

-				ENABLE_BITCODE = NO;

-				FRAMEWORK_SEARCH_PATHS = (

-					"$(inherited)",

-					"$(PROJECT_DIR)/Flutter",

-				);

-				INFOPLIST_FILE = Runner/Info.plist;

-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

-				LIBRARY_SEARCH_PATHS = (

-					"$(inherited)",

-					"$(PROJECT_DIR)/Flutter",

-				);

-				PRODUCT_BUNDLE_IDENTIFIER = com.example.githubSearch;

-				PRODUCT_NAME = "$(TARGET_NAME)";

-				VERSIONING_SYSTEM = "apple-generic";

-			};

-			name = Debug;

-		};

-		97C147071CF9000F007C117D /* Release */ = {

-			isa = XCBuildConfiguration;

-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;

-			buildSettings = {

-				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;

-				CURRENT_PROJECT_VERSION = 1;

-				ENABLE_BITCODE = NO;

-				FRAMEWORK_SEARCH_PATHS = (

-					"$(inherited)",

-					"$(PROJECT_DIR)/Flutter",

-				);

-				INFOPLIST_FILE = Runner/Info.plist;

-				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

-				LIBRARY_SEARCH_PATHS = (

-					"$(inherited)",

-					"$(PROJECT_DIR)/Flutter",

-				);

-				PRODUCT_BUNDLE_IDENTIFIER = com.example.githubSearch;

-				PRODUCT_NAME = "$(TARGET_NAME)";

-				VERSIONING_SYSTEM = "apple-generic";

-			};

-			name = Release;

-		};

-/* End XCBuildConfiguration section */

-

-/* Begin XCConfigurationList section */

-		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {

-			isa = XCConfigurationList;

-			buildConfigurations = (

-				97C147031CF9000F007C117D /* Debug */,

-				97C147041CF9000F007C117D /* Release */,

-			);

-			defaultConfigurationIsVisible = 0;

-			defaultConfigurationName = Release;

-		};

-		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {

-			isa = XCConfigurationList;

-			buildConfigurations = (

-				97C147061CF9000F007C117D /* Debug */,

-				97C147071CF9000F007C117D /* Release */,

-			);

-			defaultConfigurationIsVisible = 0;

-			defaultConfigurationName = Release;

-		};

-/* End XCConfigurationList section */

-	};

-	rootObject = 97C146E61CF9000F007C117D /* Project object */;

-}

diff --git a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100755
index 59c6d39..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<Workspace

-   version = "1.0">

-   <FileRef

-      location = "group:Runner.xcodeproj">

-   </FileRef>

-</Workspace>

diff --git a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
deleted file mode 100755
index 5995c2f..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ /dev/null
@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<Scheme

-   LastUpgradeVersion = "0910"

-   version = "1.3">

-   <BuildAction

-      parallelizeBuildables = "YES"

-      buildImplicitDependencies = "YES">

-      <BuildActionEntries>

-         <BuildActionEntry

-            buildForTesting = "YES"

-            buildForRunning = "YES"

-            buildForProfiling = "YES"

-            buildForArchiving = "YES"

-            buildForAnalyzing = "YES">

-            <BuildableReference

-               BuildableIdentifier = "primary"

-               BlueprintIdentifier = "97C146ED1CF9000F007C117D"

-               BuildableName = "Runner.app"

-               BlueprintName = "Runner"

-               ReferencedContainer = "container:Runner.xcodeproj">

-            </BuildableReference>

-         </BuildActionEntry>

-      </BuildActionEntries>

-   </BuildAction>

-   <TestAction

-      buildConfiguration = "Debug"

-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"

-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"

-      language = ""

-      shouldUseLaunchSchemeArgsEnv = "YES">

-      <Testables>

-      </Testables>

-      <MacroExpansion>

-         <BuildableReference

-            BuildableIdentifier = "primary"

-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"

-            BuildableName = "Runner.app"

-            BlueprintName = "Runner"

-            ReferencedContainer = "container:Runner.xcodeproj">

-         </BuildableReference>

-      </MacroExpansion>

-      <AdditionalOptions>

-      </AdditionalOptions>

-   </TestAction>

-   <LaunchAction

-      buildConfiguration = "Debug"

-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"

-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"

-      language = ""

-      launchStyle = "0"

-      useCustomWorkingDirectory = "NO"

-      ignoresPersistentStateOnLaunch = "NO"

-      debugDocumentVersioning = "YES"

-      debugServiceExtension = "internal"

-      allowLocationSimulation = "YES">

-      <BuildableProductRunnable

-         runnableDebuggingMode = "0">

-         <BuildableReference

-            BuildableIdentifier = "primary"

-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"

-            BuildableName = "Runner.app"

-            BlueprintName = "Runner"

-            ReferencedContainer = "container:Runner.xcodeproj">

-         </BuildableReference>

-      </BuildableProductRunnable>

-      <AdditionalOptions>

-      </AdditionalOptions>

-   </LaunchAction>

-   <ProfileAction

-      buildConfiguration = "Release"

-      shouldUseLaunchSchemeArgsEnv = "YES"

-      savedToolIdentifier = ""

-      useCustomWorkingDirectory = "NO"

-      debugDocumentVersioning = "YES">

-      <BuildableProductRunnable

-         runnableDebuggingMode = "0">

-         <BuildableReference

-            BuildableIdentifier = "primary"

-            BlueprintIdentifier = "97C146ED1CF9000F007C117D"

-            BuildableName = "Runner.app"

-            BlueprintName = "Runner"

-            ReferencedContainer = "container:Runner.xcodeproj">

-         </BuildableReference>

-      </BuildableProductRunnable>

-   </ProfileAction>

-   <AnalyzeAction

-      buildConfiguration = "Debug">

-   </AnalyzeAction>

-   <ArchiveAction

-      buildConfiguration = "Release"

-      revealArchiveInOrganizer = "YES">

-   </ArchiveAction>

-</Scheme>

diff --git a/rxdart/example/flutter/github_search/ios/Runner.xcworkspace/contents.xcworkspacedata b/rxdart/example/flutter/github_search/ios/Runner.xcworkspace/contents.xcworkspacedata
deleted file mode 100755
index 59c6d39..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<Workspace

-   version = "1.0">

-   <FileRef

-      location = "group:Runner.xcodeproj">

-   </FileRef>

-</Workspace>

diff --git a/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.h b/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.h
deleted file mode 100755
index a7bb489..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#import <Flutter/Flutter.h>

-#import <UIKit/UIKit.h>

-

-@interface AppDelegate : FlutterAppDelegate

-

-@end

diff --git a/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.m b/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.m
deleted file mode 100755
index 658750b..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/AppDelegate.m
+++ /dev/null
@@ -1,13 +0,0 @@
-#include "AppDelegate.h"

-#include "GeneratedPluginRegistrant.h"

-

-@implementation AppDelegate

-

-- (BOOL)application:(UIApplication *)application

-    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

-  [GeneratedPluginRegistrant registerWithRegistry:self];

-  // Override point for customization after application launch.

-  return [super application:application didFinishLaunchingWithOptions:launchOptions];

-}

-

-@end

diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100755
index 1950fd8..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,122 +0,0 @@
-{

-  "images" : [

-    {

-      "size" : "20x20",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-20x20@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "20x20",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-20x20@3x.png",

-      "scale" : "3x"

-    },

-    {

-      "size" : "29x29",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-29x29@1x.png",

-      "scale" : "1x"

-    },

-    {

-      "size" : "29x29",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-29x29@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "29x29",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-29x29@3x.png",

-      "scale" : "3x"

-    },

-    {

-      "size" : "40x40",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-40x40@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "40x40",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-40x40@3x.png",

-      "scale" : "3x"

-    },

-    {

-      "size" : "60x60",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-60x60@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "60x60",

-      "idiom" : "iphone",

-      "filename" : "Icon-App-60x60@3x.png",

-      "scale" : "3x"

-    },

-    {

-      "size" : "20x20",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-20x20@1x.png",

-      "scale" : "1x"

-    },

-    {

-      "size" : "20x20",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-20x20@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "29x29",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-29x29@1x.png",

-      "scale" : "1x"

-    },

-    {

-      "size" : "29x29",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-29x29@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "40x40",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-40x40@1x.png",

-      "scale" : "1x"

-    },

-    {

-      "size" : "40x40",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-40x40@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "76x76",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-76x76@1x.png",

-      "scale" : "1x"

-    },

-    {

-      "size" : "76x76",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-76x76@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "83.5x83.5",

-      "idiom" : "ipad",

-      "filename" : "Icon-App-83.5x83.5@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "size" : "1024x1024",

-      "idiom" : "ios-marketing",

-      "filename" : "Icon-App-1024x1024@1x.png",

-      "scale" : "1x"

-    }

-  ],

-  "info" : {

-    "version" : 1,

-    "author" : "xcode"

-  }

-}

diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100755
index 3d43d11..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100755
index 28c6bf0..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100755
index 2ccbfd9..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100755
index f091b6b..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100755
index 4cde121..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100755
index d0ef06e..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100755
index dcdc230..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100755
index 2ccbfd9..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100755
index c8f9ed8..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100755
index a6d6b86..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100755
index a6d6b86..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100755
index 75b2d16..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100755
index c4df70d..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100755
index 6a84f41..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100755
index d0e1f58..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
deleted file mode 100755
index d08a4de..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{

-  "images" : [

-    {

-      "idiom" : "universal",

-      "filename" : "LaunchImage.png",

-      "scale" : "1x"

-    },

-    {

-      "idiom" : "universal",

-      "filename" : "LaunchImage@2x.png",

-      "scale" : "2x"

-    },

-    {

-      "idiom" : "universal",

-      "filename" : "LaunchImage@3x.png",

-      "scale" : "3x"

-    }

-  ],

-  "info" : {

-    "version" : 1,

-    "author" : "xcode"

-  }

-}

diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
deleted file mode 100755
index 9da19ea..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
deleted file mode 100755
index 9da19ea..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
deleted file mode 100755
index 9da19ea..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
+++ /dev/null
Binary files differ
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
deleted file mode 100755
index 65a94b5..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Launch Screen Assets

-

-You can customize the launch screen with your own desired assets by replacing the image files in this directory.

-

-You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/LaunchScreen.storyboard b/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/LaunchScreen.storyboard
deleted file mode 100755
index 497371e..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>

-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">

-    <dependencies>

-        <deployment identifier="iOS"/>

-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>

-    </dependencies>

-    <scenes>

-        <!--View Controller-->

-        <scene sceneID="EHf-IW-A2E">

-            <objects>

-                <viewController id="01J-lp-oVM" sceneMemberID="viewController">

-                    <layoutGuides>

-                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>

-                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>

-                    </layoutGuides>

-                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">

-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

-                        <subviews>

-                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">

-                            </imageView>

-                        </subviews>

-                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>

-                        <constraints>

-                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>

-                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>

-                        </constraints>

-                    </view>

-                </viewController>

-                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>

-            </objects>

-            <point key="canvasLocation" x="53" y="375"/>

-        </scene>

-    </scenes>

-    <resources>

-        <image name="LaunchImage" width="168" height="185"/>

-    </resources>

-</document>

diff --git a/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/Main.storyboard b/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/Main.storyboard
deleted file mode 100755
index bbb83ca..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>

-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">

-    <dependencies>

-        <deployment identifier="iOS"/>

-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>

-    </dependencies>

-    <scenes>

-        <!--Flutter View Controller-->

-        <scene sceneID="tne-QT-ifu">

-            <objects>

-                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">

-                    <layoutGuides>

-                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>

-                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>

-                    </layoutGuides>

-                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">

-                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>

-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

-                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>

-                    </view>

-                </viewController>

-                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>

-            </objects>

-        </scene>

-    </scenes>

-</document>

diff --git a/rxdart/example/flutter/github_search/ios/Runner/Info.plist b/rxdart/example/flutter/github_search/ios/Runner/Info.plist
deleted file mode 100755
index 6452c11..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/Info.plist
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

-<plist version="1.0">

-<dict>

-	<key>CFBundleDevelopmentRegion</key>

-	<string>en</string>

-	<key>CFBundleExecutable</key>

-	<string>$(EXECUTABLE_NAME)</string>

-	<key>CFBundleIdentifier</key>

-	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

-	<key>CFBundleInfoDictionaryVersion</key>

-	<string>6.0</string>

-	<key>CFBundleName</key>

-	<string>github_search</string>

-	<key>CFBundlePackageType</key>

-	<string>APPL</string>

-	<key>CFBundleShortVersionString</key>

-	<string>1.0</string>

-	<key>CFBundleSignature</key>

-	<string>????</string>

-	<key>CFBundleVersion</key>

-	<string>1</string>

-	<key>LSRequiresIPhoneOS</key>

-	<true/>

-	<key>UILaunchStoryboardName</key>

-	<string>LaunchScreen</string>

-	<key>UIMainStoryboardFile</key>

-	<string>Main</string>

-	<key>UISupportedInterfaceOrientations</key>

-	<array>

-		<string>UIInterfaceOrientationPortrait</string>

-		<string>UIInterfaceOrientationLandscapeLeft</string>

-		<string>UIInterfaceOrientationLandscapeRight</string>

-	</array>

-	<key>UISupportedInterfaceOrientations~ipad</key>

-	<array>

-		<string>UIInterfaceOrientationPortrait</string>

-		<string>UIInterfaceOrientationPortraitUpsideDown</string>

-		<string>UIInterfaceOrientationLandscapeLeft</string>

-		<string>UIInterfaceOrientationLandscapeRight</string>

-	</array>

-	<key>UIViewControllerBasedStatusBarAppearance</key>

-	<false/>

-</dict>

-</plist>

diff --git a/rxdart/example/flutter/github_search/ios/Runner/main.m b/rxdart/example/flutter/github_search/ios/Runner/main.m
deleted file mode 100755
index 4618607..0000000
--- a/rxdart/example/flutter/github_search/ios/Runner/main.m
+++ /dev/null
@@ -1,9 +0,0 @@
-#import <Flutter/Flutter.h>

-#import <UIKit/UIKit.h>

-#import "AppDelegate.h"

-

-int main(int argc, char* argv[]) {

-  @autoreleasepool {

-    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/empty_result_widget.dart b/rxdart/example/flutter/github_search/lib/empty_result_widget.dart
deleted file mode 100755
index 3545fea..0000000
--- a/rxdart/example/flutter/github_search/lib/empty_result_widget.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-import 'package:flutter/material.dart';

-

-class EmptyWidget extends StatelessWidget {

-  final bool visible;

-

-  const EmptyWidget({Key key, this.visible}) : super(key: key);

-

-  @override

-  Widget build(BuildContext context) {

-    return AnimatedOpacity(

-      duration: Duration(milliseconds: 300),

-      opacity: visible ? 1.0 : 0.0,

-      child: Container(

-        alignment: FractionalOffset.center,

-        child: Column(

-          mainAxisAlignment: MainAxisAlignment.center,

-          children: <Widget>[

-            Icon(

-              Icons.warning,

-              color: Colors.yellow[200],

-              size: 80.0,

-            ),

-            Container(

-              padding: EdgeInsets.only(top: 16.0),

-              child: Text(

-                "No results",

-                style: TextStyle(color: Colors.yellow[100]),

-              ),

-            )

-          ],

-        ),

-      ),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/github_api.dart b/rxdart/example/flutter/github_search/lib/github_api.dart
deleted file mode 100755
index 1f54a76..0000000
--- a/rxdart/example/flutter/github_search/lib/github_api.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-import 'dart:async';

-import 'dart:convert';

-import 'dart:io';

-

-import 'package:http/http.dart' as http;

-

-class GithubApi {

-  final String baseUrl;

-  final Map<String, SearchResult> cache;

-  final http.Client client;

-

-  GithubApi({

-    HttpClient client,

-    Map<String, SearchResult> cache,

-    this.baseUrl = "https://api.github.com/search/repositories?q=",

-  })  : this.client = client ?? http.Client(),

-        this.cache = cache ?? <String, SearchResult>{};

-

-  /// Search Github for repositories using the given term

-  Future<SearchResult> search(String term) async {

-    if (cache.containsKey(term)) {

-      return cache[term];

-    } else {

-      final result = await _fetchResults(term);

-

-      cache[term] = result;

-

-      return result;

-    }

-  }

-

-  Future<SearchResult> _fetchResults(String term) async {

-    final response = await client.get(Uri.parse("$baseUrl$term"));

-    final results = json.decode(response.body);

-

-    return SearchResult.fromJson(results['items']);

-  }

-}

-

-class SearchResult {

-  final List<SearchResultItem> items;

-

-  SearchResult(this.items);

-

-  factory SearchResult.fromJson(dynamic json) {

-    final items = (json as List)

-        .cast<Map<String, Object>>()

-        .map((Map<String, Object> item) {

-      return SearchResultItem.fromJson(item);

-    }).toList();

-

-    return SearchResult(items);

-  }

-

-  bool get isPopulated => items.isNotEmpty;

-

-  bool get isEmpty => items.isEmpty;

-}

-

-class SearchResultItem {

-  final String fullName;

-  final String url;

-  final String avatarUrl;

-

-  SearchResultItem(this.fullName, this.url, this.avatarUrl);

-

-  factory SearchResultItem.fromJson(Map<String, Object> json) {

-    return SearchResultItem(

-      json['full_name'] as String,

-      json["html_url"] as String,

-      (json["owner"] as Map<String, Object>)["avatar_url"] as String,

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/main.dart b/rxdart/example/flutter/github_search/lib/main.dart
deleted file mode 100755
index ecd25de..0000000
--- a/rxdart/example/flutter/github_search/lib/main.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'package:flutter/material.dart';

-import 'package:github_search/github_api.dart';

-import 'package:github_search/search_widget.dart';

-

-void main(GithubApi api) {

-  runApp(SearchApp(api: api));

-}

-

-class SearchApp extends StatefulWidget {

-  final GithubApi api;

-

-  SearchApp({Key key, this.api}) : super(key: key);

-

-  @override

-  _RxDartGithubSearchAppState createState() => _RxDartGithubSearchAppState();

-}

-

-class _RxDartGithubSearchAppState extends State<SearchApp> {

-  @override

-  Widget build(BuildContext context) {

-    return MaterialApp(

-      title: 'RxDart Github Search',

-      theme: ThemeData(

-        brightness: Brightness.dark,

-        primarySwatch: Colors.grey,

-      ),

-      home: SearchScreen(api: widget.api),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_bloc.dart b/rxdart/example/flutter/github_search/lib/search_bloc.dart
deleted file mode 100755
index 5e9a34f..0000000
--- a/rxdart/example/flutter/github_search/lib/search_bloc.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-import 'dart:async';

-

-import 'package:github_search/github_api.dart';

-import 'package:github_search/search_state.dart';

-import 'package:rxdart/rxdart.dart';

-

-class SearchBloc {

-  final Sink<String> onTextChanged;

-  final Stream<SearchState> state;

-

-  factory SearchBloc(GithubApi api) {

-    final onTextChanged = PublishSubject<String>();

-

-    final state = onTextChanged

-        // If the text has not changed, do not perform a new search

-        .distinct()

-        // Wait for the user to stop typing for 250ms before running a search

-        .debounce(const Duration(milliseconds: 250))

-        // Call the Github api with the given search term and convert it to a

-        // State. If another search term is entered, flatMapLatest will ensure

-        // the previous search is discarded so we don't deliver stale results

-        // to the View.

-        .switchMap<SearchState>((String term) => _search(term, api))

-        // The initial state to deliver to the screen.

-        .startWith(SearchNoTerm());

-

-    return SearchBloc._(onTextChanged, state);

-  }

-

-  SearchBloc._(this.onTextChanged, this.state);

-

-  void dispose() {

-    onTextChanged.close();

-  }

-

-  static Stream<SearchState> _search(String term, GithubApi api) async* {

-    if (term.isEmpty) {

-      yield SearchNoTerm();

-    } else {

-      yield SearchLoading();

-

-      try {

-        final result = await api.search(term);

-

-        if (result.isEmpty) {

-          yield SearchEmpty();

-        } else {

-          yield SearchPopulated(result);

-        }

-      } catch (e) {

-        yield SearchError();

-      }

-    }

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_error_widget.dart b/rxdart/example/flutter/github_search/lib/search_error_widget.dart
deleted file mode 100755
index ae4bbb6..0000000
--- a/rxdart/example/flutter/github_search/lib/search_error_widget.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-import 'package:flutter/material.dart';

-

-class SearchErrorWidget extends StatelessWidget {

-  final bool visible;

-

-  const SearchErrorWidget({Key key, this.visible}) : super(key: key);

-

-  @override

-  Widget build(BuildContext context) {

-    return AnimatedOpacity(

-      duration: Duration(milliseconds: 300),

-      opacity: visible ? 1.0 : 0.0,

-      child: Container(

-        alignment: FractionalOffset.center,

-        child: Column(

-          mainAxisAlignment: MainAxisAlignment.center,

-          crossAxisAlignment: CrossAxisAlignment.center,

-          children: <Widget>[

-            Icon(Icons.error_outline, color: Colors.red[300], size: 80.0),

-            Container(

-              padding: EdgeInsets.only(top: 16.0),

-              child: Text(

-                "Rate limit exceeded",

-                style: TextStyle(

-                  color: Colors.red[300],

-                ),

-              ),

-            )

-          ],

-        ),

-      ),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_intro_widget.dart b/rxdart/example/flutter/github_search/lib/search_intro_widget.dart
deleted file mode 100755
index aea6b44..0000000
--- a/rxdart/example/flutter/github_search/lib/search_intro_widget.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'package:flutter/material.dart';

-

-class SearchIntro extends StatelessWidget {

-  final bool visible;

-

-  const SearchIntro({Key key, this.visible}) : super(key: key);

-

-  @override

-  Widget build(BuildContext context) {

-    return AnimatedOpacity(

-      duration: Duration(milliseconds: 300),

-      opacity: visible ? 1.0 : 0.0,

-      child: Container(

-        alignment: FractionalOffset.center,

-        child: Column(

-          mainAxisAlignment: MainAxisAlignment.center,

-          children: <Widget>[

-            Icon(Icons.info, color: Colors.green[200], size: 80.0),

-            Container(

-              padding: EdgeInsets.only(top: 16.0),

-              child: Text(

-                "Enter a search term to begin",

-                style: TextStyle(

-                  color: Colors.green[100],

-                ),

-              ),

-            )

-          ],

-        ),

-      ),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_loading_widget.dart b/rxdart/example/flutter/github_search/lib/search_loading_widget.dart
deleted file mode 100755
index f4165e1..0000000
--- a/rxdart/example/flutter/github_search/lib/search_loading_widget.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-import 'package:flutter/material.dart';

-

-class LoadingWidget extends StatelessWidget {

-  final bool visible;

-

-  const LoadingWidget({Key key, this.visible}) : super(key: key);

-

-  @override

-  Widget build(BuildContext context) {

-    return AnimatedOpacity(

-      duration: Duration(milliseconds: 300),

-      opacity: visible ? 1.0 : 0.0,

-      child: Container(

-        alignment: FractionalOffset.center,

-        child: CircularProgressIndicator(),

-      ),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_result_widget.dart b/rxdart/example/flutter/github_search/lib/search_result_widget.dart
deleted file mode 100755
index 32d978d..0000000
--- a/rxdart/example/flutter/github_search/lib/search_result_widget.dart
+++ /dev/null
@@ -1,111 +0,0 @@
-import 'package:flutter/material.dart';

-import 'package:github_search/github_api.dart';

-

-class SearchResultWidget extends StatelessWidget {

-  final List<SearchResultItem> items;

-  final bool visible;

-

-  SearchResultWidget({Key key, @required this.items, bool visible})

-      : this.visible = visible ?? items.isNotEmpty,

-        super(key: key);

-

-  @override

-  Widget build(BuildContext context) {

-    return AnimatedOpacity(

-      duration: Duration(milliseconds: 300),

-      opacity: visible ? 1.0 : 0.0,

-      child: ListView.builder(

-        itemCount: items.length,

-        itemBuilder: (context, index) {

-          final item = items[index];

-

-          return InkWell(

-            onTap: () => showItem(context, item),

-            child: Container(

-              alignment: FractionalOffset.center,

-              margin: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 12.0),

-              child: Row(

-                crossAxisAlignment: CrossAxisAlignment.start,

-                children: <Widget>[

-                  Container(

-                    margin: EdgeInsets.only(right: 16.0),

-                    child: Hero(

-                      tag: item.fullName,

-                      child: ClipOval(

-                        child: Image.network(

-                          item.avatarUrl,

-                          width: 56.0,

-                          height: 56.0,

-                        ),

-                      ),

-                    ),

-                  ),

-                  Expanded(

-                    child: Column(

-                      crossAxisAlignment: CrossAxisAlignment.start,

-                      children: <Widget>[

-                        Container(

-                          margin: EdgeInsets.only(

-                            top: 6.0,

-                            bottom: 4.0,

-                          ),

-                          child: Text(

-                            "${item.fullName}",

-                            maxLines: 1,

-                            overflow: TextOverflow.ellipsis,

-                            style: TextStyle(

-                              fontFamily: "Montserrat",

-                              fontSize: 16.0,

-                              fontWeight: FontWeight.bold,

-                            ),

-                          ),

-                        ),

-                        Container(

-                          child: Text(

-                            "${item.url}",

-                            style: TextStyle(

-                              fontFamily: "Hind",

-                            ),

-                            maxLines: 1,

-                            overflow: TextOverflow.ellipsis,

-                          ),

-                        )

-                      ],

-                    ),

-                  )

-                ],

-              ),

-            ),

-          );

-        },

-      ),

-    );

-  }

-

-  void showItem(BuildContext context, SearchResultItem item) {

-    Navigator.push(

-      context,

-      MaterialPageRoute<Null>(

-        builder: (BuildContext context) {

-          return Scaffold(

-            resizeToAvoidBottomPadding: false,

-            body: GestureDetector(

-              key: Key(item.avatarUrl),

-              onTap: () => Navigator.pop(context),

-              child: SizedBox.expand(

-                child: Hero(

-                  tag: item.fullName,

-                  child: Image.network(

-                    item.avatarUrl,

-                    width: MediaQuery.of(context).size.width,

-                    height: 300.0,

-                  ),

-                ),

-              ),

-            ),

-          );

-        },

-      ),

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/lib/search_state.dart b/rxdart/example/flutter/github_search/lib/search_state.dart
deleted file mode 100755
index 0be1285..0000000
--- a/rxdart/example/flutter/github_search/lib/search_state.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-import 'package:github_search/github_api.dart';

-

-// The State represents the data the View requires. The View consumes a Stream

-// of States. The view rebuilds every time the Stream emits a new State!

-//

-// The State Stream will emit new States depending on the situation: The

-// initial state, loading states, the list of results, and any errors that

-// happen.

-//

-// The State Stream responds to input from the View by accepting a

-// Stream<String>. We call this Stream the onTextChanged "intent".

-class SearchState {}

-

-class SearchLoading extends SearchState {}

-

-class SearchError extends SearchState {}

-

-class SearchNoTerm extends SearchState {}

-

-class SearchPopulated extends SearchState {

-  final SearchResult result;

-

-  SearchPopulated(this.result);

-}

-

-class SearchEmpty extends SearchState {}

diff --git a/rxdart/example/flutter/github_search/lib/search_widget.dart b/rxdart/example/flutter/github_search/lib/search_widget.dart
deleted file mode 100755
index ecd33b9..0000000
--- a/rxdart/example/flutter/github_search/lib/search_widget.dart
+++ /dev/null
@@ -1,106 +0,0 @@
-import 'package:flutter/material.dart';

-import 'package:github_search/empty_result_widget.dart';

-import 'package:github_search/github_api.dart';

-import 'package:github_search/search_bloc.dart';

-import 'package:github_search/search_error_widget.dart';

-import 'package:github_search/search_intro_widget.dart';

-import 'package:github_search/search_loading_widget.dart';

-import 'package:github_search/search_result_widget.dart';

-import 'package:github_search/search_state.dart';

-

-// The View in a Stream-based architecture takes two arguments: The State Stream

-// and the onTextChanged callback. In our case, the onTextChanged callback will

-// emit the latest String to a Stream<String> whenever it is called.

-//

-// The State will use the Stream<String> to send new search requests to the

-// GithubApi.

-class SearchScreen extends StatefulWidget {

-  final GithubApi api;

-

-  SearchScreen({Key key, GithubApi api})

-      : this.api = api ?? GithubApi(),

-        super(key: key);

-

-  @override

-  SearchScreenState createState() {

-    return SearchScreenState();

-  }

-}

-

-class SearchScreenState extends State<SearchScreen> {

-  SearchBloc bloc;

-

-  @override

-  void initState() {

-    super.initState();

-

-    bloc = SearchBloc(widget.api);

-  }

-

-  @override

-  void dispose() {

-    bloc.dispose();

-    super.dispose();

-  }

-

-  @override

-  Widget build(BuildContext context) {

-    return StreamBuilder<SearchState>(

-      stream: bloc.state,

-      initialData: SearchNoTerm(),

-      builder: (BuildContext context, AsyncSnapshot<SearchState> snapshot) {

-        final state = snapshot.data;

-

-        return Scaffold(

-          body: Stack(

-            children: <Widget>[

-              Flex(direction: Axis.vertical, children: <Widget>[

-                Container(

-                  padding: EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 4.0),

-                  child: TextField(

-                    decoration: InputDecoration(

-                      border: InputBorder.none,

-                      hintText: 'Search Github...',

-                    ),

-                    style: TextStyle(

-                      fontSize: 36.0,

-                      fontFamily: "Hind",

-                      decoration: TextDecoration.none,

-                    ),

-                    onChanged: bloc.onTextChanged.add,

-                  ),

-                ),

-                Expanded(

-                  child: Stack(

-                    children: <Widget>[

-                      // Fade in an intro screen if no term has been entered

-                      SearchIntro(visible: state is SearchNoTerm),

-

-                      // Fade in an Empty Result screen if the search contained

-                      // no items

-                      EmptyWidget(visible: state is SearchEmpty),

-

-                      // Fade in a loading screen when results are being fetched

-                      // from Github

-                      LoadingWidget(visible: state is SearchLoading),

-

-                      // Fade in an error if something went wrong when fetching

-                      // the results

-                      SearchErrorWidget(visible: state is SearchError),

-

-                      // Fade in the Result if available

-                      SearchResultWidget(

-                        items:

-                            state is SearchPopulated ? state.result.items : [],

-                      ),

-                    ],

-                  ),

-                )

-              ])

-            ],

-          ),

-        );

-      },

-    );

-  }

-}

diff --git a/rxdart/example/flutter/github_search/pubspec.yaml b/rxdart/example/flutter/github_search/pubspec.yaml
deleted file mode 100755
index 5d31261..0000000
--- a/rxdart/example/flutter/github_search/pubspec.yaml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: github_search

-description: A new flutter project.

-

-dependencies:

-  flutter:

-    sdk: flutter

-  rxdart:

-    path: ../../../

-  http: "^0.11.3+16"

-

-dev_dependencies:

-  mockito: 3.0.0

-  flutter_test:

-    sdk: flutter

-

-# For information on the generic Dart part of this file, see the

-# following page: https://www.dartlang.org/tools/pub/pubspec

-

-# The following section is specific to Flutter.

-flutter:

-

-  # The following line ensures that the Material Icons font is

-  # included with your application, so that you can use the icons in

-  # the Icons class.

-  uses-material-design: true

-

-  # To add assets to your application, add an assets section here, in

-  # this "flutter" section, as in:

-  # assets:

-  #  - images/a_dot_burr.jpeg

-  #  - images/a_dot_ham.jpeg

-

-  # To add custom fonts to your application, add a fonts section here,

-  # in this "flutter" section. Each entry in this list should have a

-  # "family" key with the font family name, and a "fonts" key with a

-  # list giving the asset and other descriptors for the font. For

-  # example:

-  fonts:

-    - family: Montserrat

-      fonts:

-        - asset: fonts/montserrat/Montserrat-Bold.ttf

-        - asset: fonts/montserrat/Montserrat-Regular.ttf

-    - family: Hind

-      fonts:

-        - asset: fonts/hind/Hind-Regular.ttf

diff --git a/rxdart/example/flutter/github_search/test/search_bloc_test.dart b/rxdart/example/flutter/github_search/test/search_bloc_test.dart
deleted file mode 100755
index d562f2f..0000000
--- a/rxdart/example/flutter/github_search/test/search_bloc_test.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-import 'dart:async';

-

-import 'package:github_search/github_api.dart';

-import 'package:github_search/search_bloc.dart';

-import 'package:github_search/search_state.dart';

-import 'package:mockito/mockito.dart';

-import 'package:test/test.dart';

-

-class MockGithubApi extends Mock implements GithubApi {}

-

-void main() {

-  group('SearchBloc', () {

-    test('starts with an initial no term state', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm]),

-      );

-    });

-

-    test('emits a loading state then result state when api call succeeds', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      when(api.search('T')).thenAnswer(

-          (_) async => SearchResult([SearchResultItem('A', 'B', 'C')]));

-

-      scheduleMicrotask(() {

-        bloc.onTextChanged.add('T');

-      });

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm, loading, populated]),

-      );

-    });

-

-    test('emits a no term state when user provides an empty search term', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      scheduleMicrotask(() {

-        bloc.onTextChanged.add('');

-      });

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm, noTerm]),

-      );

-    });

-

-    test('emits an empty state when no results are returned', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      when(api.search('T')).thenAnswer((_) async => SearchResult([]));

-

-      scheduleMicrotask(() {

-        bloc.onTextChanged.add('T');

-      });

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm, loading, empty]),

-      );

-    });

-

-    test('throws an error when the backend errors', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      when(api.search('T')).thenThrow(Exception());

-

-      scheduleMicrotask(() {

-        bloc.onTextChanged.add('T');

-      });

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm, loading, error]),

-      );

-    });

-

-    test('closes the stream on dispose', () {

-      final api = MockGithubApi();

-      final bloc = SearchBloc(api);

-

-      scheduleMicrotask(() {

-        bloc.dispose();

-      });

-

-      expect(

-        bloc.state,

-        emitsInOrder([noTerm, emitsDone]),

-      );

-    });

-  });

-}

-

-const noTerm = isInstanceOf<SearchNoTerm>();

-

-const loading = isInstanceOf<SearchLoading>();

-

-const empty = isInstanceOf<SearchEmpty>();

-

-const populated = isInstanceOf<SearchPopulated>();

-

-const error = isInstanceOf<SearchError>();

diff --git a/rxdart/example/web/dragdrop/dragdrop.dart b/rxdart/example/web/dragdrop/dragdrop.dart
deleted file mode 100755
index f6f1b6f..0000000
--- a/rxdart/example/web/dragdrop/dragdrop.dart
+++ /dev/null
@@ -1,31 +0,0 @@
-import 'dart:html';

-

-import 'package:rxdart/rxdart.dart';

-

-// Side note: To maintain readability, this example was not formatted using dart_fmt.

-

-void main() {

-  final dragTarget = querySelector('#dragTarget');

-  final mouseUp = new Observable<MouseEvent>(document.onMouseUp);

-  final mouseMove = new Observable<MouseEvent>(document.onMouseMove);

-  final mouseDown = new Observable<MouseEvent>(document.onMouseDown);

-

-  mouseDown

-      // Use map() to calculate the left and top properties on mouseDown

-      .map((event) => new Point<num>(event.client.x - dragTarget.offset.left,

-          event.client.y - dragTarget.offset.top))

-      // Use switchMap() to get the mouse position on each mouseMove

-      .switchMap((startPosition) {

-    return mouseMove

-        // Use map() to calculate the left and top properties on each mouseMove

-        .map((event) => new Point<num>(

-            event.client.x - startPosition.x, event.client.y - startPosition.y))

-        // Use takeUntil() to stop calculations when a mouseUp occurs

-        .takeUntil(mouseUp);

-  })

-      // Use listen() to update the position of the dragTarget

-      .listen((position) {

-    dragTarget.style.left = '${position.x}px';

-    dragTarget.style.top = '${position.y}px';

-  });

-}

diff --git a/rxdart/example/web/konami/konamicode.dart b/rxdart/example/web/konami/konamicode.dart
deleted file mode 100755
index ce835d1..0000000
--- a/rxdart/example/web/konami/konamicode.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-import 'dart:html';

-

-import 'package:collection/collection.dart';

-import 'package:rxdart/rxdart.dart';

-

-// Side note: To maintain readability, this example was not formatted using dart_fmt.

-

-void main() {

-  const konamiKeyCodes = const <int>[

-    KeyCode.UP,

-    KeyCode.UP,

-    KeyCode.DOWN,

-    KeyCode.DOWN,

-    KeyCode.LEFT,

-    KeyCode.RIGHT,

-    KeyCode.LEFT,

-    KeyCode.RIGHT,

-    KeyCode.B,

-    KeyCode.A

-  ];

-

-  final result = querySelector('#result');

-  final keyUp = new Observable<KeyboardEvent>(document.onKeyUp);

-

-  keyUp

-      // Use map() to get the keyCode

-      .map((event) => event.keyCode)

-      // Use bufferCount() to remember the last 10 keyCodes

-      .bufferCount(10, 1)

-      // Use where() to check for matching values

-      .where((lastTenKeyCodes) =>

-          const IterableEquality<int>().equals(lastTenKeyCodes, konamiKeyCodes))

-      // Use listen() to display the result

-      .listen((_) => result.innerHtml = 'KONAMI!');

-}

diff --git a/rxdart/example/web/search_github/search_github.dart b/rxdart/example/web/search_github/search_github.dart
deleted file mode 100755
index f5c59cb..0000000
--- a/rxdart/example/web/search_github/search_github.dart
+++ /dev/null
@@ -1,60 +0,0 @@
-import 'dart:async';

-import 'dart:convert';

-import 'dart:html';

-

-import 'package:rxdart/rxdart.dart';

-

-void main() {

-  final searchInput = querySelector('#searchInput');

-  final resultsField = querySelector('#resultsField');

-  final keyUp = new Observable(searchInput.onKeyUp);

-

-  keyUp

-      // return the event target

-      .map((event) => event.target)

-      // cast the event target as InputElement

-      .ofType(const TypeToken<InputElement>())

-      // Use map() to take the value from the input field

-      .map((inputElement) => (inputElement.value))

-      // Use distinct() to ignore all keystrokes that don't have an impact on

-      // the input field's value (brake, ctrl, shift, ..)

-      .distinct()

-      // Ensure the term has some value before calling the API

-      .where((term) => term.isNotEmpty)

-      // Use debounce() to prevent calling the server on fast following

-      // keystrokes

-      .debounce(const Duration(milliseconds: 250))

-      // Use doOnData() to clear resultsField

-      .doOnData((_) => resultsField.innerHtml = '')

-      // Use switchMap to call the gitHub API

-      //

-      // When a new search term follows a previous term quite fast, it's

-      // possible the server is still looking for the previous one. Since

-      // we're only interested in the results of the very last search term

-      // entered, switchMap will cancel the previous request, and notify use

-      // of the last result that comes in. Normal flatMap() would give us all

-      // previous results as well.

-      .switchMap((term) => new Observable.fromFuture(_searchGithubFor(term)))

-      .listen((result) => result.forEach((item) => resultsField.innerHtml +=

-          "<li>${item['fullName']} (${item['url']})</li>"));

-}

-

-Future<List<Map<String, String>>> _searchGithubFor(String term) async {

-  if (term.isEmpty) {

-    throw new ArgumentError('Need to provide a term');

-  }

-

-  final request = await HttpRequest.request(

-    'https://api.github.com/search/repositories?q=$term',

-    requestHeaders: {"Content-Type": "application/json"},

-  );

-  final List<Map<String, dynamic>> items =

-      json.decode(request.responseText)['items'].cast<Map<String, dynamic>>();

-

-  return items.map((item) {

-    return {

-      "fullName": item['full_name'].toString(),

-      "url": item["html_url"].toString()

-    };

-  }).toList();

-}

diff --git a/rxdart/lib/futures.dart b/rxdart/lib/futures.dart
deleted file mode 100755
index 0a681de..0000000
--- a/rxdart/lib/futures.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-library rx_futures;

-

-export 'package:rxdart/src/futures/as_observable_future.dart';

-export 'package:rxdart/src/futures/stream_max_future.dart';

-export 'package:rxdart/src/futures/stream_min_future.dart';

-export 'package:rxdart/src/futures/wrapped_future.dart';

diff --git a/rxdart/lib/rxdart.dart b/rxdart/lib/rxdart.dart
deleted file mode 100755
index a2c602f..0000000
--- a/rxdart/lib/rxdart.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-library rx;

-

-export 'package:rxdart/futures.dart';

-export 'package:rxdart/samplers.dart';

-export 'package:rxdart/src/observables/connectable_observable.dart';

-export 'package:rxdart/src/observables/observable.dart';

-export 'package:rxdart/src/observables/replay_observable.dart';

-export 'package:rxdart/src/observables/value_observable.dart';

-export 'package:rxdart/streams.dart';

-export 'package:rxdart/subjects.dart';

-export 'package:rxdart/transformers.dart';

-export 'package:rxdart/src/utils/composite_subscription.dart';

diff --git a/rxdart/lib/samplers.dart b/rxdart/lib/samplers.dart
deleted file mode 100755
index 4c41c6d..0000000
--- a/rxdart/lib/samplers.dart
+++ /dev/null
@@ -1,4 +0,0 @@
-library rx_samplers;

-

-export 'package:rxdart/src/samplers/buffer_strategy.dart';

-export 'package:rxdart/src/samplers/utils.dart';

diff --git a/rxdart/lib/src/futures/as_observable_future.dart b/rxdart/lib/src/futures/as_observable_future.dart
deleted file mode 100755
index fff2dc6..0000000
--- a/rxdart/lib/src/futures/as_observable_future.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/rxdart.dart';

-import 'package:rxdart/src/futures/wrapped_future.dart';

-

-/// A future that can be converted directly to an Observable using

-/// the `asObservable` method.

-///

-/// This class simply wraps a normal Future, providing one additional method

-/// for more fluent interoperability with the Observable class.

-///

-/// Example:

-///

-///     new Observable.fromIterable(["hello", "friends"])

-///         .join(" ") // Returns an AsObservableFuture

-///         .asObservable() // Fluently convert the Future back to an Observable

-///         .flatMap((message) => new Observable.just(message.length)); // Use the operators you need

-class AsObservableFuture<T> extends WrappedFuture<T> {

-  AsObservableFuture(Future<T> wrapped) : super(wrapped);

-

-  Observable<T> asObservable() {

-    return Observable<T>.fromFuture(wrapped);

-  }

-}

diff --git a/rxdart/lib/src/futures/stream_max_future.dart b/rxdart/lib/src/futures/stream_max_future.dart
deleted file mode 100755
index 217e6c6..0000000
--- a/rxdart/lib/src/futures/stream_max_future.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/futures/wrapped_future.dart';

-

-/// Converts a Stream into a Future that completes with the largest item emitted

-/// by the Stream.

-///

-/// This is similar to finding the max value in a list, but the values are

-/// asynchronous.

-///

-/// ### Example

-///

-///     int max = await new StreamMaxFuture(new Stream.fromIterable([1, 2, 3]));

-///

-///     print(max); // prints 3

-///

-/// ### Example with custom [Comparator]

-///

-///     Stream<String> stream = new Stream.fromIterable("short", "loooooooong");

-///     Comparator<String> stringLengthComparator = (a, b) => a.length - b.length;

-///     String max = await new StreamMaxFuture(stream, stringLengthComparator);

-///

-///     print(max); // prints "loooooooong"

-class StreamMaxFuture<T> extends WrappedFuture<T> {

-  StreamMaxFuture(Stream<T> stream, [Comparator<T> comparator])

-      : super(stream

-            .toList()

-            .then((List<T> values) => (values..sort(comparator)).last));

-}

diff --git a/rxdart/lib/src/futures/stream_min_future.dart b/rxdart/lib/src/futures/stream_min_future.dart
deleted file mode 100755
index d6ebde8..0000000
--- a/rxdart/lib/src/futures/stream_min_future.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/futures/wrapped_future.dart';

-

-/// Converts a Stream into a Future that completes with the smallest item

-/// emitted by the Stream.

-///

-/// This is similar to finding the min value in a list, but the values are

-/// asynchronous.

-///

-/// ### Example

-///

-///     int min = await new StreamMinFuture(new Stream.fromIterable([1, 2, 3]));

-///

-///     print(min); // prints 1

-///

-/// ### Example with custom [Comparator]

-///

-///     Stream<String> stream = new Stream.fromIterable("short", "loooooooong");

-///     Comparator<String> stringLengthComparator = (a, b) => a.length - b.length;

-///     String min = await new StreamMinFuture(stream, stringLengthComparator);

-///

-///     print(min); // prints "short"

-class StreamMinFuture<T> extends WrappedFuture<T> {

-  StreamMinFuture(Stream<T> stream, [Comparator<T> comparator])

-      : super(stream

-            .toList()

-            .then((List<T> values) => (values..sort(comparator)).first));

-}

diff --git a/rxdart/lib/src/futures/wrapped_future.dart b/rxdart/lib/src/futures/wrapped_future.dart
deleted file mode 100755
index a31c18a..0000000
--- a/rxdart/lib/src/futures/wrapped_future.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'dart:async';

-

-/// A future that simply wraps another Future.

-///

-/// This Future provides no additional functionality to the passed in Future

-/// by default. This is meant as a base implementation that allows you to extend

-/// Futures you can't directly create.

-///

-/// For example, the AsObservableFuture adds one method to the Futures returned

-/// by some Stream methods.

-class WrappedFuture<T> implements Future<T> {

-  final Future<T> wrapped;

-

-  WrappedFuture(this.wrapped);

-

-  @override

-  Stream<T> asStream() => wrapped.asStream();

-

-  @override

-  Future<T> catchError(Function onError, {bool test(Object error)}) =>

-      wrapped.catchError(onError, test: test);

-

-  @override

-  Future<S> then<S>(FutureOr<S> onValue(T value), {Function onError}) =>

-      wrapped.then(onValue, onError: onError);

-

-  @override

-  Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()}) =>

-      wrapped.timeout(timeLimit, onTimeout: onTimeout);

-

-  @override

-  Future<T> whenComplete(void action()) => wrapped.whenComplete(action);

-}

diff --git a/rxdart/lib/src/observables/connectable_observable.dart b/rxdart/lib/src/observables/connectable_observable.dart
deleted file mode 100755
index d69175e..0000000
--- a/rxdart/lib/src/observables/connectable_observable.dart
+++ /dev/null
@@ -1,259 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/rxdart.dart';

-import 'package:rxdart/src/observables/replay_observable.dart';

-

-/// A ConnectableObservable resembles an ordinary Observable, except that it

-/// can be listened to multiple times and does not begin emitting items when

-/// it is listened to, but only when its [connect] method is called.

-///

-/// This class can be used to broadcast a single-subscription Stream, and

-/// can be used to wait for all intended Observers to [listen] to the

-/// Observable before it begins emitting items.

-abstract class ConnectableObservable<T> extends Observable<T> {

-  ConnectableObservable(Stream<T> stream) : super(stream);

-

-  /// Returns an Observable that automatically connects (at most once) to this

-  /// ConnectableObservable when the first Observer subscribes.

-  ///

-  /// To disconnect from the source Stream, provide a [connection] callback and

-  /// cancel the `subscription` at the appropriate time.

-  Observable<T> autoConnect({

-    void Function(StreamSubscription<T> subscription) connection,

-  });

-

-  /// Instructs the [ConnectableObservable] to begin emitting items from the

-  /// source Stream. To disconnect from the source stream, cancel the

-  /// subscription.

-  StreamSubscription<T> connect();

-

-  /// Returns an Observable that stays connected to this ConnectableObservable

-  /// as long as there is at least one subscription to this

-  /// ConnectableObservable.

-  Observable<T> refCount();

-}

-

-/// A [ConnectableObservable] that converts a single-subscription Stream into

-/// a broadcast Stream.

-class PublishConnectableObservable<T> extends ConnectableObservable<T> {

-  final Stream<T> _source;

-  final PublishSubject<T> _subject;

-

-  factory PublishConnectableObservable(Stream<T> source) {

-    return PublishConnectableObservable<T>._(source, PublishSubject<T>());

-  }

-

-  PublishConnectableObservable._(this._source, this._subject) : super(_subject);

-

-  @override

-  Observable<T> autoConnect({

-    void Function(StreamSubscription<T> subscription) connection,

-  }) {

-    _subject.onListen = () {

-      if (connection != null) {

-        connection(connect());

-      } else {

-        connect();

-      }

-    };

-

-    return _subject;

-  }

-

-  @override

-  StreamSubscription<T> connect() {

-    return ConnectableObservableStreamSubscription<T>(

-      _source.listen(_subject.add, onError: _subject.addError),

-      _subject,

-    );

-  }

-

-  @override

-  Observable<T> refCount() {

-    ConnectableObservableStreamSubscription<T> subscription;

-

-    _subject.onListen = () {

-      subscription = ConnectableObservableStreamSubscription<T>(

-        _source.listen(_subject.add, onError: _subject.addError),

-        _subject,

-      );

-    };

-

-    _subject.onCancel = () {

-      subscription.cancel();

-    };

-

-    return _subject;

-  }

-}

-

-/// A [ConnectableObservable] that converts a single-subscription Stream into

-/// a broadcast Stream that replays the latest value to any new listener, and

-/// provides synchronous access to the latest emitted value.

-class ValueConnectableObservable<T> extends ConnectableObservable<T>

-    implements ValueObservable<T> {

-  final Stream<T> _source;

-  final BehaviorSubject<T> _subject;

-

-  ValueConnectableObservable._(this._source, this._subject) : super(_subject);

-

-  factory ValueConnectableObservable(Stream<T> source) =>

-      ValueConnectableObservable<T>._(

-        source,

-        BehaviorSubject<T>(),

-      );

-

-  factory ValueConnectableObservable.seeded(

-    Stream<T> source,

-    T seedValue,

-  ) =>

-      ValueConnectableObservable<T>._(

-        source,

-        BehaviorSubject<T>.seeded(seedValue),

-      );

-

-  @override

-  ValueObservable<T> autoConnect({

-    void Function(StreamSubscription<T> subscription) connection,

-  }) {

-    _subject.onListen = () {

-      if (connection != null) {

-        connection(connect());

-      } else {

-        connect();

-      }

-    };

-

-    return _subject;

-  }

-

-  @override

-  StreamSubscription<T> connect() {

-    return ConnectableObservableStreamSubscription<T>(

-      _source.listen(_subject.add, onError: _subject.addError),

-      _subject,

-    );

-  }

-

-  @override

-  ValueObservable<T> refCount() {

-    ConnectableObservableStreamSubscription<T> subscription;

-

-    _subject.onListen = () {

-      subscription = ConnectableObservableStreamSubscription<T>(

-        _source.listen(_subject.add, onError: _subject.addError),

-        _subject,

-      );

-    };

-

-    _subject.onCancel = () {

-      subscription.cancel();

-    };

-

-    return _subject;

-  }

-

-  @override

-  T get value => _subject.value;

-

-  @override

-  bool get hasValue => _subject.hasValue;

-}

-

-/// A [ConnectableObservable] that converts a single-subscription Stream into

-/// a broadcast Stream that replays emitted items to any new listener, and

-/// provides synchronous access to the list of emitted values.

-class ReplayConnectableObservable<T> extends ConnectableObservable<T>

-    implements ReplayObservable<T> {

-  final Stream<T> _source;

-  final ReplaySubject<T> _subject;

-

-  factory ReplayConnectableObservable(Stream<T> stream, {int maxSize}) {

-    return ReplayConnectableObservable<T>._(

-      stream,

-      ReplaySubject<T>(maxSize: maxSize),

-    );

-  }

-

-  ReplayConnectableObservable._(this._source, this._subject) : super(_subject);

-

-  @override

-  ReplayObservable<T> autoConnect({

-    void Function(StreamSubscription<T> subscription) connection,

-  }) {

-    _subject.onListen = () {

-      if (connection != null) {

-        connection(connect());

-      } else {

-        connect();

-      }

-    };

-

-    return _subject;

-  }

-

-  @override

-  StreamSubscription<T> connect() {

-    return ConnectableObservableStreamSubscription<T>(

-      _source.listen(_subject.add, onError: _subject.addError),

-      _subject,

-    );

-  }

-

-  @override

-  ReplayObservable<T> refCount() {

-    ConnectableObservableStreamSubscription<T> subscription;

-

-    _subject.onListen = () {

-      subscription = ConnectableObservableStreamSubscription<T>(

-        _source.listen(_subject.add, onError: _subject.addError),

-        _subject,

-      );

-    };

-

-    _subject.onCancel = () {

-      subscription.cancel();

-    };

-

-    return _subject;

-  }

-

-  @override

-  List<T> get values => _subject.values;

-}

-

-/// A special [StreamSubscription] that not only cancels the connection to

-/// the source [Stream], but also closes down a subject that drives the Stream.

-class ConnectableObservableStreamSubscription<T> extends StreamSubscription<T> {

-  final StreamSubscription<T> _source;

-  final Subject<T> _subject;

-

-  ConnectableObservableStreamSubscription(this._source, this._subject);

-

-  @override

-  Future<dynamic> cancel() {

-    _subject.close();

-    return _source.cancel();

-  }

-

-  @override

-  Future<E> asFuture<E>([E futureValue]) => _source.asFuture(futureValue);

-

-  @override

-  bool get isPaused => _source.isPaused;

-

-  @override

-  void onData(void Function(T data) handleData) => _source.onData(handleData);

-

-  @override

-  void onDone(void Function() handleDone) => _source.onDone(handleDone);

-

-  @override

-  void onError(Function handleError) => _source.onError(handleError);

-

-  @override

-  void pause([Future<dynamic> resumeSignal]) => _source.pause(resumeSignal);

-

-  @override

-  void resume() => _source.resume();

-}

diff --git a/rxdart/lib/src/observables/observable.dart b/rxdart/lib/src/observables/observable.dart
deleted file mode 100755
index 9e0ea77..0000000
--- a/rxdart/lib/src/observables/observable.dart
+++ /dev/null
@@ -1,2621 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/futures.dart';

-import 'package:rxdart/samplers.dart';

-import 'package:rxdart/src/observables/connectable_observable.dart';

-import 'package:rxdart/src/observables/replay_observable.dart';

-import 'package:rxdart/src/observables/value_observable.dart';

-import 'package:rxdart/streams.dart';

-import 'package:rxdart/transformers.dart';

-

-/// A wrapper class that extends Stream. It combines all the Streams and

-/// StreamTransformers contained in this library into a fluent api.

-///

-/// ### Example

-///

-///     new Observable(new Stream.fromIterable([1]))

-///       .interval(new Duration(seconds: 1))

-///       .flatMap((i) => new Observable.just(2))

-///       .take(1)

-///       .listen(print); // prints 2

-///

-/// ### Learning RxDart

-///

-/// This library contains documentation and examples for each method. In

-/// addition, more complex examples can be found in the

-/// [RxDart github repo](https://github.com/ReactiveX/rxdart) demonstrating how

-/// to use RxDart with web, command line, and Flutter applications.

-///

-/// #### Additional Resources

-///

-/// In addition to the RxDart documentation and examples, you can find many

-/// more articles on Dart Streams that teach the fundamentals upon which

-/// RxDart is built.

-///

-///   - [Asynchronous Programming: Streams](https://www.dartlang.org/tutorials/language/streams)

-///   - [Single-Subscription vs. Broadcast Streams](https://www.dartlang.org/articles/libraries/broadcast-streams)

-///   - [Creating Streams in Dart](https://www.dartlang.org/articles/libraries/creating-streams)

-///   - [Testing Streams: Stream Matchers](https://pub.dartlang.org/packages/test#stream-matchers)

-///

-/// ### Dart Streams vs Observables

-///

-/// In order to integrate fluently with the Dart ecosystem, the Observable class

-/// extends the Dart `Stream` class. This provides several advantages:

-///

-///    - Observables work with any API that expects a Dart Stream as an input.

-///    - Inherit the many methods and properties from the core Stream API.

-///    - Ability to create Streams with language-level syntax.

-///

-/// Overall, we attempt to follow the Observable spec as closely as we can, but

-/// prioritize fitting in with the Dart ecosystem when a trade-off must be made.

-/// Therefore, there are some important differences to note between Dart's

-/// `Stream` class and standard Rx `Observable`.

-///

-/// First, Cold Observables in Dart are single-subscription. In other words,

-/// you can only listen to Observables once, unless it is a hot (aka broadcast)

-/// Stream. If you attempt to listen to a cold stream twice, a StateError will

-/// be thrown. If you need to listen to a stream multiple times, you can simply

-/// create a factory function that returns a new instance of the stream.

-///

-/// Second, many methods contained within, such as `first` and `last` do not

-/// return a `Single` nor an `Observable`, but rather must return a Dart Future.

-/// Luckily, Dart Futures are easy to work with, and easily convert back to a

-/// Stream using the `myFuture.asStream()` method if needed.

-///

-/// Third, Streams in Dart do not close by default when an error occurs. In Rx,

-/// an Error causes the Observable to terminate unless it is intercepted by

-/// an operator. Dart has mechanisms for creating streams that close when an

-/// error occurs, but the majority of Streams do not exhibit this behavior.

-///

-/// Fourth, Dart streams are asynchronous by default, whereas Observables are

-/// synchronous by default, unless you schedule work on a different Scheduler.

-/// You can create synchronous Streams with Dart, but please be aware the the

-/// default is simply different.

-///

-/// Finally, when using Dart Broadcast Streams (similar to Hot Observables),

-/// please know that `onListen` will only be called the first time the

-/// broadcast stream is listened to.

-class Observable<T> extends Stream<T> {

-  final Stream<T> _stream;

-

-  Observable(Stream<T> stream) : this._stream = stream;

-

-  @override

-  AsObservableFuture<bool> any(bool test(T element)) =>

-      AsObservableFuture<bool>(_stream.any(test));

-

-  /// Merges the given Streams into one Observable that emits a List of the

-  /// values emitted by the source Stream. This is helpful when you need to

-  /// combine a dynamic number of Streams.

-  ///

-  /// The Observable will not emit any lists of values until all of the source

-  /// streams have emitted at least one value.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatestList([

-  ///       Observable.just(1),

-  ///       Observable.fromIterable([0, 1, 2]),

-  ///     ])

-  ///     .listen(print); // prints [1, 0], [1, 1], [1, 2]

-  static Observable<R> combineLatest<T, R>(

-          Iterable<Stream<T>> streams, R combiner(List<T> values)) =>

-      Observable<R>(CombineLatestStream<T, R>(streams, combiner));

-

-  /// Merges the given Streams into one Observable that emits a List of the

-  /// values emitted by the source Stream. This is helpful when you need to

-  /// combine a dynamic number of Streams.

-  ///

-  /// The Observable will not emit any lists of values until all of the source

-  /// streams have emitted at least one value.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatestList([

-  ///       Observable.just(1),

-  ///       Observable.fromIterable([0, 1, 2]),

-  ///     ])

-  ///     .listen(print); // prints [1, 0], [1, 1], [1, 2]

-  static Observable<List<T>> combineLatestList<T>(

-          Iterable<Stream<T>> streams) =>

-      Observable<List<T>>(CombineLatestStream.list<T>(streams));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest2(

-  ///       new Observable.just(1),

-  ///       new Observable.fromIterable([0, 1, 2]),

-  ///       (a, b) => a + b)

-  ///     .listen(print); //prints 1, 2, 3

-  static Observable<T> combineLatest2<A, B, T>(

-          Stream<A> streamA, Stream<B> streamB, T combiner(A a, B b)) =>

-      Observable<T>(CombineLatestStream.combine2(streamA, streamB, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest3(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.fromIterable(["c", "c"]),

-  ///       (a, b, c) => a + b + c)

-  ///     .listen(print); //prints "abc", "abc"

-  static Observable<T> combineLatest3<A, B, C, T>(Stream<A> streamA,

-          Stream<B> streamB, Stream<C> streamC, T combiner(A a, B b, C c)) =>

-      Observable<T>(

-          CombineLatestStream.combine3(streamA, streamB, streamC, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest4(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.fromIterable(["d", "d"]),

-  ///       (a, b, c, d) => a + b + c + d)

-  ///     .listen(print); //prints "abcd", "abcd"

-  static Observable<T> combineLatest4<A, B, C, D, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          T combiner(A a, B b, C c, D d)) =>

-      Observable<T>(CombineLatestStream.combine4(

-          streamA, streamB, streamC, streamD, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest5(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.fromIterable(["e", "e"]),

-  ///       (a, b, c, d, e) => a + b + c + d + e)

-  ///     .listen(print); //prints "abcde", "abcde"

-  static Observable<T> combineLatest5<A, B, C, D, E, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          T combiner(A a, B b, C c, D d, E e)) =>

-      Observable<T>(CombineLatestStream.combine5(

-          streamA, streamB, streamC, streamD, streamE, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest6(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.fromIterable(["f", "f"]),

-  ///       (a, b, c, d, e, f) => a + b + c + d + e + f)

-  ///     .listen(print); //prints "abcdef", "abcdef"

-  static Observable<T> combineLatest6<A, B, C, D, E, F, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          T combiner(A a, B b, C c, D d, E e, F f)) =>

-      Observable<T>(CombineLatestStream.combine6(

-          streamA, streamB, streamC, streamD, streamE, streamF, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest7(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.fromIterable(["g", "g"]),

-  ///       (a, b, c, d, e, f, g) => a + b + c + d + e + f + g)

-  ///     .listen(print); //prints "abcdefg", "abcdefg"

-  static Observable<T> combineLatest7<A, B, C, D, E, F, G, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          T combiner(A a, B b, C c, D d, E e, F f, G g)) =>

-      Observable<T>(CombineLatestStream.combine7(streamA, streamB, streamC,

-          streamD, streamE, streamF, streamG, combiner));

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest8(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.just("g"),

-  ///       new Observable.fromIterable(["h", "h"]),

-  ///       (a, b, c, d, e, f, g, h) => a + b + c + d + e + f + g + h)

-  ///     .listen(print); //prints "abcdefgh", "abcdefgh"

-  static Observable<T> combineLatest8<A, B, C, D, E, F, G, H, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          Stream<H> streamH,

-          T combiner(A a, B b, C c, D d, E e, F f, G g, H h)) =>

-      Observable<T>(

-        CombineLatestStream.combine8(

-          streamA,

-          streamB,

-          streamC,

-          streamD,

-          streamE,

-          streamF,

-          streamG,

-          streamH,

-          combiner,

-        ),

-      );

-

-  /// Merges the given Streams into one Observable sequence by using the

-  /// [combiner] function whenever any of the observable sequences emits an

-  /// item.

-  ///

-  /// The Observable will not emit until all streams have emitted at least one

-  /// item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.combineLatest9(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.just("g"),

-  ///       new Observable.just("h"),

-  ///       new Observable.fromIterable(["i", "i"]),

-  ///       (a, b, c, d, e, f, g, h, i) => a + b + c + d + e + f + g + h + i)

-  ///     .listen(print); //prints "abcdefghi", "abcdefghi"

-  static Observable<T> combineLatest9<A, B, C, D, E, F, G, H, I, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          Stream<H> streamH,

-          Stream<I> streamI,

-          T combiner(A a, B b, C c, D d, E e, F f, G g, H h, I i)) =>

-      Observable<T>(

-        CombineLatestStream.combine9(

-          streamA,

-          streamB,

-          streamC,

-          streamD,

-          streamE,

-          streamF,

-          streamG,

-          streamH,

-          streamI,

-          combiner,

-        ),

-      );

-

-  /// Concatenates all of the specified stream sequences, as long as the

-  /// previous stream sequence terminated successfully.

-  ///

-  /// It does this by subscribing to each stream one by one, emitting all items

-  /// and completing before subscribing to the next stream.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#concat)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.concat([

-  ///       new Observable.just(1),

-  ///       new Observable.timer(2, new Duration(days: 1)),

-  ///       new Observable.just(3)

-  ///     ])

-  ///     .listen(print); // prints 1, 2, 3

-  factory Observable.concat(Iterable<Stream<T>> streams) =>

-      Observable<T>(ConcatStream<T>(streams));

-

-  /// Concatenates all of the specified stream sequences, as long as the

-  /// previous stream sequence terminated successfully.

-  ///

-  /// In the case of concatEager, rather than subscribing to one stream after

-  /// the next, all streams are immediately subscribed to. The events are then

-  /// captured and emitted at the correct time, after the previous stream has

-  /// finished emitting items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#concat)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.concatEager([

-  ///       new Observable.just(1),

-  ///       new Observable.timer(2, new Duration(days: 1)),

-  ///       new Observable.just(3)

-  ///     ])

-  ///     .listen(print); // prints 1, 2, 3

-  factory Observable.concatEager(Iterable<Stream<T>> streams) =>

-      Observable<T>(ConcatEagerStream<T>(streams));

-

-  /// The defer factory waits until an observer subscribes to it, and then it

-  /// creates an Observable with the given factory function.

-  ///

-  /// In some circumstances, waiting until the last minute (that is, until

-  /// subscription time) to generate the Observable can ensure that this

-  /// Observable contains the freshest data.

-  ///

-  /// By default, DeferStreams are single-subscription. However, it's possible

-  /// to make them reusable.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.defer(() => new Observable.just(1))

-  ///       .listen(print); //prints 1

-  factory Observable.defer(Stream<T> streamFactory(),

-          {bool reusable = false}) =>

-      Observable<T>(DeferStream<T>(streamFactory, reusable: reusable));

-

-  /// Returns an observable sequence that emits an [error], then immediately

-  /// completes.

-  ///

-  /// The error operator is one with very specific and limited behavior. It is

-  /// mostly useful for testing purposes.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.error(new ArgumentError());

-  factory Observable.error(Object error) =>

-      Observable<T>(ErrorStream<T>(error));

-

-  ///  Creates an Observable where all events of an existing stream are piped

-  ///  through a sink-transformation.

-  ///

-  ///  The given [mapSink] closure is invoked when the returned stream is

-  ///  listened to. All events from the [source] are added into the event sink

-  ///  that is returned from the invocation. The transformation puts all

-  ///  transformed events into the sink the [mapSink] closure received during

-  ///  its invocation. Conceptually the [mapSink] creates a transformation pipe

-  ///  with the input sink being the returned [EventSink] and the output sink

-  ///  being the sink it received.

-  factory Observable.eventTransformed(

-          Stream<T> source, EventSink<T> mapSink(EventSink<T> sink)) =>

-      Observable<T>((Stream<T>.eventTransformed(source, mapSink)));

-

-  /// Creates an Observable from the future.

-  ///

-  /// When the future completes, the stream will fire one event, either

-  /// data or error, and then close with a done-event.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromFuture(new Future.value("Hello"))

-  ///       .listen(print); // prints "Hello"

-  factory Observable.fromFuture(Future<T> future) =>

-      Observable<T>((Stream<T>.fromFuture(future)));

-

-  /// Creates an Observable that gets its data from [data].

-  ///

-  /// The iterable is iterated when the stream receives a listener, and stops

-  /// iterating if the listener cancels the subscription.

-  ///

-  /// If iterating [data] throws an error, the stream ends immediately with

-  /// that error. No done event will be sent (iteration is not complete), but no

-  /// further data events will be generated either, since iteration cannot

-  /// continue.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2]).listen(print); // prints 1, 2

-  factory Observable.fromIterable(Iterable<T> data) =>

-      Observable<T>((Stream<T>.fromIterable(data)));

-

-  /// Creates an Observable that contains a single value

-  ///

-  /// The value is emitted when the stream receives a listener.

-  ///

-  /// ### Example

-  ///

-  ///      new Observable.just(1).listen(print); // prints 1

-  factory Observable.just(T data) =>

-      Observable<T>((Stream<T>.fromIterable(<T>[data])));

-

-  /// Creates an Observable that contains no values.

-  ///

-  /// No items are emitted from the stream, and done is called upon listening.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.empty().listen(

-  ///       (_) => print("data"), onDone: () => print("done")); // prints "done"

-  factory Observable.empty() => Observable<T>((Stream<T>.empty()));

-

-  /// Flattens the items emitted by the given [streams] into a single Observable

-  /// sequence.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#merge)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.merge([

-  ///       new Observable.timer(1, new Duration(days: 10)),

-  ///       new Observable.just(2)

-  ///     ])

-  ///     .listen(print); // prints 2, 1

-  factory Observable.merge(Iterable<Stream<T>> streams) =>

-      Observable<T>(MergeStream<T>(streams));

-

-  /// Returns a non-terminating observable sequence, which can be used to denote

-  /// an infinite duration.

-  ///

-  /// The never operator is one with very specific and limited behavior. These

-  /// are useful for testing purposes, and sometimes also for combining with

-  /// other Observables or as parameters to operators that expect other

-  /// Observables as parameters.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.never().listen(print); // Neither prints nor terminates

-  factory Observable.never() => Observable<T>(NeverStream<T>());

-

-  /// Creates an Observable that repeatedly emits events at [period] intervals.

-  ///

-  /// The event values are computed by invoking [computation]. The argument to

-  /// this callback is an integer that starts with 0 and is incremented for

-  /// every event.

-  ///

-  /// If [computation] is omitted the event values will all be `null`.

-  ///

-  /// ### Example

-  ///

-  ///      new Observable.periodic(new Duration(seconds: 1), (i) => i).take(3)

-  ///        .listen(print); // prints 0, 1, 2

-  factory Observable.periodic(Duration period,

-          [T computation(int computationCount)]) =>

-      Observable<T>((Stream<T>.periodic(period, computation)));

-

-  /// Given two or more source [streams], emit all of the items from only

-  /// the first of these [streams] to emit an item or notification.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#amb)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.race([

-  ///       new Observable.timer(1, new Duration(days: 1)),

-  ///       new Observable.timer(2, new Duration(days: 2)),

-  ///       new Observable.timer(3, new Duration(seconds: 1))

-  ///     ]).listen(print); // prints 3

-  factory Observable.race(Iterable<Stream<T>> streams) =>

-      Observable<T>(RaceStream<T>(streams));

-

-  /// Returns an Observable that emits a sequence of Integers within a specified

-  /// range.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(1, 3).listen((i) => print(i)); // Prints 1, 2, 3

-  ///

-  ///     Observable.range(3, 1).listen((i) => print(i)); // Prints 3, 2, 1

-  static Observable<int> range(int startInclusive, int endInclusive) =>

-      Observable<int>(RangeStream(startInclusive, endInclusive));

-

-  /// Creates a [Stream] that will recreate and re-listen to the source

-  /// Stream the specified number of times until the [Stream] terminates

-  /// successfully.

-  ///

-  /// If [count] is not specified, it repeats indefinitely.

-  ///

-  /// ### Example

-  ///

-  ///     new RepeatStream((int repeatCount) =>

-  ///       Observable.just('repeat index: $repeatCount'), 3)

-  ///         .listen((i) => print(i)); // Prints 'repeat index: 0, repeat index: 1, repeat index: 2'

-  factory Observable.repeat(Stream<T> streamFactory(int repeatIndex),

-          [int count]) =>

-      Observable(RepeatStream<T>(streamFactory, count));

-

-  /// Creates an Observable that will recreate and re-listen to the source

-  /// Stream the specified number of times until the Stream terminates

-  /// successfully.

-  ///

-  /// If the retry count is not specified, it retries indefinitely. If the retry

-  /// count is met, but the Stream has not terminated successfully, a

-  /// [RetryError] will be thrown. The RetryError will contain all of the Errors

-  /// and StackTraces that caused the failure.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.retry(() { new Observable.just(1); })

-  ///         .listen((i) => print(i)); // Prints 1

-  ///

-  ///     new Observable

-  ///        .retry(() {

-  ///          new Observable.just(1).concatWith([new Observable.error(new Error())]);

-  ///        }, 1)

-  ///        .listen(print, onError: (e, s) => print(e)); // Prints 1, 1, RetryError

-  factory Observable.retry(Stream<T> streamFactory(), [int count]) {

-    return Observable<T>(RetryStream<T>(streamFactory, count));

-  }

-

-  /// Creates a Stream that will recreate and re-listen to the source

-  /// Stream when the notifier emits a new value. If the source Stream

-  /// emits an error or it completes, the Stream terminates.

-  ///

-  /// If the [retryWhenFactory] emits an error a [RetryError] will be

-  /// thrown. The RetryError will contain all of the [Error]s and

-  /// [StackTrace]s that caused the failure.

-  ///

-  /// ### Basic Example

-  /// ```dart

-  /// new RetryWhenStream<int>(

-  ///   () => new Stream<int>.fromIterable(<int>[1]),

-  ///   (dynamic error, StackTrace s) => throw error,

-  /// ).listen(print); // Prints 1

-  /// ```

-  ///

-  /// ### Periodic Example

-  /// ```dart

-  /// new RetryWhenStream<int>(

-  ///   () => new Observable<int>

-  ///       .periodic(const Duration(seconds: 1), (int i) => i)

-  ///       .map((int i) => i == 2 ? throw 'exception' : i),

-  ///   (dynamic e, StackTrace s) {

-  ///     return new Observable<String>

-  ///         .timer('random value', const Duration(milliseconds: 200));

-  ///   },

-  /// ).take(4).listen(print); // Prints 0, 1, 0, 1

-  /// ```

-  ///

-  /// ### Complex Example

-  /// ```dart

-  /// bool errorHappened = false;

-  /// new RetryWhenStream(

-  ///   () => new Observable

-  ///       .periodic(const Duration(seconds: 1), (i) => i)

-  ///       .map((i) {

-  ///         if (i == 3 && !errorHappened) {

-  ///           throw 'We can take this. Please restart.';

-  ///         } else if (i == 4) {

-  ///           throw 'It\'s enough.';

-  ///         } else {

-  ///           return i;

-  ///         }

-  ///       }),

-  ///   (e, s) {

-  ///     errorHappened = true;

-  ///     if (e == 'We can take this. Please restart.') {

-  ///       return new Observable.just('Ok. Here you go!');

-  ///     } else {

-  ///       return new Observable.error(e);

-  ///     }

-  ///   },

-  /// ).listen(

-  ///   print,

-  ///   onError: (e, s) => print(e),

-  /// ); // Prints 0, 1, 2, 0, 1, 2, 3, RetryError

-  /// ```

-  factory Observable.retryWhen(Stream<T> streamFactory(),

-      Stream<void> retryWhenFactory(dynamic error, StackTrace stack)) {

-    return Observable<T>(RetryWhenStream<T>(streamFactory, retryWhenFactory));

-  }

-

-  /// Convert a Stream that emits Streams (aka a "Higher Order Stream") into a

-  /// single Observable that emits the items emitted by the

-  /// most-recently-emitted of those Streams.

-  ///

-  /// This Observable will unsubscribe from the previously-emitted Stream when

-  /// a new Stream is emitted from the source Stream and subscribe to the new

-  /// Stream.

-  ///

-  /// ### Example

-  ///

-  /// ```dart

-  /// final switchLatestStream = new SwitchLatestStream<String>(

-  ///   new Stream.fromIterable(<Stream<String>>[

-  ///     new Observable.timer('A', new Duration(seconds: 2)),

-  ///     new Observable.timer('B', new Duration(seconds: 1)),

-  ///     new Observable.just('C'),

-  ///   ]),

-  /// );

-  ///

-  /// // Since the first two Streams do not emit data for 1-2 seconds, and the

-  /// // 3rd Stream will be emitted before that time, only data from the 3rd

-  /// // Stream will be emitted to the listener.

-  /// switchLatestStream.listen(print); // prints 'C'

-  /// ```

-  factory Observable.switchLatest(Stream<Stream<T>> streams) =>

-      Observable<T>(SwitchLatestStream<T>(streams));

-

-  /// Emits the given value after a specified amount of time.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.timer("hi", new Duration(minutes: 1))

-  ///         .listen((i) => print(i)); // print "hi" after 1 minute

-  factory Observable.timer(T value, Duration duration) =>

-      Observable<T>((TimerStream<T>(value, duration)));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip2(

-  ///       new Observable.just("Hi "),

-  ///       new Observable.fromIterable(["Friend", "Dropped"]),

-  ///       (a, b) => a + b)

-  ///     .listen(print); // prints "Hi Friend"

-  static Observable<T> zip2<A, B, T>(

-          Stream<A> streamA, Stream<B> streamB, T zipper(A a, B b)) =>

-      Observable<T>(ZipStream.zip2(streamA, streamB, zipper));

-

-  /// Merges the iterable streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip(

-  ///       [

-  ///         Observable.just("Hi "),

-  ///         Observable.fromIterable(["Friend", "Dropped"]),

-  ///       ],

-  ///       (values) => values.first + values.last

-  ///     )

-  ///     .listen(print); // prints "Hi Friend"

-  static Observable<R> zip<T, R>(

-          Iterable<Stream<T>> streams, R zipper(List<T> values)) =>

-      Observable<R>(ZipStream(streams, zipper));

-

-  /// Merges the iterable streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zipList(

-  ///       [

-  ///         Observable.just("Hi "),

-  ///         Observable.fromIterable(["Friend", "Dropped"]),

-  ///       ],

-  ///     )

-  ///     .listen(print); // prints ['Hi ', 'Friend']

-  static Observable<List<T>> zipList<T>(Iterable<Stream<T>> streams) =>

-      Observable<List<T>>(ZipStream.list(streams));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip3(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.fromIterable(["c", "dropped"]),

-  ///       (a, b, c) => a + b + c)

-  ///     .listen(print); //prints "abc"

-  static Observable<T> zip3<A, B, C, T>(Stream<A> streamA, Stream<B> streamB,

-          Stream<C> streamC, T zipper(A a, B b, C c)) =>

-      Observable<T>(ZipStream.zip3(streamA, streamB, streamC, zipper));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip4(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.fromIterable(["d", "dropped"]),

-  ///       (a, b, c, d) => a + b + c + d)

-  ///     .listen(print); //prints "abcd"

-  static Observable<T> zip4<A, B, C, D, T>(Stream<A> streamA, Stream<B> streamB,

-          Stream<C> streamC, Stream<D> streamD, T zipper(A a, B b, C c, D d)) =>

-      Observable<T>(ZipStream.zip4(streamA, streamB, streamC, streamD, zipper));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip5(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.fromIterable(["e", "dropped"]),

-  ///       (a, b, c, d, e) => a + b + c + d + e)

-  ///     .listen(print); //prints "abcde"

-  static Observable<T> zip5<A, B, C, D, E, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          T zipper(A a, B b, C c, D d, E e)) =>

-      Observable<T>(

-          ZipStream.zip5(streamA, streamB, streamC, streamD, streamE, zipper));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip6(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.fromIterable(["f", "dropped"]),

-  ///       (a, b, c, d, e, f) => a + b + c + d + e + f)

-  ///     .listen(print); //prints "abcdef"

-  static Observable<T> zip6<A, B, C, D, E, F, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          T zipper(A a, B b, C c, D d, E e, F f)) =>

-      Observable<T>(ZipStream.zip6(

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        zipper,

-      ));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip7(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.fromIterable(["g", "dropped"]),

-  ///       (a, b, c, d, e, f, g) => a + b + c + d + e + f + g)

-  ///     .listen(print); //prints "abcdefg"

-  static Observable<T> zip7<A, B, C, D, E, F, G, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          T zipper(A a, B b, C c, D d, E e, F f, G g)) =>

-      Observable<T>(ZipStream.zip7(

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        streamG,

-        zipper,

-      ));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip8(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.just("g"),

-  ///       new Observable.fromIterable(["h", "dropped"]),

-  ///       (a, b, c, d, e, f, g, h) => a + b + c + d + e + f + g + h)

-  ///     .listen(print); //prints "abcdefgh"

-  static Observable<T> zip8<A, B, C, D, E, F, G, H, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          Stream<H> streamH,

-          T zipper(A a, B b, C c, D d, E e, F f, G g, H h)) =>

-      Observable<T>(ZipStream.zip8(

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        streamG,

-        streamH,

-        zipper,

-      ));

-

-  /// Merges the specified streams into one observable sequence using the given

-  /// zipper function whenever all of the observable sequences have produced

-  /// an element at a corresponding index.

-  ///

-  /// It applies this function in strict sequence, so the first item emitted by

-  /// the new Observable will be the result of the function applied to the first

-  /// item emitted by Observable #1 and the first item emitted by Observable #2;

-  /// the second item emitted by the new zip-Observable will be the result of

-  /// the function applied to the second item emitted by Observable #1 and the

-  /// second item emitted by Observable #2; and so forth. It will only emit as

-  /// many items as the number of items emitted by the source Observable that

-  /// emits the fewest items.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#zip)

-  ///

-  /// ### Example

-  ///

-  ///     Observable.zip9(

-  ///       new Observable.just("a"),

-  ///       new Observable.just("b"),

-  ///       new Observable.just("c"),

-  ///       new Observable.just("d"),

-  ///       new Observable.just("e"),

-  ///       new Observable.just("f"),

-  ///       new Observable.just("g"),

-  ///       new Observable.just("h"),

-  ///       new Observable.fromIterable(["i", "dropped"]),

-  ///       (a, b, c, d, e, f, g, h, i) => a + b + c + d + e + f + g + h + i)

-  ///     .listen(print); //prints "abcdefghi"

-  static Observable<T> zip9<A, B, C, D, E, F, G, H, I, T>(

-          Stream<A> streamA,

-          Stream<B> streamB,

-          Stream<C> streamC,

-          Stream<D> streamD,

-          Stream<E> streamE,

-          Stream<F> streamF,

-          Stream<G> streamG,

-          Stream<H> streamH,

-          Stream<I> streamI,

-          T zipper(A a, B b, C c, D d, E e, F f, G g, H h, I i)) =>

-      Observable<T>(ZipStream.zip9(

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        streamG,

-        streamH,

-        streamI,

-        zipper,

-      ));

-

-  /// Returns a multi-subscription stream that produces the same events as this.

-  ///

-  /// The returned stream will subscribe to this stream when its first

-  /// subscriber is added, and will stay subscribed until this stream ends, or a

-  /// callback cancels the subscription.

-  ///

-  /// If onListen is provided, it is called with a subscription-like object that

-  /// represents the underlying subscription to this stream. It is possible to

-  /// pause, resume or cancel the subscription during the call to onListen. It

-  /// is not possible to change the event handlers, including using

-  /// StreamSubscription.asFuture.

-  ///

-  /// If onCancel is provided, it is called in a similar way to onListen when

-  /// the returned stream stops having listener. If it later gets a new

-  /// listener, the onListen function is called again.

-  ///

-  /// Use the callbacks, for example, for pausing the underlying subscription

-  /// while having no subscribers to prevent losing events, or canceling the

-  /// subscription when there are no listeners.

-  @override

-  Observable<T> asBroadcastStream(

-          {void onListen(StreamSubscription<T> subscription),

-          void onCancel(StreamSubscription<T> subscription)}) =>

-      Observable<T>(

-          _stream.asBroadcastStream(onListen: onListen, onCancel: onCancel));

-

-  /// Maps each emitted item to a new [Stream] using the given mapper, then

-  /// subscribes to each new stream one after the next until all values are

-  /// emitted.

-  ///

-  /// asyncExpand is similar to flatMap, but ensures order by guaranteeing that

-  /// all items from the created stream will be emitted before moving to the

-  /// next created stream. This process continues until all created streams have

-  /// completed.

-  ///

-  /// This is functionally equivalent to `concatMap`, which exists as an alias

-  /// for a more fluent Rx API.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(4, 1)

-  ///       .asyncExpand((i) =>

-  ///         new Observable.timer(i, new Duration(minutes: i))

-  ///       .listen(print); // prints 4, 3, 2, 1

-  @override

-  Observable<S> asyncExpand<S>(Stream<S> mapper(T value)) =>

-      Observable<S>(_stream.asyncExpand(mapper));

-

-  /// Creates an Observable with each data event of this stream asynchronously

-  /// mapped to a new event.

-  ///

-  /// This acts like map, except that convert may return a Future, and in that

-  /// case, the stream waits for that future to complete before continuing with

-  /// its result.

-  ///

-  /// The returned stream is a broadcast stream if this stream is.

-  @override

-  Observable<S> asyncMap<S>(FutureOr<S> convert(T value)) =>

-      Observable<S>(_stream.asyncMap(convert));

-

-  /// Creates an Observable where each item is a [List] containing the items

-  /// from the source sequence, batched by the [sampler].

-  ///

-  /// ### Example with [onCount]

-  ///

-  ///     Observable.range(1, 4)

-  ///       .buffer(onCount(2))

-  ///       .listen(print); // prints [1, 2], [3, 4]

-  ///

-  /// ### Example with [onFuture]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .buffer(onFuture(() => new Future.delayed(const Duration(milliseconds: 220))))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  ///

-  /// ### Example with [onTest]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .buffer(onTest((i) => i % 2 == 0))

-  ///       .listen(print); // prints [0], [1, 2] [3, 4] [5, 6] ...

-  ///

-  /// ### Example with [onTime]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .buffer(onTime(const Duration(milliseconds: 220)))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  ///

-  /// ### Example with [onStream]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .buffer(onStream(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i)))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  ///

-  /// You can create your own sampler by extending [StreamView]

-  /// should the above samplers be insufficient for your use case.

-  Observable<List<T>> buffer(SamplerBuilder<T, List<T>> sampler) =>

-      transform(BufferStreamTransformer<T>((Stream<T> stream,

-              OnDataTransform<T, List<T>> bufferHandler,

-              OnDataTransform<List<T>, List<T>> scheduleHandler) =>

-          sampler(stream, bufferHandler, scheduleHandler)));

-

-  /// Buffers a number of values from the source Observable by [count] then

-  /// emits the buffer and clears it, and starts a new buffer each

-  /// [startBufferEvery] values. If [startBufferEvery] is not provided or is

-  /// null, then new buffers are started immediately at the start of the source

-  /// and when each buffer closes and is emitted.

-  ///

-  /// ### Example

-  /// [count] is the maximum size of the buffer emitted

-  ///

-  ///     Observable.range(1, 4)

-  ///       .bufferCount(2)

-  ///       .listen(print); // prints [1, 2], [3, 4] done!

-  ///

-  /// ### Example

-  /// if [startBufferEvery] is 2, then a new buffer will be started

-  /// on every other value from the source. A new buffer is started at the

-  /// beginning of the source by default.

-  ///

-  ///     Observable.range(1, 5)

-  ///       .bufferCount(3, 2)

-  ///       .listen(print); // prints [1, 2, 3], [3, 4, 5], [5] done!

-  Observable<List<T>> bufferCount(int count, [int startBufferEvery = 0]) =>

-      transform(BufferStreamTransformer<T>(

-          onCount<T, List<T>>(count, startBufferEvery)));

-

-  /// Creates an Observable where each item is a [List] containing the items

-  /// from the source sequence, batched whenever [onFutureHandler] completes.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .bufferFuture(() => new Future.delayed(const Duration(milliseconds: 220)))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  Observable<List<T>> bufferFuture<O>(Future<O> onFutureHandler()) => transform(

-      BufferStreamTransformer<T>(onFuture<T, List<T>, O>(onFutureHandler)));

-

-  /// Creates an Observable where each item is a [List] containing the items

-  /// from the source sequence, batched whenever [onTestHandler] passes.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .bufferTest((i) => i % 2 == 0)

-  ///       .listen(print); // prints [0], [1, 2] [3, 4] [5, 6] ...

-  Observable<List<T>> bufferTest(bool onTestHandler(T event)) =>

-      transform(BufferStreamTransformer<T>(onTest<T, List<T>>(onTestHandler)));

-

-  /// Creates an Observable where each item is a [List] containing the items

-  /// from the source sequence, sampled on a time frame with [duration].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .bufferTime(const Duration(milliseconds: 220))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  Observable<List<T>> bufferTime(Duration duration) =>

-      transform(BufferStreamTransformer<T>(onTime(duration)));

-

-  /// Creates an Observable where each item is a [List] containing the items

-  /// from the source sequence, sampled on [onStream].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .bufferWhen(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i))

-  ///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-  Observable<List<T>> bufferWhen<O>(Stream<O> other) =>

-      transform(BufferStreamTransformer<T>(onStream(other)));

-

-  ///

-  /// Adapt this stream to be a `Stream<R>`.

-  ///

-  /// If this stream already has the desired type, its returned directly.

-  /// Otherwise it is wrapped as a `Stream<R>` which checks at run-time that

-  /// each data event emitted by this stream is also an instance of [R].

-  ///

-  @override

-  Observable<R> cast<R>() => Observable<R>(_stream.cast<R>());

-

-  /// Maps each emitted item to a new [Stream] using the given mapper, then

-  /// subscribes to each new stream one after the next until all values are

-  /// emitted.

-  ///

-  /// ConcatMap is similar to flatMap, but ensures order by guaranteeing that

-  /// all items from the created stream will be emitted before moving to the

-  /// next created stream. This process continues until all created streams have

-  /// completed.

-  ///

-  /// This is a simple alias for Dart Stream's `asyncExpand`, but is included to

-  /// ensure a more consistent Rx API.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(4, 1)

-  ///       .concatMap((i) =>

-  ///         new Observable.timer(i, new Duration(minutes: i))

-  ///       .listen(print); // prints 4, 3, 2, 1

-  Observable<S> concatMap<S>(Stream<S> mapper(T value)) =>

-      Observable<S>(_stream.asyncExpand(mapper));

-

-  /// Returns an Observable that emits all items from the current Observable,

-  /// then emits all items from the given observable, one after the next.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.timer(1, new Duration(seconds: 10))

-  ///         .concatWith([new Observable.just(2)])

-  ///         .listen(print); // prints 1, 2

-  Observable<T> concatWith(Iterable<Stream<T>> other) =>

-      Observable<T>(ConcatStream<T>(<Stream<T>>[_stream]..addAll(other)));

-

-  @override

-  AsObservableFuture<bool> contains(Object needle) =>

-      AsObservableFuture<bool>(_stream.contains(needle));

-

-  /// Creates an Observable that will only emit items from the source sequence

-  /// if a particular time span has passed without the source sequence emitting

-  /// another item.

-  ///

-  /// The Debounce operator filters out items emitted by the source Observable

-  /// that are rapidly followed by another emitted item.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#debounce)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.range(1, 100)

-  ///       .debounce(new Duration(seconds: 1))

-  ///       .listen(print); // prints 100

-  Observable<T> debounce(Duration duration) =>

-      transform(DebounceStreamTransformer<T>(duration));

-

-  /// Emit items from the source Stream, or a single default item if the source

-  /// Stream emits nothing.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.empty().defaultIfEmpty(10).listen(print); // prints 10

-  Observable<T> defaultIfEmpty(T defaultValue) =>

-      transform(DefaultIfEmptyStreamTransformer<T>(defaultValue));

-

-  /// The Delay operator modifies its source Observable by pausing for

-  /// a particular increment of time (that you specify) before emitting

-  /// each of the source Observable’s items.

-  /// This has the effect of shifting the entire sequence of items emitted

-  /// by the Observable forward in time by that specified increment.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#delay)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3, 4])

-  ///       .delay(new Duration(seconds: 1))

-  ///       .listen(print); // [after one second delay] prints 1, 2, 3, 4 immediately

-  Observable<T> delay(Duration duration) =>

-      transform(DelayStreamTransformer<T>(duration));

-

-  /// Converts the onData, onDone, and onError [Notification] objects from a

-  /// materialized stream into normal onData, onDone, and onError events.

-  ///

-  /// When a stream has been materialized, it emits onData, onDone, and onError

-  /// events as [Notification] objects. Dematerialize simply reverses this by

-  /// transforming [Notification] objects back to a normal stream of events.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable<Notification<int>>

-  ///         .fromIterable([new Notification.onData(1), new Notification.onDone()])

-  ///         .dematerialize()

-  ///         .listen((i) => print(i)); // Prints 1

-  ///

-  /// ### Error example

-  ///

-  ///     new Observable<Notification<int>>

-  ///         .just(new Notification.onError(new Exception(), null))

-  ///         .dematerialize()

-  ///         .listen(null, onError: (e, s) { print(e) }); // Prints Exception

-  Observable<S> dematerialize<S>() {

-    return cast<Notification<S>>()

-        .transform(DematerializeStreamTransformer<S>());

-  }

-

-  /// WARNING: More commonly known as distinctUntilChanged in other Rx

-  /// implementations. Creates an Observable where data events are skipped if

-  /// they are equal to the previous data event.

-  ///

-  /// The returned stream provides the same events as this stream, except that

-  /// it never provides two consecutive data events that are equal.

-  ///

-  /// Equality is determined by the provided equals method. If that is omitted,

-  /// the '==' operator on the last provided data element is used.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. If a

-  /// broadcast stream is listened to more than once, each subscription will

-  /// individually perform the equals test.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#distinctUntilChanged)

-  @override

-  Observable<T> distinct([bool equals(T previous, T next)]) =>

-      Observable<T>(_stream.distinct(equals));

-

-  /// WARNING: More commonly known as distinct in other Rx implementations.

-  /// Creates an Observable where data events are skipped if they have already

-  /// been emitted before.

-  ///

-  /// Equality is determined by the provided equals and hashCode methods.

-  /// If these are omitted, the '==' operator and hashCode on the last provided

-  /// data element are used.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. If a

-  /// broadcast stream is listened to more than once, each subscription will

-  /// individually perform the equals and hashCode tests.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#distinct)

-  Observable<T> distinctUnique({bool equals(T e1, T e2), int hashCode(T e)}) =>

-      transform(DistinctUniqueStreamTransformer<T>(

-          equals: equals, hashCode: hashCode));

-

-  /// Invokes the given callback function when the stream subscription is

-  /// cancelled. Often called doOnUnsubscribe or doOnDispose in other

-  /// implementations.

-  ///

-  /// ### Example

-  ///

-  ///     final subscription = new Observable.timer(1, new Duration(minutes: 1))

-  ///       .doOnCancel(() => print("hi"));

-  ///       .listen(null);

-  ///

-  ///     subscription.cancel(); // prints "hi"

-  Observable<T> doOnCancel(void onCancel()) =>

-      transform(DoStreamTransformer<T>(onCancel: onCancel));

-

-  /// Invokes the given callback function when the stream emits an item. In

-  /// other implementations, this is called doOnNext.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///       .doOnData(print)

-  ///       .listen(null); // prints 1, 2, 3

-  Observable<T> doOnData(void onData(T event)) =>

-      transform(DoStreamTransformer<T>(onData: onData));

-

-  /// Invokes the given callback function when the stream finishes emitting

-  /// items. In other implementations, this is called doOnComplete(d).

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///       .doOnDone(() => print("all set"))

-  ///       .listen(null); // prints "all set"

-  Observable<T> doOnDone(void onDone()) =>

-      transform(DoStreamTransformer<T>(onDone: onDone));

-

-  /// Invokes the given callback function when the stream emits data, emits

-  /// an error, or emits done. The callback receives a [Notification] object.

-  ///

-  /// The [Notification] object contains the [Kind] of event (OnData, onDone,

-  /// or OnError), and the item or error that was emitted. In the case of

-  /// onDone, no data is emitted as part of the [Notification].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(1)

-  ///       .doOnEach(print)

-  ///       .listen(null); // prints Notification{kind: OnData, value: 1, errorAndStackTrace: null}, Notification{kind: OnDone, value: null, errorAndStackTrace: null}

-  Observable<T> doOnEach(void onEach(Notification<T> notification)) =>

-      transform(DoStreamTransformer<T>(onEach: onEach));

-

-  /// Invokes the given callback function when the stream emits an error.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.error(new Exception())

-  ///       .doOnError((error, stacktrace) => print("oh no"))

-  ///       .listen(null); // prints "Oh no"

-  Observable<T> doOnError(Function onError) =>

-      transform(DoStreamTransformer<T>(onError: onError));

-

-  /// Invokes the given callback function when the stream is first listened to.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(1)

-  ///       .doOnListen(() => print("Is someone there?"))

-  ///       .listen(null); // prints "Is someone there?"

-  Observable<T> doOnListen(void onListen()) =>

-      transform(DoStreamTransformer<T>(onListen: onListen));

-

-  /// Invokes the given callback function when the stream subscription is

-  /// paused.

-  ///

-  /// ### Example

-  ///

-  ///     final subscription = new Observable.just(1)

-  ///       .doOnPause(() => print("Gimme a minute please"))

-  ///       .listen(null);

-  ///

-  ///     subscription.pause(); // prints "Gimme a minute please"

-  Observable<T> doOnPause(void onPause(Future<dynamic> resumeSignal)) =>

-      transform(DoStreamTransformer<T>(onPause: onPause));

-

-  /// Invokes the given callback function when the stream subscription

-  /// resumes receiving items.

-  ///

-  /// ### Example

-  ///

-  ///     final subscription = new Observable.just(1)

-  ///       .doOnResume(() => print("Let's do this!"))

-  ///       .listen(null);

-  ///

-  ///     subscription.pause();

-  ///     subscription.resume(); "Let's do this!"

-  Observable<T> doOnResume(void onResume()) =>

-      transform(DoStreamTransformer<T>(onResume: onResume));

-

-  @override

-  AsObservableFuture<S> drain<S>([S futureValue]) =>

-      AsObservableFuture<S>(_stream.drain(futureValue));

-

-  @override

-  AsObservableFuture<T> elementAt(int index) =>

-      AsObservableFuture<T>(_stream.elementAt(index));

-

-  @override

-  AsObservableFuture<bool> every(bool test(T element)) =>

-      AsObservableFuture<bool>(_stream.every(test));

-

-  /// Converts items from the source stream into a new Stream using a given

-  /// mapper. It ignores all items from the source stream until the new stream

-  /// completes.

-  ///

-  /// Useful when you have a noisy source Stream and only want to respond once

-  /// the previous async operation is finished.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(0, 2).interval(new Duration(milliseconds: 50))

-  ///       .exhaustMap((i) =>

-  ///         new Observable.timer(i, new Duration(milliseconds: 75)))

-  ///       .listen(print); // prints 0, 2

-  Observable<S> exhaustMap<S>(Stream<S> mapper(T value)) =>

-      transform(ExhaustMapStreamTransformer<T, S>(mapper));

-

-  /// Creates an Observable from this stream that converts each element into

-  /// zero or more events.

-  ///

-  /// Each incoming event is converted to an Iterable of new events, and each of

-  /// these new events are then sent by the returned Observable in order.

-  ///

-  /// The returned Observable is a broadcast stream if this stream is. If a

-  /// broadcast stream is listened to more than once, each subscription will

-  /// individually call convert and expand the events.

-  @override

-  Observable<S> expand<S>(Iterable<S> convert(T value)) =>

-      Observable<S>(_stream.expand(convert));

-

-  @override

-  AsObservableFuture<T> get first => AsObservableFuture<T>(_stream.first);

-

-  @override

-  AsObservableFuture<T> firstWhere(bool test(T element),

-          {dynamic defaultValue(), T orElse()}) =>

-      AsObservableFuture<T>(_stream.firstWhere(test, orElse: orElse));

-

-  /// Converts each emitted item into a new Stream using the given mapper

-  /// function. The newly created Stream will be be listened to and begin

-  /// emitting items downstream.

-  ///

-  /// The items emitted by each of the new Streams are emitted downstream in the

-  /// same order they arrive. In other words, the sequences are merged

-  /// together.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(4, 1)

-  ///       .flatMap((i) =>

-  ///         new Observable.timer(i, new Duration(minutes: i))

-  ///       .listen(print); // prints 1, 2, 3, 4

-  Observable<S> flatMap<S>(Stream<S> mapper(T value)) =>

-      transform(FlatMapStreamTransformer<T, S>(mapper));

-

-  /// Converts each item into a new Stream. The Stream must return an

-  /// Iterable. Then, each item from the Iterable will be emitted one by one.

-  ///

-  /// Use case: you may have an API that returns a list of items, such as

-  /// a Stream<List<String>>. However, you might want to operate on the individual items

-  /// rather than the list itself. This is the job of `flatMapIterable`.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(1, 4)

-  ///       .flatMapIterable((i) =>

-  ///         new Observable.just([i])

-  ///       .listen(print); // prints 1, 2, 3, 4

-  Observable<S> flatMapIterable<S>(Stream<Iterable<S>> mapper(T value)) =>

-      transform(FlatMapStreamTransformer<T, Iterable<S>>(mapper))

-          .expand((Iterable<S> iterable) => iterable);

-

-  @override

-  AsObservableFuture<S> fold<S>(

-          S initialValue, S combine(S previous, T element)) =>

-      AsObservableFuture<S>(_stream.fold(initialValue, combine));

-

-  @override

-  AsObservableFuture<dynamic> forEach(void action(T element)) =>

-      AsObservableFuture<dynamic>(_stream.forEach(action));

-

-  /// The GroupBy operator divides an [Observable] that emits items into

-  /// an [Observable] that emits [GroupByObservable],

-  /// each one of which emits some subset of the items

-  /// from the original source [Observable].

-  ///

-  /// [GroupByObservable] acts like a regular [Observable], yet

-  /// adding a 'key' property, which receives its [Type] and value from

-  /// the [grouper] Function.

-  ///

-  /// All items with the same key are emitted by the same [GroupByObservable].

-  Observable<GroupByObservable<T, S>> groupBy<S>(S grouper(T value)) =>

-      transform(GroupByStreamTransformer<T, S>(grouper));

-

-  /// Creates a wrapper Stream that intercepts some errors from this stream.

-  ///

-  /// If this stream sends an error that matches test, then it is intercepted by

-  /// the handle function.

-  ///

-  /// The onError callback must be of type void onError(error) or void

-  /// onError(error, StackTrace stackTrace). Depending on the function type the

-  /// stream either invokes onError with or without a stack trace. The stack

-  /// trace argument might be null if the stream itself received an error

-  /// without stack trace.

-  ///

-  /// An asynchronous error e is matched by a test function if test(e) returns

-  /// true. If test is omitted, every error is considered matching.

-  ///

-  /// If the error is intercepted, the handle function can decide what to do

-  /// with it. It can throw if it wants to raise a new (or the same) error, or

-  /// simply return to make the stream forget the error.

-  ///

-  /// If you need to transform an error into a data event, use the more generic

-  /// Stream.transform to handle the event by writing a data event to the output

-  /// sink.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. If a

-  /// broadcast stream is listened to more than once, each subscription will

-  /// individually perform the test and handle the error.

-  @override

-  Observable<T> handleError(Function onError, {bool test(dynamic error)}) =>

-      Observable<T>(_stream.handleError(onError, test: test));

-

-  /// Creates an Observable where all emitted items are ignored, only the

-  /// error / completed notifications are passed

-  ///

-  /// ### Example

-  ///

-  ///    new Observable.merge([

-  ///      new Observable.just(1),

-  ///      new Observable.error(new Exception())

-  ///    ])

-  ///    .listen(print, onError: print); // prints Exception

-  Observable<T> ignoreElements() =>

-      transform(IgnoreElementsStreamTransformer<T>());

-

-  /// Creates an Observable that emits each item in the Stream after a given

-  /// duration.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///       .interval(new Duration(seconds: 1))

-  ///       .listen((i) => print("$i sec"); // prints 1 sec, 2 sec, 3 sec

-  Observable<T> interval(Duration duration) =>

-      transform(IntervalStreamTransformer<T>(duration));

-

-  @override

-  bool get isBroadcast {

-    return (_stream != null) ? _stream.isBroadcast : false;

-  }

-

-  @override

-  AsObservableFuture<bool> get isEmpty =>

-      AsObservableFuture<bool>(_stream.isEmpty);

-

-  @override

-  AsObservableFuture<String> join([String separator = ""]) =>

-      AsObservableFuture<String>(_stream.join(separator));

-

-  @override

-  AsObservableFuture<T> get last => AsObservableFuture<T>(_stream.last);

-

-  @override

-  AsObservableFuture<T> lastWhere(bool test(T element),

-          {Object defaultValue(), T orElse()}) =>

-      AsObservableFuture<T>(_stream.lastWhere(test, orElse: orElse));

-

-  /// Adds a subscription to this stream. Returns a [StreamSubscription] which

-  /// handles events from the stream using the provided [onData], [onError] and

-  /// [onDone] handlers.

-  ///

-  /// The handlers can be changed on the subscription, but they start out

-  /// as the provided functions.

-  ///

-  /// On each data event from this stream, the subscriber's [onData] handler

-  /// is called. If [onData] is `null`, nothing happens.

-  ///

-  /// On errors from this stream, the [onError] handler is called with the

-  /// error object and possibly a stack trace.

-  ///

-  /// The [onError] callback must be of type `void onError(error)` or

-  /// `void onError(error, StackTrace stackTrace)`. If [onError] accepts

-  /// two arguments it is called with the error object and the stack trace

-  /// (which could be `null` if the stream itself received an error without

-  /// stack trace).

-  /// Otherwise it is called with just the error object.

-  /// If [onError] is omitted, any errors on the stream are considered unhandled,

-  /// and will be passed to the current [Zone]'s error handler.

-  /// By default unhandled async errors are treated

-  /// as if they were uncaught top-level errors.

-  ///

-  /// If this stream closes and sends a done event, the [onDone] handler is

-  /// called. If [onDone] is `null`, nothing happens.

-  ///

-  /// If [cancelOnError] is true, the subscription is automatically cancelled

-  /// when the first error event is delivered. The default is `false`.

-  ///

-  /// While a subscription is paused, or when it has been cancelled,

-  /// the subscription doesn't receive events and none of the

-  /// event handler functions are called.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(1).listen(print); // prints 1

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return _stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  @override

-  AsObservableFuture<int> get length => AsObservableFuture<int>(_stream.length);

-

-  /// Maps values from a source sequence through a function and emits the

-  /// returned values.

-  ///

-  /// The returned sequence completes when the source sequence completes.

-  /// The returned sequence throws an error if the source sequence throws an

-  /// error.

-  @override

-  Observable<S> map<S>(S convert(T event)) =>

-      Observable<S>(_stream.map(convert));

-

-  /// Emits the given constant value on the output Observable every time the source Observable emits a value.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.fromIterable([1, 2, 3, 4])

-  ///       .mapTo(true)

-  ///       .listen(print); // prints true, true, true, true

-  Observable<S> mapTo<S>(S value) =>

-      transform(MapToStreamTransformer<T, S>(value));

-

-  /// Converts the onData, on Done, and onError events into [Notification]

-  /// objects that are passed into the downstream onData listener.

-  ///

-  /// The [Notification] object contains the [Kind] of event (OnData, onDone, or

-  /// OnError), and the item or error that was emitted. In the case of onDone,

-  /// no data is emitted as part of the [Notification].

-  ///

-  /// Example:

-  ///     new Observable<int>.just(1)

-  ///         .materialize()

-  ///         .listen((i) => print(i)); // Prints onData & onDone Notification

-  ///

-  ///     new Observable<int>.error(new Exception())

-  ///         .materialize()

-  ///         .listen((i) => print(i)); // Prints onError Notification

-  Observable<Notification<T>> materialize() =>

-      transform(MaterializeStreamTransformer<T>());

-

-  /// Converts a Stream into a Future that completes with the largest item emitted

-  /// by the Stream.

-  ///

-  /// This is similar to finding the max value in a list, but the values are

-  /// asynchronous.

-  ///

-  /// ### Example

-  ///

-  ///     final max = await new Observable.fromIterable([1, 2, 3]).max();

-  ///

-  ///     print(max); // prints 3

-  ///

-  /// ### Example with custom [Comparator]

-  ///

-  ///     final observable = new Observable.fromIterable("short", "looooooong");

-  ///     final max = await observable.max((a, b) => a.length - b.length);

-  ///

-  ///     print(max); // prints "looooooong"

-  AsObservableFuture<T> max([Comparator<T> comparator]) =>

-      AsObservableFuture<T>(StreamMaxFuture<T>(_stream, comparator));

-

-  /// Combines the items emitted by multiple streams into a single stream of

-  /// items. The items are emitted in the order they are emitted by their

-  /// sources.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.timer(1, new Duration(seconds: 10))

-  ///         .mergeWith([new Observable.just(2)])

-  ///         .listen(print); // prints 2, 1

-  Observable<T> mergeWith(Iterable<Stream<T>> streams) =>

-      Observable<T>(MergeStream<T>(<Stream<T>>[_stream]..addAll(streams)));

-

-  /// Converts a Stream into a Future that completes with the smallest item

-  /// emitted by the Stream.

-  ///

-  /// This is similar to finding the min value in a list, but the values are

-  /// asynchronous!

-  ///

-  /// ### Example

-  ///

-  ///     final min = await new Observable.fromIterable([1, 2, 3]).min();

-  ///

-  ///     print(min); // prints 1

-  ///

-  /// ### Example with custom [Comparator]

-  ///

-  ///     final observable = new Observable.fromIterable("short", "looooooong");

-  ///     final min = await observable.min((a, b) => a.length - b.length);

-  ///

-  ///     print(min); // prints "short"

-  AsObservableFuture<T> min([Comparator<T> comparator]) =>

-      AsObservableFuture<T>(StreamMinFuture<T>(_stream, comparator));

-

-  /// Filters a sequence so that only events of a given type pass

-  ///

-  /// In order to capture the Type correctly, it needs to be wrapped

-  /// in a [TypeToken] as the generic parameter.

-  ///

-  /// Given the way Dart generics work, one cannot simply use the `is T` / `as T`

-  /// checks and castings with this method alone. Therefore, the

-  /// [TypeToken] class was introduced to capture the type of class you'd

-  /// like `ofType` to filter down to.

-  ///

-  /// ### Examples

-  ///

-  ///     new Observable.fromIterable([1, "hi"])

-  ///       .ofType(new TypeToken<String>)

-  ///       .listen(print); // prints "hi"

-  ///

-  /// As a shortcut, you can use some pre-defined constants to write the above

-  /// in the following way:

-  ///

-  ///     new Observable.fromIterable([1, "hi"])

-  ///       .ofType(kString)

-  ///       .listen(print); // prints "hi"

-  ///

-  /// If you'd like to create your own shortcuts like the example above,

-  /// simply create a constant:

-  ///

-  ///     const TypeToken<Map<Int, String>> kMapIntString =

-  ///       const TypeToken<Map<Int, String>>();

-  Observable<S> ofType<S>(TypeToken<S> typeToken) =>

-      transform(OfTypeStreamTransformer<T, S>(typeToken));

-

-  /// Intercepts error events and switches to the given recovery stream in

-  /// that case

-  ///

-  /// The onErrorResumeNext operator intercepts an onError notification from

-  /// the source Observable. Instead of passing the error through to any

-  /// listeners, it replaces it with another Stream of items.

-  ///

-  /// If you need to perform logic based on the type of error that was emitted,

-  /// please consider using [onErrorResume].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.error(new Exception())

-  ///       .onErrorResumeNext(new Observable.fromIterable([1, 2, 3]))

-  ///       .listen(print); // prints 1, 2, 3

-  Observable<T> onErrorResumeNext(Stream<T> recoveryStream) => transform(

-      OnErrorResumeStreamTransformer<T>((dynamic e) => recoveryStream));

-

-  /// Intercepts error events and switches to a recovery stream created by the

-  /// provided [recoveryFn].

-  ///

-  /// The onErrorResume operator intercepts an onError notification from

-  /// the source Observable. Instead of passing the error through to any

-  /// listeners, it replaces it with another Stream of items created by the

-  /// [recoveryFn].

-  ///

-  /// The [recoveryFn] receives the emitted error and returns a Stream. You can

-  /// perform logic in the [recoveryFn] to return different Streams based on the

-  /// type of error that was emitted.

-  ///

-  /// If you do not need to perform logic based on the type of error that was

-  /// emitted, please consider using [onErrorResumeNext] or [onErrorReturn].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable<int>.error(new Exception())

-  ///       .onErrorResume((dynamic e) =>

-  ///           new Observable.just(e is StateError ? 1 : 0)

-  ///       .listen(print); // prints 0

-  Observable<T> onErrorResume(Stream<T> Function(dynamic error) recoveryFn) =>

-      transform(OnErrorResumeStreamTransformer<T>(recoveryFn));

-

-  /// instructs an Observable to emit a particular item when it encounters an

-  /// error, and then terminate normally

-  ///

-  /// The onErrorReturn operator intercepts an onError notification from

-  /// the source Observable. Instead of passing it through to any observers, it

-  /// replaces it with a given item, and then terminates normally.

-  ///

-  /// If you need to perform logic based on the type of error that was emitted,

-  /// please consider using [onErrorReturnWith].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.error(new Exception())

-  ///       .onErrorReturn(1)

-  ///       .listen(print); // prints 1

-  Observable<T> onErrorReturn(T returnValue) =>

-      transform(OnErrorResumeStreamTransformer<T>(

-          (dynamic e) => Observable<T>.just(returnValue)));

-

-  /// instructs an Observable to emit a particular item created by the

-  /// [returnFn] when it encounters an error, and then terminate normally.

-  ///

-  /// The onErrorReturnWith operator intercepts an onError notification from

-  /// the source Observable. Instead of passing it through to any observers, it

-  /// replaces it with a given item, and then terminates normally.

-  ///

-  /// The [returnFn] receives the emitted error and returns a Stream. You can

-  /// perform logic in the [returnFn] to return different Streams based on the

-  /// type of error that was emitted.

-  ///

-  /// If you do not need to perform logic based on the type of error that was

-  /// emitted, please consider using [onErrorReturn].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.error(new Exception())

-  ///       .onErrorReturnWith((e) => e is Exception ? 1 : 0)

-  ///       .listen(print); // prints 1

-  Observable<T> onErrorReturnWith(T Function(dynamic error) returnFn) =>

-      transform(OnErrorResumeStreamTransformer<T>(

-          (dynamic e) => Observable<T>.just(returnFn(e))));

-

-  /// Triggers on the second and subsequent triggerings of the input observable.

-  /// The Nth triggering of the input observable passes the arguments from the N-1th and Nth triggering as a pair.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(1, 4)

-  ///       .pairwise()

-  ///       .listen(print); // prints [1, 2], [2, 3]

-  Observable<List<T>> pairwise() =>

-      transform(BufferStreamTransformer<T>(onCount<T, List<T>>(2, 1),

-          exhaustBufferOnDone: false));

-

-  @override

-  AsObservableFuture<dynamic> pipe(StreamConsumer<T> streamConsumer) =>

-      AsObservableFuture<dynamic>(_stream.pipe(streamConsumer));

-

-  @override

-  AsObservableFuture<T> reduce(T combine(T previous, T element)) =>

-      AsObservableFuture<T>(_stream.reduce(combine));

-

-  /// Returns an Observable that, when the specified sample stream emits

-  /// an item or completes, emits the most recently emitted item (if any)

-  /// emitted by the source stream since the previous emission from

-  /// the sample stream.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///       .sample(new Observable.timer(1, new Duration(seconds: 1))

-  ///       .listen(print); // prints 3

-  Observable<T> sample(Stream<dynamic> sampleStream) =>

-      transform(SampleStreamTransformer<T>(sampleStream));

-

-  /// Applies an accumulator function over an observable sequence and returns

-  /// each intermediate result. The optional seed value is used as the initial

-  /// accumulator value.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///        .scan((acc, curr, i) => acc + curr, 0)

-  ///        .listen(print); // prints 1, 3, 6

-  Observable<S> scan<S>(S accumulator(S accumulated, T value, int index),

-          [S seed]) =>

-      transform(ScanStreamTransformer<T, S>(accumulator, seed));

-

-  @override

-  AsObservableFuture<T> get single => AsObservableFuture<T>(_stream.single);

-

-  @override

-  AsObservableFuture<T> singleWhere(bool test(T element), {T orElse()}) =>

-      AsObservableFuture<T>(_stream.singleWhere(test, orElse: orElse));

-

-  /// Skips the first count data events from this stream.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. For a

-  /// broadcast stream, the events are only counted from the time the returned

-  /// stream is listened to.

-  @override

-  Observable<T> skip(int count) => Observable<T>(_stream.skip(count));

-

-  /// Starts emitting items only after the given stream emits an item.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.merge([

-  ///         new Observable.just(1),

-  ///         new Observable.timer(2, new Duration(minutes: 2))

-  ///       ])

-  ///       .skipUntil(new Observable.timer(true, new Duration(minutes: 1)))

-  ///       .listen(print); // prints 2;

-  Observable<T> skipUntil<S>(Stream<S> otherStream) =>

-      transform(SkipUntilStreamTransformer<T, S>(otherStream));

-

-  /// Skip data events from this stream while they are matched by test.

-  ///

-  /// Error and done events are provided by the returned stream unmodified.

-  ///

-  /// Starting with the first data event where test returns false for the event

-  /// data, the returned stream will have the same events as this stream.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. For a

-  /// broadcast stream, the events are only tested from the time the returned

-  /// stream is listened to.

-  @override

-  Observable<T> skipWhile(bool test(T element)) =>

-      Observable<T>(_stream.skipWhile(test));

-

-  /// Prepends a value to the source Observable.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(2).startWith(1).listen(print); // prints 1, 2

-  Observable<T> startWith(T startValue) =>

-      transform(StartWithStreamTransformer<T>(startValue));

-

-  /// Prepends a sequence of values to the source Observable.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(3).startWithMany([1, 2])

-  ///       .listen(print); // prints 1, 2, 3

-  Observable<T> startWithMany(List<T> startValues) =>

-      transform(StartWithManyStreamTransformer<T>(startValues));

-

-  /// When the original observable emits no items, this operator subscribes to

-  /// the given fallback stream and emits items from that observable instead.

-  ///

-  /// This can be particularly useful when consuming data from multiple sources.

-  /// For example, when using the Repository Pattern. Assuming you have some

-  /// data you need to load, you might want to start with the fastest access

-  /// point and keep falling back to the slowest point. For example, first query

-  /// an in-memory database, then a database on the file system, then a network

-  /// call if the data isn't on the local machine.

-  ///

-  /// This can be achieved quite simply with switchIfEmpty!

-  ///

-  /// ### Example

-  ///

-  ///     // Let's pretend we have some Data sources that complete without emitting

-  ///     // any items if they don't contain the data we're looking for

-  ///     Observable<Data> memory;

-  ///     Observable<Data> disk;

-  ///     Observable<Data> network;

-  ///

-  ///     // Start with memory, fallback to disk, then fallback to network.

-  ///     // Simple as that!

-  ///     Observable<Data> getThatData =

-  ///         memory.switchIfEmpty(disk).switchIfEmpty(network);

-  Observable<T> switchIfEmpty(Stream<T> fallbackStream) =>

-      transform(SwitchIfEmptyStreamTransformer<T>(fallbackStream));

-

-  /// Converts each emitted item into a new Stream using the given mapper

-  /// function. The newly created Stream will be be listened to and begin

-  /// emitting items, and any previously created Stream will stop emitting.

-  ///

-  /// The switchMap operator is similar to the flatMap and concatMap methods,

-  /// but it only emits items from the most recently created Stream.

-  ///

-  /// This can be useful when you only want the very latest state from

-  /// asynchronous APIs, for example.

-  ///

-  /// ### Example

-  ///

-  ///     Observable.range(4, 1)

-  ///       .switchMap((i) =>

-  ///         new Observable.timer(i, new Duration(minutes: i))

-  ///       .listen(print); // prints 1

-  Observable<S> switchMap<S>(Stream<S> mapper(T value)) =>

-      transform(SwitchMapStreamTransformer<T, S>(mapper));

-

-  /// Provides at most the first `n` values of this stream.

-  /// Forwards the first n data events of this stream, and all error events, to

-  /// the returned stream, and ends with a done event.

-  ///

-  /// If this stream produces fewer than count values before it's done, so will

-  /// the returned stream.

-  ///

-  /// Stops listening to the stream after the first n elements have been

-  /// received.

-  ///

-  /// Internally the method cancels its subscription after these elements. This

-  /// means that single-subscription (non-broadcast) streams are closed and

-  /// cannot be reused after a call to this method.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. For a

-  /// broadcast stream, the events are only counted from the time the returned

-  /// stream is listened to

-  @override

-  Observable<T> take(int count) => Observable<T>(_stream.take(count));

-

-  /// Returns the values from the source observable sequence until the other

-  /// observable sequence produces a value.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.merge([

-  ///         new Observable.just(1),

-  ///         new Observable.timer(2, new Duration(minutes: 1))

-  ///       ])

-  ///       .takeUntil(new Observable.timer(3, new Duration(seconds: 10)))

-  ///       .listen(print); // prints 1

-  Observable<T> takeUntil<S>(Stream<S> otherStream) =>

-      transform(TakeUntilStreamTransformer<T, S>(otherStream));

-

-  /// Forwards data events while test is successful.

-  ///

-  /// The returned stream provides the same events as this stream as long as

-  /// test returns true for the event data. The stream is done when either this

-  /// stream is done, or when this stream first provides a value that test

-  /// doesn't accept.

-  ///

-  /// Stops listening to the stream after the accepted elements.

-  ///

-  /// Internally the method cancels its subscription after these elements. This

-  /// means that single-subscription (non-broadcast) streams are closed and

-  /// cannot be reused after a call to this method.

-  ///

-  /// The returned stream is a broadcast stream if this stream is. For a

-  /// broadcast stream, the events are only tested from the time the returned

-  /// stream is listened to.

-  @override

-  Observable<T> takeWhile(bool test(T element)) =>

-      Observable<T>(_stream.takeWhile(test));

-

-  /// Returns an Observable that emits only the first item emitted by the source

-  /// Observable during sequential time windows of a specified duration.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2, 3])

-  ///       .throttle(new Duration(seconds: 1))

-  ///       .listen(print); // prints 1

-  Observable<T> throttle(Duration duration) =>

-      transform(ThrottleStreamTransformer<T>(duration));

-

-  /// Records the time interval between consecutive values in an observable

-  /// sequence.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(1)

-  ///       .interval(new Duration(seconds: 1))

-  ///       .timeInterval()

-  ///       .listen(print); // prints TimeInterval{interval: 0:00:01, value: 1}

-  Observable<TimeInterval<T>> timeInterval() =>

-      transform(TimeIntervalStreamTransformer<T>());

-

-  /// The Timeout operator allows you to abort an Observable with an onError

-  /// termination if that Observable fails to emit any items during a specified

-  /// duration.  You may optionally provide a callback function to execute on

-  /// timeout.

-  @override

-  Observable<T> timeout(Duration timeLimit,

-          {void onTimeout(EventSink<T> sink)}) =>

-      Observable<T>(_stream.timeout(timeLimit, onTimeout: onTimeout));

-

-  /// Wraps each item emitted by the source Observable in a [Timestamped] object

-  /// that includes the emitted item and the time when the item was emitted.

-  ///

-  /// Example

-  ///

-  ///     new Observable.just(1)

-  ///        .timestamp()

-  ///        .listen((i) => print(i)); // prints 'TimeStamp{timestamp: XXX, value: 1}';

-  Observable<Timestamped<T>> timestamp() {

-    return transform(TimestampStreamTransformer<T>());

-  }

-

-  @override

-  Observable<S> transform<S>(StreamTransformer<T, S> streamTransformer) =>

-      Observable<S>(super.transform(streamTransformer));

-

-  @override

-  AsObservableFuture<List<T>> toList() =>

-      AsObservableFuture<List<T>>(_stream.toList());

-

-  @override

-  AsObservableFuture<Set<T>> toSet() =>

-      AsObservableFuture<Set<T>>(_stream.toSet());

-

-  /// Filters the elements of an observable sequence based on the test.

-  @override

-  Observable<T> where(bool test(T event)) => Observable<T>(_stream.where(test));

-

-  /// Creates an Observable where each item is a [Stream] containing the items

-  /// from the source sequence, batched by the [sampler].

-  ///

-  /// ### Example with [onCount]

-  ///

-  ///     Observable.range(1, 4)

-  ///       .window(onCount(2))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 1, 2, next window 3, 4

-  ///

-  /// ### Example with [onFuture]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .window(onFuture(() => new Future.delayed(const Duration(milliseconds: 220))))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  ///

-  /// ### Example with [onTest]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .window(onTest((i) => i % 2 == 0))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, next window 1, 2 next window 3, 4,  ...

-  ///

-  /// ### Example with [onTime]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .window(onTime(const Duration(milliseconds: 220)))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  ///

-  /// ### Example with [onStream]

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .window(onStream(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i)))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  ///

-  /// You can create your own sampler by extending [StreamView]

-  /// should the above samplers be insufficient for your use case.

-  Observable<Stream<T>> window(SamplerBuilder<T, Stream<T>> sampler) =>

-      transform(WindowStreamTransformer<T>((Stream<T> stream,

-              OnDataTransform<T, Stream<T>> bufferHandler,

-              OnDataTransform<Stream<T>, Stream<T>> scheduleHandler) =>

-          sampler(stream, bufferHandler, scheduleHandler)));

-

-  /// Buffers a number of values from the source Observable by [count] then

-  /// emits the values inside the buffer as a new [Stream],

-  /// and starts a new buffer each [startBufferEvery] values.

-  /// If [startBufferEvery] is not provided or is null, then new buffers are

-  /// started immediately at the start of the source and when each buffer

-  /// closes and is emitted.

-  ///

-  /// ### Example

-  /// [count] is the maximum size of the buffer emitted

-  ///

-  ///     Observable.range(1, 4)

-  ///       .windowCount(2)

-  ///       .doOnData((_) => print('new Stream emitted))

-  ///       .flatMap((stream) => stream)

-  ///       .listen(print); // prints new Stream emitted, 1, 2, new Stream emitted, 3, 4 done!

-  ///

-  /// ### Example

-  /// if [startBufferEvery] is 2, then a new buffer will be started

-  /// on every other value from the source. A new buffer is started at the

-  /// beginning of the source by default.

-  ///

-  ///     Observable.range(1, 5)

-  ///       .windowCount(3, 2)

-  ///       .doOnData((_) => print('new Stream emitted))

-  ///       .flatMap((stream) => stream)

-  ///       .listen(print); // prints new Stream emitted, 1, 2, 3 new Stream emitted 3, 4, 5 new Stream emitted 5 done!

-  Observable<Stream<T>> windowCount(int count, [int startBufferEvery = 0]) =>

-      transform(WindowStreamTransformer<T>(onCount(count, startBufferEvery)));

-

-  /// Creates an Observable where each item is a [Stream] containing the items

-  /// from the source sequence, batched whenever [onFutureHandler] completes.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .windowFuture(() => new Future.delayed(const Duration(milliseconds: 220)))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  Observable<Stream<T>> windowFuture<O>(Future<O> onFutureHandler()) =>

-      transform(WindowStreamTransformer<T>(onFuture(onFutureHandler)));

-

-  /// Creates an Observable where each item is a [Stream] containing the items

-  /// from the source sequence, batched whenever [onTestHandler] passes.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .windowTest((i) => i % 2 == 0)

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, next window 1, 2 next window 3, 4,  ...

-  Observable<Stream<T>> windowTest(bool onTestHandler(T event)) =>

-      transform(WindowStreamTransformer<T>(onTest(onTestHandler)));

-

-  /// Creates an Observable where each item is a [Stream] containing the items

-  /// from the source sequence, sampled on a time frame with [duration].

-  ///

-  /// ### Example

-  ///

-  ////     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .windowTime(const Duration(milliseconds: 220))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  Observable<Stream<T>> windowTime(Duration duration) =>

-      transform(WindowStreamTransformer<T>(onTime(duration)));

-

-  /// Creates an Observable where each item is a [Stream] containing the items

-  /// from the source sequence, sampled on [onStream].

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-  ///       .windowWhen(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i))

-  ///       .doOnData((_) => print('next window'))

-  ///       .flatMap((s) => s)

-  ///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-  Observable<Stream<T>> windowWhen<O>(Stream<O> other) =>

-      transform(WindowStreamTransformer<T>(onStream(other)));

-

-  /// Creates an Observable that emits when the source stream emits, combining

-  /// the latest values from the two streams using the provided function.

-  ///

-  /// If the latestFromStream has not emitted any values, this stream will not

-  /// emit either.

-  ///

-  /// [Interactive marble diagram](http://rxmarbles.com/#withLatestFrom)

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.fromIterable([1, 2]).withLatestFrom(

-  ///       new Observable.fromIterable([2, 3]), (a, b) => a + b)

-  ///       .listen(print); // prints 4 (due to the async nature of streams)

-  Observable<R> withLatestFrom<S, R>(

-          Stream<S> latestFromStream, R fn(T t, S s)) =>

-      transform(WithLatestFromStreamTransformer<T, S, R>(latestFromStream, fn));

-

-  /// Returns an Observable that combines the current stream together with

-  /// another stream using a given zipper function.

-  ///

-  /// ### Example

-  ///

-  ///     new Observable.just(1)

-  ///         .zipWith(new Observable.just(2), (one, two) => one + two)

-  ///         .listen(print); // prints 3

-  Observable<R> zipWith<S, R>(Stream<S> other, R zipper(T t, S s)) =>

-      Observable<R>(ZipStream.zip2(_stream, other, zipper));

-

-  /// Convert the current Observable into a [ConnectableObservable]

-  /// that can be listened to multiple times. It will not begin emitting items

-  /// from the original Observable until the `connect` method is invoked.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// final source = Observable.fromIterable([1, 2, 3]);

-  /// final connectable = source.publish();

-  ///

-  /// // Does not print anything at first

-  /// connectable.listen(print);

-  ///

-  /// // Start listening to the source Observable. Will cause the previous

-  /// // line to start printing 1, 2, 3

-  /// final subscription = connectable.connect();

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // Subject

-  /// subscription.cancel();

-  /// ```

-  ConnectableObservable<T> publish() => PublishConnectableObservable<T>(this);

-

-  /// Convert the current Observable into a [ValueConnectableObservable]

-  /// that can be listened to multiple times. It will not begin emitting items

-  /// from the original Observable until the `connect` method is invoked.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream that replays the latest emitted value to any new

-  /// listener. It also provides access to the latest value synchronously.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// final source = Observable.fromIterable([1, 2, 3]);

-  /// final connectable = source.publishValue();

-  ///

-  /// // Does not print anything at first

-  /// connectable.listen(print);

-  ///

-  /// // Start listening to the source Observable. Will cause the previous

-  /// // line to start printing 1, 2, 3

-  /// final subscription = connectable.connect();

-  ///

-  /// // Late subscribers will receive the last emitted value

-  /// connectable.listen(print); // Prints 3

-  ///

-  /// // Can access the latest emitted value synchronously. Prints 3

-  /// print(connectable.value);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // BehaviorSubject

-  /// subscription.cancel();

-  /// ```

-  ValueConnectableObservable<T> publishValue() =>

-      ValueConnectableObservable<T>(this);

-

-  /// Convert the current Observable into a [ValueConnectableObservable]

-  /// that can be listened to multiple times, providing an initial seeded value.

-  /// It will not begin emitting items from the original Observable

-  /// until the `connect` method is invoked.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream that replays the latest emitted value to any new

-  /// listener. It also provides access to the latest value synchronously.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// final source = Observable.fromIterable([1, 2, 3]);

-  /// final connectable = source.publishValueSeeded(0);

-  ///

-  /// // Does not print anything at first

-  /// connectable.listen(print);

-  ///

-  /// // Start listening to the source Observable. Will cause the previous

-  /// // line to start printing 0, 1, 2, 3

-  /// final subscription = connectable.connect();

-  ///

-  /// // Late subscribers will receive the last emitted value

-  /// connectable.listen(print); // Prints 3

-  ///

-  /// // Can access the latest emitted value synchronously. Prints 3

-  /// print(connectable.value);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // BehaviorSubject

-  /// subscription.cancel();

-  /// ```

-  ValueConnectableObservable<T> publishValueSeeded(T seedValue) =>

-      ValueConnectableObservable<T>.seeded(this, seedValue);

-

-  /// Convert the current Observable into a [ReplayConnectableObservable]

-  /// that can be listened to multiple times. It will not begin emitting items

-  /// from the original Observable until the `connect` method is invoked.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream that replays a given number of items to any new

-  /// listener. It also provides access to the emitted values synchronously.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// final source = Observable.fromIterable([1, 2, 3]);

-  /// final connectable = source.publishReplay();

-  ///

-  /// // Does not print anything at first

-  /// connectable.listen(print);

-  ///

-  /// // Start listening to the source Observable. Will cause the previous

-  /// // line to start printing 1, 2, 3

-  /// final subscription = connectable.connect();

-  ///

-  /// // Late subscribers will receive the emitted value, up to a specified

-  /// // maxSize

-  /// connectable.listen(print); // Prints 1, 2, 3

-  ///

-  /// // Can access a list of the emitted values synchronously. Prints [1, 2, 3]

-  /// print(connectable.values);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // ReplaySubject

-  /// subscription.cancel();

-  /// ```

-  ReplayConnectableObservable<T> publishReplay({int maxSize}) =>

-      ReplayConnectableObservable<T>(this, maxSize: maxSize);

-

-  /// Convert the current Observable into a new Observable that can be listened

-  /// to multiple times. It will automatically begin emitting items when first

-  /// listened to, and shut down when no listeners remain.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// // Convert a single-subscription fromIterable stream into a broadcast

-  /// // stream

-  /// final observable = Observable.fromIterable([1, 2, 3]).share();

-  ///

-  /// // Start listening to the source Observable. Will start printing 1, 2, 3

-  /// final subscription = observable.listen(print);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // PublishSubject

-  /// subscription.cancel();

-  /// ```

-  Observable<T> share() => publish().refCount();

-

-  /// Convert the current Observable into a new [ValueObservable] that can

-  /// be listened to multiple times. It will automatically begin emitting items

-  /// when first listened to, and shut down when no listeners remain.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream. It's also useful for providing sync access to the latest

-  /// emitted value.

-  ///

-  /// It will replay the latest emitted value to any new listener.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// // Convert a single-subscription fromIterable stream into a broadcast

-  /// // stream that will emit the latest value to any new listeners

-  /// final observable = Observable.fromIterable([1, 2, 3]).shareValue();

-  ///

-  /// // Start listening to the source Observable. Will start printing 1, 2, 3

-  /// final subscription = observable.listen(print);

-  ///

-  /// // Synchronously print the latest value

-  /// print(observable.value);

-  ///

-  /// // Subscribe again later. This will print 3 because it receives the last

-  /// // emitted value.

-  /// final subscription2 = observable.listen(print);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // BehaviorSubject by cancelling all subscriptions.

-  /// subscription.cancel();

-  /// subscription2.cancel();

-  /// ```

-  ValueObservable<T> shareValue() => publishValue().refCount();

-

-  /// Convert the current Observable into a new [ValueObservable] that can

-  /// be listened to multiple times, providing an initial value.

-  /// It will automatically begin emitting items when first listened to,

-  /// and shut down when no listeners remain.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream. It's also useful for providing sync access to the latest

-  /// emitted value.

-  ///

-  /// It will replay the latest emitted value to any new listener.

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// // Convert a single-subscription fromIterable stream into a broadcast

-  /// // stream that will emit the latest value to any new listeners

-  /// final observable = Observable.fromIterable([1, 2, 3]).shareValueSeeded(0);

-  ///

-  /// // Start listening to the source Observable. Will start printing 0, 1, 2, 3

-  /// final subscription = observable.listen(print);

-  ///

-  /// // Synchronously print the latest value

-  /// print(observable.value);

-  ///

-  /// // Subscribe again later. This will print 3 because it receives the last

-  /// // emitted value.

-  /// final subscription2 = observable.listen(print);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // BehaviorSubject by cancelling all subscriptions.

-  /// subscription.cancel();

-  /// subscription2.cancel();

-  /// ```

-  ValueObservable<T> shareValueSeeded(T seedValue) =>

-      publishValueSeeded(seedValue).refCount();

-

-  /// Convert the current Observable into a new [ReplayObservable] that can

-  /// be listened to multiple times. It will automatically begin emitting items

-  /// when first listened to, and shut down when no listeners remain.

-  ///

-  /// This is useful for converting a single-subscription stream into a

-  /// broadcast Stream. It's also useful for gaining access to the l

-  ///

-  /// It will replay the emitted values to any new listener, up to a given

-  /// [maxSize].

-  ///

-  /// ### Example

-  ///

-  /// ```

-  /// // Convert a single-subscription fromIterable stream into a broadcast

-  /// // stream that will emit the latest value to any new listeners

-  /// final observable = Observable.fromIterable([1, 2, 3]).shareReplay();

-  ///

-  /// // Start listening to the source Observable. Will start printing 1, 2, 3

-  /// final subscription = observable.listen(print);

-  ///

-  /// // Synchronously print the emitted values up to a given maxSize

-  /// // Prints [1, 2, 3]

-  /// print(observable.values);

-  ///

-  /// // Subscribe again later. This will print 1, 2, 3 because it receives the

-  /// // last emitted value.

-  /// final subscription2 = observable.listen(print);

-  ///

-  /// // Stop emitting items from the source stream and close the underlying

-  /// // ReplaySubject by cancelling all subscriptions.

-  /// subscription.cancel();

-  /// subscription2.cancel();

-  /// ```

-  ReplayObservable<T> shareReplay({int maxSize}) =>

-      publishReplay(maxSize: maxSize).refCount();

-}

diff --git a/rxdart/lib/src/observables/replay_observable.dart b/rxdart/lib/src/observables/replay_observable.dart
deleted file mode 100755
index 0d61374..0000000
--- a/rxdart/lib/src/observables/replay_observable.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-import 'package:rxdart/src/observables/observable.dart';

-

-/// An [Observable] that provides synchronous access to the emitted values

-abstract class ReplayObservable<T> implements Observable<T> {

-  List<T> get values;

-}

diff --git a/rxdart/lib/src/observables/value_observable.dart b/rxdart/lib/src/observables/value_observable.dart
deleted file mode 100755
index 1460791..0000000
--- a/rxdart/lib/src/observables/value_observable.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-import 'package:rxdart/src/observables/observable.dart';

-

-/// An [Observable] that provides synchronous access to the last emitted item

-abstract class ValueObservable<T> implements Observable<T> {

-  /// Last emitted value, or null if there has been no emission yet

-  /// See [hasValue]

-  T get value;

-

-  bool get hasValue;

-}

diff --git a/rxdart/lib/src/samplers/buffer_strategy.dart b/rxdart/lib/src/samplers/buffer_strategy.dart
deleted file mode 100755
index a8eedb3..0000000
--- a/rxdart/lib/src/samplers/buffer_strategy.dart
+++ /dev/null
@@ -1,264 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/samplers/utils.dart';

-import 'package:rxdart/src/transformers/do.dart';

-import 'package:rxdart/src/transformers/sample.dart';

-import 'package:rxdart/src/transformers/take_until.dart';

-

-/// Higher order function signature which matches the method signature

-/// of buffer and window.

-typedef Stream<S> SamplerBuilder<T, S>(Stream<T> stream,

-    OnDataTransform<T, S> bufferHandler, OnDataTransform<S, S> scheduleHandler);

-

-/// Higher order function implementation for [_OnCountSampler]

-/// which matches the method signature of buffer and window.

-///

-/// Each item is a sequence containing the items

-/// from the source sequence, in batches of [count].

-///

-/// If [startBufferEvery] is provided, each group will start where the previous group

-/// ended minus the [startBufferEvery] value.

-Stream<S> Function(

-  Stream<T> stream,

-  OnDataTransform<T, S>,

-  OnDataTransform<S, S>,

-) onCount<T, S>(int count, [int startBufferEvery = 0]) => (

-      Stream<T> stream,

-      OnDataTransform<T, S> bufferHandler,

-      OnDataTransform<S, S> scheduleHandler,

-    ) {

-      return _OnCountSampler<T, S>(

-        stream,

-        bufferHandler,

-        scheduleHandler,

-        count,

-        startBufferEvery,

-      );

-    };

-

-/// Higher order function implementation for [_OnStreamSampler]

-/// which matches the method signature of buffer and window.

-///

-/// Each item is a sequence containing the items

-/// from the source sequence, sampled on [onStream].

-Stream<S> Function(

-  Stream<T> stream,

-  OnDataTransform<T, S>,

-  OnDataTransform<S, S>,

-) onStream<T, S, O>(Stream<O> onStream) {

-  return (

-    Stream<T> stream,

-    OnDataTransform<T, S> bufferHandler,

-    OnDataTransform<S, S> scheduleHandler,

-  ) {

-    return _OnStreamSampler<T, S, O>(

-      stream,

-      bufferHandler,

-      scheduleHandler,

-      onStream,

-    );

-  };

-}

-

-/// Higher order function implementation for [_OnStreamSampler]

-/// which matches the method signature of buffer and window.

-///

-/// Each item is a sequence containing the items

-/// from the source sequence, sampled on a time frame with [duration].

-Stream<S> Function(

-  Stream<T> stream,

-  OnDataTransform<T, S>,

-  OnDataTransform<S, S>,

-) onTime<T, S>(Duration duration) {

-  return (

-    Stream<T> stream,

-    OnDataTransform<T, S> bufferHandler,

-    OnDataTransform<S, S> scheduleHandler,

-  ) {

-    if (duration == null) {

-      throw ArgumentError('duration cannot be null');

-    }

-

-    return _OnStreamSampler<T, S, Null>(

-      stream,

-      bufferHandler,

-      scheduleHandler,

-      Stream<Null>.periodic(duration),

-    );

-  };

-}

-

-/// Higher order function implementation for [_OnStreamSampler]

-/// which matches the method signature of buffer and window.

-///

-/// Each item is a sequence containing the items

-/// from the source sequence, batched whenever [onFuture] completes.

-Stream<S> Function(

-  Stream<T> stream,

-  OnDataTransform<T, S>,

-  OnDataTransform<S, S>,

-) onFuture<T, S, O>(Future<O> onFuture()) {

-  return (

-    Stream<T> stream,

-    OnDataTransform<T, S> bufferHandler,

-    OnDataTransform<S, S> scheduleHandler,

-  ) {

-    if (onFuture == null) {

-      throw ArgumentError('onFuture cannot be null');

-    }

-

-    return _OnStreamSampler<T, S, O>(

-      stream,

-      bufferHandler,

-      scheduleHandler,

-      _onFutureSampler(onFuture),

-    );

-  };

-}

-

-/// transforms [onFuture] into a sampler [Stream] by recursively awaiting

-/// the next [Future]

-Stream<O> _onFutureSampler<O>(Future<O> onFuture()) async* {

-  yield await onFuture();

-  yield* _onFutureSampler(onFuture);

-}

-

-/// Higher order function implementation for [_OnTestSampler]

-/// which matches the method signature of buffer and window.

-///

-/// Each item is a sequence containing the items

-/// from the source sequence, batched whenever [onTest] passes.

-Stream<S> Function(

-        Stream<T> stream, OnDataTransform<T, S>, OnDataTransform<S, S>)

-    onTest<T, S>(bool onTest(T event)) => (Stream<T> stream,

-            OnDataTransform<T, S> bufferHandler,

-            OnDataTransform<S, S> scheduleHandler) =>

-        _OnTestSampler<T, S>(stream, bufferHandler, scheduleHandler, onTest);

-

-/// A buffer strategy where each item is a sequence containing the items

-/// from the source sequence, sampled on [onStream].

-class _OnStreamSampler<T, S, O> extends StreamView<S> {

-  @override

-  _OnStreamSampler._(Stream<S> state) : super(state);

-

-  factory _OnStreamSampler(

-      Stream<T> stream,

-      OnDataTransform<T, S> bufferHandler,

-      OnDataTransform<S, S> scheduleHandler,

-      Stream<O> onStream) {

-    final doneController = StreamController<bool>();

-    if (onStream == null) {

-      throw ArgumentError('onStream cannot be null');

-    }

-

-    final ticker = onStream.transform<dynamic>(

-        TakeUntilStreamTransformer<Null, dynamic>(doneController.stream));

-

-    void onDone() {

-      if (doneController.isClosed) return;

-

-      doneController.add(true);

-      doneController.close();

-    }

-

-    final scheduler = stream

-        .transform(DoStreamTransformer<T>(onDone: onDone, onCancel: onDone))

-        .transform(StreamTransformer<T, S>.fromHandlers(

-            handleData: (T data, EventSink<S> sink) {

-              bufferHandler(data, sink, 0);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)))

-        .transform(SampleStreamTransformer<S>(ticker, sampleOnValueOnly: false))

-        .transform(StreamTransformer<S, S>.fromHandlers(

-            handleData: (S data, EventSink<S> sink) {

-              scheduleHandler(data, sink, 0);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)));

-

-    return _OnStreamSampler<T, S, O>._(scheduler);

-  }

-}

-

-/// A buffer strategy where each item is a sequence containing the items

-/// from the source sequence, in batches of the specified count.

-///

-/// If [skip] is provided, each group will start where the previous group

-/// ended minus the [skip] value.

-class _OnCountSampler<T, S> extends StreamView<S> {

-  @override

-  _OnCountSampler._(Stream<S> state) : super(state);

-

-  factory _OnCountSampler(Stream<T> stream, OnDataTransform<T, S> bufferHandler,

-      OnDataTransform<S, S> scheduleHandler, int count,

-      [int startBufferEvery = 0]) {

-    var eventIndex = 0;

-

-    if (count == null) {

-      throw ArgumentError('count cannot be null');

-    } else if (count < 1) {

-      throw ArgumentError(

-          'count needs to be greater than 1, currently it is: $count');

-    }

-

-    if (startBufferEvery < 0) {

-      throw ArgumentError(

-          'startBufferEvery needs to be greater than, or equal to 0, currently it is: $startBufferEvery');

-    }

-

-    bool maybeNewBuffer(S _) => eventIndex % count == 0;

-

-    final scheduler = stream

-        .transform<S>(StreamTransformer<T, S>.fromHandlers(

-            handleData: (T data, EventSink<S> sink) {

-              if (++eventIndex > 0) bufferHandler(data, sink, startBufferEvery);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)))

-        .where(maybeNewBuffer)

-        .transform<S>(StreamTransformer<S, S>.fromHandlers(

-            handleData: (S data, EventSink<S> sink) {

-              eventIndex -= startBufferEvery;

-              scheduleHandler(data, sink, startBufferEvery);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)));

-

-    return _OnCountSampler<T, S>._(scheduler);

-  }

-}

-

-/// A buffer strategy where each item is a sequence containing the items

-/// from the source sequence, batched whenever [onTest] passes.

-class _OnTestSampler<T, S> extends StreamView<S> {

-  @override

-  _OnTestSampler._(Stream<S> state) : super(state);

-

-  factory _OnTestSampler(Stream<T> stream, OnDataTransform<T, S> bufferHandler,

-      OnDataTransform<S, S> scheduleHandler, bool onTest(T event)) {

-    var testResult = false;

-

-    if (onTest == null) {

-      throw ArgumentError('onTest cannot be null');

-    }

-

-    final scheduler = stream

-        .transform<S>(StreamTransformer<T, S>.fromHandlers(

-            handleData: (T data, EventSink<S> sink) {

-              testResult = onTest(data);

-              bufferHandler(data, sink, 0);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)))

-        .where((_) => testResult)

-        .transform<S>(StreamTransformer<S, S>.fromHandlers(

-            handleData: (S data, EventSink<S> sink) {

-              scheduleHandler(data, sink, 0);

-            },

-            handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-                sink.addError(error, s)));

-

-    return _OnTestSampler<T, S>._(scheduler);

-  }

-}

diff --git a/rxdart/lib/src/samplers/utils.dart b/rxdart/lib/src/samplers/utils.dart
deleted file mode 100755
index e6c9e60..0000000
--- a/rxdart/lib/src/samplers/utils.dart
+++ /dev/null
@@ -1,3 +0,0 @@
-import 'dart:async';

-

-typedef void OnDataTransform<T, S>(T event, EventSink<S> sink, [int skip]);

diff --git a/rxdart/lib/src/streams/amb.dart b/rxdart/lib/src/streams/amb.dart
deleted file mode 100755
index 129a27f..0000000
--- a/rxdart/lib/src/streams/amb.dart
+++ /dev/null
@@ -1,86 +0,0 @@
-import 'dart:async';

-

-/// Deprecated: Use RaceStream

-///

-/// Given two or more source streams, emit all of the items from only

-/// the first of these streams to emit an item or notification.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#amb)

-///

-/// ### Example

-///

-///     new AmbStream([

-///       new TimerStream(1, new Duration(days: 1)),

-///       new TimerStream(2, new Duration(days: 2)),

-///       new TimerStream(3, new Duration(seconds: 3))

-///     ]).listen(print); // prints 3

-@deprecated

-class AmbStream<T> extends Stream<T> {

-  final StreamController<T> controller;

-

-  AmbStream(Iterable<Stream<T>> streams)

-      : controller = _buildController(streams);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static StreamController<T> _buildController<T>(Iterable<Stream<T>> streams) {

-    if (streams == null) {

-      throw ArgumentError('streams cannot be null');

-    } else if (streams.isEmpty) {

-      throw ArgumentError('provide at least 1 stream');

-    }

-

-    final subscriptions = <StreamSubscription<T>>[];

-    var isDisambiguated = false;

-

-    StreamController<T> controller;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          void doUpdate(int i, T value) {

-            try {

-              if (!isDisambiguated)

-                for (var k = subscriptions.length - 1; k >= 0; k--) {

-                  if (k != i) {

-                    subscriptions[k].cancel();

-                    subscriptions.removeAt(k);

-                  }

-                }

-

-              isDisambiguated = true;

-

-              controller.add(value);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          }

-

-          for (var i = 0, len = streams.length; i < len; i++) {

-            var stream = streams.elementAt(i);

-

-            subscriptions.add(stream.listen((value) => doUpdate(i, value),

-                onError: controller.addError,

-                onDone: () => controller.close()));

-          }

-        },

-        onPause: ([Future<dynamic> resumeSignal]) => subscriptions.forEach(

-            (StreamSubscription<T> subscription) =>

-                subscription.pause(resumeSignal)),

-        onResume: () => subscriptions.forEach(

-            (StreamSubscription<T> subscription) => subscription.resume()),

-        onCancel: () => Future.wait<dynamic>(

-                subscriptions.map((StreamSubscription<T> subscription) {

-              if (subscription != null) return subscription.cancel();

-

-              return Future<dynamic>.value();

-            }).where((Future<dynamic> cancelFuture) => cancelFuture != null)));

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/combine_latest.dart b/rxdart/lib/src/streams/combine_latest.dart
deleted file mode 100755
index ca647e8..0000000
--- a/rxdart/lib/src/streams/combine_latest.dart
+++ /dev/null
@@ -1,314 +0,0 @@
-import 'dart:async';

-

-/// Merges the given Streams into one Stream sequence by using the

-/// combiner function whenever any of the source stream sequences emits an

-/// item.

-///

-/// The Stream will not emit until all Streams have emitted at least one

-/// item.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#combineLatest)

-///

-/// ### Basic Example

-///

-/// This constructor takes in an `Iterable<Stream<T>>` and outputs a

-/// `Stream<Iterable<T>>` whenever any of the values change from the source

-/// stream. This is useful with a dynamic number of source streams!

-///

-///     CombineLatestStream.list<String>([

-///       Stream.fromIterable(["a"]),

-///       Stream.fromIterable(["b"]),

-///       Stream.fromIterable(["C", "D"])])

-///     .listen(print); //prints ['a', 'b', 'C'], ['a', 'b', 'D']

-///

-/// ### Example with combiner

-///

-/// If you wish to combine the list of values into a new object before you

-///

-///     CombineLatestStream(

-///       [

-///         Stream.fromIterable(["a"]),

-///         Stream.fromIterable(["b"]),

-///         Stream.fromIterable(["C", "D"])

-///       ],

-///       (values) => values.last

-///     )

-///     .listen(print); //prints 'C', 'D'

-///

-/// ### Example with a specific number of Streams

-///

-/// If you wish to combine a specific number of Streams together with proper

-/// types information for the value of each Stream, use the

-/// [combine2] - [combine9] operators.

-///

-///     CombineLatestStream.combine2(

-///       Stream.fromIterable(1),

-///       Stream.fromIterable([2, 3]),

-///       (a, b) => a + b,

-///     )

-///     .listen(print); // prints 3, 4

-class CombineLatestStream<T, R> extends StreamView<R> {

-  CombineLatestStream(

-    Iterable<Stream<T>> streams,

-    R combiner(List<T> values),

-  )   : assert(streams != null && streams.every((s) => s != null),

-            'streams cannot be null'),

-        assert(streams.length > 1, 'provide at least 2 streams'),

-        assert(combiner != null, 'must provide a combiner function'),

-        super(_buildController(streams, combiner).stream);

-

-  static CombineLatestStream<T, List<T>> list<T>(

-    Iterable<Stream<T>> streams,

-  ) {

-    return CombineLatestStream<T, List<T>>(

-      streams,

-      (List<T> values) => values,

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine2<A, B, R>(

-    Stream<A> streamOne,

-    Stream<B> streamTwo,

-    R combiner(A a, B b),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamOne, streamTwo],

-      (List<dynamic> values) => combiner(values[0] as A, values[1] as B),

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine3<A, B, C, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    R combiner(A a, B b, C c),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine4<A, B, C, D, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    R combiner(A a, B b, C c, D d),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine5<A, B, C, D, E, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    R combiner(A a, B b, C c, D d, E e),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine6<A, B, C, D, E, F, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    R combiner(A a, B b, C c, D d, E e, F f),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine7<A, B, C, D, E, F, G, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    R combiner(A a, B b, C c, D d, E e, F f, G g),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF, streamG],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine8<A, B, C, D, E, F, G, H, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    Stream<H> streamH,

-    R combiner(A a, B b, C c, D d, E e, F f, G g, H h),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF, streamG, streamH],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-          values[7] as H,

-        );

-      },

-    );

-  }

-

-  static CombineLatestStream<dynamic, R> combine9<A, B, C, D, E, F, G, H, I, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    Stream<H> streamH,

-    Stream<I> streamI,

-    R combiner(A a, B b, C c, D d, E e, F f, G g, H h, I i),

-  ) {

-    return CombineLatestStream<dynamic, R>(

-      [

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        streamG,

-        streamH,

-        streamI

-      ],

-      (List<dynamic> values) {

-        return combiner(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-          values[7] as H,

-          values[8] as I,

-        );

-      },

-    );

-  }

-

-  static StreamController<R> _buildController<T, R>(

-    Iterable<Stream<T>> streams,

-    R combiner(List<T> values),

-  ) {

-    final subscriptions = List<StreamSubscription<dynamic>>(streams.length);

-    StreamController<R> controller;

-

-    controller = StreamController<R>(

-      sync: true,

-      onListen: () {

-        final values = List<T>(streams.length);

-        final triggered = List.generate(streams.length, (_) => false);

-        final completedStatus = List.generate(streams.length, (_) => false);

-        var allStreamsHaveEvents = false;

-

-        for (var i = 0, len = streams.length; i < len; i++) {

-          final stream = streams.elementAt(i);

-

-          subscriptions[i] = stream.listen(

-            (T value) {

-              values[i] = value;

-              triggered[i] = true;

-

-              if (!allStreamsHaveEvents)

-                allStreamsHaveEvents = triggered.every((t) => t);

-

-              if (allStreamsHaveEvents) {

-                try {

-                  controller.add(combiner(values.toList()));

-                } catch (e, s) {

-                  controller.addError(e, s);

-                }

-              }

-            },

-            onError: controller.addError,

-            onDone: () {

-              completedStatus[i] = true;

-

-              if (completedStatus.every((c) => c)) controller.close();

-            },

-          );

-        }

-      },

-      onPause: ([Future<dynamic> resumeSignal]) => subscriptions.forEach(

-          (StreamSubscription<dynamic> subscription) =>

-              subscription.pause(resumeSignal)),

-      onResume: () => subscriptions.forEach(

-          (StreamSubscription<dynamic> subscription) => subscription.resume()),

-      onCancel: () => Future.wait<dynamic>(subscriptions

-          .map((StreamSubscription<dynamic> subscription) =>

-              subscription.cancel())

-          .where((Future<dynamic> cancelFuture) => cancelFuture != null)),

-    );

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/concat.dart b/rxdart/lib/src/streams/concat.dart
deleted file mode 100755
index 29b2a9a..0000000
--- a/rxdart/lib/src/streams/concat.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-import 'dart:async';

-

-/// Concatenates all of the specified stream sequences, as long as the

-/// previous stream sequence terminated successfully.

-///

-/// It does this by subscribing to each stream one by one, emitting all items

-/// and completing before subscribing to the next stream.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#concat)

-///

-/// ### Example

-///

-///     new ConcatStream([

-///       new Stream.fromIterable([1]),

-///       new TimerStream(2, new Duration(days: 1)),

-///       new Stream.fromIterable([3])

-///     ])

-///     .listen(print); // prints 1, 2, 3

-class ConcatStream<T> extends Stream<T> {

-  final StreamController<T> controller;

-

-  ConcatStream(Iterable<Stream<T>> streams)

-      : controller = _buildController(streams);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static StreamController<T> _buildController<T>(Iterable<Stream<T>> streams) {

-    if (streams == null) {

-      throw ArgumentError('Streams cannot be null');

-    } else if (streams.isEmpty) {

-      throw ArgumentError('At least 1 stream needs to be provided');

-    } else if (streams.any((Stream<T> stream) => stream == null)) {

-      throw ArgumentError('One of the provided streams is null');

-    }

-

-    StreamController<T> controller;

-    StreamSubscription<T> subscription;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          final len = streams.length;

-          var index = 0;

-

-          void moveNext() {

-            var stream = streams.elementAt(index);

-            subscription?.cancel();

-

-            subscription = stream.listen(controller.add,

-                onError: controller.addError, onDone: () {

-              index++;

-

-              if (index == len)

-                controller.close();

-              else

-                moveNext();

-            });

-          }

-

-          moveNext();

-        },

-        onPause: ([Future<dynamic> resumeSignal]) =>

-            subscription?.pause(resumeSignal),

-        onResume: () => subscription?.resume(),

-        onCancel: () => subscription.cancel());

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/concat_eager.dart b/rxdart/lib/src/streams/concat_eager.dart
deleted file mode 100755
index 841b86b..0000000
--- a/rxdart/lib/src/streams/concat_eager.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-import 'dart:async';

-

-/// Concatenates all of the specified stream sequences, as long as the

-/// previous stream sequence terminated successfully.

-///

-/// In the case of concatEager, rather than subscribing to one stream after

-/// the next, all streams are immediately subscribed to. The events are then

-/// captured and emitted at the correct time, after the previous stream has

-/// finished emitting items.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#concat)

-///

-/// ### Example

-///

-///     new ConcatEagerStream([

-///       new Stream.fromIterable([1]),

-///       new TimerStream(2, new Duration(days: 1)),

-///       new Stream.fromIterable([3])

-///     ])

-///     .listen(print); // prints 1, 2, 3

-class ConcatEagerStream<T> extends Stream<T> {

-  final StreamController<T> controller;

-

-  ConcatEagerStream(Iterable<Stream<T>> streams)

-      : controller = _buildController(streams);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static StreamController<T> _buildController<T>(Iterable<Stream<T>> streams) {

-    if (streams == null) {

-      throw ArgumentError('streams cannot be null');

-    } else if (streams.isEmpty) {

-      throw ArgumentError('at least 1 stream needs to be provided');

-    } else if (streams.any((Stream<T> stream) => stream == null)) {

-      throw ArgumentError('One of the provided streams is null');

-    }

-

-    final subscriptions = List<StreamSubscription<T>>(streams.length);

-    final completeEvents =

-        streams != null ? List<Completer<dynamic>>(streams.length) : null;

-    StreamController<T> controller;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          for (var i = 0, len = streams.length; i < len; i++) {

-            completeEvents[i] = Completer<dynamic>();

-

-            subscriptions[i] = streams.elementAt(i).listen(controller.add,

-                onError: controller.addError, onDone: () {

-              completeEvents[i].complete();

-

-              if (i == len - 1) controller.close();

-            });

-

-            if (i > 0) subscriptions[i].pause(completeEvents[i - 1].future);

-          }

-        },

-        onPause: ([Future<dynamic> resumeSignal]) => subscriptions.forEach(

-            (StreamSubscription<T> subscription) =>

-                subscription.pause(resumeSignal)),

-        onResume: () => subscriptions.forEach(

-            (StreamSubscription<T> subscription) => subscription.resume()),

-        onCancel: () => Future.wait<dynamic>(subscriptions

-            .map((StreamSubscription<T> subscription) => subscription.cancel())

-            .where((Future<dynamic> cancelFuture) => cancelFuture != null)));

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/defer.dart b/rxdart/lib/src/streams/defer.dart
deleted file mode 100755
index 06557a0..0000000
--- a/rxdart/lib/src/streams/defer.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/streams/utils.dart';

-

-/// The defer factory waits until an observer subscribes to it, and then it

-/// creates an Observable with the given factory function.

-///

-/// In some circumstances, waiting until the last minute (that is, until

-/// subscription time) to generate the Observable can ensure that this

-/// Observable contains the freshest data.

-///

-/// By default, DeferStreams are single-subscription. However, it's possible

-/// to make them reusable.

-///

-/// ### Example

-///

-///     new DeferStream(() => new Observable.just(1)).listen(print); //prints 1

-class DeferStream<T> extends Stream<T> {

-  final StreamFactory<T> _streamFactory;

-  final bool _isReusable;

-  bool _isUsed = false;

-

-  @override

-  bool get isBroadcast => _isReusable;

-

-  DeferStream(this._streamFactory, {bool reusable = false})

-      : _isReusable = reusable;

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    if (_isUsed && !_isReusable)

-      throw StateError("Stream has already been listened to.");

-    _isUsed = true;

-

-    return _streamFactory().listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-}

diff --git a/rxdart/lib/src/streams/error.dart b/rxdart/lib/src/streams/error.dart
deleted file mode 100755
index 40b5e15..0000000
--- a/rxdart/lib/src/streams/error.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-import 'dart:async';

-

-/// Returns an observable sequence that emits an [error], then immediately

-/// completes.

-///

-/// The error operator is one with very specific and limited behavior. It is

-/// mostly useful for testing purposes.

-///

-/// ### Example

-///

-///     new ErrorStream(new ArgumentError());

-class ErrorStream<T> extends Stream<T> {

-  final Object error;

-  StreamController<T> controller = StreamController<T>();

-

-  ErrorStream(this.error);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    controller.addError(error);

-

-    controller.close();

-

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-}

diff --git a/rxdart/lib/src/streams/merge.dart b/rxdart/lib/src/streams/merge.dart
deleted file mode 100755
index fb08c3e..0000000
--- a/rxdart/lib/src/streams/merge.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-import 'dart:async';

-

-/// Flattens the items emitted by the given streams into a single Observable

-/// sequence.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#merge)

-///

-/// ### Example

-///

-///     new MergeStream([

-///       new TimerStream(1, new Duration(days: 10)),

-///       new Stream.fromIterable([2])

-///     ])

-///     .listen(print); // prints 2, 1

-class MergeStream<T> extends Stream<T> {

-  final StreamController<T> controller;

-

-  MergeStream(Iterable<Stream<T>> streams)

-      : controller = _buildController(streams);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static StreamController<T> _buildController<T>(Iterable<Stream<T>> streams) {

-    if (streams == null) {

-      throw ArgumentError('streams cannot be null');

-    } else if (streams.isEmpty) {

-      throw ArgumentError('at least 1 stream needs to be provided');

-    } else if (streams.any((Stream<T> stream) => stream == null)) {

-      throw ArgumentError('One of the provided streams is null');

-    }

-

-    final subscriptions = List<StreamSubscription<T>>(streams.length);

-    StreamController<T> controller;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          final completedStatus = List.generate(streams.length, (_) => false);

-

-          for (var i = 0, len = streams.length; i < len; i++) {

-            var stream = streams.elementAt(i);

-

-            subscriptions[i] = stream.listen(controller.add,

-                onError: controller.addError, onDone: () {

-              completedStatus[i] = true;

-

-              if (completedStatus.reduce((a, b) => a && b)) controller.close();

-            });

-          }

-        },

-        onPause: ([Future<dynamic> resumeSignal]) => subscriptions.forEach(

-            (StreamSubscription<T> subscription) =>

-                subscription.pause(resumeSignal)),

-        onResume: () => subscriptions.forEach(

-            (StreamSubscription<T> subscription) => subscription.resume()),

-        onCancel: () => Future.wait<dynamic>(subscriptions

-            .map((StreamSubscription<T> subscription) => subscription.cancel())

-            .where((Future<dynamic> cancelFuture) => cancelFuture != null)));

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/never.dart b/rxdart/lib/src/streams/never.dart
deleted file mode 100755
index d8bbddc..0000000
--- a/rxdart/lib/src/streams/never.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-import 'dart:async';

-

-/// Returns a non-terminating observable sequence, which can be used to denote

-/// an infinite duration.

-///

-/// The never operator is one with very specific and limited behavior. These

-/// are useful for testing purposes, and sometimes also for combining with

-/// other Observables or as parameters to operators that expect other

-/// Observables as parameters.

-///

-/// ### Example

-///

-///     new NeverStream().listen(print); // Neither prints nor terminates

-class NeverStream<T> extends Stream<T> {

-  // ignore: close_sinks

-  StreamController<T> controller = StreamController<T>();

-

-  NeverStream();

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-}

diff --git a/rxdart/lib/src/streams/race.dart b/rxdart/lib/src/streams/race.dart
deleted file mode 100755
index 7b109a7..0000000
--- a/rxdart/lib/src/streams/race.dart
+++ /dev/null
@@ -1,83 +0,0 @@
-import 'dart:async';

-

-/// Given two or more source streams, emit all of the items from only

-/// the first of these streams to emit an item or notification.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#amb)

-///

-/// ### Example

-///

-///     new RaceStream([

-///       new TimerStream(1, new Duration(days: 1)),

-///       new TimerStream(2, new Duration(days: 2)),

-///       new TimerStream(3, new Duration(seconds: 3))

-///     ]).listen(print); // prints 3

-class RaceStream<T> extends Stream<T> {

-  final StreamController<T> controller;

-

-  RaceStream(Iterable<Stream<T>> streams)

-      : controller = _buildController(streams);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static StreamController<T> _buildController<T>(Iterable<Stream<T>> streams) {

-    if (streams == null) {

-      throw ArgumentError('streams cannot be null');

-    } else if (streams.isEmpty) {

-      throw ArgumentError('provide at least 1 stream');

-    }

-

-    final subscriptions = <StreamSubscription<T>>[];

-    var isDisambiguated = false;

-

-    StreamController<T> controller;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          void doUpdate(int i, T value) {

-            try {

-              if (!isDisambiguated)

-                for (var k = subscriptions.length - 1; k >= 0; k--) {

-                  if (k != i) {

-                    subscriptions[k].cancel();

-                    subscriptions.removeAt(k);

-                  }

-                }

-

-              isDisambiguated = true;

-

-              controller.add(value);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          }

-

-          for (var i = 0, len = streams.length; i < len; i++) {

-            var stream = streams.elementAt(i);

-

-            subscriptions.add(stream.listen((T value) => doUpdate(i, value),

-                onError: controller.addError,

-                onDone: () => controller.close()));

-          }

-        },

-        onPause: ([Future<dynamic> resumeSignal]) => subscriptions.forEach(

-            (StreamSubscription<T> subscription) =>

-                subscription.pause(resumeSignal)),

-        onResume: () => subscriptions.forEach(

-            (StreamSubscription<T> subscription) => subscription.resume()),

-        onCancel: () => Future.wait<dynamic>(

-                subscriptions.map((StreamSubscription<T> subscription) {

-              if (subscription != null) return subscription.cancel();

-

-              return Future<dynamic>.value();

-            }).where((Future<dynamic> cancelFuture) => cancelFuture != null)));

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/range.dart b/rxdart/lib/src/streams/range.dart
deleted file mode 100755
index 1bd59d1..0000000
--- a/rxdart/lib/src/streams/range.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'dart:async';

-

-/// Returns a Stream that emits a sequence of Integers within a specified

-/// range.

-///

-/// ### Examples

-///

-///     new RangeStream(1, 3).listen((i) => print(i)); // Prints 1, 2, 3

-///

-///     new RangeStream(3, 1).listen((i) => print(i)); // Prints 3, 2, 1

-class RangeStream extends Stream<int> {

-  final Stream<int> stream;

-

-  RangeStream(int startInclusive, int endInclusive)

-      : stream = buildStream(startInclusive, endInclusive);

-

-  @override

-  StreamSubscription<int> listen(void onData(int event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    return stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-  }

-

-  static Stream<int> buildStream(int startInclusive, int endInclusive) {

-    final length = (endInclusive - startInclusive).abs() + 1;

-

-    return Stream.fromIterable(List.generate(

-        length,

-        (int i) => startInclusive > endInclusive

-            ? startInclusive - i

-            : startInclusive + i));

-  }

-}

diff --git a/rxdart/lib/src/streams/repeat.dart b/rxdart/lib/src/streams/repeat.dart
deleted file mode 100755
index d86bfdc..0000000
--- a/rxdart/lib/src/streams/repeat.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'dart:async';

-

-/// Creates a [Stream] that will recreate and re-listen to the source

-/// Stream the specified number of times until the [Stream] terminates

-/// successfully.

-///

-/// If [count] is not specified, it repeats indefinitely.

-///

-/// ### Example

-///

-///     new RepeatStream((int repeatCount) =>

-///       Observable.just('repeat index: $repeatCount'), 3)

-///         .listen((i) => print(i)); // Prints 'repeat index: 0, repeat index: 1, repeat index: 2'

-class RepeatStream<T> extends Stream<T> {

-  final Stream<T> Function(int) streamFactory;

-  final int count;

-  int repeatStep = 0;

-  StreamController<T> controller;

-  StreamSubscription<T> subscription;

-  bool _isUsed = false;

-

-  RepeatStream(this.streamFactory, [this.count]);

-

-  @override

-  StreamSubscription<T> listen(

-    void onData(T event), {

-    Function onError,

-    void onDone(),

-    bool cancelOnError,

-  }) {

-    if (_isUsed) throw StateError("Stream has already been listened to.");

-    _isUsed = true;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: maybeRepeatNext,

-        onPause: ([Future<dynamic> resumeSignal]) =>

-            subscription.pause(resumeSignal),

-        onResume: () => subscription.resume(),

-        onCancel: () => subscription?.cancel());

-

-    return controller.stream.listen(

-      onData,

-      onError: onError,

-      onDone: onDone,

-      cancelOnError: cancelOnError,

-    );

-  }

-

-  void repeatNext() {

-    void onDone() {

-      subscription?.cancel();

-

-      maybeRepeatNext();

-    }

-

-    try {

-      subscription = streamFactory(repeatStep++).listen(controller.add,

-          onError: controller.addError, onDone: onDone, cancelOnError: false);

-    } catch (e, s) {

-      controller.addError(e, s);

-    }

-  }

-

-  void maybeRepeatNext() {

-    if (repeatStep == count) {

-      controller.close();

-    } else {

-      repeatNext();

-    }

-  }

-}

diff --git a/rxdart/lib/src/streams/retry.dart b/rxdart/lib/src/streams/retry.dart
deleted file mode 100755
index ed4a7aa..0000000
--- a/rxdart/lib/src/streams/retry.dart
+++ /dev/null
@@ -1,79 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/streams/utils.dart';

-

-/// Creates a Stream that will recreate and re-listen to the source

-/// Stream the specified number of times until the Stream terminates

-/// successfully.

-///

-/// If the retry count is not specified, it retries indefinitely. If the retry

-/// count is met, but the Stream has not terminated successfully, a

-/// `RetryError` will be thrown. The RetryError will contain all of the Errors

-/// and StackTraces that caused the failure.

-///

-/// ### Example

-///

-///     new RetryStream(() { new Stream.fromIterable([1]); })

-///         .listen((i) => print(i)); // Prints 1

-///

-///     new RetryStream(() {

-///          new Stream.fromIterable([1])

-///             .concatWith([new ErrorStream(new Error())]);

-///        }, 1)

-///        .listen(print, onError: (e, s) => print(e)); // Prints 1, 1, RetryError

-class RetryStream<T> extends Stream<T> {

-  final StreamFactory<T> streamFactory;

-  int count;

-  int retryStep = 0;

-  StreamController<T> controller;

-  StreamSubscription<T> subscription;

-  bool _isUsed = false;

-  List<ErrorAndStacktrace> _errors = <ErrorAndStacktrace>[];

-

-  RetryStream(this.streamFactory, [this.count]);

-

-  @override

-  StreamSubscription<T> listen(

-    void onData(T event), {

-    Function onError,

-    void onDone(),

-    bool cancelOnError,

-  }) {

-    if (_isUsed) throw StateError("Stream has already been listened to.");

-    _isUsed = true;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: retry,

-        onPause: ([Future<dynamic> resumeSignal]) =>

-            subscription.pause(resumeSignal),

-        onResume: () => subscription.resume(),

-        onCancel: () => subscription.cancel());

-

-    return controller.stream.listen(

-      onData,

-      onError: onError,

-      onDone: onDone,

-      cancelOnError: cancelOnError,

-    );

-  }

-

-  void retry() {

-    subscription = streamFactory().listen(controller.add,

-        onError: (dynamic e, StackTrace s) {

-      subscription.cancel();

-

-      if (count == retryStep) {

-        controller.addError(RetryError(

-          'Received an error after attempting $count retries',

-          _errors..add(ErrorAndStacktrace(e, s)),

-        ));

-        controller.close();

-      } else {

-        retryStep++;

-        _errors.add(ErrorAndStacktrace(e, s));

-        retry();

-      }

-    }, onDone: controller.close, cancelOnError: false);

-  }

-}

diff --git a/rxdart/lib/src/streams/retry_when.dart b/rxdart/lib/src/streams/retry_when.dart
deleted file mode 100755
index 18be27c..0000000
--- a/rxdart/lib/src/streams/retry_when.dart
+++ /dev/null
@@ -1,126 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/streams/utils.dart';

-

-/// Creates a Stream that will recreate and re-listen to the source

-/// Stream when the notifier emits a new value. If the source Stream

-/// emits an error or it completes, the Stream terminates.

-///

-/// If the [retryWhenFactory] emits an error a [RetryError] will be

-/// thrown. The RetryError will contain all of the [Error]s and

-/// [StackTrace]s that caused the failure.

-///

-/// ### Basic Example

-/// ```dart

-/// new RetryWhenStream<int>(

-///   () => new Stream<int>.fromIterable(<int>[1]),

-///   (dynamic error, StackTrace s) => throw error,

-/// ).listen(print); // Prints 1

-/// ```

-///

-/// ### Periodic Example

-/// ```dart

-/// new RetryWhenStream<int>(

-///   () => new Observable<int>

-///       .periodic(const Duration(seconds: 1), (int i) => i)

-///       .map((int i) => i == 2 ? throw 'exception' : i),

-///   (dynamic e, StackTrace s) {

-///     return new Observable<String>

-///         .timer('random value', const Duration(milliseconds: 200));

-///   },

-/// ).take(4).listen(print); // Prints 0, 1, 0, 1

-/// ```

-///

-/// ### Complex Example

-/// ```dart

-/// bool errorHappened = false;

-/// new RetryWhenStream(

-///   () => new Observable

-///       .periodic(const Duration(seconds: 1), (i) => i)

-///       .map((i) {

-///         if (i == 3 && !errorHappened) {

-///           throw 'We can take this. Please restart.';

-///         } else if (i == 4) {

-///           throw 'It\'s enough.';

-///         } else {

-///           return i;

-///         }

-///       }),

-///   (e, s) {

-///     errorHappened = true;

-///     if (e == 'We can take this. Please restart.') {

-///       return new Observable.just('Ok. Here you go!');

-///     } else {

-///       return new Observable.error(e);

-///     }

-///   },

-/// ).listen(

-///   print,

-///   onError: (e, s) => print(e),

-/// ); // Prints 0, 1, 2, 0, 1, 2, 3, RetryError

-/// ```

-class RetryWhenStream<T> extends Stream<T> {

-  final StreamFactory<T> streamFactory;

-  final RetryWhenStreamFactory retryWhenFactory;

-  StreamController<T> controller;

-  StreamSubscription<T> subscription;

-  bool _isUsed = false;

-  List<ErrorAndStacktrace> _errors = <ErrorAndStacktrace>[];

-

-  RetryWhenStream(this.streamFactory, this.retryWhenFactory);

-

-  @override

-  StreamSubscription<T> listen(

-    void onData(T event), {

-    Function onError,

-    void onDone(),

-    bool cancelOnError,

-  }) {

-    if (_isUsed) throw StateError('Stream has already been listened to.');

-    _isUsed = true;

-

-    controller = StreamController<T>(

-      sync: true,

-      onListen: retry,

-      onPause: ([Future<dynamic> resumeSignal]) =>

-          subscription.pause(resumeSignal),

-      onResume: () => subscription.resume(),

-      onCancel: () => subscription.cancel(),

-    );

-

-    return controller.stream.listen(

-      onData,

-      onError: onError,

-      onDone: onDone,

-      cancelOnError: cancelOnError,

-    );

-  }

-

-  void retry() {

-    subscription = streamFactory().listen(

-      controller.add,

-      onError: (dynamic e, StackTrace s) {

-        subscription.cancel();

-

-        StreamSubscription<void> sub;

-        sub = retryWhenFactory(e, s).listen(

-          (dynamic event) {

-            sub.cancel();

-            _errors.add(ErrorAndStacktrace(e, s));

-            retry();

-          },

-          onError: (dynamic e, StackTrace s) {

-            sub.cancel();

-            controller.addError(RetryError(

-              'Received an error after attempting to retry.',

-              _errors..add(ErrorAndStacktrace(e, s)),

-            ));

-            controller.close();

-          },

-        );

-      },

-      onDone: controller.close,

-      cancelOnError: false,

-    );

-  }

-}

diff --git a/rxdart/lib/src/streams/switch_latest.dart b/rxdart/lib/src/streams/switch_latest.dart
deleted file mode 100755
index 5280870..0000000
--- a/rxdart/lib/src/streams/switch_latest.dart
+++ /dev/null
@@ -1,112 +0,0 @@
-import 'dart:async';

-

-/// Convert a Stream that emits Streams (aka a "Higher Order Stream") into a

-/// single Stream that emits the items emitted by the most-recently-emitted of

-/// those Streams.

-///

-/// This stream will unsubscribe from the previously-emitted Stream when a new

-/// Stream is emitted from the source Stream.

-///

-/// ### Example

-///

-/// ```dart

-/// final switchLatestStream = new SwitchLatestStream<String>(

-///   new Stream.fromIterable(<Stream<String>>[

-///     new Observable.timer('A', new Duration(seconds: 2)),

-///     new Observable.timer('B', new Duration(seconds: 1)),

-///     new Observable.just('C'),

-///   ]),

-/// );

-///

-/// // Since the first two Streams do not emit data for 1-2 seconds, and the 3rd

-/// // Stream will be emitted before that time, only data from the 3rd Stream

-/// // will be emitted to the listener.

-/// switchLatestStream.listen(print); // prints 'C'

-/// ```

-class SwitchLatestStream<T> extends Stream<T> {

-  final Stream<Stream<T>> streams;

-

-  // ignore: close_sinks

-  StreamController<T> _controller;

-

-  SwitchLatestStream(this.streams);

-

-  @override

-  StreamSubscription<T> listen(

-    void onData(T event), {

-    Function onError,

-    void onDone(),

-    bool cancelOnError,

-  }) {

-    if (_controller == null) {

-      _controller = _buildController(streams, cancelOnError);

-    }

-

-    return _controller.stream.listen(

-      onData,

-      onError: onError,

-      onDone: onDone,

-      cancelOnError: cancelOnError,

-    );

-  }

-

-  static StreamController<T> _buildController<T>(

-    Stream<Stream<T>> streams,

-    bool cancelOnError,

-  ) {

-    StreamController<T> controller;

-    StreamSubscription<Stream<T>> subscription;

-    StreamSubscription<T> otherSubscription;

-    var leftClosed = false, rightClosed = false, hasMainEvent = false;

-

-    controller = StreamController<T>(

-        sync: true,

-        onListen: () {

-          subscription = streams.listen(

-            (Stream<T> value) {

-              try {

-                otherSubscription?.cancel();

-

-                hasMainEvent = true;

-

-                otherSubscription = value.listen(

-                  controller.add,

-                  onError: controller.addError,

-                  onDone: () {

-                    rightClosed = true;

-

-                    if (leftClosed) controller.close();

-                  },

-                );

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-            onError: controller.addError,

-            onDone: () {

-              leftClosed = true;

-

-              if (rightClosed || !hasMainEvent) {

-                controller.close();

-              }

-            },

-            cancelOnError: cancelOnError,

-          );

-        },

-        onPause: ([Future<dynamic> resumeSignal]) {

-          subscription.pause(resumeSignal);

-          otherSubscription?.pause(resumeSignal);

-        },

-        onResume: () {

-          subscription.resume();

-          otherSubscription?.resume();

-        },

-        onCancel: () async {

-          await subscription.cancel();

-

-          if (hasMainEvent) await otherSubscription.cancel();

-        });

-

-    return controller;

-  }

-}

diff --git a/rxdart/lib/src/streams/timer.dart b/rxdart/lib/src/streams/timer.dart
deleted file mode 100755
index 2d65800..0000000
--- a/rxdart/lib/src/streams/timer.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-import 'dart:async';

-

-/// Emits the given value after a specified amount of time.

-///

-/// ### Example

-///

-///     new TimerStream("hi", new Duration(minutes: 1))

-///         .listen((i) => print(i)); // print "hi" after 1 minute

-class TimerStream<T> extends Stream<T> {

-  final T value;

-  final Duration duration;

-  final StreamController<T> controller = StreamController<T>();

-

-  TimerStream(this.value, this.duration);

-

-  @override

-  StreamSubscription<T> listen(void onData(T event),

-      {Function onError, void onDone(), bool cancelOnError}) {

-    final subscription = controller.stream.listen(onData,

-        onError: onError, onDone: onDone, cancelOnError: cancelOnError);

-

-    Timer(duration, () {

-      controller.add(value);

-      controller.close();

-    });

-

-    return subscription;

-  }

-}

diff --git a/rxdart/lib/src/streams/utils.dart b/rxdart/lib/src/streams/utils.dart
deleted file mode 100755
index 830302d..0000000
--- a/rxdart/lib/src/streams/utils.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-import 'dart:async';

-

-typedef Stream<T> StreamFactory<T>();

-typedef Stream<void> RetryWhenStreamFactory(dynamic error, StackTrace stack);

-

-class RetryError extends Error {

-  final String message;

-  final List<ErrorAndStacktrace> errors;

-

-  RetryError(this.message, this.errors);

-

-  @override

-  String toString() => message;

-}

-

-class ErrorAndStacktrace {

-  final dynamic error;

-  final StackTrace stacktrace;

-

-  ErrorAndStacktrace(this.error, this.stacktrace);

-

-  @override

-  String toString() {

-    return 'ErrorAndStacktrace{error: $error, stacktrace: $stacktrace}';

-  }

-

-  @override

-  bool operator ==(Object other) =>

-      identical(this, other) ||

-      other is ErrorAndStacktrace &&

-          runtimeType == other.runtimeType &&

-          error == other.error &&

-          stacktrace == other.stacktrace;

-

-  @override

-  int get hashCode => error.hashCode ^ stacktrace.hashCode;

-}

diff --git a/rxdart/lib/src/streams/zip.dart b/rxdart/lib/src/streams/zip.dart
deleted file mode 100755
index c3bdd3e..0000000
--- a/rxdart/lib/src/streams/zip.dart
+++ /dev/null
@@ -1,296 +0,0 @@
-import 'dart:async';

-

-/// Merges the specified streams into one observable sequence using the given

-/// zipper function whenever all of the observable sequences have produced

-/// an element at a corresponding index.

-///

-/// It applies this function in strict sequence, so the first item emitted by

-/// the new Observable will be the result of the function applied to the first

-/// item emitted by Observable #1 and the first item emitted by Observable #2;

-/// the second item emitted by the new zip-Observable will be the result of

-/// the function applied to the second item emitted by Observable #1 and the

-/// second item emitted by Observable #2; and so forth. It will only emit as

-/// many items as the number of items emitted by the source Observable that

-/// emits the fewest items.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#zip)

-///

-/// ### Example

-///

-///     new ZipStream([

-///         new Stream.fromIterable([1]),

-///         new Stream.fromIterable([2, 3])

-///       ], (a, b) => a + b)

-///       .listen(print); // prints 3

-class ZipStream<T, R> extends StreamView<R> {

-  ZipStream(

-    Iterable<Stream<T>> streams,

-    R zipper(List<T> values),

-  )   : assert(streams != null && streams.every((s) => s != null),

-            'streams cannot be null'),

-        assert(streams.length > 1, 'provide at least 2 streams'),

-        assert(zipper != null, 'must provide a zipper function'),

-        super(_buildController(streams, zipper).stream);

-

-  static ZipStream<T, List<T>> list<T>(Iterable<Stream<T>> streams) {

-    return ZipStream<T, List<T>>(

-      streams,

-      (List<T> values) => values,

-    );

-  }

-

-  static ZipStream<dynamic, R> zip2<A, B, R>(

-    Stream<A> streamOne,

-    Stream<B> streamTwo,

-    R zipper(A a, B b),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamOne, streamTwo],

-      (List<dynamic> values) => zipper(values[0] as A, values[1] as B),

-    );

-  }

-

-  static ZipStream<dynamic, R> zip3<A, B, C, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    R zipper(A a, B b, C c),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip4<A, B, C, D, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    R zipper(A a, B b, C c, D d),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip5<A, B, C, D, E, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    R zipper(A a, B b, C c, D d, E e),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip6<A, B, C, D, E, F, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    R zipper(A a, B b, C c, D d, E e, F f),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip7<A, B, C, D, E, F, G, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    R zipper(A a, B b, C c, D d, E e, F f, G g),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF, streamG],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip8<A, B, C, D, E, F, G, H, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    Stream<H> streamH,

-    R zipper(A a, B b, C c, D d, E e, F f, G g, H h),

-  ) {

-    return ZipStream<dynamic, R>(

-      [streamA, streamB, streamC, streamD, streamE, streamF, streamG, streamH],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-          values[7] as H,

-        );

-      },

-    );

-  }

-

-  static ZipStream<dynamic, R> zip9<A, B, C, D, E, F, G, H, I, R>(

-    Stream<A> streamA,

-    Stream<B> streamB,

-    Stream<C> streamC,

-    Stream<D> streamD,

-    Stream<E> streamE,

-    Stream<F> streamF,

-    Stream<G> streamG,

-    Stream<H> streamH,

-    Stream<I> streamI,

-    R zipper(A a, B b, C c, D d, E e, F f, G g, H h, I i),

-  ) {

-    return ZipStream<dynamic, R>(

-      [

-        streamA,

-        streamB,

-        streamC,

-        streamD,

-        streamE,

-        streamF,

-        streamG,

-        streamH,

-        streamI

-      ],

-      (List<dynamic> values) {

-        return zipper(

-          values[0] as A,

-          values[1] as B,

-          values[2] as C,

-          values[3] as D,

-          values[4] as E,

-          values[5] as F,

-          values[6] as G,

-          values[7] as H,

-          values[8] as I,

-        );

-      },

-    );

-  }

-

-  static StreamController<R> _buildController<T, R>(

-    Iterable<Stream<T>> streams,

-    R zipper(List<T> values),

-  ) {

-    {

-      StreamController<R> controller;

-      final subscriptions = List<StreamSubscription<T>>(streams.length);

-

-      controller = StreamController<R>(

-          sync: true,

-          onListen: () {

-            try {

-              final values = List<List<T>>.generate(streams.length, (_) => []);

-              final completedStatus =

-                  List.generate(streams.length, (_) => false);

-

-              void doUpdate(int index, T value) {

-                values[index].add(value);

-

-                if (values.every((v) => v.isNotEmpty)) {

-                  try {

-                    controller.add(zipper(

-                        values.fold([], (prev, vals) => prev..add(vals[0]))));

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-

-                  values.forEach((v) => v..removeAt(0));

-                }

-              }

-

-              void markDone(int i) {

-                completedStatus[i] = true;

-

-                if (completedStatus.reduce((bool a, bool b) => a && b))

-                  controller.close();

-              }

-

-              for (var i = 0, len = streams.length; i < len; i++) {

-                var stream = streams.elementAt(i);

-

-                subscriptions[i] = stream.listen(

-                    (T value) => doUpdate(i, value),

-                    onError: controller.addError,

-                    onDone: () => markDone(i));

-              }

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscriptions.where((StreamSubscription<dynamic> subscription) => subscription != null).forEach(

-                  (StreamSubscription<dynamic> subscription) =>

-                      subscription.pause(resumeSignal)),

-          onResume: () =>

-              subscriptions.where((StreamSubscription<dynamic> subscription) => subscription != null).forEach(

-                  (StreamSubscription<dynamic> subscription) =>

-                      subscription.resume()),

-          onCancel: () => Future.wait<dynamic>(subscriptions

-              .map((StreamSubscription<dynamic> subscription) => subscription.cancel())

-              .where((Future<dynamic> cancelFuture) => cancelFuture != null)));

-

-      return controller;

-    }

-  }

-}

diff --git a/rxdart/lib/src/subjects/behavior_subject.dart b/rxdart/lib/src/subjects/behavior_subject.dart
deleted file mode 100755
index 0d5acd7..0000000
--- a/rxdart/lib/src/subjects/behavior_subject.dart
+++ /dev/null
@@ -1,160 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/observables/observable.dart';

-import 'package:rxdart/src/observables/value_observable.dart';

-import 'package:rxdart/src/subjects/subject.dart';

-

-/// A special StreamController that captures the latest item that has been

-/// added to the controller, and emits that as the first item to any new

-/// listener.

-///

-/// This subject allows sending data, error and done events to the listener.

-/// The latest item that has been added to the subject will be sent to any

-/// new listeners of the subject. After that, any new events will be

-/// appropriately sent to the listeners. It is possible to provide a seed value

-/// that will be emitted if no items have been added to the subject.

-///

-/// BehaviorSubject is, by default, a broadcast (aka hot) controller, in order

-/// to fulfill the Rx Subject contract. This means the Subject's `stream` can

-/// be listened to multiple times.

-///

-/// ### Example

-///

-///     final subject = new BehaviorSubject<int>();

-///

-///     subject.add(1);

-///     subject.add(2);

-///     subject.add(3);

-///

-///     subject.stream.listen(print); // prints 3

-///     subject.stream.listen(print); // prints 3

-///     subject.stream.listen(print); // prints 3

-///

-/// ### Example with seed value

-///

-///     final subject = new BehaviorSubject<int>.seeded(1);

-///

-///     subject.stream.listen(print); // prints 1

-///     subject.stream.listen(print); // prints 1

-///     subject.stream.listen(print); // prints 1

-class BehaviorSubject<T> extends Subject<T> implements ValueObservable<T> {

-  _Wrapper<T> _wrapper;

-

-  BehaviorSubject._(

-    StreamController<T> controller,

-    Observable<T> observable,

-    this._wrapper,

-  ) : super(controller, observable);

-

-  factory BehaviorSubject({

-    void onListen(),

-    void onCancel(),

-    bool sync = false,

-  }) {

-    // ignore: close_sinks

-    final controller = StreamController<T>.broadcast(

-      onListen: onListen,

-      onCancel: onCancel,

-      sync: sync,

-    );

-

-    final wrapper = _Wrapper<T>();

-

-    return BehaviorSubject<T>._(

-        controller,

-        Observable<T>.defer(() {

-          if (wrapper.latestIsError) {

-            scheduleMicrotask(() => controller.addError(

-                wrapper.latestError, wrapper.latestStackTrace));

-          } else if (wrapper.latestIsValue) {

-            return Observable<T>(controller.stream)

-                .startWith(wrapper.latestValue);

-          }

-

-          return controller.stream;

-        }, reusable: true),

-        wrapper);

-  }

-

-  factory BehaviorSubject.seeded(

-    T seedValue, {

-    void onListen(),

-    void onCancel(),

-    bool sync = false,

-  }) {

-    // ignore: close_sinks

-    final controller = StreamController<T>.broadcast(

-      onListen: onListen,

-      onCancel: onCancel,

-      sync: sync,

-    );

-

-    final wrapper = _Wrapper<T>.seeded(seedValue);

-

-    return BehaviorSubject<T>._(

-        controller,

-        Observable<T>.defer(() {

-          if (wrapper.latestIsError) {

-            scheduleMicrotask(() => controller.addError(

-                wrapper.latestError, wrapper.latestStackTrace));

-          }

-

-          return Observable<T>(controller.stream)

-              .startWith(wrapper.latestValue);

-        }, reusable: true),

-        wrapper);

-  }

-

-  @override

-  void onAdd(T event) => _wrapper.setValue(event);

-

-  @override

-  void onAddError(Object error, [StackTrace stackTrace]) =>

-      _wrapper.setError(error, stackTrace);

-

-  @override

-  ValueObservable<T> get stream => this;

-

-  @override

-  bool get hasValue => _wrapper.latestIsValue;

-

-  /// Get the latest value emitted by the Subject

-  @override

-  T get value => _wrapper.latestValue;

-

-  /// Set and emit the new value

-  set value(T newValue) => add(newValue);

-}

-

-class _Wrapper<T> {

-  T latestValue;

-  Object latestError;

-  StackTrace latestStackTrace;

-

-  bool latestIsValue = false, latestIsError = false;

-

-  /// Non-seeded constructor

-  _Wrapper();

-

-  _Wrapper.seeded(this.latestValue) : latestIsValue = true;

-

-  void setValue(T event) {

-    latestIsValue = true;

-    latestIsError = false;

-

-    latestValue = event;

-

-    latestError = null;

-    latestStackTrace = null;

-  }

-

-  void setError(Object error, [StackTrace stackTrace]) {

-    latestIsValue = false;

-    latestIsError = true;

-

-    latestValue = null;

-

-    latestError = error;

-    latestStackTrace = stackTrace;

-  }

-}

diff --git a/rxdart/lib/src/subjects/publish_subject.dart b/rxdart/lib/src/subjects/publish_subject.dart
deleted file mode 100755
index 512407c..0000000
--- a/rxdart/lib/src/subjects/publish_subject.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/observables/observable.dart';

-import 'package:rxdart/src/subjects/subject.dart';

-

-/// Exactly like a normal broadcast StreamController with one exception:

-/// `stream` returns an `Observable` instead of a `Stream`.

-///

-/// This Subject allows sending data, error and done events to the listener.

-///

-/// PublishSubject is, by default, a broadcast (aka hot) controller, in order

-/// to fulfill the Rx Subject contract. This means the Subject's `stream` can

-/// be listened to multiple times.

-///

-/// ### Example

-///

-///     final subject = new PublishSubject<int>();

-///

-///     // observer1 will receive all data and done events

-///     subject.stream.listen(observer1);

-///     subject.add(1);

-///     subject.add(2);

-///

-///     // observer2 will only receive 3 and done event

-///     subject.stream.listen(observe2);

-///     subject.add(3);

-///     subject.close();

-class PublishSubject<T> extends Subject<T> {

-  PublishSubject._(StreamController<T> controller, Observable<T> observable)

-      : super(controller, observable);

-

-  factory PublishSubject(

-      {void onListen(), void onCancel(), bool sync = false}) {

-    // ignore: close_sinks

-    final controller = StreamController<T>.broadcast(

-      onListen: onListen,

-      onCancel: onCancel,

-      sync: sync,

-    );

-

-    return PublishSubject<T>._(

-      controller,

-      Observable<T>(controller.stream),

-    );

-  }

-}

diff --git a/rxdart/lib/src/subjects/replay_subject.dart b/rxdart/lib/src/subjects/replay_subject.dart
deleted file mode 100755
index 672448c..0000000
--- a/rxdart/lib/src/subjects/replay_subject.dart
+++ /dev/null
@@ -1,95 +0,0 @@
-import 'dart:async';

-import 'dart:collection';

-

-import 'package:rxdart/src/observables/observable.dart';

-import 'package:rxdart/src/observables/replay_observable.dart';

-import 'package:rxdart/src/subjects/subject.dart';

-

-/// A special StreamController that captures all of the items that have been

-/// added to the controller, and emits those as the first items to any new

-/// listener.

-///

-/// This subject allows sending data, error and done events to the listener.

-/// As items are added to the subject, the ReplaySubject will store them.

-/// When the stream is listened to, those recorded items will be emitted to

-/// the listener. After that, any new events will be appropriately sent to the

-/// listeners. It is possible to cap the number of stored events by setting

-/// a maxSize value.

-///

-/// ReplaySubject is, by default, a broadcast (aka hot) controller, in order

-/// to fulfill the Rx Subject contract. This means the Subject's `stream` can

-/// be listened to multiple times.

-///

-/// ### Example

-///

-///     final subject = new ReplaySubject<int>();

-///

-///     subject.add(1);

-///     subject.add(2);

-///     subject.add(3);

-///

-///     subject.stream.listen(print); // prints 1, 2, 3

-///     subject.stream.listen(print); // prints 1, 2, 3

-///     subject.stream.listen(print); // prints 1, 2, 3

-///

-/// ### Example with maxSize

-///

-///     final subject = new ReplaySubject<int>(maxSize: 2);

-///

-///     subject.add(1);

-///     subject.add(2);

-///     subject.add(3);

-///

-///     subject.stream.listen(print); // prints 2, 3

-///     subject.stream.listen(print); // prints 2, 3

-///     subject.stream.listen(print); // prints 2, 3

-class ReplaySubject<T> extends Subject<T> implements ReplayObservable<T> {

-  final Queue<T> _queue;

-  final int _maxSize;

-

-  factory ReplaySubject({

-    int maxSize,

-    void onListen(),

-    void onCancel(),

-    bool sync = false,

-  }) {

-    // ignore: close_sinks

-    final controller = StreamController<T>.broadcast(

-      onListen: onListen,

-      onCancel: onCancel,

-      sync: sync,

-    );

-

-    final queue = Queue<T>();

-

-    return ReplaySubject<T>._(

-      controller,

-      Observable<T>.defer(

-          () => Observable<T>(controller.stream)

-              .startWithMany(queue.toList(growable: false)),

-          reusable: true),

-      queue,

-      maxSize,

-    );

-  }

-

-  ReplaySubject._(

-    StreamController<T> controller,

-    Observable<T> observable,

-    this._queue,

-    this._maxSize,

-  ) : super(controller, observable);

-

-  @override

-  void onAdd(T event) {

-    if (_queue.length == _maxSize) {

-      _queue.removeFirst();

-    }

-

-    _queue.add(event);

-  }

-

-  /// Synchronously get the values stored in Subject. May be empty.

-  @override

-  List<T> get values => _queue.toList(growable: false);

-}

diff --git a/rxdart/lib/src/subjects/subject.dart b/rxdart/lib/src/subjects/subject.dart
deleted file mode 100755
index 4ff5af4..0000000
--- a/rxdart/lib/src/subjects/subject.dart
+++ /dev/null
@@ -1,177 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/observables/observable.dart';

-

-/// The base for all Subjects. If you'd like to create a new Subject,

-/// extend from this class.

-///

-/// It handles all of the nitty-gritty details that conform to the

-/// StreamController spec and don't need to be repeated over and

-/// over.

-///

-/// Please see `PublishSubject` for the simplest example of how to

-/// extend from this class, or `BehaviorSubject` for a slightly more

-/// complex example.

-abstract class Subject<T> extends Observable<T> implements StreamController<T> {

-  final StreamController<T> controller;

-

-  bool _isAddingStreamItems = false;

-

-  Subject(StreamController<T> controller, Observable<T> observable)

-      : this.controller = controller,

-        super(observable);

-

-  @override

-  StreamSink<T> get sink => _StreamSinkWrapper<T>(this);

-

-  @override

-  ControllerCallback get onListen => controller.onListen;

-

-  @override

-  set onListen(void onListenHandler()) {

-    controller.onListen = onListenHandler;

-  }

-

-  @override

-  Observable<T> get stream => this;

-

-  @override

-  ControllerCallback get onPause =>

-      throw UnsupportedError("Subjects do not support pause callbacks");

-

-  @override

-  set onPause(void onPauseHandler()) =>

-      throw UnsupportedError("Subjects do not support pause callbacks");

-

-  @override

-  ControllerCallback get onResume =>

-      throw UnsupportedError("Subjects do not support resume callbacks");

-

-  @override

-  set onResume(void onResumeHandler()) =>

-      throw UnsupportedError("Subjects do not support resume callbacks");

-

-  @override

-  ControllerCancelCallback get onCancel => controller.onCancel;

-

-  @override

-  set onCancel(void onCancelHandler()) {

-    controller.onCancel = onCancelHandler;

-  }

-

-  @override

-  bool get isClosed => controller.isClosed;

-

-  @override

-  bool get isPaused => controller.isPaused;

-

-  @override

-  bool get hasListener => controller.hasListener;

-

-  @override

-  Future<dynamic> get done => controller.done;

-

-  @override

-  void addError(Object error, [StackTrace stackTrace]) {

-    if (_isAddingStreamItems) {

-      throw StateError(

-          "You cannot add an error while items are being added from addStream");

-    }

-

-    _addError(error, stackTrace);

-  }

-

-  void _addError(Object error, [StackTrace stackTrace]) {

-    onAddError(error, stackTrace);

-

-    controller.addError(error, stackTrace);

-  }

-

-  /// An extension point for sub-classes. Perform any side-effect / state

-  /// management you need to here, rather than overriding the `add` method

-  /// directly.

-  void onAddError(Object error, [StackTrace stackTrace]) {}

-

-  @override

-  Future<dynamic> addStream(Stream<T> source, {bool cancelOnError = true}) {

-    if (_isAddingStreamItems) {

-      throw StateError(

-          "You cannot add items while items are being added from addStream");

-    }

-

-    final completer = Completer<T>();

-    _isAddingStreamItems = true;

-

-    source.listen((T event) {

-      _add(event);

-    }, onError: (dynamic e, StackTrace s) {

-      controller.addError(e, s);

-

-      if (cancelOnError) {

-        _isAddingStreamItems = false;

-        completer.completeError(e);

-      }

-    }, onDone: () {

-      _isAddingStreamItems = false;

-      completer.complete();

-    }, cancelOnError: cancelOnError);

-

-    return completer.future;

-  }

-

-  @override

-  void add(T event) {

-    if (_isAddingStreamItems) {

-      throw StateError(

-          "You cannot add items while items are being added from addStream");

-    }

-

-    _add(event);

-  }

-

-  void _add(T event) {

-    onAdd(event);

-

-    controller.add(event);

-  }

-

-  /// An extension point for sub-classes. Perform any side-effect / state

-  /// management you need to here, rather than overriding the `add` method

-  /// directly.

-  void onAdd(T event) {}

-

-  @override

-  Future<dynamic> close() {

-    if (_isAddingStreamItems) {

-      throw StateError(

-          "You cannot close the subject while items are being added from addStream");

-    }

-

-    return controller.close();

-  }

-}

-

-class _StreamSinkWrapper<T> implements StreamSink<T> {

-  final StreamController<T> _target;

-

-  _StreamSinkWrapper(this._target);

-

-  @override

-  void add(T data) {

-    _target.add(data);

-  }

-

-  @override

-  void addError(Object error, [StackTrace stackTrace]) {

-    _target.addError(error, stackTrace);

-  }

-

-  @override

-  Future<dynamic> close() => _target.close();

-

-  @override

-  Future<dynamic> addStream(Stream<T> source) => _target.addStream(source);

-

-  @override

-  Future<dynamic> get done => _target.done;

-}

diff --git a/rxdart/lib/src/transformers/buffer.dart b/rxdart/lib/src/transformers/buffer.dart
deleted file mode 100755
index 3497cab..0000000
--- a/rxdart/lib/src/transformers/buffer.dart
+++ /dev/null
@@ -1,109 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/samplers/buffer_strategy.dart';

-

-/// Creates an Observable where each item is a [List] containing the items

-/// from the source sequence, batched by the [sampler].

-///

-/// ### Example with [onCount]

-///

-///     Observable.range(1, 4)

-///       .buffer(onCount(2))

-///       .listen(print); // prints [1, 2], [3, 4]

-///

-/// ### Example with [onFuture]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .buffer(onFuture(() => new Future.delayed(const Duration(milliseconds: 220))))

-///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-///

-/// ### Example with [onTest]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .buffer(onTest((i) => i % 2 == 0))

-///       .listen(print); // prints [0], [1, 2] [3, 4] [5, 6] ...

-///

-/// ### Example with [onTime]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .buffer(onTime(const Duration(milliseconds: 220)))

-///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-///

-/// ### Example with [onStream]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .buffer(onStream(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i)))

-///       .listen(print); // prints [0, 1] [2, 3] [4, 5] ...

-///

-/// You can create your own sampler by extending [StreamView]

-/// should the above samplers be insufficient for your use case.

-class BufferStreamTransformer<T> extends StreamTransformerBase<T, List<T>> {

-  final SamplerBuilder<T, List<T>> sampler;

-  final bool exhaustBufferOnDone;

-

-  BufferStreamTransformer(this.sampler, {this.exhaustBufferOnDone = true});

-

-  @override

-  Stream<List<T>> bind(Stream<T> stream) =>

-      _buildTransformer<T>(sampler, exhaustBufferOnDone).bind(stream);

-

-  static StreamTransformer<T, List<T>> _buildTransformer<T>(

-      SamplerBuilder<T, List<T>> scheduler, bool exhaustBufferOnDone) {

-    assertSampler(scheduler);

-

-    return StreamTransformer<T, List<T>>((Stream<T> input, bool cancelOnError) {

-      StreamController<List<T>> controller;

-      StreamSubscription<List<T>> subscription;

-      var buffer = <T>[];

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        if (exhaustBufferOnDone && buffer.isNotEmpty)

-          controller.add(List<T>.from(buffer));

-

-        controller.close();

-      }

-

-      controller = StreamController<List<T>>(

-          sync: true,

-          onListen: () {

-            try {

-              subscription = scheduler(input, (

-                T data,

-                EventSink<List<T>> sink, [

-                int startBufferEvery,

-              ]) {

-                buffer.add(data);

-                sink.add(buffer);

-              }, (_, EventSink<List<T>> sink, [int startBufferEvery = 0]) {

-                startBufferEvery ?? 0;

-

-                sink.add(List<T>.unmodifiable(buffer));

-                buffer =

-                    startBufferEvery > 0 && startBufferEvery < buffer.length

-                        ? buffer.sublist(startBufferEvery)

-                        : <T>[];

-              }).listen(controller.add,

-                  onError: controller.addError,

-                  onDone: onDone,

-                  cancelOnError: cancelOnError);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-

-  static void assertSampler<T>(SamplerBuilder<T, List<T>> scheduler) {

-    if (scheduler == null) {

-      throw ArgumentError('scheduler cannot be null');

-    }

-  }

-}

diff --git a/rxdart/lib/src/transformers/debounce.dart b/rxdart/lib/src/transformers/debounce.dart
deleted file mode 100755
index 51b1b1a..0000000
--- a/rxdart/lib/src/transformers/debounce.dart
+++ /dev/null
@@ -1,85 +0,0 @@
-import 'dart:async';

-

-/// Transforms a Stream so that will only emit items from the source sequence

-/// if a particular time span has passed without the source sequence emitting

-/// another item.

-///

-/// The Debounce Transformer filters out items emitted by the source Observable

-/// that are rapidly followed by another emitted item.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#debounce)

-///

-/// ### Example

-///

-///     new Observable.fromIterable([1, 2, 3, 4])

-///       .debounce(new Duration(seconds: 1))

-///       .listen(print); // prints 4

-class DebounceStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  DebounceStreamTransformer(Duration duration)

-      : transformer = _buildTransformer(duration);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(Duration duration) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      T lastEvent;

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      Timer timer;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  lastEvent = value;

-

-                  try {

-                    _cancelTimerIfActive(timer);

-

-                    timer = Timer(duration, () {

-                      controller.add(lastEvent);

-                      lastEvent = null;

-                    });

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  _cancelTimerIfActive(timer);

-

-                  if (lastEvent != null) {

-                    scheduleMicrotask(() {

-                      controller.add(lastEvent);

-

-                      controller.close();

-                    });

-                  } else {

-                    controller.close();

-                  }

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () {

-            _cancelTimerIfActive(timer);

-

-            return subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-

-  static void _cancelTimerIfActive(Timer _timer) {

-    if (_timer != null && _timer.isActive) {

-      _timer.cancel();

-    }

-  }

-}

diff --git a/rxdart/lib/src/transformers/default_if_empty.dart b/rxdart/lib/src/transformers/default_if_empty.dart
deleted file mode 100755
index 62d0566..0000000
--- a/rxdart/lib/src/transformers/default_if_empty.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-import 'dart:async';

-

-/// Emit items from the source Stream, or a single default item if the source

-/// Stream emits nothing.

-///

-/// ### Example

-///

-///     new Stream.empty()

-///       .transform(new DefaultIfEmptyStreamTransformer(10))

-///       .listen(print); // prints 10

-class DefaultIfEmptyStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  DefaultIfEmptyStreamTransformer(T defaultValue)

-      : transformer = _buildTransformer(defaultValue);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(T defaultValue) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      var hasEvent = false;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  hasEvent = true;

-                  try {

-                    controller.add(value);

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  if (!hasEvent) controller.add(defaultValue);

-                  controller.close();

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/delay.dart b/rxdart/lib/src/transformers/delay.dart
deleted file mode 100755
index 1f83681..0000000
--- a/rxdart/lib/src/transformers/delay.dart
+++ /dev/null
@@ -1,82 +0,0 @@
-import 'dart:async';

-

-/// The Delay operator modifies its source Observable by pausing for

-/// a particular increment of time (that you specify) before emitting

-/// each of the source Observable’s items.

-/// This has the effect of shifting the entire sequence of items emitted

-/// by the Observable forward in time by that specified increment.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#delay)

-///

-/// ### Example

-///

-///     new Observable.fromIterable([1, 2, 3, 4])

-///       .delay(new Duration(seconds: 1))

-///       .listen(print); // [after one second delay] prints 1, 2, 3, 4 immediately

-class DelayStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  DelayStreamTransformer(Duration duration)

-      : transformer = _buildTransformer(duration);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(Duration duration) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      var onDoneCalled = false, hasNoEvents = true;

-      var timers = <Timer>[];

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  hasNoEvents = false;

-

-                  try {

-                    Timer timer;

-                    timer = Timer(duration, () {

-                      controller.add(value);

-

-                      timers.remove(timer);

-

-                      if (onDoneCalled && timers.isEmpty) {

-                        controller.close();

-                      }

-                    });

-

-                    timers.add(timer);

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  if (hasNoEvents) controller.close();

-

-                  onDoneCalled = true;

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () {

-            timers.forEach(_cancelTimerIfActive);

-

-            return subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-

-  static void _cancelTimerIfActive(Timer _timer) {

-    if (_timer != null && _timer.isActive) {

-      _timer.cancel();

-    }

-  }

-}

diff --git a/rxdart/lib/src/transformers/dematerialize.dart b/rxdart/lib/src/transformers/dematerialize.dart
deleted file mode 100755
index 68781d8..0000000
--- a/rxdart/lib/src/transformers/dematerialize.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/utils/notification.dart';

-

-/// Converts the onData, onDone, and onError [Notification] objects from a

-/// materialized stream into normal onData, onDone, and onError events.

-///

-/// When a stream has been materialized, it emits onData, onDone, and onError

-/// events as [Notification] objects. Dematerialize simply reverses this by

-/// transforming [Notification] objects back to a normal stream of events.

-///

-/// ### Example

-///

-///     new Stream<Notification<int>>

-///         .fromIterable([new Notification.onData(1), new Notification.onDone()])

-///         .transform(dematerializeTransformer())

-///         .listen((i) => print(i)); // Prints 1

-///

-/// ### Error example

-///

-///     new Stream<Notification<int>>

-///         .fromIterable([new Notification.onError(new Exception(), null)])

-///         .transform(dematerializeTransformer())

-///         .listen(null, onError: (e, s) { print(e) }); // Prints Exception

-class DematerializeStreamTransformer<T>

-    extends StreamTransformerBase<Notification<T>, T> {

-  final StreamTransformer<Notification<T>, T> transformer;

-

-  DematerializeStreamTransformer() : transformer = _buildTransformer();

-

-  @override

-  Stream<T> bind(Stream<Notification<T>> stream) => transformer.bind(stream);

-

-  static StreamTransformer<Notification<T>, T> _buildTransformer<T>() {

-    return StreamTransformer<Notification<T>, T>(

-        (Stream<Notification<T>> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<Notification<T>> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((Notification<T> notification) {

-              try {

-                if (notification.isOnData) {

-                  controller.add(notification.value);

-                } else if (notification.isOnDone) {

-                  controller.close();

-                } else if (notification.isOnError) {

-                  controller.addError(

-                      notification.error, notification.stackTrace);

-                }

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            subscription.pause(resumeSignal);

-          },

-          onResume: () {

-            subscription.resume();

-          },

-          onCancel: () {

-            return subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/distinct_unique.dart b/rxdart/lib/src/transformers/distinct_unique.dart
deleted file mode 100755
index 9c33091..0000000
--- a/rxdart/lib/src/transformers/distinct_unique.dart
+++ /dev/null
@@ -1,66 +0,0 @@
-import 'dart:async';

-import 'dart:collection';

-

-/// Create an `Observable` which implements a [HashSet] under the hood, using

-/// the provided `equals` as equality.

-///

-/// The `Observable` will only emit an event, if that event is not yet found

-/// within the underlying [HashSet].

-///

-/// ###  Example

-///

-///     new Stream.fromIterable([1, 2, 1, 2, 1, 2, 3, 2, 1])

-///         .listen((event) => print(event));

-///

-/// will emit:

-///     1, 2, 3

-///

-/// The provided `equals` must define a stable equivalence relation, and

-/// `hashCode` must be consistent with `equals`.

-///

-/// If `equals` or `hashCode` are omitted, the set uses the elements' intrinsic

-/// `Object.==` and `Object.hashCode`. If you supply one of `equals` and

-/// `hashCode`, you should generally also to supply the other.

-class DistinctUniqueStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  DistinctUniqueStreamTransformer({bool equals(T e1, T e2), int hashCode(T e)})

-      : transformer = _buildTransformer(equals, hashCode);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-      bool equals(T e1, T e2), int hashCode(T e)) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      var collection = HashSet<T>(equals: equals, hashCode: hashCode);

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T value) {

-              try {

-                if (collection.add(value)) controller.add(value);

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () {

-            collection.clear();

-            collection = null;

-            return subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/do.dart b/rxdart/lib/src/transformers/do.dart
deleted file mode 100755
index 5c0c357..0000000
--- a/rxdart/lib/src/transformers/do.dart
+++ /dev/null
@@ -1,223 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/utils/notification.dart';

-

-typedef void VoidFunc();

-typedef Future<dynamic> FutureFunc();

-

-/// Invokes the given callback at the corresponding point the the stream

-/// lifecycle. For example, if you pass in an onDone callback, it will

-/// be invoked when the stream finishes emitting items.

-///

-/// This transformer can be used for debugging, logging, etc. by intercepting

-/// the stream at different points to run arbitrary actions.

-///

-/// It is possible to hook onto the following parts of the stream lifecycle:

-///

-///   - onCancel

-///   - onData

-///   - onDone

-///   - onError

-///   - onListen

-///   - onPause

-///   - onResume

-///

-/// In addition, the `onEach` argument is called at `onData`, `onDone`, and

-/// `onError` with a [Notification] passed in. The [Notification] argument

-/// contains the [Kind] of event (OnData, OnDone, OnError), and the item or

-/// error that was emitted. In the case of onDone, no data is emitted as part

-/// of the [Notification].

-///

-/// If no callbacks are passed in, a runtime error will be thrown in dev mode

-/// in order to "fail fast" and alert the developer that the transformer should

-/// be used or safely removed.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1])

-///         .transform(new DoStreamTransformer(

-///           onData: print,

-///           onError: (e, s) => print("Oh no!"),

-///           onDone: () => print("Done")))

-///         .listen(null); // Prints: 1, "Done"

-class DoStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  DoStreamTransformer(

-      {void onCancel(),

-      void onData(T event),

-      void onDone(),

-      void onEach(Notification<T> notification),

-      Function onError,

-      void onListen(),

-      void onPause(Future<dynamic> resumeSignal),

-      void onResume()})

-      : transformer = _buildTransformer(

-            onCancel: onCancel,

-            onData: onData,

-            onDone: onDone,

-            onEach: onEach,

-            onError: onError,

-            onListen: onListen,

-            onPause: onPause,

-            onResume: onResume);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-      {dynamic onCancel(),

-      void onData(T event),

-      void onDone(),

-      void onEach(Notification<T> notification),

-      Function onError,

-      void onListen(),

-      void onPause(Future<dynamic> resumeSignal),

-      void onResume()}) {

-    if (onCancel == null &&

-        onData == null &&

-        onDone == null &&

-        onEach == null &&

-        onError == null &&

-        onListen == null &&

-        onPause == null &&

-        onResume == null) {

-      throw ArgumentError("Must provide at least one handler");

-    }

-

-    final subscriptions = <Stream<dynamic>, StreamSubscription<dynamic>>{};

-

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      final VoidFunc onListenLocal = () {

-        if (onListen != null) {

-          try {

-            onListen();

-          } catch (e, s) {

-            controller.addError(e, s);

-          }

-        }

-        subscriptions.putIfAbsent(

-          input,

-          () {

-            return input.listen(

-              (T value) {

-                if (onData != null) {

-                  try {

-                    onData(value);

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                }

-                if (onEach != null) {

-                  try {

-                    onEach(Notification<T>.onData(value));

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                }

-                controller.add(value);

-              },

-              onError: (dynamic e, StackTrace s) {

-                if (onError != null) {

-                  try {

-                    onError(e, s);

-                  } catch (e2, s2) {

-                    controller.addError(e2, s2);

-                  }

-                }

-                if (onEach != null) {

-                  try {

-                    onEach(Notification<T>.onError(e, s));

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                }

-                controller.addError(e, s);

-              },

-              onDone: () {

-                if (onDone != null) {

-                  try {

-                    onDone();

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                }

-                if (onEach != null) {

-                  try {

-                    onEach(Notification<T>.onDone());

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                }

-                controller.close();

-              },

-              cancelOnError: cancelOnError,

-            );

-          },

-        );

-      };

-      final FutureFunc onCancelLocal = () {

-        dynamic onCancelResult;

-

-        if (onCancel != null) {

-          try {

-            onCancelResult = onCancel();

-          } catch (e, s) {

-            if (!controller.isClosed) {

-              controller.addError(e, s);

-            } else {

-              Zone.current.handleUncaughtError(e, s);

-            }

-          }

-        }

-        final cancelResultFuture = onCancelResult is Future

-            ? onCancelResult

-            : Future<dynamic>.value(onCancelResult);

-        final cancelFuture =

-            subscriptions[input].cancel() ?? Future<dynamic>.value();

-

-        return Future.wait<dynamic>([cancelFuture, cancelResultFuture])

-            .whenComplete(() => subscriptions.remove(input));

-      };

-

-      if (input.isBroadcast) {

-        controller = StreamController<T>.broadcast(

-          sync: true,

-          onListen: onListenLocal,

-          onCancel: onCancelLocal,

-        );

-      } else {

-        controller = StreamController<T>(

-          sync: true,

-          onListen: onListenLocal,

-          onCancel: onCancelLocal,

-          onPause: ([Future<dynamic> resumeSignal]) {

-            if (onPause != null) {

-              try {

-                onPause(resumeSignal);

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            }

-

-            subscriptions[input].pause(resumeSignal);

-          },

-          onResume: () {

-            if (onResume != null) {

-              try {

-                onResume();

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            }

-

-            subscriptions[input].resume();

-          },

-        );

-      }

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/exhaust_map.dart b/rxdart/lib/src/transformers/exhaust_map.dart
deleted file mode 100755
index f2a5fbe..0000000
--- a/rxdart/lib/src/transformers/exhaust_map.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-import 'dart:async';

-

-/// Converts items from the source stream into a new Stream using a given

-/// mapper. It ignores all items from the source stream until the new stream

-/// completes.

-///

-/// Useful when you have a noisy source Stream and only want to respond once

-/// the previous async operation is finished.

-///

-/// ### Example

-///     // Emits 0, 1, 2

-///     new Stream.periodic(new Duration(milliseconds: 200), (i) => i).take(3)

-///       .transform(new ExhaustMapStreamTransformer(

-///         // Emits the value it's given after 200ms

-///         (i) => new Observable.timer(i, new Duration(milliseconds: 200)),

-///       ))

-///     .listen(print); // prints 0, 2

-class ExhaustMapStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final StreamTransformer<T, S> transformer;

-

-  ExhaustMapStreamTransformer(Stream<S> mapper(T value))

-      : transformer = _buildTransformer(mapper);

-

-  @override

-  Stream<S> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      Stream<S> mapper(T value)) {

-    return StreamTransformer<T, S>((Stream<T> input, bool cancelOnError) {

-      StreamController<S> controller;

-      StreamSubscription<T> inputSubscription;

-      StreamSubscription<S> outputSubscription;

-      var inputClosed = false, outputIsEmitting = false;

-

-      controller = StreamController<S>(

-        sync: true,

-        onListen: () {

-          inputSubscription = input.listen(

-            (T value) {

-              try {

-                if (!outputIsEmitting) {

-                  outputIsEmitting = true;

-                  outputSubscription = mapper(value).listen(

-                    controller.add,

-                    onError: controller.addError,

-                    onDone: () {

-                      outputIsEmitting = false;

-                      if (inputClosed) controller.close();

-                    },

-                  );

-                }

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-            onError: controller.addError,

-            onDone: () {

-              inputClosed = true;

-              if (!outputIsEmitting) controller.close();

-            },

-            cancelOnError: cancelOnError,

-          );

-        },

-        onPause: ([Future<dynamic> resumeSignal]) {

-          inputSubscription.pause(resumeSignal);

-          outputSubscription?.pause(resumeSignal);

-        },

-        onResume: () {

-          inputSubscription.resume();

-          outputSubscription?.resume();

-        },

-        onCancel: () async {

-          await inputSubscription.cancel();

-          if (outputIsEmitting) await outputSubscription.cancel();

-        },

-      );

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/flat_map.dart b/rxdart/lib/src/transformers/flat_map.dart
deleted file mode 100755
index b0946bb..0000000
--- a/rxdart/lib/src/transformers/flat_map.dart
+++ /dev/null
@@ -1,96 +0,0 @@
-import 'dart:async';

-

-/// Converts each emitted item into a new Stream using the given mapper

-/// function. The newly created Stream will be be listened to and begin

-/// emitting items downstream.

-///

-/// The items emitted by each of the new Streams are emitted downstream in the

-/// same order they arrive. In other words, the sequences are merged

-/// together.

-///

-/// ### Example

-///

-///       new Stream.fromIterable([4, 3, 2, 1])

-///         .transform(new FlatMapStreamTransformer((i) =>

-///           new Stream.fromFuture(

-///             new Future.delayed(new Duration(minutes: i), () => i))

-///         .listen(print); // prints 1, 2, 3, 4

-class FlatMapStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final StreamTransformer<T, S> transformer;

-

-  FlatMapStreamTransformer(Stream<S> mapper(T value))

-      : transformer = _buildTransformer(mapper);

-

-  @override

-  Stream<S> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      Stream<S> mapper(T value)) {

-    return StreamTransformer<T, S>((Stream<T> input, bool cancelOnError) {

-      final subscriptions = <StreamSubscription<S>>[];

-      StreamController<S> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> otherSubscription;

-      var closeAfterNextEvent = false, hasMainEvent = false, openStreams = 0;

-

-      controller = StreamController<S>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  try {

-                    var otherStream = mapper(value);

-

-                    hasMainEvent = true;

-

-                    openStreams++;

-

-                    otherSubscription = otherStream.listen(controller.add,

-                        onError: controller.addError, onDone: () {

-                      openStreams--;

-                      subscriptions.remove(otherSubscription);

-

-                      if (closeAfterNextEvent && openStreams == 0)

-                        controller.close();

-                    });

-

-                    subscriptions.add(otherSubscription);

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  if (!hasMainEvent || openStreams == 0)

-                    controller.close();

-                  else

-                    closeAfterNextEvent = true;

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            subscription.pause(resumeSignal);

-

-            subscriptions.forEach((StreamSubscription<S> otherSubscription) =>

-                otherSubscription.pause(resumeSignal));

-          },

-          onResume: () {

-            subscription.resume();

-

-            subscriptions.forEach((StreamSubscription<S> otherSubscription) =>

-                otherSubscription.resume());

-          },

-          onCancel: () {

-            final list = List<StreamSubscription<dynamic>>.from(subscriptions)

-              ..add(subscription);

-

-            return Future.wait<dynamic>(list

-                .map((StreamSubscription<dynamic> subscription) =>

-                    subscription.cancel())

-                .where((Future<dynamic> cancelFuture) => cancelFuture != null));

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/flat_map_latest.dart b/rxdart/lib/src/transformers/flat_map_latest.dart
deleted file mode 100755
index b982def..0000000
--- a/rxdart/lib/src/transformers/flat_map_latest.dart
+++ /dev/null
@@ -1,85 +0,0 @@
-import 'dart:async';

-

-/// Deprecated: Use SwitchMapStreamTransformer.

-///

-/// Converts each emitted item into a new Stream using the given mapper

-/// function. The newly created Stream will be be listened to and begin

-/// emitting items, and any previously created Stream will stop emitting.

-///

-/// The flatMapLatest operator is similar to the flatMap and concatMap

-/// methods, but it only emits items from the most recently created Stream.

-///

-/// This can be useful when you only want the very latest state from

-/// asynchronous APIs, for example.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([4, 3, 2, 1])

-///       .transform(new FlatMapLatestStreamTransformer((i) =>

-///         new Stream.fromFuture(

-///           new Future.delayed(new Duration(minutes: i), () => i))

-///       .listen(print); // prints 1

-@deprecated

-class FlatMapLatestStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final StreamTransformer<T, S> transformer;

-

-  FlatMapLatestStreamTransformer(Stream<S> mapper(T value))

-      : transformer = _buildTransformer(mapper);

-

-  @override

-  Stream<S> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      Stream<S> mapper(T value)) {

-    return StreamTransformer<T, S>((Stream<T> input, bool cancelOnError) {

-      StreamController<S> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> otherSubscription;

-      var leftClosed = false, rightClosed = false, hasMainEvent = false;

-

-      controller = StreamController<S>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  try {

-                    otherSubscription?.cancel();

-

-                    hasMainEvent = true;

-

-                    otherSubscription = mapper(value).listen(controller.add,

-                        onError: controller.addError, onDone: () {

-                      rightClosed = true;

-

-                      if (leftClosed) controller.close();

-                    });

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  leftClosed = true;

-

-                  if (rightClosed || !hasMainEvent) controller.close();

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            subscription.pause(resumeSignal);

-            otherSubscription?.pause(resumeSignal);

-          },

-          onResume: () {

-            subscription.resume();

-            otherSubscription?.resume();

-          },

-          onCancel: () async {

-            await subscription.cancel();

-

-            if (hasMainEvent) await otherSubscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/group_by.dart b/rxdart/lib/src/transformers/group_by.dart
deleted file mode 100755
index 9c188b5..0000000
--- a/rxdart/lib/src/transformers/group_by.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/observables/observable.dart' show Observable;

-

-/// The GroupBy operator divides an [Observable] that emits items into

-/// an [Observable] that emits [GroupByObservable],

-/// each one of which emits some subset of the items

-/// from the original source [Observable].

-///

-/// [GroupByObservable] acts like a regular [Observable], yet

-/// adding a 'key' property, which receives its [Type] and value from

-/// the [grouper] Function.

-///

-/// All items with the same key are emitted by the same [GroupByObservable].

-

-class GroupByStreamTransformer<T, S>

-    extends StreamTransformerBase<T, GroupByObservable<T, S>> {

-  final S Function(T) grouper;

-

-  GroupByStreamTransformer(this.grouper);

-

-  @override

-  Stream<GroupByObservable<T, S>> bind(Stream<T> stream) =>

-      _buildTransformer<T, S>(grouper).bind(stream);

-

-  static StreamTransformer<T, GroupByObservable<T, S>> _buildTransformer<T, S>(

-      S Function(T) grouper) {

-    return StreamTransformer<T, GroupByObservable<T, S>>(

-        (Stream<T> input, bool cancelOnError) {

-      final mapper = <S, StreamController<T>>{};

-      StreamController<GroupByObservable<T, S>> controller;

-      StreamSubscription<T> subscription;

-

-      final controllerBuilder = (S forKey) => () {

-            final groupedController = StreamController<T>();

-

-            controller

-                .add(GroupByObservable<T, S>(forKey, groupedController.stream));

-

-            return groupedController;

-          };

-

-      controller = StreamController<GroupByObservable<T, S>>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  try {

-                    final key = grouper(value);

-                    // ignore: close_sinks

-                    final groupedController =

-                        mapper.putIfAbsent(key, controllerBuilder(key));

-

-                    groupedController.add(value);

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  mapper.values.forEach((controller) => controller.close());

-                  mapper.clear();

-

-                  controller.close();

-                });

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

-

-class GroupByObservable<T, S> extends Observable<T> {

-  final S key;

-

-  GroupByObservable(this.key, Stream<T> stream) : super(stream);

-}

diff --git a/rxdart/lib/src/transformers/ignore_elements.dart b/rxdart/lib/src/transformers/ignore_elements.dart
deleted file mode 100755
index 3f34682..0000000
--- a/rxdart/lib/src/transformers/ignore_elements.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-import 'dart:async';

-

-/// Creates an Observable where all emitted items are ignored, only the

-/// error / completed notifications are passed

-///

-/// ### Example

-///

-///     new MergeStream([

-///       new Stream.fromIterable([1]),

-///       new ErrorStream(new Exception())

-///     ])

-///     .listen(print, onError: print); // prints Exception

-class IgnoreElementsStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  IgnoreElementsStreamTransformer() : transformer = _buildTransformer();

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>() {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(null,

-                onError: controller.addError,

-                onDone: () => controller.close(),

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/interval.dart b/rxdart/lib/src/transformers/interval.dart
deleted file mode 100755
index 1657356..0000000
--- a/rxdart/lib/src/transformers/interval.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-import 'dart:async';

-

-/// Creates a Stream that emits each item in the Stream after a given

-/// duration.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, 2, 3])

-///       .transform(new IntervalStreamTransformer(seconds: 1))

-///       .listen((i) => print("$i sec"); // prints 1 sec, 2 sec, 3 sec

-class IntervalStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  IntervalStreamTransformer(Duration duration)

-      : transformer = _buildTransformer(duration);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(Duration duration) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T value) {

-              try {

-                final completer = Completer<T>();

-

-                Timer(duration, () => completer.complete(value));

-

-                subscription.pause(completer.future.then<T>((T event) {

-                  controller.add(event);

-

-                  return event;

-                }));

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: () => subscription.pause(),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/map_to.dart b/rxdart/lib/src/transformers/map_to.dart
deleted file mode 100755
index 5ca4542..0000000
--- a/rxdart/lib/src/transformers/map_to.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-import 'dart:async';

-

-/// Emits the given constant value on the output Observable every time the source Observable emits a value.

-///

-/// ### Example

-///

-///     Observable.fromIterable([1, 2, 3, 4])

-///       .mapTo(true)

-///       .listen(print); // prints true, true, true, true

-class MapToStreamTransformer<S, T> extends StreamTransformerBase<S, T> {

-  final StreamTransformer<S, T> transformer;

-

-  MapToStreamTransformer(T value) : transformer = _buildTransformer(value);

-

-  @override

-  Stream<T> bind(Stream<S> stream) => transformer.bind(stream);

-

-  static StreamTransformer<S, T> _buildTransformer<S, T>(T value) =>

-      StreamTransformer<S, T>((Stream<S> input, bool cancelOnError) {

-        StreamController<T> controller;

-        StreamSubscription<S> subscription;

-

-        controller = StreamController<T>(

-            sync: true,

-            onListen: () {

-              subscription = input.listen((_) => controller.add(value),

-                  onError: controller.addError,

-                  onDone: controller.close,

-                  cancelOnError: cancelOnError);

-            },

-            onPause: ([Future<dynamic> resumeSignal]) =>

-                subscription.pause(resumeSignal),

-            onResume: () => subscription.resume(),

-            onCancel: () => subscription.cancel());

-

-        return controller.stream.listen(null);

-      });

-}

diff --git a/rxdart/lib/src/transformers/materialize.dart b/rxdart/lib/src/transformers/materialize.dart
deleted file mode 100755
index 638a8f3..0000000
--- a/rxdart/lib/src/transformers/materialize.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/utils/notification.dart';

-

-/// Converts the onData, on Done, and onError events into [Notification]

-/// objects that are passed into the downstream onData listener.

-///

-/// The [Notification] object contains the [Kind] of event (OnData, onDone, or

-/// OnError), and the item or error that was emitted. In the case of onDone,

-/// no data is emitted as part of the [Notification].

-///

-/// ### Example

-///

-///     new Stream<int>.fromIterable([1])

-///         .transform(materializeTransformer())

-///         .listen((i) => print(i)); // Prints onData & onDone Notification

-class MaterializeStreamTransformer<T>

-    extends StreamTransformerBase<T, Notification<T>> {

-  final StreamTransformer<T, Notification<T>> transformer;

-

-  MaterializeStreamTransformer() : transformer = _buildTransformer();

-

-  @override

-  Stream<Notification<T>> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, Notification<T>> _buildTransformer<T>() {

-    return StreamTransformer<T, Notification<T>>(

-        (Stream<T> input, bool cancelOnError) {

-      StreamController<Notification<T>> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<Notification<T>>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T value) {

-              try {

-                controller.add(Notification<T>.onData(value));

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            }, onError: (dynamic e, StackTrace s) {

-              controller.add(Notification<T>.onError(e, s));

-            }, onDone: () {

-              controller.add(Notification<T>.onDone());

-

-              controller.close();

-            }, cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            subscription.pause(resumeSignal);

-          },

-          onResume: () {

-            subscription.resume();

-          },

-          onCancel: () {

-            return subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/of_type.dart b/rxdart/lib/src/transformers/of_type.dart
deleted file mode 100755
index 8440c5a..0000000
--- a/rxdart/lib/src/transformers/of_type.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/utils/type_token.dart';

-

-/// Filters a sequence so that only events of a given type pass

-///

-/// In order to capture the Type correctly, it needs to be wrapped

-/// in a [TypeToken] as the generic parameter.

-///

-/// Given the way Dart generics work, one cannot simply use the `is T` / `as T`

-/// checks and castings within `OfTypeStreamTransformer` itself. Therefore, the

-/// [TypeToken] class was introduced to capture the type of class you'd

-/// like `ofType` to filter down to.

-///

-/// ### Examples

-///

-///     new Stream.fromIterable([1, "hi"])

-///       .ofType(new TypeToken<String>)

-///       .listen(print); // prints "hi"

-///

-/// As a shortcut, you can use some pre-defined constants to write the above

-/// in the following way:

-///

-///     new Stream.fromIterable([1, "hi"])

-///       .transform(new OfTypeStreamTransformer(kString))

-///       .listen(print); // prints "hi"

-///

-/// If you'd like to create your own shortcuts like the example above,

-/// simply create a constant:

-///

-///     const TypeToken<Map<Int, String>> kMapIntString =

-///       const TypeToken<Map<Int, String>>();

-class OfTypeStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final StreamTransformer<T, S> transformer;

-

-  OfTypeStreamTransformer(TypeToken<S> typeToken)

-      : transformer = _buildTransformer(typeToken);

-

-  @override

-  Stream<S> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      TypeToken<S> typeToken) {

-    return StreamTransformer<T, S>((Stream<T> input, bool cancelOnError) {

-      StreamController<S> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<S>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T value) {

-              try {

-                if (typeToken.isType(value)) {

-                  controller.add(typeToken.toType(value));

-                }

-              } catch (e, s) {

-                controller.addError(e, s);

-              }

-            },

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/on_error_resume.dart b/rxdart/lib/src/transformers/on_error_resume.dart
deleted file mode 100755
index a9455fb..0000000
--- a/rxdart/lib/src/transformers/on_error_resume.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'dart:async';

-

-/// Intercepts error events and switches to a recovery stream created by the

-/// provided recoveryFn function.

-///

-/// The OnErrorResumeStreamTransformer intercepts an onError notification from

-/// the source Stream. Instead of passing the error through to any

-/// listeners, it replaces it with another Stream of items created by the

-/// recoveryFn.

-///

-/// The recoveryFn receives the emitted error and returns a Stream. You can

-/// perform logic in the recoveryFn to return different Streams based on the

-/// type of error that was emitted.

-///

-/// ### Example

-///

-///     new Observable<int>.error(new Exception())

-///       .onErrorResume((dynamic e) =>

-///           new Observable.just(e is StateError ? 1 : 0)

-///       .listen(print); // prints 0

-class OnErrorResumeStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  OnErrorResumeStreamTransformer(Stream<T> Function(dynamic error) recoveryFn)

-      : transformer = _buildTransformer(recoveryFn);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-    Stream<T> Function(dynamic error) recoveryFn,

-  ) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamSubscription<T> inputSubscription;

-      StreamSubscription<T> recoverySubscription;

-      StreamController<T> controller;

-      var shouldCloseController = true;

-

-      void safeClose() {

-        if (shouldCloseController) {

-          controller.close();

-        }

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            inputSubscription = input.listen(

-              controller.add,

-              onError: (dynamic e, dynamic s) {

-                shouldCloseController = false;

-

-                recoverySubscription = recoveryFn(e).listen(

-                  controller.add,

-                  onError: controller.addError,

-                  onDone: controller.close,

-                  cancelOnError: cancelOnError,

-                );

-

-                inputSubscription.cancel();

-              },

-              onDone: safeClose,

-              cancelOnError: cancelOnError,

-            );

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            inputSubscription?.pause(resumeSignal);

-            recoverySubscription?.pause(resumeSignal);

-          },

-          onResume: () {

-            inputSubscription?.resume();

-            recoverySubscription?.resume();

-          },

-          onCancel: () {

-            return Future.wait<dynamic>(<Future<dynamic>>[

-              inputSubscription?.cancel(),

-              recoverySubscription?.cancel()

-            ].where((Future<dynamic> future) => future != null));

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/on_error_resume_next.dart b/rxdart/lib/src/transformers/on_error_resume_next.dart
deleted file mode 100755
index 889ca17..0000000
--- a/rxdart/lib/src/transformers/on_error_resume_next.dart
+++ /dev/null
@@ -1,76 +0,0 @@
-import 'dart:async';

-

-/// Deprecated: Please use OnErrorResumeStreamTransformer.

-///

-/// Intercepts error events and switches to the given recovery stream in

-/// that case

-///

-/// The OnErrorResumeNextStreamTransformer intercepts an onError notification from

-/// the source Stream. Instead of passing the error through to any

-/// listeners, it replaces it with another Stream of items.

-///

-/// ### Example

-///

-///     new ErrorStream(new Exception())

-///       .transform(new OnErrorResumeNextStreamTransformer(

-///         new Observable.fromIterable([1, 2, 3])))

-///       .listen(print); // prints 1, 2, 3

-@deprecated

-class OnErrorResumeNextStreamTransformer<T>

-    extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  OnErrorResumeNextStreamTransformer(Stream<T> recoveryStream)

-      : transformer = _buildTransformer(recoveryStream);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-      Stream<T> recoveryStream) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamSubscription<T> inputSubscription;

-      StreamSubscription<T> recoverySubscription;

-      StreamController<T> controller;

-      var shouldCloseController = true;

-

-      void safeClose() {

-        if (shouldCloseController) {

-          controller.close();

-        }

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            inputSubscription =

-                input.listen(controller.add, onError: (dynamic e, dynamic s) {

-              shouldCloseController = false;

-

-              recoverySubscription = recoveryStream.listen(controller.add,

-                  onError: controller.addError,

-                  onDone: controller.close,

-                  cancelOnError: cancelOnError);

-

-              inputSubscription.cancel();

-            }, onDone: safeClose, cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            inputSubscription?.pause(resumeSignal);

-            recoverySubscription?.pause(resumeSignal);

-          },

-          onResume: () {

-            inputSubscription?.resume();

-            recoverySubscription?.resume();

-          },

-          onCancel: () {

-            return Future.wait<dynamic>(<Future<dynamic>>[

-              inputSubscription?.cancel(),

-              recoverySubscription?.cancel()

-            ].where((Future<dynamic> future) => future != null));

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/sample.dart b/rxdart/lib/src/transformers/sample.dart
deleted file mode 100755
index 2cc941e..0000000
--- a/rxdart/lib/src/transformers/sample.dart
+++ /dev/null
@@ -1,84 +0,0 @@
-import 'dart:async';

-

-/// A StreamTransformer that, when the specified sample stream emits

-/// an item or completes, emits the most recently emitted item (if any)

-/// emitted by the source stream since the previous emission from

-/// the sample stream.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, 2, 3])

-///       .transform(new SampleStreamTransformer(new TimerStream(1, new Duration(seconds: 1)))

-///       .listen(print); // prints 3

-class SampleStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  SampleStreamTransformer(Stream<dynamic> sampleStream,

-      {bool sampleOnValueOnly = true})

-      : transformer = _buildTransformer(sampleStream,

-            sampleOnValueOnly: sampleOnValueOnly);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-      Stream<dynamic> sampleStream,

-      {bool sampleOnValueOnly = true}) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<dynamic> sampleSubscription;

-      T currentValue;

-      var hasValue = false;

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        if (hasValue) {

-          hasValue = false;

-          controller.add(currentValue);

-        }

-

-        controller.close();

-      }

-

-      void onSample(dynamic _) {

-        if (hasValue || !sampleOnValueOnly) {

-          controller.add(currentValue);

-          hasValue = false;

-          currentValue = null;

-        }

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            try {

-              subscription = input.listen((T value) {

-                hasValue = true;

-                currentValue = value;

-              },

-                  onError: controller.addError,

-                  onDone: onDone,

-                  cancelOnError: cancelOnError);

-

-              sampleSubscription = sampleStream.listen(onSample,

-                  onError: controller.addError,

-                  onDone: onDone,

-                  cancelOnError: cancelOnError);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () async {

-            await sampleSubscription.cancel();

-            await subscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/scan.dart b/rxdart/lib/src/transformers/scan.dart
deleted file mode 100755
index 0e479bf..0000000
--- a/rxdart/lib/src/transformers/scan.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-import 'dart:async';

-

-/// Applies an accumulator function over an observable sequence and returns

-/// each intermediate result. The optional seed value is used as the initial

-/// accumulator value.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, 2, 3])

-///        .transform(new ScanStreamTransformer((acc, curr, i) => acc + curr, 0))

-///        .listen(print); // prints 1, 3, 6

-class ScanStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final _ScanStreamTransformerAccumulator<T, S> accumulator;

-  final S seed;

-

-  ScanStreamTransformer(this.accumulator, [this.seed]);

-

-  @override

-  Stream<S> bind(Stream<T> stream) =>

-      _buildTransformer<T, S>(accumulator, seed).bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      S accumulator(S accumulated, T value, int index),

-      [S seed]) {

-    var index = 0;

-    var acc = seed;

-

-    return StreamTransformer<T, S>.fromHandlers(

-        handleData: (T data, EventSink<S> sink) {

-          acc = accumulator(acc, data, index++);

-

-          sink.add(acc);

-        },

-        handleError: (Object error, StackTrace s, EventSink<S> sink) =>

-            sink.addError(error));

-  }

-}

-

-typedef S _ScanStreamTransformerAccumulator<T, S>(

-    S accumulated, T value, int index);

diff --git a/rxdart/lib/src/transformers/skip_until.dart b/rxdart/lib/src/transformers/skip_until.dart
deleted file mode 100755
index 2647e53..0000000
--- a/rxdart/lib/src/transformers/skip_until.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'dart:async';

-

-/// Starts emitting items only after the given stream emits an item.

-///

-/// ### Example

-///

-///     new MergeStream([

-///       new Observable.just(1),

-///       new TimerStream(2, new Duration(minutes: 2))

-///     ])

-///     .transform(skipUntilTransformer(new TimerStream(1, new Duration(minutes: 1))))

-///     .listen(print); // prints 2;

-class SkipUntilStreamTransformer<T, S> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  SkipUntilStreamTransformer(Stream<S> otherStream)

-      : transformer = _buildTransformer(otherStream);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T, S>(

-      Stream<S> otherStream) {

-    if (otherStream == null) {

-      throw ArgumentError('otherStream cannot be null');

-    }

-

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> otherSubscription;

-      var goTime = false;

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        controller.close();

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T data) {

-              if (goTime) {

-                controller.add(data);

-              }

-            },

-                onError: controller.addError,

-                onDone: onDone,

-                cancelOnError: cancelOnError);

-

-            otherSubscription = otherStream.listen((_) {

-              goTime = true;

-

-              otherSubscription.cancel();

-            },

-                onError: controller.addError,

-                cancelOnError: cancelOnError,

-                onDone: onDone);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () async {

-            await otherSubscription?.cancel();

-            await subscription?.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/start_with.dart b/rxdart/lib/src/transformers/start_with.dart
deleted file mode 100755
index dcf5af2..0000000
--- a/rxdart/lib/src/transformers/start_with.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'dart:async';

-

-/// Prepends a value to the source Stream.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([2])

-///       .transform(new StartWithStreamTransformer(1))

-///       .listen(print); // prints 1, 2

-class StartWithStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  StartWithStreamTransformer(T startValue)

-      : transformer = _buildTransformer(startValue);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(T startValue) {

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            try {

-              controller.add(startValue);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-

-            subscription = input.listen(controller.add,

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/start_with_many.dart b/rxdart/lib/src/transformers/start_with_many.dart
deleted file mode 100755
index c823c42..0000000
--- a/rxdart/lib/src/transformers/start_with_many.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-import 'dart:async';

-

-/// Prepends a sequence of values to the source Stream.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([3])

-///       .transform(new StartWithManyStreamTransformer([1, 2]))

-///       .listen(print); // prints 1, 2, 3

-class StartWithManyStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  StartWithManyStreamTransformer(Iterable<T> startValues)

-      : transformer = _buildTransformer(startValues);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(Iterable<T> startValues) {

-    if (startValues == null) {

-      throw ArgumentError('startValues cannot be null');

-    }

-

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            startValues.forEach(controller.add);

-

-            subscription = input.listen(

-              controller.add,

-              onError: controller.addError,

-              onDone: controller.close,

-              cancelOnError: cancelOnError,

-            );

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/switch_if_empty.dart b/rxdart/lib/src/transformers/switch_if_empty.dart
deleted file mode 100755
index 8204946..0000000
--- a/rxdart/lib/src/transformers/switch_if_empty.dart
+++ /dev/null
@@ -1,91 +0,0 @@
-import 'dart:async';

-

-/// When the original observable emits no items, this operator subscribes to

-/// the given fallback stream and emits items from that observable instead.

-///

-/// This can be particularly useful when consuming data from multiple sources.

-/// For example, when using the Repository Pattern. Assuming you have some

-/// data you need to load, you might want to start with the fastest access

-/// point and keep falling back to the slowest point. For example, first query

-/// an in-memory database, then a database on the file system, then a network

-/// call if the data isn't on the local machine.

-///

-/// This can be achieved quite simply with switchIfEmpty!

-///

-/// ### Example

-///

-///     // Let's pretend we have some Data sources that complete without emitting

-///     // any items if they don't contain the data we're looking for

-///     Stream<Data> memory;

-///     Stream<Data> disk;

-///     Stream<Data> network;

-///

-///     // Start with memory, fallback to disk, then fallback to network.

-///     // Simple as that!

-///     Stream<Data> getThatData =

-///         memory.switchIfEmpty(disk).switchIfEmpty(network);

-class SwitchIfEmptyStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  SwitchIfEmptyStreamTransformer(Stream<T> fallbackStream)

-      : transformer = _buildTransformer(fallbackStream);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(

-      Stream<T> fallbackStream) {

-    if (fallbackStream == null) {

-      throw ArgumentError('fallbackStream cannot be null');

-    }

-

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> defaultSubscription;

-      StreamSubscription<T> switchSubscription;

-      var hasEvent = false;

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        controller.close();

-        switchSubscription?.cancel();

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            defaultSubscription = input.listen(

-                (T value) {

-                  hasEvent = true;

-                  controller.add(value);

-                },

-                onError: controller.addError,

-                onDone: () {

-                  if (hasEvent) {

-                    controller.close();

-                  } else {

-                    switchSubscription = fallbackStream.listen(

-                      controller.add,

-                      onError: controller.addError,

-                      onDone: onDone,

-                      cancelOnError: cancelOnError,

-                    );

-                  }

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            defaultSubscription?.pause(resumeSignal);

-            switchSubscription?.pause(resumeSignal);

-          },

-          onResume: () {

-            defaultSubscription?.resume();

-            switchSubscription?.resume();

-          },

-          onCancel: () => defaultSubscription?.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/switch_map.dart b/rxdart/lib/src/transformers/switch_map.dart
deleted file mode 100755
index 559b230..0000000
--- a/rxdart/lib/src/transformers/switch_map.dart
+++ /dev/null
@@ -1,82 +0,0 @@
-import 'dart:async';

-

-/// Converts each emitted item into a new Stream using the given mapper

-/// function. The newly created Stream will be be listened to and begin

-/// emitting items, and any previously created Stream will stop emitting.

-///

-/// The switchMap operator is similar to the flatMap and concatMap

-/// methods, but it only emits items from the most recently created Stream.

-///

-/// This can be useful when you only want the very latest state from

-/// asynchronous APIs, for example.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([4, 3, 2, 1])

-///       .transform(new SwitchMapStreamTransformer((i) =>

-///         new Stream.fromFuture(

-///           new Future.delayed(new Duration(minutes: i), () => i))

-///       .listen(print); // prints 1

-class SwitchMapStreamTransformer<T, S> extends StreamTransformerBase<T, S> {

-  final StreamTransformer<T, S> transformer;

-

-  SwitchMapStreamTransformer(Stream<S> mapper(T value))

-      : transformer = _buildTransformer(mapper);

-

-  @override

-  Stream<S> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, S> _buildTransformer<T, S>(

-      Stream<S> mapper(T value)) {

-    return StreamTransformer<T, S>((Stream<T> input, bool cancelOnError) {

-      StreamController<S> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> otherSubscription;

-      var leftClosed = false, rightClosed = false, hasMainEvent = false;

-

-      controller = StreamController<S>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(

-                (T value) {

-                  try {

-                    otherSubscription?.cancel();

-

-                    hasMainEvent = true;

-

-                    otherSubscription = mapper(value).listen(controller.add,

-                        onError: controller.addError, onDone: () {

-                      rightClosed = true;

-

-                      if (leftClosed) controller.close();

-                    });

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-                },

-                onError: controller.addError,

-                onDone: () {

-                  leftClosed = true;

-

-                  if (rightClosed || !hasMainEvent) controller.close();

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) {

-            subscription.pause(resumeSignal);

-            otherSubscription?.pause(resumeSignal);

-          },

-          onResume: () {

-            subscription.resume();

-            otherSubscription?.resume();

-          },

-          onCancel: () async {

-            await subscription.cancel();

-

-            if (hasMainEvent) await otherSubscription.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/take_until.dart b/rxdart/lib/src/transformers/take_until.dart
deleted file mode 100755
index 81f42e4..0000000
--- a/rxdart/lib/src/transformers/take_until.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-import 'dart:async';

-

-/// Returns the values from the source observable sequence until the other

-/// stream sequence produces a value.

-///

-/// ### Example

-///

-///     new MergeStream([

-///         new Stream.fromIterable([1]),

-///         new TimerStream(2, new Duration(minutes: 1))

-///       ])

-///       .transform(new TakeUntilStreamTransformer(

-///         new TimerStream(3, new Duration(seconds: 10))))

-///       .listen(print); // prints 1

-class TakeUntilStreamTransformer<T, S> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  TakeUntilStreamTransformer(Stream<S> otherStream)

-      : transformer = _buildTransformer(otherStream);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T, S>(

-      Stream<S> otherStream) {

-    if (otherStream == null) {

-      throw ArgumentError("take until stream cannot be null");

-    }

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> otherSubscription;

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        controller.close();

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen(controller.add,

-                onError: controller.addError,

-                onDone: onDone,

-                cancelOnError: cancelOnError);

-

-            otherSubscription = otherStream.listen((_) => onDone(),

-                onError: controller.addError,

-                cancelOnError: cancelOnError,

-                onDone: onDone);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () async {

-            await otherSubscription?.cancel();

-            await subscription?.cancel();

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/throttle.dart b/rxdart/lib/src/transformers/throttle.dart
deleted file mode 100755
index 8f88b39..0000000
--- a/rxdart/lib/src/transformers/throttle.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-import 'dart:async';

-

-/// A StreamTransformer that emits only the first item emitted by the source

-/// Stream during sequential time windows of a specified duration.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, 2, 3])

-///       .transform(new ThrottleStreamTransformer(new Duration(seconds: 1)))

-///       .listen(print); // prints 1

-class ThrottleStreamTransformer<T> extends StreamTransformerBase<T, T> {

-  final StreamTransformer<T, T> transformer;

-

-  ThrottleStreamTransformer(Duration duration)

-      : transformer = _buildTransformer(duration);

-

-  @override

-  Stream<T> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, T> _buildTransformer<T>(Duration duration) {

-    if (duration == null) {

-      throw ArgumentError('duration cannot be null');

-    }

-

-    return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {

-      StreamController<T> controller;

-      StreamSubscription<T> subscription;

-      Timer _timer;

-      var _closeAfterNextEvent = false;

-

-      bool _resetTimer() {

-        if (_timer != null && _timer.isActive) return false;

-

-        try {

-          _timer = Timer(duration, () {

-            if (_closeAfterNextEvent && !controller.isClosed)

-              controller.close();

-          });

-        } catch (e, s) {

-          controller.addError(e, s);

-        }

-

-        return true;

-      }

-

-      controller = StreamController<T>(

-          sync: true,

-          onListen: () {

-            subscription = input

-                .where((_) => _resetTimer())

-                .listen(controller.add, onError: controller.addError,

-                    onDone: () {

-              _closeAfterNextEvent = true;

-            }, cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/transformers/time_interval.dart b/rxdart/lib/src/transformers/time_interval.dart
deleted file mode 100755
index ee8adb4..0000000
--- a/rxdart/lib/src/transformers/time_interval.dart
+++ /dev/null
@@ -1,90 +0,0 @@
-import 'dart:async';

-

-/// Records the time interval between consecutive values in an observable

-/// sequence.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1])

-///       .transform(new IntervalStreamTransformer(new Duration(seconds: 1)))

-///       .transform(new TimeIntervalStreamTransformer())

-///       .listen(print); // prints TimeInterval{interval: 0:00:01, value: 1}

-class TimeIntervalStreamTransformer<T>

-    extends StreamTransformerBase<T, TimeInterval<T>> {

-  final StreamTransformer<T, TimeInterval<T>> transformer;

-

-  TimeIntervalStreamTransformer() : transformer = _buildTransformer();

-

-  @override

-  Stream<TimeInterval<T>> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, TimeInterval<T>> _buildTransformer<T>() {

-    return StreamTransformer<T, TimeInterval<T>>(

-        (Stream<T> input, bool cancelOnError) {

-      StreamController<TimeInterval<T>> controller;

-      StreamSubscription<T> subscription;

-

-      controller = StreamController<TimeInterval<T>>(

-          sync: true,

-          onListen: () {

-            var stopwatch = Stopwatch()..start();

-            int ems;

-

-            subscription = input.listen(

-                (T value) {

-                  ems = stopwatch.elapsedMicroseconds;

-

-                  stopwatch.stop();

-

-                  try {

-                    controller.add(

-                        TimeInterval<T>(value, Duration(microseconds: ems)));

-                  } catch (e, s) {

-                    controller.addError(e, s);

-                  }

-

-                  stopwatch = Stopwatch()..start();

-                },

-                onError: controller.addError,

-                onDone: () {

-                  stopwatch.stop();

-                  controller.close();

-                },

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

-

-class TimeInterval<T> {

-  final Duration interval;

-  final T value;

-

-  TimeInterval(this.value, this.interval);

-

-  @override

-  bool operator ==(Object other) {

-    if (identical(this, other)) {

-      return true;

-    }

-    return other is TimeInterval &&

-        this.interval == other.interval &&

-        this.value == other.value;

-  }

-

-  @override

-  int get hashCode {

-    return interval.hashCode ^ value.hashCode;

-  }

-

-  @override

-  String toString() {

-    return 'TimeInterval{interval: $interval, value: $value}';

-  }

-}

diff --git a/rxdart/lib/src/transformers/timestamp.dart b/rxdart/lib/src/transformers/timestamp.dart
deleted file mode 100755
index 3a7b54b..0000000
--- a/rxdart/lib/src/transformers/timestamp.dart
+++ /dev/null
@@ -1,71 +0,0 @@
-import 'dart:async';

-

-/// Wraps each item emitted by the source Observable in a [Timestamped] object

-/// that includes the emitted item and the time when the item was emitted.

-///

-/// Example

-///

-///     new Stream.fromIterable([1])

-///        .transform(new TimestampStreamTransformer())

-///        .listen((i) => print(i)); // prints 'TimeStamp{timestamp: XXX, value: 1}';

-class TimestampStreamTransformer<T>

-    extends StreamTransformerBase<T, Timestamped<T>> {

-  final StreamTransformer<T, Timestamped<T>> transformer;

-

-  TimestampStreamTransformer() : transformer = _buildTransformer();

-

-  @override

-  Stream<Timestamped<T>> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, Timestamped<T>> _buildTransformer<T>() {

-    return StreamTransformer<T, Timestamped<T>>(

-        (Stream<T> input, bool cancelOnError) {

-      StreamController<Timestamped<T>> controller;

-      StreamSubscription<Timestamped<T>> subscription;

-

-      controller = StreamController<Timestamped<T>>(

-          sync: true,

-          onListen: () {

-            subscription = input

-                .map((T value) => Timestamped<T>(DateTime.now(), value))

-                .listen(controller.add,

-                    onError: controller.addError,

-                    onDone: controller.close,

-                    cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-}

-

-class Timestamped<T> {

-  final T value;

-  final DateTime timestamp;

-

-  Timestamped(this.timestamp, this.value);

-

-  @override

-  bool operator ==(Object other) {

-    if (identical(this, other)) {

-      return true;

-    }

-    return other is Timestamped &&

-        this.timestamp == other.timestamp &&

-        this.value == other.value;

-  }

-

-  @override

-  int get hashCode {

-    return timestamp.hashCode ^ value.hashCode;

-  }

-

-  @override

-  String toString() {

-    return 'TimeStamp{timestamp: $timestamp, value: $value}';

-  }

-}

diff --git a/rxdart/lib/src/transformers/window.dart b/rxdart/lib/src/transformers/window.dart
deleted file mode 100755
index 210db16..0000000
--- a/rxdart/lib/src/transformers/window.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-import 'dart:async';

-

-import 'package:rxdart/src/samplers/buffer_strategy.dart';

-

-/// Creates an Observable where each item is a [Stream] containing the items

-/// from the source sequence, batched by the [sampler].

-///

-/// ### Example with [onCount]

-///

-///     Observable.range(1, 4)

-///       .window(onCount(2))

-///       .doOnData((_) => print('next window'))

-///       .flatMap((s) => s)

-///       .listen(print); // prints next window 1, 2, next window 3, 4

-///

-/// ### Example with [onFuture]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .window(onFuture(() => new Future.delayed(const Duration(milliseconds: 220))))

-///       .doOnData((_) => print('next window'))

-///       .flatMap((s) => s)

-///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-///

-/// ### Example with [onTest]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .window(onTest((i) => i % 2 == 0))

-///       .doOnData((_) => print('next window'))

-///       .flatMap((s) => s)

-///       .listen(print); // prints next window 0, next window 1, 2 next window 3, 4,  ...

-///

-/// ### Example with [onTime]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .window(onTime(const Duration(milliseconds: 220)))

-///       .doOnData((_) => print('next window'))

-///       .flatMap((s) => s)

-///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-///

-/// ### Example with [onStream]

-///

-///     new Observable.periodic(const Duration(milliseconds: 100), (int i) => i)

-///       .window(onStream(new Stream.periodic(const Duration(milliseconds: 220), (int i) => i)))

-///       .doOnData((_) => print('next window'))

-///       .flatMap((s) => s)

-///       .listen(print); // prints next window 0, 1, next window 2, 3, ...

-///

-/// You can create your own sampler by extending [StreamView]

-/// should the above samplers be insufficient for your use case.

-class WindowStreamTransformer<T> extends StreamTransformerBase<T, Stream<T>> {

-  final SamplerBuilder<T, Stream<T>> sampler;

-  final bool exhaustBufferOnDone;

-

-  WindowStreamTransformer(this.sampler, {this.exhaustBufferOnDone = true});

-

-  @override

-  Stream<Stream<T>> bind(Stream<T> stream) =>

-      _buildTransformer<T>(sampler, exhaustBufferOnDone).bind(stream);

-

-  static StreamTransformer<T, Stream<T>> _buildTransformer<T>(

-      SamplerBuilder<T, Stream<T>> scheduler, bool exhaustBufferOnDone) {

-    assertSampler(scheduler);

-

-    return StreamTransformer<T, Stream<T>>(

-        (Stream<T> input, bool cancelOnError) {

-      StreamController<Stream<T>> controller;

-      StreamSubscription<Stream<T>> subscription;

-      var buffer = <T>[];

-

-      void onDone() {

-        if (controller.isClosed) return;

-

-        if (exhaustBufferOnDone && buffer.isNotEmpty)

-          controller.add(Stream<T>.fromIterable(buffer));

-

-        controller.close();

-      }

-

-      controller = StreamController<Stream<T>>(

-          sync: true,

-          onListen: () {

-            try {

-              subscription = scheduler(input, (T data,

-                  EventSink<Stream<T>> sink, [int startBufferEvery = 0]) {

-                buffer.add(data);

-                sink.add(Stream<T>.fromIterable(buffer));

-              }, (_, EventSink<Stream<T>> sink, [int startBufferEvery = 0]) {

-                startBufferEvery ?? 0;

-

-                sink.add(Stream<T>.fromIterable(buffer));

-                buffer =

-                    startBufferEvery > 0 && startBufferEvery < buffer.length

-                        ? buffer.sublist(startBufferEvery)

-                        : <T>[];

-              }).listen(controller.add,

-                  onError: controller.addError,

-                  onDone: onDone,

-                  cancelOnError: cancelOnError);

-            } catch (e, s) {

-              controller.addError(e, s);

-            }

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () => subscription.cancel());

-

-      return controller.stream.listen(null);

-    });

-  }

-

-  static void assertSampler<T>(SamplerBuilder<T, Stream<T>> scheduler) {

-    if (scheduler == null) {

-      throw ArgumentError('scheduler cannot be null');

-    }

-  }

-}

diff --git a/rxdart/lib/src/transformers/with_latest_from.dart b/rxdart/lib/src/transformers/with_latest_from.dart
deleted file mode 100755
index cdb3a41..0000000
--- a/rxdart/lib/src/transformers/with_latest_from.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-import 'dart:async';

-

-/// A StreamTransformer that emits when the source stream emits, combining

-/// the latest values from the two streams using the provided function.

-///

-/// If the latestFromStream has not emitted any values, this stream will not

-/// emit either.

-///

-/// [Interactive marble diagram](http://rxmarbles.com/#withLatestFrom)

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, 2]).transform(

-///       new WithLatestFromStreamTransformer(

-///         new Stream.fromIterable([2, 3]), (a, b) => a + b)

-///       .listen(print); // prints 4 (due to the async nature of streams)

-class WithLatestFromStreamTransformer<T, S, R>

-    extends StreamTransformerBase<T, R> {

-  final StreamTransformer<T, R> transformer;

-

-  WithLatestFromStreamTransformer(Stream<S> latestFromStream, R fn(T t, S s))

-      : transformer = _buildTransformer(latestFromStream, fn);

-

-  @override

-  Stream<R> bind(Stream<T> stream) => transformer.bind(stream);

-

-  static StreamTransformer<T, R> _buildTransformer<T, S, R>(

-      Stream<S> latestFromStream, R fn(T t, S s)) {

-    if (latestFromStream == null) {

-      throw ArgumentError('latestFromStream cannot be null');

-    } else if (fn == null) {

-      throw ArgumentError('combiner cannot be null');

-    }

-

-    return StreamTransformer<T, R>((Stream<T> input, bool cancelOnError) {

-      StreamController<R> controller;

-      StreamSubscription<T> subscription;

-      StreamSubscription<S> latestFromSubscription;

-      S latestValue;

-

-      controller = StreamController<R>(

-          sync: true,

-          onListen: () {

-            subscription = input.listen((T value) {

-              if (latestValue != null) {

-                try {

-                  controller.add(fn(value, latestValue));

-                } catch (e, s) {

-                  controller.addError(e, s);

-                }

-              }

-            }, onError: controller.addError);

-

-            latestFromSubscription = latestFromStream.listen((S latest) {

-              latestValue = latest;

-            },

-                onError: controller.addError,

-                onDone: controller.close,

-                cancelOnError: cancelOnError);

-          },

-          onPause: ([Future<dynamic> resumeSignal]) =>

-              subscription.pause(resumeSignal),

-          onResume: () => subscription.resume(),

-          onCancel: () {

-            return Future.wait<dynamic>(<Future<dynamic>>[

-              subscription.cancel(),

-              latestFromSubscription.cancel()

-            ].where((Future<dynamic> cancelFuture) => cancelFuture != null));

-          });

-

-      return controller.stream.listen(null);

-    });

-  }

-}

diff --git a/rxdart/lib/src/utils/composite_subscription.dart b/rxdart/lib/src/utils/composite_subscription.dart
deleted file mode 100755
index de0d7b9..0000000
--- a/rxdart/lib/src/utils/composite_subscription.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-import 'dart:async';

-

-/// Acts as a container for multiple subscriptions that can be canceled at once

-/// e.g. view subcriptions in Flutter that need to be canceled on view disposal

-///

-/// Can be cleared or disposed. When disposed, cannot be used again.

-/// ### Example

-/// // init your subscriptions

-/// composite.add(observable1.listen(listener1))

-/// ..add(observable2.listen(listener1))

-/// ..add(observable3.listen(listener1));

-///

-/// // clear them all at once

-/// composite.clear();

-class CompositeSubscription {

-  bool _isDisposed = false;

-

-  final List<StreamSubscription<dynamic>> _subscriptionsList =

-      List<StreamSubscription<dynamic>>();

-

-  /// Checks if this composite is disposed. If it is, the composite can't be used again

-  /// and will throw an error if you try to add more subscriptions to it.

-  bool get isDisposed => _isDisposed;

-

-  /// Adds new subscription to this composite.

-  ///

-  /// Throws an exception if this composite was disposed

-  StreamSubscription<T> add<T>(StreamSubscription<T> subscription) {

-    if (isDisposed)

-      throw ("This composite was disposed, try to use new instance instead");

-    _subscriptionsList.add(subscription);

-    return subscription;

-  }

-

-  /// Cancels subscripiton and removes it from this composite.

-  void remove(StreamSubscription<dynamic> subscription) {

-    subscription.cancel();

-    _subscriptionsList.remove(subscription);

-  }

-

-  /// Cancels all subscriptions added to this composite. Clears subscriptions collection.

-  ///

-  /// This composite can be reused after calling this method.

-  void clear() {

-    _subscriptionsList.forEach(

-        (StreamSubscription<dynamic> subscription) => subscription.cancel());

-    _subscriptionsList.clear();

-  }

-

-  /// Cancels all subscriptions added to this composite. Disposes this.

-  ///

-  /// This composite can't be reused after calling this method.

-  void dispose() {

-    clear();

-    _isDisposed = true;

-  }

-}

diff --git a/rxdart/lib/src/utils/notification.dart b/rxdart/lib/src/utils/notification.dart
deleted file mode 100755
index 992167e..0000000
--- a/rxdart/lib/src/utils/notification.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-/// The type of event

-enum Kind { OnData, OnDone, OnError }

-

-/// A class that encapsulates the [Kind] of event, value of the event in case of

-/// onData, or the Error in the case of onError.

-

-/// A container object that wraps the [Kind] of event (OnData, OnDone, OnError),

-/// and the item or error that was emitted. In the case of onDone, no data is

-/// emitted as part of the [Notification].

-class Notification<T> {

-  final Kind kind;

-  final T value;

-  final dynamic error;

-  final StackTrace stackTrace;

-

-  Notification(this.kind, this.value, this.error, this.stackTrace);

-

-  factory Notification.onData(T value) =>

-      Notification<T>(Kind.OnData, value, null, null);

-

-  factory Notification.onDone() =>

-      Notification<T>(Kind.OnDone, null, null, null);

-

-  factory Notification.onError(dynamic e, StackTrace s) =>

-      Notification<T>(Kind.OnError, null, e, s);

-

-  @override

-  bool operator ==(Object other) {

-    if (identical(this, other)) {

-      return true;

-    }

-    return other is Notification &&

-        this.kind == other.kind &&

-        this.error == other.error &&

-        this.stackTrace == other.stackTrace &&

-        this.value == other.value;

-  }

-

-  @override

-  int get hashCode {

-    return kind.hashCode ^

-        error.hashCode ^

-        stackTrace.hashCode ^

-        value.hashCode;

-  }

-

-  @override

-  String toString() {

-    return 'Notification{kind: $kind, value: $value, error: $error, stackTrace: $stackTrace}';

-  }

-

-  bool get isOnData => kind == Kind.OnData;

-

-  bool get isOnDone => kind == Kind.OnDone;

-

-  bool get isOnError => kind == Kind.OnError;

-}

diff --git a/rxdart/lib/src/utils/type_token.dart b/rxdart/lib/src/utils/type_token.dart
deleted file mode 100755
index c552215..0000000
--- a/rxdart/lib/src/utils/type_token.dart
+++ /dev/null
@@ -1,42 +0,0 @@
-/// A class that captures the Type to filter down to using `ofType` or `cast`.

-///

-/// Given the way Dart generics work, one cannot simply use the `is T` / `as T`

-/// checks and castings within an ofTypeObservable itself. Therefore, this class

-/// was introduced to capture the type of class you'd like `ofType` to filter

-/// down to, or `cast` to cast to.

-///

-/// ### Example

-///

-///     new Stream.fromIterable([1, "hi"])

-///       .ofType(new TypeToken<String>)

-///       .listen(print); // prints "hi"

-class TypeToken<S> {

-  const TypeToken();

-

-  bool isType(dynamic other) {

-    return other is S;

-  }

-

-  S toType(dynamic other) {

-    // ignore: avoid_as

-    return other as S;

-  }

-}

-

-/// Filter the observable to only Booleans

-const TypeToken<bool> kBool = TypeToken<bool>();

-

-/// Filter the observable to only Doubles

-const TypeToken<double> kDouble = TypeToken<double>();

-

-/// Filter the observable to only Integers

-const TypeToken<int> kInt = TypeToken<int>();

-

-/// Filter the observable to only Numbers

-const TypeToken<num> kNum = TypeToken<num>();

-

-/// Filter the observable to only Strings

-const TypeToken<String> kString = TypeToken<String>();

-

-/// Filter the observable to only Symbols

-const TypeToken<Symbol> kSymbol = TypeToken<Symbol>();

diff --git a/rxdart/lib/streams.dart b/rxdart/lib/streams.dart
deleted file mode 100755
index abaff25..0000000
--- a/rxdart/lib/streams.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-library rx_streams;

-

-export 'package:rxdart/src/streams/amb.dart';

-export 'package:rxdart/src/streams/combine_latest.dart';

-export 'package:rxdart/src/streams/concat.dart';

-export 'package:rxdart/src/streams/concat_eager.dart';

-export 'package:rxdart/src/streams/defer.dart';

-export 'package:rxdart/src/streams/error.dart';

-export 'package:rxdart/src/streams/merge.dart';

-export 'package:rxdart/src/streams/never.dart';

-export 'package:rxdart/src/streams/race.dart';

-export 'package:rxdart/src/streams/range.dart';

-export 'package:rxdart/src/streams/repeat.dart';

-export 'package:rxdart/src/streams/retry.dart';

-export 'package:rxdart/src/streams/retry_when.dart';

-export 'package:rxdart/src/streams/switch_latest.dart';

-export 'package:rxdart/src/streams/timer.dart';

-export 'package:rxdart/src/streams/utils.dart';

-export 'package:rxdart/src/streams/zip.dart';

diff --git a/rxdart/lib/subjects.dart b/rxdart/lib/subjects.dart
deleted file mode 100755
index fe0f664..0000000
--- a/rxdart/lib/subjects.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-library rx_subjects;

-

-export 'package:rxdart/src/subjects/subject.dart';

-export 'package:rxdart/src/subjects/behavior_subject.dart';

-export 'package:rxdart/src/subjects/publish_subject.dart';

-export 'package:rxdart/src/subjects/replay_subject.dart';

diff --git a/rxdart/lib/transformers.dart b/rxdart/lib/transformers.dart
deleted file mode 100755
index b0a1488..0000000
--- a/rxdart/lib/transformers.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-library rx_transformers;

-

-export 'package:rxdart/src/transformers/buffer.dart';

-export 'package:rxdart/src/transformers/debounce.dart';

-export 'package:rxdart/src/transformers/default_if_empty.dart';

-export 'package:rxdart/src/transformers/delay.dart';

-export 'package:rxdart/src/transformers/dematerialize.dart';

-export 'package:rxdart/src/transformers/distinct_unique.dart';

-export 'package:rxdart/src/transformers/do.dart';

-export 'package:rxdart/src/transformers/exhaust_map.dart';

-export 'package:rxdart/src/transformers/flat_map.dart';

-export 'package:rxdart/src/transformers/flat_map_latest.dart';

-export 'package:rxdart/src/transformers/group_by.dart';

-export 'package:rxdart/src/transformers/ignore_elements.dart';

-export 'package:rxdart/src/transformers/interval.dart';

-export 'package:rxdart/src/transformers/map_to.dart';

-export 'package:rxdart/src/transformers/materialize.dart';

-export 'package:rxdart/src/transformers/of_type.dart';

-export 'package:rxdart/src/transformers/on_error_resume_next.dart';

-export 'package:rxdart/src/transformers/on_error_resume.dart';

-export 'package:rxdart/src/transformers/sample.dart';

-export 'package:rxdart/src/transformers/scan.dart';

-export 'package:rxdart/src/transformers/skip_until.dart';

-export 'package:rxdart/src/transformers/start_with.dart';

-export 'package:rxdart/src/transformers/start_with_many.dart';

-export 'package:rxdart/src/transformers/switch_if_empty.dart';

-export 'package:rxdart/src/transformers/switch_map.dart';

-export 'package:rxdart/src/transformers/take_until.dart';

-export 'package:rxdart/src/transformers/throttle.dart';

-export 'package:rxdart/src/transformers/time_interval.dart';

-export 'package:rxdart/src/transformers/timestamp.dart';

-export 'package:rxdart/src/transformers/window.dart';

-export 'package:rxdart/src/transformers/with_latest_from.dart';

-export 'package:rxdart/src/utils/notification.dart';

-export 'package:rxdart/src/utils/type_token.dart';

diff --git a/rxdart/pubspec.yaml b/rxdart/pubspec.yaml
deleted file mode 100755
index 0d3955a..0000000
--- a/rxdart/pubspec.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: rxdart

-version: 0.21.0

-authors:

-- Frank Pepermans <frank@igindo.com>

-- Brian Egan <brian@brianegan.com>

-description: >

-  RxDart is an implementation of the popular reactiveX api for asynchronous

-  programming, leveraging the native Dart Streams api.

-homepage: https://github.com/ReactiveX/rxdart

-

-environment:

-  sdk: '>=2.0.0-dev <3.0.0'

-

-dev_dependencies:

-  test: ^1.0.0

-  benchmark_harness: ^1.0.0

-  coverage: '>=0.11.0 <0.13.0'

-  stack_trace: ^1.9.2

-  build_runner: ^0.9.0

-  build_web_compilers: ^0.4.0

-  mockito: ^3.0.0

diff --git a/rxdart/tool/check_format.sh b/rxdart/tool/check_format.sh
deleted file mode 100755
index 21c3e3f..0000000
--- a/rxdart/tool/check_format.sh
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/bin/sh

-

-# Determine which files have been modified and should be run through the formatter

-dart_files=$(git diff --cached --name-only --diff-filter=ACM | grep '.dart$')

-[ -z "$dart_files" ] && exit 0

-

-# Run dartfmt to check if the modified files are properly formatted.

-unformatted=$(dartfmt -n ${dart_files})

-[ -z "$unformatted" ] && exit 0

-

-# If the files are not properly formatted. Print message and fail.

-echo >&2 "The following dart files must be formatted with dartfmt. Please run: dartfmt -w ./**/*.dart from the root of the git repository."

-for fn in ${unformatted}; do

-  echo >&2 "./$fn"

-done

-

-exit 1

diff --git a/rxdart/tool/travis.sh b/rxdart/tool/travis.sh
deleted file mode 100755
index c37a142..0000000
--- a/rxdart/tool/travis.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-# Ensure the source is formatted

-$(dirname -- "$0")/ensure_dartfmt.sh

-

-# Fast fail the script on failures.

-set -e

-

-# Run all tests.

-pub run test -p content-shell test/all_tests.html

-

-# Install dart_coveralls; gather and send coverage data.

-if [ "$REPO_TOKEN" ]; then

-  pub global activate dart_coveralls

-  pub global run dart_coveralls report \

-    --token $REPO_TOKEN \

-    --retry 2 \

-    --exclude-test-files \

-    test/all_tests.dart

-fi

diff --git a/video_player/BUILD.gn b/video_player/BUILD.gn
index 7c40252..6bb3b13 100644
--- a/video_player/BUILD.gn
+++ b/video_player/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for video_player-0.10.1+6
+# This file is generated by importer.py for video_player-0.10.2
 
 import("//build/dart/dart_library.gni")
 
diff --git a/video_player/CHANGELOG.md b/video_player/CHANGELOG.md
index 207a9ca..abf41af 100644
--- a/video_player/CHANGELOG.md
+++ b/video_player/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.10.2
+
+* **Android Only** Adds optional VideoFormat used to signal what format the plugin should try.
+
+## 0.10.1+7
+
+* Fix tests by ignoring deprecated member use.
+
 ## 0.10.1+6
 
 * [iOS] Fixed a memory leak with notification observing.
diff --git a/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java
index 2193391..1883527 100644
--- a/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java
+++ b/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java
@@ -1,3 +1,7 @@
+// Copyright 2019 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.
+
 package io.flutter.plugins.videoplayer;
 
 import io.flutter.plugin.common.EventChannel;
diff --git a/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
index 0be5e77..50c26d4 100644
--- a/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
+++ b/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
@@ -53,6 +53,10 @@
 public class VideoPlayerPlugin implements MethodCallHandler {
 
   private static class VideoPlayer {
+    private static final String FORMAT_SS = "ss";
+    private static final String FORMAT_DASH = "dash";
+    private static final String FORMAT_HLS = "hls";
+    private static final String FORMAT_OTHER = "other";
 
     private SimpleExoPlayer exoPlayer;
 
@@ -71,7 +75,8 @@
         EventChannel eventChannel,
         TextureRegistry.SurfaceTextureEntry textureEntry,
         String dataSource,
-        Result result) {
+        Result result,
+        String formatHint) {
       this.eventChannel = eventChannel;
       this.textureEntry = textureEntry;
 
@@ -93,7 +98,7 @@
                 true);
       }
 
-      MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, context);
+      MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
       exoPlayer.prepare(mediaSource);
 
       setupVideoPlayer(eventChannel, textureEntry, result);
@@ -108,8 +113,29 @@
     }
 
     private MediaSource buildMediaSource(
-        Uri uri, DataSource.Factory mediaDataSourceFactory, Context context) {
-      int type = Util.inferContentType(uri.getLastPathSegment());
+        Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
+      int type;
+      if (formatHint == null) {
+        type = Util.inferContentType(uri.getLastPathSegment());
+      } else {
+        switch (formatHint) {
+          case FORMAT_SS:
+            type = C.TYPE_SS;
+            break;
+          case FORMAT_DASH:
+            type = C.TYPE_DASH;
+            break;
+          case FORMAT_HLS:
+            type = C.TYPE_HLS;
+            break;
+          case FORMAT_OTHER:
+            type = C.TYPE_OTHER;
+            break;
+          default:
+            type = -1;
+            break;
+        }
+      }
       switch (type) {
         case C.TYPE_SS:
           return new SsMediaSource.Factory(
@@ -303,7 +329,8 @@
   }
 
   private void onDestroy() {
-    // The whole FlutterView is being destroyed. Here we release resources acquired for all instances
+    // The whole FlutterView is being destroyed. Here we release resources acquired for all
+    // instances
     // of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may
     // be replaced with just asserting that videoPlayers.isEmpty().
     // https://github.com/flutter/flutter/issues/20989 tracks this.
@@ -343,12 +370,18 @@
                     eventChannel,
                     handle,
                     "asset:///" + assetLookupKey,
-                    result);
+                    result,
+                    null);
             videoPlayers.put(handle.id(), player);
           } else {
             player =
                 new VideoPlayer(
-                    registrar.context(), eventChannel, handle, call.argument("uri"), result);
+                    registrar.context(),
+                    eventChannel,
+                    handle,
+                    call.argument("uri"),
+                    result,
+                    call.argument("formatHint"));
             videoPlayers.put(handle.id(), player);
           }
           break;
diff --git a/video_player/example/lib/main.dart b/video_player/example/lib/main.dart
index 320df27..dea1086 100644
--- a/video_player/example/lib/main.dart
+++ b/video_player/example/lib/main.dart
@@ -246,6 +246,8 @@
           leading: const Icon(Icons.airline_seat_flat_angled),
           title: Text(title),
         ),
+        // TODO(jackson): Remove when deprecation is on stable branch
+        // ignore: deprecated_member_use
         ButtonTheme.bar(
           child: ButtonBar(
             children: <Widget>[
diff --git a/video_player/lib/video_player.dart b/video_player/lib/video_player.dart
index 3990f53..059799b 100644
--- a/video_player/lib/video_player.dart
+++ b/video_player/lib/video_player.dart
@@ -32,6 +32,8 @@
   String toString() => '$runtimeType(start: $start, end: $end)';
 }
 
+enum VideoFormat { dash, hls, ss, other }
+
 /// The duration, current position, buffering state, error state and settings
 /// of a [VideoPlayerController].
 class VideoPlayerValue {
@@ -148,6 +150,7 @@
   /// package and null otherwise.
   VideoPlayerController.asset(this.dataSource, {this.package})
       : dataSourceType = DataSourceType.asset,
+        formatHint = null,
         super(VideoPlayerValue(duration: null));
 
   /// Constructs a [VideoPlayerController] playing a video from obtained from
@@ -155,7 +158,9 @@
   ///
   /// The URI for the video is given by the [dataSource] argument and must not be
   /// null.
-  VideoPlayerController.network(this.dataSource)
+  /// **Android only**: The [formatHint] option allows the caller to override
+  /// the video format detection code.
+  VideoPlayerController.network(this.dataSource, {this.formatHint})
       : dataSourceType = DataSourceType.network,
         package = null,
         super(VideoPlayerValue(duration: null));
@@ -168,10 +173,12 @@
       : dataSource = 'file://${file.path}',
         dataSourceType = DataSourceType.file,
         package = null,
+        formatHint = null,
         super(VideoPlayerValue(duration: null));
 
   int _textureId;
   final String dataSource;
+  final VideoFormat formatHint;
 
   /// Describes the type of data source this [VideoPlayerController]
   /// is constructed with.
@@ -203,7 +210,10 @@
         dataSourceDescription = <String, dynamic>{'uri': dataSource};
         break;
       case DataSourceType.file:
-        dataSourceDescription = <String, dynamic>{'uri': dataSource};
+        dataSourceDescription = <String, dynamic>{
+          'uri': dataSource,
+          'formatHint': _videoFormatStringMap[formatHint]
+        };
     }
     final Map<String, dynamic> response =
         await _channel.invokeMapMethod<String, dynamic>(
@@ -397,6 +407,14 @@
     value = value.copyWith(volume: volume.clamp(0.0, 1.0));
     await _applyVolume();
   }
+
+  static const Map<VideoFormat, String> _videoFormatStringMap =
+      <VideoFormat, String>{
+    VideoFormat.ss: 'ss',
+    VideoFormat.hls: 'hls',
+    VideoFormat.dash: 'dash',
+    VideoFormat.other: 'other',
+  };
 }
 
 class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
diff --git a/video_player/pubspec.yaml b/video_player/pubspec.yaml
index 218c50f..3c18e73 100644
--- a/video_player/pubspec.yaml
+++ b/video_player/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Flutter plugin for displaying inline video with other Flutter
   widgets on Android and iOS.
 author: Flutter Team <flutter-dev@googlegroups.com>
-version: 0.10.1+6
+version: 0.10.2
 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player
 
 flutter:
diff --git a/vm_service/.gitignore b/vm_service/.gitignore
index 113d783..2316a7c 100644
--- a/vm_service/.gitignore
+++ b/vm_service/.gitignore
@@ -2,3 +2,7 @@
 .dart_tool
 .packages
 pubspec.lock
+
+java/build/
+java/dist/
+java/out/
diff --git a/vm_service/BUILD.gn b/vm_service/BUILD.gn
index fa8fd8d..eb1e6f1 100644
--- a/vm_service/BUILD.gn
+++ b/vm_service/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for vm_service-1.1.0
+# This file is generated by importer.py for vm_service-1.1.1
 
 import("//build/dart/dart_library.gni")
 
diff --git a/vm_service/CHANGELOG.md b/vm_service/CHANGELOG.md
index 20c71b0..b4b0668 100644
--- a/vm_service/CHANGELOG.md
+++ b/vm_service/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 1.1.1
+- Fixed issue serializing list arguments for certain VM service methods.
+  - Issue #37872
+
 ## 1.1.0
 - Support service protocol version 3.25:
   - Added `getInboundReferences`, `getRetainingPath` methods
diff --git a/vm_service/example/vm_service_assert.dart b/vm_service/example/vm_service_assert.dart
index 8010e63..df11b53 100644
--- a/vm_service/example/vm_service_assert.dart
+++ b/vm_service/example/vm_service_assert.dart
@@ -28,14 +28,14 @@
   return obj;
 }
 
-List<int> assertInts(List<int> list) {
+List<int> assertListOfInt(List<int> list) {
   for (int elem in list) {
     assertInt(elem);
   }
   return list;
 }
 
-List<String> assertStrings(List<String> list) {
+List<String> assertListOfString(List<String> list) {
   for (String elem in list) {
     assertString(elem);
   }
@@ -236,7 +236,7 @@
 vms.AllocationProfile assertAllocationProfile(vms.AllocationProfile obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertClassHeapStatss(obj.members);
+  assertListOfClassHeapStats(obj.members);
   assertMemoryUsage(obj.memoryUsage);
   return obj;
 }
@@ -273,7 +273,8 @@
   return obj;
 }
 
-List<vms.BoundVariable> assertBoundVariables(List<vms.BoundVariable> list) {
+List<vms.BoundVariable> assertListOfBoundVariable(
+    List<vms.BoundVariable> list) {
   for (vms.BoundVariable elem in list) {
     assertBoundVariable(elem);
   }
@@ -296,7 +297,7 @@
   return obj;
 }
 
-List<vms.Breakpoint> assertBreakpoints(List<vms.Breakpoint> list) {
+List<vms.Breakpoint> assertListOfBreakpoint(List<vms.Breakpoint> list) {
   for (vms.Breakpoint elem in list) {
     assertBreakpoint(elem);
   }
@@ -311,7 +312,7 @@
   return obj;
 }
 
-List<vms.ClassRef> assertClassRefs(List<vms.ClassRef> list) {
+List<vms.ClassRef> assertListOfClassRef(List<vms.ClassRef> list) {
   for (vms.ClassRef elem in list) {
     assertClassRef(elem);
   }
@@ -326,10 +327,10 @@
   assertBool(obj.isAbstract);
   assertBool(obj.isConst);
   assertLibraryRef(obj.library);
-  assertInstanceRefs(obj.interfaces);
-  assertFieldRefs(obj.fields);
-  assertFuncRefs(obj.functions);
-  assertClassRefs(obj.subclasses);
+  assertListOfInstanceRef(obj.interfaces);
+  assertListOfFieldRef(obj.fields);
+  assertListOfFuncRef(obj.functions);
+  assertListOfClassRef(obj.subclasses);
   return obj;
 }
 
@@ -344,7 +345,8 @@
   return obj;
 }
 
-List<vms.ClassHeapStats> assertClassHeapStatss(List<vms.ClassHeapStats> list) {
+List<vms.ClassHeapStats> assertListOfClassHeapStats(
+    List<vms.ClassHeapStats> list) {
   for (vms.ClassHeapStats elem in list) {
     assertClassHeapStats(elem);
   }
@@ -354,7 +356,7 @@
 vms.ClassList assertClassList(vms.ClassList obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertClassRefs(obj.classes);
+  assertListOfClassRef(obj.classes);
   return obj;
 }
 
@@ -367,7 +369,7 @@
   return obj;
 }
 
-List<vms.CodeRef> assertCodeRefs(List<vms.CodeRef> list) {
+List<vms.CodeRef> assertListOfCodeRef(List<vms.CodeRef> list) {
   for (vms.CodeRef elem in list) {
     assertCodeRef(elem);
   }
@@ -391,7 +393,7 @@
   return obj;
 }
 
-List<vms.ContextRef> assertContextRefs(List<vms.ContextRef> list) {
+List<vms.ContextRef> assertListOfContextRef(List<vms.ContextRef> list) {
   for (vms.ContextRef elem in list) {
     assertContextRef(elem);
   }
@@ -403,7 +405,7 @@
   assertString(obj.type);
   assertString(obj.id);
   assertInt(obj.length);
-  assertContextElements(obj.variables);
+  assertListOfContextElement(obj.variables);
   return obj;
 }
 
@@ -419,7 +421,8 @@
   return obj;
 }
 
-List<vms.ContextElement> assertContextElements(List<vms.ContextElement> list) {
+List<vms.ContextElement> assertListOfContextElement(
+    List<vms.ContextElement> list) {
   for (vms.ContextElement elem in list) {
     assertContextElement(elem);
   }
@@ -435,7 +438,7 @@
   return obj;
 }
 
-List<vms.ErrorRef> assertErrorRefs(List<vms.ErrorRef> list) {
+List<vms.ErrorRef> assertListOfErrorRef(List<vms.ErrorRef> list) {
   for (vms.ErrorRef elem in list) {
     assertErrorRef(elem);
   }
@@ -477,7 +480,7 @@
   return obj;
 }
 
-List<vms.FieldRef> assertFieldRefs(List<vms.FieldRef> list) {
+List<vms.FieldRef> assertListOfFieldRef(List<vms.FieldRef> list) {
   for (vms.FieldRef elem in list) {
     assertFieldRef(elem);
   }
@@ -505,7 +508,7 @@
   return obj;
 }
 
-List<vms.Flag> assertFlags(List<vms.Flag> list) {
+List<vms.Flag> assertListOfFlag(List<vms.Flag> list) {
   for (vms.Flag elem in list) {
     assertFlag(elem);
   }
@@ -515,7 +518,7 @@
 vms.FlagList assertFlagList(vms.FlagList obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertFlags(obj.flags);
+  assertListOfFlag(obj.flags);
   return obj;
 }
 
@@ -526,7 +529,7 @@
   return obj;
 }
 
-List<vms.Frame> assertFrames(List<vms.Frame> list) {
+List<vms.Frame> assertListOfFrame(List<vms.Frame> list) {
   for (vms.Frame elem in list) {
     assertFrame(elem);
   }
@@ -552,7 +555,7 @@
   return obj;
 }
 
-List<vms.FuncRef> assertFuncRefs(List<vms.FuncRef> list) {
+List<vms.FuncRef> assertListOfFuncRef(List<vms.FuncRef> list) {
   for (vms.FuncRef elem in list) {
     assertFuncRef(elem);
   }
@@ -585,7 +588,7 @@
   return obj;
 }
 
-List<vms.InstanceRef> assertInstanceRefs(List<vms.InstanceRef> list) {
+List<vms.InstanceRef> assertListOfInstanceRef(List<vms.InstanceRef> list) {
   for (vms.InstanceRef elem in list) {
     assertInstanceRef(elem);
   }
@@ -610,7 +613,7 @@
   return obj;
 }
 
-List<vms.IsolateRef> assertIsolateRefs(List<vms.IsolateRef> list) {
+List<vms.IsolateRef> assertListOfIsolateRef(List<vms.IsolateRef> list) {
   for (vms.IsolateRef elem in list) {
     assertIsolateRef(elem);
   }
@@ -628,8 +631,8 @@
   assertInt(obj.livePorts);
   assertBool(obj.pauseOnExit);
   assertEvent(obj.pauseEvent);
-  assertLibraryRefs(obj.libraries);
-  assertBreakpoints(obj.breakpoints);
+  assertListOfLibraryRef(obj.libraries);
+  assertListOfBreakpoint(obj.breakpoints);
   assertExceptionPauseMode(obj.exceptionPauseMode);
   return obj;
 }
@@ -637,7 +640,7 @@
 vms.InboundReferences assertInboundReferences(vms.InboundReferences obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertInboundReferences(obj.references);
+  assertListOfInboundReference(obj.references);
   return obj;
 }
 
@@ -647,11 +650,19 @@
   return obj;
 }
 
+List<vms.InboundReference> assertListOfInboundReference(
+    List<vms.InboundReference> list) {
+  for (vms.InboundReference elem in list) {
+    assertInboundReference(elem);
+  }
+  return list;
+}
+
 vms.InstanceSet assertInstanceSet(vms.InstanceSet obj) {
   assertNotNull(obj);
   assertString(obj.type);
   assertInt(obj.totalCount);
-  assertObjRefs(obj.instances);
+  assertListOfObjRef(obj.instances);
   return obj;
 }
 
@@ -664,7 +675,7 @@
   return obj;
 }
 
-List<vms.LibraryRef> assertLibraryRefs(List<vms.LibraryRef> list) {
+List<vms.LibraryRef> assertListOfLibraryRef(List<vms.LibraryRef> list) {
   for (vms.LibraryRef elem in list) {
     assertLibraryRef(elem);
   }
@@ -678,11 +689,11 @@
   assertString(obj.name);
   assertString(obj.uri);
   assertBool(obj.debuggable);
-  assertLibraryDependencies(obj.dependencies);
-  assertScriptRefs(obj.scripts);
-  assertFieldRefs(obj.variables);
-  assertFuncRefs(obj.functions);
-  assertClassRefs(obj.classes);
+  assertListOfLibraryDependency(obj.dependencies);
+  assertListOfScriptRef(obj.scripts);
+  assertListOfFieldRef(obj.variables);
+  assertListOfFuncRef(obj.functions);
+  assertListOfClassRef(obj.classes);
   return obj;
 }
 
@@ -695,7 +706,7 @@
   return obj;
 }
 
-List<vms.LibraryDependency> assertLibraryDependencies(
+List<vms.LibraryDependency> assertListOfLibraryDependency(
     List<vms.LibraryDependency> list) {
   for (vms.LibraryDependency elem in list) {
     assertLibraryDependency(elem);
@@ -755,7 +766,7 @@
   return obj;
 }
 
-List<vms.Message> assertMessages(List<vms.Message> list) {
+List<vms.Message> assertListOfMessage(List<vms.Message> list) {
   for (vms.Message elem in list) {
     assertMessage(elem);
   }
@@ -772,7 +783,7 @@
   return obj;
 }
 
-List<vms.NullValRef> assertNullValRefs(List<vms.NullValRef> list) {
+List<vms.NullValRef> assertListOfNullValRef(List<vms.NullValRef> list) {
   for (vms.NullValRef elem in list) {
     assertNullValRef(elem);
   }
@@ -796,7 +807,7 @@
   return obj;
 }
 
-List<vms.ObjRef> assertObjRefs(List<vms.ObjRef> list) {
+List<vms.ObjRef> assertListOfObjRef(List<vms.ObjRef> list) {
   for (vms.ObjRef elem in list) {
     assertObjRef(elem);
   }
@@ -823,12 +834,20 @@
   return obj;
 }
 
+List<vms.RetainingObject> assertListOfRetainingObject(
+    List<vms.RetainingObject> list) {
+  for (vms.RetainingObject elem in list) {
+    assertRetainingObject(elem);
+  }
+  return list;
+}
+
 vms.RetainingPath assertRetainingPath(vms.RetainingPath obj) {
   assertNotNull(obj);
   assertString(obj.type);
   assertInt(obj.length);
   assertString(obj.gcRootType);
-  assertRetainingObjects(obj.elements);
+  assertListOfRetainingObject(obj.elements);
   return obj;
 }
 
@@ -854,7 +873,7 @@
   return obj;
 }
 
-List<vms.ScriptRef> assertScriptRefs(List<vms.ScriptRef> list) {
+List<vms.ScriptRef> assertListOfScriptRef(List<vms.ScriptRef> list) {
   for (vms.ScriptRef elem in list) {
     assertScriptRef(elem);
   }
@@ -873,7 +892,7 @@
 vms.ScriptList assertScriptList(vms.ScriptList obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertScriptRefs(obj.scripts);
+  assertListOfScriptRef(obj.scripts);
   return obj;
 }
 
@@ -888,16 +907,16 @@
 vms.SourceReport assertSourceReport(vms.SourceReport obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertSourceReportRanges(obj.ranges);
-  assertScriptRefs(obj.scripts);
+  assertListOfSourceReportRange(obj.ranges);
+  assertListOfScriptRef(obj.scripts);
   return obj;
 }
 
 vms.SourceReportCoverage assertSourceReportCoverage(
     vms.SourceReportCoverage obj) {
   assertNotNull(obj);
-  assertInts(obj.hits);
-  assertInts(obj.misses);
+  assertListOfInt(obj.hits);
+  assertListOfInt(obj.misses);
   return obj;
 }
 
@@ -910,7 +929,7 @@
   return obj;
 }
 
-List<vms.SourceReportRange> assertSourceReportRanges(
+List<vms.SourceReportRange> assertListOfSourceReportRange(
     List<vms.SourceReportRange> list) {
   for (vms.SourceReportRange elem in list) {
     assertSourceReportRange(elem);
@@ -921,15 +940,15 @@
 vms.Stack assertStack(vms.Stack obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertFrames(obj.frames);
-  assertMessages(obj.messages);
+  assertListOfFrame(obj.frames);
+  assertListOfMessage(obj.messages);
   return obj;
 }
 
 vms.Timeline assertTimeline(vms.Timeline obj) {
   assertNotNull(obj);
   assertString(obj.type);
-  assertTimelineEvents(obj.traceEvents);
+  assertListOfTimelineEvent(obj.traceEvents);
   assertInt(obj.timeOriginMicros);
   assertInt(obj.timeExtentMicros);
   return obj;
@@ -940,7 +959,8 @@
   return obj;
 }
 
-List<vms.TimelineEvent> assertTimelineEvents(List<vms.TimelineEvent> list) {
+List<vms.TimelineEvent> assertListOfTimelineEvent(
+    List<vms.TimelineEvent> list) {
   for (vms.TimelineEvent elem in list) {
     assertTimelineEvent(elem);
   }
@@ -951,8 +971,8 @@
   assertNotNull(obj);
   assertString(obj.type);
   assertString(obj.recorderName);
-  assertStrings(obj.availableStreams);
-  assertStrings(obj.recordedStreams);
+  assertListOfString(obj.availableStreams);
+  assertListOfString(obj.recordedStreams);
   return obj;
 }
 
@@ -971,7 +991,7 @@
   return obj;
 }
 
-List<vms.TypeArgumentsRef> assertTypeArgumentsRefs(
+List<vms.TypeArgumentsRef> assertListOfTypeArgumentsRef(
     List<vms.TypeArgumentsRef> list) {
   for (vms.TypeArgumentsRef elem in list) {
     assertTypeArgumentsRef(elem);
@@ -984,7 +1004,7 @@
   assertString(obj.type);
   assertString(obj.id);
   assertString(obj.name);
-  assertInstanceRefs(obj.types);
+  assertListOfInstanceRef(obj.types);
   return obj;
 }
 
@@ -1010,7 +1030,7 @@
   return obj;
 }
 
-List<vms.VMRef> assertVMRefs(List<vms.VMRef> list) {
+List<vms.VMRef> assertListOfVMRef(List<vms.VMRef> list) {
   for (vms.VMRef elem in list) {
     assertVMRef(elem);
   }
@@ -1028,6 +1048,6 @@
   assertString(obj.version);
   assertInt(obj.pid);
   assertInt(obj.startTime);
-  assertIsolateRefs(obj.isolates);
+  assertListOfIsolateRef(obj.isolates);
   return obj;
 }
diff --git a/vm_service/java/.gitignore b/vm_service/java/.gitignore
index d568567..4fef6d5 100644
--- a/vm_service/java/.gitignore
+++ b/vm_service/java/.gitignore
@@ -1,4 +1,100 @@
-build/
-dist/
-out/
-src/gen/
+# This is a generated file.
+
+src/org/dartlang/vm/service/VmService.java
+src/org/dartlang/vm/service/consumer/AllocationProfileConsumer.java
+src/org/dartlang/vm/service/consumer/BreakpointConsumer.java
+src/org/dartlang/vm/service/consumer/EvaluateConsumer.java
+src/org/dartlang/vm/service/consumer/EvaluateInFrameConsumer.java
+src/org/dartlang/vm/service/consumer/FlagListConsumer.java
+src/org/dartlang/vm/service/consumer/GetInboundReferencesConsumer.java
+src/org/dartlang/vm/service/consumer/GetIsolateConsumer.java
+src/org/dartlang/vm/service/consumer/GetMemoryUsageConsumer.java
+src/org/dartlang/vm/service/consumer/GetObjectConsumer.java
+src/org/dartlang/vm/service/consumer/InstanceSetConsumer.java
+src/org/dartlang/vm/service/consumer/InvokeConsumer.java
+src/org/dartlang/vm/service/consumer/ReloadReportConsumer.java
+src/org/dartlang/vm/service/consumer/RetainingPathConsumer.java
+src/org/dartlang/vm/service/consumer/ScriptListConsumer.java
+src/org/dartlang/vm/service/consumer/SourceReportConsumer.java
+src/org/dartlang/vm/service/consumer/StackConsumer.java
+src/org/dartlang/vm/service/consumer/SuccessConsumer.java
+src/org/dartlang/vm/service/consumer/TimelineConsumer.java
+src/org/dartlang/vm/service/consumer/TimelineFlagsConsumer.java
+src/org/dartlang/vm/service/consumer/TimestampConsumer.java
+src/org/dartlang/vm/service/consumer/VMConsumer.java
+src/org/dartlang/vm/service/consumer/VersionConsumer.java
+src/org/dartlang/vm/service/element/AllocationProfile.java
+src/org/dartlang/vm/service/element/BoundField.java
+src/org/dartlang/vm/service/element/BoundVariable.java
+src/org/dartlang/vm/service/element/Breakpoint.java
+src/org/dartlang/vm/service/element/ClassHeapStats.java
+src/org/dartlang/vm/service/element/ClassList.java
+src/org/dartlang/vm/service/element/ClassObj.java
+src/org/dartlang/vm/service/element/ClassRef.java
+src/org/dartlang/vm/service/element/Code.java
+src/org/dartlang/vm/service/element/CodeKind.java
+src/org/dartlang/vm/service/element/CodeRef.java
+src/org/dartlang/vm/service/element/Context.java
+src/org/dartlang/vm/service/element/ContextElement.java
+src/org/dartlang/vm/service/element/ContextRef.java
+src/org/dartlang/vm/service/element/ErrorKind.java
+src/org/dartlang/vm/service/element/ErrorObj.java
+src/org/dartlang/vm/service/element/ErrorRef.java
+src/org/dartlang/vm/service/element/Event.java
+src/org/dartlang/vm/service/element/EventKind.java
+src/org/dartlang/vm/service/element/ExceptionPauseMode.java
+src/org/dartlang/vm/service/element/ExtensionData.java
+src/org/dartlang/vm/service/element/Field.java
+src/org/dartlang/vm/service/element/FieldRef.java
+src/org/dartlang/vm/service/element/Flag.java
+src/org/dartlang/vm/service/element/FlagList.java
+src/org/dartlang/vm/service/element/Frame.java
+src/org/dartlang/vm/service/element/FrameKind.java
+src/org/dartlang/vm/service/element/Func.java
+src/org/dartlang/vm/service/element/FuncRef.java
+src/org/dartlang/vm/service/element/InboundReference.java
+src/org/dartlang/vm/service/element/InboundReferences.java
+src/org/dartlang/vm/service/element/Instance.java
+src/org/dartlang/vm/service/element/InstanceKind.java
+src/org/dartlang/vm/service/element/InstanceRef.java
+src/org/dartlang/vm/service/element/InstanceSet.java
+src/org/dartlang/vm/service/element/Isolate.java
+src/org/dartlang/vm/service/element/IsolateRef.java
+src/org/dartlang/vm/service/element/Library.java
+src/org/dartlang/vm/service/element/LibraryDependency.java
+src/org/dartlang/vm/service/element/LibraryRef.java
+src/org/dartlang/vm/service/element/LogRecord.java
+src/org/dartlang/vm/service/element/MapAssociation.java
+src/org/dartlang/vm/service/element/MemoryUsage.java
+src/org/dartlang/vm/service/element/Message.java
+src/org/dartlang/vm/service/element/Null.java
+src/org/dartlang/vm/service/element/NullRef.java
+src/org/dartlang/vm/service/element/Obj.java
+src/org/dartlang/vm/service/element/ObjRef.java
+src/org/dartlang/vm/service/element/ReloadReport.java
+src/org/dartlang/vm/service/element/Response.java
+src/org/dartlang/vm/service/element/RetainingObject.java
+src/org/dartlang/vm/service/element/RetainingPath.java
+src/org/dartlang/vm/service/element/Script.java
+src/org/dartlang/vm/service/element/ScriptList.java
+src/org/dartlang/vm/service/element/ScriptRef.java
+src/org/dartlang/vm/service/element/Sentinel.java
+src/org/dartlang/vm/service/element/SentinelKind.java
+src/org/dartlang/vm/service/element/SourceLocation.java
+src/org/dartlang/vm/service/element/SourceReport.java
+src/org/dartlang/vm/service/element/SourceReportCoverage.java
+src/org/dartlang/vm/service/element/SourceReportKind.java
+src/org/dartlang/vm/service/element/SourceReportRange.java
+src/org/dartlang/vm/service/element/Stack.java
+src/org/dartlang/vm/service/element/StepOption.java
+src/org/dartlang/vm/service/element/Success.java
+src/org/dartlang/vm/service/element/Timeline.java
+src/org/dartlang/vm/service/element/TimelineEvent.java
+src/org/dartlang/vm/service/element/TimelineFlags.java
+src/org/dartlang/vm/service/element/Timestamp.java
+src/org/dartlang/vm/service/element/TypeArguments.java
+src/org/dartlang/vm/service/element/TypeArgumentsRef.java
+src/org/dartlang/vm/service/element/UnresolvedSourceLocation.java
+src/org/dartlang/vm/service/element/VM.java
+src/org/dartlang/vm/service/element/VMRef.java
+src/org/dartlang/vm/service/element/Version.java
diff --git a/vm_service/java/src/org/dartlang/vm/service/element/Element.java b/vm_service/java/src/org/dartlang/vm/service/element/Element.java
index 2319998..cdee4bd 100644
--- a/vm_service/java/src/org/dartlang/vm/service/element/Element.java
+++ b/vm_service/java/src/org/dartlang/vm/service/element/Element.java
@@ -1,6 +1,8 @@
 package org.dartlang.vm.service.element;
 
 import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
 import com.google.gson.JsonObject;
 
 import java.util.ArrayList;
@@ -17,6 +19,30 @@
   }
 
   /**
+   * A utility method to handle null values and JsonNull values.
+   */
+  String getAsString(String name) {
+    final JsonElement element = json.get(name);
+    return (element == null || element == JsonNull.INSTANCE) ? null : element.getAsString();
+  }
+
+  /**
+   * A utility method to handle null values and JsonNull values.
+   */
+  int getAsInt(String name) {
+    final JsonElement element = json.get(name);
+    return (element == null || element == JsonNull.INSTANCE) ? -1 : element.getAsInt();
+  }
+
+  /**
+   * A utility method to handle null values and JsonNull values.
+   */
+  boolean getAsBoolean(String name) {
+    final JsonElement element = json.get(name);
+    return (element == null || element == JsonNull.INSTANCE) ? false : element.getAsBoolean();
+  }
+
+  /**
    * Return the underlying JSON backing this element.
    */
   public JsonObject getJson() {
diff --git a/vm_service/java/version.properties b/vm_service/java/version.properties
index 68164e7..55a8c99 100644
--- a/vm_service/java/version.properties
+++ b/vm_service/java/version.properties
@@ -1 +1 @@
-version=3.23
+version=3.26
diff --git a/vm_service/lib/vm_service.dart b/vm_service/lib/vm_service.dart
index 64dcf89..0030b6b 100644
--- a/vm_service/lib/vm_service.dart
+++ b/vm_service/lib/vm_service.dart
@@ -17,7 +17,7 @@
 
 export 'src/service_extension_registry.dart' show ServiceExtensionRegistry;
 
-const String vmServiceVersion = '3.25.0';
+const String vmServiceVersion = '3.26.0';
 
 /// @optional
 const String optional = 'optional';
@@ -186,6 +186,7 @@
   'registerService': const ['Success'],
   'reloadSources': const ['ReloadReport'],
   'removeBreakpoint': const ['Success'],
+  'requestHeapSnapshot': const ['Success'],
   'resume': const ['Success'],
   'setExceptionPauseMode': const ['Success'],
   'setFlag': const ['Success'],
@@ -692,6 +693,15 @@
   /// See [Success].
   Future<Success> removeBreakpoint(String isolateId, String breakpointId);
 
+  /// Requests a dump of the Dart heap of the given isolate.
+  ///
+  /// This method immediately returns success. The VM will then begin delivering
+  /// binary events on the `HeapSnapshot` event stream. The binary data in these
+  /// events, when concatenated together, conforms to the SnapshotGraph type.
+  /// The splitting of the SnapshotGraph into events can happen at any byte
+  /// offset, including the middle of scalar fields.
+  Future<Success> requestHeapSnapshot(String isolateId);
+
   /// The `resume` RPC is used to resume execution of a paused isolate.
   ///
   /// If the `step` parameter is not provided, the program will resume regular
@@ -795,6 +805,7 @@
   /// Timeline | TimelineEvents
   /// Logging | Logging
   /// Service | ServiceRegistered, ServiceUnregistered
+  /// HeapSnapshot | HeapSnapshot
   ///
   /// Additionally, some embedders provide the `Stdout` and `Stderr` streams.
   /// These streams allow the client to subscribe to writes to stdout and
@@ -918,7 +929,7 @@
             params['isolateId'],
             params['targetId'],
             params['selector'],
-            params['argumentIds'],
+            List<String>.from(params['argumentIds'] ?? []),
             disableBreakpoints: params['disableBreakpoints'],
           );
           break;
@@ -1002,7 +1013,7 @@
         case 'getSourceReport':
           response = await _serviceImplementation.getSourceReport(
             params['isolateId'],
-            params['reports'],
+            List<String>.from(params['reports'] ?? []),
             scriptId: params['scriptId'],
             tokenPos: params['tokenPos'],
             endTokenPos: params['endTokenPos'],
@@ -1052,6 +1063,11 @@
             params['breakpointId'],
           );
           break;
+        case 'requestHeapSnapshot':
+          response = await _serviceImplementation.requestHeapSnapshot(
+            params['isolateId'],
+          );
+          break;
         case 'resume':
           response = await _serviceImplementation.resume(
             params['isolateId'],
@@ -1091,7 +1107,7 @@
           break;
         case 'setVMTimelineFlags':
           response = await _serviceImplementation.setVMTimelineFlags(
-            params['recordedStreams'],
+            List<String>.from(params['recordedStreams'] ?? []),
           );
           break;
         case 'streamCancel':
@@ -1233,6 +1249,10 @@
   // ServiceRegistered, ServiceUnregistered
   Stream<Event> get onServiceEvent => _getEventController('Service').stream;
 
+  // HeapSnapshot
+  Stream<Event> get onHeapSnapshotEvent =>
+      _getEventController('HeapSnapshot').stream;
+
   // WriteEvent
   Stream<Event> get onStdoutEvent => _getEventController('Stdout').stream;
 
@@ -1510,6 +1530,11 @@
   }
 
   @override
+  Future<Success> requestHeapSnapshot(String isolateId) {
+    return _call('requestHeapSnapshot', {'isolateId': isolateId});
+  }
+
+  @override
   Future<Success> resume(String isolateId,
       {/*StepOption*/ String step, int frameIndex}) {
     Map m = {'isolateId': isolateId};
@@ -1841,6 +1866,7 @@
   static const String kTimeline = 'Timeline';
   static const String kLogging = 'Logging';
   static const String kService = 'Service';
+  static const String kHeapSnapshot = 'HeapSnapshot';
   static const String kStdout = 'Stdout';
   static const String kStderr = 'Stderr';
 }
diff --git a/vm_service/pubspec.yaml b/vm_service/pubspec.yaml
index b2e57b3..244e9f6 100644
--- a/vm_service/pubspec.yaml
+++ b/vm_service/pubspec.yaml
@@ -2,7 +2,7 @@
 description: >-
   A library to communicate with a service implementing the Dart VM
   service protocol.
-version: 1.1.0
+version: 1.1.1
 
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service
diff --git a/vm_service/tool/dart/generate_dart.dart b/vm_service/tool/dart/generate_dart.dart
index 04e8b59..be4a182 100644
--- a/vm_service/tool/dart/generate_dart.dart
+++ b/vm_service/tool/dart/generate_dart.dart
@@ -635,8 +635,13 @@
         } else {
           gen.write("response = await _serviceImplementation.${m.name}(");
           // Positional args
-          m.args.where((arg) => !arg.optional).forEach((arg) {
-            gen.write("params['${arg.name}'], ");
+          m.args.where((arg) => !arg.optional).forEach((MethodArg arg) {
+            if (arg.type.isArray) {
+              gen.write(
+                  "${arg.type.listCreationRef}.from(params['${arg.name}'] ?? []), ");
+            } else {
+              gen.write("params['${arg.name}'], ");
+            }
           });
           // Optional named args
           var namedArgs = m.args.where((arg) => arg.optional);
@@ -811,14 +816,14 @@
   return obj;
 }
 
-List<int> assertInts(List<int> list) {
+List<int> assertListOfInt(List<int> list) {
   for (int elem in list) {
     assertInt(elem);
   }
   return list;
 }
 
-List<String> assertStrings(List<String> list) {
+List<String> assertListOfString(List<String> list) {
   for (String elem in list) {
     assertString(elem);
   }
@@ -902,15 +907,17 @@
           [
             'BoundVariable',
             'Breakpoint',
+            'ClassHeapStats',
+            'CodeRegion',
             'ContextElement',
             'Flag',
             'Frame',
+            'InboundReference',
             'LibraryDependency',
             'Message',
-            'SourceReportRange',
-            'ClassHeapStats',
-            'CodeRegion',
             'ProfileFunction',
+            'RetainingObject',
+            'SourceReportRange',
             'TimelineEvent',
           ].contains(type.name)) {
         type.generateListAssert(gen);
@@ -1167,6 +1174,16 @@
     }
   }
 
+  String get listCreationRef {
+    assert(arrayDepth == 1);
+
+    if (isListTypeSimple) {
+      return 'List<$name>';
+    } else {
+      return 'List<String>';
+    }
+  }
+
   String get listTypeArg => arrayDepth == 2 ? 'List<$name>' : name;
 
   bool get isArray => arrayDepth > 0;
@@ -1187,10 +1204,6 @@
           name == 'bool' ||
           name == 'double');
 
-  String get namePlural => name.endsWith('y')
-      ? name.substring(0, name.length - 1) + 'ies'
-      : name + 's';
-
   String toString() => ref;
 }
 
@@ -1207,6 +1220,8 @@
   void generate(DartGenerator gen) {
     gen.write('${type.ref} ${name}');
   }
+
+  String toString() => '$type $name';
 }
 
 class Type extends Member {
@@ -1251,10 +1266,6 @@
 
   bool get isRef => name.endsWith('Ref');
 
-  String get namePlural => name.endsWith('y')
-      ? name.substring(0, name.length - 1) + 'ies'
-      : name + 's';
-
   bool get supportsIdentity {
     if (fields.any((f) => f.name == 'id')) return true;
     return superName == null ? false : getSuper().supportsIdentity;
@@ -1535,9 +1546,9 @@
         if (type.isArray) {
           TypeRef arrayType = type.types.first;
           if (arrayType.arrayDepth == 1) {
-            String assertMethodName = 'assert' +
+            String assertMethodName = 'assertListOf' +
                 arrayType.name.substring(0, 1).toUpperCase() +
-                arrayType.namePlural.substring(1);
+                arrayType.name.substring(1);
             gen.writeln('$assertMethodName(obj.${field.generatableName});');
           } else {
             gen.writeln(
@@ -1574,7 +1585,7 @@
 
   void generateListAssert(DartGenerator gen) {
     gen.writeln('List<vms.${name}> '
-        'assert${namePlural}(List<vms.${name}> list) {');
+        'assertListOf${name}(List<vms.${name}> list) {');
     gen.writeln('for (vms.${name} elem in list) {');
     gen.writeln('assert${name}(elem);');
     gen.writeln('}');
diff --git a/vm_service/tool/generate.dart b/vm_service/tool/generate.dart
index 5ff5c7d..73e8d2e 100644
--- a/vm_service/tool/generate.dart
+++ b/vm_service/tool/generate.dart
@@ -17,20 +17,11 @@
 /// Parse the 'service.md' into a model and generate both Dart and Java
 /// libraries.
 main(List<String> args) async {
-  bool generateJava = false;
-  if (args.length != 0) {
-    if ((args.length == 1) && (args.first == '--generate-java')) {
-      generateJava = true;
-    } else {
-      print('Invalid options: $args. Usage: dart generate [--generate-java].');
-      return;
-    }
-  }
   String appDirPath = dirname(Platform.script.toFilePath());
 
   // Parse service.md into a model.
-  var file =
-      new File(join(appDirPath, '../../../runtime/vm/service/service.md'));
+  var file = new File(
+      normalize(join(appDirPath, '../../../runtime/vm/service/service.md')));
   var document = new Document();
   StringBuffer buf = new StringBuffer(file.readAsStringSync());
   var nodes = document.parseLines(buf.toString().split('\n'));
@@ -38,15 +29,14 @@
   print('Service protocol version ${ApiParseUtil.parseVersionString(nodes)}.');
 
   // Generate code from the model.
+  print('');
+
   await _generateDart(appDirPath, nodes);
-  if (generateJava) {
-    await _generateJava(appDirPath, nodes);
-  }
+  await _generateJava(appDirPath, nodes);
   await _generateAsserts(appDirPath, nodes);
 }
 
 _generateDart(String appDirPath, List<Node> nodes) async {
-  print('');
   var outDirPath = normalize(join(appDirPath, '..', 'lib'));
   var outDir = new Directory(outDirPath);
   if (!outDir.existsSync()) outDir.createSync(recursive: true);
@@ -56,7 +46,11 @@
   dart.api.parse(nodes);
   dart.api.generate(generator);
   outputFile.writeAsStringSync(generator.toString());
-  Process.runSync('dartfmt', ['-w', outDirPath]);
+  ProcessResult result = Process.runSync('dartfmt', ['-w', outDirPath]);
+  if (result.exitCode != 0) {
+    print('dartfmt: ${result.stdout}\n${result.stderr}');
+    throw result.exitCode;
+  }
 
   if (_stampPubspecVersion) {
     // Update the pubspec file.
@@ -71,14 +65,26 @@
 }
 
 _generateJava(String appDirPath, List<Node> nodes) async {
-  print('');
-  var srcDirPath = normalize(join(appDirPath, '..', 'java', 'src', 'gen'));
-  assert(new Directory(srcDirPath).existsSync());
+  var srcDirPath = normalize(join(appDirPath, '..', 'java', 'src'));
   var generator = new java.JavaGenerator(srcDirPath);
   java.api = new java.Api();
   java.api.parse(nodes);
   java.api.generate(generator);
 
+  // We generate files into the java/src/ folder; ensure the generated files
+  // aren't committed to git (but manually maintained files in the same
+  // directory are).
+  List<String> generatedPaths = generator.allWrittenFiles
+      .map((path) => relative(path, from: 'java'))
+      .toList();
+  generatedPaths.sort();
+  File gitignoreFile = new File(join(appDirPath, '..', 'java', '.gitignore'));
+  gitignoreFile.writeAsStringSync('''
+# This is a generated file.
+
+${generatedPaths.join('\n')}
+''');
+
   // Generate a version file.
   Version version = ApiParseUtil.parseVersionSemVer(nodes);
   File file = new File(join('java', 'version.properties'));
@@ -88,7 +94,6 @@
 }
 
 _generateAsserts(String appDirPath, List<Node> nodes) async {
-  print('');
   var outDirPath = normalize(join(appDirPath, '..', 'example'));
   var outDir = new Directory(outDirPath);
   if (!outDir.existsSync()) outDir.createSync(recursive: true);
@@ -98,7 +103,11 @@
   dart.api.parse(nodes);
   dart.api.generateAsserts(generator);
   outputFile.writeAsStringSync(generator.toString());
-  Process.runSync('dartfmt', ['-w', outDirPath]);
+  ProcessResult result = Process.runSync('dartfmt', ['-w', outDirPath]);
+  if (result.exitCode != 0) {
+    print('dartfmt: ${result.stdout}\n${result.stderr}');
+    throw result.exitCode;
+  }
 
   if (_stampPubspecVersion) {
     // Update the pubspec file.
diff --git a/vm_service/tool/java/generate_java.dart b/vm_service/tool/java/generate_java.dart
index 1039f87..099e0e0 100644
--- a/vm_service/tool/java/generate_java.dart
+++ b/vm_service/tool/java/generate_java.dart
@@ -1038,11 +1038,8 @@
           writer.addLine('final JsonElement elem = json.get("$propertyName");');
           writer.addLine(
               'return elem != null ? elem.getAsBoolean() : $defaultValue;');
-        } else if (optional) {
-          writer.addLine('return json.get("$propertyName") == null ? '
-              'false : json.get("$propertyName").getAsBoolean();');
         } else {
-          writer.addLine('return json.get("$propertyName").getAsBoolean();');
+          writer.addLine('return getAsBoolean("$propertyName");');
         }
       }
     } else if (name == 'int') {
@@ -1057,8 +1054,7 @@
           writer.addLine('return json.get("$propertyName") == null ? '
               '-1 : json.get("$propertyName").getAsLong();');
         } else {
-          writer.addLine('return json.get("$propertyName") == null ? '
-              '-1 : json.get("$propertyName").getAsInt();');
+          writer.addLine('return getAsInt("$propertyName");');
         }
       }
     } else if (name == 'double') {
@@ -1080,11 +1076,8 @@
         } else {
           writer.addLine('return getListString("$propertyName");');
         }
-      } else if (optional) {
-        writer.addLine('return json.get("$propertyName") == null ? '
-            'null : json.get("$propertyName").getAsString();');
       } else {
-        writer.addLine('return json.get("$propertyName").getAsString();');
+        writer.addLine('return getAsString("$propertyName");');
       }
     } else if (isEnum) {
       if (isArray) {
diff --git a/vm_service/tool/java/src_gen_java.dart b/vm_service/tool/java/src_gen_java.dart
index 8c8e5d1..2e298a6 100644
--- a/vm_service/tool/java/src_gen_java.dart
+++ b/vm_service/tool/java/src_gen_java.dart
@@ -50,8 +50,12 @@
   /// The java source directory into which files are generated.
   final String srcDirPath;
 
+  Set<String> _generatedPaths = new Set();
+
   JavaGenerator(this.srcDirPath);
 
+  Iterable<String> get allWrittenFiles => _generatedPaths;
+
   /// Generate a Java class/interface in the given package
   void writeType(String typeName, WriteType write) {
     var classWriter = new TypeWriter(typeName);
@@ -61,6 +65,7 @@
     if (!pkgDir.existsSync()) pkgDir.createSync(recursive: true);
     var classFilePath = join(pkgDirPath, '${classNameFor(typeName)}.java');
     var classFile = new File(classFilePath);
+    _generatedPaths.add(classFilePath);
     classFile.writeAsStringSync(classWriter.toSource());
   }
 }
diff --git a/webkit_inspection_protocol/BUILD.gn b/webkit_inspection_protocol/BUILD.gn
index e0ced3f..12419e9 100644
--- a/webkit_inspection_protocol/BUILD.gn
+++ b/webkit_inspection_protocol/BUILD.gn
@@ -1,4 +1,4 @@
-# This file is generated by importer.py for webkit_inspection_protocol-0.4.2
+# This file is generated by importer.py for webkit_inspection_protocol-0.5.0
 
 import("//build/dart/dart_library.gni")
 
@@ -12,9 +12,6 @@
   disable_analysis = true
 
   deps = [
-    "//third_party/dart-pkg/pub/shelf",
-    "//third_party/dart-pkg/pub/args",
     "//third_party/dart-pkg/pub/logging",
-    "//third_party/dart-pkg/pub/shelf_web_socket",
   ]
 }
diff --git a/webkit_inspection_protocol/analysis_options.yaml b/webkit_inspection_protocol/analysis_options.yaml
index 715751a..e6d99f9 100644
--- a/webkit_inspection_protocol/analysis_options.yaml
+++ b/webkit_inspection_protocol/analysis_options.yaml
@@ -1,5 +1,11 @@
 analyzer:
   errors:
     deprecated_member_use_from_same_package: ignore
-  strong-mode:
-    implicit-casts: false
\ No newline at end of file
+
+linter:
+  rules:
+    - always_declare_return_types
+    - avoid_init_to_null
+    - directives_ordering
+    - slash_for_doc_comments
+    - prefer_final_fields
diff --git a/webkit_inspection_protocol/changelog.md b/webkit_inspection_protocol/changelog.md
index 950a3fa..1f1847f 100644
--- a/webkit_inspection_protocol/changelog.md
+++ b/webkit_inspection_protocol/changelog.md
@@ -1,5 +1,9 @@
 # webkit_inspection_protocol.dart
 
+## 0.5.0
+- removed the bin/multiplex.dart binary to the example/ directory
+- remove dependencies on `package:args`, package:shelf`, and `package:shelf_web_socket`
+
 ## 0.4.2
 - Cast `HttpClientResponse` to `Stream<List<int>>` in response to
   SDK breaking change.
diff --git a/webkit_inspection_protocol/bin/multiplex.dart b/webkit_inspection_protocol/example/multiplex.dart
similarity index 92%
rename from webkit_inspection_protocol/bin/multiplex.dart
rename to webkit_inspection_protocol/example/multiplex.dart
index 2b512eb..6ed123d 100644
--- a/webkit_inspection_protocol/bin/multiplex.dart
+++ b/webkit_inspection_protocol/example/multiplex.dart
@@ -8,11 +8,12 @@
 import 'package:args/args.dart' show ArgParser;
 import 'package:logging/logging.dart'
     show hierarchicalLoggingEnabled, Level, Logger, LogRecord;
-import 'package:webkit_inspection_protocol/multiplex.dart' show Server;
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
     show ChromeConnection;
 
-main(List<String> argv) async {
+import 'multiplex_impl.dart' show Server;
+
+void main(List<String> argv) async {
   var args = (new ArgParser()
         ..addFlag('verbose', abbr: 'v', defaultsTo: false, negatable: false)
         ..addFlag('model_dom', defaultsTo: false, negatable: true)
diff --git a/webkit_inspection_protocol/lib/multiplex.dart b/webkit_inspection_protocol/example/multiplex_impl.dart
similarity index 98%
rename from webkit_inspection_protocol/lib/multiplex.dart
rename to webkit_inspection_protocol/example/multiplex_impl.dart
index d877ded..c2e5d5a 100644
--- a/webkit_inspection_protocol/lib/multiplex.dart
+++ b/webkit_inspection_protocol/example/multiplex_impl.dart
@@ -1,7 +1,7 @@
 // Copyright 2015 Google. All rights reserved. Use of this source code is
 // governed by a BSD-style license that can be found in the LICENSE file.
 
-library wip.multiplex;
+library wip.multiplex_impl;
 
 import 'dart:async' show Future;
 import 'dart:convert' show jsonEncode;
@@ -154,7 +154,7 @@
     }
   }
 
-  _jsonEncode(obj) {
+  Object _jsonEncode(Object obj) {
     if (obj is ChromeTab) {
       var json = <String, dynamic>{
         'description': obj.description,
diff --git a/webkit_inspection_protocol/lib/dom_model.dart b/webkit_inspection_protocol/lib/dom_model.dart
index 215b9b5..72e70da 100644
--- a/webkit_inspection_protocol/lib/dom_model.dart
+++ b/webkit_inspection_protocol/lib/dom_model.dart
@@ -30,7 +30,7 @@
 
   final WipDom _dom;
 
-  Map<int, _Node> _nodeCache = {};
+  final Map<int, _Node> _nodeCache = {};
   Future<_Node> _root;
 
   Stream<AttributeModifiedEvent> onAttributeModified;
@@ -77,39 +77,39 @@
               ..listen(_logEvent);
   }
 
-  _logEvent(WipEvent event) {
+  void _logEvent(WipEvent event) {
     _log.finest('Event $event');
   }
 
-  _onAttributeModified(
+  void _onAttributeModified(
       AttributeModifiedEvent event, EventSink<AttributeModifiedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
     node._attributes[event.name] = event.value;
     sink.add(event);
   }
 
-  _onAttributeRemoved(
+  void _onAttributeRemoved(
       AttributeRemovedEvent event, EventSink<AttributeRemovedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
     node._attributes.remove(event.name);
     sink.add(event);
   }
 
-  _onCharacterDataModified(CharacterDataModifiedEvent event,
+  void _onCharacterDataModified(CharacterDataModifiedEvent event,
       EventSink<CharacterDataModifiedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
     node._nodeValue = event.characterData;
     sink.add(event);
   }
 
-  _onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event,
+  void _onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event,
       EventSink<ChildNodeCountUpdatedEvent> sink) {
     var node = _getOrCreateNode(event.nodeId);
     node._childNodeCount = event.childNodeCount;
     sink.add(event);
   }
 
-  _onChildNodeInserted(
+  void _onChildNodeInserted(
       ChildNodeInsertedEvent event, EventSink<ChildNodeInsertedEvent> sink) {
     var parent = _getOrCreateNode(event.parentNodeId);
     int index = 0;
@@ -123,7 +123,7 @@
     sink.add(event);
   }
 
-  _onChildNodeRemoved(
+  void _onChildNodeRemoved(
       ChildNodeRemovedEvent event, EventSink<ChildNodeRemovedEvent> sink) {
     var parent = _getOrCreateNode(event.parentNodeId);
     var node = _nodeCache.remove(event.nodeId);
@@ -132,14 +132,14 @@
     sink.add(event);
   }
 
-  _onDocumentUpdated(
+  void _onDocumentUpdated(
       DocumentUpdatedEvent event, EventSink<DocumentUpdatedEvent> sink) {
     _nodeCache.clear();
     _root = null;
     sink.add(event);
   }
 
-  _onSetChildNodes(
+  void _onSetChildNodes(
       SetChildNodesEvent event, EventSink<SetChildNodesEvent> sink) {
     var parent = _getOrCreateNode(event.nodeId);
     parent._children =
@@ -202,41 +202,50 @@
     }
   }
 
-  noSuchMethod(Invocation invocation) => reflect(_dom).delegate(invocation);
+  dynamic noSuchMethod(Invocation invocation) =>
+      reflect(_dom).delegate(invocation);
 }
 
 class _Node implements Node {
   Map<String, String> _attributes;
+
   @override
   Map<String, String> get attributes =>
       _attributes != null ? new UnmodifiableMapView(_attributes) : null;
 
   int _childNodeCount;
+
   @override
   int get childNodeCount => _childNodeCount;
 
   List<_Node> _children;
+
   @override
   List<Node> get children =>
       _children != null ? new UnmodifiableListView(_children) : null;
 
   _Node _contentDocument;
+
   @override
   Node get contentDocument => _contentDocument;
 
   String _documentUrl;
+
   @override
   String get documentUrl => _documentUrl;
 
   String _internalSubset;
+
   @override
   String get internalSubset => _internalSubset;
 
   String _localName;
+
   @override
   String get localName => _localName;
 
   String _name;
+
   @override
   String get name => _name;
 
@@ -244,30 +253,37 @@
   final int nodeId;
 
   String _nodeName;
+
   @override
   String get nodeName => _nodeName;
 
   int _nodeType;
+
   @override
   int get nodeType => _nodeType;
 
   String _nodeValue;
+
   @override
   String get nodeValue => _nodeValue;
 
   String _publicId;
+
   @override
   String get publicId => _publicId;
 
   String _systemId;
+
   @override
   String get systemId => _systemId;
 
   String _value;
+
   @override
   String get value => _value;
 
   String _xmlVersion;
+
   @override
   String get xmlVersion => _xmlVersion;
 
diff --git a/webkit_inspection_protocol/lib/src/debugger.dart b/webkit_inspection_protocol/lib/src/debugger.dart
index 5e48c6f..5c3808b 100644
--- a/webkit_inspection_protocol/lib/src/debugger.dart
+++ b/webkit_inspection_protocol/lib/src/debugger.dart
@@ -162,11 +162,9 @@
   // "catch", "closure", "global", "local", "with"
   String get scope => _map['scope'] as String;
 
-  /**
-   * Object representing the scope. For global and with scopes it represents the
-   * actual object; for the rest of the scopes, it is artificial transient
-   * object enumerating scope variables as its properties.
-   */
+  /// Object representing the scope. For global and with scopes it represents
+  /// the actual object; for the rest of the scopes, it is artificial transient
+  /// object enumerating scope variables as its properties.
   WipRemoteObject get object =>
       new WipRemoteObject(_map['object'] as Map<String, dynamic>);
 }
diff --git a/webkit_inspection_protocol/lib/webkit_inspection_protocol.dart b/webkit_inspection_protocol/lib/webkit_inspection_protocol.dart
index bf0fe31..358c951 100644
--- a/webkit_inspection_protocol/lib/webkit_inspection_protocol.dart
+++ b/webkit_inspection_protocol/lib/webkit_inspection_protocol.dart
@@ -26,12 +26,10 @@
 export 'src/runtime.dart';
 export 'src/target.dart';
 
-/**
- * A class to connect to a Chrome instance and reflect on its available tabs.
- *
- * This assumes the browser has been started with the `--remote-debugging-port`
- * flag. The data is read from the `http://{host}:{port}/json` url.
- */
+/// A class to connect to a Chrome instance and reflect on its available tabs.
+///
+/// This assumes the browser has been started with the `--remote-debugging-port`
+/// flag. The data is read from the `http://{host}:{port}/json` url.
 class ChromeConnection {
   final HttpClient _client = new HttpClient();
 
@@ -119,15 +117,11 @@
   String toString() => url;
 }
 
-/**
- * A Webkit Inspection Protocol (WIP) connection.
- */
+/// A Webkit Inspection Protocol (WIP) connection.
 class WipConnection {
   static final _logger = new Logger('WipConnection');
 
-  /**
-   * The WebSocket URL.
-   */
+  /// The WebSocket URL.
   final String url;
 
   final WebSocket _ws;
@@ -277,7 +271,7 @@
 const String optional = 'optional';
 
 abstract class WipDomain {
-  Map<String, Stream> _eventStreams = {};
+  final Map<String, Stream> _eventStreams = {};
 
   final WipConnection connection;
   Stream<WipDomain> _onClosed;
diff --git a/webkit_inspection_protocol/pubspec.yaml b/webkit_inspection_protocol/pubspec.yaml
index 95705c2..8df358b 100644
--- a/webkit_inspection_protocol/pubspec.yaml
+++ b/webkit_inspection_protocol/pubspec.yaml
@@ -1,6 +1,6 @@
 name: webkit_inspection_protocol
-version: 0.4.2
-description: A client for the Webkit Inspection Protocol (WIP).
+version: 0.5.0
+description: A client for the Chrome DevTools Protocol (previously called the Webkit Inspection Protocol).
 
 homepage: https://github.com/google/webkit_inspection_protocol.dart
 authors:
@@ -8,13 +8,14 @@
 - Marc Fisher <fisherii@google.com>
 
 environment:
-  sdk: '>=2.0.0-dev.54.0 <3.0.0'
+  sdk: '>=2.0.0 <3.0.0'
 
 dependencies:
-  args: '>=0.13.0 <2.0.0'
   logging: ^0.11.0
-  shelf: ^0.7.0
-  shelf_web_socket: ^0.2.0
+
 dev_dependencies:
+  args: '>=0.13.0 <2.0.0'
+  shelf: ^0.7.0
   shelf_static: ^0.2.0
+  shelf_web_socket: ^0.2.0
   test: ^1.0.0
diff --git a/webkit_inspection_protocol/readme.md b/webkit_inspection_protocol/readme.md
index 1f7b23d..ff209a4 100644
--- a/webkit_inspection_protocol/readme.md
+++ b/webkit_inspection_protocol/readme.md
@@ -5,8 +5,9 @@
 
 ## What is it?
 
-The `webkit_inspection_protocol` package is a client of the Webkit Inspection
-Protocol (WIP). It's used to talk to Chrome DevTools based debuggers.
+The `webkit_inspection_protocol` package is a client for the Chrome DevTools Protocol
+(previously called the Webkit Inspection Protocol). It's used to talk to Chrome DevTools
+based debuggers.
 
 ## The protocol