[css_colors] Import from flutter/css_colors
diff --git a/.ci/Dockerfile b/.ci/Dockerfile
new file mode 100644
index 0000000..a7b75d6
--- /dev/null
+++ b/.ci/Dockerfile
@@ -0,0 +1,20 @@
+# Last updated 10/22/2020 (to rebuild the docker image, update this timestamp)
+FROM cirrusci/flutter:stable-web
+
+RUN sudo apt-get update && \
+    sudo apt-get upgrade --yes && \
+    sudo apt-get install --yes gpg-agent && \
+    sudo apt-get clean --yes
+
+# This must occur after the install of gpg-agent
+RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - && \
+    sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main" && \
+    sudo apt-get update && \
+    sudo apt-get install --yes --allow-unauthenticated clang-format-5.0 && \
+    sudo apt-get clean --yes
+
+RUN yes | sdkmanager \
+    "platforms;android-27" \
+    "build-tools;27.0.3" \
+    "extras;google;m2repository" \
+    "extras;android;m2repository"
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 0000000..1147347
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,90 @@
+gcp_credentials: ENCRYPTED[1816835da1e936dabb469b51501856ec8468676e35e967bd0fd720a815498e5ee6c8a6a79219ce273f67bcb8f1aa948a]
+
+task:
+  gke_container:
+    dockerfile: .ci/Dockerfile
+    builder_image_name: docker-builder # gce vm image
+    builder_image_project: flutter-cirrus
+    cluster_name: test-cluster
+    zone: us-central1-a
+    namespace: default
+    cpu: 4
+    memory: 8G
+  upgrade_script:
+    - flutter channel master
+    - flutter upgrade
+    - flutter doctor
+    - git fetch origin master
+  activate_script: pub global activate flutter_plugin_tools
+  matrix:
+    - name: analyze
+      script: ./script/incremental_build.sh analyze --custom-analysis=web_benchmarks/testing/test_app
+    - name: publishable
+      script: ./script/check_publish.sh
+      depends_on:
+        - analyze
+    - name: test+format
+      format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-5.0
+      test_script: ./script/incremental_build.sh test
+      depends_on:
+        - analyze
+    - name: build-apks+java-test
+      env:
+        matrix:
+          BUILD_SHARDING: "--shardIndex 0 --shardCount 2"
+          BUILD_SHARDING: "--shardIndex 1 --shardCount 2"
+      script:
+        - ./script/incremental_build.sh build-examples --apk
+        - ./script/incremental_build.sh java-test  # must come after apk build
+      depends_on:
+        - analyze
+    - name: web_benchmarks_test
+      script:
+        - ./script/install_chromium.sh
+        - export CHROME_EXECUTABLE=$(pwd)/.chromium/chrome-linux/chrome
+        - flutter config --enable-web
+        - cd packages/web_benchmarks/testing/test_app
+        - flutter packages get
+        - cd ../..
+        - flutter packages get
+        - dart testing/web_benchmarks_test.dart
+
+task:
+  name: build-ipas
+  use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
+  osx_instance:
+    image: big-sur-xcode-12.4
+  env:
+    PATH: $PATH:/usr/local/bin
+    matrix:
+      BUILD_SHARDING: "--shardIndex 0 --shardCount 2"
+      BUILD_SHARDING: "--shardIndex 1 --shardCount 2"
+  setup_script:
+    - flutter channel master
+    - flutter upgrade
+    - flutter doctor
+    - git fetch origin master
+    - pub global activate flutter_plugin_tools
+  build_script:
+    - ./script/incremental_build.sh build-examples --ipa
+
+task:
+  name: local_tests
+  use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
+  osx_instance:
+    image: big-sur-xcode-12.4
+  env:
+    PATH: $PATH:/usr/local/bin
+    matrix:
+      CHANNEL: "master"
+      CHANNEL: "stable"
+  setup_script:
+    - git fetch origin master
+    - pub global activate flutter_plugin_tools
+    - brew install clang-format
+  upgrade_script:
+    - flutter channel $CHANNEL
+    - flutter upgrade
+    - flutter doctor
+  build_script:
+    - ./script/local_tests.sh
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..31fda4b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,25 @@
+# Auto detect text files and perform LF normalization
+*        text=auto
+
+# Always perform LF normalization on these files
+*.dart   text
+*.gradle text
+*.html   text
+*.java   text
+*.json   text
+*.md     text
+*.py     text
+*.sh     text
+*.txt    text
+*.xml    text
+*.yaml   text
+
+# Make sure that these Windows files always have CRLF line endings in checkout
+*.bat    text eol=crlf
+*.ps1    text eol=crlf
+
+# Never perform LF normalization on these files
+*.ico    binary
+*.jar    binary
+*.png    binary
+*.zip    binary
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..da11475
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,31 @@
+*Replace this paragraph with a description of what this PR is changing or adding, and why.*
+
+*List which issues are fixed by this PR. You must list at least one issue.*
+
+*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
+
+## Pre-launch Checklist
+
+- [ ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]`
+- [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
+- [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
+- [ ] I read and followed the [Flutter Style Guide].
+- [ ] I listed at least one issue that this PR fixes in the description above.
+- [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt.
+- [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy].
+- [ ] I updated `CHANGELOG.md` to add a description of the change.
+- [ ] I updated/added relevant documentation (doc comments with `///`).
+- [ ] I signed the [CLA].
+- [ ] All existing and new tests are passing.
+
+If you need help, consider asking for advice on the `#hackers-new` channel on [Discord].
+
+<!-- Links -->
+[Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
+[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
+[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
+[CLA]: https://cla.developers.google.com/
+[flutter/tests]: https://github.com/flutter/tests
+[breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
+[Discord]: https://github.com/flutter/flutter/wiki/Chat
+[pub versioning philosophy]: https://dart.dev/tools/pub/versioning
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8199ca4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.DS_Store
+.atom/
+.idea
+.packages
+.pub/
+.dart_tool/
+
+pubspec.lock
+
+Podfile.lock
+Pods/
+
+*instrumentscli*.trace
+*.cipd
+
+# Build directories are produced when building using the Flutter CLI.
+build
+
+# This file is produced as a back-up when web_benchmarks fails to parse a
+# Chrome trace.
+chrome-trace.json
+
+# Generated files on example apps
+flutter_export_environment.sh
+.flutter-plugins*
+local.properties
+**/Flutter/Generated.xcconfig
+
+generated_plugin_registrant.*
+GeneratedPluginRegistrant.*
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..2c91cc0
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 5e3e5a2a1a977c34b22f3709109fd237b5cab9c6
+  channel: master
+
+project_type: package
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..3bdb841
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
+Britannio Jarrett <britanniojarrett@gmail.com>
+Sarbagya Dhaubanjar <mail@sarbagyastha.com.np>
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..06ceccd
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,97 @@
+# Contributing to Flutter
+
+[![Build Status](https://api.cirrus-ci.com/github/flutter/packages.svg)](https://cirrus-ci.com/github/flutter/packages)
+
+_See also: [Flutter's code of conduct](https://flutter.io/design-principles/#code-of-conduct)_
+
+## Things you will need
+
+- Linux, Mac OS X, or Windows.
+- git (used for source version control).
+- An ssh client (used to authenticate with GitHub).
+
+## Getting the code and configuring your environment
+
+- Ensure all the dependencies described in the previous section are installed.
+- Fork `https://github.com/flutter/packages` into your own GitHub account. If
+  you already have a fork, and are now installing a development environment on
+  a new machine, make sure you've updated your fork so that you don't use stale
+  configuration options from long ago.
+- If you haven't configured your machine with an SSH key that's known to github, then
+  follow [GitHub's directions](https://help.github.com/articles/generating-ssh-keys/)
+  to generate an SSH key.
+- `git clone git@github.com:<your_name_here>/packages.git`
+- `cd packages`
+- `git remote add upstream git@github.com:flutter/packages.git` (So that you
+  fetch from the master repository, not your clone, when running `git fetch`
+  et al.)
+
+## Running the examples
+
+To run an example with a prebuilt binary from the cloud, switch to that
+example's directory, run `flutter packages get` to make sure its dependencies have been
+downloaded, and use `flutter run`. Make sure you have a device connected over
+USB and debugging enabled on that device. For example:
+
+- `cd packages/palette_generator/example`
+- `flutter packages get`
+- `flutter run`
+
+## Contributing code
+
+We gladly accept contributions via GitHub pull requests.
+
+Please peruse our
+[style guide](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo) and
+[design principles](https://flutter.io/design-principles/) before
+working on anything non-trivial. These guidelines are intended to
+keep the code consistent and avoid common pitfalls.
+
+To start working on a patch:
+
+- `git fetch upstream`
+- `git checkout upstream/master -b <name_of_your_branch>`
+- Hack away.
+- Verify changes with [flutter_plugin_tools](https://pub.dartlang.org/packages/flutter_plugin_tools)
+
+```shell
+pub global activate flutter_plugin_tools
+pub global run flutter_plugin_tools format --plugins package_name
+pub global run flutter_plugin_tools analyze --plugins package_name
+pub global run flutter_plugin_tools test --plugins package_name
+```
+
+_If `pub` is not available, use `flutter pub` instead._
+
+- Check that the package can be published (but don't publish it until it has landed!):
+
+```shell
+cd packages/package_name; pub publish --dry-run
+```
+
+- `git commit -am "<your informative commit message>"`
+- `git push origin <name_of_your_branch>`
+
+To send us a pull request:
+
+- `git pull-request` (if you are using [Hub](http://github.com/github/hub/)) or
+  go to `https://github.com/flutter/packages` and click the
+  "Compare & pull request" button
+
+Please make sure all your checkins have detailed commit messages explaining the patch.
+
+Once you've gotten an LGTM from a project maintainer and once your PR has received
+the green light from all our automated testing (Travis, AppVeyor, etc), submit your
+changes to the `master` branch using one of the following methods:
+
+- Wait for one of the project maintainers to submit it for you.
+- Click the green "Merge pull request" button on the GitHub UI of your pull
+  request (requires commit access).
+
+You must complete the [Contributor License Agreement](https://cla.developers.google.com/clas).
+You can do this online, and it only takes a minute.
+If you've never submitted code before, you must add your (or your
+organization's) name and contact info to the [AUTHORS](AUTHORS) file.
+
+We grant commit access to people who have gained our trust and demonstrated
+a commitment to Flutter.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f0aea64
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# Flutter Packages
+
+[![Build Status](https://api.cirrus-ci.com/github/flutter/packages.svg)](https://cirrus-ci.com/github/flutter/packages/master)
+
+This repo is a companion repo to the main [flutter repo](
+https://github.com/flutter/flutter). It contains the source code for Flutter's
+first-party packages (i.e., packages developed by the core Flutter team).
+Check the [`packages`](./packages) directory to see all packages.
+
+These packages are also available on [pub](https://pub.dartlang.org/flutter/packages).
+
+## Issues
+
+Please file any issues, bugs, or feature requests in the [main flutter
+repo](https://github.com/flutter/flutter/issues/new).
+
+## Contributing
+
+If you wish to contribute a new package to the Flutter ecosystem, please
+see the documentation for [developing packages](https://flutter.io/developing-packages/). You can store
+your package source code in any GitHub repository (the present repo is only
+intended for packages developed by the core Flutter team). Once your package
+is ready you can [publish](https://flutter.io/developing-packages/#publish)
+to the [pub repository](https://pub.dartlang.org/).
+
+If you wish to contribute a change to any of the existing packages in this repo,
+please review our [contribution guide](https://github.com/flutter/packages/blob/master/CONTRIBUTING.md),
+and send a [pull request](https://github.com/flutter/packages/pulls).
+
+## Plugins
+
+These are the available packages in this repository.
+
+| Plugin | Pub |
+|--------|-----|
+| [animations](./packages/animations/) | [![pub package](https://img.shields.io/pub/v/animations.svg)](https://pub.dev/packages/animations) |
+| [extension_google_sign_in_as_googleapis_auth](./packages/extension_google_sign_in_as_googleapis_auth/) | [![pub package](https://img.shields.io/pub/v/extension_google_sign_in_as_googleapis_auth.svg)](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth) |
+| [fuchsia_ctl](./packages/fuchsia_ctl/) | [![pub package](https://img.shields.io/pub/v/fuchsia_ctl.svg)](https://pub.dev/packages/fuchsia_ctl) |
+| [multicast_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) |
+| [palette_generator](./packages/palette_generator/) | [![pub package](https://img.shields.io/pub/v/palette_generator.svg)](https://pub.dartlang.org/packages/palette_generator) |
+| [pigeon](./packages/pigeon/) | [![pub package](https://img.shields.io/pub/v/pigeon.svg)](https://pub.dev/packages/pigeon) |
+| [pointer_interceptor](./packages/pointer_interceptor/) | [![pub package](https://img.shields.io/pub/v/pointer_interceptor.svg)](https://pub.dev/packages/pointer_interceptor) |
+| [xdg_directories](./packages/xdg_directories/) | [![pub package](https://img.shields.io/pub/v/xdg_directories.svg)](https://pub.dev/packages/xdg_directories) |
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..6671951
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,211 @@
+# Specify analysis options.
+#
+# Until there are meta linter rules, each desired lint must be explicitly enabled.
+# See: https://github.com/dart-lang/linter/issues/288
+#
+# For a list of lints, see: http://dart-lang.github.io/linter/lints/
+# See the configuration guide for more
+# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
+#
+# This file is derived from the master file in the flutter repo, and should be
+# kept in sync with it.
+
+analyzer:
+  strong-mode:
+    implicit-dynamic: false
+  errors:
+    # treat missing required parameters as a warning (not a hint)
+    missing_required_param: warning
+    # treat missing returns as a warning (not a hint)
+    missing_return: warning
+    # allow having TODOs in the code
+    todo: ignore
+    # Turned off until null-safe rollout is complete.
+    unnecessary_null_comparison: ignore
+
+linter:
+  rules:
+    # these rules are documented on and in the same order as
+    # the Dart Lint rules page to make maintenance easier
+    # https://github.com/dart-lang/linter/blob/master/example/all.yaml
+    - always_declare_return_types
+    - always_put_control_body_on_new_line
+    # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
+    - always_require_non_null_named_parameters
+    - always_specify_types
+    # - always_use_package_imports # we do this commonly
+    - annotate_overrides
+    # - avoid_annotating_with_dynamic # conflicts with always_specify_types
+    - avoid_bool_literals_in_conditional_expressions
+    # - avoid_catches_without_on_clauses # we do this commonly
+    # - avoid_catching_errors # we do this commonly
+    - avoid_classes_with_only_static_members
+    # - avoid_double_and_int_checks # only useful when targeting JS runtime
+    # - avoid_dynamic_calls # not yet tested
+    - avoid_empty_else
+    - avoid_equals_and_hash_code_on_mutable_classes
+    # - avoid_escaping_inner_quotes # not yet tested
+    - avoid_field_initializers_in_const_classes
+    - avoid_function_literals_in_foreach_calls
+    # - avoid_implementing_value_types # not yet tested
+    - avoid_init_to_null
+    # - avoid_js_rounded_ints # only useful when targeting JS runtime
+    - avoid_null_checks_in_equality_operators
+    # - avoid_positional_boolean_parameters # not yet tested
+    # - avoid_print # not yet tested
+    # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
+    # - avoid_redundant_argument_values # not yet tested
+    - avoid_relative_lib_imports
+    - avoid_renaming_method_parameters
+    - avoid_return_types_on_setters
+    # - avoid_returning_null # there are plenty of valid reasons to return null
+    # - avoid_returning_null_for_future # not yet tested
+    - avoid_returning_null_for_void
+    # - avoid_returning_this # there are plenty of valid reasons to return this
+    # - avoid_setters_without_getters # not yet tested
+    - avoid_shadowing_type_parameters
+    - avoid_single_cascade_in_expression_statements
+    - avoid_slow_async_io
+    - avoid_type_to_string
+    - avoid_types_as_parameter_names
+    # - avoid_types_on_closure_parameters # conflicts with always_specify_types
+    - avoid_unnecessary_containers
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    # - avoid_web_libraries_in_flutter # not yet tested
+    - await_only_futures
+    - camel_case_extensions
+    - camel_case_types
+    - cancel_subscriptions
+    # - cascade_invocations # not yet tested
+    - cast_nullable_to_non_nullable
+    # - close_sinks # not reliable enough
+    # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142
+    # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
+    - control_flow_in_finally
+    # - curly_braces_in_flow_control_structures # not required by flutter style
+    - deprecated_consistency
+    # - diagnostic_describe_all_properties # not yet tested
+    - directives_ordering
+    # - do_not_use_environment # we do this commonly
+    - empty_catches
+    - empty_constructor_bodies
+    - empty_statements
+    - exhaustive_cases
+    - file_names
+    - flutter_style_todos
+    - hash_and_equals
+    - implementation_imports
+    # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
+    - iterable_contains_unrelated_type
+    # - join_return_with_assignment # not required by flutter style
+    - leading_newlines_in_multiline_strings
+    - library_names
+    - library_prefixes
+    # - lines_longer_than_80_chars # not required by flutter style
+    - list_remove_unrelated_type
+    # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    # - no_default_cases # too many false positives
+    - no_duplicate_case_values
+    - no_logic_in_create_state
+    # - no_runtimeType_toString # ok in tests; we enable this only in packages/
+    - non_constant_identifier_names
+    - null_check_on_nullable_type_parameter
+    - null_closures
+    # - omit_local_variable_types # opposite of always_specify_types
+    # - one_member_abstracts # too many false positives
+    # - only_throw_errors # https://github.com/flutter/flutter/issues/5792
+    - overridden_fields
+    - package_api_docs
+    - package_names
+    - package_prefixed_library_names
+    # - parameter_assignments # we do this commonly
+    - prefer_adjacent_string_concatenation
+    - prefer_asserts_in_initializer_lists
+    # - prefer_asserts_with_message # not required by flutter style
+    - prefer_collection_literals
+    - prefer_conditional_assignment
+    - prefer_const_constructors
+    - prefer_const_constructors_in_immutables
+    - prefer_const_declarations
+    - prefer_const_literals_to_create_immutables
+    # - prefer_constructors_over_static_methods # far too many false positives
+    - prefer_contains
+    # - prefer_double_quotes # opposite of prefer_single_quotes
+    - prefer_equal_for_default_values
+    # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
+    - prefer_final_fields
+    - prefer_final_in_for_each
+    - prefer_final_locals
+    - prefer_for_elements_to_map_fromIterable
+    - prefer_foreach
+    - prefer_function_declarations_over_variables
+    - prefer_generic_function_type_aliases
+    - prefer_if_elements_to_conditional_expressions
+    - prefer_if_null_operators
+    - prefer_initializing_formals
+    - prefer_inlined_adds
+    # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants
+    # - prefer_interpolation_to_compose_strings # doesn't work with raw strings, see https://github.com/dart-lang/linter/issues/2490
+    - prefer_is_empty
+    - prefer_is_not_empty
+    - prefer_is_not_operator
+    - prefer_iterable_whereType
+    # - prefer_mixin # https://github.com/dart-lang/language/issues/32
+    - prefer_null_aware_operators
+    # - prefer_relative_imports # incompatible with sub-package imports
+    - prefer_single_quotes
+    - prefer_spread_collections
+    - prefer_typing_uninitialized_variables
+    - prefer_void_to_null
+    - provide_deprecation_message
+    - public_member_api_docs
+    - recursive_getters
+    - sized_box_for_whitespace
+    - slash_for_doc_comments
+    # - sort_child_properties_last # not yet tested
+    - sort_constructors_first
+    - sort_pub_dependencies
+    - sort_unnamed_constructors_first
+    - test_types_in_equals
+    - throw_in_finally
+    - tighten_type_of_initializing_formals
+    # - type_annotate_public_apis # subset of always_specify_types
+    - type_init_formals
+    # - unawaited_futures # too many false positives
+    - unnecessary_await_in_return
+    - unnecessary_brace_in_string_interps
+    - unnecessary_const
+    # - unnecessary_final # conflicts with prefer_final_locals
+    - unnecessary_getters_setters
+    # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
+    - unnecessary_new
+    - unnecessary_null_aware_assignments
+    # - unnecessary_null_checks # not yet tested
+    - unnecessary_null_in_if_null_operators
+    - unnecessary_nullable_for_final_variable_declarations
+    - unnecessary_overrides
+    - unnecessary_parenthesis
+    # - unnecessary_raw_strings # not yet tested
+    - unnecessary_statements
+    - unnecessary_string_escapes
+    - unnecessary_string_interpolations
+    - unnecessary_this
+    - unrelated_type_equality_checks
+    # - unsafe_html # not yet tested
+    - use_full_hex_values_for_flutter_colors
+    - use_function_type_syntax_for_parameters
+    # - use_if_null_to_convert_nulls_to_bools # not yet tested
+    - use_is_even_rather_than_modulo
+    - use_key_in_widget_constructors
+    - use_late_for_private_fields_and_variables
+    # - use_named_constants # not yet yested
+    - use_raw_strings
+    - use_rethrow_when_possible
+    # - use_setters_to_change_properties # not yet tested
+    # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
+    # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
+    - valid_regexps
+    - void_checks
diff --git a/customer_testing.bat b/customer_testing.bat
new file mode 100644
index 0000000..28de6ee
--- /dev/null
+++ b/customer_testing.bat
@@ -0,0 +1,13 @@
+REM This file is used by
+REM https://github.com/flutter/tests/tree/master/registry/flutter_packages.test
+REM to run the tests of certain packages in this repository as a presubmit
+REM for the flutter/flutter repository.
+REM Changes to this file (and any tests in this repository) are only honored
+REM after the commit hash in the "flutter_packages.test" mentioned above has
+REM been updated.
+REM Remember to also update the Posix version (customer_testing.sh) when
+REM changing this file.
+
+CD packages/animations
+CALL flutter analyze
+CALL flutter test
diff --git a/customer_testing.sh b/customer_testing.sh
new file mode 100755
index 0000000..5e9b670
--- /dev/null
+++ b/customer_testing.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This file is used by
+# https://github.com/flutter/tests/tree/master/registry/flutter_packages.test
+# to run the tests of certain packages in this repository as a presubmit
+# for the flutter/flutter repository.
+# Changes to this file (and any tests in this repository) are only honored
+# after the commit hash in the "flutter_packages.test" mentioned above has been
+# updated.
+# Remember to also update the Windows version (customer_testing.bat) when
+# changing this file.
+
+set -e
+
+cd packages/animations
+flutter analyze
+flutter test
diff --git a/dev/README.md b/dev/README.md
new file mode 100644
index 0000000..4690b19
--- /dev/null
+++ b/dev/README.md
@@ -0,0 +1,19 @@
+This directory contains resources that the Flutter team uses during 
+the development of packages.
+
+## Luci builder file
+`try_builders.json` contains the supported luci try builders 
+for packages. It follows format:
+```json
+{
+    "builders":[
+        {
+            "name":"yyy",
+            "repo":"packages",
+            "enabled":true
+        }
+    ]
+}
+```
+This file will be mainly used in [`flutter/cocoon`](https://github.com/flutter/cocoon) 
+to trigger/update pre-submit luci tasks.
diff --git a/dev/try_builders.json b/dev/try_builders.json
new file mode 100644
index 0000000..6986e9e
--- /dev/null
+++ b/dev/try_builders.json
@@ -0,0 +1,9 @@
+{
+   "builders":[
+      {
+         "name":"fuchsia_ctl",
+         "repo":"packages",
+         "enabled":true
+      }
+   ]
+}
diff --git a/packages/animations/.gitignore b/packages/animations/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/animations/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/animations/.metadata b/packages/animations/.metadata
new file mode 100644
index 0000000..28ddf84
--- /dev/null
+++ b/packages/animations/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: ce3c467292293176452a3b4b4fee70073f5af9b3
+  channel: master
+
+project_type: package
diff --git a/packages/animations/CHANGELOG.md b/packages/animations/CHANGELOG.md
new file mode 100644
index 0000000..0ab0f79
--- /dev/null
+++ b/packages/animations/CHANGELOG.md
@@ -0,0 +1,60 @@
+## 2.0.0
+
+* Migrates to null safety.
+* Add `routeSettings` and `filter` option to `showModal`.
+
+## 1.1.2
+
+* Fixes for upcoming changes to the flutter framework.
+
+## 1.1.1
+
+* Hide implementation of `DualTransitionBuilder` as the widget has been implemented in the Flutter framework.
+
+## 1.1.0
+
+* Introduce usage of `DualTransitionBuilder` for all transition widgets, preventing ongoing animations at the start of the transition animation from resetting at the end of the transition animations.
+* Fix `FadeScaleTransition` example's `FloatingActionButton` being accessible
+and tappable when it is supposed to be hidden.
+* `showModal` now defaults to using `FadeScaleTransitionConfiguration` instead of `null`.
+* Added const constructors for `FadeScaleTransitionConfiguration` and `ModalConfiguration`.
+* Add custom fillColor property to `SharedAxisTransition` and `SharedAxisPageTransitionsBuilder`.
+* Fix prefer_const_constructors lint in test and example.
+* Add option `useRootNavigator` to `OpenContainer`.
+* Add `OpenContainer.onClosed`, which is called with a returned value when the container was popped and has returned to the closed state.
+* Fixes a bug with OpenContainer where a crash occurs when the container is dismissed after the container widget itself is removed.
+
+
+## 1.0.0+5
+
+* Fix override analyzer ignore placement.
+
+
+## 1.0.0+4
+
+* Fix a typo in the changelog dates
+* Revert use of modern Material text style nomenclature in the example app
+  to be compatible with Flutter's `stable` branch for the time being.
+* Add override analyzer ignore in modal.dart for reverseTransitionDuration
+  until Flutter's stable branch contains
+  https://github.com/flutter/flutter/pull/48274.
+
+
+## 1.0.0+3
+
+* Update README.md to better describe Material motion
+
+
+## 1.0.0+2
+
+* Fixes to pubspec.yaml
+
+
+## 1.0.0+1
+
+* Fixes to pubspec.yaml
+
+
+## 1.0.0
+
+* Initial release
diff --git a/packages/animations/LICENSE b/packages/animations/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/animations/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/animations/README.md b/packages/animations/README.md
new file mode 100644
index 0000000..3a6f046
--- /dev/null
+++ b/packages/animations/README.md
@@ -0,0 +1,71 @@
+# High quality pre-built Animations for Flutter
+
+This package contains pre-canned animations for commonly-desired effects. The animations can be customized with your content and dropped into your application to delight your users.
+
+To see examples of the following animations on a device or simulator:
+
+```bash
+cd example/
+flutter run --release
+```
+
+## Material motion for Flutter
+
+Material motion is a set of transition patterns that help users understand and navigate an app. Currently,
+the following transition patterns are available in this library:
+
+1.  [Container transform](#container-transform)
+2.  [Shared axis](#shared-axis)
+3.  [Fade through](#fade-through)
+4.  [Fade](#fade)
+
+### Container transform
+
+The **container transform** pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements.
+
+!["Container transform gallery - normal speed and slow motion"](example/demo_gifs/container_transform_lineup.gif)
+_Examples of the container transform:_
+
+1.  _A card into a details page_
+2.  _A list item into a details page_
+3.  _A FAB into a details page_
+4.  _A search bar into expanded search_
+
+### Shared axis
+
+The **shared axis** pattern is used for transitions between UI elements that
+have a spatial or navigational relationship. This pattern uses a shared
+transformation on the x, y, or z axis to reinforce the relationship between
+elements.
+
+!["Shared axis gallery - normal speed and slow motion"](example/demo_gifs/shared_axis_lineup.gif)
+_Examples of the shared axis pattern:_
+
+1.  _An onboarding flow transitions along the x-axis_
+2.  _A stepper transitions along the y-axis_
+3.  _A parent-child navigation transitions along the z-axis_
+
+### Fade through
+
+The **fade through** pattern is used for transitions between UI elements that do
+not have a strong relationship to each other.
+
+!["Fade through gallery - normal speed and slow motion"](example/demo_gifs/fade_through_lineup.gif)
+_Examples of the fade through pattern:_
+
+1.  _Tapping destinations in a bottom navigation bar_
+2.  _Tapping a refresh icon_
+3.  _Tapping an account switcher_
+
+### Fade
+
+The **fade** pattern is used for UI elements that enter or exit within the
+bounds of the screen, such as a dialog that fades in the center of the screen.
+
+!["Fade gallery - normal speed and slow motion"](example/demo_gifs/fade_lineup.gif)
+_Examples of the fade pattern:_
+
+1.  _A dialog_
+2.  _A menu_
+3.  _A snackbar_
+4.  _A FAB_
diff --git a/packages/animations/example/.gitignore b/packages/animations/example/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/animations/example/.gitignore
@@ -0,0 +1,37 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/animations/example/.metadata b/packages/animations/example/.metadata
new file mode 100644
index 0000000..6b34df6
--- /dev/null
+++ b/packages/animations/example/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 0bced151e44ffa138e608c2e2dbff3309ab8bd75
+  channel: master
+
+project_type: app
diff --git a/packages/animations/example/README.md b/packages/animations/example/README.md
new file mode 100644
index 0000000..a170f60
--- /dev/null
+++ b/packages/animations/example/README.md
@@ -0,0 +1,3 @@
+# Example Catalog for package:animations.
+
+Run `flutter run --release` in this directory to launch the catalog on a device.
diff --git a/packages/animations/example/android/.gitignore b/packages/animations/example/android/.gitignore
new file mode 100644
index 0000000..bc2100d
--- /dev/null
+++ b/packages/animations/example/android/.gitignore
@@ -0,0 +1,7 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
diff --git a/packages/animations/example/android/app/build.gradle b/packages/animations/example/android/app/build.gradle
new file mode 100644
index 0000000..5180451
--- /dev/null
+++ b/packages/animations/example/android/app/build.gradle
@@ -0,0 +1,66 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        applicationId "dev.flutter.packages.animations.example"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.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 {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}
diff --git a/packages/animations/example/android/app/src/debug/AndroidManifest.xml b/packages/animations/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..7eb704d
--- /dev/null
+++ b/packages/animations/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="dev.flutter.packages.animations.example">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/animations/example/android/app/src/main/AndroidManifest.xml b/packages/animations/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1fc0ab4
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="dev.flutter.packages.animations.example">
+    <!-- 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="example"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            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>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt b/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt
new file mode 100644
index 0000000..cde3a3b
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/kotlin/dev/flutter/packages/animations/example/MainActivity.kt
@@ -0,0 +1,12 @@
+package dev.flutter.packages.animations.example
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+        GeneratedPluginRegistrant.registerWith(flutterEngine);
+    }
+}
diff --git a/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml b/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/animations/example/android/app/src/main/res/values/styles.xml b/packages/animations/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/animations/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+</resources>
diff --git a/packages/animations/example/android/app/src/profile/AndroidManifest.xml b/packages/animations/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..7eb704d
--- /dev/null
+++ b/packages/animations/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="dev.flutter.packages.animations.example">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/animations/example/android/build.gradle b/packages/animations/example/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/animations/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+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/packages/animations/example/android/gradle.properties b/packages/animations/example/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/animations/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/animations/example/android/settings.gradle b/packages/animations/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/animations/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/animations/example/assets/avatar_logo.png b/packages/animations/example/assets/avatar_logo.png
new file mode 100755
index 0000000..a5d394f
--- /dev/null
+++ b/packages/animations/example/assets/avatar_logo.png
Binary files differ
diff --git a/packages/animations/example/assets/placeholder_image.png b/packages/animations/example/assets/placeholder_image.png
new file mode 100644
index 0000000..913c40e
--- /dev/null
+++ b/packages/animations/example/assets/placeholder_image.png
Binary files differ
diff --git a/packages/animations/example/demo_gifs/container_transform_lineup.gif b/packages/animations/example/demo_gifs/container_transform_lineup.gif
new file mode 100644
index 0000000..c17dab1
--- /dev/null
+++ b/packages/animations/example/demo_gifs/container_transform_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/fade_lineup.gif b/packages/animations/example/demo_gifs/fade_lineup.gif
new file mode 100644
index 0000000..76dbd18
--- /dev/null
+++ b/packages/animations/example/demo_gifs/fade_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/fade_through_lineup.gif b/packages/animations/example/demo_gifs/fade_through_lineup.gif
new file mode 100644
index 0000000..4c5095b
--- /dev/null
+++ b/packages/animations/example/demo_gifs/fade_through_lineup.gif
Binary files differ
diff --git a/packages/animations/example/demo_gifs/shared_axis_lineup.gif b/packages/animations/example/demo_gifs/shared_axis_lineup.gif
new file mode 100644
index 0000000..2d92418
--- /dev/null
+++ b/packages/animations/example/demo_gifs/shared_axis_lineup.gif
Binary files differ
diff --git a/packages/animations/example/ios/.gitignore b/packages/animations/example/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/animations/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist b/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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/packages/animations/example/ios/Flutter/Debug.xcconfig b/packages/animations/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/animations/example/ios/Flutter/Release.xcconfig b/packages/animations/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/animations/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0ab5cd8
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,506 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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 = (
+			);
+			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>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; 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>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		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 = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			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 = 1020;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			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 */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets 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\" embed_and_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 = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift 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 */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = "";
+				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 = dev.flutter.packages.animations.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = "";
+				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 = dev.flutter.packages.animations.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = "";
+				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 = dev.flutter.packages.animations.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				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 */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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"
+      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"
+      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 = "Profile"
+      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/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/animations/example/ios/Runner/AppDelegate.swift b/packages/animations/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/animations/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "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/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "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/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard b/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/animations/example/ios/Runner/Info.plist b/packages/animations/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..a060db6
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>example</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/animations/example/ios/Runner/Runner-Bridging-Header.h b/packages/animations/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..7335fdf
--- /dev/null
+++ b/packages/animations/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
diff --git a/packages/animations/example/lib/container_transition.dart b/packages/animations/example/lib/container_transition.dart
new file mode 100644
index 0000000..e26983f
--- /dev/null
+++ b/packages/animations/example/lib/container_transition.dart
@@ -0,0 +1,537 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:animations/animations.dart';
+
+const String _loremIpsumParagraph =
+    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
+    'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
+    'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
+    'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
+    'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
+    'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
+    'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
+    'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
+    'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
+    'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
+    'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
+    'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
+    'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
+    'vitae.\n'
+    '\n'
+    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
+    'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
+    'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
+    'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
+    'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
+    'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
+    'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
+    'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
+    'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
+    'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
+    'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
+    'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
+    'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
+    'vitae';
+
+const double _fabDimension = 56.0;
+
+/// The demo page for [OpenContainerTransform].
+class OpenContainerTransformDemo extends StatefulWidget {
+  /// Creates the demo page for [OpenContainerTransform].
+  const OpenContainerTransformDemo({Key? key}) : super(key: key);
+
+  @override
+  _OpenContainerTransformDemoState createState() {
+    return _OpenContainerTransformDemoState();
+  }
+}
+
+class _OpenContainerTransformDemoState
+    extends State<OpenContainerTransformDemo> {
+  ContainerTransitionType _transitionType = ContainerTransitionType.fade;
+
+  void _showMarkedAsDoneSnackbar(bool? isMarkedAsDone) {
+    if (isMarkedAsDone ?? false)
+      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+        content: Text('Marked as done!'),
+      ));
+  }
+
+  void _showSettingsBottomModalSheet(BuildContext context) {
+    showModalBottomSheet<void>(
+      context: context,
+      builder: (BuildContext context) {
+        return StatefulBuilder(
+          builder: (BuildContext context, StateSetter setModalState) {
+            return Container(
+              height: 125,
+              padding: const EdgeInsets.all(15.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: <Widget>[
+                  Text(
+                    'Fade mode',
+                    style: Theme.of(context).textTheme.caption,
+                  ),
+                  const SizedBox(height: 12),
+                  ToggleButtons(
+                    borderRadius: BorderRadius.circular(2.0),
+                    selectedBorderColor: Theme.of(context).colorScheme.primary,
+                    onPressed: (int index) {
+                      setModalState(() {
+                        setState(() {
+                          _transitionType = index == 0
+                              ? ContainerTransitionType.fade
+                              : ContainerTransitionType.fadeThrough;
+                        });
+                      });
+                    },
+                    isSelected: <bool>[
+                      _transitionType == ContainerTransitionType.fade,
+                      _transitionType == ContainerTransitionType.fadeThrough,
+                    ],
+                    children: const <Widget>[
+                      Text('FADE'),
+                      Padding(
+                        padding: EdgeInsets.symmetric(horizontal: 10.0),
+                        child: Text('FADE THROUGH'),
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Container transform'),
+        actions: <Widget>[
+          IconButton(
+            icon: const Icon(Icons.settings),
+            onPressed: () {
+              _showSettingsBottomModalSheet(context);
+            },
+          ),
+        ],
+      ),
+      body: ListView(
+        padding: const EdgeInsets.all(8.0),
+        children: <Widget>[
+          _OpenContainerWrapper(
+            transitionType: _transitionType,
+            closedBuilder: (BuildContext _, VoidCallback openContainer) {
+              return _ExampleCard(openContainer: openContainer);
+            },
+            onClosed: _showMarkedAsDoneSnackbar,
+          ),
+          const SizedBox(height: 16.0),
+          _OpenContainerWrapper(
+            transitionType: _transitionType,
+            closedBuilder: (BuildContext _, VoidCallback openContainer) {
+              return _ExampleSingleTile(openContainer: openContainer);
+            },
+            onClosed: _showMarkedAsDoneSnackbar,
+          ),
+          const SizedBox(height: 16.0),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: _OpenContainerWrapper(
+                  transitionType: _transitionType,
+                  closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                    return _SmallerCard(
+                      openContainer: openContainer,
+                      subtitle: 'Secondary text',
+                    );
+                  },
+                  onClosed: _showMarkedAsDoneSnackbar,
+                ),
+              ),
+              const SizedBox(width: 8.0),
+              Expanded(
+                child: _OpenContainerWrapper(
+                  transitionType: _transitionType,
+                  closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                    return _SmallerCard(
+                      openContainer: openContainer,
+                      subtitle: 'Secondary text',
+                    );
+                  },
+                  onClosed: _showMarkedAsDoneSnackbar,
+                ),
+              ),
+            ],
+          ),
+          const SizedBox(height: 16.0),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: _OpenContainerWrapper(
+                  transitionType: _transitionType,
+                  closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                    return _SmallerCard(
+                      openContainer: openContainer,
+                      subtitle: 'Secondary',
+                    );
+                  },
+                  onClosed: _showMarkedAsDoneSnackbar,
+                ),
+              ),
+              const SizedBox(width: 8.0),
+              Expanded(
+                child: _OpenContainerWrapper(
+                  transitionType: _transitionType,
+                  closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                    return _SmallerCard(
+                      openContainer: openContainer,
+                      subtitle: 'Secondary',
+                    );
+                  },
+                  onClosed: _showMarkedAsDoneSnackbar,
+                ),
+              ),
+              const SizedBox(width: 8.0),
+              Expanded(
+                child: _OpenContainerWrapper(
+                  transitionType: _transitionType,
+                  closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                    return _SmallerCard(
+                      openContainer: openContainer,
+                      subtitle: 'Secondary',
+                    );
+                  },
+                  onClosed: _showMarkedAsDoneSnackbar,
+                ),
+              ),
+            ],
+          ),
+          const SizedBox(height: 16.0),
+          ...List<Widget>.generate(10, (int index) {
+            return OpenContainer<bool>(
+              transitionType: _transitionType,
+              openBuilder: (BuildContext _, VoidCallback openContainer) {
+                return const _DetailsPage();
+              },
+              onClosed: _showMarkedAsDoneSnackbar,
+              tappable: false,
+              closedShape: const RoundedRectangleBorder(),
+              closedElevation: 0.0,
+              closedBuilder: (BuildContext _, VoidCallback openContainer) {
+                return ListTile(
+                  leading: Image.asset(
+                    'assets/avatar_logo.png',
+                    width: 40,
+                  ),
+                  onTap: openContainer,
+                  title: Text('List item ${index + 1}'),
+                  subtitle: const Text('Secondary text'),
+                );
+              },
+            );
+          }),
+        ],
+      ),
+      floatingActionButton: OpenContainer(
+        transitionType: _transitionType,
+        openBuilder: (BuildContext context, VoidCallback _) {
+          return const _DetailsPage(
+            includeMarkAsDoneButton: false,
+          );
+        },
+        closedElevation: 6.0,
+        closedShape: const RoundedRectangleBorder(
+          borderRadius: BorderRadius.all(
+            Radius.circular(_fabDimension / 2),
+          ),
+        ),
+        closedColor: Theme.of(context).colorScheme.secondary,
+        closedBuilder: (BuildContext context, VoidCallback openContainer) {
+          return SizedBox(
+            height: _fabDimension,
+            width: _fabDimension,
+            child: Center(
+              child: Icon(
+                Icons.add,
+                color: Theme.of(context).colorScheme.onSecondary,
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+class _OpenContainerWrapper extends StatelessWidget {
+  const _OpenContainerWrapper({
+    required this.closedBuilder,
+    required this.transitionType,
+    required this.onClosed,
+  });
+
+  final CloseContainerBuilder closedBuilder;
+  final ContainerTransitionType transitionType;
+  final ClosedCallback<bool?> onClosed;
+
+  @override
+  Widget build(BuildContext context) {
+    return OpenContainer<bool>(
+      transitionType: transitionType,
+      openBuilder: (BuildContext context, VoidCallback _) {
+        return const _DetailsPage();
+      },
+      onClosed: onClosed,
+      tappable: false,
+      closedBuilder: closedBuilder,
+    );
+  }
+}
+
+class _ExampleCard extends StatelessWidget {
+  const _ExampleCard({required this.openContainer});
+
+  final VoidCallback openContainer;
+
+  @override
+  Widget build(BuildContext context) {
+    return _InkWellOverlay(
+      openContainer: openContainer,
+      height: 300,
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        children: <Widget>[
+          Expanded(
+            child: Container(
+              color: Colors.black38,
+              child: Center(
+                child: Image.asset(
+                  'assets/placeholder_image.png',
+                  width: 100,
+                ),
+              ),
+            ),
+          ),
+          const ListTile(
+            title: Text('Title'),
+            subtitle: Text('Secondary text'),
+          ),
+          Padding(
+            padding: const EdgeInsets.only(
+              left: 16.0,
+              right: 16.0,
+              bottom: 16.0,
+            ),
+            child: Text(
+              'Lorem ipsum dolor sit amet, consectetur '
+              'adipiscing elit, sed do eiusmod tempor.',
+              style: Theme.of(context)
+                  .textTheme
+                  .bodyText2!
+                  .copyWith(color: Colors.black54),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _SmallerCard extends StatelessWidget {
+  const _SmallerCard({
+    required this.openContainer,
+    required this.subtitle,
+  });
+
+  final VoidCallback openContainer;
+  final String subtitle;
+
+  @override
+  Widget build(BuildContext context) {
+    return _InkWellOverlay(
+      openContainer: openContainer,
+      height: 225,
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: <Widget>[
+          Container(
+            color: Colors.black38,
+            height: 150,
+            child: Center(
+              child: Image.asset(
+                'assets/placeholder_image.png',
+                width: 80,
+              ),
+            ),
+          ),
+          Expanded(
+            child: Padding(
+              padding: const EdgeInsets.all(10.0),
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: <Widget>[
+                  Text(
+                    'Title',
+                    style: Theme.of(context).textTheme.headline6,
+                  ),
+                  const SizedBox(height: 4),
+                  Text(
+                    subtitle,
+                    style: Theme.of(context).textTheme.caption,
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _ExampleSingleTile extends StatelessWidget {
+  const _ExampleSingleTile({required this.openContainer});
+
+  final VoidCallback openContainer;
+
+  @override
+  Widget build(BuildContext context) {
+    const double height = 100.0;
+
+    return _InkWellOverlay(
+      openContainer: openContainer,
+      height: height,
+      child: Row(
+        children: <Widget>[
+          Container(
+            color: Colors.black38,
+            height: height,
+            width: height,
+            child: Center(
+              child: Image.asset(
+                'assets/placeholder_image.png',
+                width: 60,
+              ),
+            ),
+          ),
+          Expanded(
+            child: Padding(
+              padding: const EdgeInsets.all(20.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: <Widget>[
+                  Text(
+                    'Title',
+                    style: Theme.of(context).textTheme.subtitle1,
+                  ),
+                  const SizedBox(height: 8),
+                  Text(
+                      'Lorem ipsum dolor sit amet, consectetur '
+                      'adipiscing elit,',
+                      style: Theme.of(context).textTheme.caption),
+                ],
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _InkWellOverlay extends StatelessWidget {
+  const _InkWellOverlay({
+    this.openContainer,
+    this.width,
+    this.height,
+    this.child,
+  });
+
+  final VoidCallback? openContainer;
+  final double? width;
+  final double? height;
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: height,
+      width: width,
+      child: InkWell(
+        onTap: openContainer,
+        child: child,
+      ),
+    );
+  }
+}
+
+class _DetailsPage extends StatelessWidget {
+  const _DetailsPage({this.includeMarkAsDoneButton = true});
+
+  final bool includeMarkAsDoneButton;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Details page'),
+        actions: <Widget>[
+          if (includeMarkAsDoneButton)
+            IconButton(
+              icon: const Icon(Icons.done),
+              onPressed: () => Navigator.pop(context, true),
+              tooltip: 'Mark as done',
+            )
+        ],
+      ),
+      body: ListView(
+        children: <Widget>[
+          Container(
+            color: Colors.black38,
+            height: 250,
+            child: Padding(
+              padding: const EdgeInsets.all(70.0),
+              child: Image.asset(
+                'assets/placeholder_image.png',
+              ),
+            ),
+          ),
+          Padding(
+            padding: const EdgeInsets.all(20.0),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: <Widget>[
+                Text(
+                  'Title',
+                  style: Theme.of(context).textTheme.headline5!.copyWith(
+                        color: Colors.black54,
+                        fontSize: 30.0,
+                      ),
+                ),
+                const SizedBox(height: 10),
+                Text(
+                  _loremIpsumParagraph,
+                  style: Theme.of(context).textTheme.bodyText2!.copyWith(
+                        color: Colors.black54,
+                        height: 1.5,
+                        fontSize: 16.0,
+                      ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/animations/example/lib/fade_scale_transition.dart b/packages/animations/example/lib/fade_scale_transition.dart
new file mode 100644
index 0000000..08ec008
--- /dev/null
+++ b/packages/animations/example/lib/fade_scale_transition.dart
@@ -0,0 +1,139 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [FadeScaleTransition].
+class FadeScaleTransitionDemo extends StatefulWidget {
+  /// Creates the demo page for [FadeScaleTransition].
+  const FadeScaleTransitionDemo({Key? key}) : super(key: key);
+
+  @override
+  _FadeScaleTransitionDemoState createState() =>
+      _FadeScaleTransitionDemoState();
+}
+
+class _FadeScaleTransitionDemoState extends State<FadeScaleTransitionDemo>
+    with SingleTickerProviderStateMixin {
+  late AnimationController _controller;
+
+  @override
+  void initState() {
+    _controller = AnimationController(
+      value: 0.0,
+      duration: const Duration(milliseconds: 150),
+      reverseDuration: const Duration(milliseconds: 75),
+      vsync: this,
+    )..addStatusListener((AnimationStatus status) {
+        setState(() {
+          // setState needs to be called to trigger a rebuild because
+          // the 'HIDE FAB'/'SHOW FAB' button needs to be updated based
+          // the latest value of [_controller.status].
+        });
+      });
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  bool get _isAnimationRunningForwardsOrComplete {
+    switch (_controller.status) {
+      case AnimationStatus.forward:
+      case AnimationStatus.completed:
+        return true;
+      case AnimationStatus.reverse:
+      case AnimationStatus.dismissed:
+        return false;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('Fade')),
+      floatingActionButton: AnimatedBuilder(
+        animation: _controller,
+        builder: (BuildContext context, Widget? child) {
+          return FadeScaleTransition(
+            animation: _controller,
+            child: child,
+          );
+        },
+        child: Visibility(
+          visible: _controller.status != AnimationStatus.dismissed,
+          child: FloatingActionButton(
+            child: const Icon(Icons.add),
+            onPressed: () {},
+          ),
+        ),
+      ),
+      bottomNavigationBar: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          const Divider(height: 0.0),
+          Padding(
+            padding: const EdgeInsets.symmetric(vertical: 8.0),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      builder: (BuildContext context) {
+                        return _ExampleAlertDialog();
+                      },
+                    );
+                  },
+                  child: const Text('SHOW MODAL'),
+                ),
+                const SizedBox(width: 10),
+                ElevatedButton(
+                  onPressed: () {
+                    if (_isAnimationRunningForwardsOrComplete) {
+                      _controller.reverse();
+                    } else {
+                      _controller.forward();
+                    }
+                  },
+                  child: _isAnimationRunningForwardsOrComplete
+                      ? const Text('HIDE FAB')
+                      : const Text('SHOW FAB'),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _ExampleAlertDialog extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      content: const Text('Alert Dialog'),
+      actions: <Widget>[
+        TextButton(
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+          child: const Text('CANCEL'),
+        ),
+        TextButton(
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+          child: const Text('DISCARD'),
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/animations/example/lib/fade_through_transition.dart b/packages/animations/example/lib/fade_through_transition.dart
new file mode 100644
index 0000000..65ccf49
--- /dev/null
+++ b/packages/animations/example/lib/fade_through_transition.dart
@@ -0,0 +1,187 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [FadeThroughTransition].
+class FadeThroughTransitionDemo extends StatefulWidget {
+  /// Creates the demo page for [FadeThroughTransition].
+  const FadeThroughTransitionDemo({Key? key}) : super(key: key);
+
+  @override
+  _FadeThroughTransitionDemoState createState() =>
+      _FadeThroughTransitionDemoState();
+}
+
+class _FadeThroughTransitionDemoState extends State<FadeThroughTransitionDemo> {
+  int pageIndex = 0;
+
+  List<Widget> pageList = <Widget>[
+    _FirstPage(),
+    _SecondPage(),
+    _ThirdPage(),
+  ];
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('Fade through')),
+      body: PageTransitionSwitcher(
+        transitionBuilder: (
+          Widget child,
+          Animation<double> animation,
+          Animation<double> secondaryAnimation,
+        ) {
+          return FadeThroughTransition(
+            animation: animation,
+            secondaryAnimation: secondaryAnimation,
+            child: child,
+          );
+        },
+        child: pageList[pageIndex],
+      ),
+      bottomNavigationBar: BottomNavigationBar(
+        currentIndex: pageIndex,
+        onTap: (int newValue) {
+          setState(() {
+            pageIndex = newValue;
+          });
+        },
+        items: const <BottomNavigationBarItem>[
+          BottomNavigationBarItem(
+            icon: Icon(Icons.photo_library),
+            label: 'Albums',
+          ),
+          BottomNavigationBarItem(
+            icon: Icon(Icons.photo),
+            label: 'Photos',
+          ),
+          BottomNavigationBarItem(
+            icon: Icon(Icons.search),
+            label: 'Search',
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _ExampleCard extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Expanded(
+      child: Card(
+        child: Stack(
+          children: <Widget>[
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              children: <Widget>[
+                Expanded(
+                  child: Container(
+                    color: Colors.black26,
+                    child: Padding(
+                      padding: const EdgeInsets.all(30.0),
+                      child: Ink.image(
+                        image: const AssetImage('assets/placeholder_image.png'),
+                      ),
+                    ),
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: <Widget>[
+                      Text(
+                        '123 photos',
+                        style: Theme.of(context).textTheme.bodyText1,
+                      ),
+                      Text(
+                        '123 photos',
+                        style: Theme.of(context).textTheme.caption,
+                      ),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+            InkWell(
+              splashColor: Colors.black38,
+              onTap: () {},
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class _FirstPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: <Widget>[
+        Expanded(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: <Widget>[
+              _ExampleCard(),
+              _ExampleCard(),
+            ],
+          ),
+        ),
+        Expanded(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: <Widget>[
+              _ExampleCard(),
+              _ExampleCard(),
+            ],
+          ),
+        ),
+        Expanded(
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: <Widget>[
+              _ExampleCard(),
+              _ExampleCard(),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _SecondPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: <Widget>[
+        _ExampleCard(),
+        _ExampleCard(),
+      ],
+    );
+  }
+}
+
+class _ThirdPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return ListView.builder(
+      itemBuilder: (BuildContext context, int index) {
+        return ListTile(
+          leading: Image.asset(
+            'assets/avatar_logo.png',
+            width: 40,
+          ),
+          title: Text('List item ${index + 1}'),
+          subtitle: const Text('Secondary text'),
+        );
+      },
+      itemCount: 10,
+    );
+  }
+}
diff --git a/packages/animations/example/lib/main.dart b/packages/animations/example/lib/main.dart
new file mode 100644
index 0000000..de137e3
--- /dev/null
+++ b/packages/animations/example/lib/main.dart
@@ -0,0 +1,162 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+
+import 'container_transition.dart';
+import 'fade_scale_transition.dart';
+import 'fade_through_transition.dart';
+import 'shared_axis_transition.dart';
+
+void main() {
+  runApp(
+    MaterialApp(
+      theme: ThemeData.from(
+        colorScheme: const ColorScheme.light(),
+      ).copyWith(
+        pageTransitionsTheme: const PageTransitionsTheme(
+          builders: <TargetPlatform, PageTransitionsBuilder>{
+            TargetPlatform.android: ZoomPageTransitionsBuilder(),
+          },
+        ),
+      ),
+      home: _TransitionsHomePage(),
+    ),
+  );
+}
+
+class _TransitionsHomePage extends StatefulWidget {
+  @override
+  _TransitionsHomePageState createState() => _TransitionsHomePageState();
+}
+
+class _TransitionsHomePageState extends State<_TransitionsHomePage> {
+  bool _slowAnimations = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('Material Transitions')),
+      body: Column(
+        children: <Widget>[
+          Expanded(
+            child: ListView(
+              children: <Widget>[
+                _TransitionListTile(
+                  title: 'Container transform',
+                  subtitle: 'OpenContainer',
+                  onTap: () {
+                    Navigator.of(context).push(
+                      MaterialPageRoute<void>(
+                        builder: (BuildContext context) {
+                          return const OpenContainerTransformDemo();
+                        },
+                      ),
+                    );
+                  },
+                ),
+                _TransitionListTile(
+                  title: 'Shared axis',
+                  subtitle: 'SharedAxisTransition',
+                  onTap: () {
+                    Navigator.of(context).push(
+                      MaterialPageRoute<void>(
+                        builder: (BuildContext context) {
+                          return const SharedAxisTransitionDemo();
+                        },
+                      ),
+                    );
+                  },
+                ),
+                _TransitionListTile(
+                  title: 'Fade through',
+                  subtitle: 'FadeThroughTransition',
+                  onTap: () {
+                    Navigator.of(context).push(
+                      MaterialPageRoute<void>(
+                        builder: (BuildContext context) {
+                          return const FadeThroughTransitionDemo();
+                        },
+                      ),
+                    );
+                  },
+                ),
+                _TransitionListTile(
+                  title: 'Fade',
+                  subtitle: 'FadeScaleTransition',
+                  onTap: () {
+                    Navigator.of(context).push(
+                      MaterialPageRoute<void>(
+                        builder: (BuildContext context) {
+                          return const FadeScaleTransitionDemo();
+                        },
+                      ),
+                    );
+                  },
+                ),
+              ],
+            ),
+          ),
+          const Divider(height: 0.0),
+          SafeArea(
+            child: SwitchListTile(
+              value: _slowAnimations,
+              onChanged: (bool value) async {
+                setState(() {
+                  _slowAnimations = value;
+                });
+                // Wait until the Switch is done animating before actually slowing
+                // down time.
+                if (_slowAnimations) {
+                  await Future<void>.delayed(const Duration(milliseconds: 300));
+                }
+                timeDilation = _slowAnimations ? 20.0 : 1.0;
+              },
+              title: const Text('Slow animations'),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _TransitionListTile extends StatelessWidget {
+  const _TransitionListTile({
+    this.onTap,
+    required this.title,
+    required this.subtitle,
+  });
+
+  final GestureTapCallback? onTap;
+  final String title;
+  final String subtitle;
+
+  @override
+  Widget build(BuildContext context) {
+    return ListTile(
+      contentPadding: const EdgeInsets.symmetric(
+        horizontal: 15.0,
+      ),
+      leading: Container(
+        width: 40.0,
+        height: 40.0,
+        decoration: BoxDecoration(
+          borderRadius: BorderRadius.circular(20.0),
+          border: Border.all(
+            color: Colors.black54,
+          ),
+        ),
+        child: const Icon(
+          Icons.play_arrow,
+          size: 35,
+        ),
+      ),
+      onTap: onTap,
+      title: Text(title),
+      subtitle: Text(subtitle),
+    );
+  }
+}
diff --git a/packages/animations/example/lib/shared_axis_transition.dart b/packages/animations/example/lib/shared_axis_transition.dart
new file mode 100644
index 0000000..707e2fd
--- /dev/null
+++ b/packages/animations/example/lib/shared_axis_transition.dart
@@ -0,0 +1,250 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:animations/animations.dart';
+
+/// The demo page for [SharedAxisPageTransitionsBuilder].
+class SharedAxisTransitionDemo extends StatefulWidget {
+  /// Creates the demo page for [SharedAxisPageTransitionsBuilder].
+  const SharedAxisTransitionDemo({Key? key}) : super(key: key);
+
+  @override
+  _SharedAxisTransitionDemoState createState() {
+    return _SharedAxisTransitionDemoState();
+  }
+}
+
+class _SharedAxisTransitionDemoState extends State<SharedAxisTransitionDemo> {
+  SharedAxisTransitionType? _transitionType =
+      SharedAxisTransitionType.horizontal;
+  bool _isLoggedIn = false;
+
+  void _updateTransitionType(SharedAxisTransitionType? newType) {
+    setState(() {
+      _transitionType = newType;
+    });
+  }
+
+  void _toggleLoginStatus() {
+    setState(() {
+      _isLoggedIn = !_isLoggedIn;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      resizeToAvoidBottomInset: false,
+      appBar: AppBar(title: const Text('Shared axis')),
+      body: SafeArea(
+        child: Column(
+          children: <Widget>[
+            Expanded(
+              child: PageTransitionSwitcher(
+                duration: const Duration(milliseconds: 300),
+                reverse: !_isLoggedIn,
+                transitionBuilder: (
+                  Widget child,
+                  Animation<double> animation,
+                  Animation<double> secondaryAnimation,
+                ) {
+                  return SharedAxisTransition(
+                    child: child,
+                    animation: animation,
+                    secondaryAnimation: secondaryAnimation,
+                    transitionType: _transitionType!,
+                  );
+                },
+                child: _isLoggedIn ? _CoursePage() : _SignInPage(),
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 15.0),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: <Widget>[
+                  TextButton(
+                    onPressed: _isLoggedIn ? _toggleLoginStatus : null,
+                    child: const Text('BACK'),
+                  ),
+                  ElevatedButton(
+                    onPressed: _isLoggedIn ? null : _toggleLoginStatus,
+                    child: const Text('NEXT'),
+                  ),
+                ],
+              ),
+            ),
+            const Divider(thickness: 2.0),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                Radio<SharedAxisTransitionType>(
+                  value: SharedAxisTransitionType.horizontal,
+                  groupValue: _transitionType,
+                  onChanged: (SharedAxisTransitionType? newValue) {
+                    _updateTransitionType(newValue);
+                  },
+                ),
+                const Text('X'),
+                Radio<SharedAxisTransitionType>(
+                  value: SharedAxisTransitionType.vertical,
+                  groupValue: _transitionType,
+                  onChanged: (SharedAxisTransitionType? newValue) {
+                    _updateTransitionType(newValue);
+                  },
+                ),
+                const Text('Y'),
+                Radio<SharedAxisTransitionType>(
+                  value: SharedAxisTransitionType.scaled,
+                  groupValue: _transitionType,
+                  onChanged: (SharedAxisTransitionType? newValue) {
+                    _updateTransitionType(newValue);
+                  },
+                ),
+                const Text('Z'),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class _CoursePage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return ListView(
+      children: <Widget>[
+        const Padding(padding: EdgeInsets.symmetric(vertical: 8.0)),
+        Text(
+          'Streamling your courses',
+          style: Theme.of(context).textTheme.headline5,
+          textAlign: TextAlign.center,
+        ),
+        const Padding(padding: EdgeInsets.symmetric(vertical: 5.0)),
+        const Padding(
+          padding: EdgeInsets.symmetric(horizontal: 10.0),
+          child: Text(
+            'Bundled categories appear as groups in your feed. '
+            'You can always change this later.',
+            style: TextStyle(
+              fontSize: 12.0,
+              color: Colors.grey,
+            ),
+            textAlign: TextAlign.center,
+          ),
+        ),
+        const _CourseSwitch(course: 'Arts & Crafts'),
+        const _CourseSwitch(course: 'Business'),
+        const _CourseSwitch(course: 'Illustration'),
+        const _CourseSwitch(course: 'Design'),
+        const _CourseSwitch(course: 'Culinary'),
+      ],
+    );
+  }
+}
+
+class _CourseSwitch extends StatefulWidget {
+  const _CourseSwitch({
+    required this.course,
+  });
+
+  final String course;
+
+  @override
+  _CourseSwitchState createState() => _CourseSwitchState();
+}
+
+class _CourseSwitchState extends State<_CourseSwitch> {
+  bool _value = true;
+
+  @override
+  Widget build(BuildContext context) {
+    final String subtitle = _value ? 'Bundled' : 'Shown Individually';
+    return SwitchListTile(
+      title: Text(widget.course),
+      subtitle: Text(subtitle),
+      value: _value,
+      onChanged: (bool newValue) {
+        setState(() {
+          _value = newValue;
+        });
+      },
+    );
+  }
+}
+
+class _SignInPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        final double maxHeight = constraints.maxHeight;
+        return Column(
+          children: <Widget>[
+            Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 20)),
+            Image.asset(
+              'assets/avatar_logo.png',
+              width: 80,
+            ),
+            Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 50)),
+            Text(
+              'Hi David Park',
+              style: Theme.of(context).textTheme.headline5,
+            ),
+            Padding(padding: EdgeInsets.symmetric(vertical: maxHeight / 50)),
+            const Text(
+              'Sign in with your account',
+              style: TextStyle(
+                fontSize: 12.0,
+                color: Colors.grey,
+              ),
+            ),
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: <Widget>[
+                const Padding(
+                  padding: EdgeInsets.only(
+                    top: 40.0,
+                    left: 15.0,
+                    right: 15.0,
+                    bottom: 10.0,
+                  ),
+                  child: TextField(
+                    decoration: InputDecoration(
+                      suffixIcon: Icon(
+                        Icons.visibility,
+                        size: 20,
+                        color: Colors.black54,
+                      ),
+                      isDense: true,
+                      labelText: 'Email or phone number',
+                      border: OutlineInputBorder(),
+                    ),
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.only(left: 10.0),
+                  child: TextButton(
+                    onPressed: () {},
+                    child: const Text('FORGOT EMAIL?'),
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.only(left: 10.0),
+                  child: TextButton(
+                    onPressed: () {},
+                    child: const Text('CREATE ACCOUNT'),
+                  ),
+                ),
+              ],
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/packages/animations/example/pubspec.yaml b/packages/animations/example/pubspec.yaml
new file mode 100644
index 0000000..ecee1e8
--- /dev/null
+++ b/packages/animations/example/pubspec.yaml
@@ -0,0 +1,26 @@
+name: animations_example
+description: A catalog containing example animations from package:animations.
+
+publish_to: none
+
+version: 0.0.1
+
+environment:
+  sdk: ">=2.12.0-259.9.beta <3.0.0"
+
+dependencies:
+  animations:
+    path: ../../animations
+  cupertino_icons: ^1.0.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
+  assets:
+    - assets/avatar_logo.png
+    - assets/placeholder_image.png
diff --git a/packages/animations/example/web/index.html b/packages/animations/example/web/index.html
new file mode 100644
index 0000000..6eff9a7
--- /dev/null
+++ b/packages/animations/example/web/index.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>example</title>
+</head>
+<body>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart
new file mode 100644
index 0000000..4178f80
--- /dev/null
+++ b/packages/animations/lib/animations.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/fade_scale_transition.dart';
+export 'src/fade_through_transition.dart';
+export 'src/modal.dart';
+export 'src/open_container.dart';
+export 'src/page_transition_switcher.dart';
+export 'src/shared_axis_transition.dart';
diff --git a/packages/animations/lib/src/fade_scale_transition.dart b/packages/animations/lib/src/fade_scale_transition.dart
new file mode 100644
index 0000000..059e5cb
--- /dev/null
+++ b/packages/animations/lib/src/fade_scale_transition.dart
@@ -0,0 +1,180 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+import 'modal.dart';
+
+/// The modal transition configuration for a Material fade transition.
+///
+/// The fade pattern is used for UI elements that enter or exit from within
+/// the screen bounds. Elements that enter use a quick fade in and scale from
+/// 80% to 100%. Elements that exit simply fade out. The scale animation is
+/// only applied to entering elements to emphasize new content over old.
+///
+/// ```dart
+/// /// Sample widget that uses [showModal] with [FadeScaleTransitionConfiguration].
+/// class MyHomePage extends StatelessWidget {
+///   @override
+///   Widget build(BuildContext context) {
+///     return Scaffold(
+///       body: Center(
+///         child: ElevatedButton(
+///           onPressed: () {
+///             showModal(
+///               context: context,
+///               configuration: FadeScaleTransitionConfiguration(),
+///               builder: (BuildContext context) {
+///                 return _CenteredFlutterLogo();
+///               },
+///             );
+///           },
+///           child: Icon(Icons.add),
+///         ),
+///       ),
+///     );
+///   }
+/// }
+///
+/// /// Displays a centered Flutter logo with size constraints.
+/// class _CenteredFlutterLogo extends StatelessWidget {
+///   const _CenteredFlutterLogo();
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     return Center(
+///       child: SizedBox(
+///         width: 250,
+///         height: 250,
+///         child: const Material(
+///           child: Center(
+///             child: FlutterLogo(size: 250),
+///           ),
+///         ),
+///       ),
+///     );
+///   }
+/// }
+/// ```
+class FadeScaleTransitionConfiguration extends ModalConfiguration {
+  /// Creates the Material fade transition configuration.
+  ///
+  /// [barrierDismissible] configures whether or not tapping the modal's
+  /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+  /// a dismissible barrier. [barrierDismissible] cannot be null. If
+  /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+  const FadeScaleTransitionConfiguration({
+    Color barrierColor = Colors.black54,
+    bool barrierDismissible = true,
+    Duration transitionDuration = const Duration(milliseconds: 150),
+    Duration reverseTransitionDuration = const Duration(milliseconds: 75),
+    String barrierLabel = 'Dismiss',
+  }) : super(
+          barrierColor: barrierColor,
+          barrierDismissible: barrierDismissible,
+          barrierLabel: barrierLabel,
+          transitionDuration: transitionDuration,
+          reverseTransitionDuration: reverseTransitionDuration,
+        );
+
+  @override
+  Widget transitionBuilder(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return FadeScaleTransition(
+      animation: animation,
+      child: child,
+    );
+  }
+}
+
+/// A widget that implements the Material fade transition.
+///
+/// The fade pattern is used for UI elements that enter or exit from within
+/// the screen bounds. Elements that enter use a quick fade in and scale from
+/// 80% to 100%. Elements that exit simply fade out. The scale animation is
+/// only applied to entering elements to emphasize new content over old.
+///
+/// This widget is not to be confused with Flutter's [FadeTransition] widget,
+/// which animates only the opacity of its child widget.
+class FadeScaleTransition extends StatelessWidget {
+  /// Creates a widget that implements the Material fade transition.
+  ///
+  /// The fade pattern is used for UI elements that enter or exit from within
+  /// the screen bounds. Elements that enter use a quick fade in and scale from
+  /// 80% to 100%. Elements that exit simply fade out. The scale animation is
+  /// only applied to entering elements to emphasize new content over old.
+  ///
+  /// This widget is not to be confused with Flutter's [FadeTransition] widget,
+  /// which animates only the opacity of its child widget.
+  ///
+  /// [animation] is typically an [AnimationController] that drives the transition
+  /// animation. [animation] cannot be null.
+  const FadeScaleTransition({
+    Key? key,
+    required this.animation,
+    this.child,
+  }) : super(key: key);
+
+  /// The animation that drives the [child]'s entrance and exit.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.animate], which is the value given to this property
+  ///    when it is used as a page transition.
+  final Animation<double> animation;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// This widget will transition in and out as driven by [animation] and
+  /// [secondaryAnimation].
+  final Widget? child;
+
+  static final Animatable<double> _fadeInTransition = CurveTween(
+    curve: const Interval(0.0, 0.3),
+  );
+  static final Animatable<double> _scaleInTransition = Tween<double>(
+    begin: 0.80,
+    end: 1.00,
+  ).chain(CurveTween(curve: decelerateEasing));
+  static final Animatable<double> _fadeOutTransition = Tween<double>(
+    begin: 1.0,
+    end: 0.0,
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return DualTransitionBuilder(
+      animation: animation,
+      forwardBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return FadeTransition(
+          opacity: _fadeInTransition.animate(animation),
+          child: ScaleTransition(
+            scale: _scaleInTransition.animate(animation),
+            child: child,
+          ),
+        );
+      },
+      reverseBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return FadeTransition(
+          opacity: _fadeOutTransition.animate(animation),
+          child: child,
+        );
+      },
+      child: child,
+    );
+  }
+}
diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart
new file mode 100644
index 0000000..8e70fc6
--- /dev/null
+++ b/packages/animations/lib/src/fade_through_transition.dart
@@ -0,0 +1,333 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+/// Used by [PageTransitionsTheme] to define a page route transition animation
+/// in which the outgoing page fades out, then the incoming page fades in and
+/// scale up.
+///
+/// This pattern is recommended for a transition between UI elements that do not
+/// have a strong relationship to one another.
+///
+/// Scale is only applied to incoming elements to emphasize new content over
+/// old.
+///
+/// The following example shows how the FadeThroughPageTransitionsBuilder can
+/// be used in a [PageTransitionsTheme] to change the default transitions
+/// of [MaterialPageRoute]s.
+///
+/// ```dart
+/// MaterialApp(
+///   theme: ThemeData(
+///     pageTransitionsTheme: PageTransitionsTheme(
+///       builders: {
+///         TargetPlatform.android: FadeThroughPageTransitionsBuilder(),
+///         TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),
+///       },
+///     ),
+///   ),
+///   routes: {
+///     '/': (BuildContext context) {
+///       return Container(
+///         color: Colors.red,
+///         child: Center(
+///           child: TextButton(
+///             child: Text('Push route'),
+///             onPressed: () {
+///               Navigator.of(context).pushNamed('/a');
+///             },
+///           ),
+///         ),
+///       );
+///     },
+///     '/a' : (BuildContext context) {
+///       return Container(
+///         color: Colors.blue,
+///         child: Center(
+///           child: TextButton(
+///             child: Text('Pop route'),
+///             onPressed: () {
+///               Navigator.of(context).pop();
+///             },
+///           ),
+///         ),
+///       );
+///     },
+///   },
+/// );
+/// ```
+class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder {
+  /// Creates a [FadeThroughPageTransitionsBuilder].
+  const FadeThroughPageTransitionsBuilder({this.fillColor});
+
+  /// The color to use for the background color during the transition.
+  ///
+  /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+  final Color? fillColor;
+
+  @override
+  Widget buildTransitions<T>(
+    PageRoute<T>? route,
+    BuildContext? context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return FadeThroughTransition(
+      animation: animation,
+      secondaryAnimation: secondaryAnimation,
+      fillColor: fillColor,
+      child: child,
+    );
+  }
+}
+
+/// Defines a transition in which outgoing elements fade out, then incoming
+/// elements fade in and scale up.
+///
+/// The fade through pattern provides a transition animation between UI elements
+/// that do not have a strong relationship to one another. As an example, the
+/// [BottomNavigationBar] may use this animation to transition the currently
+/// displayed content when a new [BottomNavigationBarItem] is selected.
+///
+/// Scale is only applied to incoming elements to emphasize new content over
+/// old.
+///
+/// Consider using [FadeThroughPageTransitionsBuilder] within a
+/// [PageTransitionsTheme] if you want to apply this kind of transition to
+/// [MaterialPageRoute] transitions within a Navigator (see
+/// [FadeThroughPageTransitionsBuilder] for some example code). Or use this transition
+/// directly in a [PageTransitionSwitcher.transitionBuilder] to transition
+/// from one widget to another as seen in the following example:
+///
+/// ```dart
+///  int _selectedIndex = 0;
+///
+///  final List<Color> _colors = [Colors.blue, Colors.red, Colors.yellow];
+///
+///  @override
+///  Widget build(BuildContext context) {
+///    return Scaffold(
+///      appBar: AppBar(
+///        title: const Text('Switcher Sample'),
+///      ),
+///      body: PageTransitionSwitcher(
+///        transitionBuilder: (
+///          Widget child,
+///          Animation<double> primaryAnimation,
+///          Animation<double> secondaryAnimation,
+///        ) {
+///          return FadeThroughTransition(
+///            child: child,
+///            animation: primaryAnimation,
+///            secondaryAnimation: secondaryAnimation,
+///          );
+///        },
+///        child: Container(
+///          key: ValueKey<int>(_selectedIndex),
+///          color: _colors[_selectedIndex],
+///        ),
+///      ),
+///      bottomNavigationBar: BottomNavigationBar(
+///        items: const <BottomNavigationBarItem>[
+///          BottomNavigationBarItem(
+///            icon: Icon(Icons.home),
+///            title: Text('Blue'),
+///          ),
+///          BottomNavigationBarItem(
+///            icon: Icon(Icons.business),
+///            title: Text('Red'),
+///          ),
+///          BottomNavigationBarItem(
+///            icon: Icon(Icons.school),
+///            title: Text('Yellow'),
+///          ),
+///        ],
+///        currentIndex: _selectedIndex,
+///        selectedItemColor: Colors.amber[800],
+///        onTap: (int index) {
+///          setState(() {
+///            _selectedIndex = index;
+///          });
+///        },
+///      ),
+///    );
+///  }
+/// ```
+class FadeThroughTransition extends StatelessWidget {
+  /// Creates a [FadeThroughTransition].
+  ///
+  /// The [animation] and [secondaryAnimation] argument are required and must
+  /// not be null.
+  const FadeThroughTransition({
+    Key? key,
+    required this.animation,
+    required this.secondaryAnimation,
+    this.fillColor,
+    this.child,
+  }) : super(key: key);
+
+  /// The animation that drives the [child]'s entrance and exit.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.animate], which is the value given to this property
+  ///    when the [FadeThroughTransition] is used as a page transition.
+  final Animation<double> animation;
+
+  /// The animation that transitions [child] when new content is pushed on top
+  /// of it.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.secondaryAnimation], which is the value given to this
+  //     property when the [FadeThroughTransition] is used as a page transition.
+  final Animation<double> secondaryAnimation;
+
+  /// The color to use for the background color during the transition.
+  ///
+  /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+  final Color? fillColor;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// This widget will transition in and out as driven by [animation] and
+  /// [secondaryAnimation].
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    return _ZoomedFadeInFadeOut(
+      animation: animation,
+      child: Container(
+        color: fillColor ?? Theme.of(context).canvasColor,
+        child: _ZoomedFadeInFadeOut(
+          animation: ReverseAnimation(secondaryAnimation),
+          child: child,
+        ),
+      ),
+    );
+  }
+}
+
+class _ZoomedFadeInFadeOut extends StatelessWidget {
+  const _ZoomedFadeInFadeOut({Key? key, required this.animation, this.child})
+      : super(key: key);
+
+  final Animation<double> animation;
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    return DualTransitionBuilder(
+      animation: animation,
+      forwardBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return _ZoomedFadeIn(
+          animation: animation,
+          child: child,
+        );
+      },
+      reverseBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return _FadeOut(
+          child: child,
+          animation: animation,
+        );
+      },
+      child: child,
+    );
+  }
+}
+
+class _ZoomedFadeIn extends StatelessWidget {
+  const _ZoomedFadeIn({
+    this.child,
+    required this.animation,
+  });
+
+  final Widget? child;
+  final Animation<double> animation;
+
+  static final CurveTween _inCurve = CurveTween(
+    curve: const Cubic(0.0, 0.0, 0.2, 1.0),
+  );
+  static final TweenSequence<double> _scaleIn = TweenSequence<double>(
+    <TweenSequenceItem<double>>[
+      TweenSequenceItem<double>(
+        tween: ConstantTween<double>(0.92),
+        weight: 6 / 20,
+      ),
+      TweenSequenceItem<double>(
+        tween: Tween<double>(begin: 0.92, end: 1.0).chain(_inCurve),
+        weight: 14 / 20,
+      ),
+    ],
+  );
+  static final TweenSequence<double> _fadeInOpacity = TweenSequence<double>(
+    <TweenSequenceItem<double>>[
+      TweenSequenceItem<double>(
+        tween: ConstantTween<double>(0.0),
+        weight: 6 / 20,
+      ),
+      TweenSequenceItem<double>(
+        tween: Tween<double>(begin: 0.0, end: 1.0).chain(_inCurve),
+        weight: 14 / 20,
+      ),
+    ],
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return FadeTransition(
+      opacity: _fadeInOpacity.animate(animation),
+      child: ScaleTransition(
+        scale: _scaleIn.animate(animation),
+        child: child,
+      ),
+    );
+  }
+}
+
+class _FadeOut extends StatelessWidget {
+  const _FadeOut({
+    this.child,
+    required this.animation,
+  });
+
+  final Widget? child;
+  final Animation<double> animation;
+
+  static final CurveTween _outCurve = CurveTween(
+    curve: const Cubic(0.4, 0.0, 1.0, 1.0),
+  );
+  static final TweenSequence<double> _fadeOutOpacity = TweenSequence<double>(
+    <TweenSequenceItem<double>>[
+      TweenSequenceItem<double>(
+        tween: Tween<double>(begin: 1.0, end: 0.0).chain(_outCurve),
+        weight: 6 / 20,
+      ),
+      TweenSequenceItem<double>(
+        tween: ConstantTween<double>(0.0),
+        weight: 14 / 20,
+      ),
+    ],
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return FadeTransition(
+      opacity: _fadeOutOpacity.animate(animation),
+      child: child,
+    );
+  }
+}
diff --git a/packages/animations/lib/src/modal.dart b/packages/animations/lib/src/modal.dart
new file mode 100644
index 0000000..65b6f60
--- /dev/null
+++ b/packages/animations/lib/src/modal.dart
@@ -0,0 +1,224 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+
+import 'fade_scale_transition.dart';
+
+/// Signature for a function that creates a widget that builds a
+/// transition.
+///
+/// Used by [PopupRoute].
+typedef _ModalTransitionBuilder = Widget Function(
+  BuildContext context,
+  Animation<double> animation,
+  Animation<double> secondaryAnimation,
+  Widget child,
+);
+
+/// Displays a modal above the current contents of the app.
+///
+/// Content below the modal is dimmed with a [ModalBarrier].
+///
+/// The `context` argument is used to look up the [Navigator] for the
+/// modal. It is only used when the method is called. Its corresponding widget
+/// can be safely removed from the tree before the modal is closed.
+///
+/// The `configuration` argument is used to determine characteristics of the
+/// modal route that will be displayed, such as the enter and exit
+/// transitions, the duration of the transitions, and modal barrier
+/// properties. By default, `configuration` is
+/// [FadeScaleTransitionConfiguration].
+///
+/// The `useRootNavigator` argument is used to determine whether to push the
+/// modal to the [Navigator] furthest from or nearest to the given `context`.
+/// By default, `useRootNavigator` is `true` and the modal route created by
+/// this method is pushed to the root navigator. If the application has
+/// multiple [Navigator] objects, it may be necessary to call
+/// `Navigator.of(context, rootNavigator: true).pop(result)` to close the
+/// modal rather than just `Navigator.pop(context, result)`.
+///
+/// Returns a [Future] that resolves to the value (if any) that was passed to
+/// [Navigator.pop] when the modal was closed.
+///
+/// See also:
+///
+/// * [ModalConfiguration], which is the configuration object used to define
+/// the modal's characteristics.
+Future<T?> showModal<T>({
+  required BuildContext context,
+  ModalConfiguration configuration = const FadeScaleTransitionConfiguration(),
+  bool useRootNavigator = true,
+  required WidgetBuilder builder,
+  RouteSettings? routeSettings,
+  ui.ImageFilter? filter,
+}) {
+  String? barrierLabel = configuration.barrierLabel;
+  // Avoid looking up [MaterialLocalizations.of(context).modalBarrierDismissLabel]
+  // if there is no dismissible barrier.
+  if (configuration.barrierDismissible && configuration.barrierLabel == null) {
+    barrierLabel = MaterialLocalizations.of(context).modalBarrierDismissLabel;
+  }
+  assert(!configuration.barrierDismissible || barrierLabel != null);
+  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
+    _ModalRoute<T>(
+      barrierColor: configuration.barrierColor,
+      barrierDismissible: configuration.barrierDismissible,
+      barrierLabel: barrierLabel,
+      transitionBuilder: configuration.transitionBuilder,
+      transitionDuration: configuration.transitionDuration,
+      reverseTransitionDuration: configuration.reverseTransitionDuration,
+      builder: builder,
+      routeSettings: routeSettings,
+      filter: filter,
+    ),
+  );
+}
+
+// A modal route that overlays a widget on the current route.
+class _ModalRoute<T> extends PopupRoute<T> {
+  /// Creates a route with general modal route.
+  ///
+  /// [barrierDismissible] configures whether or not tapping the modal's
+  /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+  /// a dismissible barrier. [barrierDismissible] cannot be null. If
+  /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+  ///
+  /// [transitionBuilder] takes in a function that creates a widget. This
+  /// widget is typically used to configure the modal's transition.
+  _ModalRoute({
+    this.barrierColor,
+    this.barrierDismissible = true,
+    this.barrierLabel,
+    required this.transitionDuration,
+    required this.reverseTransitionDuration,
+    required _ModalTransitionBuilder transitionBuilder,
+    required this.builder,
+    RouteSettings? routeSettings,
+    ui.ImageFilter? filter,
+  })  : assert(!barrierDismissible || barrierLabel != null),
+        _transitionBuilder = transitionBuilder,
+        super(filter: filter, settings: routeSettings);
+
+  @override
+  final Color? barrierColor;
+
+  @override
+  final bool barrierDismissible;
+
+  @override
+  final String? barrierLabel;
+
+  @override
+  final Duration transitionDuration;
+
+  @override
+  final Duration reverseTransitionDuration;
+
+  /// The primary contents of the modal.
+  final WidgetBuilder builder;
+
+  final _ModalTransitionBuilder _transitionBuilder;
+
+  @override
+  Widget buildPage(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+  ) {
+    final ThemeData theme = Theme.of(context);
+    return Semantics(
+      child: SafeArea(
+        child: Builder(
+          builder: (BuildContext context) {
+            final Widget child = Builder(builder: builder);
+            return Theme(data: theme, child: child);
+          },
+        ),
+      ),
+      scopesRoute: true,
+      explicitChildNodes: true,
+    );
+  }
+
+  @override
+  Widget buildTransitions(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return _transitionBuilder(
+      context,
+      animation,
+      secondaryAnimation,
+      child,
+    );
+  }
+}
+
+/// A configuration object containing the properties needed to implement a
+/// modal route.
+///
+/// The `barrierDismissible` argument is used to determine whether this route
+/// can be dismissed by tapping the modal barrier. This argument defaults
+/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be
+/// provided.
+///
+/// The `barrierLabel` argument is the semantic label used for a dismissible
+/// barrier. This argument defaults to "Dismiss".
+abstract class ModalConfiguration {
+  /// Creates a modal configuration object that provides the necessary
+  /// properties to implement a modal route.
+  ///
+  /// [barrierDismissible] configures whether or not tapping the modal's
+  /// scrim dismisses the modal. [barrierLabel] sets the semantic label for
+  /// a dismissible barrier. [barrierDismissible] cannot be null. If
+  /// [barrierDismissible] is true, the [barrierLabel] cannot be null.
+  ///
+  /// [transitionDuration] and [reverseTransitionDuration] determine the
+  /// duration of the transitions when the modal enters and exits the
+  /// application. [transitionDuration] and [reverseTransitionDuration]
+  /// cannot be null.
+  const ModalConfiguration({
+    required this.barrierColor,
+    required this.barrierDismissible,
+    this.barrierLabel,
+    required this.transitionDuration,
+    required this.reverseTransitionDuration,
+  }) : assert(!barrierDismissible || barrierLabel != null);
+
+  /// The color to use for the modal barrier. If this is null, the barrier will
+  /// be transparent.
+  final Color barrierColor;
+
+  /// Whether you can dismiss this route by tapping the modal barrier.
+  final bool barrierDismissible;
+
+  /// The semantic label used for a dismissible barrier.
+  final String? barrierLabel;
+
+  /// The duration of the transition running forwards.
+  final Duration transitionDuration;
+
+  /// The duration of the transition running in reverse.
+  final Duration reverseTransitionDuration;
+
+  /// A builder that defines how the route arrives on and leaves the screen.
+  ///
+  /// The [buildTransitions] method is typically used to define transitions
+  /// that animate the new topmost route's comings and goings. When the
+  /// [Navigator] pushes a route on the top of its stack, the new route's
+  /// primary [animation] runs from 0.0 to 1.0. When the [Navigator] pops the
+  /// topmost route, e.g. because the use pressed the back button, the
+  /// primary animation runs from 1.0 to 0.0.
+  Widget transitionBuilder(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  );
+}
diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart
new file mode 100644
index 0000000..7b243ba
--- /dev/null
+++ b/packages/animations/lib/src/open_container.dart
@@ -0,0 +1,903 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+
+/// Signature for `action` callback function provided to [OpenContainer.openBuilder].
+///
+/// Parameter `returnValue` is the value which will be provided to [OpenContainer.onClosed]
+/// when `action` is called.
+typedef CloseContainerActionCallback<S> = void Function({S? returnValue});
+
+/// Signature for a function that creates a [Widget] in open state within an
+/// [OpenContainer].
+///
+/// The `action` callback provided to [OpenContainer.openBuilder] can be used
+/// to close the container.
+typedef OpenContainerBuilder<S> = Widget Function(
+  BuildContext context,
+  CloseContainerActionCallback<S> action,
+);
+
+/// Signature for a function that creates a [Widget] in closed state within an
+/// [OpenContainer].
+///
+/// The `action` callback provided to [OpenContainer.closedBuilder] can be used
+/// to open the container.
+typedef CloseContainerBuilder = Widget Function(
+  BuildContext context,
+  VoidCallback action,
+);
+
+/// The [OpenContainer] widget's fade transition type.
+///
+/// This determines the type of fade transition that the incoming and outgoing
+/// contents will use.
+enum ContainerTransitionType {
+  /// Fades the incoming element in over the outgoing element.
+  fade,
+
+  /// First fades the outgoing element out, and starts fading the incoming
+  /// element in once the outgoing element has completely faded out.
+  fadeThrough,
+}
+
+/// Callback function which is called when the [OpenContainer]
+/// is closed.
+typedef ClosedCallback<S> = void Function(S data);
+
+/// A container that grows to fill the screen to reveal new content when tapped.
+///
+/// While the container is closed, it shows the [Widget] returned by
+/// [closedBuilder]. When the container is tapped it grows to fill the entire
+/// size of the surrounding [Navigator] while fading out the widget returned by
+/// [closedBuilder] and fading in the widget returned by [openBuilder]. When the
+/// container is closed again via the callback provided to [openBuilder] or via
+/// Android's back button, the animation is reversed: The container shrinks back
+/// to its original size while the widget returned by [openBuilder] is faded out
+/// and the widget returned by [openBuilder] is faded back in.
+///
+/// By default, the container is in the closed state. During the transition from
+/// closed to open and vice versa the widgets returned by the [openBuilder] and
+/// [closedBuilder] exist in the tree at the same time. Therefore, the widgets
+/// returned by these builders cannot include the same global key.
+///
+/// `T` refers to the type of data returned by the route when the container
+/// is closed. This value can be accessed in the `onClosed` function.
+///
+// TODO(goderbauer): Add example animations and sample code.
+///
+/// See also:
+///
+///  * [Transitions with animated containers](https://material.io/design/motion/choreography.html#transformation)
+///    in the Material spec.
+@optionalTypeArgs
+class OpenContainer<T extends Object?> extends StatefulWidget {
+  /// Creates an [OpenContainer].
+  ///
+  /// All arguments except for [key] must not be null. The arguments
+  /// [openBuilder] and [closedBuilder] are required.
+  const OpenContainer({
+    Key? key,
+    this.closedColor = Colors.white,
+    this.openColor = Colors.white,
+    this.middleColor,
+    this.closedElevation = 1.0,
+    this.openElevation = 4.0,
+    this.closedShape = const RoundedRectangleBorder(
+      borderRadius: BorderRadius.all(Radius.circular(4.0)),
+    ),
+    this.openShape = const RoundedRectangleBorder(),
+    this.onClosed,
+    required this.closedBuilder,
+    required this.openBuilder,
+    this.tappable = true,
+    this.transitionDuration = const Duration(milliseconds: 300),
+    this.transitionType = ContainerTransitionType.fade,
+    this.useRootNavigator = false,
+    this.routeSettings,
+    this.clipBehavior = Clip.antiAlias,
+  }) : super(key: key);
+
+  /// Background color of the container while it is closed.
+  ///
+  /// When the container is opened, it will first transition from this color
+  /// to [middleColor] and then transition from there to [openColor] in one
+  /// smooth animation. When the container is closed, it will transition back to
+  /// this color from [openColor] via [middleColor].
+  ///
+  /// Defaults to [Colors.white].
+  ///
+  /// See also:
+  ///
+  ///  * [Material.color], which is used to implement this property.
+  final Color closedColor;
+
+  /// Background color of the container while it is open.
+  ///
+  /// When the container is closed, it will first transition from [closedColor]
+  /// to [middleColor] and then transition from there to this color in one
+  /// smooth animation. When the container is closed, it will transition back to
+  /// [closedColor] from this color via [middleColor].
+  ///
+  /// Defaults to [Colors.white].
+  ///
+  /// See also:
+  ///
+  ///  * [Material.color], which is used to implement this property.
+  final Color openColor;
+
+  /// The color to use for the background color during the transition
+  /// with [ContainerTransitionType.fadeThrough].
+  ///
+  /// Defaults to [Theme]'s [ThemeData.canvasColor].
+  ///
+  /// See also:
+  ///
+  ///  * [Material.color], which is used to implement this property.
+  final Color? middleColor;
+
+  /// Elevation of the container while it is closed.
+  ///
+  /// When the container is opened, it will transition from this elevation to
+  /// [openElevation]. When the container is closed, it will transition back
+  /// from [openElevation] to this elevation.
+  ///
+  /// Defaults to 1.0.
+  ///
+  /// See also:
+  ///
+  ///  * [Material.elevation], which is used to implement this property.
+  final double closedElevation;
+
+  /// Elevation of the container while it is open.
+  ///
+  /// When the container is opened, it will transition to this elevation from
+  /// [closedElevation]. When the container is closed, it will transition back
+  /// from this elevation to [closedElevation].
+  ///
+  /// Defaults to 4.0.
+  ///
+  /// See also:
+  ///
+  ///  * [Material.elevation], which is used to implement this property.
+  final double openElevation;
+
+  /// Shape of the container while it is closed.
+  ///
+  /// When the container is opened it will transition from this shape to
+  /// [openShape]. When the container is closed, it will transition back to this
+  /// shape.
+  ///
+  /// Defaults to a [RoundedRectangleBorder] with a [Radius.circular] of 4.0.
+  ///
+  /// See also:
+  ///
+  ///  * [Material.shape], which is used to implement this property.
+  final ShapeBorder closedShape;
+
+  /// Shape of the container while it is open.
+  ///
+  /// When the container is opened it will transition from [closedShape] to
+  /// this shape. When the container is closed, it will transition from this
+  /// shape back to [closedShape].
+  ///
+  /// Defaults to a rectangular.
+  ///
+  /// See also:
+  ///
+  ///  * [Material.shape], which is used to implement this property.
+  final ShapeBorder openShape;
+
+  /// Called when the container was popped and has returned to the closed state.
+  ///
+  /// The return value from the popped screen is passed to this function as an
+  /// argument.
+  ///
+  /// If no value is returned via [Navigator.pop] or [OpenContainer.openBuilder.action],
+  /// `null` will be returned by default.
+  final ClosedCallback<T?>? onClosed;
+
+  /// Called to obtain the child for the container in the closed state.
+  ///
+  /// The [Widget] returned by this builder is faded out when the container
+  /// opens and at the same time the widget returned by [openBuilder] is faded
+  /// in while the container grows to fill the surrounding [Navigator].
+  ///
+  /// The `action` callback provided to the builder can be called to open the
+  /// container.
+  final CloseContainerBuilder closedBuilder;
+
+  /// Called to obtain the child for the container in the open state.
+  ///
+  /// The [Widget] returned by this builder is faded in when the container
+  /// opens and at the same time the widget returned by [closedBuilder] is
+  /// faded out while the container grows to fill the surrounding [Navigator].
+  ///
+  /// The `action` callback provided to the builder can be called to close the
+  /// container.
+  final OpenContainerBuilder<T> openBuilder;
+
+  /// Whether the entire closed container can be tapped to open it.
+  ///
+  /// Defaults to true.
+  ///
+  /// When this is set to false the container can only be opened by calling the
+  /// `action` callback that is provided to the [closedBuilder].
+  final bool tappable;
+
+  /// The time it will take to animate the container from its closed to its
+  /// open state and vice versa.
+  ///
+  /// Defaults to 300ms.
+  final Duration transitionDuration;
+
+  /// The type of fade transition that the container will use for its
+  /// incoming and outgoing widgets.
+  ///
+  /// Defaults to [ContainerTransitionType.fade].
+  final ContainerTransitionType transitionType;
+
+  /// The [useRootNavigator] argument is used to determine whether to push the
+  /// route for [openBuilder] to the Navigator furthest from or nearest to
+  /// the given context.
+  ///
+  /// By default, [useRootNavigator] is false and the route created will push
+  /// to the nearest navigator.
+  final bool useRootNavigator;
+
+  /// Provides additional data to the [openBuilder] route pushed by the Navigator.
+  final RouteSettings? routeSettings;
+
+  /// The [closedBuilder] will be clipped (or not) according to this option.
+  ///
+  /// Defaults to [Clip.antiAlias], and must not be null.
+  ///
+  /// See also:
+  ///
+  ///  * [Material.clipBehavior], which is used to implement this property.
+  final Clip clipBehavior;
+
+  @override
+  _OpenContainerState<T> createState() => _OpenContainerState<T>();
+}
+
+class _OpenContainerState<T> extends State<OpenContainer<T?>> {
+  // Key used in [_OpenContainerRoute] to hide the widget returned by
+  // [OpenContainer.openBuilder] in the source route while the container is
+  // opening/open. A copy of that widget is included in the
+  // [_OpenContainerRoute] where it fades out. To avoid issues with double
+  // shadows and transparency, we hide it in the source route.
+  final GlobalKey<_HideableState> _hideableKey = GlobalKey<_HideableState>();
+
+  // Key used to steal the state of the widget returned by
+  // [OpenContainer.openBuilder] from the source route and attach it to the
+  // same widget included in the [_OpenContainerRoute] where it fades out.
+  final GlobalKey _closedBuilderKey = GlobalKey();
+
+  Future<void> openContainer() async {
+    final Color middleColor =
+        widget.middleColor ?? Theme.of(context).canvasColor;
+    final T? data = await Navigator.of(
+      context,
+      rootNavigator: widget.useRootNavigator,
+    ).push(_OpenContainerRoute<T>(
+      closedColor: widget.closedColor,
+      openColor: widget.openColor,
+      middleColor: middleColor,
+      closedElevation: widget.closedElevation,
+      openElevation: widget.openElevation,
+      closedShape: widget.closedShape,
+      openShape: widget.openShape,
+      closedBuilder: widget.closedBuilder,
+      openBuilder: widget.openBuilder,
+      hideableKey: _hideableKey,
+      closedBuilderKey: _closedBuilderKey,
+      transitionDuration: widget.transitionDuration,
+      transitionType: widget.transitionType,
+      useRootNavigator: widget.useRootNavigator,
+      routeSettings: widget.routeSettings,
+    ));
+    if (widget.onClosed != null) {
+      widget.onClosed!(data);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return _Hideable(
+      key: _hideableKey,
+      child: GestureDetector(
+        onTap: widget.tappable ? openContainer : null,
+        child: Material(
+          clipBehavior: widget.clipBehavior,
+          color: widget.closedColor,
+          elevation: widget.closedElevation,
+          shape: widget.closedShape,
+          child: Builder(
+            key: _closedBuilderKey,
+            builder: (BuildContext context) {
+              return widget.closedBuilder(context, openContainer);
+            },
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// Controls the visibility of its child.
+///
+/// The child can be in one of three states:
+///
+///  * It is included in the tree and fully visible. (The `placeholderSize` is
+///    null and `isVisible` is true.)
+///  * It is included in the tree, but not visible; its size is maintained.
+///    (The `placeholderSize` is null and `isVisible` is false.)
+///  * It is not included in the tree. Instead a [SizedBox] of dimensions
+///    specified by `placeholderSize` is included in the tree. (The value of
+///    `isVisible` is ignored).
+class _Hideable extends StatefulWidget {
+  const _Hideable({
+    Key? key,
+    this.child,
+  }) : super(key: key);
+
+  final Widget? child;
+
+  @override
+  State<_Hideable> createState() => _HideableState();
+}
+
+class _HideableState extends State<_Hideable> {
+  /// When non-null the child is replaced by a [SizedBox] of the set size.
+  Size? get placeholderSize => _placeholderSize;
+  Size? _placeholderSize;
+  set placeholderSize(Size? value) {
+    if (_placeholderSize == value) {
+      return;
+    }
+    setState(() {
+      _placeholderSize = value;
+    });
+  }
+
+  /// When true the child is not visible, but will maintain its size.
+  ///
+  /// The value of this property is ignored when [placeholderSize] is non-null
+  /// (i.e. [isInTree] returns false).
+  bool get isVisible => _visible;
+  bool _visible = true;
+  set isVisible(bool value) {
+    if (_visible == value) {
+      return;
+    }
+    setState(() {
+      _visible = value;
+    });
+  }
+
+  /// Whether the child is currently included in the tree.
+  ///
+  /// When it is included, it may be visible or not according to [isVisible].
+  bool get isInTree => _placeholderSize == null;
+
+  @override
+  Widget build(BuildContext context) {
+    if (_placeholderSize != null) {
+      return SizedBox.fromSize(size: _placeholderSize);
+    }
+    return Opacity(
+      opacity: _visible ? 1.0 : 0.0,
+      child: widget.child,
+    );
+  }
+}
+
+class _OpenContainerRoute<T> extends ModalRoute<T> {
+  _OpenContainerRoute({
+    required this.closedColor,
+    required this.openColor,
+    required this.middleColor,
+    required double closedElevation,
+    required this.openElevation,
+    required ShapeBorder closedShape,
+    required this.openShape,
+    required this.closedBuilder,
+    required this.openBuilder,
+    required this.hideableKey,
+    required this.closedBuilderKey,
+    required this.transitionDuration,
+    required this.transitionType,
+    required this.useRootNavigator,
+    required RouteSettings? routeSettings,
+  })  : _elevationTween = Tween<double>(
+          begin: closedElevation,
+          end: openElevation,
+        ),
+        _shapeTween = ShapeBorderTween(
+          begin: closedShape,
+          end: openShape,
+        ),
+        _colorTween = _getColorTween(
+          transitionType: transitionType,
+          closedColor: closedColor,
+          openColor: openColor,
+          middleColor: middleColor,
+        ),
+        _closedOpacityTween = _getClosedOpacityTween(transitionType),
+        _openOpacityTween = _getOpenOpacityTween(transitionType),
+        super(settings: routeSettings);
+
+  static _FlippableTweenSequence<Color?> _getColorTween({
+    required ContainerTransitionType transitionType,
+    required Color closedColor,
+    required Color openColor,
+    required Color middleColor,
+  }) {
+    switch (transitionType) {
+      case ContainerTransitionType.fade:
+        return _FlippableTweenSequence<Color?>(
+          <TweenSequenceItem<Color?>>[
+            TweenSequenceItem<Color>(
+              tween: ConstantTween<Color>(closedColor),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<Color?>(
+              tween: ColorTween(begin: closedColor, end: openColor),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<Color>(
+              tween: ConstantTween<Color>(openColor),
+              weight: 3 / 5,
+            ),
+          ],
+        );
+      case ContainerTransitionType.fadeThrough:
+        return _FlippableTweenSequence<Color?>(
+          <TweenSequenceItem<Color?>>[
+            TweenSequenceItem<Color?>(
+              tween: ColorTween(begin: closedColor, end: middleColor),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<Color?>(
+              tween: ColorTween(begin: middleColor, end: openColor),
+              weight: 4 / 5,
+            ),
+          ],
+        );
+    }
+  }
+
+  static _FlippableTweenSequence<double> _getClosedOpacityTween(
+      ContainerTransitionType transitionType) {
+    switch (transitionType) {
+      case ContainerTransitionType.fade:
+        return _FlippableTweenSequence<double>(
+          <TweenSequenceItem<double>>[
+            TweenSequenceItem<double>(
+              tween: ConstantTween<double>(1.0),
+              weight: 1,
+            ),
+          ],
+        );
+      case ContainerTransitionType.fadeThrough:
+        return _FlippableTweenSequence<double>(
+          <TweenSequenceItem<double>>[
+            TweenSequenceItem<double>(
+              tween: Tween<double>(begin: 1.0, end: 0.0),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<double>(
+              tween: ConstantTween<double>(0.0),
+              weight: 4 / 5,
+            ),
+          ],
+        );
+    }
+  }
+
+  static _FlippableTweenSequence<double> _getOpenOpacityTween(
+      ContainerTransitionType transitionType) {
+    switch (transitionType) {
+      case ContainerTransitionType.fade:
+        return _FlippableTweenSequence<double>(
+          <TweenSequenceItem<double>>[
+            TweenSequenceItem<double>(
+              tween: ConstantTween<double>(0.0),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<double>(
+              tween: Tween<double>(begin: 0.0, end: 1.0),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<double>(
+              tween: ConstantTween<double>(1.0),
+              weight: 3 / 5,
+            ),
+          ],
+        );
+      case ContainerTransitionType.fadeThrough:
+        return _FlippableTweenSequence<double>(
+          <TweenSequenceItem<double>>[
+            TweenSequenceItem<double>(
+              tween: ConstantTween<double>(0.0),
+              weight: 1 / 5,
+            ),
+            TweenSequenceItem<double>(
+              tween: Tween<double>(begin: 0.0, end: 1.0),
+              weight: 4 / 5,
+            ),
+          ],
+        );
+    }
+  }
+
+  final Color closedColor;
+  final Color openColor;
+  final Color middleColor;
+  final double openElevation;
+  final ShapeBorder openShape;
+  final CloseContainerBuilder closedBuilder;
+  final OpenContainerBuilder<T> openBuilder;
+
+  // See [_OpenContainerState._hideableKey].
+  final GlobalKey<_HideableState> hideableKey;
+
+  // See [_OpenContainerState._closedBuilderKey].
+  final GlobalKey closedBuilderKey;
+
+  @override
+  final Duration transitionDuration;
+  final ContainerTransitionType transitionType;
+
+  final bool useRootNavigator;
+
+  final Tween<double> _elevationTween;
+  final ShapeBorderTween _shapeTween;
+  final _FlippableTweenSequence<double> _closedOpacityTween;
+  final _FlippableTweenSequence<double> _openOpacityTween;
+  final _FlippableTweenSequence<Color?> _colorTween;
+
+  static final TweenSequence<Color?> _scrimFadeInTween = TweenSequence<Color?>(
+    <TweenSequenceItem<Color?>>[
+      TweenSequenceItem<Color?>(
+        tween: ColorTween(begin: Colors.transparent, end: Colors.black54),
+        weight: 1 / 5,
+      ),
+      TweenSequenceItem<Color>(
+        tween: ConstantTween<Color>(Colors.black54),
+        weight: 4 / 5,
+      ),
+    ],
+  );
+  static final Tween<Color?> _scrimFadeOutTween = ColorTween(
+    begin: Colors.transparent,
+    end: Colors.black54,
+  );
+
+  // Key used for the widget returned by [OpenContainer.openBuilder] to keep
+  // its state when the shape of the widget tree is changed at the end of the
+  // animation to remove all the craft that was necessary to make the animation
+  // work.
+  final GlobalKey _openBuilderKey = GlobalKey();
+
+  // Defines the position and the size of the (opening) [OpenContainer] within
+  // the bounds of the enclosing [Navigator].
+  final RectTween _rectTween = RectTween();
+
+  AnimationStatus? _lastAnimationStatus;
+  AnimationStatus? _currentAnimationStatus;
+
+  @override
+  TickerFuture didPush() {
+    _takeMeasurements(navigatorContext: hideableKey.currentContext!);
+
+    animation!.addStatusListener((AnimationStatus status) {
+      _lastAnimationStatus = _currentAnimationStatus;
+      _currentAnimationStatus = status;
+      switch (status) {
+        case AnimationStatus.dismissed:
+          _toggleHideable(hide: false);
+          break;
+        case AnimationStatus.completed:
+          _toggleHideable(hide: true);
+          break;
+        case AnimationStatus.forward:
+        case AnimationStatus.reverse:
+          break;
+      }
+    });
+
+    return super.didPush();
+  }
+
+  @override
+  bool didPop(T? result) {
+    _takeMeasurements(
+      navigatorContext: subtreeContext!,
+      delayForSourceRoute: true,
+    );
+    return super.didPop(result);
+  }
+
+  @override
+  void dispose() {
+    if (hideableKey.currentState?.isVisible == false) {
+      // This route may be disposed without dismissing its animation if it is
+      // removed by the navigator.
+      SchedulerBinding.instance!
+          .addPostFrameCallback((Duration d) => _toggleHideable(hide: false));
+    }
+    super.dispose();
+  }
+
+  void _toggleHideable({required bool hide}) {
+    if (hideableKey.currentState != null) {
+      hideableKey.currentState!
+        ..placeholderSize = null
+        ..isVisible = !hide;
+    }
+  }
+
+  void _takeMeasurements({
+    required BuildContext navigatorContext,
+    bool delayForSourceRoute = false,
+  }) {
+    final RenderBox navigator = Navigator.of(
+      navigatorContext,
+      rootNavigator: useRootNavigator,
+    ).context.findRenderObject()! as RenderBox;
+    final Size navSize = _getSize(navigator);
+    _rectTween.end = Offset.zero & navSize;
+
+    void takeMeasurementsInSourceRoute([Duration? _]) {
+      if (!navigator.attached || hideableKey.currentContext == null) {
+        return;
+      }
+      _rectTween.begin = _getRect(hideableKey, navigator);
+      hideableKey.currentState!.placeholderSize = _rectTween.begin!.size;
+    }
+
+    if (delayForSourceRoute) {
+      SchedulerBinding.instance!
+          .addPostFrameCallback(takeMeasurementsInSourceRoute);
+    } else {
+      takeMeasurementsInSourceRoute();
+    }
+  }
+
+  Size _getSize(RenderBox render) {
+    assert(render.hasSize);
+    return render.size;
+  }
+
+  // Returns the bounds of the [RenderObject] identified by `key` in the
+  // coordinate system of `ancestor`.
+  Rect _getRect(GlobalKey key, RenderBox ancestor) {
+    assert(key.currentContext != null);
+    assert(ancestor.hasSize);
+    final RenderBox render =
+        key.currentContext!.findRenderObject()! as RenderBox;
+    assert(render.hasSize);
+    return MatrixUtils.transformRect(
+      render.getTransformTo(ancestor),
+      Offset.zero & render.size,
+    );
+  }
+
+  bool get _transitionWasInterrupted {
+    bool wasInProgress = false;
+    bool isInProgress = false;
+
+    switch (_currentAnimationStatus) {
+      case AnimationStatus.completed:
+      case AnimationStatus.dismissed:
+        isInProgress = false;
+        break;
+      case AnimationStatus.forward:
+      case AnimationStatus.reverse:
+        isInProgress = true;
+        break;
+      case null:
+        break;
+    }
+    switch (_lastAnimationStatus) {
+      case AnimationStatus.completed:
+      case AnimationStatus.dismissed:
+        wasInProgress = false;
+        break;
+      case AnimationStatus.forward:
+      case AnimationStatus.reverse:
+        wasInProgress = true;
+        break;
+      case null:
+        break;
+    }
+    return wasInProgress && isInProgress;
+  }
+
+  void closeContainer({T? returnValue}) {
+    Navigator.of(subtreeContext!).pop(returnValue);
+  }
+
+  @override
+  Widget buildPage(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+  ) {
+    return Align(
+      alignment: Alignment.topLeft,
+      child: AnimatedBuilder(
+        animation: animation,
+        builder: (BuildContext context, Widget? child) {
+          if (animation.isCompleted) {
+            return SizedBox.expand(
+              child: Material(
+                color: openColor,
+                elevation: openElevation,
+                shape: openShape,
+                child: Builder(
+                  key: _openBuilderKey,
+                  builder: (BuildContext context) {
+                    return openBuilder(context, closeContainer);
+                  },
+                ),
+              ),
+            );
+          }
+
+          final Animation<double> curvedAnimation = CurvedAnimation(
+            parent: animation,
+            curve: Curves.fastOutSlowIn,
+            reverseCurve:
+                _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
+          );
+          TweenSequence<Color?>? colorTween;
+          TweenSequence<double>? closedOpacityTween, openOpacityTween;
+          Animatable<Color?>? scrimTween;
+          switch (animation.status) {
+            case AnimationStatus.dismissed:
+            case AnimationStatus.forward:
+              closedOpacityTween = _closedOpacityTween;
+              openOpacityTween = _openOpacityTween;
+              colorTween = _colorTween;
+              scrimTween = _scrimFadeInTween;
+              break;
+            case AnimationStatus.reverse:
+              if (_transitionWasInterrupted) {
+                closedOpacityTween = _closedOpacityTween;
+                openOpacityTween = _openOpacityTween;
+                colorTween = _colorTween;
+                scrimTween = _scrimFadeInTween;
+                break;
+              }
+              closedOpacityTween = _closedOpacityTween.flipped;
+              openOpacityTween = _openOpacityTween.flipped;
+              colorTween = _colorTween.flipped;
+              scrimTween = _scrimFadeOutTween;
+              break;
+            case AnimationStatus.completed:
+              assert(false); // Unreachable.
+              break;
+          }
+          assert(colorTween != null);
+          assert(closedOpacityTween != null);
+          assert(openOpacityTween != null);
+          assert(scrimTween != null);
+
+          final Rect rect = _rectTween.evaluate(curvedAnimation)!;
+          return SizedBox.expand(
+            child: Container(
+              color: scrimTween!.evaluate(curvedAnimation),
+              child: Align(
+                alignment: Alignment.topLeft,
+                child: Transform.translate(
+                  offset: Offset(rect.left, rect.top),
+                  child: SizedBox(
+                    width: rect.width,
+                    height: rect.height,
+                    child: Material(
+                      clipBehavior: Clip.antiAlias,
+                      animationDuration: Duration.zero,
+                      color: colorTween!.evaluate(animation),
+                      shape: _shapeTween.evaluate(curvedAnimation),
+                      elevation: _elevationTween.evaluate(curvedAnimation),
+                      child: Stack(
+                        fit: StackFit.passthrough,
+                        children: <Widget>[
+                          // Closed child fading out.
+                          FittedBox(
+                            fit: BoxFit.fitWidth,
+                            alignment: Alignment.topLeft,
+                            child: SizedBox(
+                              width: _rectTween.begin!.width,
+                              height: _rectTween.begin!.height,
+                              child: (hideableKey.currentState?.isInTree ??
+                                      false)
+                                  ? null
+                                  : Opacity(
+                                      opacity: closedOpacityTween!
+                                          .evaluate(animation),
+                                      child: Builder(
+                                        key: closedBuilderKey,
+                                        builder: (BuildContext context) {
+                                          // Use dummy "open container" callback
+                                          // since we are in the process of opening.
+                                          return closedBuilder(context, () {});
+                                        },
+                                      ),
+                                    ),
+                            ),
+                          ),
+
+                          // Open child fading in.
+                          FittedBox(
+                            fit: BoxFit.fitWidth,
+                            alignment: Alignment.topLeft,
+                            child: SizedBox(
+                              width: _rectTween.end!.width,
+                              height: _rectTween.end!.height,
+                              child: Opacity(
+                                opacity: openOpacityTween!.evaluate(animation),
+                                child: Builder(
+                                  key: _openBuilderKey,
+                                  builder: (BuildContext context) {
+                                    return openBuilder(context, closeContainer);
+                                  },
+                                ),
+                              ),
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  bool get maintainState => true;
+
+  @override
+  Color? get barrierColor => null;
+
+  @override
+  bool get opaque => true;
+
+  @override
+  bool get barrierDismissible => false;
+
+  @override
+  String? get barrierLabel => null;
+}
+
+class _FlippableTweenSequence<T> extends TweenSequence<T> {
+  _FlippableTweenSequence(this._items) : super(_items);
+
+  final List<TweenSequenceItem<T>> _items;
+  _FlippableTweenSequence<T>? _flipped;
+
+  _FlippableTweenSequence<T>? get flipped {
+    if (_flipped == null) {
+      final List<TweenSequenceItem<T>> newItems = <TweenSequenceItem<T>>[];
+      for (int i = 0; i < _items.length; i++) {
+        newItems.add(TweenSequenceItem<T>(
+          tween: _items[i].tween,
+          weight: _items[_items.length - 1 - i].weight,
+        ));
+      }
+      _flipped = _FlippableTweenSequence<T>(newItems);
+    }
+    return _flipped;
+  }
+}
diff --git a/packages/animations/lib/src/page_transition_switcher.dart b/packages/animations/lib/src/page_transition_switcher.dart
new file mode 100644
index 0000000..ad00703
--- /dev/null
+++ b/packages/animations/lib/src/page_transition_switcher.dart
@@ -0,0 +1,438 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/animation.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+/// An internal representation of a child widget subtree that, now or in the past,
+/// was set on the [PageTransitionSwitcher.child] field and is now in the process of
+/// transitioning.
+///
+/// The internal representation includes fields that we don't want to expose to
+/// the public API (like the controllers).
+class _ChildEntry {
+  /// Creates a [_ChildEntry].
+  ///
+  /// The [primaryController], [secondaryController], [transition] and
+  /// [widgetChild] parameters must not be null.
+  _ChildEntry({
+    required this.primaryController,
+    required this.secondaryController,
+    required this.transition,
+    required this.widgetChild,
+  });
+
+  /// The animation controller for the child's transition.
+  final AnimationController primaryController;
+
+  /// The (curved) animation being used to drive the transition.
+  final AnimationController secondaryController;
+
+  /// The currently built transition for this child.
+  Widget transition;
+
+  /// The widget's child at the time this entry was created or updated.
+  /// Used to rebuild the transition if necessary.
+  Widget widgetChild;
+
+  /// Release the resources used by this object.
+  ///
+  /// The object is no longer usable after this method is called.
+  void dispose() {
+    primaryController.dispose();
+    secondaryController.dispose();
+  }
+
+  @override
+  String toString() {
+    return 'PageTransitionSwitcherEntry#${shortHash(this)}($widgetChild)';
+  }
+}
+
+/// Signature for builders used to generate custom layouts for
+/// [PageTransitionSwitcher].
+///
+/// The builder should return a widget which contains the given children, laid
+/// out as desired. It must not return null. The builder should be able to
+/// handle an empty list of `entries`.
+typedef PageTransitionSwitcherLayoutBuilder = Widget Function(
+  List<Widget> entries,
+);
+
+/// Signature for builders used to generate custom transitions for
+/// [PageTransitionSwitcher].
+///
+/// The function should return a widget which wraps the given `child`.
+///
+/// When a [PageTransitionSwitcher]'s `child` is replaced, the new child's
+/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is
+/// usually fixed at 0.0. At the same time, the old child's `secondaryAnimation`
+/// runs forward, and the value of its primaryAnimation is usually fixed at 1.0.
+///
+/// The widget returned by the [PageTransitionSwitcherTransitionBuilder] can
+/// incorporate both animations. It will use the primary animation to define how
+/// its child appears, and the secondary animation to define how its child
+/// disappears.
+typedef PageTransitionSwitcherTransitionBuilder = Widget Function(
+  Widget child,
+  Animation<double> primaryAnimation,
+  Animation<double> secondaryAnimation,
+);
+
+/// A widget that transitions from an old child to a new child whenever [child]
+/// changes using an animation specified by [transitionBuilder].
+///
+/// This is a variation of an [AnimatedSwitcher], but instead of using the
+/// same transition for enter and exit, two separate transitions can be
+/// specified, similar to how the enter and exit transitions of a [PageRoute]
+/// are defined.
+///
+/// When a new [child] is specified, the [transitionBuilder] is effectively
+/// applied twice, once to the old child and once to the new one. When
+/// [reverse] is false, the old child's `secondaryAnimation` runs forward, and
+/// the value of its `primaryAnimation` is usually fixed at 1.0. The new child's
+/// `primaryAnimation` runs forward and the value of its `secondaryAnimation` is
+/// usually fixed at 0.0. The widget returned by the [transitionBuilder] can
+/// incorporate both animations. It will use the primary animation to define how
+/// its child appears, and the secondary animation to define how its child
+/// disappears. This is similar to the transition associated with pushing a new
+/// [PageRoute] on top of another.
+///
+/// When [reverse] is true, the old child's `primaryAnimation` runs in reverse
+/// and the value of its `secondaryAnimation` is usually fixed at 0.0. The new
+/// child's `secondaryAnimation` runs in reverse and the value of its
+/// `primaryAnimation` is usually fixed at 1.0. This is similar to popping a
+/// [PageRoute] to reveal another [PageRoute] underneath it.
+///
+/// This process is the same as the one used by [PageRoute.buildTransitions].
+///
+/// The following example shows a [transitionBuilder] that slides out the
+/// old child to the right (driven by the `secondaryAnimation`) while the new
+/// child fades in (driven by the `primaryAnimation`):
+///
+/// ```dart
+/// transitionBuilder: (
+///   Widget child,
+///   Animation<double> primaryAnimation,
+///   Animation<double> secondaryAnimation,
+/// ) {
+///   return SlideTransition(
+///     position: Tween<Offset>(
+///       begin: Offset.zero,
+///       end: const Offset(1.5, 0.0),
+///     ).animate(secondaryAnimation),
+///     child: FadeTransition(
+///       opacity: Tween<double>(
+///         begin: 0.0,
+///         end: 1.0,
+///       ).animate(primaryAnimation),
+///       child: child,
+///     ),
+///   );
+/// },
+/// ```
+///
+/// If the children are swapped fast enough (i.e. before [duration] elapses),
+/// more than one old child can exist and be transitioning out while the
+/// newest one is transitioning in.
+///
+/// If the *new* child is the same widget type and key as the *old* child,
+/// but with different parameters, then [PageTransitionSwitcher] will *not* do a
+/// transition between them, since as far as the framework is concerned, they
+/// are the same widget and the existing widget can be updated with the new
+/// parameters. To force the transition to occur, set a [Key] on each child
+/// widget that you wish to be considered unique (typically a [ValueKey] on the
+/// widget data that distinguishes this child from the others). For example,
+/// changing the child from `SizedBox(width: 10)` to `SizedBox(width: 100)`
+/// would not trigger a transition but changing the child from
+/// `SizedBox(width: 10)` to `SizedBox(key: Key('foo'), width: 100)` would.
+/// Similarly, changing the child to `Container(width: 10)` would trigger a
+/// transition.
+///
+/// The same key can be used for a new child as was used for an already-outgoing
+/// child; the two will not be considered related. For example, if a progress
+/// indicator with key A is first shown, then an image with key B, then another
+/// progress indicator with key A again, all in rapid succession, then the old
+/// progress indicator and the image will be fading out while a new progress
+/// indicator is fading in.
+///
+/// PageTransitionSwitcher uses the [layoutBuilder] property to lay out the
+/// old and new child widgets. By default, [defaultLayoutBuilder] is used.
+/// See the documentation for [layoutBuilder] for suggestions on how to
+/// configure the layout of the incoming and outgoing child widgets if
+/// [defaultLayoutBuilder] is not your desired layout.
+class PageTransitionSwitcher extends StatefulWidget {
+  /// Creates a [PageTransitionSwitcher].
+  ///
+  /// The [duration], [reverse], and [transitionBuilder] parameters
+  /// must not be null.
+  const PageTransitionSwitcher({
+    Key? key,
+    this.duration = const Duration(milliseconds: 300),
+    this.reverse = false,
+    required this.transitionBuilder,
+    this.layoutBuilder = defaultLayoutBuilder,
+    this.child,
+  }) : super(key: key);
+
+  /// The current child widget to display.
+  ///
+  /// If there was an old child, it will be transitioned out using the
+  /// secondary animation of the [transitionBuilder], while the new child
+  /// transitions in using the primary animation of the [transitionBuilder].
+  ///
+  /// If there was no old child, then this child will transition in using
+  /// the primary animation of the [transitionBuilder].
+  ///
+  /// The child is considered to be "new" if it has a different type or [Key]
+  /// (see [Widget.canUpdate]).
+  final Widget? child;
+
+  /// The duration of the transition from the old [child] value to the new one.
+  ///
+  /// This duration is applied to the given [child] when that property is set to
+  /// a new child. Changing [duration] will not affect the durations of
+  /// transitions already in progress.
+  final Duration duration;
+
+  /// Indicates whether the new [child] will visually appear on top of or
+  /// underneath the old child.
+  ///
+  /// When this is false, the new child will transition in on top of the
+  /// old child while its primary animation and the secondary
+  /// animation of the old child are running forward. This is similar to
+  /// the transition associated with pushing a new [PageRoute] on top of
+  /// another.
+  ///
+  /// When this is true, the new child will transition in below the
+  /// old child while its secondary animation and the primary
+  /// animation of the old child are running in reverse. This is similar to
+  /// the transition associated with popping a [PageRoute] to reveal a new
+  /// [PageRoute] below it.
+  final bool reverse;
+
+  /// A function that wraps a new [child] with a primary and secondary animation
+  /// set define how the child appears and disappears.
+  ///
+  /// This is only called when a new [child] is set (not for each build), or
+  /// when a new [transitionBuilder] is set. If a new [transitionBuilder] is
+  /// set, then the transition is rebuilt for the current child and all old
+  /// children using the new [transitionBuilder]. The function must not return
+  /// null.
+  ///
+  /// The child provided to the transitionBuilder may be null.
+  final PageTransitionSwitcherTransitionBuilder transitionBuilder;
+
+  /// A function that wraps all of the children that are transitioning out, and
+  /// the [child] that's transitioning in, with a widget that lays all of them
+  /// out. This is called every time this widget is built. The function must not
+  /// return null.
+  ///
+  /// The default [PageTransitionSwitcherLayoutBuilder] used is
+  /// [defaultLayoutBuilder].
+  ///
+  /// The following example shows a [layoutBuilder] that places all entries in a
+  /// [Stack] that sizes itself to match the largest of the active entries.
+  /// All children are aligned on the top left corner of the [Stack].
+  ///
+  /// ```dart
+  /// PageTransitionSwitcher(
+  ///   duration: const Duration(milliseconds: 100),
+  ///   child: Container(color: Colors.red),
+  ///   layoutBuilder: (
+  ///     List<Widget> entries,
+  ///   ) {
+  ///     return Stack(
+  ///       children: entries,
+  ///       alignment: Alignment.topLeft,
+  ///     );
+  ///   },
+  /// ),
+  /// ```
+  /// See [PageTransitionSwitcherLayoutBuilder] for more information about
+  /// how a layout builder should function.
+  final PageTransitionSwitcherLayoutBuilder layoutBuilder;
+
+  /// The default layout builder for [PageTransitionSwitcher].
+  ///
+  /// This function is the default way for how the new and old child widgets are placed
+  /// during the transition between the two widgets. All children are placed in a
+  /// [Stack] that sizes itself to match the largest of the child or a previous child.
+  /// The children are centered on each other.
+  ///
+  /// See [PageTransitionSwitcherTransitionBuilder] for more information on the function
+  /// signature.
+  static Widget defaultLayoutBuilder(List<Widget> entries) {
+    return Stack(
+      children: entries,
+      alignment: Alignment.center,
+    );
+  }
+
+  @override
+  _PageTransitionSwitcherState createState() => _PageTransitionSwitcherState();
+}
+
+class _PageTransitionSwitcherState extends State<PageTransitionSwitcher>
+    with TickerProviderStateMixin {
+  final List<_ChildEntry> _activeEntries = <_ChildEntry>[];
+  _ChildEntry? _currentEntry;
+  int _childNumber = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    _addEntryForNewChild(shouldAnimate: false);
+  }
+
+  @override
+  void didUpdateWidget(PageTransitionSwitcher oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    // If the transition builder changed, then update all of the old
+    // transitions.
+    if (widget.transitionBuilder != oldWidget.transitionBuilder) {
+      _activeEntries.forEach(_updateTransitionForEntry);
+    }
+
+    final bool hasNewChild = widget.child != null;
+    final bool hasOldChild = _currentEntry != null;
+    if (hasNewChild != hasOldChild ||
+        hasNewChild &&
+            !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) {
+      // Child has changed, fade current entry out and add new entry.
+      _childNumber += 1;
+      _addEntryForNewChild(shouldAnimate: true);
+    } else if (_currentEntry != null) {
+      assert(hasOldChild && hasNewChild);
+      assert(Widget.canUpdate(widget.child!, _currentEntry!.widgetChild));
+      // Child has been updated. Make sure we update the child widget and
+      // transition in _currentEntry even though we're not going to start a new
+      // animation, but keep the key from the old transition so that we
+      // update the transition instead of replacing it.
+      _currentEntry!.widgetChild = widget.child!;
+      _updateTransitionForEntry(_currentEntry!); // uses entry.widgetChild
+    }
+  }
+
+  void _addEntryForNewChild({required bool shouldAnimate}) {
+    assert(shouldAnimate || _currentEntry == null);
+    if (_currentEntry != null) {
+      assert(shouldAnimate);
+      if (widget.reverse) {
+        _currentEntry!.primaryController.reverse();
+      } else {
+        _currentEntry!.secondaryController.forward();
+      }
+      _currentEntry = null;
+    }
+    if (widget.child == null) {
+      return;
+    }
+    final AnimationController primaryController = AnimationController(
+      duration: widget.duration,
+      vsync: this,
+    );
+    final AnimationController secondaryController = AnimationController(
+      duration: widget.duration,
+      vsync: this,
+    );
+    if (shouldAnimate) {
+      if (widget.reverse) {
+        primaryController.value = 1.0;
+        secondaryController.value = 1.0;
+        secondaryController.reverse();
+      } else {
+        primaryController.forward();
+      }
+    } else {
+      assert(_activeEntries.isEmpty);
+      primaryController.value = 1.0;
+    }
+    _currentEntry = _newEntry(
+      child: widget.child!,
+      primaryController: primaryController,
+      secondaryController: secondaryController,
+      builder: widget.transitionBuilder,
+    );
+    if (widget.reverse && _activeEntries.isNotEmpty) {
+      // Add below old child.
+      _activeEntries.insert(_activeEntries.length - 1, _currentEntry!);
+    } else {
+      // Add on top of old child.
+      _activeEntries.add(_currentEntry!);
+    }
+  }
+
+  _ChildEntry _newEntry({
+    required Widget child,
+    required PageTransitionSwitcherTransitionBuilder builder,
+    required AnimationController primaryController,
+    required AnimationController secondaryController,
+  }) {
+    final Widget transition = builder(
+      child,
+      primaryController,
+      secondaryController,
+    );
+    final _ChildEntry entry = _ChildEntry(
+      widgetChild: child,
+      transition: KeyedSubtree.wrap(
+        transition,
+        _childNumber,
+      ),
+      primaryController: primaryController,
+      secondaryController: secondaryController,
+    );
+    secondaryController.addStatusListener((AnimationStatus status) {
+      if (status == AnimationStatus.completed) {
+        assert(mounted);
+        assert(_activeEntries.contains(entry));
+        setState(() {
+          _activeEntries.remove(entry);
+          entry.dispose();
+        });
+      }
+    });
+    primaryController.addStatusListener((AnimationStatus status) {
+      if (status == AnimationStatus.dismissed) {
+        assert(mounted);
+        assert(_activeEntries.contains(entry));
+        setState(() {
+          _activeEntries.remove(entry);
+          entry.dispose();
+        });
+      }
+    });
+    return entry;
+  }
+
+  void _updateTransitionForEntry(_ChildEntry entry) {
+    final Widget transition = widget.transitionBuilder(
+      entry.widgetChild,
+      entry.primaryController,
+      entry.secondaryController,
+    );
+    entry.transition = KeyedSubtree(
+      key: entry.transition.key,
+      child: transition,
+    );
+  }
+
+  @override
+  void dispose() {
+    for (final _ChildEntry entry in _activeEntries) {
+      entry.dispose();
+    }
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.layoutBuilder(_activeEntries
+        .map<Widget>((_ChildEntry entry) => entry.transition)
+        .toList());
+  }
+}
diff --git a/packages/animations/lib/src/shared_axis_transition.dart b/packages/animations/lib/src/shared_axis_transition.dart
new file mode 100644
index 0000000..a8cf497
--- /dev/null
+++ b/packages/animations/lib/src/shared_axis_transition.dart
@@ -0,0 +1,483 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/animation.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+/// Determines which type of shared axis transition is used.
+enum SharedAxisTransitionType {
+  /// Creates a shared axis vertical (y-axis) page transition.
+  vertical,
+
+  /// Creates a shared axis horizontal (x-axis) page transition.
+  horizontal,
+
+  /// Creates a shared axis scaled (z-axis) page transition.
+  scaled,
+}
+
+/// Used by [PageTransitionsTheme] to define a page route transition animation
+/// in which outgoing and incoming elements share a fade transition.
+///
+/// The shared axis pattern provides the transition animation between UI elements
+/// that have a spatial or navigational relationship. For example,
+/// transitioning from one page of a sign up page to the next one.
+///
+/// The following example shows how the SharedAxisPageTransitionsBuilder can
+/// be used in a [PageTransitionsTheme] to change the default transitions
+/// of [MaterialPageRoute]s.
+///
+/// ```dart
+/// MaterialApp(
+///   theme: ThemeData(
+///     pageTransitionsTheme: PageTransitionsTheme(
+///       builders: {
+///         TargetPlatform.android: SharedAxisPageTransitionsBuilder(
+///           transitionType: SharedAxisTransitionType.horizontal,
+///         ),
+///         TargetPlatform.iOS: SharedAxisPageTransitionsBuilder(
+///           transitionType: SharedAxisTransitionType.horizontal,
+///         ),
+///       },
+///     ),
+///   ),
+///   routes: {
+///     '/': (BuildContext context) {
+///       return Container(
+///         color: Colors.red,
+///         child: Center(
+///           child: ElevatedButton(
+///             child: Text('Push route'),
+///             onPressed: () {
+///               Navigator.of(context).pushNamed('/a');
+///             },
+///           ),
+///         ),
+///       );
+///     },
+///     '/a' : (BuildContext context) {
+///       return Container(
+///         color: Colors.blue,
+///         child: Center(
+///           child: ElevatedButton(
+///             child: Text('Pop route'),
+///             onPressed: () {
+///               Navigator.of(context).pop();
+///             },
+///           ),
+///         ),
+///       );
+///     },
+///   },
+/// );
+/// ```
+class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder {
+  /// Construct a [SharedAxisPageTransitionsBuilder].
+  const SharedAxisPageTransitionsBuilder({
+    required this.transitionType,
+    this.fillColor,
+  });
+
+  /// Determines which [SharedAxisTransitionType] to build.
+  final SharedAxisTransitionType transitionType;
+
+  /// The color to use for the background color during the transition.
+  ///
+  /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+  final Color? fillColor;
+
+  @override
+  Widget buildTransitions<T>(
+    PageRoute<T>? route,
+    BuildContext? context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return SharedAxisTransition(
+      animation: animation,
+      secondaryAnimation: secondaryAnimation,
+      transitionType: transitionType,
+      fillColor: fillColor,
+      child: child,
+    );
+  }
+}
+
+/// Defines a transition in which outgoing and incoming elements share a fade
+/// transition.
+///
+/// The shared axis pattern provides the transition animation between UI elements
+/// that have a spatial or navigational relationship. For example,
+/// transitioning from one page of a sign up page to the next one.
+///
+/// Consider using [SharedAxisTransition] within a
+/// [PageTransitionsTheme] if you want to apply this kind of transition to
+/// [MaterialPageRoute] transitions within a Navigator (see
+/// [SharedAxisPageTransitionsBuilder] for example code).
+///
+/// This transition can also be used directly in a
+/// [PageTransitionSwitcher.transitionBuilder] to transition
+/// from one widget to another as seen in the following example:
+///
+/// ```dart
+/// int _selectedIndex = 0;
+///
+/// final List<Color> _colors = [Colors.white, Colors.red, Colors.yellow];
+///
+/// @override
+/// Widget build(BuildContext context) {
+///   return Scaffold(
+///     appBar: AppBar(
+///       title: const Text('Page Transition Example'),
+///     ),
+///     body: PageTransitionSwitcher(
+///       // reverse: true, // uncomment to see transition in reverse
+///       transitionBuilder: (
+///         Widget child,
+///         Animation<double> primaryAnimation,
+///         Animation<double> secondaryAnimation,
+///       ) {
+///         return SharedAxisTransition(
+///           animation: primaryAnimation,
+///           secondaryAnimation: secondaryAnimation,
+///           transitionType: SharedAxisTransitionType.horizontal,
+///           child: child,
+///         );
+///       },
+///       child: Container(
+///         key: ValueKey<int>(_selectedIndex),
+///         color: _colors[_selectedIndex],
+///         child: Center(
+///           child: FlutterLogo(size: 300),
+///         )
+///       ),
+///     ),
+///     bottomNavigationBar: BottomNavigationBar(
+///       items: const <BottomNavigationBarItem>[
+///         BottomNavigationBarItem(
+///           icon: Icon(Icons.home),
+///           title: Text('White'),
+///         ),
+///         BottomNavigationBarItem(
+///           icon: Icon(Icons.business),
+///           title: Text('Red'),
+///         ),
+///         BottomNavigationBarItem(
+///           icon: Icon(Icons.school),
+///           title: Text('Yellow'),
+///         ),
+///       ],
+///       currentIndex: _selectedIndex,
+///       onTap: (int index) {
+///         setState(() {
+///           _selectedIndex = index;
+///         });
+///       },
+///     ),
+///   );
+/// }
+/// ```
+class SharedAxisTransition extends StatelessWidget {
+  /// Creates a [SharedAxisTransition].
+  ///
+  /// The [animation] and [secondaryAnimation] argument are required and must
+  /// not be null.
+  const SharedAxisTransition({
+    Key? key,
+    required this.animation,
+    required this.secondaryAnimation,
+    required this.transitionType,
+    this.fillColor,
+    this.child,
+  }) : super(key: key);
+
+  /// The animation that drives the [child]'s entrance and exit.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.animate], which is the value given to this property
+  ///    when it is used as a page transition.
+  final Animation<double> animation;
+
+  /// The animation that transitions [child] when new content is pushed on top
+  /// of it.
+  ///
+  /// See also:
+  ///
+  ///  * [TransitionRoute.secondaryAnimation], which is the value given to this
+  ///    property when the it is used as a page transition.
+  final Animation<double> secondaryAnimation;
+
+  /// Determines which type of shared axis transition is used.
+  ///
+  /// See also:
+  ///
+  ///  * [SharedAxisTransitionType], which defines and describes all shared
+  ///    axis transition types.
+  final SharedAxisTransitionType transitionType;
+
+  /// The color to use for the background color during the transition.
+  ///
+  /// This defaults to the [Theme]'s [ThemeData.canvasColor].
+  final Color? fillColor;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// This widget will transition in and out as driven by [animation] and
+  /// [secondaryAnimation].
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    final Color color = fillColor ?? Theme.of(context).canvasColor;
+    return DualTransitionBuilder(
+      animation: animation,
+      forwardBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return _EnterTransition(
+          animation: animation,
+          transitionType: transitionType,
+          child: child,
+        );
+      },
+      reverseBuilder: (
+        BuildContext context,
+        Animation<double> animation,
+        Widget? child,
+      ) {
+        return _ExitTransition(
+          animation: animation,
+          transitionType: transitionType,
+          reverse: true,
+          fillColor: color,
+          child: child,
+        );
+      },
+      child: DualTransitionBuilder(
+        animation: ReverseAnimation(secondaryAnimation),
+        forwardBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return _EnterTransition(
+            animation: animation,
+            transitionType: transitionType,
+            reverse: true,
+            child: child,
+          );
+        },
+        reverseBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return _ExitTransition(
+            animation: animation,
+            transitionType: transitionType,
+            fillColor: color,
+            child: child,
+          );
+        },
+        child: child,
+      ),
+    );
+  }
+}
+
+class _EnterTransition extends StatelessWidget {
+  const _EnterTransition({
+    required this.animation,
+    required this.transitionType,
+    this.reverse = false,
+    this.child,
+  });
+
+  final Animation<double> animation;
+  final SharedAxisTransitionType transitionType;
+  final Widget? child;
+  final bool reverse;
+
+  static final Animatable<double> _fadeInTransition = CurveTween(
+    curve: decelerateEasing,
+  ).chain(CurveTween(curve: const Interval(0.3, 1.0)));
+
+  static final Animatable<double> _scaleDownTransition = Tween<double>(
+    begin: 1.10,
+    end: 1.00,
+  ).chain(CurveTween(curve: standardEasing));
+
+  static final Animatable<double> _scaleUpTransition = Tween<double>(
+    begin: 0.80,
+    end: 1.00,
+  ).chain(CurveTween(curve: standardEasing));
+
+  @override
+  Widget build(BuildContext context) {
+    switch (transitionType) {
+      case SharedAxisTransitionType.horizontal:
+        final Animatable<Offset> slideInTransition = Tween<Offset>(
+          begin: Offset(!reverse ? 30.0 : -30.0, 0.0),
+          end: Offset.zero,
+        ).chain(CurveTween(curve: standardEasing));
+
+        return FadeTransition(
+          opacity: _fadeInTransition.animate(animation),
+          child: AnimatedBuilder(
+            animation: animation,
+            builder: (BuildContext context, Widget? child) {
+              return Transform.translate(
+                offset: slideInTransition.evaluate(animation),
+                child: child,
+              );
+            },
+            child: child,
+          ),
+        );
+      case SharedAxisTransitionType.vertical:
+        final Animatable<Offset> slideInTransition = Tween<Offset>(
+          begin: Offset(0.0, !reverse ? 30.0 : -30.0),
+          end: Offset.zero,
+        ).chain(CurveTween(curve: standardEasing));
+
+        return FadeTransition(
+          opacity: _fadeInTransition.animate(animation),
+          child: AnimatedBuilder(
+            animation: animation,
+            builder: (BuildContext context, Widget? child) {
+              return Transform.translate(
+                offset: slideInTransition.evaluate(animation),
+                child: child,
+              );
+            },
+            child: child,
+          ),
+        );
+      case SharedAxisTransitionType.scaled:
+        return FadeTransition(
+          opacity: _fadeInTransition.animate(animation),
+          child: ScaleTransition(
+            scale: (!reverse ? _scaleUpTransition : _scaleDownTransition)
+                .animate(animation),
+            child: child,
+          ),
+        );
+    }
+  }
+}
+
+class _ExitTransition extends StatelessWidget {
+  const _ExitTransition({
+    required this.animation,
+    required this.transitionType,
+    this.reverse = false,
+    required this.fillColor,
+    this.child,
+  });
+
+  final Animation<double> animation;
+  final SharedAxisTransitionType transitionType;
+  final bool reverse;
+  final Color fillColor;
+  final Widget? child;
+
+  static final Animatable<double> _fadeOutTransition = _FlippedCurveTween(
+    curve: accelerateEasing,
+  ).chain(CurveTween(curve: const Interval(0.0, 0.3)));
+
+  static final Animatable<double> _scaleUpTransition = Tween<double>(
+    begin: 1.00,
+    end: 1.10,
+  ).chain(CurveTween(curve: standardEasing));
+
+  static final Animatable<double> _scaleDownTransition = Tween<double>(
+    begin: 1.00,
+    end: 0.80,
+  ).chain(CurveTween(curve: standardEasing));
+
+  @override
+  Widget build(BuildContext context) {
+    switch (transitionType) {
+      case SharedAxisTransitionType.horizontal:
+        final Animatable<Offset> slideOutTransition = Tween<Offset>(
+          begin: Offset.zero,
+          end: Offset(!reverse ? -30.0 : 30.0, 0.0),
+        ).chain(CurveTween(curve: standardEasing));
+
+        return FadeTransition(
+          opacity: _fadeOutTransition.animate(animation),
+          child: Container(
+            color: fillColor,
+            child: AnimatedBuilder(
+              animation: animation,
+              builder: (BuildContext context, Widget? child) {
+                return Transform.translate(
+                  offset: slideOutTransition.evaluate(animation),
+                  child: child,
+                );
+              },
+              child: child,
+            ),
+          ),
+        );
+      case SharedAxisTransitionType.vertical:
+        final Animatable<Offset> slideOutTransition = Tween<Offset>(
+          begin: Offset.zero,
+          end: Offset(0.0, !reverse ? -30.0 : 30.0),
+        ).chain(CurveTween(curve: standardEasing));
+
+        return FadeTransition(
+          opacity: _fadeOutTransition.animate(animation),
+          child: Container(
+            color: fillColor,
+            child: AnimatedBuilder(
+              animation: animation,
+              builder: (BuildContext context, Widget? child) {
+                return Transform.translate(
+                  offset: slideOutTransition.evaluate(animation),
+                  child: child,
+                );
+              },
+              child: child,
+            ),
+          ),
+        );
+      case SharedAxisTransitionType.scaled:
+        return FadeTransition(
+          opacity: _fadeOutTransition.animate(animation),
+          child: Container(
+            color: fillColor,
+            child: ScaleTransition(
+              scale: (!reverse ? _scaleUpTransition : _scaleDownTransition)
+                  .animate(animation),
+              child: child,
+            ),
+          ),
+        );
+    }
+  }
+}
+
+/// Enables creating a flipped [CurveTween].
+///
+/// This creates a [CurveTween] that evaluates to a result that flips the
+/// tween vertically.
+///
+/// This tween sequence assumes that the evaluated result has to be a double
+/// between 0.0 and 1.0.
+class _FlippedCurveTween extends CurveTween {
+  /// Creates a vertically flipped [CurveTween].
+  _FlippedCurveTween({
+    required Curve curve,
+  }) : super(curve: curve);
+
+  @override
+  double transform(double t) => 1.0 - super.transform(t);
+}
diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml
new file mode 100644
index 0000000..81150b1
--- /dev/null
+++ b/packages/animations/pubspec.yaml
@@ -0,0 +1,16 @@
+name: animations
+description: Fancy pre-built animations that can easily be integrated into any Flutter application.
+version: 2.0.0
+homepage: https://github.com/flutter/packages/tree/master/packages/animations
+
+environment:
+  sdk: '>=2.12.0-259.9.beta <3.0.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  vector_math: ^2.1.0
diff --git a/packages/animations/test/dual_transition_builder_test.dart b/packages/animations/test/dual_transition_builder_test.dart
new file mode 100644
index 0000000..09a80af
--- /dev/null
+++ b/packages/animations/test/dual_transition_builder_test.dart
@@ -0,0 +1,298 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  testWidgets('runs animations', (WidgetTester tester) async {
+    final AnimationController controller = AnimationController(
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+
+    await tester.pumpWidget(Center(
+      child: DualTransitionBuilder(
+        animation: controller,
+        forwardBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return ScaleTransition(
+            scale: animation,
+            child: child,
+          );
+        },
+        reverseBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return FadeTransition(
+            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+            child: child,
+          );
+        },
+        child: Container(
+          color: Colors.green,
+          height: 100,
+          width: 100,
+        ),
+      ),
+    ));
+    expect(_getScale(tester), 0.0);
+    expect(_getOpacity(tester), 1.0);
+
+    controller.forward();
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 0.5);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pumpAndSettle();
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 1.0);
+
+    controller.reverse();
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.5);
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.0);
+
+    await tester.pumpAndSettle();
+    expect(_getScale(tester), 0.0);
+    expect(_getOpacity(tester), 1.0);
+  });
+
+  testWidgets('keeps state', (WidgetTester tester) async {
+    final AnimationController controller = AnimationController(
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+
+    await tester.pumpWidget(Directionality(
+      textDirection: TextDirection.ltr,
+      child: Center(
+        child: DualTransitionBuilder(
+          animation: controller,
+          forwardBuilder: (
+            BuildContext context,
+            Animation<double> animation,
+            Widget? child,
+          ) {
+            return ScaleTransition(
+              scale: animation,
+              child: child,
+            );
+          },
+          reverseBuilder: (
+            BuildContext context,
+            Animation<double> animation,
+            Widget? child,
+          ) {
+            return FadeTransition(
+              opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+              child: child,
+            );
+          },
+          child: const _StatefulTestWidget(name: 'Foo'),
+        ),
+      ),
+    ));
+    final State<StatefulWidget> state =
+        tester.state(find.byType(_StatefulTestWidget));
+    expect(state, isNotNull);
+
+    controller.forward();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    controller.reverse();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+  });
+
+  testWidgets('does not jump when interrupted - forward',
+      (WidgetTester tester) async {
+    final AnimationController controller = AnimationController(
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+    await tester.pumpWidget(Center(
+      child: DualTransitionBuilder(
+        animation: controller,
+        forwardBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return ScaleTransition(
+            scale: animation,
+            child: child,
+          );
+        },
+        reverseBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return FadeTransition(
+            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+            child: child,
+          );
+        },
+        child: Container(
+          color: Colors.green,
+          height: 100,
+          width: 100,
+        ),
+      ),
+    ));
+    expect(_getScale(tester), 0.0);
+    expect(_getOpacity(tester), 1.0);
+
+    controller.forward();
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 0.5);
+    expect(_getOpacity(tester), 1.0);
+
+    controller.reverse();
+    expect(_getScale(tester), 0.5);
+    expect(_getOpacity(tester), 1.0);
+    await tester.pump();
+    expect(_getScale(tester), 0.5);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(_getScale(tester), 0.25);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(_getScale(tester), 0.0);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pumpAndSettle();
+    expect(_getScale(tester), 0.0);
+    expect(_getOpacity(tester), 1.0);
+  });
+
+  testWidgets('does not jump when interrupted - reverse',
+      (WidgetTester tester) async {
+    final AnimationController controller = AnimationController(
+      value: 1.0,
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+    await tester.pumpWidget(Center(
+      child: DualTransitionBuilder(
+        animation: controller,
+        forwardBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return ScaleTransition(
+            scale: animation,
+            child: child,
+          );
+        },
+        reverseBuilder: (
+          BuildContext context,
+          Animation<double> animation,
+          Widget? child,
+        ) {
+          return FadeTransition(
+            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
+            child: child,
+          );
+        },
+        child: Container(
+          color: Colors.green,
+          height: 100,
+          width: 100,
+        ),
+      ),
+    ));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 1.0);
+
+    controller.reverse();
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.5);
+
+    controller.forward();
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.5);
+    await tester.pump();
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.5);
+
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 0.75);
+
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 1.0);
+
+    await tester.pumpAndSettle();
+    expect(_getScale(tester), 1.0);
+    expect(_getOpacity(tester), 1.0);
+  });
+}
+
+double _getScale(WidgetTester tester) {
+  final ScaleTransition scale = tester.widget(find.byType(ScaleTransition));
+  return scale.scale.value;
+}
+
+double _getOpacity(WidgetTester tester) {
+  final FadeTransition scale = tester.widget(find.byType(FadeTransition));
+  return scale.opacity.value;
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+  const _StatefulTestWidget({Key? key, required this.name}) : super(key: key);
+
+  final String name;
+
+  @override
+  State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+  @override
+  Widget build(BuildContext context) {
+    return Text(widget.name);
+  }
+}
diff --git a/packages/animations/test/fade_scale_transition_test.dart b/packages/animations/test/fade_scale_transition_test.dart
new file mode 100644
index 0000000..f87d182
--- /dev/null
+++ b/packages/animations/test/fade_scale_transition_test.dart
@@ -0,0 +1,501 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:animations/src/fade_scale_transition.dart';
+import 'package:animations/src/modal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  testWidgets(
+    'FadeScaleTransitionConfiguration builds a new route',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: const FadeScaleTransitionConfiguration(),
+                      builder: (BuildContext context) {
+                        return const _FlutterLogoModal();
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+    },
+  );
+
+  testWidgets(
+    'FadeScaleTransitionConfiguration runs forward',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: const FadeScaleTransitionConfiguration(),
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+      // Opacity duration: First 30% of 150ms, linear transition
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      double topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      expect(topScale, 0.80);
+
+      // 3/10 * 150ms = 45ms (total opacity animation duration)
+      // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity
+      // animation
+      await tester.pump(const Duration(milliseconds: 23));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // End of opacity animation
+      await tester.pump(const Duration(milliseconds: 22));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // 100ms into the animation
+      await tester.pump(const Duration(milliseconds: 55));
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // Get to the end of the animation
+      await tester.pump(const Duration(milliseconds: 50));
+      topScale = _getScale(key, tester);
+      expect(topScale, 1.0);
+
+      await tester.pump();
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+    },
+  );
+
+  testWidgets(
+    'FadeScaleTransitionConfiguration runs forward',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: const FadeScaleTransitionConfiguration(),
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+      // Show the incoming modal and let it animate in fully.
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+
+      // Tap on modal barrier to start reverse animation.
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+
+      // Opacity duration: Linear transition throughout 75ms
+      // No scale animations on exit transition.
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      double topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.66, 0.05));
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.33, 0.05));
+      expect(topScale, 1.0);
+
+      // End of opacity animation
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'FadeScaleTransitionConfiguration does not jump when interrupted',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: const FadeScaleTransitionConfiguration(),
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+      // Opacity duration: First 30% of 150ms, linear transition
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      double topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      expect(topScale, 0.80);
+
+      // 3/10 * 150ms = 45ms (total opacity animation duration)
+      // End of opacity animation
+      await tester.pump(const Duration(milliseconds: 45));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // 100ms into the animation
+      await tester.pump(const Duration(milliseconds: 55));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // Start the reverse transition by interrupting the forwards
+      // transition.
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+      // Opacity and scale values should remain the same after
+      // the reverse animation starts.
+      expect(_getOpacity(key, tester), topFadeTransitionOpacity);
+      expect(_getScale(key, tester), topScale);
+
+      // Should animate in reverse with 2/3 * 75ms = 50ms
+      // using the enter transition's animation pattern
+      // instead of the exit animation pattern.
+
+      // Calculation for the time when the linear fade
+      // transition should start if running backwards:
+      // 3/10 * 75ms = 22.5ms
+      // To get the 22.5ms timestamp, run backwards for:
+      // 50ms - 22.5ms = ~27.5ms
+      await tester.pump(const Duration(milliseconds: 27));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // Halfway through fade animation
+      await tester.pump(const Duration(milliseconds: 12));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // Complete the rest of the animation
+      await tester.pump(const Duration(milliseconds: 11));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, 0.8);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'State is not lost when transitioning',
+    (WidgetTester tester) async {
+      final GlobalKey bottomKey = GlobalKey();
+      final GlobalKey topKey = GlobalKey();
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: Column(
+                  children: <Widget>[
+                    ElevatedButton(
+                      onPressed: () {
+                        showModal<void>(
+                          context: context,
+                          configuration:
+                              const FadeScaleTransitionConfiguration(),
+                          builder: (BuildContext context) {
+                            return _FlutterLogoModal(
+                              key: topKey,
+                              name: 'top route',
+                            );
+                          },
+                        );
+                      },
+                      child: const Icon(Icons.add),
+                    ),
+                    _FlutterLogoModal(
+                      key: bottomKey,
+                      name: 'bottom route',
+                    ),
+                  ],
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // The bottom route's state should already exist.
+      final _FlutterLogoModalState bottomState = tester.state(
+        find.byKey(bottomKey),
+      );
+      expect(bottomState.widget.name, 'bottom route');
+
+      // Start the enter transition of the modal route.
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+      await tester.pump();
+
+      // The bottom route's state should be retained at the start of the
+      // transition.
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      // The top route's state should be created.
+      final _FlutterLogoModalState topState = tester.state(
+        find.byKey(topKey),
+      );
+      expect(topState.widget.name, 'top route');
+
+      // Halfway point of forwards animation.
+      await tester.pump(const Duration(milliseconds: 75));
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // End the transition and see if top and bottom routes'
+      // states persist.
+      await tester.pumpAndSettle();
+      expect(
+        tester.state(find.byKey(
+          bottomKey,
+          skipOffstage: false,
+        )),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // Start the reverse animation. Both top and bottom
+      // routes' states should persist.
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // Halfway point of the exit transition.
+      await tester.pump(const Duration(milliseconds: 38));
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // End the exit transition. The bottom route's state should
+      // persist, whereas the top route's state should no longer
+      // be present.
+      await tester.pumpAndSettle();
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(find.byKey(topKey), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'should preserve state',
+    (WidgetTester tester) async {
+      final AnimationController controller = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Center(
+              child: FadeScaleTransition(
+                animation: controller,
+                child: const _FlutterLogoModal(),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      final State<StatefulWidget> state = tester.state(
+        find.byType(_FlutterLogoModal),
+      );
+      expect(state, isNotNull);
+
+      controller.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+
+      controller.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+
+      controller.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_FlutterLogoModal))));
+    },
+  );
+}
+
+double _getOpacity(GlobalKey key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(key),
+    matching: find.byType(FadeTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final FadeTransition transition = widget as FadeTransition;
+    return a * transition.opacity.value;
+  });
+}
+
+double _getScale(GlobalKey key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(key),
+    matching: find.byType(ScaleTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final ScaleTransition transition = widget as ScaleTransition;
+    return a * transition.scale.value;
+  });
+}
+
+class _FlutterLogoModal extends StatefulWidget {
+  const _FlutterLogoModal({
+    Key? key,
+    this.name,
+  }) : super(key: key);
+
+  final String? name;
+
+  @override
+  _FlutterLogoModalState createState() => _FlutterLogoModalState();
+}
+
+class _FlutterLogoModalState extends State<_FlutterLogoModal> {
+  @override
+  Widget build(BuildContext context) {
+    return const Center(
+      child: SizedBox(
+        width: 250,
+        height: 250,
+        child: Material(
+          child: Center(
+            child: FlutterLogo(size: 250),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart
new file mode 100644
index 0000000..ac0ba19
--- /dev/null
+++ b/packages/animations/test/fade_through_transition_test.dart
@@ -0,0 +1,507 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:animations/src/fade_through_transition.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  testWidgets(
+      'FadeThroughPageTransitionsBuilder builds a FadeThroughTransition',
+      (WidgetTester tester) async {
+    final AnimationController animation = AnimationController(
+      vsync: const TestVSync(),
+    );
+    final AnimationController secondaryAnimation = AnimationController(
+      vsync: const TestVSync(),
+    );
+
+    await tester.pumpWidget(
+        const FadeThroughPageTransitionsBuilder().buildTransitions<void>(
+      null,
+      null,
+      animation,
+      secondaryAnimation,
+      const Placeholder(),
+    ));
+
+    expect(find.byType(FadeThroughTransition), findsOneWidget);
+  });
+
+  testWidgets('FadeThroughTransition runs forward',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+    const String bottomRoute = '/';
+    const String topRoute = '/a';
+
+    await tester.pumpWidget(
+      _TestWidget(
+        navigatorKey: navigator,
+      ),
+    );
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 1.0);
+    expect(find.text(topRoute), findsNothing);
+
+    navigator.currentState!.pushNamed(topRoute);
+    await tester.pump();
+    await tester.pump();
+
+    // Bottom route is full size and fully visible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 1.0);
+    // top route is at 95% of full size and not visible yet.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 0.92);
+    expect(_getOpacity(topRoute, tester), 0.0);
+
+    // Jump to half-way through the fade out (total duration is 300ms, 6/12th of
+    // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms.
+    await tester.pump(const Duration(milliseconds: 45));
+    // Bottom route is fading out.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    final double bottomOpacity = _getOpacity(bottomRoute, tester);
+    expect(bottomOpacity, lessThan(1.0));
+    expect(bottomOpacity, greaterThan(0.0));
+    // Top route is still invisible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 0.92);
+    expect(_getOpacity(topRoute, tester), 0.0);
+
+    // Let's jump to the end of the fade-out.
+    await tester.pump(const Duration(milliseconds: 45));
+    // Bottom route is completely faded out.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+    // Top route is still invisible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 0.92);
+    expect(_getOpacity(topRoute, tester), 0.0);
+
+    // Let's jump to the middle of the fade-in.
+    await tester.pump(const Duration(milliseconds: 105));
+    // Bottom route is not visible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+    // Top route is fading/scaling in.
+    expect(find.text(topRoute), findsOneWidget);
+    final double topScale = _getScale(topRoute, tester);
+    final double topOpacity = _getOpacity(topRoute, tester);
+    expect(topScale, greaterThan(0.92));
+    expect(topScale, lessThan(1.0));
+    expect(topOpacity, greaterThan(0.0));
+    expect(topOpacity, lessThan(1.0));
+
+    // Let's jump to the end of the animation.
+    await tester.pump(const Duration(milliseconds: 105));
+    // Bottom route is not visible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+    // Top route fully scaled in and visible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 1.0);
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text(bottomRoute), findsNothing);
+    expect(find.text(topRoute), findsOneWidget);
+  });
+
+  testWidgets('FadeThroughTransition runs backwards',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+    const String bottomRoute = '/';
+    const String topRoute = '/a';
+
+    await tester.pumpWidget(
+      _TestWidget(
+        navigatorKey: navigator,
+      ),
+    );
+    navigator.currentState!.pushNamed('/a');
+    await tester.pumpAndSettle();
+
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 1.0);
+    expect(find.text(bottomRoute), findsNothing);
+
+    navigator.currentState!.pop();
+    await tester.pump();
+
+    // Top route is full size and fully visible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 1.0);
+    // Bottom route is at 95% of full size and not visible yet.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 0.92);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+
+    // Jump to half-way through the fade out (total duration is 300ms, 6/12th of
+    // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms.
+    await tester.pump(const Duration(milliseconds: 45));
+    // Bottom route is fading out.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    final double topOpacity = _getOpacity(topRoute, tester);
+    expect(topOpacity, lessThan(1.0));
+    expect(topOpacity, greaterThan(0.0));
+    // Top route is still invisible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 0.92);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+
+    // Let's jump to the end of the fade-out.
+    await tester.pump(const Duration(milliseconds: 45));
+    // Bottom route is completely faded out.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 0.0);
+    // Top route is still invisible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(
+      _getScale(bottomRoute, tester),
+      moreOrLessEquals(0.92, epsilon: 0.005),
+    );
+    expect(
+      _getOpacity(bottomRoute, tester),
+      moreOrLessEquals(0.0, epsilon: 0.005),
+    );
+
+    // Let's jump to the middle of the fade-in.
+    await tester.pump(const Duration(milliseconds: 105));
+    // Bottom route is not visible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 0.0);
+    // Top route is fading/scaling in.
+    expect(find.text(bottomRoute), findsOneWidget);
+    final double bottomScale = _getScale(bottomRoute, tester);
+    final double bottomOpacity = _getOpacity(bottomRoute, tester);
+    expect(bottomScale, greaterThan(0.96));
+    expect(bottomScale, lessThan(1.0));
+    expect(bottomOpacity, greaterThan(0.1));
+    expect(bottomOpacity, lessThan(1.0));
+
+    // Let's jump to the end of the animation.
+    await tester.pump(const Duration(milliseconds: 105));
+    // Bottom route is not visible.
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 1.0);
+    expect(_getOpacity(topRoute, tester), 0.0);
+    // Top route fully scaled in and visible.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 1.0);
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text(topRoute), findsNothing);
+    expect(find.text(bottomRoute), findsOneWidget);
+  });
+
+  testWidgets('FadeThroughTransition does not jump when interrupted',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+    const String bottomRoute = '/';
+    const String topRoute = '/a';
+
+    await tester.pumpWidget(
+      _TestWidget(
+        navigatorKey: navigator,
+      ),
+    );
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(find.text(topRoute), findsNothing);
+
+    navigator.currentState!.pushNamed(topRoute);
+    await tester.pump();
+
+    // Jump to halfway point of transition.
+    await tester.pump(const Duration(milliseconds: 150));
+    // Bottom route is fully faded out.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+    // Top route is fading/scaling in.
+    expect(find.text(topRoute), findsOneWidget);
+    final double topScale = _getScale(topRoute, tester);
+    final double topOpacity = _getOpacity(topRoute, tester);
+    expect(topScale, greaterThan(0.92));
+    expect(topScale, lessThan(1.0));
+    expect(topOpacity, greaterThan(0.0));
+    expect(topOpacity, lessThan(1.0));
+
+    // Interrupt the transition with a pop.
+    navigator.currentState!.pop();
+    await tester.pump();
+    // Noting changed.
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 0.0);
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), topScale);
+    expect(_getOpacity(topRoute, tester), topOpacity);
+
+    // Jump to the halfway point.
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    final double bottomOpacity = _getOpacity(bottomRoute, tester);
+    expect(bottomOpacity, greaterThan(0.0));
+    expect(bottomOpacity, lessThan(1.0));
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), lessThan(topScale));
+    expect(_getOpacity(topRoute, tester), lessThan(topOpacity));
+
+    // Jump to the end.
+    await tester.pump(const Duration(milliseconds: 75));
+    expect(find.text(bottomRoute), findsOneWidget);
+    expect(_getScale(bottomRoute, tester), 1.0);
+    expect(_getOpacity(bottomRoute, tester), 1.0);
+    expect(find.text(topRoute), findsOneWidget);
+    expect(_getScale(topRoute, tester), 0.92);
+    expect(_getOpacity(topRoute, tester), 0.0);
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text(topRoute), findsNothing);
+    expect(find.text(bottomRoute), findsOneWidget);
+  });
+
+  testWidgets('State is not lost when transitioning',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+    const String bottomRoute = '/';
+    const String topRoute = '/a';
+
+    await tester.pumpWidget(
+      _TestWidget(
+        navigatorKey: navigator,
+        contentBuilder: (RouteSettings settings) {
+          return _StatefulTestWidget(
+            key: ValueKey<String?>(settings.name),
+            name: settings.name,
+          );
+        },
+      ),
+    );
+
+    final _StatefulTestWidgetState bottomState =
+        tester.state(find.byKey(const ValueKey<String?>(bottomRoute)));
+    expect(bottomState.widget.name, bottomRoute);
+
+    navigator.currentState!.pushNamed(topRoute);
+    await tester.pump();
+    await tester.pump();
+
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+      bottomState,
+    );
+    final _StatefulTestWidgetState topState = tester.state(
+      find.byKey(const ValueKey<String?>(topRoute)),
+    );
+    expect(topState.widget.name, topRoute);
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+      bottomState,
+    );
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+      topState,
+    );
+
+    await tester.pumpAndSettle();
+    expect(
+      tester.state(find.byKey(
+        const ValueKey<String?>(bottomRoute),
+        skipOffstage: false,
+      )),
+      bottomState,
+    );
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+      topState,
+    );
+
+    navigator.currentState!.pop();
+    await tester.pump();
+
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+      bottomState,
+    );
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+      topState,
+    );
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+      bottomState,
+    );
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+      topState,
+    );
+
+    await tester.pumpAndSettle();
+    expect(
+      tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+      bottomState,
+    );
+    expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+  });
+
+  testWidgets('should keep state', (WidgetTester tester) async {
+    final AnimationController animation = AnimationController(
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+    final AnimationController secondaryAnimation = AnimationController(
+      vsync: const TestVSync(),
+      duration: const Duration(milliseconds: 300),
+    );
+    await tester.pumpWidget(Directionality(
+      textDirection: TextDirection.ltr,
+      child: Center(
+        child: FadeThroughTransition(
+          child: const _StatefulTestWidget(name: 'Foo'),
+          animation: animation,
+          secondaryAnimation: secondaryAnimation,
+        ),
+      ),
+    ));
+    final State<StatefulWidget> state = tester.state(
+      find.byType(_StatefulTestWidget),
+    );
+    expect(state, isNotNull);
+
+    animation.forward();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    secondaryAnimation.forward();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    secondaryAnimation.reverse();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+    animation.reverse();
+    await tester.pump();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    await tester.pumpAndSettle();
+    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+  });
+}
+
+double _getOpacity(String key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(ValueKey<String?>(key)),
+    matching: find.byType(FadeTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final FadeTransition transition = widget as FadeTransition;
+    return a * transition.opacity.value;
+  });
+}
+
+double _getScale(String key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(ValueKey<String?>(key)),
+    matching: find.byType(ScaleTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final ScaleTransition transition = widget as ScaleTransition;
+    return a * transition.scale.value;
+  });
+}
+
+class _TestWidget extends StatelessWidget {
+  const _TestWidget({this.navigatorKey, this.contentBuilder});
+
+  final Key? navigatorKey;
+  final _ContentBuilder? contentBuilder;
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      navigatorKey: navigatorKey as GlobalKey<NavigatorState>?,
+      theme: ThemeData(
+        platform: TargetPlatform.android,
+        pageTransitionsTheme: const PageTransitionsTheme(
+          builders: <TargetPlatform, PageTransitionsBuilder>{
+            TargetPlatform.android: FadeThroughPageTransitionsBuilder(),
+          },
+        ),
+      ),
+      onGenerateRoute: (RouteSettings settings) {
+        return MaterialPageRoute<void>(
+          settings: settings,
+          builder: (BuildContext context) {
+            return contentBuilder != null
+                ? contentBuilder!(settings)
+                : Center(
+                    key: ValueKey<String?>(settings.name),
+                    child: Text(settings.name!),
+                  );
+          },
+        );
+      },
+    );
+  }
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+  const _StatefulTestWidget({Key? key, this.name}) : super(key: key);
+
+  final String? name;
+
+  @override
+  State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+  @override
+  Widget build(BuildContext context) {
+    return Text(widget.name!);
+  }
+}
+
+typedef _ContentBuilder = Widget Function(RouteSettings settings);
diff --git a/packages/animations/test/modal_test.dart b/packages/animations/test/modal_test.dart
new file mode 100644
index 0000000..7d9c463
--- /dev/null
+++ b/packages/animations/test/modal_test.dart
@@ -0,0 +1,623 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' as ui;
+
+import 'package:animations/src/modal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+void main() {
+  testWidgets(
+    'showModal builds a new route with specified barrier properties',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: _TestModalConfiguration(),
+                      builder: (BuildContext context) {
+                        return const _FlutterLogoModal();
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+
+      // New route containing _FlutterLogoModal is present.
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+      final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>(
+        find.byType(ModalBarrier).at(1),
+      );
+
+      // Verify new route's modal barrier properties are correct.
+      expect(topModalBarrier.color, Colors.green);
+      expect(topModalBarrier.barrierSemanticsDismissible, true);
+      expect(topModalBarrier.semanticsLabel, 'customLabel');
+    },
+  );
+
+  testWidgets(
+    'showModal forwards animation',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: _TestModalConfiguration(),
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // Start forwards animation
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+
+      // Opacity duration: Linear transition throughout 300ms
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+
+      // Halfway through forwards animation.
+      await tester.pump(const Duration(milliseconds: 150));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.5);
+
+      // The end of the transition.
+      await tester.pump(const Duration(milliseconds: 150));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+    },
+  );
+
+  testWidgets(
+    'showModal reverse animation',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      configuration: _TestModalConfiguration(),
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // Start forwards animation
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+
+      // Opacity duration: Linear transition throughout 200ms
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+
+      // Halfway through forwards animation.
+      await tester.pump(const Duration(milliseconds: 100));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.5);
+
+      // The end of the transition.
+      await tester.pump(const Duration(milliseconds: 100));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'showModal builds a new route with specified barrier properties '
+    'with default configuration(FadeScaleTransitionConfiguration)',
+    (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      builder: (BuildContext context) {
+                        return const _FlutterLogoModal();
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+
+      // New route containing _FlutterLogoModal is present.
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+      final ModalBarrier topModalBarrier = tester.widget<ModalBarrier>(
+        find.byType(ModalBarrier).at(1),
+      );
+
+      // Verify new route's modal barrier properties are correct.
+      expect(topModalBarrier.color, Colors.black54);
+      expect(topModalBarrier.barrierSemanticsDismissible, true);
+      expect(topModalBarrier.semanticsLabel, 'Dismiss');
+    },
+  );
+
+  testWidgets(
+    'showModal forwards animation '
+    'with default configuration(FadeScaleTransitionConfiguration)',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // Start forwards animation
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+
+      // Opacity duration: First 30% of 150ms, linear transition
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      double topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      expect(topScale, 0.80);
+
+      // 3/10 * 150ms = 45ms (total opacity animation duration)
+      // 1/2 * 45ms = ~23ms elapsed for halfway point of opacity
+      // animation
+      await tester.pump(const Duration(milliseconds: 23));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.5, 0.05));
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // End of opacity animation.
+      await tester.pump(const Duration(milliseconds: 22));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // 100ms into the animation
+      await tester.pump(const Duration(milliseconds: 55));
+      topScale = _getScale(key, tester);
+      expect(topScale, greaterThan(0.80));
+      expect(topScale, lessThan(1.0));
+
+      // Get to the end of the animation
+      await tester.pump(const Duration(milliseconds: 50));
+      topScale = _getScale(key, tester);
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+    },
+  );
+
+  testWidgets(
+    'showModal reverse animation '
+    'with default configuration(FadeScaleTransitionConfiguration)',
+    (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: ElevatedButton(
+                  onPressed: () {
+                    showModal<void>(
+                      context: context,
+                      builder: (BuildContext context) {
+                        return _FlutterLogoModal(key: key);
+                      },
+                    );
+                  },
+                  child: const Icon(Icons.add),
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // Start forwards animation
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+      // Tap on modal barrier to start reverse animation.
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+
+      // Opacity duration: Linear transition throughout 75ms
+      // No scale animations on exit transition.
+      double topFadeTransitionOpacity = _getOpacity(key, tester);
+      double topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, 1.0);
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.66, 0.05));
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      topScale = _getScale(key, tester);
+      expect(topFadeTransitionOpacity, closeTo(0.33, 0.05));
+      expect(topScale, 1.0);
+
+      // End of opacity animation
+      await tester.pump(const Duration(milliseconds: 25));
+      topFadeTransitionOpacity = _getOpacity(key, tester);
+      expect(topFadeTransitionOpacity, 0.0);
+      topScale = _getScale(key, tester);
+      expect(topScale, 1.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.byType(_FlutterLogoModal), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'State is not lost when transitioning',
+    (WidgetTester tester) async {
+      final GlobalKey bottomKey = GlobalKey();
+      final GlobalKey topKey = GlobalKey();
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Scaffold(
+            body: Builder(builder: (BuildContext context) {
+              return Center(
+                child: Column(
+                  children: <Widget>[
+                    ElevatedButton(
+                      onPressed: () {
+                        showModal<void>(
+                          context: context,
+                          configuration: _TestModalConfiguration(),
+                          builder: (BuildContext context) {
+                            return _FlutterLogoModal(
+                              key: topKey,
+                              name: 'top route',
+                            );
+                          },
+                        );
+                      },
+                      child: const Icon(Icons.add),
+                    ),
+                    _FlutterLogoModal(
+                      key: bottomKey,
+                      name: 'bottom route',
+                    ),
+                  ],
+                ),
+              );
+            }),
+          ),
+        ),
+      );
+
+      // The bottom route's state should already exist.
+      final _FlutterLogoModalState bottomState = tester.state(
+        find.byKey(bottomKey),
+      );
+      expect(bottomState.widget.name, 'bottom route');
+
+      // Start the enter transition of the modal route.
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pump();
+      await tester.pump();
+
+      // The bottom route's state should be retained at the start of the
+      // transition.
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      // The top route's state should be created.
+      final _FlutterLogoModalState topState = tester.state(
+        find.byKey(topKey),
+      );
+      expect(topState.widget.name, 'top route');
+
+      // Halfway point of forwards animation.
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // End the transition and see if top and bottom routes'
+      // states persist.
+      await tester.pumpAndSettle();
+      expect(
+        tester.state(find.byKey(
+          bottomKey,
+          skipOffstage: false,
+        )),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // Start the reverse animation. Both top and bottom
+      // routes' states should persist.
+      await tester.tapAt(Offset.zero);
+      await tester.pump();
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // Halfway point of the exit transition.
+      await tester.pump(const Duration(milliseconds: 100));
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(
+        tester.state(find.byKey(topKey)),
+        topState,
+      );
+
+      // End the exit transition. The bottom route's state should
+      // persist, whereas the top route's state should no longer
+      // be present.
+      await tester.pumpAndSettle();
+      expect(
+        tester.state(find.byKey(bottomKey)),
+        bottomState,
+      );
+      expect(find.byKey(topKey), findsNothing);
+    },
+  );
+
+  testWidgets(
+    'showModal builds a new route with specified route settings',
+    (WidgetTester tester) async {
+      const RouteSettings routeSettings = RouteSettings(
+        name: 'route-name',
+        arguments: 'arguments',
+      );
+
+      final Widget button = Builder(builder: (BuildContext context) {
+        return Center(
+          child: ElevatedButton(
+            onPressed: () {
+              showModal<void>(
+                context: context,
+                configuration: _TestModalConfiguration(),
+                routeSettings: routeSettings,
+                builder: (BuildContext context) {
+                  return const _FlutterLogoModal();
+                },
+              );
+            },
+            child: const Icon(Icons.add),
+          ),
+        );
+      });
+
+      await tester.pumpWidget(_boilerplate(button));
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+
+      // New route containing _FlutterLogoModal is present.
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+
+      // Expect the last route pushed to the navigator to contain RouteSettings
+      // equal to the RouteSettings passed to showModal
+      final ModalRoute<dynamic> modalRoute = ModalRoute.of(
+        tester.element(find.byType(_FlutterLogoModal)),
+      )!;
+      expect(modalRoute.settings, routeSettings);
+    },
+  );
+
+  testWidgets(
+    'showModal builds a new route with specified image filter',
+    (WidgetTester tester) async {
+      final ui.ImageFilter filter = ui.ImageFilter.blur(sigmaX: 1, sigmaY: 1);
+
+      final Widget button = Builder(builder: (BuildContext context) {
+        return Center(
+          child: ElevatedButton(
+            onPressed: () {
+              showModal<void>(
+                context: context,
+                configuration: _TestModalConfiguration(),
+                filter: filter,
+                builder: (BuildContext context) {
+                  return const _FlutterLogoModal();
+                },
+              );
+            },
+            child: const Icon(Icons.add),
+          ),
+        );
+      });
+
+      await tester.pumpWidget(_boilerplate(button));
+      await tester.tap(find.byType(ElevatedButton));
+      await tester.pumpAndSettle();
+
+      // New route containing _FlutterLogoModal is present.
+      expect(find.byType(_FlutterLogoModal), findsOneWidget);
+      final BackdropFilter backdropFilter = tester.widget<BackdropFilter>(
+        find.byType(BackdropFilter),
+      );
+
+      // Verify new route's backdrop filter has been applied
+      expect(backdropFilter.filter, filter);
+    },
+  );
+}
+
+Widget _boilerplate(Widget child) => MaterialApp(home: Scaffold(body: child));
+
+double _getOpacity(GlobalKey key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(key),
+    matching: find.byType(FadeTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final FadeTransition transition = widget as FadeTransition;
+    return a * transition.opacity.value;
+  });
+}
+
+double _getScale(GlobalKey key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(key),
+    matching: find.byType(ScaleTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final ScaleTransition transition = widget as ScaleTransition;
+    return a * transition.scale.value;
+  });
+}
+
+class _FlutterLogoModal extends StatefulWidget {
+  const _FlutterLogoModal({
+    Key? key,
+    this.name,
+  }) : super(key: key);
+
+  final String? name;
+
+  @override
+  _FlutterLogoModalState createState() => _FlutterLogoModalState();
+}
+
+class _FlutterLogoModalState extends State<_FlutterLogoModal> {
+  @override
+  Widget build(BuildContext context) {
+    return const Center(
+      child: SizedBox(
+        width: 250,
+        height: 250,
+        child: Material(
+          child: Center(
+            child: FlutterLogo(size: 250),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _TestModalConfiguration extends ModalConfiguration {
+  _TestModalConfiguration({
+    Color barrierColor = Colors.green,
+    bool barrierDismissible = true,
+    String barrierLabel = 'customLabel',
+    Duration transitionDuration = const Duration(milliseconds: 300),
+    Duration reverseTransitionDuration = const Duration(milliseconds: 200),
+  }) : super(
+          barrierColor: barrierColor,
+          barrierDismissible: barrierDismissible,
+          barrierLabel: barrierLabel,
+          transitionDuration: transitionDuration,
+          reverseTransitionDuration: reverseTransitionDuration,
+        );
+
+  @override
+  Widget transitionBuilder(
+    BuildContext context,
+    Animation<double> animation,
+    Animation<double> secondaryAnimation,
+    Widget child,
+  ) {
+    return FadeTransition(
+      opacity: animation,
+      child: child,
+    );
+  }
+}
diff --git a/packages/animations/test/open_container_test.dart b/packages/animations/test/open_container_test.dart
new file mode 100644
index 0000000..1b05f44
--- /dev/null
+++ b/packages/animations/test/open_container_test.dart
@@ -0,0 +1,1978 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:animations/src/open_container.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets(
+    'Container opens - Fade (by default)',
+    (WidgetTester tester) async {
+      const ShapeBorder shape = RoundedRectangleBorder(
+        borderRadius: BorderRadius.all(Radius.circular(8.0)),
+      );
+      bool closedBuilderCalled = false;
+      bool openBuilderCalled = false;
+
+      await tester.pumpWidget(_boilerplate(
+        child: Center(
+          child: OpenContainer(
+            closedColor: Colors.green,
+            openColor: Colors.blue,
+            closedElevation: 4.0,
+            openElevation: 8.0,
+            closedShape: shape,
+            closedBuilder: (BuildContext context, VoidCallback _) {
+              closedBuilderCalled = true;
+              return const Text('Closed');
+            },
+            openBuilder: (BuildContext context, VoidCallback _) {
+              openBuilderCalled = true;
+              return const Text('Open');
+            },
+          ),
+        ),
+      ));
+
+      // Closed container has the expected properties.
+      final StatefulElement srcMaterialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        ),
+      );
+      final Material srcMaterial = srcMaterialElement.widget as Material;
+      expect(srcMaterial.color, Colors.green);
+      expect(srcMaterial.elevation, 4.0);
+      expect(srcMaterial.shape, shape);
+      expect(find.text('Closed'), findsOneWidget);
+      expect(find.text('Open'), findsNothing);
+      expect(closedBuilderCalled, isTrue);
+      expect(openBuilderCalled, isFalse);
+      final Rect srcMaterialRect = tester.getRect(
+        find.byElementPredicate((Element e) => e == srcMaterialElement),
+      );
+
+      // Open the container.
+      await tester.tap(find.text('Closed'));
+      expect(find.text('Closed'), findsOneWidget);
+      expect(find.text('Open'), findsNothing);
+      await tester.pump();
+
+      // On the first frame of the animation everything still looks like before.
+      final StatefulElement destMaterialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        ),
+      );
+      final Material closedMaterial = destMaterialElement.widget as Material;
+      expect(closedMaterial.color, Colors.green);
+      expect(closedMaterial.elevation, 4.0);
+      expect(closedMaterial.shape, shape);
+      expect(find.text('Closed'), findsOneWidget);
+      expect(find.text('Open'), findsOneWidget);
+      final Rect closedMaterialRect = tester.getRect(
+        find.byElementPredicate((Element e) => e == destMaterialElement),
+      );
+      expect(closedMaterialRect, srcMaterialRect);
+      expect(_getOpacity(tester, 'Open'), 0.0);
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      final _TrackedData dataClosed = _TrackedData(
+        closedMaterial,
+        closedMaterialRect,
+      );
+
+      // Jump to the start of the fade in.
+      await tester.pump(const Duration(milliseconds: 60)); // 300ms * 1/5 = 60ms
+      final _TrackedData dataPreFade = _TrackedData(
+        destMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == destMaterialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataClosed,
+        biggerMaterial: dataPreFade,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump to the middle of the fade in.
+      await tester
+          .pump(const Duration(milliseconds: 30)); // 300ms * 3/10 = 90ms
+      final _TrackedData dataMidFadeIn = _TrackedData(
+        destMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == destMaterialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataPreFade,
+        biggerMaterial: dataMidFadeIn,
+        tester: tester,
+      );
+      expect(dataMidFadeIn.material.color, isNot(dataPreFade.material.color));
+      expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+      expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump to the end of the fade in at 2/5 of 300ms.
+      await tester.pump(
+        const Duration(milliseconds: 30),
+      ); // 300ms * 2/5 = 120ms
+
+      final _TrackedData dataPostFadeIn = _TrackedData(
+        destMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == destMaterialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataMidFadeIn,
+        biggerMaterial: dataPostFadeIn,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Open'), moreOrLessEquals(1.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump almost to the end of the transition.
+      await tester.pump(const Duration(milliseconds: 180));
+      final _TrackedData dataTransitionDone = _TrackedData(
+        destMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == destMaterialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataMidFadeIn,
+        biggerMaterial: dataTransitionDone,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Open'), 1.0);
+      expect(dataTransitionDone.material.color, Colors.blue);
+      expect(dataTransitionDone.material.elevation, 8.0);
+      expect(dataTransitionDone.radius, 0.0);
+      expect(dataTransitionDone.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.text('Closed'), findsNothing); // No longer in the tree.
+      expect(find.text('Open'), findsOneWidget);
+      final StatefulElement finalMaterialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Open'),
+          matching: find.byType(Material),
+        ),
+      );
+      final _TrackedData dataOpen = _TrackedData(
+        finalMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == finalMaterialElement),
+        ),
+      );
+      expect(dataOpen.material.color, dataTransitionDone.material.color);
+      expect(
+          dataOpen.material.elevation, dataTransitionDone.material.elevation);
+      expect(dataOpen.radius, dataTransitionDone.radius);
+      expect(dataOpen.rect, dataTransitionDone.rect);
+    },
+  );
+
+  testWidgets(
+    'Container closes - Fade (by default)',
+    (WidgetTester tester) async {
+      const ShapeBorder shape = RoundedRectangleBorder(
+        borderRadius: BorderRadius.all(Radius.circular(8.0)),
+      );
+
+      await tester.pumpWidget(_boilerplate(
+        child: Center(
+          child: OpenContainer(
+            closedColor: Colors.green,
+            openColor: Colors.blue,
+            closedElevation: 4.0,
+            openElevation: 8.0,
+            closedShape: shape,
+            closedBuilder: (BuildContext context, VoidCallback _) {
+              return const Text('Closed');
+            },
+            openBuilder: (BuildContext context, VoidCallback _) {
+              return const Text('Open');
+            },
+          ),
+        ),
+      ));
+
+      await tester.tap(find.text('Closed'));
+      await tester.pumpAndSettle();
+
+      // Open container has the expected properties.
+      expect(find.text('Closed'), findsNothing);
+      expect(find.text('Open'), findsOneWidget);
+      final StatefulElement initialMaterialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Open'),
+          matching: find.byType(Material),
+        ),
+      );
+      final _TrackedData dataOpen = _TrackedData(
+        initialMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == initialMaterialElement),
+        ),
+      );
+      expect(dataOpen.material.color, Colors.blue);
+      expect(dataOpen.material.elevation, 8.0);
+      expect(dataOpen.radius, 0.0);
+      expect(dataOpen.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+      // Close the container.
+      final NavigatorState navigator = tester.state(find.byType(Navigator));
+      navigator.pop();
+      await tester.pump();
+
+      expect(find.text('Closed'), findsOneWidget);
+      expect(find.text('Open'), findsOneWidget);
+      final StatefulElement materialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Open'),
+          matching: find.byType(Material),
+        ),
+      );
+      final _TrackedData dataTransitionStart = _TrackedData(
+        materialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == materialElement),
+        ),
+      );
+      expect(dataTransitionStart.material.color, dataOpen.material.color);
+      expect(
+          dataTransitionStart.material.elevation, dataOpen.material.elevation);
+      expect(dataTransitionStart.radius, dataOpen.radius);
+      expect(dataTransitionStart.rect, dataOpen.rect);
+      expect(_getOpacity(tester, 'Open'), 1.0);
+      await tester.pump(const Duration(microseconds: 1)); // 300 * 1/5 = 60
+
+      // Jump to start of fade out: 1/5 of 300.
+      await tester.pump(const Duration(milliseconds: 60)); // 300 * 1/5 = 60
+      final _TrackedData dataPreFadeOut = _TrackedData(
+        materialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == materialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataPreFadeOut,
+        biggerMaterial: dataTransitionStart,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Open'), moreOrLessEquals(1.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump to the middle of the fade out.
+      await tester.pump(const Duration(milliseconds: 30)); // 300 * 3/10 = 90
+      final _TrackedData dataMidpoint = _TrackedData(
+        materialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == materialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataMidpoint,
+        biggerMaterial: dataPreFadeOut,
+        tester: tester,
+      );
+      expect(dataMidpoint.material.color, isNot(dataPreFadeOut.material.color));
+      expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+      expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump to the end of the fade out.
+      await tester.pump(const Duration(milliseconds: 30)); // 300 * 2/5 = 120
+      final _TrackedData dataPostFadeOut = _TrackedData(
+        materialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == materialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataPostFadeOut,
+        biggerMaterial: dataMidpoint,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Jump almost to the end of the transition.
+      await tester.pump(const Duration(milliseconds: 180));
+      final _TrackedData dataTransitionDone = _TrackedData(
+        materialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == materialElement),
+        ),
+      );
+      _expectMaterialPropertiesHaveAdvanced(
+        smallerMaterial: dataTransitionDone,
+        biggerMaterial: dataPostFadeOut,
+        tester: tester,
+      );
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+      expect(_getOpacity(tester, 'Open'), 0.0);
+      expect(dataTransitionDone.material.color, Colors.green);
+      expect(dataTransitionDone.material.elevation, 4.0);
+      expect(dataTransitionDone.radius, 8.0);
+
+      await tester.pump(const Duration(milliseconds: 1));
+      expect(find.text('Open'), findsNothing); // No longer in the tree.
+      expect(find.text('Closed'), findsOneWidget);
+      final StatefulElement finalMaterialElement = tester.firstElement(
+        find.ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        ),
+      );
+      final _TrackedData dataClosed = _TrackedData(
+        finalMaterialElement.widget as Material,
+        tester.getRect(
+          find.byElementPredicate((Element e) => e == finalMaterialElement),
+        ),
+      );
+      expect(dataClosed.material.color, dataTransitionDone.material.color);
+      expect(
+        dataClosed.material.elevation,
+        dataTransitionDone.material.elevation,
+      );
+      expect(dataClosed.radius, dataTransitionDone.radius);
+      expect(dataClosed.rect, dataTransitionDone.rect);
+    },
+  );
+
+  testWidgets('Container opens - Fade through', (WidgetTester tester) async {
+    const ShapeBorder shape = RoundedRectangleBorder(
+      borderRadius: BorderRadius.all(Radius.circular(8.0)),
+    );
+    bool closedBuilderCalled = false;
+    bool openBuilderCalled = false;
+
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedColor: Colors.green,
+          openColor: Colors.blue,
+          middleColor: Colors.red,
+          closedElevation: 4.0,
+          openElevation: 8.0,
+          closedShape: shape,
+          closedBuilder: (BuildContext context, VoidCallback _) {
+            closedBuilderCalled = true;
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback _) {
+            openBuilderCalled = true;
+            return const Text('Open');
+          },
+          transitionType: ContainerTransitionType.fadeThrough,
+        ),
+      ),
+    ));
+
+    // Closed container has the expected properties.
+    final StatefulElement srcMaterialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Closed'),
+        matching: find.byType(Material),
+      ),
+    );
+    final Material srcMaterial = srcMaterialElement.widget as Material;
+    expect(srcMaterial.color, Colors.green);
+    expect(srcMaterial.elevation, 4.0);
+    expect(srcMaterial.shape, shape);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Open'), findsNothing);
+    expect(closedBuilderCalled, isTrue);
+    expect(openBuilderCalled, isFalse);
+    final Rect srcMaterialRect = tester.getRect(
+      find.byElementPredicate((Element e) => e == srcMaterialElement),
+    );
+
+    // Open the container.
+    await tester.tap(find.text('Closed'));
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Open'), findsNothing);
+    await tester.pump();
+
+    // On the first frame of the animation everything still looks like before.
+    final StatefulElement destMaterialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Closed'),
+        matching: find.byType(Material),
+      ),
+    );
+    final Material closedMaterial = destMaterialElement.widget as Material;
+    expect(closedMaterial.color, Colors.green);
+    expect(closedMaterial.elevation, 4.0);
+    expect(closedMaterial.shape, shape);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Open'), findsOneWidget);
+    final Rect closedMaterialRect = tester.getRect(
+      find.byElementPredicate((Element e) => e == destMaterialElement),
+    );
+    expect(closedMaterialRect, srcMaterialRect);
+    expect(_getOpacity(tester, 'Open'), 0.0);
+    expect(_getOpacity(tester, 'Closed'), 1.0);
+
+    final _TrackedData dataClosed = _TrackedData(
+      closedMaterial,
+      closedMaterialRect,
+    );
+
+    // The fade-out takes 1/5 of 300ms. Let's jump to the midpoint of that.
+    await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/10 = 30ms
+    final _TrackedData dataMidFadeOut = _TrackedData(
+      destMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == destMaterialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataClosed,
+      biggerMaterial: dataMidFadeOut,
+      tester: tester,
+    );
+    expect(dataMidFadeOut.material.color, isNot(dataClosed.material.color));
+    expect(_getOpacity(tester, 'Open'), 0.0);
+    expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
+    expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
+
+    // Let's jump to the crossover point at 1/5 of 300ms.
+    await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/5 = 60ms
+    final _TrackedData dataMidpoint = _TrackedData(
+      destMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == destMaterialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidFadeOut,
+      biggerMaterial: dataMidpoint,
+      tester: tester,
+    );
+    expect(dataMidpoint.material.color, Colors.red);
+    expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+    expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
+
+    // Let's jump to the middle of the fade-in at 3/5 of 300ms
+    await tester.pump(const Duration(milliseconds: 120)); // 300ms * 3/5 = 180ms
+    final _TrackedData dataMidFadeIn = _TrackedData(
+      destMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == destMaterialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidpoint,
+      biggerMaterial: dataMidFadeIn,
+      tester: tester,
+    );
+    expect(dataMidFadeIn.material.color, isNot(dataMidpoint.material.color));
+    expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+    expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+    expect(_getOpacity(tester, 'Closed'), 0.0);
+
+    // Let's jump almost to the end of the transition.
+    await tester.pump(const Duration(milliseconds: 120));
+    final _TrackedData dataTransitionDone = _TrackedData(
+      destMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == destMaterialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidFadeIn,
+      biggerMaterial: dataTransitionDone,
+      tester: tester,
+    );
+    expect(
+      dataTransitionDone.material.color,
+      isNot(dataMidFadeIn.material.color),
+    );
+    expect(_getOpacity(tester, 'Open'), 1.0);
+    expect(_getOpacity(tester, 'Closed'), 0.0);
+    expect(dataTransitionDone.material.color, Colors.blue);
+    expect(dataTransitionDone.material.elevation, 8.0);
+    expect(dataTransitionDone.radius, 0.0);
+    expect(dataTransitionDone.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Closed'), findsNothing); // No longer in the tree.
+    expect(find.text('Open'), findsOneWidget);
+    final StatefulElement finalMaterialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Open'),
+        matching: find.byType(Material),
+      ),
+    );
+    final _TrackedData dataOpen = _TrackedData(
+      finalMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == finalMaterialElement),
+      ),
+    );
+    expect(dataOpen.material.color, dataTransitionDone.material.color);
+    expect(dataOpen.material.elevation, dataTransitionDone.material.elevation);
+    expect(dataOpen.radius, dataTransitionDone.radius);
+    expect(dataOpen.rect, dataTransitionDone.rect);
+  });
+
+  testWidgets('Container closes - Fade through', (WidgetTester tester) async {
+    const ShapeBorder shape = RoundedRectangleBorder(
+      borderRadius: BorderRadius.all(Radius.circular(8.0)),
+    );
+
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedColor: Colors.green,
+          openColor: Colors.blue,
+          middleColor: Colors.red,
+          closedElevation: 4.0,
+          openElevation: 8.0,
+          closedShape: shape,
+          closedBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Open');
+          },
+          transitionType: ContainerTransitionType.fadeThrough,
+        ),
+      ),
+    ));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    // Open container has the expected properties.
+    expect(find.text('Closed'), findsNothing);
+    expect(find.text('Open'), findsOneWidget);
+    final StatefulElement initialMaterialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Open'),
+        matching: find.byType(Material),
+      ),
+    );
+    final _TrackedData dataOpen = _TrackedData(
+      initialMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == initialMaterialElement),
+      ),
+    );
+    expect(dataOpen.material.color, Colors.blue);
+    expect(dataOpen.material.elevation, 8.0);
+    expect(dataOpen.radius, 0.0);
+    expect(dataOpen.rect, const Rect.fromLTRB(0, 0, 800, 600));
+
+    // Close the container.
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Open'), findsOneWidget);
+    final StatefulElement materialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Open'),
+        matching: find.byType(Material),
+      ),
+    );
+    final _TrackedData dataTransitionStart = _TrackedData(
+      materialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == materialElement),
+      ),
+    );
+    expect(dataTransitionStart.material.color, dataOpen.material.color);
+    expect(dataTransitionStart.material.elevation, dataOpen.material.elevation);
+    expect(dataTransitionStart.radius, dataOpen.radius);
+    expect(dataTransitionStart.rect, dataOpen.rect);
+    expect(_getOpacity(tester, 'Open'), 1.0);
+    expect(_getOpacity(tester, 'Closed'), 0.0);
+
+    // Jump to mid-point of fade-out: 1/10 of 300ms.
+    await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/10 = 30ms
+    final _TrackedData dataMidFadeOut = _TrackedData(
+      materialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == materialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidFadeOut,
+      biggerMaterial: dataTransitionStart,
+      tester: tester,
+    );
+    expect(
+      dataMidFadeOut.material.color,
+      isNot(dataTransitionStart.material.color),
+    );
+    expect(_getOpacity(tester, 'Closed'), 0.0);
+    expect(_getOpacity(tester, 'Open'), lessThan(1.0));
+    expect(_getOpacity(tester, 'Open'), greaterThan(0.0));
+
+    // Let's jump to the crossover point at 1/5 of 300ms.
+    await tester.pump(const Duration(milliseconds: 30)); // 300ms * 1/5 = 60ms
+    final _TrackedData dataMidpoint = _TrackedData(
+      materialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == materialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidpoint,
+      biggerMaterial: dataMidFadeOut,
+      tester: tester,
+    );
+    expect(dataMidpoint.material.color, Colors.red);
+    expect(_getOpacity(tester, 'Open'), moreOrLessEquals(0.0));
+    expect(_getOpacity(tester, 'Closed'), moreOrLessEquals(0.0));
+
+    // Let's jump to the middle of the fade-in at 3/5 of 300ms
+    await tester.pump(const Duration(milliseconds: 120)); // 300ms * 3/5 = 180ms
+    final _TrackedData dataMidFadeIn = _TrackedData(
+      materialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == materialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataMidFadeIn,
+      biggerMaterial: dataMidpoint,
+      tester: tester,
+    );
+    expect(dataMidFadeIn.material.color, isNot(dataMidpoint.material.color));
+    expect(_getOpacity(tester, 'Closed'), lessThan(1.0));
+    expect(_getOpacity(tester, 'Closed'), greaterThan(0.0));
+    expect(_getOpacity(tester, 'Open'), 0.0);
+
+    // Let's jump almost to the end of the transition.
+    await tester.pump(const Duration(milliseconds: 120));
+    final _TrackedData dataTransitionDone = _TrackedData(
+      materialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == materialElement),
+      ),
+    );
+    _expectMaterialPropertiesHaveAdvanced(
+      smallerMaterial: dataTransitionDone,
+      biggerMaterial: dataMidFadeIn,
+      tester: tester,
+    );
+    expect(
+      dataTransitionDone.material.color,
+      isNot(dataMidFadeIn.material.color),
+    );
+    expect(_getOpacity(tester, 'Closed'), 1.0);
+    expect(_getOpacity(tester, 'Open'), 0.0);
+    expect(dataTransitionDone.material.color, Colors.green);
+    expect(dataTransitionDone.material.elevation, 4.0);
+    expect(dataTransitionDone.radius, 8.0);
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Open'), findsNothing); // No longer in the tree.
+    expect(find.text('Closed'), findsOneWidget);
+    final StatefulElement finalMaterialElement = tester.firstElement(
+      find.ancestor(
+        of: find.text('Closed'),
+        matching: find.byType(Material),
+      ),
+    );
+    final _TrackedData dataClosed = _TrackedData(
+      finalMaterialElement.widget as Material,
+      tester.getRect(
+        find.byElementPredicate((Element e) => e == finalMaterialElement),
+      ),
+    );
+    expect(dataClosed.material.color, dataTransitionDone.material.color);
+    expect(
+      dataClosed.material.elevation,
+      dataTransitionDone.material.elevation,
+    );
+    expect(dataClosed.radius, dataTransitionDone.radius);
+    expect(dataClosed.rect, dataTransitionDone.rect);
+  });
+
+  testWidgets('Cannot tap container if tappable=false',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          tappable: false,
+          closedBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Open');
+          },
+        ),
+      ),
+    ));
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+  });
+
+  testWidgets('Action callbacks work', (WidgetTester tester) async {
+    late VoidCallback open, close;
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          tappable: false,
+          closedBuilder: (BuildContext context, VoidCallback action) {
+            open = action;
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback action) {
+            close = action;
+            return const Text('Open');
+          },
+        ),
+      ),
+    ));
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+
+    expect(open, isNotNull);
+    open();
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    expect(close, isNotNull);
+    close();
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+  });
+
+  testWidgets('open widget keeps state', (WidgetTester tester) async {
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedBuilder: (BuildContext context, VoidCallback action) {
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback action) {
+            return const DummyStatefulWidget();
+          },
+        ),
+      ),
+    ));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pump(const Duration(milliseconds: 200));
+
+    final State stateOpening = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateOpening, isNotNull);
+
+    await tester.pumpAndSettle();
+    expect(find.text('Closed'), findsNothing);
+    final State stateOpen = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateOpen, isNotNull);
+    expect(stateOpen, same(stateOpening));
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump(const Duration(milliseconds: 200));
+    expect(find.text('Closed'), findsOneWidget);
+    final State stateClosing = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateClosing, isNotNull);
+    expect(stateClosing, same(stateOpen));
+  });
+
+  testWidgets('closed widget keeps state', (WidgetTester tester) async {
+    late VoidCallback open;
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedBuilder: (BuildContext context, VoidCallback action) {
+            open = action;
+            return const DummyStatefulWidget();
+          },
+          openBuilder: (BuildContext context, VoidCallback action) {
+            return const Text('Open');
+          },
+        ),
+      ),
+    ));
+
+    final State stateClosed = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateClosed, isNotNull);
+
+    open();
+    await tester.pump(const Duration(milliseconds: 200));
+    expect(find.text('Open'), findsOneWidget);
+
+    final State stateOpening = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateOpening, same(stateClosed));
+
+    await tester.pumpAndSettle();
+    expect(find.byType(DummyStatefulWidget), findsNothing);
+    expect(find.text('Open'), findsOneWidget);
+    final State stateOpen = tester.state(find.byType(
+      DummyStatefulWidget,
+      skipOffstage: false,
+    ));
+    expect(stateOpen, same(stateOpening));
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump(const Duration(milliseconds: 200));
+    expect(find.text('Open'), findsOneWidget);
+    final State stateClosing = tester.state(find.byType(DummyStatefulWidget));
+    expect(stateClosing, same(stateOpen));
+
+    await tester.pumpAndSettle();
+    expect(find.text('Open'), findsNothing);
+    final State stateClosedAgain =
+        tester.state(find.byType(DummyStatefulWidget));
+    expect(stateClosedAgain, same(stateClosing));
+  });
+
+  testWidgets('closes to the right location when src position has changed',
+      (WidgetTester tester) async {
+    final Widget openContainer = OpenContainer(
+      closedBuilder: (BuildContext context, VoidCallback action) {
+        return const SizedBox(
+          height: 100,
+          width: 100,
+          child: Text('Closed'),
+        );
+      },
+      openBuilder: (BuildContext context, VoidCallback action) {
+        return GestureDetector(
+          onTap: action,
+          child: const Text('Open'),
+        );
+      },
+    );
+
+    await tester.pumpWidget(_boilerplate(
+      child: Align(
+        alignment: Alignment.topLeft,
+        child: openContainer,
+      ),
+    ));
+
+    final Rect originTextRect = tester.getRect(find.text('Closed'));
+    expect(originTextRect.topLeft, Offset.zero);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    await tester.pumpWidget(_boilerplate(
+      child: Align(
+        alignment: Alignment.bottomLeft,
+        child: openContainer,
+      ),
+    ));
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    await tester.tap(find.text('Open'));
+    await tester.pump(); // Need one frame to measure things in the old route.
+    await tester.pump(const Duration(milliseconds: 300));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+
+    final Rect transitionEndTextRect = tester.getRect(find.text('Open'));
+    expect(transitionEndTextRect.topLeft, const Offset(0.0, 600.0 - 100.0));
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+
+    final Rect finalTextRect = tester.getRect(find.text('Closed'));
+    expect(finalTextRect.topLeft, transitionEndTextRect.topLeft);
+  });
+
+  testWidgets('src changes size while open', (WidgetTester tester) async {
+    final Widget openContainer = _boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedBuilder: (BuildContext context, VoidCallback action) {
+            return const _SizableContainer(
+              initialSize: 100,
+              child: Text('Closed'),
+            );
+          },
+          openBuilder: (BuildContext context, VoidCallback action) {
+            return GestureDetector(
+              onTap: action,
+              child: const Text('Open'),
+            );
+          },
+        ),
+      ),
+    );
+
+    await tester.pumpWidget(openContainer);
+
+    final Size orignalClosedRect = tester.getSize(find
+        .ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        )
+        .first);
+    expect(orignalClosedRect, const Size(100, 100));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    final _SizableContainerState containerState = tester.state(find.byType(
+      _SizableContainer,
+      skipOffstage: false,
+    ));
+    containerState.size = 200;
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+    await tester.tap(find.text('Open'));
+    await tester.pump(); // Need one frame to measure things in the old route.
+    await tester.pump(const Duration(milliseconds: 300));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+
+    final Size transitionEndSize = tester.getSize(find
+        .ancestor(
+          of: find.text('Open'),
+          matching: find.byType(Material),
+        )
+        .first);
+    expect(transitionEndSize, const Size(200, 200));
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+
+    final Size finalSize = tester.getSize(find
+        .ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        )
+        .first);
+    expect(finalSize, const Size(200, 200));
+  });
+
+  testWidgets('transition is interrupted and should not jump',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedBuilder: (BuildContext context, VoidCallback action) {
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback action) {
+            return const Text('Open');
+          },
+        ),
+      ),
+    ));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+
+    final Material openingMaterial = tester.firstWidget(find.ancestor(
+      of: find.text('Closed'),
+      matching: find.byType(Material),
+    ));
+    final Rect openingRect = tester.getRect(
+      find.byWidgetPredicate((Widget w) => w == openingMaterial),
+    );
+
+    // Close the container while it is half way to open.
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+
+    final Material closingMaterial = tester.firstWidget(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    ));
+    final Rect closingRect = tester.getRect(
+      find.byWidgetPredicate((Widget w) => w == closingMaterial),
+    );
+
+    expect(closingMaterial.elevation, openingMaterial.elevation);
+    expect(closingMaterial.color, openingMaterial.color);
+    expect(closingMaterial.shape, openingMaterial.shape);
+    expect(closingRect, openingRect);
+  });
+
+  testWidgets('navigator is not full size', (WidgetTester tester) async {
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        width: 300,
+        height: 400,
+        child: _boilerplate(
+          child: Center(
+            child: OpenContainer(
+              closedBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Closed');
+              },
+              openBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Open');
+              },
+            ),
+          ),
+        ),
+      ),
+    ));
+    const Rect fullNavigator = Rect.fromLTWH(250, 100, 300, 400);
+
+    expect(tester.getRect(find.byType(Navigator)), fullNavigator);
+    final Rect materialRectClosed = tester.getRect(find
+        .ancestor(
+          of: find.text('Closed'),
+          matching: find.byType(Material),
+        )
+        .first);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pump();
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+    final Rect materialRectTransitionStart = tester.getRect(find.ancestor(
+      of: find.text('Closed'),
+      matching: find.byType(Material),
+    ));
+    expect(materialRectTransitionStart, materialRectClosed);
+
+    await tester.pump(const Duration(milliseconds: 300));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+    final Rect materialRectTransitionEnd = tester.getRect(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    ));
+    expect(materialRectTransitionEnd, fullNavigator);
+    await tester.pumpAndSettle();
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+    final Rect materialRectOpen = tester.getRect(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    ));
+    expect(materialRectOpen, fullNavigator);
+  });
+
+  testWidgets('does not crash when disposed right after pop',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        width: 300,
+        height: 400,
+        child: _boilerplate(
+          child: Center(
+            child: OpenContainer(
+              closedBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Closed');
+              },
+              openBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Open');
+              },
+            ),
+          ),
+        ),
+      ),
+    ));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+
+    await tester.pumpWidget(const Placeholder());
+    expect(tester.takeException(), isNull);
+    await tester.pumpAndSettle();
+    expect(tester.takeException(), isNull);
+  });
+
+  testWidgets('can specify a duration', (WidgetTester tester) async {
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        width: 300,
+        height: 400,
+        child: _boilerplate(
+          child: Center(
+            child: OpenContainer(
+              transitionDuration: const Duration(seconds: 2),
+              closedBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Closed');
+              },
+              openBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Open');
+              },
+            ),
+          ),
+        ),
+      ),
+    ));
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pump();
+
+    // Jump to the end of the transition.
+    await tester.pump(const Duration(seconds: 2));
+    expect(find.text('Open'), findsOneWidget); // faded in
+    expect(find.text('Closed'), findsOneWidget); // faded out
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+
+    // Jump to the end of the transition.
+    await tester.pump(const Duration(seconds: 2));
+    expect(find.text('Open'), findsOneWidget); // faded out
+    expect(find.text('Closed'), findsOneWidget); // faded in
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+  });
+
+  testWidgets('can specify an open shape', (WidgetTester tester) async {
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        width: 300,
+        height: 400,
+        child: _boilerplate(
+          child: Center(
+            child: OpenContainer(
+              closedShape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(10),
+              ),
+              openShape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(40),
+              ),
+              closedBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Closed');
+              },
+              openBuilder: (BuildContext context, VoidCallback action) {
+                return const Text('Open');
+              },
+            ),
+          ),
+        ),
+      ),
+    ));
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    final double closedRadius = _getRadius(tester.firstWidget(find.ancestor(
+      of: find.text('Closed'),
+      matching: find.byType(Material),
+    )));
+    expect(closedRadius, 10.0);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pump();
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+    final double openingRadius = _getRadius(tester.firstWidget(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    )));
+    expect(openingRadius, 10.0);
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+    final double halfwayRadius = _getRadius(tester.firstWidget(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    )));
+    expect(halfwayRadius, greaterThan(10.0));
+    expect(halfwayRadius, lessThan(40.0));
+
+    await tester.pump(const Duration(milliseconds: 150));
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsOneWidget);
+    final double openRadius = _getRadius(tester.firstWidget(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    )));
+    expect(openRadius, 40.0);
+
+    await tester.pump(const Duration(milliseconds: 1));
+    expect(find.text('Closed'), findsNothing);
+    expect(find.text('Open'), findsOneWidget);
+    final double finalRadius = _getRadius(tester.firstWidget(find.ancestor(
+      of: find.text('Open'),
+      matching: find.byType(Material),
+    )));
+    expect(finalRadius, 40.0);
+  });
+
+  testWidgets('Scrim', (WidgetTester tester) async {
+    const ShapeBorder shape = RoundedRectangleBorder(
+      borderRadius: BorderRadius.all(Radius.circular(8.0)),
+    );
+
+    await tester.pumpWidget(_boilerplate(
+      child: Center(
+        child: OpenContainer(
+          closedColor: Colors.green,
+          openColor: Colors.blue,
+          closedElevation: 4.0,
+          openElevation: 8.0,
+          closedShape: shape,
+          closedBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Closed');
+          },
+          openBuilder: (BuildContext context, VoidCallback _) {
+            return const Text('Open');
+          },
+        ),
+      ),
+    ));
+
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Open'), findsNothing);
+    await tester.tap(find.text('Closed'));
+    await tester.pump();
+
+    expect(_getScrimColor(tester), Colors.transparent);
+
+    await tester.pump(const Duration(milliseconds: 50));
+    final Color halfwayFadeInColor = _getScrimColor(tester);
+    expect(halfwayFadeInColor, isNot(Colors.transparent));
+    expect(halfwayFadeInColor, isNot(Colors.black54));
+
+    // Scrim is done fading in early.
+    await tester.pump(const Duration(milliseconds: 50));
+    expect(_getScrimColor(tester), Colors.black54);
+
+    await tester.pump(const Duration(milliseconds: 200));
+    expect(_getScrimColor(tester), Colors.black54);
+
+    await tester.pumpAndSettle();
+
+    // Close the container
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+
+    expect(_getScrimColor(tester), Colors.black54);
+
+    // Scrim takes longer to fade out (vs. fade in).
+    await tester.pump(const Duration(milliseconds: 200));
+    final Color halfwayFadeOutColor = _getScrimColor(tester);
+    expect(halfwayFadeOutColor, isNot(Colors.transparent));
+    expect(halfwayFadeOutColor, isNot(Colors.black54));
+
+    await tester.pump(const Duration(milliseconds: 100));
+    expect(_getScrimColor(tester), Colors.transparent);
+  });
+
+  testWidgets(
+      'Container partly offscreen can be opened without crash - vertical',
+      (WidgetTester tester) async {
+    final ScrollController controller =
+        ScrollController(initialScrollOffset: 50);
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        height: 200,
+        width: 200,
+        child: _boilerplate(
+          child: ListView.builder(
+            cacheExtent: 0,
+            controller: controller,
+            itemBuilder: (BuildContext context, int index) {
+              return OpenContainer(
+                closedBuilder: (BuildContext context, VoidCallback _) {
+                  return SizedBox(
+                    height: 100,
+                    width: 100,
+                    child: Text('Closed $index'),
+                  );
+                },
+                openBuilder: (BuildContext context, VoidCallback _) {
+                  return Text('Open $index');
+                },
+              );
+            },
+          ),
+        ),
+      ),
+    ));
+
+    void expectClosedState() {
+      expect(find.text('Closed 0'), findsOneWidget);
+      expect(find.text('Closed 1'), findsOneWidget);
+      expect(find.text('Closed 2'), findsOneWidget);
+      expect(find.text('Closed 3'), findsNothing);
+
+      expect(find.text('Open 0'), findsNothing);
+      expect(find.text('Open 1'), findsNothing);
+      expect(find.text('Open 2'), findsNothing);
+      expect(find.text('Open 3'), findsNothing);
+    }
+
+    expectClosedState();
+
+    // Open container that's partly visible at top.
+    await tester.tapAt(
+      tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
+    );
+    await tester.pump();
+    await tester.pumpAndSettle();
+    expect(find.text('Closed 0'), findsNothing);
+    expect(find.text('Open 0'), findsOneWidget);
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+    await tester.pumpAndSettle();
+    expectClosedState();
+
+    // Open container that's partly visible at bottom.
+    await tester.tapAt(
+      tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
+    );
+    await tester.pump();
+    await tester.pumpAndSettle();
+
+    expect(find.text('Closed 2'), findsNothing);
+    expect(find.text('Open 2'), findsOneWidget);
+  });
+
+  testWidgets(
+      'Container partly offscreen can be opened without crash - horizontal',
+      (WidgetTester tester) async {
+    final ScrollController controller =
+        ScrollController(initialScrollOffset: 50);
+    await tester.pumpWidget(Center(
+      child: SizedBox(
+        height: 200,
+        width: 200,
+        child: _boilerplate(
+          child: ListView.builder(
+            scrollDirection: Axis.horizontal,
+            cacheExtent: 0,
+            controller: controller,
+            itemBuilder: (BuildContext context, int index) {
+              return OpenContainer(
+                closedBuilder: (BuildContext context, VoidCallback _) {
+                  return SizedBox(
+                    height: 100,
+                    width: 100,
+                    child: Text('Closed $index'),
+                  );
+                },
+                openBuilder: (BuildContext context, VoidCallback _) {
+                  return Text('Open $index');
+                },
+              );
+            },
+          ),
+        ),
+      ),
+    ));
+
+    void expectClosedState() {
+      expect(find.text('Closed 0'), findsOneWidget);
+      expect(find.text('Closed 1'), findsOneWidget);
+      expect(find.text('Closed 2'), findsOneWidget);
+      expect(find.text('Closed 3'), findsNothing);
+
+      expect(find.text('Open 0'), findsNothing);
+      expect(find.text('Open 1'), findsNothing);
+      expect(find.text('Open 2'), findsNothing);
+      expect(find.text('Open 3'), findsNothing);
+    }
+
+    expectClosedState();
+
+    // Open container that's partly visible at left edge.
+    await tester.tapAt(
+      tester.getBottomRight(find.text('Closed 0')) - const Offset(20, 20),
+    );
+    await tester.pump();
+    await tester.pumpAndSettle();
+    expect(find.text('Closed 0'), findsNothing);
+    expect(find.text('Open 0'), findsOneWidget);
+
+    final NavigatorState navigator = tester.state(find.byType(Navigator));
+    navigator.pop();
+    await tester.pump();
+    await tester.pumpAndSettle();
+    expectClosedState();
+
+    // Open container that's partly visible at right edge.
+    await tester.tapAt(
+      tester.getTopLeft(find.text('Closed 2')) + const Offset(20, 20),
+    );
+    await tester.pump();
+    await tester.pumpAndSettle();
+
+    expect(find.text('Closed 2'), findsNothing);
+    expect(find.text('Open 2'), findsOneWidget);
+  });
+
+  testWidgets(
+      'Container can be dismissed after container widget itself is removed without crash',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(_boilerplate(child: _RemoveOpenContainerExample()));
+
+    expect(find.text('Closed'), findsOneWidget);
+    expect(find.text('Closed', skipOffstage: false), findsOneWidget);
+    expect(find.text('Open'), findsNothing);
+
+    await tester.tap(find.text('Open the container'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Closed'), findsNothing);
+    expect(find.text('Closed', skipOffstage: false), findsOneWidget);
+    expect(find.text('Open'), findsOneWidget);
+
+    await tester.tap(find.text('Remove the container'));
+    await tester.pump();
+
+    expect(find.text('Closed'), findsNothing);
+    expect(find.text('Closed', skipOffstage: false), findsNothing);
+    expect(find.text('Open'), findsOneWidget);
+
+    await tester.tap(find.text('Close the container'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Closed'), findsNothing);
+    expect(find.text('Closed', skipOffstage: false), findsNothing);
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Container has been removed'), findsOneWidget);
+  });
+
+  testWidgets('onClosed callback is called when container has closed',
+      (WidgetTester tester) async {
+    bool hasClosed = false;
+    final Widget openContainer = OpenContainer(
+      onClosed: (dynamic _) {
+        hasClosed = true;
+      },
+      closedBuilder: (BuildContext context, VoidCallback action) {
+        return GestureDetector(
+          onTap: action,
+          child: const Text('Closed'),
+        );
+      },
+      openBuilder: (BuildContext context, VoidCallback action) {
+        return GestureDetector(
+          onTap: action,
+          child: const Text('Open'),
+        );
+      },
+    );
+
+    await tester.pumpWidget(
+      _boilerplate(child: openContainer),
+    );
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(hasClosed, isFalse);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    await tester.tap(find.text('Open'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(hasClosed, isTrue);
+  });
+
+  testWidgets(
+      'onClosed callback receives popped value when container has closed',
+      (WidgetTester tester) async {
+    bool? value = false;
+    final Widget openContainer = OpenContainer<bool>(
+      onClosed: (bool? poppedValue) {
+        value = poppedValue;
+      },
+      closedBuilder: (BuildContext context, VoidCallback action) {
+        return GestureDetector(
+          onTap: action,
+          child: const Text('Closed'),
+        );
+      },
+      openBuilder:
+          (BuildContext context, CloseContainerActionCallback<bool> action) {
+        return GestureDetector(
+          onTap: () => action(returnValue: true),
+          child: const Text('Open'),
+        );
+      },
+    );
+
+    await tester.pumpWidget(
+      _boilerplate(child: openContainer),
+    );
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(value, isFalse);
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsOneWidget);
+    expect(find.text('Closed'), findsNothing);
+
+    await tester.tap(find.text('Open'));
+    await tester.pumpAndSettle();
+
+    expect(find.text('Open'), findsNothing);
+    expect(find.text('Closed'), findsOneWidget);
+    expect(value, isTrue);
+  });
+
+  testWidgets('closedBuilder has anti-alias clip by default',
+      (WidgetTester tester) async {
+    final GlobalKey closedBuilderKey = GlobalKey();
+    final Widget openContainer = OpenContainer(
+      closedBuilder: (BuildContext context, VoidCallback action) {
+        return Text('Close', key: closedBuilderKey);
+      },
+      openBuilder:
+          (BuildContext context, CloseContainerActionCallback<bool> action) {
+        return const Text('Open');
+      },
+    );
+
+    await tester.pumpWidget(
+      _boilerplate(child: openContainer),
+    );
+
+    final Finder closedBuilderMaterial = find
+        .ancestor(
+          of: find.byKey(closedBuilderKey),
+          matching: find.byType(Material),
+        )
+        .first;
+
+    final Material material = tester.widget<Material>(closedBuilderMaterial);
+    expect(material.clipBehavior, Clip.antiAlias);
+  });
+
+  testWidgets('closedBuilder has no clip', (WidgetTester tester) async {
+    final GlobalKey closedBuilderKey = GlobalKey();
+    final Widget openContainer = OpenContainer(
+      closedBuilder: (BuildContext context, VoidCallback action) {
+        return Text('Close', key: closedBuilderKey);
+      },
+      openBuilder:
+          (BuildContext context, CloseContainerActionCallback<bool> action) {
+        return const Text('Open');
+      },
+      clipBehavior: Clip.none,
+    );
+
+    await tester.pumpWidget(
+      _boilerplate(child: openContainer),
+    );
+
+    final Finder closedBuilderMaterial = find
+        .ancestor(
+          of: find.byKey(closedBuilderKey),
+          matching: find.byType(Material),
+        )
+        .first;
+
+    final Material material = tester.widget<Material>(closedBuilderMaterial);
+    expect(material.clipBehavior, Clip.none);
+  });
+
+  Widget _createRootNavigatorTest({
+    required Key appKey,
+    required Key nestedNavigatorKey,
+    required bool useRootNavigator,
+  }) {
+    return Center(
+      child: SizedBox(
+        width: 100,
+        height: 100,
+        child: MaterialApp(
+          key: appKey,
+          // a nested navigator
+          home: Center(
+            child: SizedBox(
+              width: 50,
+              height: 50,
+              child: Navigator(
+                key: nestedNavigatorKey,
+                onGenerateRoute: (RouteSettings route) {
+                  return MaterialPageRoute<dynamic>(
+                    settings: route,
+                    builder: (BuildContext context) {
+                      return OpenContainer(
+                        useRootNavigator: useRootNavigator,
+                        closedBuilder: (BuildContext context, _) {
+                          return const Text('Closed');
+                        },
+                        openBuilder: (BuildContext context, _) {
+                          return const Text('Opened');
+                        },
+                      );
+                    },
+                  );
+                },
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  testWidgets(
+      'Verify that "useRootNavigator: false" uses the correct navigator',
+      (WidgetTester tester) async {
+    const Key appKey = Key('App');
+    const Key nestedNavigatorKey = Key('Nested Navigator');
+
+    await tester.pumpWidget(_createRootNavigatorTest(
+        appKey: appKey,
+        nestedNavigatorKey: nestedNavigatorKey,
+        useRootNavigator: false));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(
+        find.descendant(of: find.byKey(appKey), matching: find.text('Opened')),
+        findsOneWidget);
+
+    expect(
+        find.descendant(
+            of: find.byKey(nestedNavigatorKey), matching: find.text('Opened')),
+        findsOneWidget);
+  });
+
+  testWidgets('Verify that "useRootNavigator: true" uses the correct navigator',
+      (WidgetTester tester) async {
+    const Key appKey = Key('App');
+    const Key nestedNavigatorKey = Key('Nested Navigator');
+
+    await tester.pumpWidget(_createRootNavigatorTest(
+        appKey: appKey,
+        nestedNavigatorKey: nestedNavigatorKey,
+        useRootNavigator: true));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(
+        find.descendant(of: find.byKey(appKey), matching: find.text('Opened')),
+        findsOneWidget);
+
+    expect(
+        find.descendant(
+            of: find.byKey(nestedNavigatorKey), matching: find.text('Opened')),
+        findsNothing);
+  });
+
+  testWidgets('Verify correct opened size  when "useRootNavigator: false"',
+      (WidgetTester tester) async {
+    const Key appKey = Key('App');
+    const Key nestedNavigatorKey = Key('Nested Navigator');
+
+    await tester.pumpWidget(_createRootNavigatorTest(
+        appKey: appKey,
+        nestedNavigatorKey: nestedNavigatorKey,
+        useRootNavigator: false));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(tester.getSize(find.text('Opened')),
+        equals(tester.getSize(find.byKey(nestedNavigatorKey))));
+  });
+
+  testWidgets('Verify correct opened size  when "useRootNavigator: true"',
+      (WidgetTester tester) async {
+    const Key appKey = Key('App');
+    const Key nestedNavigatorKey = Key('Nested Navigator');
+
+    await tester.pumpWidget(_createRootNavigatorTest(
+        appKey: appKey,
+        nestedNavigatorKey: nestedNavigatorKey,
+        useRootNavigator: true));
+
+    await tester.tap(find.text('Closed'));
+    await tester.pumpAndSettle();
+
+    expect(tester.getSize(find.text('Opened')),
+        equals(tester.getSize(find.byKey(appKey))));
+  });
+
+  testWidgets(
+    'Verify routeSettings passed to Navigator',
+    (WidgetTester tester) async {
+      const RouteSettings routeSettings = RouteSettings(
+        name: 'route-name',
+        arguments: 'arguments',
+      );
+
+      final Widget openContainer = OpenContainer(
+        routeSettings: routeSettings,
+        closedBuilder: (BuildContext context, VoidCallback action) {
+          return GestureDetector(
+            onTap: action,
+            child: const Text('Closed'),
+          );
+        },
+        openBuilder: (BuildContext context, VoidCallback action) {
+          return GestureDetector(
+            onTap: action,
+            child: const Text('Open'),
+          );
+        },
+      );
+
+      await tester.pumpWidget(_boilerplate(child: openContainer));
+
+      // Open the container
+      await tester.tap(find.text('Closed'));
+      await tester.pumpAndSettle();
+
+      // Expect the last route pushed to the navigator to contain RouteSettings
+      // equal to the RouteSettings passed to the OpenContainer
+      final ModalRoute<dynamic> modalRoute = ModalRoute.of(
+        tester.element(find.text('Open')),
+      )!;
+      expect(modalRoute.settings, routeSettings);
+    },
+  );
+
+  // Regression test for https://github.com/flutter/flutter/issues/72238.
+  testWidgets(
+    'OpenContainer\'s source widget is visible in closed container route if '
+    'open container route is pushed from not using the OpenContainer itself',
+    (WidgetTester tester) async {
+      final Widget openContainer = OpenContainer(
+        closedBuilder: (BuildContext context, VoidCallback action) {
+          return GestureDetector(
+            onTap: action,
+            child: const Text('Closed'),
+          );
+        },
+        openBuilder: (BuildContext context, VoidCallback action) {
+          return GestureDetector(
+            onTap: action,
+            child: const Text('Open'),
+          );
+        },
+      );
+      await tester.pumpWidget(_boilerplate(child: openContainer));
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+
+      // Open the container
+      await tester.tap(find.text('Closed'));
+      await tester.pumpAndSettle();
+
+      final Element container = tester.element(
+        find.byType(OpenContainer, skipOffstage: false),
+      );
+      // Replace the open container route.
+      Navigator.pushReplacement<void, void>(container,
+          MaterialPageRoute<void>(builder: (_) => const Placeholder()));
+      await tester.pumpAndSettle();
+      // Go back to the main page and verify the closed builder is showed.
+      Navigator.pop(container);
+      await tester.pumpAndSettle();
+
+      expect(_getOpacity(tester, 'Closed'), 1.0);
+    },
+  );
+}
+
+Color _getScrimColor(WidgetTester tester) {
+  return tester.widget<ColoredBox>(find.byType(ColoredBox)).color;
+}
+
+void _expectMaterialPropertiesHaveAdvanced({
+  required _TrackedData biggerMaterial,
+  required _TrackedData smallerMaterial,
+  required WidgetTester tester,
+}) {
+  expect(
+    biggerMaterial.material.elevation,
+    greaterThan(smallerMaterial.material.elevation),
+  );
+  expect(biggerMaterial.radius, lessThan(smallerMaterial.radius));
+  expect(biggerMaterial.rect.height, greaterThan(smallerMaterial.rect.height));
+  expect(biggerMaterial.rect.width, greaterThan(smallerMaterial.rect.width));
+  expect(biggerMaterial.rect.top, lessThan(smallerMaterial.rect.top));
+  expect(biggerMaterial.rect.left, lessThan(smallerMaterial.rect.left));
+}
+
+double _getOpacity(WidgetTester tester, String label) {
+  final Opacity widget = tester.firstWidget(find.ancestor(
+    of: find.text(label),
+    matching: find.byType(Opacity),
+  ));
+  return widget.opacity;
+}
+
+class _TrackedData {
+  _TrackedData(this.material, this.rect);
+
+  final Material material;
+  final Rect rect;
+
+  double get radius => _getRadius(material);
+}
+
+double _getRadius(Material material) {
+  final RoundedRectangleBorder? shape =
+      material.shape as RoundedRectangleBorder?;
+  if (shape == null) {
+    return 0.0;
+  }
+  final BorderRadius radius = shape.borderRadius as BorderRadius;
+  return radius.topRight.x;
+}
+
+Widget _boilerplate({required Widget child}) {
+  return MaterialApp(
+    home: Scaffold(
+      body: child,
+    ),
+  );
+}
+
+class _SizableContainer extends StatefulWidget {
+  const _SizableContainer({required this.initialSize, required this.child});
+
+  final double initialSize;
+  final Widget child;
+
+  @override
+  State<_SizableContainer> createState() => _SizableContainerState();
+}
+
+class _SizableContainerState extends State<_SizableContainer> {
+  @override
+  void initState() {
+    super.initState();
+    _size = widget.initialSize;
+  }
+
+  double get size => _size;
+  late double _size;
+  set size(double value) {
+    if (value == _size) {
+      return;
+    }
+    setState(() {
+      _size = value;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: size,
+      width: size,
+      child: widget.child,
+    );
+  }
+}
+
+class _RemoveOpenContainerExample extends StatefulWidget {
+  @override
+  __RemoveOpenContainerExampleState createState() =>
+      __RemoveOpenContainerExampleState();
+}
+
+class __RemoveOpenContainerExampleState
+    extends State<_RemoveOpenContainerExample> {
+  bool removeOpenContainerWidget = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return removeOpenContainerWidget
+        ? const Text('Container has been removed')
+        : OpenContainer(
+            closedBuilder: (BuildContext context, VoidCallback action) =>
+                Column(
+              children: <Widget>[
+                const Text('Closed'),
+                ElevatedButton(
+                  onPressed: action,
+                  child: const Text('Open the container'),
+                ),
+              ],
+            ),
+            openBuilder: (BuildContext context, VoidCallback action) => Column(
+              children: <Widget>[
+                const Text('Open'),
+                ElevatedButton(
+                  onPressed: action,
+                  child: const Text('Close the container'),
+                ),
+                ElevatedButton(
+                    onPressed: () {
+                      setState(() {
+                        removeOpenContainerWidget = true;
+                      });
+                    },
+                    child: const Text('Remove the container')),
+              ],
+            ),
+          );
+  }
+}
+
+class DummyStatefulWidget extends StatefulWidget {
+  const DummyStatefulWidget({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => DummyState();
+}
+
+class DummyState extends State<DummyStatefulWidget> {
+  @override
+  Widget build(BuildContext context) => const SizedBox.expand();
+}
diff --git a/packages/animations/test/page_transition_switcher_test.dart b/packages/animations/test/page_transition_switcher_test.dart
new file mode 100644
index 0000000..ad3965e
--- /dev/null
+++ b/packages/animations/test/page_transition_switcher_test.dart
@@ -0,0 +1,647 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:animations/animations.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('transitions in a new child.', (WidgetTester tester) async {
+    final UniqueKey containerOne = UniqueKey();
+    final UniqueKey containerTwo = UniqueKey();
+    final UniqueKey containerThree = UniqueKey();
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerOne, color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    Map<Key, double> _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne], tester);
+    Map<Key, double> _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne], tester);
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerTwo, color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 40));
+
+    _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+    _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+    // Secondary is running for outgoing widget.
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4));
+    // Primary is running for incoming widget.
+    expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4));
+    expect(_secondaryAnimation[containerTwo], equals(0.0));
+
+    // Container one is underneath container two
+    final Container container = tester.firstWidget(find.byType(Container));
+    expect(container.key, containerOne);
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerThree, color: const Color(0xffff0000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 20));
+
+    _primaryAnimation = _getPrimaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    _secondaryAnimation = _getSecondaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], equals(0.6));
+    expect(_primaryAnimation[containerTwo], equals(0.6));
+    expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.2));
+    expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2));
+    expect(_secondaryAnimation[containerThree], equals(0.0));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets('transitions in a new child in reverse.',
+      (WidgetTester tester) async {
+    final UniqueKey containerOne = UniqueKey();
+    final UniqueKey containerTwo = UniqueKey();
+    final UniqueKey containerThree = UniqueKey();
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerOne, color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+
+    Map<Key, double> _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne], tester);
+    Map<Key, double> _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne], tester);
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerTwo, color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 40));
+
+    _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+    _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+    // Primary is running forward for outgoing widget.
+    expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+    // Secondary is running forward for incoming widget.
+    expect(_primaryAnimation[containerTwo], equals(1.0));
+    expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6));
+
+    // Container two two is underneath container one.
+    final Container container = tester.firstWidget(find.byType(Container));
+    expect(container.key, containerTwo);
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerThree, color: const Color(0xffff0000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 20));
+
+    _primaryAnimation = _getPrimaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    _secondaryAnimation = _getSecondaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    expect(_primaryAnimation[containerOne], equals(0.4));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+    expect(_primaryAnimation[containerTwo], equals(0.8));
+    expect(_secondaryAnimation[containerTwo], equals(0.4));
+    expect(_primaryAnimation[containerThree], equals(1.0));
+    expect(_secondaryAnimation[containerThree], equals(0.8));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets('switch from forward to reverse', (WidgetTester tester) async {
+    final UniqueKey containerOne = UniqueKey();
+    final UniqueKey containerTwo = UniqueKey();
+    final UniqueKey containerThree = UniqueKey();
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerOne, color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    Map<Key, double> _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne], tester);
+    Map<Key, double> _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne], tester);
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerTwo, color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 40));
+
+    _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+    _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+    expect(_secondaryAnimation[containerOne], moreOrLessEquals(0.4));
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerTwo], equals(0.0));
+    expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.4));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerThree, color: const Color(0xffff0000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 20));
+
+    _primaryAnimation = _getPrimaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    _secondaryAnimation = _getSecondaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    expect(_secondaryAnimation[containerOne], equals(0.6));
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerTwo], equals(0.0));
+    expect(_primaryAnimation[containerTwo], moreOrLessEquals(0.2));
+    expect(_secondaryAnimation[containerThree], equals(0.8));
+    expect(_primaryAnimation[containerThree], equals(1.0));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets('switch from reverse to forward.', (WidgetTester tester) async {
+    final UniqueKey containerOne = UniqueKey();
+    final UniqueKey containerTwo = UniqueKey();
+    final UniqueKey containerThree = UniqueKey();
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerOne, color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+
+    Map<Key, double> _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne], tester);
+    Map<Key, double> _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne], tester);
+    expect(_primaryAnimation[containerOne], equals(1.0));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerTwo, color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: true,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 40));
+
+    _primaryAnimation =
+        _getPrimaryAnimation(<Key>[containerOne, containerTwo], tester);
+    _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[containerOne, containerTwo], tester);
+    // Primary is running in reverse for outgoing widget.
+    expect(_primaryAnimation[containerOne], moreOrLessEquals(0.6));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+    // Secondary is running in reverse for incoming widget.
+    expect(_primaryAnimation[containerTwo], equals(1.0));
+    expect(_secondaryAnimation[containerTwo], moreOrLessEquals(0.6));
+
+    // Container two is underneath container one.
+    final Container container = tester.firstWidget(find.byType(Container));
+    expect(container.key, containerTwo);
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: containerThree, color: const Color(0xffff0000)),
+        transitionBuilder: _transitionBuilder,
+        reverse: false,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 20));
+
+    // Container one is expected to continue running its primary animation in
+    // reverse since it is exiting. Container two's secondary animation switches
+    // from running its secondary animation in reverse to running forwards since
+    // it should now be exiting underneath container three. Container three's
+    // primary animation should be running forwards since it is entering above
+    // container two.
+    _primaryAnimation = _getPrimaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    _secondaryAnimation = _getSecondaryAnimation(
+        <Key>[containerOne, containerTwo, containerThree], tester);
+    expect(_primaryAnimation[containerOne], equals(0.4));
+    expect(_secondaryAnimation[containerOne], equals(0.0));
+    expect(_primaryAnimation[containerTwo], equals(1.0));
+    expect(_secondaryAnimation[containerTwo], equals(0.8));
+    expect(_primaryAnimation[containerThree], moreOrLessEquals(0.2));
+    expect(_secondaryAnimation[containerThree], equals(0.0));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets('using custom layout', (WidgetTester tester) async {
+    Widget newLayoutBuilder(List<Widget> activeEntries) {
+      return Column(
+        children: activeEntries,
+      );
+    }
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+        layoutBuilder: newLayoutBuilder,
+      ),
+    );
+
+    expect(find.byType(Column), findsOneWidget);
+  });
+
+  testWidgets("doesn't transition in a new child of the same type.",
+      (WidgetTester tester) async {
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(color: const Color(0x00000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    expect(find.byType(FadeTransition), findsOneWidget);
+    expect(find.byType(ScaleTransition), findsOneWidget);
+    FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+    ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(1.0));
+    expect(scale.scale.value, equals(1.0));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 50));
+    expect(find.byType(FadeTransition), findsOneWidget);
+    expect(find.byType(ScaleTransition), findsOneWidget);
+    fade = tester.firstWidget(find.byType(FadeTransition));
+    scale = tester.firstWidget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(1.0));
+    expect(scale.scale.value, equals(1.0));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets('handles null children.', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const PageTransitionSwitcher(
+        duration: Duration(milliseconds: 100),
+        child: null,
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    expect(find.byType(FadeTransition), findsNothing);
+    expect(find.byType(ScaleTransition), findsNothing);
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 40));
+    expect(find.byType(FadeTransition), findsOneWidget);
+    expect(find.byType(ScaleTransition), findsOneWidget);
+    FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+    ScaleTransition scale = tester.firstWidget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(1.0));
+    expect(scale.scale.value, moreOrLessEquals(0.4));
+    await tester.pumpAndSettle(); // finish transitions.
+
+    await tester.pumpWidget(
+      const PageTransitionSwitcher(
+        duration: Duration(milliseconds: 100),
+        child: null,
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 50));
+    expect(find.byType(FadeTransition), findsOneWidget);
+    expect(find.byType(ScaleTransition), findsOneWidget);
+    fade = tester.firstWidget(find.byType(FadeTransition));
+    scale = tester.firstWidget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(0.5));
+    expect(scale.scale.value, equals(1.0));
+    await tester.pumpAndSettle();
+  });
+
+  testWidgets("doesn't start any animations after dispose.",
+      (WidgetTester tester) async {
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: UniqueKey(), color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: Container(key: UniqueKey(), color: const Color(0xff000000)),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+    await tester.pump(const Duration(milliseconds: 50));
+
+    expect(find.byType(FadeTransition), findsNWidgets(2));
+    expect(find.byType(ScaleTransition), findsNWidgets(2));
+    final FadeTransition fade = tester.firstWidget(find.byType(FadeTransition));
+    final ScaleTransition scale =
+        tester.firstWidget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(0.5));
+    expect(scale.scale.value, equals(1.0));
+
+    // Change the widget tree in the middle of the animation.
+    await tester.pumpWidget(Container(color: const Color(0xffff0000)));
+    expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)),
+        equals(1));
+  });
+
+  testWidgets("doesn't reset state of the children in transitions.",
+      (WidgetTester tester) async {
+    final UniqueKey statefulOne = UniqueKey();
+    final UniqueKey statefulTwo = UniqueKey();
+    final UniqueKey statefulThree = UniqueKey();
+
+    StatefulTestWidgetState.generation = 0;
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: StatefulTestWidget(key: statefulOne),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    Map<Key, double> _primaryAnimation =
+        _getPrimaryAnimation(<Key>[statefulOne], tester);
+    Map<Key, double> _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[statefulOne], tester);
+    expect(_primaryAnimation[statefulOne], equals(1.0));
+    expect(_secondaryAnimation[statefulOne], equals(0.0));
+    expect(StatefulTestWidgetState.generation, equals(1));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: StatefulTestWidget(key: statefulTwo),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 50));
+    expect(find.byType(FadeTransition), findsNWidgets(2));
+    _primaryAnimation =
+        _getPrimaryAnimation(<Key>[statefulOne, statefulTwo], tester);
+    _secondaryAnimation =
+        _getSecondaryAnimation(<Key>[statefulOne, statefulTwo], tester);
+    expect(_primaryAnimation[statefulTwo], equals(0.5));
+    expect(_secondaryAnimation[statefulTwo], equals(0.0));
+    expect(StatefulTestWidgetState.generation, equals(2));
+
+    await tester.pumpWidget(
+      PageTransitionSwitcher(
+        duration: const Duration(milliseconds: 100),
+        child: StatefulTestWidget(key: statefulThree),
+        transitionBuilder: _transitionBuilder,
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 10));
+    expect(StatefulTestWidgetState.generation, equals(3));
+    await tester.pumpAndSettle();
+    expect(StatefulTestWidgetState.generation, equals(3));
+  });
+
+  testWidgets('updates widgets without animating if they are isomorphic.',
+      (WidgetTester tester) async {
+    Future<void> pumpChild(Widget child) async {
+      return tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.rtl,
+          child: PageTransitionSwitcher(
+            duration: const Duration(milliseconds: 100),
+            child: child,
+            transitionBuilder: _transitionBuilder,
+          ),
+        ),
+      );
+    }
+
+    await pumpChild(const Text('1'));
+    await tester.pump(const Duration(milliseconds: 10));
+    FadeTransition fade = tester.widget(find.byType(FadeTransition));
+    ScaleTransition scale = tester.widget(find.byType(ScaleTransition));
+    expect(fade.opacity.value, equals(1.0));
+    expect(scale.scale.value, equals(1.0));
+    expect(find.text('1'), findsOneWidget);
+    expect(find.text('2'), findsNothing);
+    await pumpChild(const Text('2'));
+    fade = tester.widget(find.byType(FadeTransition));
+    scale = tester.widget(find.byType(ScaleTransition));
+    await tester.pump(const Duration(milliseconds: 20));
+    expect(fade.opacity.value, equals(1.0));
+    expect(scale.scale.value, equals(1.0));
+    expect(find.text('1'), findsNothing);
+    expect(find.text('2'), findsOneWidget);
+  });
+
+  testWidgets(
+      'updates previous child transitions if the transitionBuilder changes.',
+      (WidgetTester tester) async {
+    final UniqueKey containerOne = UniqueKey();
+    final UniqueKey containerTwo = UniqueKey();
+    final UniqueKey containerThree = UniqueKey();
+
+    // Insert three unique children so that we have some previous children.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: PageTransitionSwitcher(
+          duration: const Duration(milliseconds: 100),
+          child: Container(key: containerOne, color: const Color(0xFFFF0000)),
+          transitionBuilder: _transitionBuilder,
+        ),
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 10));
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: PageTransitionSwitcher(
+          duration: const Duration(milliseconds: 100),
+          child: Container(key: containerTwo, color: const Color(0xFF00FF00)),
+          transitionBuilder: _transitionBuilder,
+        ),
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 10));
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: PageTransitionSwitcher(
+          duration: const Duration(milliseconds: 100),
+          child: Container(key: containerThree, color: const Color(0xFF0000FF)),
+          transitionBuilder: _transitionBuilder,
+        ),
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 10));
+
+    expect(find.byType(FadeTransition), findsNWidgets(3));
+    expect(find.byType(ScaleTransition), findsNWidgets(3));
+    expect(find.byType(SlideTransition), findsNothing);
+    expect(find.byType(SizeTransition), findsNothing);
+
+    Widget newTransitionBuilder(
+        Widget child, Animation<double> primary, Animation<double> secondary) {
+      return SlideTransition(
+        position: Tween<Offset>(begin: Offset.zero, end: const Offset(20, 30))
+            .animate(primary),
+        child: SizeTransition(
+          sizeFactor: Tween<double>(begin: 10, end: 0.0).animate(secondary),
+          child: child,
+        ),
+      );
+    }
+
+    // Now set a new transition builder and make sure all the previous
+    // transitions are replaced.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: PageTransitionSwitcher(
+          duration: const Duration(milliseconds: 100),
+          child: Container(key: containerThree, color: const Color(0x00000000)),
+          transitionBuilder: newTransitionBuilder,
+        ),
+      ),
+    );
+
+    await tester.pump(const Duration(milliseconds: 10));
+
+    expect(find.byType(FadeTransition), findsNothing);
+    expect(find.byType(ScaleTransition), findsNothing);
+    expect(find.byType(SlideTransition), findsNWidgets(3));
+    expect(find.byType(SizeTransition), findsNWidgets(3));
+  });
+}
+
+class StatefulTestWidget extends StatefulWidget {
+  const StatefulTestWidget({Key? key}) : super(key: key);
+
+  @override
+  StatefulTestWidgetState createState() => StatefulTestWidgetState();
+}
+
+class StatefulTestWidgetState extends State<StatefulTestWidget> {
+  StatefulTestWidgetState();
+  static int generation = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    generation++;
+  }
+
+  @override
+  Widget build(BuildContext context) => Container();
+}
+
+Widget _transitionBuilder(
+    Widget child, Animation<double> primary, Animation<double> secondary) {
+  return ScaleTransition(
+    scale: Tween<double>(begin: 0.0, end: 1.0).animate(primary),
+    child: FadeTransition(
+      opacity: Tween<double>(begin: 1.0, end: 0.0).animate(secondary),
+      child: child,
+    ),
+  );
+}
+
+Map<Key, double> _getSecondaryAnimation(List<Key> keys, WidgetTester tester) {
+  expect(find.byType(FadeTransition), findsNWidgets(keys.length));
+  final Map<Key, double> result = <Key, double>{};
+  for (final Key key in keys) {
+    final FadeTransition transition = tester.firstWidget(
+      find.ancestor(
+        of: find.byKey(key),
+        matching: find.byType(FadeTransition),
+      ),
+    );
+    result[key] = 1.0 - transition.opacity.value;
+  }
+  return result;
+}
+
+Map<Key, double> _getPrimaryAnimation(List<Key> keys, WidgetTester tester) {
+  expect(find.byType(ScaleTransition), findsNWidgets(keys.length));
+  final Map<Key, double> result = <Key, double>{};
+  for (final Key key in keys) {
+    final ScaleTransition transition = tester.firstWidget(
+      find.ancestor(
+        of: find.byKey(key),
+        matching: find.byType(ScaleTransition),
+      ),
+    );
+    result[key] = transition.scale.value;
+  }
+  return result;
+}
diff --git a/packages/animations/test/shared_axis_transition_test.dart b/packages/animations/test/shared_axis_transition_test.dart
new file mode 100644
index 0000000..03ac704
--- /dev/null
+++ b/packages/animations/test/shared_axis_transition_test.dart
@@ -0,0 +1,1951 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:animations/src/shared_axis_transition.dart';
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+import 'package:vector_math/vector_math_64.dart' hide Colors;
+
+void main() {
+  group('SharedAxisTransitionType.horizontal', () {
+    testWidgets(
+      'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+      (WidgetTester tester) async {
+        final AnimationController animation = AnimationController(
+          vsync: const TestVSync(),
+        );
+        final AnimationController secondaryAnimation = AnimationController(
+          vsync: const TestVSync(),
+        );
+
+        await tester.pumpWidget(
+          const SharedAxisPageTransitionsBuilder(
+            transitionType: SharedAxisTransitionType.horizontal,
+          ).buildTransitions<void>(
+            null,
+            null,
+            animation,
+            secondaryAnimation,
+            const Placeholder(),
+          ),
+        );
+
+        expect(find.byType(SharedAxisTransition), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs forward',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.horizontal,
+          ),
+        );
+
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        // Bottom route is not offset and fully visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        // Top route is offset to the right by 30.0 pixels
+        // and not visible yet.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Bottom route is now invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is still invisible, but moving towards the left.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        double? topOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        expect(topOffset, lessThan(30.0));
+        expect(topOffset, greaterThan(0.0));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Bottom route is still invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is fading in
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), greaterThan(0));
+        expect(_getOpacity(topRoute, tester), lessThan(1.0));
+        topOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        expect(topOffset, greaterThan(0.0));
+        expect(topOffset, lessThan(30.0));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Bottom route is not visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          -30.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route has no offset and is visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(bottomRoute), findsNothing);
+        expect(find.text(topRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs in reverse',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.horizontal,
+          ),
+        );
+
+        navigator.currentState!.pushNamed('/a');
+        await tester.pumpAndSettle();
+
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+        expect(find.text(bottomRoute), findsNothing);
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Top route is is not offset and fully visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+        // Bottom route is offset to the left and is not visible yet.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          -30.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Top route is now invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is still invisible, but moving towards the right.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester),
+            moreOrLessEquals(0, epsilon: 0.005));
+        double? bottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        expect(bottomOffset, lessThan(0.0));
+        expect(bottomOffset, greaterThan(-30.0));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Top route is still invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is fading in
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+        bottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        expect(bottomOffset, lessThan(0.0));
+        expect(bottomOffset, greaterThan(-30.0));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Top route is not visible and is offset to the right.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is not offset and is visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition does not jump when interrupted',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.horizontal,
+          ),
+        );
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+
+        // Jump to halfway point of transition.
+        await tester.pump(const Duration(milliseconds: 150));
+        // Bottom route is fully faded out.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        final double halfwayBottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        expect(halfwayBottomOffset, lessThan(0.0));
+        expect(halfwayBottomOffset, greaterThan(-30.0));
+
+        // Top route is fading/coming in.
+        expect(find.text(topRoute), findsOneWidget);
+        final double halfwayTopOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.horizontal,
+        );
+        final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+        expect(halfwayTopOffset, greaterThan(0.0));
+        expect(halfwayTopOffset, lessThan(30.0));
+        expect(halfwayTopOpacity, greaterThan(0.0));
+        expect(halfwayTopOpacity, lessThan(1.0));
+
+        // Interrupt the transition with a pop.
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Nothing should change.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          halfwayBottomOffset,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          halfwayTopOffset,
+        );
+        expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+        // Jump to the 1/4 (75 ms) point of transition
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          lessThan(0.0),
+        );
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          greaterThan(-30.0),
+        );
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          greaterThan(halfwayBottomOffset),
+        );
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+        // Jump to the end.
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.horizontal,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'State is not lost when transitioning',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            contentBuilder: (RouteSettings settings) {
+              return _StatefulTestWidget(
+                key: ValueKey<String?>(settings.name),
+                name: settings.name!,
+              );
+            },
+            transitionType: SharedAxisTransitionType.horizontal,
+          ),
+        );
+
+        final _StatefulTestWidgetState bottomState = tester.state(
+          find.byKey(const ValueKey<String?>(bottomRoute)),
+        );
+        expect(bottomState.widget.name, bottomRoute);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        final _StatefulTestWidgetState topState = tester.state(
+          find.byKey(const ValueKey<String?>(topRoute)),
+        );
+        expect(topState.widget.name, topRoute);
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(
+            const ValueKey<String?>(bottomRoute),
+            skipOffstage: false,
+          )),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+      },
+    );
+
+    testWidgets('default fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      // The default fill color should be derived from ThemeData.canvasColor.
+      final Color defaultFillColor = ThemeData().canvasColor;
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          transitionType: SharedAxisTransitionType.horizontal,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+    });
+
+    testWidgets('custom fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          fillColor: Colors.green,
+          transitionType: SharedAxisTransitionType.horizontal,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+    });
+
+    testWidgets('should keep state', (WidgetTester tester) async {
+      final AnimationController animation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      final AnimationController secondaryAnimation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      await tester.pumpWidget(Directionality(
+        textDirection: TextDirection.ltr,
+        child: Center(
+          child: SharedAxisTransition(
+            transitionType: SharedAxisTransitionType.horizontal,
+            child: const _StatefulTestWidget(name: 'Foo'),
+            animation: animation,
+            secondaryAnimation: secondaryAnimation,
+          ),
+        ),
+      ));
+      final State<StatefulWidget> state = tester.state(
+        find.byType(_StatefulTestWidget),
+      );
+      expect(state, isNotNull);
+
+      animation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      animation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    });
+  });
+
+  group('SharedAxisTransitionType.vertical', () {
+    testWidgets(
+      'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+      (WidgetTester tester) async {
+        final AnimationController animation = AnimationController(
+          vsync: const TestVSync(),
+        );
+        final AnimationController secondaryAnimation = AnimationController(
+          vsync: const TestVSync(),
+        );
+
+        await tester.pumpWidget(
+          const SharedAxisPageTransitionsBuilder(
+            transitionType: SharedAxisTransitionType.vertical,
+          ).buildTransitions<void>(
+            null,
+            null,
+            animation,
+            secondaryAnimation,
+            const Placeholder(),
+          ),
+        );
+
+        expect(find.byType(SharedAxisTransition), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs forward',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.vertical,
+          ),
+        );
+
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        // Bottom route is not offset and fully visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        // Top route is offset down by 30.0 pixels
+        // and not visible yet.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Bottom route is now invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is still invisible, but moving up.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        double? topOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        expect(topOffset, lessThan(30.0));
+        expect(topOffset, greaterThan(0.0));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Bottom route is still invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is fading in
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), greaterThan(0));
+        expect(_getOpacity(topRoute, tester), lessThan(1.0));
+        topOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        expect(topOffset, greaterThan(0.0));
+        expect(topOffset, lessThan(30.0));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Bottom route is not visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          -30.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route has no offset and is visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(bottomRoute), findsNothing);
+        expect(find.text(topRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs in reverse',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.vertical,
+          ),
+        );
+
+        navigator.currentState!.pushNamed('/a');
+        await tester.pumpAndSettle();
+
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+        expect(find.text(bottomRoute), findsNothing);
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Top route is is not offset and fully visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(topRoute, tester), 1.0);
+        // Bottom route is offset up and is not visible yet.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          -30.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Top route is now invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is still invisible, but moving down.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getOpacity(bottomRoute, tester),
+          moreOrLessEquals(0, epsilon: 0.005),
+        );
+        double? bottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        expect(bottomOffset, lessThan(0.0));
+        expect(bottomOffset, greaterThan(-30.0));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Top route is still invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is fading in
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+        bottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        expect(bottomOffset, lessThan(0.0));
+        expect(bottomOffset, greaterThan(-30.0));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Top route is not visible and is offset down.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is not offset and is visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition does not jump when interrupted',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.vertical,
+          ),
+        );
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+
+        // Jump to halfway point of transition.
+        await tester.pump(const Duration(milliseconds: 150));
+        // Bottom route is fully faded out.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        final double halfwayBottomOffset = _getTranslationOffset(
+          bottomRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        expect(halfwayBottomOffset, lessThan(0.0));
+        expect(halfwayBottomOffset, greaterThan(-30.0));
+
+        // Top route is fading/coming in.
+        expect(find.text(topRoute), findsOneWidget);
+        final double halfwayTopOffset = _getTranslationOffset(
+          topRoute,
+          tester,
+          SharedAxisTransitionType.vertical,
+        );
+        final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+        expect(halfwayTopOffset, greaterThan(0.0));
+        expect(halfwayTopOffset, lessThan(30.0));
+        expect(halfwayTopOpacity, greaterThan(0.0));
+        expect(halfwayTopOpacity, lessThan(1.0));
+
+        // Interrupt the transition with a pop.
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Nothing should change.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          halfwayBottomOffset,
+        );
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          halfwayTopOffset,
+        );
+        expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+        // Jump to the 1/4 (75 ms) point of transition
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          lessThan(0.0),
+        );
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          greaterThan(-30.0),
+        );
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          greaterThan(halfwayBottomOffset),
+        );
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+        // Jump to the end.
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            bottomRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          0.0,
+        );
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(
+          _getTranslationOffset(
+            topRoute,
+            tester,
+            SharedAxisTransitionType.vertical,
+          ),
+          30.0,
+        );
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'State is not lost when transitioning',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            contentBuilder: (RouteSettings settings) {
+              return _StatefulTestWidget(
+                key: ValueKey<String?>(settings.name),
+                name: settings.name!,
+              );
+            },
+            transitionType: SharedAxisTransitionType.vertical,
+          ),
+        );
+
+        final _StatefulTestWidgetState bottomState = tester.state(
+          find.byKey(const ValueKey<String?>(bottomRoute)),
+        );
+        expect(bottomState.widget.name, bottomRoute);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        final _StatefulTestWidgetState topState = tester.state(
+          find.byKey(const ValueKey<String?>(topRoute)),
+        );
+        expect(topState.widget.name, topRoute);
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(
+            const ValueKey<String?>(bottomRoute),
+            skipOffstage: false,
+          )),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+      },
+    );
+
+    testWidgets('default fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      // The default fill color should be derived from ThemeData.canvasColor.
+      final Color defaultFillColor = ThemeData().canvasColor;
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          transitionType: SharedAxisTransitionType.vertical,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+    });
+
+    testWidgets('custom fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          fillColor: Colors.green,
+          transitionType: SharedAxisTransitionType.vertical,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+    });
+
+    testWidgets('should keep state', (WidgetTester tester) async {
+      final AnimationController animation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      final AnimationController secondaryAnimation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      await tester.pumpWidget(Directionality(
+        textDirection: TextDirection.ltr,
+        child: Center(
+          child: SharedAxisTransition(
+            transitionType: SharedAxisTransitionType.vertical,
+            child: const _StatefulTestWidget(name: 'Foo'),
+            animation: animation,
+            secondaryAnimation: secondaryAnimation,
+          ),
+        ),
+      ));
+      final State<StatefulWidget> state = tester.state(
+        find.byType(_StatefulTestWidget),
+      );
+      expect(state, isNotNull);
+
+      animation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      animation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    });
+  });
+
+  group('SharedAxisTransitionType.scaled', () {
+    testWidgets(
+      'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition',
+      (WidgetTester tester) async {
+        final AnimationController animation = AnimationController(
+          vsync: const TestVSync(),
+        );
+        final AnimationController secondaryAnimation = AnimationController(
+          vsync: const TestVSync(),
+        );
+
+        await tester.pumpWidget(
+          const SharedAxisPageTransitionsBuilder(
+            transitionType: SharedAxisTransitionType.scaled,
+          ).buildTransitions<void>(
+            null,
+            null,
+            animation,
+            secondaryAnimation,
+            const Placeholder(),
+          ),
+        );
+
+        expect(find.byType(SharedAxisTransition), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs forward',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.scaled,
+          ),
+        );
+
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.0);
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        // Bottom route is full size and fully visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.0);
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        // Top route is at 80% of full size and not visible yet.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 0.8);
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Bottom route is now invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is still invisible, but scaling up.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        double topScale = _getScale(topRoute, tester);
+        expect(topScale, greaterThan(0.8));
+        expect(topScale, lessThan(1.0));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Bottom route is still invisible
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route is fading in
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), greaterThan(0));
+        expect(_getOpacity(topRoute, tester), lessThan(1.0));
+        topScale = _getScale(topRoute, tester);
+        expect(topScale, greaterThan(0.8));
+        expect(topScale, lessThan(1.0));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Bottom route is not visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.1);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        // Top route fully scaled in and visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 1.0);
+        expect(_getOpacity(topRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(bottomRoute), findsNothing);
+        expect(find.text(topRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition runs in reverse',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.scaled,
+          ),
+        );
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pumpAndSettle();
+
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 1.0);
+        expect(_getOpacity(topRoute, tester), 1.0);
+        expect(find.text(bottomRoute), findsNothing);
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Top route is full size and fully visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 1.0);
+        expect(_getOpacity(topRoute, tester), 1.0);
+        // Bottom route is at 110% of full size and not visible yet.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.1);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+
+        // Jump 3/10ths of the way through the transition, bottom route
+        // should be be completely faded out while the top route
+        // is also completely faded out.
+        // Transition time: 300ms, 3/10 * 300ms = 90ms
+        await tester.pump(const Duration(milliseconds: 90));
+
+        // Bottom route is now invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Top route is still invisible, but scaling down.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(
+          _getOpacity(bottomRoute, tester),
+          moreOrLessEquals(0, epsilon: 0.005),
+        );
+        double bottomScale = _getScale(bottomRoute, tester);
+        expect(bottomScale, greaterThan(1.0));
+        expect(bottomScale, lessThan(1.1));
+
+        // Jump to the middle of fading in
+        await tester.pump(const Duration(milliseconds: 90));
+        // Top route is still invisible
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route is fading in
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+        bottomScale = _getScale(bottomRoute, tester);
+        expect(bottomScale, greaterThan(1.0));
+        expect(bottomScale, lessThan(1.1));
+
+        // Jump to the end of the transition
+        await tester.pump(const Duration(milliseconds: 120));
+        // Top route is not visible.
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 0.8);
+        expect(_getOpacity(topRoute, tester), 0.0);
+        // Bottom route fully scaled in and visible.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.0);
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition does not jump when interrupted',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.scaled,
+          ),
+        );
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+
+        // Jump to halfway point of transition.
+        await tester.pump(const Duration(milliseconds: 150));
+        // Bottom route is fully faded out.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        final double halfwayBottomScale = _getScale(bottomRoute, tester);
+        expect(halfwayBottomScale, greaterThan(1.0));
+        expect(halfwayBottomScale, lessThan(1.1));
+
+        // Top route is fading/scaling in.
+        expect(find.text(topRoute), findsOneWidget);
+        final double halfwayTopScale = _getScale(topRoute, tester);
+        final double halfwayTopOpacity = _getOpacity(topRoute, tester);
+        expect(halfwayTopScale, greaterThan(0.8));
+        expect(halfwayTopScale, lessThan(1.0));
+        expect(halfwayTopOpacity, greaterThan(0.0));
+        expect(halfwayTopOpacity, lessThan(1.0));
+
+        // Interrupt the transition with a pop.
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        // Nothing should change.
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), halfwayBottomScale);
+        expect(_getOpacity(bottomRoute, tester), 0.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), halfwayTopScale);
+        expect(_getOpacity(topRoute, tester), halfwayTopOpacity);
+
+        // Jump to the 1/4 (75 ms) point of transition
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), greaterThan(1.0));
+        expect(_getScale(bottomRoute, tester), lessThan(1.1));
+        expect(_getScale(bottomRoute, tester), lessThan(halfwayBottomScale));
+        expect(_getOpacity(bottomRoute, tester), greaterThan(0.0));
+        expect(_getOpacity(bottomRoute, tester), lessThan(1.0));
+
+        // Jump to the end.
+        await tester.pump(const Duration(milliseconds: 75));
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(_getScale(bottomRoute, tester), 1.0);
+        expect(_getOpacity(bottomRoute, tester), 1.0);
+        expect(find.text(topRoute), findsOneWidget);
+        expect(_getScale(topRoute, tester), 0.80);
+        expect(_getOpacity(topRoute, tester), 0.0);
+
+        await tester.pump(const Duration(milliseconds: 1));
+        expect(find.text(topRoute), findsNothing);
+        expect(find.text(bottomRoute), findsOneWidget);
+      },
+    );
+
+    testWidgets(
+      'SharedAxisTransition properly disposes animation',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.scaled,
+          ),
+        );
+        expect(find.text(bottomRoute), findsOneWidget);
+        expect(find.text(topRoute), findsNothing);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+
+        // Jump to halfway point of transition.
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(find.byType(SharedAxisTransition), findsNWidgets(2));
+
+        // Rebuild the app without the transition.
+        await tester.pumpWidget(
+          MaterialApp(
+            navigatorKey: navigator,
+            home: const Material(
+              child: Text('abc'),
+            ),
+          ),
+        );
+        await tester.pump();
+        // Transitions should have been disposed of.
+        expect(find.byType(SharedAxisTransition), findsNothing);
+      },
+    );
+
+    testWidgets(
+      'State is not lost when transitioning',
+      (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+        const String bottomRoute = '/';
+        const String topRoute = '/a';
+
+        await tester.pumpWidget(
+          _TestWidget(
+            navigatorKey: navigator,
+            transitionType: SharedAxisTransitionType.scaled,
+            contentBuilder: (RouteSettings settings) {
+              return _StatefulTestWidget(
+                key: ValueKey<String?>(settings.name),
+                name: settings.name!,
+              );
+            },
+          ),
+        );
+
+        final _StatefulTestWidgetState bottomState = tester.state(
+          find.byKey(const ValueKey<String?>(bottomRoute)),
+        );
+        expect(bottomState.widget.name, bottomRoute);
+
+        navigator.currentState!.pushNamed(topRoute);
+        await tester.pump();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        final _StatefulTestWidgetState topState = tester.state(
+          find.byKey(const ValueKey<String?>(topRoute)),
+        );
+        expect(topState.widget.name, topRoute);
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(
+            const ValueKey<String?>(bottomRoute),
+            skipOffstage: false,
+          )),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        navigator.currentState!.pop();
+        await tester.pump();
+
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pump(const Duration(milliseconds: 150));
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(topRoute))),
+          topState,
+        );
+
+        await tester.pumpAndSettle();
+        expect(
+          tester.state(find.byKey(const ValueKey<String?>(bottomRoute))),
+          bottomState,
+        );
+        expect(find.byKey(const ValueKey<String?>(topRoute)), findsNothing);
+      },
+    );
+
+    testWidgets('default fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      // The default fill color should be derived from ThemeData.canvasColor.
+      final Color defaultFillColor = ThemeData().canvasColor;
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          transitionType: SharedAxisTransitionType.scaled,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color,
+          defaultFillColor);
+    });
+
+    testWidgets('custom fill color', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
+      const String bottomRoute = '/';
+      const String topRoute = '/a';
+
+      await tester.pumpWidget(
+        _TestWidget(
+          navigatorKey: navigator,
+          fillColor: Colors.green,
+          transitionType: SharedAxisTransitionType.scaled,
+        ),
+      );
+
+      expect(find.text(bottomRoute), findsOneWidget);
+      Finder fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+
+      navigator.currentState!.pushNamed(topRoute);
+      await tester.pump();
+      await tester.pumpAndSettle();
+
+      fillContainerFinder = find
+          .ancestor(
+            matching: find.byType(Container),
+            of: find.byKey(const ValueKey<String?>('/a')),
+          )
+          .last;
+      expect(fillContainerFinder, findsOneWidget);
+      expect(tester.widget<Container>(fillContainerFinder).color, Colors.green);
+    });
+
+    testWidgets('should keep state', (WidgetTester tester) async {
+      final AnimationController animation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      final AnimationController secondaryAnimation = AnimationController(
+        vsync: const TestVSync(),
+        duration: const Duration(milliseconds: 300),
+      );
+      await tester.pumpWidget(Directionality(
+        textDirection: TextDirection.ltr,
+        child: Center(
+          child: SharedAxisTransition(
+            transitionType: SharedAxisTransitionType.scaled,
+            child: const _StatefulTestWidget(name: 'Foo'),
+            animation: animation,
+            secondaryAnimation: secondaryAnimation,
+          ),
+        ),
+      ));
+      final State<StatefulWidget> state = tester.state(
+        find.byType(_StatefulTestWidget),
+      );
+      expect(state, isNotNull);
+
+      animation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.forward();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      secondaryAnimation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+
+      animation.reverse();
+      await tester.pump();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pump(const Duration(milliseconds: 150));
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+      await tester.pumpAndSettle();
+      expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
+    });
+  });
+}
+
+double _getOpacity(String key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(ValueKey<String?>(key)),
+    matching: find.byType(FadeTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final FadeTransition transition = widget as FadeTransition;
+    return a * transition.opacity.value;
+  });
+}
+
+double _getTranslationOffset(
+  String key,
+  WidgetTester tester,
+  SharedAxisTransitionType transitionType,
+) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(ValueKey<String?>(key)),
+    matching: find.byType(Transform),
+  );
+
+  switch (transitionType) {
+    case SharedAxisTransitionType.horizontal:
+      return tester.widgetList<Transform>(finder).fold<double>(0.0,
+          (double a, Widget widget) {
+        final Transform transition = widget as Transform;
+        final Vector3 translation = transition.transform.getTranslation();
+        return a + translation.x;
+      });
+    case SharedAxisTransitionType.vertical:
+      return tester.widgetList<Transform>(finder).fold<double>(0.0,
+          (double a, Widget widget) {
+        final Transform transition = widget as Transform;
+        final Vector3 translation = transition.transform.getTranslation();
+        return a + translation.y;
+      });
+    case SharedAxisTransitionType.scaled:
+      assert(
+        false,
+        'SharedAxisTransitionType.scaled does not have a translation offset',
+      );
+      return 0.0;
+  }
+}
+
+double _getScale(String key, WidgetTester tester) {
+  final Finder finder = find.ancestor(
+    of: find.byKey(ValueKey<String?>(key)),
+    matching: find.byType(ScaleTransition),
+  );
+  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
+    final ScaleTransition transition = widget as ScaleTransition;
+    return a * transition.scale.value;
+  });
+}
+
+class _TestWidget extends StatelessWidget {
+  const _TestWidget({
+    required this.navigatorKey,
+    this.contentBuilder,
+    required this.transitionType,
+    this.fillColor,
+  });
+
+  final Key navigatorKey;
+  final _ContentBuilder? contentBuilder;
+  final SharedAxisTransitionType transitionType;
+  final Color? fillColor;
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      navigatorKey: navigatorKey as GlobalKey<NavigatorState>?,
+      theme: ThemeData(
+        platform: TargetPlatform.android,
+        pageTransitionsTheme: PageTransitionsTheme(
+          builders: <TargetPlatform, PageTransitionsBuilder>{
+            TargetPlatform.android: SharedAxisPageTransitionsBuilder(
+              fillColor: fillColor,
+              transitionType: transitionType,
+            ),
+          },
+        ),
+      ),
+      onGenerateRoute: (RouteSettings settings) {
+        return MaterialPageRoute<void>(
+          settings: settings,
+          builder: (BuildContext context) {
+            return contentBuilder != null
+                ? contentBuilder!(settings)
+                : Center(
+                    key: ValueKey<String?>(settings.name),
+                    child: Text(settings.name!),
+                  );
+          },
+        );
+      },
+    );
+  }
+}
+
+class _StatefulTestWidget extends StatefulWidget {
+  const _StatefulTestWidget({Key? key, required this.name}) : super(key: key);
+
+  final String name;
+
+  @override
+  State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
+}
+
+class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
+  @override
+  Widget build(BuildContext context) {
+    return Text(widget.name);
+  }
+}
+
+typedef _ContentBuilder = Widget Function(RouteSettings settings);
diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md
new file mode 100644
index 0000000..960c870
--- /dev/null
+++ b/packages/cross_file/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 0.3.1+1
+
+* Rehomed to `flutter/packages` repository.
+
+## 0.3.1
+
+* Fix nullability of `XFileBase`'s `path` and `name` to match the
+  implementations to avoid potential analyzer issues.
+
+## 0.3.0
+
+* Migrated package to null-safety.
+* **breaking change** According to our unit tests, the API should be backwards-compatible. Some relevant changes were made, however:
+  * Web: `lastModified` returns the epoch time as a default value, to maintain the `Future<DateTime>` return type (and not `null`)
+
+## 0.2.1
+
+* Prepare for breaking `package:http` change.
+
+## 0.2.0
+
+* **breaking change** Make sure the `saveTo` method returns a `Future` so it can be awaited and users are sure the file has been written to disk.
+
+## 0.1.0+2
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 0.1.0+1
+
+* Update Flutter SDK constraint.
+
+## 0.1.0
+
+* Initial open-source release.
diff --git a/packages/cross_file/LICENSE b/packages/cross_file/LICENSE
new file mode 100644
index 0000000..2c91f14
--- /dev/null
+++ b/packages/cross_file/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/cross_file/README.md b/packages/cross_file/README.md
new file mode 100644
index 0000000..65bd418
--- /dev/null
+++ b/packages/cross_file/README.md
@@ -0,0 +1,34 @@
+# cross_file
+
+An abstraction to allow working with files across multiple platforms.
+
+# Usage
+
+Import `package:cross/cross_info.dart`, instantiate a `CrossFile` 
+using a path or byte array and use its methods and properties to 
+access the file and its metadata.
+
+Example:
+
+```dart
+import 'package:cross_file/cross_file.dart';
+
+final file = CrossFile('assets/hello.txt');
+
+print('File information:');
+print('- Path: ${file.path}');
+print('- Name: ${file.name}');
+print('- MIME type: ${file.mimeType}');
+
+final fileContent = await file.readAsString();
+print('Content of the file: ${fileContent}');  // e.g. "Moto G (4)"
+```
+
+You will find links to the API docs on the [pub page](https://pub.dev/packages/cross_file).
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](http://flutter.io/).
+
+For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code).
\ No newline at end of file
diff --git a/packages/cross_file/lib/cross_file.dart b/packages/cross_file/lib/cross_file.dart
new file mode 100644
index 0000000..a3e2873
--- /dev/null
+++ b/packages/cross_file/lib/cross_file.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'src/x_file.dart';
diff --git a/packages/cross_file/lib/src/types/base.dart b/packages/cross_file/lib/src/types/base.dart
new file mode 100644
index 0000000..98c2f8c
--- /dev/null
+++ b/packages/cross_file/lib/src/types/base.dart
@@ -0,0 +1,87 @@
+// 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:typed_data';
+
+/// The interface for a CrossFile.
+///
+/// A CrossFile is a container that wraps the path of a selected
+/// file by the user and (in some platforms, like web) the bytes
+/// with the contents of the file.
+///
+/// This class is a very limited subset of dart:io [File], so all
+/// the methods should seem familiar.
+abstract class XFileBase {
+  /// Construct a CrossFile
+  // ignore: avoid_unused_constructor_parameters
+  XFileBase(String? path);
+
+  /// Save the CrossFile at the indicated file path.
+  Future<void> saveTo(String path) {
+    throw UnimplementedError('saveTo has not been implemented.');
+  }
+
+  /// Get the path of the picked file.
+  ///
+  /// This should only be used as a backwards-compatibility clutch
+  /// for mobile apps, or cosmetic reasons only (to show the user
+  /// the path they've picked).
+  ///
+  /// Accessing the data contained in the picked file by its path
+  /// is platform-dependant (and won't work on web), so use the
+  /// byte getters in the CrossFile instance instead.
+  String get path {
+    throw UnimplementedError('.path has not been implemented.');
+  }
+
+  /// The name of the file as it was selected by the user in their device.
+  ///
+  /// Use only for cosmetic reasons, do not try to use this as a path.
+  String get name {
+    throw UnimplementedError('.name has not been implemented.');
+  }
+
+  /// For web, it may be necessary for a file to know its MIME type.
+  String? get mimeType {
+    throw UnimplementedError('.mimeType has not been implemented.');
+  }
+
+  /// Get the length of the file. Returns a `Future<int>` that completes with the length in bytes.
+  Future<int> length() {
+    throw UnimplementedError('.length() has not been implemented.');
+  }
+
+  /// Synchronously read the entire file contents as a string using the given [Encoding].
+  ///
+  /// By default, `encoding` is [utf8].
+  ///
+  /// Throws Exception if the operation fails.
+  Future<String> readAsString({Encoding encoding = utf8}) {
+    throw UnimplementedError('readAsString() has not been implemented.');
+  }
+
+  /// Synchronously read the entire file contents as a list of bytes.
+  ///
+  /// Throws Exception if the operation fails.
+  Future<Uint8List> readAsBytes() {
+    throw UnimplementedError('readAsBytes() has not been implemented.');
+  }
+
+  /// Create a new independent [Stream] for the contents of this file.
+  ///
+  /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0).
+  ///
+  /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file.
+  ///
+  /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled.
+  Stream<Uint8List> openRead([int? start, int? end]) {
+    throw UnimplementedError('openRead() has not been implemented.');
+  }
+
+  /// Get the last-modified time for the CrossFile
+  Future<DateTime> lastModified() {
+    throw UnimplementedError('openRead() has not been implemented.');
+  }
+}
diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart
new file mode 100644
index 0000000..ef69af5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/html.dart
@@ -0,0 +1,143 @@
+// 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';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+import '../web_helpers/web_helpers.dart';
+import './base.dart';
+
+/// A CrossFile that works on web.
+///
+/// It wraps the bytes of a selected file.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object from its ObjectUrl.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve files later.
+  ///
+  /// `name` needs to be passed from the outside, since we only have
+  /// access to it while we create the ObjectUrl.
+  XFile(
+    this.path, {
+    this.mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+        name = name ?? '',
+        super(path);
+
+  /// Construct an CrossFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+    String? path,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+        name = name ?? '',
+        super(path) {
+    if (path == null) {
+      final Blob blob = (mimeType == null)
+          ? Blob(<dynamic>[bytes])
+          : Blob(<dynamic>[bytes], mimeType);
+      this.path = Url.createObjectUrl(blob);
+    } else {
+      this.path = path;
+    }
+  }
+
+  @override
+  final String? mimeType;
+  @override
+  final String name;
+  @override
+  late String path;
+
+  final Uint8List? _data;
+  final int? _length;
+  final DateTime? _lastModified;
+
+  late Element _target;
+
+  final CrossFileTestOverrides? _overrides;
+
+  bool get _hasTestOverrides => _overrides != null;
+
+  @override
+  Future<DateTime> lastModified() async =>
+      Future<DateTime>.value(_lastModified);
+
+  Future<Uint8List> get _bytes async {
+    if (_data != null) {
+      return Future<Uint8List>.value(UnmodifiableUint8ListView(_data!));
+    }
+
+    // We can force 'response' to be a byte buffer by passing responseType:
+    final ByteBuffer? response =
+        (await HttpRequest.request(path, responseType: 'arraybuffer')).response;
+
+    return response?.asUint8List() ?? Uint8List(0);
+  }
+
+  @override
+  Future<int> length() async => _length ?? (await _bytes).length;
+
+  @override
+  Future<String> readAsString({Encoding encoding = utf8}) async {
+    return encoding.decode(await _bytes);
+  }
+
+  @override
+  Future<Uint8List> readAsBytes() async =>
+      Future<Uint8List>.value(await _bytes);
+
+  @override
+  Stream<Uint8List> openRead([int? start, int? end]) async* {
+    final Uint8List bytes = await _bytes;
+    yield bytes.sublist(start ?? 0, end ?? bytes.length);
+  }
+
+  /// Saves the data of this CrossFile at the location indicated by path.
+  /// For the web implementation, the path variable is ignored.
+  @override
+  Future<void> saveTo(String path) async {
+    // Create a DOM container where we can host the anchor.
+    _target = ensureInitialized('__x_file_dom_element');
+
+    // Create an <a> tag with the appropriate download attributes and click it
+    // May be overridden with CrossFileTestOverrides
+    final AnchorElement element = _hasTestOverrides
+        ? _overrides!.createAnchorElement(this.path, name) as AnchorElement
+        : createAnchorElement(this.path, name);
+
+    // Clear the children in our container so we can add an element to click
+    _target.children.clear();
+    addElementToContainerAndClick(_target, element);
+  }
+}
+
+/// Overrides some functions to allow testing
+@visibleForTesting
+class CrossFileTestOverrides {
+  /// Default constructor for overrides
+  CrossFileTestOverrides({required this.createAnchorElement});
+
+  /// For overriding the creation of the file input element.
+  Element Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart
new file mode 100644
index 0000000..91afac5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/interface.dart
@@ -0,0 +1,60 @@
+// 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:typed_data';
+import 'package:meta/meta.dart';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile is a cross-platform, simplified File abstraction.
+///
+/// It wraps the bytes of a selected file, and its (platform-dependant) path.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object from its path.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve data later.
+  ///
+  /// `name` may be passed from the outside, for those cases where the effective
+  /// `path` of the file doesn't match what the user sees when selecting it
+  /// (like in web)
+  XFile(
+    String path, {
+    String? mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'CrossFile is not available in your current platform.');
+  }
+
+  /// Construct a CrossFile object from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    String? mimeType,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+    String? path,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'CrossFile is not available in your current platform.');
+  }
+}
+
+/// Overrides some functions of CrossFile for testing purposes
+@visibleForTesting
+class CrossFileTestOverrides {
+  /// Default constructor for overrides
+  CrossFileTestOverrides({required this.createAnchorElement});
+
+  /// For overriding the creation of the file input element.
+  dynamic Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart
new file mode 100644
index 0000000..6d649ce
--- /dev/null
+++ b/packages/cross_file/lib/src/types/io.dart
@@ -0,0 +1,119 @@
+// 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:io';
+import 'dart:typed_data';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile backed by a dart:io File.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object backed by a dart:io File.
+  XFile(
+    String path, {
+    this.mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+  })  : _file = File(path),
+        _bytes = null,
+        _lastModified = lastModified,
+        super(path);
+
+  /// Construct an CrossFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    String? path,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+  })  : _bytes = bytes,
+        _file = File(path ?? ''),
+        _length = length,
+        _lastModified = lastModified,
+        super(path) {
+    if (length == null) {
+      _length = bytes.length;
+    }
+  }
+
+  final File _file;
+  @override
+  final String? mimeType;
+  final DateTime? _lastModified;
+  int? _length;
+
+  final Uint8List? _bytes;
+
+  @override
+  Future<DateTime> lastModified() {
+    if (_lastModified != null) {
+      return Future<DateTime>.value(_lastModified);
+    }
+    // ignore: avoid_slow_async_io
+    return _file.lastModified();
+  }
+
+  @override
+  Future<void> saveTo(String path) async {
+    final File fileToSave = File(path);
+    await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
+    await fileToSave.create();
+  }
+
+  @override
+  String get path {
+    return _file.path;
+  }
+
+  @override
+  String get name {
+    return _file.path.split(Platform.pathSeparator).last;
+  }
+
+  @override
+  Future<int> length() {
+    if (_length != null) {
+      return Future<int>.value(_length);
+    }
+    return _file.length();
+  }
+
+  @override
+  Future<String> readAsString({Encoding encoding = utf8}) {
+    if (_bytes != null) {
+      return Future<String>.value(String.fromCharCodes(_bytes!));
+    }
+    return _file.readAsString(encoding: encoding);
+  }
+
+  @override
+  Future<Uint8List> readAsBytes() {
+    if (_bytes != null) {
+      return Future<Uint8List>.value(_bytes);
+    }
+    return _file.readAsBytes();
+  }
+
+  Stream<Uint8List> _getBytes(int? start, int? end) async* {
+    final Uint8List bytes = _bytes!;
+    yield bytes.sublist(start ?? 0, end ?? bytes.length);
+  }
+
+  @override
+  Stream<Uint8List> openRead([int? start, int? end]) {
+    if (_bytes != null) {
+      return _getBytes(start, end);
+    } else {
+      return _file
+          .openRead(start ?? 0, end)
+          .map((List<int> chunk) => Uint8List.fromList(chunk));
+    }
+  }
+}
diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
new file mode 100644
index 0000000..9440d8a
--- /dev/null
+++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
@@ -0,0 +1,38 @@
+// 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:html';
+
+/// Create anchor element with download attribute
+AnchorElement createAnchorElement(String href, String? suggestedName) {
+  final AnchorElement element = AnchorElement(href: href);
+
+  if (suggestedName == null) {
+    element.download = 'download';
+  } else {
+    element.download = suggestedName;
+  }
+
+  return element;
+}
+
+/// Add an element to a container and click it
+void addElementToContainerAndClick(Element container, Element element) {
+  // Add the element and click it
+  // All previous elements will be removed before adding the new one
+  container.children.add(element);
+  element.click();
+}
+
+/// Initializes a DOM container where we can host elements.
+Element ensureInitialized(String id) {
+  Element? target = querySelector('#$id');
+  if (target == null) {
+    final Element targetElement = Element.tag('flt-x-file')..id = id;
+
+    querySelector('body')!.children.add(targetElement);
+    target = targetElement;
+  }
+  return target;
+}
diff --git a/packages/cross_file/lib/src/x_file.dart b/packages/cross_file/lib/src/x_file.dart
new file mode 100644
index 0000000..6136bff
--- /dev/null
+++ b/packages/cross_file/lib/src/x_file.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export 'types/interface.dart'
+    if (dart.library.html) 'types/html.dart'
+    if (dart.library.io) 'types/io.dart';
diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml
new file mode 100644
index 0000000..5195602
--- /dev/null
+++ b/packages/cross_file/pubspec.yaml
@@ -0,0 +1,18 @@
+name: cross_file
+description: An abstraction to allow working with files across multiple platforms.
+repository: https://github.com/flutter/packages/tree/master/packages/cross_file
+version: 0.3.1+1
+
+dependencies:
+  flutter:
+    sdk: flutter
+  meta: ^1.3.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.10.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=1.22.0"
diff --git a/packages/cross_file/test/assets/hello.txt b/packages/cross_file/test/assets/hello.txt
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/packages/cross_file/test/assets/hello.txt
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart
new file mode 100644
index 0000000..aadcc20
--- /dev/null
+++ b/packages/cross_file/test/x_file_html_test.dart
@@ -0,0 +1,108 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final html.File textFile = html.File(<Object>[bytes], 'hello.txt');
+final String textFileUrl = html.Url.createObjectUrl(textFile);
+
+void main() {
+  group('Create with an objectUrl', () {
+    final XFile file = XFile(textFileUrl);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('Create from data', () {
+    final XFile file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('saveTo(..)', () {
+    const String crossFileDomElementId = '__x_file_dom_element';
+
+    group('CrossFile saveTo(..)', () {
+      test('creates a DOM container', () async {
+        final XFile file = XFile.fromData(bytes);
+
+        await file.saveTo('');
+
+        final html.Element? container =
+            html.querySelector('#$crossFileDomElementId');
+
+        expect(container, isNotNull);
+      });
+
+      test('create anchor element', () async {
+        final XFile file = XFile.fromData(bytes, name: textFile.name);
+
+        await file.saveTo('path');
+
+        final html.Element container =
+            html.querySelector('#$crossFileDomElementId')!;
+        final html.AnchorElement element = container.children
+                .firstWhere((html.Element element) => element.tagName == 'A')
+            as html.AnchorElement;
+
+        // if element is not found, the `firstWhere` call will throw StateError.
+        expect(element.href, file.path);
+        expect(element.download, file.name);
+      });
+
+      test('anchor element is clicked', () async {
+        final html.AnchorElement mockAnchor = html.AnchorElement();
+
+        final CrossFileTestOverrides overrides = CrossFileTestOverrides(
+          createAnchorElement: (_, __) => mockAnchor,
+        );
+
+        final XFile file =
+            XFile.fromData(bytes, name: textFile.name, overrides: overrides);
+
+        bool clicked = false;
+        mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true);
+
+        await file.saveTo('path');
+
+        expect(clicked, true);
+      });
+    });
+  });
+}
diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart
new file mode 100644
index 0000000..a8edbe5
--- /dev/null
+++ b/packages/cross_file/test/x_file_io_test.dart
@@ -0,0 +1,90 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('vm') // Uses dart:io
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+final String pathPrefix =
+    Directory.current.path.endsWith('test') ? './assets/' : './test/assets/';
+final String path = pathPrefix + 'hello.txt';
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final File textFile = File(path);
+final String textFilePath = textFile.path;
+
+void main() {
+  group('Create with a path', () {
+    final XFile file = XFile(textFilePath);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('saveTo(..) creates file', () async {
+      final File removeBeforeTest = File(pathPrefix + 'newFilePath.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFilePath.txt');
+      final File newFile = File(pathPrefix + 'newFilePath.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+
+  group('Create with data', () {
+    final XFile file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('Function saveTo(..) creates file', () async {
+      final File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFileData.txt');
+      final File newFile = File(pathPrefix + 'newFileData.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md
new file mode 100644
index 0000000..e2ccf47
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 2.0.2
+
+* Update example to use a scope name as a constant from the People API, instead of a hardcoded string.
+* Remove `x` bit from README and pubspec.yaml
+
+## 2.0.1
+
+* Rehomed to `flutter/packages` repository.
+* Update to `googleapis_auth: ^1.1.0`.
+
+## 2.0.0
+
+* Migrate to null safety.
+* Fixes the requested scopes to use the `GoogleSignIn` instance's `scopes`.
+
+## 1.0.4
+
+* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets.
+
+## 1.0.3
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 1.0.2
+
+* Update Flutter SDK constraint.
+
+## 1.0.1
+
+* Update android compileSdkVersion to 29.
+
+## 1.0.0
+
+* First published version.
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/LICENSE b/packages/extension_google_sign_in_as_googleapis_auth/LICENSE
new file mode 100644
index 0000000..b707cc8
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/README.md b/packages/extension_google_sign_in_as_googleapis_auth/README.md
new file mode 100644
index 0000000..c97b22f
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/README.md
@@ -0,0 +1,46 @@
+# extension_google_sign_in_as_googleapis_auth
+
+A bridge package between Flutter's [`google_sign_in` plugin](https://pub.dev/packages/google_sign_in) and Dart's [`googleapis` package](https://pub.dev/packages/googleapis), that is able to create [`googleapis_auth`-like `AuthClient` instances](https://pub.dev/documentation/googleapis_auth/latest/googleapis_auth.auth/AuthClient-class.html) directly from the `GoogleSignIn` plugin.
+
+## Usage
+
+This package is implemented as an [extension method](https://dart.dev/guides/language/extension-methods) on top of the `GoogleSignIn` plugin.
+
+In order to use it, you need to add a `dependency` to your `pubspec.yaml`. Then, wherever you're importing `package:google_sign_in/google_sign_in.dart`, add the following:
+
+```dart
+...
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+...
+```
+
+From that moment on, your `GoogleSignIn` instance will have an additional `Future<AuthClient?> authenticatedClient()` method that you can call once your sign in is successful to retrieve an `AuthClient`.
+
+That object can then be used to create instances of `googleapis` API clients:
+
+```dart
+...
+final peopleApi = PeopleApi((await _googleSignIn.authenticatedClient())!);
+final response = await peopleApi.people.connections.list(
+  'people/me',
+  personFields: 'names',
+);
+...
+```
+
+## Example
+
+This package contains a modified version of Flutter's Google Sign In example app that uses `package:googleapis`' API clients, instead of raw http requests.
+
+See it [here](https://github.com/flutter/packages/blob/master/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart).
+
+The original code (and its license) can be seen [here](https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in/example/lib/main.dart).
+
+## Testing
+
+Run tests with `flutter test`.
+
+## Issues and feedback
+
+Please file [issues](https://github.com/flutter/flutter/issues/new)
+to send feedback or report a bug. Thank you!
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/README.md b/packages/extension_google_sign_in_as_googleapis_auth/example/README.md
new file mode 100644
index 0000000..a7ad235
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/README.md
@@ -0,0 +1,8 @@
+# extension_google_sign_in_example
+
+Demonstrates how to use the google_sign_in plugin with the `googleapis` package.
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](https://flutter.dev/).
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle
new file mode 100755
index 0000000..2952c3b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle
@@ -0,0 +1,64 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 29
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        applicationId "io.flutter.plugins.googlesigninexample"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.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
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation 'com.google.android.gms:play-services-auth:16.0.1'
+    testImplementation'junit:junit:4.12'
+    testImplementation 'org.mockito:mockito-core:2.17.0'
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json
new file mode 100644
index 0000000..efa5245
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json
@@ -0,0 +1,246 @@
+{
+  "project_info": {
+    "project_number": "479882132969",
+    "firebase_url": "https://my-flutter-proj.firebaseio.com",
+    "project_id": "my-flutter-proj",
+    "storage_bucket": "my-flutter-proj.appspot.com"
+  },
+  "client": [
+    {
+      "client_info": {
+        "mobilesdk_app_id": "1:479882132969:android:c73fd19ff7e2c0be",
+        "android_client_info": {
+          "package_name": "io.flutter.plugins.cameraexample"
+        }
+      },
+      "oauth_client": [
+        {
+          "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+          "client_type": 3
+        }
+      ],
+      "api_key": [
+        {
+          "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+        }
+      ],
+      "services": {
+        "analytics_service": {
+          "status": 1
+        },
+        "appinvite_service": {
+          "status": 1,
+          "other_platform_oauth_client": []
+        },
+        "ads_service": {
+          "status": 2
+        }
+      }
+    },
+    {
+      "client_info": {
+        "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139",
+        "android_client_info": {
+          "package_name": "io.flutter.plugins.firebasedynamiclinksexample"
+        }
+      },
+      "oauth_client": [
+        {
+          "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com",
+          "client_type": 1,
+          "android_info": {
+            "package_name": "io.flutter.plugins.firebasedynamiclinksexample",
+            "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+          }
+        },
+        {
+          "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+          "client_type": 3
+        }
+      ],
+      "api_key": [
+        {
+          "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+        }
+      ],
+      "services": {
+        "analytics_service": {
+          "status": 1
+        },
+        "appinvite_service": {
+          "status": 2,
+          "other_platform_oauth_client": [
+            {
+              "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+              "client_type": 3
+            },
+            {
+              "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+              "client_type": 2,
+              "ios_info": {
+                "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+              }
+            }
+          ]
+        },
+        "ads_service": {
+          "status": 2
+        }
+      }
+    },
+    {
+      "client_info": {
+        "mobilesdk_app_id": "1:479882132969:android:ae50362b4bc06086",
+        "android_client_info": {
+          "package_name": "io.flutter.plugins.firebasemlvisionexample"
+        }
+      },
+      "oauth_client": [
+        {
+          "client_id": "479882132969-9pp74fkgmtvt47t9rikc1p861v7n85tn.apps.googleusercontent.com",
+          "client_type": 1,
+          "android_info": {
+            "package_name": "io.flutter.plugins.firebasemlvisionexample",
+            "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+          }
+        },
+        {
+          "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+          "client_type": 3
+        }
+      ],
+      "api_key": [
+        {
+          "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+        }
+      ],
+      "services": {
+        "analytics_service": {
+          "status": 1
+        },
+        "appinvite_service": {
+          "status": 2,
+          "other_platform_oauth_client": [
+            {
+              "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+              "client_type": 3
+            },
+            {
+              "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+              "client_type": 2,
+              "ios_info": {
+                "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+              }
+            }
+          ]
+        },
+        "ads_service": {
+          "status": 2
+        }
+      }
+    },
+    {
+      "client_info": {
+        "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b",
+        "android_client_info": {
+          "package_name": "io.flutter.plugins.firebaseperformanceexample"
+        }
+      },
+      "oauth_client": [
+        {
+          "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com",
+          "client_type": 1,
+          "android_info": {
+            "package_name": "io.flutter.plugins.firebaseperformanceexample",
+            "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+          }
+        },
+        {
+          "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+          "client_type": 3
+        }
+      ],
+      "api_key": [
+        {
+          "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+        }
+      ],
+      "services": {
+        "analytics_service": {
+          "status": 1
+        },
+        "appinvite_service": {
+          "status": 2,
+          "other_platform_oauth_client": [
+            {
+              "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+              "client_type": 3
+            },
+            {
+              "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+              "client_type": 2,
+              "ios_info": {
+                "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+              }
+            }
+          ]
+        },
+        "ads_service": {
+          "status": 2
+        }
+      }
+    },
+    {
+      "client_info": {
+        "mobilesdk_app_id": "1:479882132969:android:5e9f1f89e134dc86",
+        "android_client_info": {
+          "package_name": "io.flutter.plugins.googlesigninexample"
+        }
+      },
+      "oauth_client": [
+        {
+          "client_id": "479882132969-90ml692hkonp587sl0v0rurmnvkekgrg.apps.googleusercontent.com",
+          "client_type": 1,
+          "android_info": {
+            "package_name": "io.flutter.plugins.googlesigninexample",
+            "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0"
+          }
+        },
+        {
+          "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+          "client_type": 3
+        }
+      ],
+      "api_key": [
+        {
+          "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs"
+        }
+      ],
+      "services": {
+        "analytics_service": {
+          "status": 1
+        },
+        "appinvite_service": {
+          "status": 2,
+          "other_platform_oauth_client": [
+            {
+              "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com",
+              "client_type": 3
+            },
+            {
+              "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com",
+              "client_type": 2,
+              "ios_info": {
+                "bundle_id": "io.flutter.plugins.firebaseMlVisionExample"
+              }
+            }
+          ]
+        },
+        "ads_service": {
+          "status": 2
+        }
+      }
+    }
+  ],
+  "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9a4163a
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..df80f82
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.plugins.googlesigninexample">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <application>
+        <activity android:name="io.flutter.embedding.android.FlutterActivity"
+                  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>
+        <activity android:name=".EmbeddingV1Activity"
+                  android:theme="@android:style/Theme.Black.NoTitleBar"
+                  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+                  android:hardwareAccelerated="true"
+                  android:windowSoftInputMode="adjustResize">
+        </activity>
+        <meta-data android:name="flutterEmbedding" android:value="2"/>
+    </application>
+</manifest>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore
new file mode 100755
index 0000000..9eb4563
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore
@@ -0,0 +1 @@
+GeneratedPluginRegistrant.java
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java
new file mode 100644
index 0000000..5ec1982
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java
@@ -0,0 +1,19 @@
+// Copyright 2017 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.googlesigninexample;
+
+import android.os.Bundle;
+import io.flutter.plugins.googlesignin.GoogleSignInPlugin;
+import io.flutter.view.FlutterMain;
+
+@SuppressWarnings("deprecation")
+public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    FlutterMain.startInitialization(this);
+    super.onCreate(savedInstanceState);
+    GoogleSignInPlugin.registerWith(registrarFor("io.flutter.plugins.googlesignin"));
+  }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java
new file mode 100644
index 0000000..3e7250c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java
@@ -0,0 +1,18 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.googlesigninexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterTestRunner.class)
+@SuppressWarnings("deprecation")
+public class EmbeddingV1ActivityTest {
+  @Rule
+  public ActivityTestRule<EmbeddingV1Activity> rule =
+      new ActivityTestRule<>(EmbeddingV1Activity.class);
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java
new file mode 100644
index 0000000..f9aa77b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java
@@ -0,0 +1,17 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.googlesigninexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterTestRunner.class)
+public class FlutterActivityTest {
+  @Rule
+  public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..db77bb4
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..17987b7
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..09d4391
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..4d6372e
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c7e28ff
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="default_web_client_id">YOUR_WEB_CLIENT_ID</string>
+</resources>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle
new file mode 100755
index 0000000..541636c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+    }
+}
+
+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/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties
new file mode 100755
index 0000000..38c8d45
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..019065d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle
new file mode 100755
index 0000000..115da6c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100755
index 0000000..6c2de80
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,30 @@
+<?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>UIRequiredDeviceCapabilities</key>
+  <array>
+    <string>arm64</string>
+  </array>
+  <key>MinimumOSVersion</key>
+  <string>8.0</string>
+</dict>
+</plist>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig
new file mode 100755
index 0000000..9803018
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig
new file mode 100755
index 0000000..a4a8c60
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Generated.xcconfig"
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist
@@ -0,0 +1,22 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..faaaa58
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,502 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		5C6F5A6E1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */; };
+		7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */; };
+		7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */; };
+		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 */; };
+		C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		0263E28FA425D1CE928BDE15 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+		5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
+		7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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>"; };
+		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>"; };
+		F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		840012C8B5EDBCF56B0E4AC1 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */,
+				F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */,
+			);
+			name = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				7ACDFB0D1E8944C400BE2D00 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				840012C8B5EDBCF56B0E4AC1 /* Pods */,
+				CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				5C6F5A6C1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.h */,
+				5C6F5A6D1EC3B4CB008D64B5 /* GeneratedPluginRegistrant.m */,
+				7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
+				7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				7A303C2D1E89D76400B1F19E /* GoogleService-Info.plist */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				97C146F11CF9000F007C117D /* Supporting Files */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		97C146F11CF9000F007C117D /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				97C146F21CF9000F007C117D /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				0263E28FA425D1CE928BDE15 /* libPods-Runner.a */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */,
+				532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */,
+				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 = 1100;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			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 = (
+				7A303C2E1E89D76400B1F19E /* GoogleService-Info.plist in Resources */,
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				7ACDFB0E1E8944C400BE2D00 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets 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\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n";
+		};
+		532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
+				"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+				"${PODS_ROOT}/../Flutter/Flutter.framework",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		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";
+		};
+		AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				5C6F5A6E1EC3B4CB008D64B5 /* 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;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+				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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				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 = io.flutter.plugins.googleSignInExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				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 = io.flutter.plugins.googleSignInExample;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			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/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100755
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100755
index 0000000..3bb3697
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1100"
+   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"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      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>
+   </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/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100755
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..d9e18e9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h
@@ -0,0 +1,10 @@
+// Copyright 2017 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 <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..f086757
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m
@@ -0,0 +1,17 @@
+// Copyright 2017 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.
+
+#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/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100755
index 0000000..d22f10b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+  "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"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100755
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100755
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100755
index 0000000..f091b6b
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100755
index 0000000..4cde121
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100755
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100755
index 0000000..dcdc230
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100755
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100755
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100755
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100755
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100755
index 0000000..75b2d16
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100755
index 0000000..c4df70d
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100755
index 0000000..6a84f41
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100755
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100755
index 0000000..ebf48f6
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?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" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <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="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100755
index 0000000..f3c2851
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 0000000..6042aab
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,44 @@
+<?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>AD_UNIT_ID_FOR_BANNER_TEST</key>
+	<string>ca-app-pub-3940256099942544/2934735716</string>
+	<key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key>
+	<string>ca-app-pub-3940256099942544/4411468910</string>
+	<key>CLIENT_ID</key>
+	<string>479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com</string>
+	<key>REVERSED_CLIENT_ID</key>
+	<string>com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u</string>
+	<key>ANDROID_CLIENT_ID</key>
+	<string>479882132969-jie8r1me6dsra60pal6ejaj8dgme3tg0.apps.googleusercontent.com</string>
+	<key>API_KEY</key>
+	<string>AIzaSyBECOwLTAN6PU4Aet1b2QLGIb3kRK8Xjew</string>
+	<key>GCM_SENDER_ID</key>
+	<string>479882132969</string>
+	<key>PLIST_VERSION</key>
+	<string>1</string>
+	<key>BUNDLE_ID</key>
+	<string>io.flutter.plugins.googleSignInExample</string>
+	<key>PROJECT_ID</key>
+	<string>my-flutter-proj</string>
+	<key>STORAGE_BUCKET</key>
+	<string>my-flutter-proj.appspot.com</string>
+	<key>IS_ADS_ENABLED</key>
+	<true></true>
+	<key>IS_ANALYTICS_ENABLED</key>
+	<false></false>
+	<key>IS_APPINVITE_ENABLED</key>
+	<true></true>
+	<key>IS_GCM_ENABLED</key>
+	<true></true>
+	<key>IS_SIGNIN_ENABLED</key>
+	<true></true>
+	<key>GOOGLE_APP_ID</key>
+	<string>1:479882132969:ios:2643f950e0a0da08</string>
+	<key>DATABASE_URL</key>
+	<string>https://my-flutter-proj.firebaseio.com</string>
+	<key>SERVER_CLIENT_ID</key>
+	<string>YOUR_SERVER_CLIENT_ID</string>
+</dict>
+</plist>
\ No newline at end of file
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist
new file mode 100755
index 0000000..e03ccfe
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist
@@ -0,0 +1,62 @@
+<?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>CFBundleDisplayName</key>
+	<string>Google Sign-In Example</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>GoogleSignInExample</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.googleusercontent.apps.479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u</string>
+			</array>
+		</dict>
+	</array>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>arm64</string>
+	</array>
+	<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/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m
new file mode 100644
index 0000000..bec320c
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m
@@ -0,0 +1,13 @@
+// Copyright 2017 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 <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/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart
new file mode 100755
index 0000000..a7ebc09
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart
@@ -0,0 +1,150 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:google_sign_in/google_sign_in.dart';
+
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+import 'package:googleapis/people/v1.dart';
+
+final GoogleSignIn _googleSignIn = GoogleSignIn(
+  scopes: <String>[PeopleServiceApi.contactsReadonlyScope],
+);
+
+void main() {
+  runApp(
+    const MaterialApp(
+      title: 'Google Sign In',
+      home: SignInDemo(),
+    ),
+  );
+}
+
+/// The main widget of this demo.
+class SignInDemo extends StatefulWidget {
+  /// Creates the main widget of this demo.
+  const SignInDemo({Key? key}) : super(key: key);
+
+  @override
+  State createState() => SignInDemoState();
+}
+
+/// The state of the main widget.
+class SignInDemoState extends State<SignInDemo> {
+  GoogleSignInAccount? _currentUser;
+  String? _contactText;
+
+  @override
+  void initState() {
+    super.initState();
+    _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {
+      setState(() {
+        _currentUser = account;
+      });
+      if (_currentUser != null) {
+        _handleGetContact();
+      }
+    });
+    _googleSignIn.signInSilently();
+  }
+
+  Future<void> _handleGetContact() async {
+    setState(() {
+      _contactText = 'Loading contact info...';
+    });
+
+    final PeopleServiceApi peopleApi =
+        PeopleServiceApi((await _googleSignIn.authenticatedClient())!);
+    final ListConnectionsResponse response =
+        await peopleApi.people.connections.list(
+      'people/me',
+      personFields: 'names',
+    );
+
+    final String? firstNamedContactName =
+        _pickFirstNamedContact(response.connections);
+
+    setState(() {
+      if (firstNamedContactName != null) {
+        _contactText = 'I see you know $firstNamedContactName!';
+      } else {
+        _contactText = 'No contacts to display.';
+      }
+    });
+  }
+
+  String? _pickFirstNamedContact(List<Person>? connections) {
+    return connections
+        ?.firstWhere(
+          (Person person) => person.names != null,
+        )
+        .names
+        ?.firstWhere(
+          (Name name) => name.displayName != null,
+        )
+        .displayName;
+  }
+
+  Future<void> _handleSignIn() async {
+    try {
+      await _googleSignIn.signIn();
+    } catch (error) {
+      print(error);
+    }
+  }
+
+  Future<void> _handleSignOut() => _googleSignIn.disconnect();
+
+  Widget _buildBody() {
+    if (_currentUser != null) {
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.spaceAround,
+        children: <Widget>[
+          ListTile(
+            leading: GoogleUserCircleAvatar(
+              identity: _currentUser!,
+            ),
+            title: Text(_currentUser!.displayName ?? ''),
+            subtitle: Text(_currentUser!.email),
+          ),
+          const Text('Signed in successfully.'),
+          Text(_contactText ?? ''),
+          ElevatedButton(
+            child: const Text('SIGN OUT'),
+            onPressed: _handleSignOut,
+          ),
+          ElevatedButton(
+            child: const Text('REFRESH'),
+            onPressed: _handleGetContact,
+          ),
+        ],
+      );
+    } else {
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.spaceAround,
+        children: <Widget>[
+          const Text('You are not currently signed in.'),
+          ElevatedButton(
+            child: const Text('SIGN IN'),
+            onPressed: _handleSignIn,
+          ),
+        ],
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        appBar: AppBar(
+          title: const Text('Google Sign In'),
+        ),
+        body: ConstrainedBox(
+          constraints: const BoxConstraints.expand(),
+          child: _buildBody(),
+        ));
+  }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml
new file mode 100644
index 0000000..b5ed9a1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml
@@ -0,0 +1,26 @@
+name: extension_google_sign_in_example
+description: Example of Google Sign-In plugin and googleapis.
+publish_to: none
+
+dependencies:
+  extension_google_sign_in_as_googleapis_auth:
+    # When depending on this package from a real application you should use:
+    #   extension_google_sign_in_as_googleapis_auth: ^x.y.z
+    # See https://dart.dev/tools/pub/dependencies#version-constraints
+    # The example app is bundled with the plugin so we use a path dependency on
+    # the parent directory to use the current plugin's version.
+    path: ../
+  flutter:
+    sdk: flutter
+  google_sign_in: ^5.0.0
+  googleapis: ^2.0.0
+
+dev_dependencies:
+  pedantic: ^1.10.0
+
+flutter:
+  uses-material-design: true
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=1.20.0"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html b/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html
new file mode 100644
index 0000000..42a7d93
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/example/web/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta name="google-signin-client_id" content="[YOUR_OAUTH_2_CLIENT_ID_FOR_WEB]" />
+  <title>Google Sign-in Example</title>
+</head>
+<body>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart
new file mode 100644
index 0000000..af81105
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart
@@ -0,0 +1,42 @@
+// Copyright 2020 The Flutter Authors
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:googleapis_auth/googleapis_auth.dart' as gapis;
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Extension on [GoogleSignIn] that adds an `authenticatedClient` method.
+///
+/// This method can be used to retrieve an authenticated [gapis.AuthClient]
+/// client that can be used with the rest of the `googleapis` libraries.
+extension GoogleApisGoogleSignInAuth on GoogleSignIn {
+  /// Retrieve a `googleapis` authenticated client.
+  Future<gapis.AuthClient?> authenticatedClient({
+    @visibleForTesting GoogleSignInAuthentication? debugAuthentication,
+    @visibleForTesting List<String>? debugScopes,
+  }) async {
+    final GoogleSignInAuthentication? auth =
+        debugAuthentication ?? await currentUser?.authentication;
+    final String? oathTokenString = auth?.accessToken;
+    if (oathTokenString == null) {
+      return null;
+    }
+    final gapis.AccessCredentials credentials = gapis.AccessCredentials(
+      gapis.AccessToken(
+        'Bearer',
+        oathTokenString,
+        // TODO(kevmoo): Use the correct value once it's available from authentication
+        // See https://github.com/flutter/flutter/issues/80905
+        DateTime.now().toUtc().add(const Duration(days: 365)),
+      ),
+      null, // We don't have a refreshToken
+      debugScopes ?? scopes,
+    );
+
+    return gapis.authenticatedClient(http.Client(), credentials);
+  }
+}
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml
new file mode 100644
index 0000000..0b6cfb1
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml
@@ -0,0 +1,27 @@
+# Copyright 2020 The Flutter Authors
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+name: extension_google_sign_in_as_googleapis_auth
+description: A bridge package between google_sign_in and googleapis_auth, to create Authenticated Clients from google_sign_in user credentials.
+version: 2.0.2
+repository: https://github.com/flutter/packages/tree/master/packages/extension_google_sign_in_as_googleapis_auth
+
+dependencies:
+  flutter:
+    sdk: flutter
+  google_sign_in: ^5.0.0
+  googleapis_auth: ^1.1.0
+  http: ^0.13.0
+  meta: ^1.3.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.10.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=1.20.0"
diff --git a/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart
new file mode 100644
index 0000000..ac43b35
--- /dev/null
+++ b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart
@@ -0,0 +1,59 @@
+// Copyright 2020 The Flutter Authors
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:googleapis_auth/googleapis_auth.dart' as gapis;
+import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+const String SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null';
+const List<String> DEBUG_FAKE_SCOPES = <String>['some-scope', 'another-scope'];
+const List<String> SIGN_IN_FAKE_SCOPES = <String>[
+  'some-scope',
+  'another-scope'
+];
+
+class FakeGoogleSignIn extends Fake implements GoogleSignIn {
+  @override
+  final List<String> scopes = SIGN_IN_FAKE_SCOPES;
+}
+
+class FakeGoogleSignInAuthentication extends Fake
+    implements GoogleSignInAuthentication {
+  @override
+  final String accessToken = SOME_FAKE_ACCESS_TOKEN;
+}
+
+void main() {
+  final GoogleSignIn signIn = FakeGoogleSignIn();
+  final FakeGoogleSignInAuthentication authMock =
+      FakeGoogleSignInAuthentication();
+
+  test('authenticatedClient returns an authenticated client', () async {
+    final gapis.AuthClient client = (await signIn.authenticatedClient(
+      debugAuthentication: authMock,
+    ))!;
+    expect(client, isA<gapis.AuthClient>());
+  });
+
+  test('authenticatedClient uses GoogleSignIn scopes by default', () async {
+    final gapis.AuthClient client = (await signIn.authenticatedClient(
+      debugAuthentication: authMock,
+    ))!;
+    expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN));
+    expect(client.credentials.scopes, equals(SIGN_IN_FAKE_SCOPES));
+  });
+
+  test('authenticatedClient returned client contains the passed-in credentials',
+      () async {
+    final gapis.AuthClient client = (await signIn.authenticatedClient(
+      debugAuthentication: authMock,
+      debugScopes: DEBUG_FAKE_SCOPES,
+    ))!;
+    expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN));
+    expect(client.credentials.scopes, equals(DEBUG_FAKE_SCOPES));
+  });
+}
diff --git a/packages/flutter_template_images/.gitignore b/packages/flutter_template_images/.gitignore
new file mode 100644
index 0000000..9e04949
--- /dev/null
+++ b/packages/flutter_template_images/.gitignore
@@ -0,0 +1,34 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/flutter_template_images/CHANGELOG.md b/packages/flutter_template_images/CHANGELOG.md
new file mode 100644
index 0000000..7442d0b
--- /dev/null
+++ b/packages/flutter_template_images/CHANGELOG.md
@@ -0,0 +1,12 @@
+## 2.0.0
+
+* Move assets common to all app templates to a new `app_shared` directory.
+* Create `list_detail_app` directory and assets to support new app template.
+
+## 1.0.1
+
+* Moved Windows app template icon for new folder structure.
+
+## 1.0.0
+
+* Windows app template icon.
diff --git a/packages/flutter_template_images/LICENSE b/packages/flutter_template_images/LICENSE
new file mode 100644
index 0000000..aea51a3
--- /dev/null
+++ b/packages/flutter_template_images/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2014 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/flutter_template_images/README.md b/packages/flutter_template_images/README.md
new file mode 100644
index 0000000..3ae9dc9
--- /dev/null
+++ b/packages/flutter_template_images/README.md
@@ -0,0 +1,9 @@
+# flutter\_template\_images
+
+Images used by the `flutter_tools` templates.
+
+This project is an internal dependency of the `flutter` tool, and is
+not intended to be used directly. It contains images files used in
+`flutter create` templates, to avoid checking them into [the main
+Flutter repository](https://github.com/flutter/flutter), where they would
+permanently increase the checkout size over time if altered.
diff --git a/packages/flutter_template_images/lib/flutter_template_images.dart b/packages/flutter_template_images/lib/flutter_template_images.dart
new file mode 100644
index 0000000..fc5fff7
--- /dev/null
+++ b/packages/flutter_template_images/lib/flutter_template_images.dart
@@ -0,0 +1,3 @@
+library flutter_template_images;
+
+// There is no source for this package, since it only exists to hold images.
diff --git a/packages/flutter_template_images/pubspec.yaml b/packages/flutter_template_images/pubspec.yaml
new file mode 100644
index 0000000..8b51fb2
--- /dev/null
+++ b/packages/flutter_template_images/pubspec.yaml
@@ -0,0 +1,7 @@
+name: flutter_template_images
+description: Image files for use in flutter_tools templates.
+version: 2.0.0
+homepage: https://github.com/flutter/packages/tree/master/packages/flutter_template_images
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
diff --git a/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl
new file mode 100644
index 0000000..c9236db
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-192.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl
new file mode 100644
index 0000000..4068c4e
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/web/icons/Icon-maskable-512.png.copy.tmpl
Binary files differ
diff --git a/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico b/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico
new file mode 100644
index 0000000..c04e20c
--- /dev/null
+++ b/packages/flutter_template_images/templates/app_shared/windows.tmpl/runner/resources/app_icon.ico
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png
new file mode 100644
index 0000000..b65164d
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/2.0x/flutter_logo.png
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png
new file mode 100644
index 0000000..97e5dc9
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/3.0x/flutter_logo.png
Binary files differ
diff --git a/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png b/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png
new file mode 100644
index 0000000..b5c6ca7
--- /dev/null
+++ b/packages/flutter_template_images/templates/list_detail_app/assets/images/flutter_logo.png
Binary files differ
diff --git a/packages/fuchsia_ctl/.gitignore b/packages/fuchsia_ctl/.gitignore
new file mode 100644
index 0000000..851938e
--- /dev/null
+++ b/packages/fuchsia_ctl/.gitignore
@@ -0,0 +1,18 @@
+# CIPD and Dart-SDK binaries
+.cipd/
+dart-sdk/
+
+# Build outputs
+*.aot
+*.dill
+.dart_tool/
+build/
+.ssh/
+
+# Fuchsia SDK binaries someone might move in for testing
+device-finder
+pm
+
+# Dart
+.packages
+!pubspec.lock
diff --git a/packages/fuchsia_ctl/CHANGELOG.md b/packages/fuchsia_ctl/CHANGELOG.md
new file mode 100644
index 0000000..43716b9
--- /dev/null
+++ b/packages/fuchsia_ctl/CHANGELOG.md
@@ -0,0 +1,76 @@
+# CHANGELOG
+
+## version:0.0.27
+
+- Flush the content of iosink when writing output to a file.
+
+## version:0.0.26
+
+- Replace amberctl with pkgctl.
+
+## version:0.0.25
+
+- Added log-file option to stream logs to a file.
+
+
+## 0.0.24
+
+- Use fuchsia sdk `device-finder` instead of `dev_finder`.
+
+## 0.0.23
+
+- Added `emu` tool for spawning an emulator instance given a Fuchsia QEMU build,
+  the Fuchsia SDK, and an Android emulator executable.
+- Fixed homepage link.
+
+## 0.0.18 - 0.0.22
+
+- Add retries to paving operations.
+- Add timeouts to paving and ssh operations.
+- Add arguments parameter to test command.
+
+## 0.0.17
+
+- Add "push-packages" option.
+
+## 0.0.16
+
+- Refactor usages of `amberctl` to its own class.
+
+## 0.0.9 - 0.0.15
+
+- Various improvements including support for Fuchsia tests.
+
+## 0.0.8
+
+- Use fuchsiapkg URLs to launch test target
+
+## 0.0.7
+
+- Set error code in wrapper script
+
+## 0.0.6
+
+- Use random port for PM serving.
+- Add more error logging on failures.
+
+## 0.0.5
+
+- Fix SSH command input so that correct exit code is returned when a command
+  fails.
+
+## 0.0.4
+
+- Fix issue where tests results were not printing.
+
+## 0.0.3
+
+- Publish correct binaries for Linux
+
+## 0.0.2
+
+- Published with mac binaries tagged as Linux. Do not use.
+
+## 0.0.1
+
+- Initial pre-release for testing. Do not use.
diff --git a/packages/fuchsia_ctl/LICENSE b/packages/fuchsia_ctl/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/packages/fuchsia_ctl/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/fuchsia_ctl/README.md b/packages/fuchsia_ctl/README.md
new file mode 100644
index 0000000..e14fbb1
--- /dev/null
+++ b/packages/fuchsia_ctl/README.md
@@ -0,0 +1,29 @@
+# fuchsia_ctl
+
+This package is used by Flutter CI systems to manage paving and testing Fuchsia
+devices.
+
+It offers some functionality similar to the `fx` command in the Fuchsia SDK.
+
+It is not intended for general use.
+
+## Building
+
+This tool is meant to be published as an AOT compiled binary distributed via
+CIPD. To build the AOT binary, run `tool/build.sh`. This must be done on a
+Linux machine, and will automatically download a suitable version of Dart to
+build the binary.
+
+To create the CIPD package, make sure that the `build/` folder does not contain
+any files from testing (e.g. a generated `.ssh` folder from paving or a copy of
+`device-finder` or `pm`). Then run:
+
+```bash
+cipd create -in build                   \
+  -name flutter/fuchsia_ctl/linux-amd64 \
+  -ref stable                           \
+  -tag version:n.n.n
+```
+
+with an appropriate version string, after running `tool/build.sh`.
+
diff --git a/packages/fuchsia_ctl/bin/main.dart b/packages/fuchsia_ctl/bin/main.dart
new file mode 100644
index 0000000..014fa30
--- /dev/null
+++ b/packages/fuchsia_ctl/bin/main.dart
@@ -0,0 +1,408 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:args/args.dart';
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:retry/retry.dart';
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+typedef AsyncResult = Future<OperationResult> Function(
+    String, DevFinder, ArgResults);
+
+const Map<String, AsyncResult> commands = <String, AsyncResult>{
+  'emu': emulator,
+  'pave': pave,
+  'pm': pm,
+  'ssh': ssh,
+  'test': test,
+  'push-packages': pushPackages,
+};
+
+Future<void> main(List<String> args) async {
+  if (!Platform.isLinux) {
+    throw UnsupportedError('This tool only supports Linux.');
+  }
+
+  final ArgParser parser = ArgParser();
+  parser
+    ..addOption('device-name',
+        abbr: 'd',
+        help: 'The device node name to use. '
+            'If not specified, the first discoverable device will be used.')
+    ..addOption('device-finder-path',
+        defaultsTo: './device-finder',
+        help: 'The path to the device-finder executable.')
+    ..addFlag('help', defaultsTo: false, help: 'Prints help.');
+
+  /// This is a blocking command and will run until exited.
+  parser.addCommand('emu')
+    ..addOption('image', help: 'Fuchsia image to run')
+    ..addOption('zbi', help: 'Bootloader image to sign and run')
+    ..addOption('qemu-kernel', help: 'QEMU kernel to run')
+    ..addOption('window-size', help: 'Emulator window size formatted "WxH"')
+    ..addOption('aemu', help: 'AEMU executable path')
+    ..addOption('sdk',
+        help: 'Location to Fuchsia SDK containing tools and images')
+    ..addOption('public-key',
+        defaultsTo: '.fuchsia/authorized_keys',
+        help: 'Path to the authorized_keys to sign zbi image with')
+    ..addFlag('headless', help: 'Run FEMU without graphical window');
+
+  parser.addCommand('ssh')
+    ..addFlag('interactive',
+        abbr: 'i',
+        help: 'Whether to ssh in interactive mode. '
+            'If --comand is specified, this is ignored.')
+    ..addOption('command',
+        abbr: 'c',
+        help: 'The command to run on the device. '
+            'If specified, --interactive is ignored.')
+    ..addOption('identity-file',
+        defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+    ..addOption('timeout-seconds',
+        defaultsTo: '120', help: 'Ssh command timeout in seconds.')
+    ..addOption('log-file',
+        defaultsTo: '', help: 'The file to write stdout and stderr.');
+  parser.addCommand('pave')
+    ..addOption('public-key',
+        abbr: 'p', help: 'The public key to add to authorized_keys.')
+    ..addOption('image',
+        abbr: 'i', help: 'The system image tgz to unpack and pave.');
+
+  final ArgParser pmSubCommand = parser.addCommand('pm')
+    ..addOption('pm-path',
+        defaultsTo: './pm', help: 'The path to the pm executable.')
+    ..addOption('repo',
+        abbr: 'r',
+        help: 'The location of the repository folder to create, '
+            'publish, or serve.')
+    ..addCommand('serve')
+    ..addCommand('newRepo');
+  pmSubCommand
+      .addCommand('publishRepo')
+      .addMultiOption('far', abbr: 'f', help: 'The .far files to publish.');
+
+  parser.addCommand('push-packages')
+    ..addOption('pm-path',
+        defaultsTo: './pm', help: 'The path to the pm executable.')
+    ..addOption('repoArchive', help: 'The path to the repo tar.gz archive.')
+    ..addOption('identity-file',
+        defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+    ..addMultiOption('packages',
+        abbr: 'p',
+        help: 'Packages from the repo that need to be pushed to the device.');
+
+  parser.addCommand('test')
+    ..addOption('pm-path',
+        defaultsTo: './pm', help: 'The path to the pm executable.')
+    ..addOption('identity-file',
+        defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
+    ..addOption('target',
+        abbr: 't', help: 'The name of the target to pass to runtests.')
+    ..addOption('arguments',
+        abbr: 'a',
+        help: 'Command line arguments to pass when invoking the tests')
+    ..addMultiOption('far',
+        abbr: 'f', help: 'The .far files to include for the test.')
+    ..addOption('timeout-seconds',
+        defaultsTo: '120', help: 'Test timeout in seconds.')
+    ..addOption('packages-directory', help: 'amber files directory.');
+
+  final ArgResults results = parser.parse(args);
+
+  if (results.command == null) {
+    stderr.writeln('Unknown command, expected one of: ${parser.commands.keys}');
+    stderr.writeln(parser.usage);
+    exit(-1);
+  }
+
+  if (results['help']) {
+    stderr.writeln(parser.commands[results.command.name].usage);
+    exit(0);
+  }
+
+  final AsyncResult command = commands[results.command.name];
+  if (command == null) {
+    stderr.writeln('Unkown command ${results.command.name}.');
+    stderr.writeln(parser.usage);
+    exit(-1);
+  }
+  final OperationResult result = await command(
+    results['device-name'],
+    DevFinder(results['device-finder-path']),
+    results.command,
+  );
+  if (!result.success) {
+    exit(-1);
+  }
+}
+
+@visibleForTesting
+Future<OperationResult> emulator(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  final Emulator emulator = Emulator(
+    aemuPath: args['aemu'],
+    fuchsiaImagePath: args['image'],
+    fuchsiaSdkPath: args['sdk'],
+    qemuKernelPath: args['qemu-kernel'],
+    sshKeyManager: SystemSshKeyManager.defaultProvider(
+      publicKeyPath: args['public-key'],
+    ),
+    zbiPath: args['zbi'],
+  );
+  await emulator.prepareEnvironment();
+
+  return emulator.start(
+    headless: args['headless'],
+    windowSize: args['window-size'],
+  );
+}
+
+@visibleForTesting
+Future<OperationResult> ssh(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  const SshClient sshClient = SshClient();
+  final String targetIp = await devFinder.getTargetAddress(deviceName);
+  final String identityFile = args['identity-file'];
+  final String outputFile = args['log-file'];
+  if (args['interactive']) {
+    return sshClient.interactive(
+      targetIp,
+      identityFilePath: identityFile,
+    );
+  }
+  final OperationResult result = await sshClient.runCommand(
+    targetIp,
+    identityFilePath: identityFile,
+    command: args['command'].split(' '),
+    timeoutMs:
+        Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
+    logFilePath: outputFile,
+  );
+  stdout.writeln(
+      '==================================== STDOUT ====================================');
+  stdout.writeln(result.info);
+  stderr.writeln(
+      '==================================== STDERR ====================================');
+  stderr.writeln(result.error);
+  return result;
+}
+
+@visibleForTesting
+Future<OperationResult> pave(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  const ImagePaver paver = ImagePaver();
+  const RetryOptions r = RetryOptions(
+    maxDelay: Duration(seconds: 30),
+    maxAttempts: 3,
+  );
+  return r.retry(() async {
+    final OperationResult result = await paver.pave(
+      args['image'],
+      deviceName,
+      publicKeyPath: args['public-key'],
+    );
+    if (!result.success) {
+      throw RetryException('Exit code different from 0', result);
+    }
+    return result;
+  }, retryIf: (Exception e) => e is RetryException);
+}
+
+@visibleForTesting
+Future<OperationResult> pm(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  final PackageServer server = PackageServer(args['pm-path']);
+  switch (args.command.name) {
+    case 'serve':
+      await server.serveRepo(args['repo']);
+      await Future<void>.delayed(const Duration(seconds: 15));
+      return server.close();
+    case 'newRepo':
+      return server.newRepo(args['repo']);
+    case 'publishRepo':
+      return server.publishRepo(args['repo'], args['far']);
+    default:
+      throw ArgumentError('Command ${args.command.name} unknown.');
+  }
+}
+
+@visibleForTesting
+Future<OperationResult> pushPackages(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  final PackageServer server = PackageServer(args['pm-path']);
+  final String repoArchive = args['repoArchive'];
+  final List<String> packages = args['packages'];
+  final String identityFile = args['identity-file'];
+
+  const FileSystem fs = LocalFileSystem();
+  final String uuid = Uuid().v4();
+  final Directory repo = fs.systemTempDirectory.childDirectory('repo_$uuid');
+  const Tar tar = SystemTar();
+  try {
+    final String targetIp = await devFinder.getTargetAddress(deviceName);
+    final AmberCtl amberCtl = AmberCtl(targetIp, identityFile);
+
+    stdout.writeln('Untaring $repoArchive to ${repo.path}');
+    repo.createSync(recursive: true);
+    final OperationResult result = await tar.untar(repoArchive, repo.path);
+    if (!result.success) {
+      stdout.writeln(
+          'Error untarring $repoArchive \nstdout: ${result.info} \nstderr: ${result.error}');
+      exit(-1);
+    }
+
+    final String repositoryBase = path.join(repo.path, 'amber-files');
+    stdout.writeln('Serving $repositoryBase to $targetIp');
+    await server.serveRepo(repositoryBase, port: 0);
+    await amberCtl.addSrc(server.serverPort);
+
+    stdout.writeln('Pushing packages $packages to $targetIp');
+    for (final String packageName in packages) {
+      stdout.writeln('Attempting to add package $packageName.');
+      await amberCtl.addPackage(packageName);
+    }
+
+    return OperationResult.success(
+        info: 'Successfully pushed $packages to $targetIp.');
+  } finally {
+    // We may not have created the repo if dev finder errored first.
+    if (repo.existsSync()) {
+      repo.deleteSync(recursive: true);
+    }
+    if (server.serving) {
+      await server.close();
+    }
+  }
+}
+
+@visibleForTesting
+Future<OperationResult> test(
+  String deviceName,
+  DevFinder devFinder,
+  ArgResults args,
+) async {
+  const FileSystem fs = LocalFileSystem();
+  final String identityFile = args['identity-file'];
+
+  //final PackageServer server = PackageServer(args['pm-path']);
+  PackageServer server;
+  const SshClient ssh = SshClient();
+  final List<String> farFiles = args['far'];
+  final String target = args['target'];
+  final String arguments = args['arguments'];
+  Directory repo;
+  if (args['packages-directory'] == null) {
+    final String uuid = Uuid().v4();
+    repo = fs.systemTempDirectory.childDirectory('repo_$uuid');
+    server = PackageServer(args['pm-path']);
+  } else {
+    final String amberFilesPath = path.join(
+      args['packages-directory'],
+      'amber-files',
+    );
+    final String pmPath = path.join(
+      args['packages-directory'],
+      'pm',
+    );
+    repo = fs.directory(amberFilesPath);
+    server = PackageServer(pmPath);
+  }
+
+  try {
+    final String targetIp = await devFinder.getTargetAddress(deviceName);
+    final AmberCtl amberCtl = AmberCtl(targetIp, identityFile);
+    OperationResult result;
+    stdout.writeln('Using ${repo.path} as repo to serve to $targetIp...');
+    if (!repo.existsSync()) {
+      repo.createSync(recursive: true);
+      result = await server.newRepo(repo.path);
+      if (!result.success) {
+        stderr.writeln('Failed to create repo at $repo.');
+        return result;
+      }
+    }
+    await server.serveRepo(repo.path, port: 0);
+    await amberCtl.addSrc(server.serverPort);
+
+    for (final String farFile in farFiles) {
+      result = await server.publishRepo(repo.path, farFile);
+      if (!result.success) {
+        stderr.writeln('Failed to publish repo at $repo with $farFiles.');
+        stderr.writeln(result.error);
+        return result;
+      }
+      final RegExp r = RegExp(r'\-0.far|.far');
+      final String packageName = fs.file(farFile).basename.replaceFirst(r, '');
+      await amberCtl.addPackage(packageName);
+    }
+
+    final OperationResult testResult = await ssh.runCommand(
+      targetIp,
+      identityFilePath: identityFile,
+      command: <String>[
+        'run',
+        'fuchsia-pkg://fuchsia.com/$target#meta/$target.cmx',
+        arguments
+      ],
+      timeoutMs:
+          Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
+    );
+    stdout.writeln('Test results (passed: ${testResult.success}):');
+    if (result.info != null) {
+      stdout.writeln(testResult.info);
+    }
+    if (result.error != null) {
+      stderr.writeln(testResult.error);
+    }
+    return testResult;
+  } finally {
+    // We may not have created the repo if dev finder errored first.
+    if (repo.existsSync() && args['packages-directory'] != null) {
+      repo.deleteSync(recursive: true);
+    }
+    if (server.serving) {
+      await server.close();
+    }
+  }
+}
+
+/// The exception thrown when an operation needs a retry.
+class RetryException implements Exception {
+  /// Creates a new [RetryException] using the specified [cause] and [result]
+  /// to force a retry.
+  const RetryException(this.cause, this.result);
+
+  /// The user-facing message to display.
+  final String cause;
+
+  /// Contains the result of the executed target command.
+  final OperationResult result;
+
+  @override
+  String toString() =>
+      '$runtimeType, cause: "$cause", underlying exception: $result.';
+}
diff --git a/packages/fuchsia_ctl/lib/fuchsia_ctl.dart b/packages/fuchsia_ctl/lib/fuchsia_ctl.dart
new file mode 100644
index 0000000..209c81a
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/fuchsia_ctl.dart
@@ -0,0 +1,13 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/amber_ctl.dart';
+export 'src/dev_finder.dart';
+export 'src/emulator.dart';
+export 'src/image_paver.dart';
+export 'src/operation_result.dart';
+export 'src/package_server.dart';
+export 'src/ssh_client.dart';
+export 'src/ssh_key_manager.dart';
+export 'src/tar.dart';
diff --git a/packages/fuchsia_ctl/lib/src/amber_ctl.dart b/packages/fuchsia_ctl/lib/src/amber_ctl.dart
new file mode 100644
index 0000000..8d0e6a4
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/amber_ctl.dart
@@ -0,0 +1,107 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+const SshClient _kSsh = SshClient();
+
+/// Wrapper for amberctl utility for commands executed on the target device.
+@immutable
+class AmberCtl {
+  /// Creates a new [AmberCtl] with the specified [targetIp] and [identityFile].
+  const AmberCtl(
+    this._targetIp,
+    this._identityFile,
+  );
+
+  final String _identityFile;
+  final String _targetIp;
+
+  /// Adds a new package update source for the target device.
+  ///
+  /// * [port] is what "pm serve" is bound to.
+  /// * Returns the name of the package update source that is randomly generated.
+  Future<String> addSrc(int port) async {
+    final String uuid = Uuid().v4();
+    final String localIp = await _getLocalIp(_targetIp);
+    final List<String> addSource = <String>[
+      'amberctl',
+      'add_src',
+      '-f',
+      'http://[$localIp]:$port/config.json',
+      '-n',
+      uuid,
+    ];
+
+    stdout.writeln('Adding amberctl source: ${addSource.join(' ')}');
+    final OperationResult result = await _kSsh.runCommand(
+      _targetIp,
+      identityFilePath: _identityFile,
+      command: addSource,
+    );
+
+    if (!result.success) {
+      throw AmberCtlException('"add_src" failed, aborting.', result);
+    } else {
+      stdout.writeln('Successfully added an update'
+          ' source on port $port with name $uuid.');
+      return uuid;
+    }
+  }
+
+  /// Adds a package with the given [packageName] to the device.
+  Future<void> addPackage(String packageName) async {
+    stdout.writeln('Adding $packageName...');
+    final List<String> updateCommand = <String>[
+      'pkgctl',
+      'resolve',
+      'fuchsia-pkg://fuchsia.com/$packageName',
+    ];
+
+    final OperationResult result = await _kSsh.runCommand(
+      _targetIp,
+      identityFilePath: _identityFile,
+      command: updateCommand,
+    );
+
+    if (!result.success) {
+      throw AmberCtlException(
+          '${updateCommand.join(' ')} failed, aborting.', result);
+    }
+  }
+
+  Future<String> _getLocalIp(String targetIp) async {
+    final OperationResult result = await _kSsh.runCommand(targetIp,
+        identityFilePath: _identityFile,
+        command: <String>[r'echo $SSH_CONNECTION']);
+
+    if (!result.success) {
+      throw AmberCtlException('Failed to get local address, aborting.', result);
+    } else {
+      return result.info.split(' ')[0].replaceAll('%', '%25');
+    }
+  }
+}
+
+/// Wraps exceptions thrown by amberctl utility.
+@immutable
+class AmberCtlException implements Exception {
+  /// Creates a new [AmberCtlException] using the specified [cause] and [result].
+  const AmberCtlException(this.cause, this.result);
+
+  /// Represents the human-readable cause for the amberctl error.
+  final String cause;
+
+  /// Contains the result of the executed target command.
+  final OperationResult result;
+
+  @override
+  String toString() =>
+      '$runtimeType, cause: "$cause", underlying exception: $result.';
+}
diff --git a/packages/fuchsia_ctl/lib/src/command_line.dart b/packages/fuchsia_ctl/lib/src/command_line.dart
new file mode 100644
index 0000000..2873b75
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/command_line.dart
@@ -0,0 +1,81 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+/// Class for common actions with processes on the command line.
+@immutable
+class CommandLine {
+  /// Create a new instance of [CommandLine].
+  const CommandLine(
+      {this.processManager = const LocalProcessManager(),
+      @visibleForTesting this.stdoutValue,
+      @visibleForTesting this.stderrValue});
+
+  /// The underlying [ProcessManager] to use for running on the current shell.
+  final ProcessManager processManager;
+
+  /// Mock value for use in tests.
+  final Stdout stdoutValue;
+
+  /// Mock value for use in tests.
+  final Stdout stderrValue;
+
+  /// Current shells stdout to use.
+  Stdout get shellStdout => stdoutValue ?? stdout;
+
+  /// Current shells stderr to use.
+  Stdout get shellStderr => stderrValue ?? stderr;
+
+  /// Run [command] and handle its stdio. Once [command] is complete, it will
+  /// output its stdio to the console.
+  ///
+  /// Use this for tasks where stdio does not need to be monitored.
+  ///
+  /// Throw [CommandLineException] if [command] returns a non-0 exit code.
+  Future<void> run(List<String> command) async {
+    shellStdout.writeln(command.join(' '));
+    final ProcessResult process = await processManager.run(command);
+    shellStdout.writeln(process.stdout);
+    shellStderr.writeln(process.stderr);
+
+    if (process.exitCode != 0) {
+      throw CommandLineException('${command.first} did not return exit code 0');
+    }
+  }
+
+  /// Start [command] and handle its stdio by streaming it to the existing
+  /// stdio. While [command] is running, its stdio is streamed to the shell.
+  ///
+  /// Use this for long running tasks where stdio should be monitored.
+  ///
+  /// Throw [CommandLineException] if [command] returns a non-0 exit code.
+  Future<Process> start(List<String> command) async {
+    shellStdout.writeln(command.join(' '));
+    final Process process = await processManager.start(command);
+    shellStdout.addStream(process.stdout);
+    shellStderr.addStream(process.stderr);
+
+    if (await process.exitCode != 0) {
+      throw CommandLineException('${command.first} did not return exit code 0');
+    }
+
+    return process;
+  }
+}
+
+/// Wraps exceptions thrown by [CommandLine].
+class CommandLineException implements Exception {
+  /// Creates a new [CommandLineException].
+  const CommandLineException(this.message);
+
+  /// The user-facing message to display.
+  final String message;
+
+  @override
+  String toString() => message;
+}
diff --git a/packages/fuchsia_ctl/lib/src/dev_finder.dart b/packages/fuchsia_ctl/lib/src/dev_finder.dart
new file mode 100644
index 0000000..ac91213
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/dev_finder.dart
@@ -0,0 +1,136 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+/// A wrapper for the Fuchsia SDK `device-finder` tool.
+@immutable
+class DevFinder {
+  /// Creates a new wrapper for the `device-finder` tool.
+  ///
+  /// All parameters must not be null.
+  const DevFinder(
+    this.devFinderPath, {
+    this.processManager = const LocalProcessManager(),
+  })  : assert(devFinderPath != null),
+        assert(processManager != null);
+
+  /// The path to the Fuchsia SDK `device-finder` tool on disk.
+  final String devFinderPath;
+
+  /// The [ProcessManager] to use for launching the `device-finder` tool.
+  final ProcessManager processManager;
+
+  Future<String> _runDevFinderWithRetries(
+    String deviceName,
+    int numTries,
+    int sleepDelay, {
+    bool local = false,
+    @required bool nullOk,
+  }) async {
+    assert(numTries != null);
+    assert(sleepDelay != null);
+    assert(local != null);
+    assert(nullOk != null);
+
+    if (deviceName == null) {
+      stderr.writeln('Warning: device name not specified; if '
+          'multiple devices are attached you may not get the right one.');
+    }
+    final List<String> command = <String>[
+      devFinderPath,
+      if (deviceName != null) 'resolve' else 'list',
+      '-device-limit', '1', //
+      if (local) '-local',
+      if (deviceName != null) deviceName,
+    ];
+
+    for (int i = 0; i < numTries; i++) {
+      final ProcessResult result = await processManager.run(command);
+      if (result.exitCode == 0) {
+        return result.stdout.toString().trim();
+      }
+      await Future<void>.delayed(Duration(seconds: sleepDelay));
+    }
+    if (!nullOk) {
+      throw DevFinderException(
+          'Failed to get ${local ? 'local' : 'target'} IP for $deviceName');
+    }
+    return null;
+  }
+
+  /// Gets the target address for the specified `deviceName`.
+  ///
+  /// If `deviceName` is null, will attempt to get the target address of the
+  /// first discoverable device.
+  ///
+  /// The `numTries` parameter must not be null, and specifies how many
+  /// times to retry finding the device. This is useful e.g. after paving a
+  /// device and waiting for it to become available. The `sleepDelay` also
+  /// must not be null, and specifies the number of seconds to wait between
+  /// retries.
+  ///
+  /// The `nullOk` parameter must not be null. If true, this method will
+  /// return null if it cannot find a device; otherwise, it will throw. The
+  /// default value is false.
+  Future<String> getTargetAddress(
+    String deviceName, {
+    int numTries = 75,
+    int sleepDelay = 4,
+    bool nullOk = false,
+  }) {
+    return _runDevFinderWithRetries(
+      deviceName,
+      numTries,
+      sleepDelay,
+      nullOk: nullOk,
+      local: false,
+    );
+  }
+
+  /// Gets the local interface address for the specified `deviceName`.
+  ///
+  /// If `deviceName` is null, will attempt to get the target address of the
+  /// first discoverable device.
+  ///
+  /// The `numTries` parameter must not be null, and specifies how many
+  /// times to retry finding the device. This is useful e.g. after paving a
+  /// device and waiting for it to become available. The `sleepDelay` also
+  /// must not be null, and specifies the number of seconds to wait between
+  /// retries.
+  ///
+  /// The `nullOk` parameter must not be null. If true, this method will
+  /// return null if it cannot find a device; otherwise, it will throw. The
+  /// default value is false.
+  Future<String> getLocalAddress(
+    String deviceName, {
+    int numTries = 30,
+    int sleepDelay = 2,
+    bool nullOk = false,
+  }) {
+    return _runDevFinderWithRetries(
+      deviceName,
+      numTries,
+      sleepDelay,
+      nullOk: nullOk,
+      local: true,
+    );
+  }
+}
+
+/// The exception thrown when a [DevFinder] lookup fails.
+class DevFinderException implements Exception {
+  /// Creates a new [DevFinderException], such as when device-finder fails to find
+  /// a device.
+  const DevFinderException(this.message);
+
+  /// The user-facing message to display.
+  final String message;
+
+  @override
+  String toString() => message;
+}
diff --git a/packages/fuchsia_ctl/lib/src/emulator.dart b/packages/fuchsia_ctl/lib/src/emulator.dart
new file mode 100644
index 0000000..1e23647
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/emulator.dart
@@ -0,0 +1,190 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+
+import 'command_line.dart';
+import 'operation_result.dart';
+import 'ssh_key_manager.dart';
+
+/// A wrapper for running Fuchsia images on the Android Emulator (AEMU).
+class Emulator {
+  /// Creates a new wrapper for the `emu` tool.
+  Emulator({
+    @required this.aemuPath,
+    @required this.fuchsiaImagePath,
+    @required this.fuchsiaSdkPath,
+    this.fs = const LocalFileSystem(),
+    this.cli = const CommandLine(),
+    @required this.qemuKernelPath,
+    @required this.sshKeyManager,
+    @required this.zbiPath,
+  })  : assert(cli != null),
+        assert(fs != null);
+
+  /// The path to the AEMU executable on disk.
+  final String aemuPath;
+
+  /// Fuchsia image to load into the emulator.
+  final String fuchsiaImagePath;
+
+  /// The path to the Fuchsia SDK that contains the tools `fvm` and `zbi`.
+  final String fuchsiaSdkPath;
+
+  /// The QEMU kernel image to use. This is only bundled in Fuchsia QEMU images.
+  final String qemuKernelPath;
+
+  /// The Fuchsia bootloader image.
+  final String zbiPath;
+
+  /// Location of `fvm` in [fuchsiaSdkPath].
+  @visibleForTesting
+  final String fvmToolPath = 'sdk/tools/fvm';
+
+  /// Location of `zbi` in [fuchsiaSdkPath].
+  @visibleForTesting
+  final String zbiToolPath = 'sdk/tools/zbi';
+
+  /// Default AEMU window size to be launched.
+  @visibleForTesting
+  final String defaultWindowSize = '1280x800';
+
+  /// Flag to pass to AEMU to run in headless mode.
+  @visibleForTesting
+  final String aemuHeadlessFlag = '-no-window';
+
+  /// [SshKeyManager] for creating `authorized_keys` to access emulator.
+  final SshKeyManager sshKeyManager;
+
+  /// The [FileSystem] to use when running the `emu` tool.
+  final FileSystem fs;
+
+  /// The [CommandLine] wrapper for interacting with the current shell.
+  final CommandLine cli;
+
+  /// FVM extended image of [fuchsiaImagePath] for running on FEMU.
+  @visibleForTesting
+  String fvmImagePath;
+
+  /// [zbiPath] that is accessible with SSH using [sshPath] keys.
+  @visibleForTesting
+  String signedZbiPath;
+
+  /// Update given Fuchsia assets to make them compatible with FEMU.
+  ///
+  /// 1. Ensure required assets exist.
+  /// 2. Create FVM image for running with FEMU.
+  /// 3. Sign boot image for host access to the guest FEMU instance.
+  Future<void> prepareEnvironment() async {
+    assert(fs.isFileSync(fuchsiaImagePath));
+    assert(fs.isFileSync(zbiPath));
+    assert(fs.isFileSync(qemuKernelPath));
+
+    final String tmpPath = fs.systemTempDirectory.createTempSync().path;
+    fvmImagePath = '$tmpPath/fvm.blk';
+    signedZbiPath = '$tmpPath/fuchsia-ssh.zbi';
+
+    await _prepareFvmImage(fuchsiaImagePath, fvmImagePath);
+    await _signBootImage(zbiPath, signedZbiPath);
+  }
+
+  /// Double the size of [fuchsiaImagePath] to make space for the emulator
+  /// to write back to it.
+  Future<void> _prepareFvmImage(String fuchsiaImagePath, String fvmPath,
+      {String fvmExecutable}) async {
+    fvmExecutable ??= path.join(fuchsiaSdkPath, fvmToolPath);
+
+    await cli.run(<String>['cp', fuchsiaImagePath, fvmPath]);
+
+    /// [fvmTool] and FEMU need write access to [fvmPath].
+    await cli.run(<String>['chmod', 'u+w', fvmPath]);
+
+    // Calculate new size by doubling the current size
+    final File fvmFile = fs.file(fvmPath)..createSync();
+    final int newSize = fvmFile.lengthSync() * 2;
+
+    await cli.run(
+        <String>[fvmExecutable, fvmPath, 'extend', '--length', '$newSize']);
+  }
+
+  /// Signed [zbiPath] using [zbiExecutable] with [publicKeyPath] to
+  /// create a bootloader image that is accessible from the host.
+  Future<void> _signBootImage(String zbiPath, String signedZbiPath,
+      {String zbiExecutable}) async {
+    zbiExecutable ??= path.join(fuchsiaSdkPath, zbiToolPath);
+
+    await sshKeyManager.createKeys();
+
+    /// Ensure `zbi` is able to find the ssh keys by giving the full path.
+    final File authorizedKeysAbsolute =
+        fs.file('.ssh/authorized_keys').absolute;
+
+    final List<String> zbiCommand = <String>[
+      zbiExecutable,
+      '--compressed=zstd',
+      '-o',
+      signedZbiPath,
+      zbiPath,
+      '-e',
+      'data/ssh/authorized_keys=${authorizedKeysAbsolute.path}'
+    ];
+    await cli.run(zbiCommand);
+  }
+
+  /// Launch AEMU with [fvmImagePath], [signedZbiPath], and [qemuKernelPath].
+  ///
+  /// [prepareEnvironment] must have been called before starting the emulator.
+  ///
+  /// If [headless] is true, AEMU will run without a graphical window. Infra
+  /// will run AEMU in headless mode.
+  ///
+  /// [windowSize] is what AEMU will set its window size to. Defaults to
+  /// [defaultWindowSize]. Expected to be in the format of "WIDTHxHEIGHT".
+  Future<OperationResult> start(
+      {bool headless = false, String windowSize}) async {
+    assert(fvmImagePath != null && fs.isFileSync(fvmImagePath));
+    assert(signedZbiPath != null && fs.isFileSync(signedZbiPath));
+
+    final List<String> aemuCommand = <String>[
+      aemuPath,
+      '-feature',
+      'VirtioInput,RefCountPipe,KVM,GLDirectMem,Vulkan',
+      '-window-size',
+      windowSize ?? defaultWindowSize,
+      '-gpu',
+      'swiftshader_indirect',
+      if (headless) aemuHeadlessFlag,
+
+      /// Anything after -fuchsia flag will be passed to QEMU
+      '-fuchsia',
+      '-kernel', qemuKernelPath,
+      '-initrd', signedZbiPath,
+      '-m', '2048',
+      '-serial', 'stdio',
+      '-vga', 'none',
+      '-device', 'virtio-keyboard-pci',
+      '-device', 'virtio_input_multi_touch_pci_1',
+      '-smp', '4,threads=2',
+      '-machine', 'q35',
+      '-device', 'isa-debug-exit,iobase=0xf4,iosize=0x04',
+      // TODO(chillers): Add hardware acceleration option to configure this.
+      '-enable-kvm',
+      '-cpu', 'host,migratable=no,+invtsc',
+      '-netdev', 'type=tap,ifname=qemu,script=no,downscript=no,id=net0',
+      '-device', 'e1000,netdev=net0,mac=52:54:00:63:5e:7a',
+      '-drive', 'file=$fvmImagePath,format=raw,if=none,id=vdisk',
+      '-device', 'virtio-blk-pci,drive=vdisk',
+      '-append',
+      // TODO(chillers): Generate entropy mixin.
+      '\'TERM=xterm-256color kernel.serial=legacy kernel.entropy-mixin=660486b6b20b4ace3fb5c81b0002abf5271289185c6a5620707606c55b377562 kernel.halt-on-panic=true\'',
+    ];
+
+    await cli.start(aemuCommand);
+
+    return OperationResult.success();
+  }
+}
diff --git a/packages/fuchsia_ctl/lib/src/image_paver.dart b/packages/fuchsia_ctl/lib/src/image_paver.dart
new file mode 100644
index 0000000..73eabf2
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/image_paver.dart
@@ -0,0 +1,139 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+import 'package:uuid/uuid.dart';
+
+import 'operation_result.dart';
+import 'ssh_key_manager.dart';
+import 'tar.dart';
+
+/// Paves a prebuilt system image to a Fuchsia device.
+///
+/// The Fuchsia device must be in zedboot mode.
+@immutable
+class ImagePaver {
+  /// Creates a new image paver.
+  ///
+  /// All properties must not be null.
+  const ImagePaver({
+    this.processManager = const LocalProcessManager(),
+    this.fs = const LocalFileSystem(),
+    this.tar = const SystemTar(processManager: LocalProcessManager()),
+    this.sshKeyManagerProvider = SystemSshKeyManager.defaultProvider,
+  })  : assert(processManager != null),
+        assert(fs != null),
+        assert(tar != null);
+
+  /// The [ProcessManager] used to launch the boot server, `tar`,
+  /// and `ssh-keygen`.
+  final ProcessManager processManager;
+
+  /// The default pave timeout as [Duration] in milliseconds.
+  static const Duration defaultPaveTimeoutMs =
+      Duration(milliseconds: 5 * 60 * 1000);
+
+  /// The [FileSystem] implementation used to
+  final FileSystem fs;
+
+  /// The implementation to use for untarring system images.
+  final Tar tar;
+
+  /// The implementation to use for creating SSH keys.
+  final SshKeyManagerProvider sshKeyManagerProvider;
+
+  /// Paves an image (in .tgz format) to the specified device.
+  ///
+  /// The `imageTgzPath` must not be null. If `deviceName` is null, the
+  /// first discoverable device will be used.
+  Future<OperationResult> pave(
+    String imageTgzPath,
+    String deviceName, {
+    String publicKeyPath,
+    bool verbose = true,
+    Duration timeoutMs = defaultPaveTimeoutMs,
+  }) async {
+    assert(imageTgzPath != null);
+    if (deviceName == null) {
+      stderr.writeln('Warning: No device name specified. '
+          'If multiple devices are attached, this may result in paving '
+          'an unexpected device.');
+    }
+    final SshKeyManager sshKeyManager = sshKeyManagerProvider(
+      processManager: processManager,
+      publicKeyPath: publicKeyPath,
+      fs: fs,
+    );
+    final String uuid = Uuid().v4();
+    final Directory imageDirectory = fs.directory('image_$uuid');
+    if (verbose) {
+      stdout.writeln('Using ${imageDirectory.path} as temp path.');
+    }
+    await imageDirectory.create();
+    final OperationResult untarResult = await tar.untar(
+      imageTgzPath,
+      imageDirectory.path,
+    );
+
+    if (!untarResult.success) {
+      if (verbose) {
+        stderr.writeln('Unpacking image $imageTgzPath failed.');
+      }
+      imageDirectory.deleteSync(recursive: true);
+      return untarResult;
+    }
+
+    final OperationResult sshResult = await sshKeyManager.createKeys();
+    if (!sshResult.success) {
+      if (verbose) {
+        stderr.writeln('Creating SSH Keys failed.');
+      }
+      imageDirectory.deleteSync(recursive: true);
+      return sshResult;
+    }
+    final Process paveProcess = await processManager.start(
+      <String>[
+        '${imageDirectory.path}/pave.sh',
+        '--fail-fast',
+        '-1', // pave once and exit
+        '--allow-zedboot-version-mismatch',
+        if (deviceName != null) ...<String>['-n', deviceName],
+        '--authorized-keys', '.ssh/authorized_keys',
+      ],
+    ).timeout(timeoutMs);
+    final StringBuffer paveStdout = StringBuffer();
+    final StringBuffer paveStderr = StringBuffer();
+    paveProcess.stdout.transform(utf8.decoder).forEach((String s) {
+      if (verbose) {
+        stdout.write(s);
+      }
+      paveStdout.write(s);
+    });
+    paveProcess.stderr.transform(utf8.decoder).forEach((String s) {
+      if (verbose) {
+        stderr.write(s);
+      }
+      paveStderr.write(s);
+    });
+    final int exitCode = await paveProcess.exitCode;
+    await stdout.flush();
+    await stderr.flush();
+    imageDirectory.deleteSync(recursive: true);
+
+    return OperationResult.fromProcessResult(
+      ProcessResult(
+        paveProcess.pid,
+        exitCode,
+        paveStdout.toString(),
+        paveStderr.toString(),
+      ),
+    );
+  }
+}
diff --git a/packages/fuchsia_ctl/lib/src/logger.dart b/packages/fuchsia_ctl/lib/src/logger.dart
new file mode 100644
index 0000000..5f908b3
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/logger.dart
@@ -0,0 +1,131 @@
+import 'dart:io';
+
+/// Defines the available log levels.
+class LogLevel {
+  const LogLevel._(this._level, this.name);
+
+  final int _level;
+
+  /// String name for the log level.
+  final String name;
+
+  /// LogLevel for messages instended for debugging.
+  static const LogLevel debug = LogLevel._(0, 'DEBUG');
+
+  /// LogLevel for messages instended to provide information about the
+  /// execution.
+  static const LogLevel info = LogLevel._(1, 'INFO');
+
+  /// LogLevel for messages instended to flag potential problems.
+  static const LogLevel warning = LogLevel._(2, 'WARN');
+
+  /// LogLevel for errors in the execution.
+  static const LogLevel error = LogLevel._(3, 'ERROR');
+}
+
+/// Abstract class for loggers.
+abstract class Logger {
+  /// Processes a debug message.
+  void debug(Object message);
+
+  /// Processes an info message.
+  void info(Object message);
+
+  /// Processes a warning message.
+  void warning(Object message);
+
+  /// Processes an error message.
+  void error(Object message);
+}
+
+/// Logger to print message to standard output.
+class PrintLogger implements Logger {
+  /// Creates a logger instance to print messages to standard output.
+  PrintLogger({
+    IOSink out,
+    bool prependLogData,
+    this.level = LogLevel.info,
+  })  : out = out ?? stdout,
+        prependLogData = prependLogData ?? true;
+
+  /// The [IOSink] to print to.
+  final IOSink out;
+
+  /// Available log levels.
+  final LogLevel level;
+
+  /// Wether to prepend datetime and log level or not.
+  final bool prependLogData;
+
+  /// Stdout buffer.
+  final StringBuffer stdoutBuffer = StringBuffer();
+
+  /// Stderr buffer.
+  final StringBuffer stderrBuffer = StringBuffer();
+
+  /// Returns all the content logged as info, debug and warning without the
+  /// datetime and log level prepended to lines.
+  String outputLog() {
+    return stdoutBuffer.toString();
+  }
+
+  /// Returns all the content logged error without the
+  /// datetime and log level prepended to lines.
+  String errorLog() {
+    return stderrBuffer.toString();
+  }
+
+  @override
+  void debug(Object message) {
+    _log(LogLevel.debug, message);
+    stdoutBuffer.writeln(message);
+  }
+
+  @override
+  void info(Object message) {
+    _log(LogLevel.info, message);
+    stdoutBuffer.writeln(message);
+  }
+
+  @override
+  void warning(Object message) {
+    _log(LogLevel.warning, message);
+    stdoutBuffer.writeln(message);
+  }
+
+  @override
+  void error(Object message) {
+    _log(LogLevel.error, message);
+    stderrBuffer.writeln(message);
+  }
+
+  void _log(LogLevel level, Object message) {
+    if (prependLogData) {
+      if (level._level >= this.level._level) {
+        out.writeln(toLogString('$message', level: level));
+      }
+    } else {
+      out.writeln('$message');
+    }
+  }
+
+  /// Flushes the IOSink to ensure all the data is written. This is specially
+  /// useful when writing to a file.
+  Future<void> flush() async {
+    await out.flush();
+  }
+}
+
+/// Transforms a [message] with [level] to a string that contains the DateTime,
+/// level and message.
+String toLogString(String message, {LogLevel level}) {
+  final StringBuffer buffer = StringBuffer();
+  buffer.write(DateTime.now().toUtc().toIso8601String());
+  buffer.write(': ');
+  if (level != null) {
+    buffer.write(level.name);
+    buffer.write(' ');
+  }
+  buffer.write(message);
+  return buffer.toString();
+}
diff --git a/packages/fuchsia_ctl/lib/src/operation_result.dart b/packages/fuchsia_ctl/lib/src/operation_result.dart
new file mode 100644
index 0000000..b4c76cb
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/operation_result.dart
@@ -0,0 +1,66 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+
+/// The result of running a command or operation.
+///
+/// This loosely corresponds to a [ProcessResult], but is not necessarily the
+/// result of running a process.
+@immutable
+class OperationResult {
+  const OperationResult._(
+    this.success, {
+    this.info = '',
+    this.error = '',
+  });
+
+  /// A successful operation result with a non-null but potentially empty info
+  /// message, and an empty error message.
+  factory OperationResult.success({
+    String info = '',
+  }) {
+    assert(info != null);
+    return OperationResult._(true, info: info, error: '');
+  }
+
+  /// A failing operation result with a non-null but potentially empty error,
+  /// and a non-null but potentially empty info.
+  factory OperationResult.error(
+    String error, {
+    String info = '',
+  }) {
+    assert(error != null);
+    assert(info != null);
+    return OperationResult._(false, info: info, error: error);
+  }
+
+  /// Creates an [OperationResult] from a [ProcessResult].
+  factory OperationResult.fromProcessResult(
+    ProcessResult result, {
+    int expectedExitCode = 0,
+  }) {
+    assert(expectedExitCode != null);
+    return OperationResult._(
+      result.exitCode == expectedExitCode,
+      info: result.stdout.toString(),
+      error: result.stderr.toString(),
+    );
+  }
+
+  /// Whether the result was successful or not. Not null.
+  final bool success;
+
+  /// Information from the result, e.g. the stdout of a process.
+  final String info;
+
+  /// Error information from the result, e.g. the stderr of a process.
+  final String error;
+
+  @override
+  String toString() =>
+      '$runtimeType{${success ? 'success' : 'failure'}, info: "$info", error: "$error"}';
+}
diff --git a/packages/fuchsia_ctl/lib/src/package_server.dart b/packages/fuchsia_ctl/lib/src/package_server.dart
new file mode 100644
index 0000000..752b34c
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/package_server.dart
@@ -0,0 +1,151 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:process/process.dart';
+import 'package:path/path.dart' as path;
+import 'package:uuid/uuid.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+/// A wrapper around the Fuchsia SDK `pm` tool.
+class PackageServer {
+  /// Creates a new package server.
+  PackageServer(
+    this.pmPath, {
+    this.processManager = const LocalProcessManager(),
+    this.fileSystem = const LocalFileSystem(),
+  })  : assert(pmPath != null),
+        assert(processManager != null),
+        assert(fileSystem != null);
+
+  /// The path on the file system to the `pm` executable.
+  final String pmPath;
+
+  /// The process manager to use for launching `pm`.
+  final ProcessManager processManager;
+
+  /// The file sytem for the package server.
+  final FileSystem fileSystem;
+
+  Process _pmServerProcess;
+
+  /// Path to the port file generated by `pm`.
+  String portPath;
+
+  /// The port the server is listening on, if the server is running.
+  ///
+  /// Throws a [StateError] if accessed when the server is not running.
+  int get serverPort {
+    if (_pmServerProcess == null) {
+      throw StateError('Attempted to get port before starting server.');
+    }
+    return _serverPort;
+  }
+
+  int _serverPort;
+
+  /// Is the server up?
+  bool get serving {
+    return _pmServerProcess != null;
+  }
+
+  /// Creates a new local repository and associated key material.
+  ///
+  /// Corresponds to `pm newrepo`.
+  Future<OperationResult> newRepo(String repo) async {
+    return OperationResult.fromProcessResult(
+      await processManager.run(
+        <String>[
+          pmPath,
+          'newrepo',
+          '-repo', repo, //
+        ],
+      ),
+    );
+  }
+
+  /// Publishes an archive package for use on a device with the specified
+  /// .far files.
+  Future<OperationResult> publishRepo(String repo, String farFile) async {
+    return OperationResult.fromProcessResult(
+      await processManager.run(
+        <String>[
+          pmPath,
+          'publish',
+          '-a',
+          '-repo', repo, //
+          '-f', farFile,
+        ],
+      ),
+    );
+  }
+
+  /// Starts a server for the specified repo path and port.
+  ///
+  /// Use port 0 to have the server choose a port. The acutal port used
+  /// will be avialalbe in the [serverPort] property after this method
+  /// returns. The stdout and stderr of the server will be printed to [stdout]
+  /// and [stderr], respectively.
+  Future<void> serveRepo(
+    String repo, {
+    String address = '',
+    int port = 0,
+    String portFilePath,
+  }) async {
+    assert(repo != null);
+    assert(port != null);
+
+    final String uuid = Uuid().v4();
+    portPath = portFilePath ??
+        path.join(fileSystem.systemTempDirectory.path, '${uuid}_port.txt');
+    final List<String> pmCommand = <String>[
+      pmPath,
+      'serve',
+      '-repo',
+      repo,
+      '-l',
+      '$address:$port',
+      '-f',
+      portPath,
+    ];
+    stdout.writeln('Running ${pmCommand.join(' ')}');
+    _pmServerProcess = await processManager.start(pmCommand);
+    await Future<void>.delayed(const Duration(seconds: 5), () async {
+      final String portString = await fileSystem.file(portPath).readAsString();
+      _serverPort = int.parse(portString);
+    });
+    _pmServerProcess.stdout
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen(stdout.writeln);
+    _pmServerProcess.stderr
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen(stderr.writeln);
+  }
+
+  /// Closes a running server.
+  ///
+  /// Calling this before calling [serveRepo] will result in a [StateError].
+  Future<OperationResult> close() async {
+    if (_pmServerProcess == null) {
+      throw StateError('Must call serveRepo before calling close.');
+    }
+    await fileSystem.file(portPath).delete();
+    _pmServerProcess.kill();
+    final int exitCode = await _pmServerProcess.exitCode;
+    _pmServerProcess = null;
+    if (exitCode == 0) {
+      return OperationResult.success();
+    }
+    return OperationResult.error(
+        'The "pm" executable exited with non-zero exit code.');
+  }
+}
diff --git a/packages/fuchsia_ctl/lib/src/ssh_client.dart b/packages/fuchsia_ctl/lib/src/ssh_client.dart
new file mode 100644
index 0000000..35d20e4
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/ssh_client.dart
@@ -0,0 +1,163 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:fuchsia_ctl/src/logger.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// A client for running SSH based commands on a Fuchsia device.
+@immutable
+class SshClient {
+  /// Creates a new SSH client.
+  ///
+  /// Relies on `ssh` being present in the $PATH.
+  ///
+  /// The `processManager` must not be null.
+  const SshClient({
+    this.processManager = const LocalProcessManager(),
+  }) : assert(processManager != null);
+
+  /// The [ProcessManager] to use for spawning `ssh`.
+  final ProcessManager processManager;
+
+  /// The default ssh timeout as [Duration] in milliseconds.
+  static const Duration defaultSshTimeoutMs =
+      Duration(milliseconds: 5 * 60 * 1000);
+
+  /// Creates a list of arguments to pass to ssh.
+  ///
+  /// This method is not intended for use outside of this library, except for
+  /// in unit tests.
+  @visibleForTesting
+  List<String> getSshArguments({
+    String identityFilePath,
+    String targetIp,
+    List<String> command = const <String>[],
+  }) {
+    assert(command != null);
+    return <String>[
+      'ssh',
+      '-o', 'CheckHostIP=no', //
+      '-o', 'StrictHostKeyChecking=no',
+      '-o', 'ForwardAgent=no',
+      '-o', 'ForwardX11=no',
+      '-o', 'GSSAPIDelegateCredentials=no',
+      '-o', 'UserKnownHostsFile=/dev/null',
+      '-o', 'User=fuchsia',
+      '-o', 'IdentitiesOnly=yes',
+      '-o', 'IdentityFile=$identityFilePath',
+      '-o', 'ControlPersist=yes',
+      '-o', 'ControlMaster=auto',
+      '-o', 'ControlPath=/tmp/fuchsia--%r@%h:%p',
+      '-o', 'ServerAliveInterval=1',
+      '-o', 'ServerAliveCountMax=10',
+      '-o', 'LogLevel=ERROR',
+      targetIp,
+      command.join(' '),
+    ];
+  }
+
+  /// Creates an interactive SSH session.
+  Future<OperationResult> interactive(
+    String targetIp, {
+    @required String identityFilePath,
+  }) async {
+    final Process ssh = await processManager.start(getSshArguments(
+      targetIp: targetIp,
+      identityFilePath: identityFilePath,
+    ));
+    ssh.stdout.transform(utf8.decoder).listen(stdout.writeln);
+    ssh.stderr.transform(utf8.decoder).listen(stderr.writeln);
+    stdin.pipe(ssh.stdin);
+
+    final int exitCode = await ssh.exitCode;
+    if (exitCode == 0) {
+      return OperationResult.success();
+    }
+    return OperationResult.error('ssh exited with code $exitCode');
+  }
+
+  /// Runs an SSH command on the specified target IP.
+  ///
+  /// A target IP can be obtained from a device node name using the
+  /// [DevFinder] class.
+  ///
+  /// All arguments must not be null.
+  Future<OperationResult> runCommand(
+    String targetIp, {
+    @required String identityFilePath,
+    @required List<String> command,
+    Duration timeoutMs = defaultSshTimeoutMs,
+    String logFilePath,
+    FileSystem fs,
+  }) async {
+    assert(targetIp != null);
+    assert(identityFilePath != null);
+    assert(command != null);
+
+    final bool logToFile = !(logFilePath == null || logFilePath.isEmpty);
+    final FileSystem fileSystem = fs ?? const LocalFileSystem();
+    PrintLogger logger;
+
+    // If no file is passed to this method we create a memoryfile to keep to
+    // return the stdout in OperationResult.
+    if (logToFile) {
+      fileSystem.file(logFilePath).existsSync() ??
+          fileSystem.file(logFilePath).deleteSync();
+      fileSystem.file(logFilePath).createSync();
+      final IOSink data = fileSystem.file(logFilePath).openWrite();
+      logger = PrintLogger(out: data);
+    } else {
+      logger = PrintLogger();
+    }
+
+    final Process process = await processManager.start(
+      getSshArguments(
+        identityFilePath: identityFilePath,
+        targetIp: targetIp,
+        command: command,
+      ),
+    );
+    final StreamSubscription<String> stdoutSubscription = process.stdout
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen((String log) {
+      logger.info(log);
+    });
+    final StreamSubscription<String> stderrSubscription = process.stderr
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen((String log) {
+      logger.error(log);
+    });
+
+    // Wait for stdout and stderr to be fully processed because proc.exitCode
+    // may complete first.
+    await Future.wait<void>(<Future<void>>[
+      stdoutSubscription.asFuture<void>(),
+      stderrSubscription.asFuture<void>(),
+    ]);
+
+    await logger.flush();
+
+    // The streams as futures have already completed, so waiting for the
+    // potentially async stream cancellation to complete likely has no benefit.
+    stdoutSubscription.cancel();
+    stderrSubscription.cancel();
+
+    final int exitCode = await process.exitCode.timeout(timeoutMs);
+
+    return exitCode != 0
+        ? OperationResult.error('Failed', info: logger.errorLog())
+        : OperationResult.success(info: logger.outputLog());
+  }
+}
diff --git a/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart b/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart
new file mode 100644
index 0000000..8fa2245
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/ssh_key_manager.dart
@@ -0,0 +1,114 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' as io;
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// Function signature for a [SshKeyManager] provider.
+typedef SshKeyManagerProvider = SshKeyManager Function({
+  ProcessManager processManager,
+  FileSystem fs,
+  String publicKeyPath,
+});
+
+/// A wrapper for managing SSH key generation.
+///
+/// Implemented by [SystemSshKeyManager].
+abstract class SshKeyManager {
+  /// Create SSH key material suitable for paving and accessing a Fuchsia image.
+  Future<OperationResult> createKeys({
+    String destinationPath = '.ssh',
+    bool force = false,
+  });
+}
+
+/// A class that delegates creating SSH keys to the system `ssh-keygen`.
+@immutable
+class SystemSshKeyManager implements SshKeyManager {
+  /// Creates a wrapper for ssh-keygen.
+  ///
+  /// The arguments must not be null, and will be used to spawn a ssh-keygen
+  /// process and manipulate the files it creates.
+  const SystemSshKeyManager({
+    this.processManager = const LocalProcessManager(),
+    this.fs = const LocalFileSystem(),
+    this.pkeyPubPath,
+  })  : assert(processManager != null),
+        assert(fs != null);
+
+  /// Creates a static provider that returns a SystemSshKeyManager.
+  static SshKeyManager defaultProvider({
+    ProcessManager processManager,
+    FileSystem fs,
+    String publicKeyPath,
+  }) {
+    return SystemSshKeyManager(
+      processManager: processManager ?? const LocalProcessManager(),
+      fs: fs ?? const LocalFileSystem(),
+      pkeyPubPath: publicKeyPath,
+    );
+  }
+
+  /// The [ProcessManager] implementation to use when spawning ssh-keygen.
+  final ProcessManager processManager;
+
+  /// The [FileSystem] implementation to use when creating the authorized_keys
+  /// file.
+  final FileSystem fs;
+
+  /// The [String] with the path to a public key.
+  final String pkeyPubPath;
+
+  /// Populates [authorizedKeys] file with the public key in [pKeyPub].
+  Future<void> createAuthorizedKeys(File authorizedKeys, File pkeyPub) async {
+    final List<String> pkeyPubParts = pkeyPub.readAsStringSync().split(' ');
+    await authorizedKeys
+        .writeAsString('${pkeyPubParts[0]} ${pkeyPubParts[1]}\n');
+  }
+
+  @override
+  Future<OperationResult> createKeys({
+    String destinationPath = '.ssh',
+    bool force = false,
+  }) async {
+    final Directory sshDir = fs.directory(destinationPath);
+    final File authorizedKeys = sshDir.childFile('authorized_keys');
+    if (authorizedKeys.existsSync() && !force) {
+      return OperationResult.success(info: 'Using previously generated keys.');
+    }
+
+    if (sshDir.existsSync()) {
+      await sshDir.delete(recursive: true);
+    }
+
+    await sshDir.create();
+    if (pkeyPubPath != null) {
+      await createAuthorizedKeys(authorizedKeys, fs.file(pkeyPubPath));
+      return OperationResult.success(info: 'Using previously generated keys.');
+    }
+
+    final File pkey = sshDir.childFile('pkey');
+    final File pkeyPub = sshDir.childFile('pkey.pub');
+    final io.ProcessResult result = await processManager.run(
+      <String>[
+        'ssh-keygen',
+        '-t', 'ed25519', //
+        '-f', pkey.path,
+        '-q',
+        '-N', '',
+      ],
+    );
+    if (result.exitCode != 0) {
+      return OperationResult.fromProcessResult(result);
+    }
+    await createAuthorizedKeys(authorizedKeys, pkeyPub);
+    return OperationResult.fromProcessResult(result);
+  }
+}
diff --git a/packages/fuchsia_ctl/lib/src/tar.dart b/packages/fuchsia_ctl/lib/src/tar.dart
new file mode 100644
index 0000000..0a55442
--- /dev/null
+++ b/packages/fuchsia_ctl/lib/src/tar.dart
@@ -0,0 +1,60 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import 'operation_result.dart';
+
+/// A wrapper for untarring artifacts.
+///
+/// Implemented by [SystemTar].
+abstract class Tar {
+  /// A const constructor to allow subclasses to create const constructors.
+  const Tar();
+
+  /// Untars a tar file.
+  Future<OperationResult> untar(
+    String src,
+    String destination, {
+    Duration timeoutMs,
+  });
+}
+
+/// The archive package is very slow and memory intensive. Use
+/// system tar.
+@immutable
+class SystemTar implements Tar {
+  /// Creates a new [SystemTar] using the specified [ProcessManager].
+  ///
+  /// The processManager parameter must not be null.
+  const SystemTar({
+    this.processManager = const LocalProcessManager(),
+  }) : assert(processManager != null);
+
+  /// The [ProcessManager] impleemntation to use when spawning the system tar
+  /// program.
+  final ProcessManager processManager;
+
+  /// The default timeout for untar operations as [Duration] in milliseconds.
+  static const Duration defaultTarTimeoutMs =
+      Duration(milliseconds: 5 * 60 * 1000);
+
+  @override
+  Future<OperationResult> untar(
+    String src,
+    String destination, {
+    Duration timeoutMs = defaultTarTimeoutMs,
+  }) async {
+    final ProcessResult result = await processManager.run(<String>[
+      'tar',
+      '-xf', src, //
+      '-C', destination,
+    ]).timeout(timeoutMs);
+
+    return OperationResult.fromProcessResult(result);
+  }
+}
diff --git a/packages/fuchsia_ctl/pubspec.lock b/packages/fuchsia_ctl/pubspec.lock
new file mode 100644
index 0000000..6fdecb7
--- /dev/null
+++ b/packages/fuchsia_ctl/pubspec.lock
@@ -0,0 +1,404 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "12.0.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.40.6"
+  args:
+    dependency: "direct main"
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.6.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.1"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.5"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.3"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.14.12"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  coverage:
+    dependency: transitive
+    description:
+      name: coverage
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.14.2"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.4"
+  file:
+    dependency: "direct main"
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.0+4"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.4"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.16.1"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.4"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.1+1"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.11.4"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.9"
+  meta:
+    dependency: "direct main"
+    description:
+      name: meta
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.3"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.6+3"
+  mockito:
+    dependency: "direct dev"
+    description:
+      name: mockito
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.1"
+  node_interop:
+    dependency: transitive
+    description:
+      name: node_interop
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  node_io:
+    dependency: transitive
+    description:
+      name: node_io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1+2"
+  node_preamble:
+    dependency: transitive
+    description:
+      name: node_preamble
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.8"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.9.3"
+  package_resolver:
+    dependency: transitive
+    description:
+      name: package_resolver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.10"
+  path:
+    dependency: "direct main"
+    description:
+      name: path
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.6.4"
+  pedantic:
+    dependency: "direct dev"
+    description:
+      name: pedantic
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.9.2"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.1"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.0"
+  process:
+    dependency: "direct main"
+    description:
+      name: process
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.12"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.4"
+  retry:
+    dependency: "direct main"
+    description:
+      name: retry
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0+1"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.5"
+  shelf_packages_handler:
+    dependency: transitive
+    description:
+      name: shelf_packages_handler
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.4"
+  shelf_static:
+    dependency: transitive
+    description:
+      name: shelf_static
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.8"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.3"
+  source_map_stack_trace:
+    dependency: transitive
+    description:
+      name: source_map_stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  source_maps:
+    dependency: transitive
+    description:
+      name: source_maps
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.9"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.7.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.9.3"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.5"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  test:
+    dependency: "direct dev"
+    description:
+      name: test
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.15.5"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.18+1"
+  test_core:
+    dependency: transitive
+    description:
+      name: test_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.11+2"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.6"
+  uuid:
+    dependency: "direct main"
+    description:
+      name: uuid
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.7+14"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  webkit_inspection_protocol:
+    dependency: transitive
+    description:
+      name: webkit_inspection_protocol
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.4"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+sdks:
+  dart: ">=2.7.0 <3.0.0"
diff --git a/packages/fuchsia_ctl/pubspec.yaml b/packages/fuchsia_ctl/pubspec.yaml
new file mode 100644
index 0000000..9106bb1
--- /dev/null
+++ b/packages/fuchsia_ctl/pubspec.yaml
@@ -0,0 +1,24 @@
+name: fuchsia_ctl
+description: >
+  A Dart package for paving, serving packages to, and running commands on a
+  Fuchsia Device. This package is primarily intended for use in Flutter's
+  continuous integration/testing infrastructure.
+homepage: https://github.com/flutter/packages/tree/master/packages/fuchsia_ctl
+version: 0.0.23
+
+environment:
+  sdk: ">=2.4.0 <3.0.0"
+
+dependencies:
+  args: ^1.5.2
+  file: ^5.0.10
+  meta: ^1.1.7
+  path: ^1.6.4
+  process: ^3.0.12
+  retry: ^3.0.0+1
+  uuid: ^2.0.2
+
+dev_dependencies:
+  mockito: ^4.1.1
+  pedantic: 1.9.2
+  test: ^1.15.5
diff --git a/packages/fuchsia_ctl/test/command_line_test.dart b/packages/fuchsia_ctl/test/command_line_test.dart
new file mode 100644
index 0000000..38faecd
--- /dev/null
+++ b/packages/fuchsia_ctl/test/command_line_test.dart
@@ -0,0 +1,78 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'package:fuchsia_ctl/src/command_line.dart';
+
+import 'fakes.dart';
+
+void main() {
+  CommandLine cli;
+  MockProcessManager mockProcessManager;
+  MockStdout mockStdout;
+  MockStdout mockStderr;
+
+  group('CommandLine', () {
+    setUp(() {
+      mockStdout = MockStdout();
+      mockStderr = MockStdout();
+      mockProcessManager = MockProcessManager();
+      cli = CommandLine(
+          processManager: mockProcessManager,
+          stderrValue: mockStderr,
+          stdoutValue: mockStdout);
+    });
+
+    test('run writes to stdout', () async {
+      when(mockProcessManager.run(any))
+          .thenAnswer((_) async => ProcessResult(123, 0, 'stdout123', ''));
+
+      await cli.run(<String>['test']);
+
+      verify(mockStdout.writeln('stdout123')).called(1);
+    });
+
+    test('run writes to stderr', () async {
+      when(mockProcessManager.run(any))
+          .thenAnswer((_) async => ProcessResult(123, 0, '', 'stderrABC'));
+
+      await cli.run(<String>['test']);
+
+      verify(mockStderr.writeln('stderrABC')).called(1);
+    });
+
+    test('run throws on non-0 exit code', () async {
+      when(mockProcessManager.run(any))
+          .thenAnswer((_) async => ProcessResult(123, 1, '', 'stderrABC'));
+
+      expect(cli.run(<String>['test']), throwsA(isA<CommandLineException>()));
+    });
+
+    test('start adds streams', () async {
+      when(mockProcessManager.start(any))
+          .thenAnswer((_) async => FakeProcess(0, <String>[''], <String>['']));
+
+      await cli.start(<String>['test']);
+
+      verify(mockStdout.addStream(any)).called(1);
+      verify(mockStderr.addStream(any)).called(1);
+    });
+
+    test('start throws on non-0 exit code', () async {
+      when(mockProcessManager.start(any))
+          .thenAnswer((_) async => FakeProcess(1, <String>[''], <String>['']));
+
+      expect(cli.start(<String>['test']), throwsA(isA<CommandLineException>()));
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockStdout extends Mock implements Stdout {}
diff --git a/packages/fuchsia_ctl/test/dev_finder_test.dart b/packages/fuchsia_ctl/test/dev_finder_test.dart
new file mode 100644
index 0000000..4549a6a
--- /dev/null
+++ b/packages/fuchsia_ctl/test/dev_finder_test.dart
@@ -0,0 +1,120 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+void main() {
+  const String devFinderPath = 'device-finder';
+  const String targetIp = 'target_ip';
+  const String localIp = 'local_ip';
+
+  test('finds local address with no device name', () async {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(mockProcessManager.run(<String>[
+      devFinderPath,
+      'list',
+      '-device-limit',
+      '1',
+      '-local',
+    ])).thenAnswer((_) async => ProcessResult(123, 0, localIp, ''));
+
+    final DevFinder devFinder = DevFinder(
+      devFinderPath,
+      processManager: mockProcessManager,
+    );
+
+    expect(await devFinder.getLocalAddress(null), localIp);
+  });
+
+  test('finds local address with device name', () async {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(mockProcessManager.run(<String>[
+      devFinderPath,
+      'resolve',
+      '-device-limit',
+      '1',
+      '-local',
+      'devicename',
+    ])).thenAnswer((_) async => ProcessResult(123, 0, localIp, ''));
+
+    final DevFinder devFinder = DevFinder(
+      devFinderPath,
+      processManager: mockProcessManager,
+    );
+
+    expect(await devFinder.getLocalAddress('devicename'), localIp);
+  });
+
+  test('finds target address with no device name', () async {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(mockProcessManager.run(<String>[
+      devFinderPath,
+      'list',
+      '-device-limit',
+      '1',
+    ])).thenAnswer((_) async => ProcessResult(123, 0, targetIp, ''));
+
+    final DevFinder devFinder = DevFinder(
+      devFinderPath,
+      processManager: mockProcessManager,
+    );
+
+    expect(await devFinder.getTargetAddress(null), targetIp);
+  });
+
+  test('finds target address with device name', () async {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(mockProcessManager.run(<String>[
+      devFinderPath,
+      'resolve',
+      '-device-limit',
+      '1',
+      'devicename',
+    ])).thenAnswer((_) async => ProcessResult(123, 0, targetIp, ''));
+
+    final DevFinder devFinder = DevFinder(
+      devFinderPath,
+      processManager: mockProcessManager,
+    );
+
+    expect(await devFinder.getTargetAddress('devicename'), targetIp);
+  });
+
+  test('retries', () async {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+
+    int tries = 0;
+    when(mockProcessManager.run(<String>[
+      devFinderPath,
+      'resolve',
+      '-device-limit',
+      '1',
+      'devicename',
+    ])).thenAnswer((_) async {
+      tries++;
+      if (tries < 4) {
+        return ProcessResult(123, 1, '', 'Simulating device not ready yet...');
+      }
+      return ProcessResult(123, 0, targetIp, '');
+    });
+
+    final DevFinder devFinder = DevFinder(
+      devFinderPath,
+      processManager: mockProcessManager,
+    );
+
+    expect(
+      await devFinder.getTargetAddress('devicename', sleepDelay: 0),
+      targetIp,
+    );
+    expect(tries, 4);
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/emulator_test.dart b/packages/fuchsia_ctl/test/emulator_test.dart
new file mode 100644
index 0000000..09f757a
--- /dev/null
+++ b/packages/fuchsia_ctl/test/emulator_test.dart
@@ -0,0 +1,114 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'package:file/memory.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+
+import 'fakes.dart';
+
+void main() {
+  const String aemuPath = '/emulator';
+  const String fuchsiaImagePath = '/fuchsia.blk';
+  const String fuchsiaSdkPath = '/fuchsia-sdk';
+  const String qemuKernelPath = '/qemu-kernel.kernel';
+  const String zbiPath = '/zircon-a.zbi';
+
+  Emulator emulator;
+  MockCommandLine mockCli;
+  MemoryFileSystem fs;
+
+  group('Emulator', () {
+    setUp(() {
+      mockCli = MockCommandLine();
+      fs = MemoryFileSystem(style: FileSystemStyle.posix);
+      fs.file(aemuPath).createSync();
+      fs.file(fuchsiaImagePath)
+        ..createSync()
+        ..writeAsString('fuchsia image content');
+      fs.file(fuchsiaSdkPath).createSync();
+      fs.file(qemuKernelPath).createSync();
+      fs.file(zbiPath).createSync();
+      emulator = Emulator(
+        aemuPath: aemuPath,
+        fs: fs,
+        fuchsiaImagePath: fuchsiaImagePath,
+        fuchsiaSdkPath: fuchsiaSdkPath,
+        cli: mockCli,
+        qemuKernelPath: qemuKernelPath,
+        sshKeyManager: MockSshKeyManager(),
+        zbiPath: zbiPath,
+      );
+    });
+
+    test('prepare environment runs as expected', () async {
+      when(mockCli.run(
+              argThat(contains('$fuchsiaSdkPath/${emulator.zbiToolPath}'))))
+          .thenAnswer((_) async {
+        fs.file(emulator.signedZbiPath).createSync();
+      });
+
+      await emulator.prepareEnvironment();
+
+      expect(fs.isFileSync(emulator.fvmImagePath), isTrue);
+      expect(fs.isFileSync(emulator.signedZbiPath), isTrue);
+    });
+
+    test('prepare environment throws on file not existing', () async {
+      fs.file(fuchsiaImagePath).deleteSync();
+
+      expect(emulator.prepareEnvironment(), throwsA(isA<AssertionError>()));
+    });
+
+    test('start throws when prepare enviornment was not called', () async {
+      expect(emulator.start(), throwsA(isA<AssertionError>()));
+
+      verifyNever(mockCli.start(any));
+    });
+
+    test('start uses default window size', () async {
+      emulator.fvmImagePath = '/fvm.blk';
+      emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+      fs.file(emulator.fvmImagePath).createSync();
+      fs.file(emulator.signedZbiPath).createSync();
+
+      when(mockCli.start(any))
+          .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+      await emulator.start();
+      verify(mockCli.start(argThat(contains(emulator.defaultWindowSize))))
+          .called(1);
+    });
+
+    test('start configures window size', () async {
+      emulator.fvmImagePath = '/fvm.blk';
+      emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+      fs.file(emulator.fvmImagePath).createSync();
+      fs.file(emulator.signedZbiPath).createSync();
+
+      when(mockCli.start(any))
+          .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+      const String customWindowSize = '50x200';
+      await emulator.start(windowSize: customWindowSize);
+      verify(mockCli.start(argThat(contains(customWindowSize)))).called(1);
+    });
+
+    test('start configures headless', () async {
+      emulator.fvmImagePath = '/fvm.blk';
+      emulator.signedZbiPath = '/fuchsia-ssh.zbi';
+      fs.file(emulator.fvmImagePath).createSync();
+      fs.file(emulator.signedZbiPath).createSync();
+
+      when(mockCli.start(any))
+          .thenAnswer((_) async => FakeProcess(0, <String>[], <String>[]));
+
+      await emulator.start(headless: true);
+      verify(mockCli.start(argThat(contains(emulator.aemuHeadlessFlag))))
+          .called(1);
+    });
+  });
+}
diff --git a/packages/fuchsia_ctl/test/fakes.dart b/packages/fuchsia_ctl/test/fakes.dart
new file mode 100644
index 0000000..19ebe5c
--- /dev/null
+++ b/packages/fuchsia_ctl/test/fakes.dart
@@ -0,0 +1,85 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:mockito/mockito.dart';
+
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/command_line.dart';
+
+class FakeProcess implements Process {
+  FakeProcess(this._exitCode, this._stdout, this._stderr);
+
+  final int _exitCode;
+  @override
+  Future<int> get exitCode async => _exitCode;
+
+  bool _killed = false;
+  bool get killed => _killed;
+
+  @override
+  bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
+    _killed = true;
+    return true;
+  }
+
+  @override
+  int get pid => 1234;
+
+  Stream<List<int>> _streamFromString(List<String> source) =>
+      Stream<List<int>>.fromIterable(
+          source.map((String line) => utf8.encode('$line\n')));
+
+  final List<String> _stderr;
+  @override
+  Stream<List<int>> get stderr => _streamFromString(_stderr);
+
+  @override
+  IOSink get stdin => FakeIOSink();
+
+  final List<String> _stdout;
+  @override
+  Stream<List<int>> get stdout => _streamFromString(_stdout);
+}
+
+class FakeIOSink implements IOSink {
+  final Completer<void> _doneCompleter = Completer<void>.sync();
+
+  @override
+  Encoding encoding = utf8;
+
+  @override
+  void add(List<int> data) {}
+
+  @override
+  void addError(Object error, [StackTrace stackTrace]) {}
+
+  @override
+  Future<dynamic> addStream(Stream<List<int>> stream) async {}
+
+  @override
+  Future<void> close() async {}
+
+  @override
+  Future<void> get done => _doneCompleter.future;
+
+  @override
+  Future<void> flush() async {}
+
+  @override
+  void write(Object obj) {}
+
+  @override
+  void writeAll(Iterable<dynamic> objects, [String separator = '']) {}
+
+  @override
+  void writeCharCode(int charCode) {}
+
+  @override
+  void writeln([Object obj = '']) {}
+}
+
+// ignore: must_be_immutable
+class MockCommandLine extends Mock implements CommandLine {}
+
+class MockSshKeyManager extends Mock implements SshKeyManager {}
diff --git a/packages/fuchsia_ctl/test/image_paver_test.dart b/packages/fuchsia_ctl/test/image_paver_test.dart
new file mode 100644
index 0000000..41888b8
--- /dev/null
+++ b/packages/fuchsia_ctl/test/image_paver_test.dart
@@ -0,0 +1,190 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:async';
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/image_paver.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:fuchsia_ctl/src/ssh_key_manager.dart';
+import 'package:fuchsia_ctl/src/tar.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+  const String deviceName = 'some-name-to-use';
+  FakeSshKeyManager sshKeyManager;
+  final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
+  FakeTar tar;
+  MockProcessManager processManager;
+  SshKeyManagerProvider sshKeyManagerProvider;
+
+  setUp(() {
+    processManager = MockProcessManager();
+    sshKeyManager = const FakeSshKeyManager(true);
+    sshKeyManagerProvider = ({
+      ProcessManager processManager,
+      FileSystem fs,
+      String publicKeyPath,
+    }) {
+      return sshKeyManager;
+    };
+  });
+
+  test('Tar fails', () async {
+    tar = FakeTar(false, fs);
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>['Good job'], <String>['']);
+    });
+
+    final ImagePaver paver = ImagePaver(
+      tar: tar,
+      sshKeyManagerProvider: sshKeyManagerProvider,
+      fs: fs,
+      processManager: processManager,
+    );
+
+    final OperationResult result = await paver.pave(
+      'generic-x64.tgz',
+      deviceName,
+      verbose: false,
+    );
+
+    verifyNever(processManager.start(any));
+    expect(result.success, false);
+    expect(result.error, 'tar failed');
+  });
+
+  test('Ssh fails', () async {
+    sshKeyManager = const FakeSshKeyManager(false);
+    tar = FakeTar(true, fs);
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>['Good job'], <String>['']);
+    });
+
+    final ImagePaver paver = ImagePaver(
+      tar: tar,
+      sshKeyManagerProvider: sshKeyManagerProvider,
+      fs: fs,
+      processManager: processManager,
+    );
+
+    final OperationResult result = await paver.pave(
+      'generic-x64.tgz',
+      deviceName,
+      verbose: false,
+    );
+
+    verifyNever(processManager.start(any));
+    expect(result.success, false);
+    expect(result.error, 'ssh failed');
+  });
+
+  test('Pave times out', () async {
+    tar = FakeTar(true, fs);
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      return Future<Process>.delayed(const Duration(milliseconds: 10), () {
+        return FakeProcess(0, <String>['Good job'], <String>['']);
+      });
+    });
+
+    final ImagePaver paver = ImagePaver(
+      tar: tar,
+      sshKeyManagerProvider: sshKeyManagerProvider,
+      fs: fs,
+      processManager: processManager,
+    );
+
+    try {
+      await paver.pave(
+        'generic-x64.tgz',
+        deviceName,
+        verbose: false,
+        timeoutMs: const Duration(milliseconds: 1),
+      );
+    } catch (e) {
+      expect(e, isA<TimeoutException>());
+    }
+  });
+
+  test('Happy path', () async {
+    tar = FakeTar(true, fs);
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>['Good job'], <String>['']);
+    });
+
+    final ImagePaver paver = ImagePaver(
+      tar: tar,
+      sshKeyManagerProvider: sshKeyManagerProvider,
+      fs: fs,
+      processManager: processManager,
+    );
+
+    final OperationResult result = await paver.pave(
+      'generic-x64.tgz',
+      deviceName,
+      verbose: false,
+    );
+
+    final List<String> capturedStartArgs = verify(
+      processManager.start(captureAny),
+    ).captured.cast<List<String>>().single;
+    expect(capturedStartArgs.first, endsWith('/pave.sh'));
+    expect(capturedStartArgs.skip(1).toList(), <String>[
+      '--fail-fast',
+      '-1',
+      '--allow-zedboot-version-mismatch',
+      '-n', deviceName, //
+      '--authorized-keys', '.ssh/authorized_keys',
+    ]);
+    expect(result.success, true);
+  });
+}
+
+class FakeTar implements Tar {
+  const FakeTar(this.passes, this.fs)
+      : assert(passes != null),
+        assert(fs != null);
+
+  final bool passes;
+  final MemoryFileSystem fs;
+
+  @override
+  Future<OperationResult> untar(String src, String destination,
+      {Duration timeoutMs}) async {
+    if (passes) {
+      final Directory dir = fs.directory(destination)
+        ..createSync(recursive: true);
+      dir.childFile('pave.sh').createSync();
+      return OperationResult.success();
+    }
+    return OperationResult.error('tar failed');
+  }
+}
+
+class FakeSshKeyManager implements SshKeyManager {
+  const FakeSshKeyManager(this.passes);
+
+  final bool passes;
+
+  @override
+  Future<OperationResult> createKeys({
+    String destinationPath = '.ssh',
+    bool force = false,
+  }) async {
+    if (passes) {
+      return OperationResult.success();
+    }
+    return OperationResult.error('ssh failed');
+  }
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/logger_test.dart b/packages/fuchsia_ctl/test/logger_test.dart
new file mode 100644
index 0000000..4e22f98
--- /dev/null
+++ b/packages/fuchsia_ctl/test/logger_test.dart
@@ -0,0 +1,58 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/src/logger.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('PrintLogger with file logs data correctly', () async {
+    final MemoryFileSystem fs = MemoryFileSystem();
+    fs.file('log.txt').createSync();
+    final IOSink data = fs.file('log.txt').openWrite();
+    final PrintLogger logger = PrintLogger(out: data, level: LogLevel.debug);
+    logger.debug('abc');
+    logger.info('cdf');
+    logger.warning('gh');
+    logger.error('jk');
+    await logger.flush();
+    final String content = fs.file('log.txt').readAsStringSync();
+    expect(content, contains('ERROR jk'));
+    expect(content, contains('INFO cdf'));
+    expect(content, contains('WARN gh'));
+    expect(content, contains('DEBUG abc'));
+  });
+  test('PrintLogger with no file logs data correctly', () async {
+    final PrintLogger logger = PrintLogger();
+    logger.debug('abc');
+    logger.info('cdf');
+    logger.warning('gh');
+    logger.error('jk');
+    final String outContent = logger.outputLog();
+    final String errContent = logger.errorLog();
+    expect(errContent, contains('jk\n'));
+    expect(outContent, contains('cdf\n'));
+    expect(outContent, contains('gh\n'));
+    expect(outContent, contains('abc\n'));
+  });
+
+  test('PrintLogger with file logs logs only data above level', () async {
+    final MemoryFileSystem fs = MemoryFileSystem();
+    fs.file('log.txt').createSync();
+    final IOSink data = fs.file('log.txt').openWrite();
+    final PrintLogger logger = PrintLogger(out: data, level: LogLevel.info);
+    logger.debug('abc');
+    logger.info('cdf');
+    logger.warning('gh');
+    logger.error('jk');
+    await logger.flush();
+    final String content = fs.file('log.txt').readAsStringSync();
+    expect(content, contains('ERROR jk'));
+    expect(content, contains('INFO cdf'));
+    expect(content, contains('WARN gh'));
+    expect(content, isNot(contains('DEBUG abc')));
+  });
+}
diff --git a/packages/fuchsia_ctl/test/package_server_test.dart b/packages/fuchsia_ctl/test/package_server_test.dart
new file mode 100644
index 0000000..7a49d23
--- /dev/null
+++ b/packages/fuchsia_ctl/test/package_server_test.dart
@@ -0,0 +1,146 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:io' show ProcessResult;
+import 'dart:math' show Random;
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/package_server.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+  const String pmBin = 'pm';
+  const String repoPath = '/repo';
+
+  test('newRepo', () async {
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.run(any)).thenAnswer((_) async {
+      return ProcessResult(0, 0, 'good job', '');
+    });
+
+    final PackageServer server = PackageServer(
+      pmBin,
+      processManager: processManager,
+    );
+
+    final OperationResult result = await server.newRepo(repoPath);
+
+    final List<String> capturedStartArgs =
+        verify(processManager.run(captureAny))
+            .captured
+            .cast<List<String>>()
+            .single;
+
+    expect(capturedStartArgs, <String>[pmBin, 'newrepo', '-repo', repoPath]);
+    expect(result.success, true);
+  });
+
+  test('publishRepo', () async {
+    const String farFile = 'flutter_runner-0.far';
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.run(any)).thenAnswer((_) async {
+      return ProcessResult(0, 0, 'good job', '');
+    });
+
+    final PackageServer server = PackageServer(
+      pmBin,
+      processManager: processManager,
+    );
+
+    final OperationResult result = await server.publishRepo(repoPath, farFile);
+
+    final List<String> capturedStartArgs =
+        verify(processManager.run(captureAny))
+            .captured
+            .cast<List<String>>()
+            .single;
+
+    expect(capturedStartArgs, <String>[
+      pmBin,
+      'publish',
+      '-a',
+      '-repo',
+      repoPath,
+      '-f',
+      farFile,
+    ]);
+    expect(result.success, true);
+  });
+
+  test('serveRepo', () async {
+    final MockProcessManager processManager = MockProcessManager();
+    final int randomPort = Random().nextInt(60000);
+    final FakeProcess serverProcess = FakeProcess(
+      0,
+      <String>[
+        '',
+      ],
+      <String>[''],
+    );
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      return serverProcess;
+    });
+
+    final MemoryFileSystem fs = MemoryFileSystem();
+
+    final PackageServer server = PackageServer(
+      pmBin,
+      processManager: processManager,
+      fileSystem: fs,
+    );
+
+    expect(server.serving, false);
+    final File portFile = fs.file(
+      'port.txt',
+    )
+      ..create()
+      ..writeAsString(
+        randomPort.toString(),
+      );
+
+    await server.serveRepo(
+      repoPath,
+      port: 0,
+      portFilePath: portFile.path,
+    );
+    expect(server.serving, true);
+
+    final List<String> capturedStartArgs =
+        verify(processManager.start(captureAny))
+            .captured
+            .cast<List<String>>()
+            .single;
+
+    expect(capturedStartArgs, <String>[
+      pmBin,
+      'serve',
+      '-repo',
+      repoPath,
+      '-l',
+      ':0',
+      '-f',
+      'port.txt',
+    ]);
+    expect(server.serverPort, randomPort);
+
+    final OperationResult result = await server.close();
+
+    expect(result.success, true);
+    expect(serverProcess.killed, true);
+
+    expect(server.serving, false);
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/ssh_client_test.dart b/packages/fuchsia_ctl/test/ssh_client_test.dart
new file mode 100644
index 0000000..c5428c4
--- /dev/null
+++ b/packages/fuchsia_ctl/test/ssh_client_test.dart
@@ -0,0 +1,140 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:async';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/fuchsia_ctl.dart';
+import 'package:fuchsia_ctl/src/ssh_client.dart';
+import 'package:fuchsia_ctl/src/operation_result.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+import 'fakes.dart';
+
+void main() {
+  const String targetIp = '127.0.0.2';
+  const String identityFilePath = '.ssh/pkey';
+
+  test('interactive', () async {
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>[''], <String>['']);
+    });
+
+    final SshClient ssh = SshClient(processManager: processManager);
+
+    final OperationResult result = await ssh.interactive(
+      targetIp,
+      identityFilePath: identityFilePath,
+    );
+
+    final List<String> capturedStartArgs =
+        verify(processManager.start(captureAny))
+            .captured
+            .cast<List<String>>()
+            .single;
+
+    expect(
+        capturedStartArgs,
+        ssh.getSshArguments(
+          identityFilePath: identityFilePath,
+          targetIp: targetIp,
+        ));
+    expect(result.success, true);
+  });
+
+  test('command', () async {
+    const List<String> command = <String>['ls', '-al'];
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>['abc'], <String>['cdf']);
+    });
+
+    final SshClient ssh = SshClient(processManager: processManager);
+
+    final OperationResult result = await ssh.runCommand(
+      targetIp,
+      identityFilePath: identityFilePath,
+      command: command,
+    );
+
+    final List<String> capturedStartArgs =
+        verify(processManager.start(captureAny))
+            .captured
+            .cast<List<String>>()
+            .single;
+
+    expect(
+        capturedStartArgs,
+        ssh.getSshArguments(
+          identityFilePath: identityFilePath,
+          targetIp: targetIp,
+          command: command,
+        ));
+    expect(result.info, 'abc\n');
+    expect(result.success, true);
+  });
+
+  test('Command output is written to a log file', () async {
+    const List<String> command = <String>['ls', '-al'];
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      return FakeProcess(0, <String>['ef'], <String>['abc']);
+    });
+
+    final SshClient ssh = SshClient(processManager: processManager);
+    final FileSystem fs = MemoryFileSystem();
+    final OperationResult result = await ssh.runCommand(
+      targetIp,
+      identityFilePath: identityFilePath,
+      command: command,
+      fs: fs,
+      logFilePath: 'myfile.txt',
+    );
+
+    final String content = await fs.file('myfile.txt').readAsString();
+    expect(content, contains('ERROR abc'));
+    expect(content, contains('INFO ef'));
+    expect(result.success, true);
+  });
+
+  test('getSshArgs', () {
+    const SshClient ssh = SshClient();
+    final List<String> args = ssh.getSshArguments(
+      identityFilePath: identityFilePath,
+      targetIp: targetIp,
+      command: const <String>['ls', '-al'],
+    );
+    expect(args.last, 'ls -al');
+  });
+
+  test('sshCommand times out', () async {
+    final MockProcessManager processManager = MockProcessManager();
+
+    when(processManager.start(any)).thenAnswer((_) async {
+      await Future<void>.delayed(const Duration(milliseconds: 3));
+      return FakeProcess(0, <String>[''], <String>['']);
+    });
+
+    final SshClient ssh = SshClient(processManager: processManager);
+    try {
+      await ssh.runCommand(
+        targetIp,
+        identityFilePath: identityFilePath,
+        command: const <String>['ls', '-al'],
+        timeoutMs: const Duration(milliseconds: 1),
+      );
+    } catch (e) {
+      expect(e, isA<TimeoutException>());
+    }
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/ssh_key_manager_test.dart b/packages/fuchsia_ctl/test/ssh_key_manager_test.dart
new file mode 100644
index 0000000..37c773c
--- /dev/null
+++ b/packages/fuchsia_ctl/test/ssh_key_manager_test.dart
@@ -0,0 +1,63 @@
+// @dart = 2.4
+import 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:path/path.dart' as path;
+import 'package:file/memory.dart';
+import 'package:fuchsia_ctl/src/ssh_key_manager.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('SystemSshKeyManager', () {
+    MemoryFileSystem fs;
+    final MockProcessManager processManager = MockProcessManager();
+
+    setUp(() async {
+      fs = MemoryFileSystem();
+    });
+    test('CreateAuthorizedKeys', () async {
+      const SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager();
+      final File authorizedKeys = fs.file('authorized_keys');
+      final File pkeyPub = fs.file('key.pub');
+      pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+      systemSshKeyManager.createAuthorizedKeys(authorizedKeys, pkeyPub);
+      final String result = await authorizedKeys.readAsString();
+      expect(result, equals('ssh-rsa AAAA==\n'));
+    });
+
+    test('KeysNotGenerated_PubKeyPassed', () async {
+      final File pkeyPub = fs.file('key.pub');
+      pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+      final SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager(
+          processManager: processManager, fs: fs, pkeyPubPath: pkeyPub.path);
+      await systemSshKeyManager.createKeys();
+      final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
+      final String result = await authorizedKeys.readAsString();
+      expect(result, equals('ssh-rsa AAAA==\n'));
+      verifyNever(processManager.run(any));
+    });
+
+    test('KeysGenerated', () async {
+      when(processManager.run(any)).thenAnswer((_) async {
+        final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
+        pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
+        final File pkey = fs.file(path.join('.ssh', 'pkey'));
+        pkey.create();
+        return ProcessResult(0, 0, 'Good job', '');
+      });
+      final SystemSshKeyManager systemSshKeyManager =
+          SystemSshKeyManager(processManager: processManager, fs: fs);
+      await systemSshKeyManager.createKeys();
+      final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
+      expect(await authorizedKeys.exists(), isTrue);
+      final File pkey = fs.file(path.join('.ssh', 'pkey'));
+      expect(await pkey.exists(), isTrue);
+      final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
+      expect(await pkeyPub.exists(), isTrue);
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/test/tar_test.dart b/packages/fuchsia_ctl/test/tar_test.dart
new file mode 100644
index 0000000..ee08006
--- /dev/null
+++ b/packages/fuchsia_ctl/test/tar_test.dart
@@ -0,0 +1,37 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// @dart = 2.4
+import 'dart:async';
+import 'dart:io';
+
+import 'package:process/process.dart';
+import 'package:test/test.dart';
+import 'package:fuchsia_ctl/src/tar.dart';
+import 'package:mockito/mockito.dart';
+
+void main() {
+  group('Tar', () {
+    test('Untar file times out', () async {
+      final MockProcessManager processManager = MockProcessManager();
+
+      when(processManager.run(any)).thenAnswer((_) async {
+        await Future<void>.delayed(const Duration(milliseconds: 3));
+        return ProcessResult(0, 0, 'Good job', '');
+      });
+
+      final Tar tar = SystemTar(processManager: processManager);
+      try {
+        await tar.untar(
+          'source.tar',
+          '/destination',
+          timeoutMs: const Duration(milliseconds: 1),
+        );
+      } catch (e) {
+        expect(e, isA<TimeoutException>());
+      }
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/fuchsia_ctl/tool/build.sh b/packages/fuchsia_ctl/tool/build.sh
new file mode 100755
index 0000000..8b0d0ad
--- /dev/null
+++ b/packages/fuchsia_ctl/tool/build.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Copyright 2019 The Flutter Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+set -e
+
+command -v cipd > /dev/null || {
+  echo "Please install CIPD (available from depot_tools) and add to path first.";
+  exit -1;
+}
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd)"
+
+cipd ensure -ensure-file $DIR/ensure_file -root $DIR
+
+pushd $DIR/..
+
+if [[ -d "build" ]]; then
+  echo "Please remove the build directory before proceeding"
+  exit -1
+fi
+
+mkdir -p build
+
+tool/dart-sdk/bin/pub get
+
+tool/dart-sdk/bin/dart2native bin/main.dart -o build/fuchsia_ctl
+cp -f LICENSE build/
+
+popd
diff --git a/packages/fuchsia_ctl/tool/ensure_file b/packages/fuchsia_ctl/tool/ensure_file
new file mode 100644
index 0000000..5d31ed5
--- /dev/null
+++ b/packages/fuchsia_ctl/tool/ensure_file
@@ -0,0 +1,3 @@
+$ServiceURL https://chrome-infra-packages.appspot.com/
+
+dart/dart-sdk/${os}-${arch} stable
\ No newline at end of file
diff --git a/packages/imitation_game/CHANGELOG.md b/packages/imitation_game/CHANGELOG.md
new file mode 100644
index 0000000..d775621
--- /dev/null
+++ b/packages/imitation_game/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* Initial version (TBD).
diff --git a/packages/imitation_game/LICENSE b/packages/imitation_game/LICENSE
new file mode 100644
index 0000000..bc67b8f
--- /dev/null
+++ b/packages/imitation_game/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/imitation_game/README.md b/packages/imitation_game/README.md
new file mode 100644
index 0000000..c416f83
--- /dev/null
+++ b/packages/imitation_game/README.md
@@ -0,0 +1,110 @@
+# Imitation Game
+
+## Description
+
+`imitation_game` is a platform for performing automated tests to compare the
+performance of different UI frameworks.  For example, how much memory does the
+same app written in Flutter and UIKit take?
+
+## Running all the tests
+
+You need a mobile device plugged into your computer and setup for development.
+The mobile device and the computer need to be on the same network, one that
+allows communication between computers since that's how the mobile phone will
+report its results to the computer.
+
+```sh
+dart imitation_game.dart
+```
+
+## Dependencies
+
+In order to run the tests you will need the union of all the platforms being
+tested.  As new tests are added please add to this list:
+
+### iOS
+
+- Flutter
+- Xcode
+- [ios_deploy](https://github.com/ios-control/ios-deploy) - used to launch apps
+  on the attached iOS device.
+
+## Example File Layout
+
+```text
+./
+├─ imitation_game.dart
+└─ imitation_tests/
+   ├─ smiley/
+   │  ├─ README.md
+   │  ├─ flutter/
+   │  │  ├─ run_ios.sh
+   │  │  └─ <flutter project files>
+   │  └─ uikit/
+   │     ├─ run_ios.sh
+   │     └─ <uikit project files>
+   └─ memory/
+      ├─ README.md
+      ├─ flutter/
+      │  ├─ run_ios.sh
+      │  └─ <flutter project files>
+      └─ uikit/
+         ├─ run_ios.sh
+         └─ <uikit project files>
+```
+
+Here there are 2 different tests with 2 different platform implementations.  The
+tests are named `smiley` and `memory`, they are both implemented on the
+platforms `flutter` and `uikit`.
+
+### Adding a test
+
+Tests should comprise of implementations on one or more platform.  The directory
+for the test should be added to `./tests`.  Inside that directory there should
+be a directory of implementations and a `README.md` file that explains the test.
+
+### Adding an implementation to a test
+
+An implementation has to follow these rules:
+
+- It needs to perform the same operations as the other implementations and
+  follow the description in the test's `README.md`.
+- It needs to contain a `run_ios.sh` script that will build and launch the test
+  on the connected device.
+- It should contain a file named `ip.txt` which will be overwritten by
+  `imitation_game.dart` with the ip address and port that should be used to
+  report results to.
+- It needs to report its results to the ip and port in the `ip.txt` via an HTTP
+  POST of JSON data.
+
+## Data format for results
+
+```json
+{
+  "test": "name_of_test",
+  "platform": "name_of_platform",
+  "results": {
+    "some_result_name": 1.23,
+    "some_result_name2": 4.56,
+  }
+}
+```
+
+A single test run can report multiple numbers.
+
+## Results
+Date created: 2020-09-09 17:18:15.594586Z
+Flutter 1.22.0-10.0.pre.95 • channel master • https:&#x2F;&#x2F;github.com&#x2F;flutter&#x2F;flutter.git
+Framework • revision d2fa384c31 (21 hours ago) • 2020-09-08 13:15:06 -0700
+Engine • revision d1d848e842
+Tools • Dart 2.10.0 (build 2.10.0-98.0.dev)
+
+- smiley
+    - flutter
+      - ios_startup_time: 0.691717
+      - adb_memory_total: 43179.0
+    - uikit
+      - ios_startup_time: 0.2632870674133301
+    - android
+      - adb_memory_total: 97941.0
+
diff --git a/packages/imitation_game/bin/imitation_game.dart b/packages/imitation_game/bin/imitation_game.dart
new file mode 100644
index 0000000..4bfa7e7
--- /dev/null
+++ b/packages/imitation_game/bin/imitation_game.dart
@@ -0,0 +1,278 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:mustache/mustache.dart';
+import 'package:imitation_game/readme_template.dart';
+import 'package:args/args.dart';
+
+// ignore_for_file: avoid_as
+
+const int _port = 4040;
+
+Future<String> _findIpAddress() async {
+  String result;
+  final List<NetworkInterface> interfaces = await NetworkInterface.list();
+  for (final NetworkInterface interface in interfaces) {
+    for (final InternetAddress address in interface.addresses) {
+      if (address.type == InternetAddressType.IPv4) {
+        // TODO(gaaclarke): Implment having multiple addresses.
+        assert(result == null);
+        result = address.address;
+      }
+    }
+  }
+  return result;
+}
+
+Future<String> _getFlutterVersion() async {
+  String result = '';
+  final Process flutterVersion =
+      await Process.start('flutter', <String>['--version']);
+  flutterVersion.stdout.transform(utf8.decoder).listen((String event) {
+    result += event;
+  });
+  await flutterVersion.exitCode;
+  return result.trim();
+}
+
+typedef FileFilter = bool Function(FileSystemEntity);
+Future<List<FileSystemEntity>> findFiles(Directory dir, {FileFilter where}) {
+  final List<FileSystemEntity> files = <FileSystemEntity>[];
+  final Completer<List<FileSystemEntity>> completer =
+      Completer<List<FileSystemEntity>>();
+  final Stream<FileSystemEntity> lister = dir.list(recursive: true);
+  lister.listen((FileSystemEntity file) {
+    if (where == null || where(file)) {
+      files.add(file);
+    }
+  }, onDone: () => completer.complete(files));
+  return completer.future;
+}
+
+Future<String> _makeMarkdownOutput(Map<String, dynamic> results) async {
+  // TODO(gaaclarke): Add the Flutter version.
+  final Template template = Template(readmeTemplate, name: 'README.md');
+  final Map<String, dynamic> values = Map<String, dynamic>.from(results);
+  values['date'] = DateTime.now().toUtc();
+  values['flutterVersion'] = await _getFlutterVersion();
+  final String output = template.renderString(values);
+  return output;
+}
+
+/// This merges [newResults] into [oldResults] such the union of the keys will
+/// have their values from [newResults] and the symmetric difference will have the
+/// value from their respective sets.
+Map<String, dynamic> _integrate(
+    {Map<String, dynamic> oldResults, Map<String, dynamic> newResults}) {
+  final Map<String, dynamic> result = Map<String, dynamic>.from(oldResults);
+  newResults.forEach((String test, dynamic testValue) {
+    final Map<String, dynamic> testMap = testValue as Map<String, dynamic>;
+    testMap.forEach((String platform, dynamic platformValue) {
+      final Map<String, dynamic> platformMap =
+          platformValue as Map<String, dynamic>;
+      platformMap.forEach((String measurement, dynamic measurementValue) {
+        if (!result.containsKey(test)) {
+          result[test] = <String, dynamic>{};
+        }
+        if (!result[test].containsKey(platform)) {
+          result[test][platform] = <String, dynamic>{};
+        }
+        result[test][platform][measurement] = measurementValue;
+      });
+    });
+  });
+  return result;
+}
+
+class _Script {
+  _Script({this.path});
+  String path;
+}
+
+class _ScriptRunner {
+  _ScriptRunner(this._scriptPaths);
+
+  final List<String> _scriptPaths;
+  Process _currentProcess;
+  StreamSubscription<String> _stdoutSubscription;
+  StreamSubscription<String> _stderrSubscription;
+
+  Future<_Script> runNext() async {
+    if (_currentProcess != null) {
+      _stdoutSubscription.cancel();
+      _stderrSubscription.cancel();
+      _currentProcess.kill();
+      _currentProcess = null;
+    }
+
+    if (_scriptPaths.isEmpty) {
+      return null;
+    } else {
+      final String path = _scriptPaths.last;
+      print('running: $path');
+      _scriptPaths.removeLast();
+      _currentProcess = await Process.start('sh', <String>[path]);
+      // TODO(gaaclarke): Implement a timeout.
+      _stdoutSubscription =
+          _currentProcess.stdout.transform(utf8.decoder).listen((String data) {
+        print(data);
+      });
+      _stderrSubscription =
+          _currentProcess.stderr.transform(utf8.decoder).listen((String data) {
+        print(data);
+      });
+      return _Script(path: path);
+    }
+  }
+}
+
+/// Recursively converts a map of maps to a map of lists of maps.
+///
+/// For example:
+/// _map2List({'a': {'b': 123}}, ['foo', 'bar']) ->
+/// {
+///   'foo':[
+///     {
+///       'name': 'a',
+///       'bar': [{'name': 'b', 'value': 123}]
+///     }
+///   ]
+/// }
+Map<String, dynamic> _map2List(Map<String, dynamic> map, List<String> names) {
+  final List<Map<String, dynamic>> returnList = <Map<String, dynamic>>[];
+  final List<String> tail = names.sublist(1);
+  map.forEach((String key, dynamic value) {
+    final Map<String, dynamic> testResult = <String, dynamic>{'name': key};
+    if (tail.isEmpty) {
+      testResult['value'] = value;
+    } else {
+      testResult[tail.first] = _map2List(value, tail)[tail.first];
+    }
+    returnList.add(testResult);
+  });
+  return <String, dynamic>{names.first: returnList};
+}
+
+class _ImitationGame {
+  final Map<String, dynamic> results = <String, dynamic>{};
+  _ScriptRunner _scriptRunner;
+  _Script _currentScript;
+
+  Future<bool> start(List<String> iosScripts) {
+    _scriptRunner = _ScriptRunner(iosScripts);
+    return _runNext();
+  }
+
+  Future<bool> handleResult(Map<String, dynamic> data) {
+    final String test = data['test'];
+    final String platform = data['platform'];
+    results.putIfAbsent(test, () => <String, dynamic>{});
+    results[test].putIfAbsent(platform, () => <String, dynamic>{});
+    data['results'].forEach((String k, dynamic v) {
+      results[test][platform][k] = v as double;
+    });
+    return _runNext();
+  }
+
+  Future<bool> handleTimeout() {
+    return _runNext();
+  }
+
+  Future<bool> _runNext() async {
+    _currentScript = await _scriptRunner.runNext();
+    return _currentScript != null;
+  }
+}
+
+enum _TargetPlatform { ANDROID, IOS }
+
+Future<void> main(List<String> args) async {
+  final ArgParser parser = ArgParser();
+  parser.addOption('platform',
+      allowed: <String>['android', 'ios'], defaultsTo: 'android');
+  final ArgResults parserResults = parser.parse(args);
+  final _TargetPlatform targetPlatform = parserResults['platform'] == 'android'
+      ? _TargetPlatform.ANDROID
+      : _TargetPlatform.IOS;
+
+  final HttpServer server = await HttpServer.bind(
+    InternetAddress.anyIPv4,
+    _port,
+  );
+  final String ipaddress = await _findIpAddress();
+  print('Listening on $ipaddress:${server.port}');
+
+  for (final FileSystemEntity entity in await findFiles(Directory.current,
+      where: (FileSystemEntity f) => f.path.endsWith('ip.txt'))) {
+    final File file = File(entity.path);
+    file.writeAsStringSync('$ipaddress:${server.port}');
+  }
+
+  final String scriptName = (targetPlatform == _TargetPlatform.ANDROID)
+      ? 'run_android.sh'
+      : () {
+          assert(targetPlatform == _TargetPlatform.IOS);
+          return 'run_ios.sh';
+        }();
+  final List<String> scripts = (await findFiles(Directory.current,
+          where: (FileSystemEntity f) => f.path.endsWith(scriptName)))
+      .map((FileSystemEntity e) => e.path)
+      .toList();
+
+  if (scripts.isEmpty) {
+    return;
+  }
+
+  final _ImitationGame game = _ImitationGame();
+  bool keepRunning = await game.start(scripts);
+
+  while (keepRunning) {
+    try {
+      final Stream<HttpRequest> timeoutServer = server.timeout(
+          const Duration(minutes: 5), onTimeout: (EventSink<HttpRequest> sink) {
+        print('TIMEOUT!');
+        throw TimeoutException('timeout');
+      });
+      await for (final HttpRequest request in timeoutServer) {
+        print('got request: ${request.method}');
+        if (request.method == 'POST') {
+          final String content = await utf8.decoder.bind(request).join();
+          final Map<String, dynamic> data =
+              jsonDecode(content) as Map<String, dynamic>;
+          print('$data');
+          keepRunning = await game.handleResult(data);
+          if (!keepRunning) {
+            break;
+          }
+        } else {
+          request.response.write('use post');
+        }
+        await request.response.close();
+      }
+    } on TimeoutException catch (_) {
+      keepRunning = await game.handleTimeout();
+    }
+  }
+
+  // TODO(gaaclarke): Add a log of what Flutter version generated the numbers.
+  const String lastResultsFilename = 'last_results.json';
+  const JsonDecoder decoder = JsonDecoder();
+  final Map<String, dynamic> lastResults =
+      decoder.convert(File(lastResultsFilename).readAsStringSync())
+          as Map<String, dynamic>;
+
+  // TODO(aaclarke): Calculate the generation time for each measurement since we
+  // can't generate everything in one pass (because you are running iOS or Android).
+  final Map<String, dynamic> totalResults =
+      _integrate(newResults: game.results, oldResults: lastResults);
+
+  const JsonEncoder encoder = JsonEncoder.withIndent('  ');
+  final String jsonResults = encoder.convert(totalResults);
+  File(lastResultsFilename).writeAsStringSync(jsonResults);
+
+  final Map<String, dynamic> markdownValues =
+      _map2List(totalResults, <String>['tests', 'platforms', 'measurements']);
+  File('README.md')
+      .writeAsStringSync(await _makeMarkdownOutput(markdownValues));
+  await server.close(force: true);
+}
diff --git a/packages/imitation_game/helper/run_android_helper.sh b/packages/imitation_game/helper/run_android_helper.sh
new file mode 100644
index 0000000..1137d54
--- /dev/null
+++ b/packages/imitation_game/helper/run_android_helper.sh
@@ -0,0 +1,25 @@
+report_results() 
+{
+  local TEST=$1
+  local PLATFORM=$2
+  local MEASUREMENTS=$3
+  curl --header "Content-Type: application/json" \
+    --request POST \
+    --data "{\"test\":\"$TEST\",\"platform\":\"$PLATFORM\",\"results\":$MEASUREMENTS}" \
+    http://localhost:4040
+}
+
+launch_apk()
+{
+  local APK_PATH=$1
+  local BUNDLE_NAME=$2
+  local ACTIVITY_NAME=$3
+  adb install -r $APK_PATH
+  adb shell am start -n $BUNDLE_NAME/$BUNDLE_NAME.$ACTIVITY_NAME
+}
+
+read_app_total_memory()
+{
+  local BUNDLE_NAME=$1
+  adb shell dumpsys meminfo $BUNDLE_NAME | sed -n 's/.*TOTAL:[ ]*\([0-9]*\).*/\1/p'
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/README.md b/packages/imitation_game/imitation_tests/smiley/README.md
new file mode 100644
index 0000000..220c40d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/README.md
@@ -0,0 +1,11 @@
+# Smiley
+
+This test is an app that just draws one large image to the screen `smiley.png`,
+stretched to fit inside the screen.
+
+The following things are measured:
+
+- 'ios_startup_time' - The time between the start of the process and the
+  rendering of the image to the screen.
+- 'adb_total_memory' - The amount of system memory the app is using after having
+  rendered the image to the screen.
diff --git a/packages/imitation_game/imitation_tests/smiley/android/.gitignore b/packages/imitation_game/imitation_tests/smiley/android/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore b/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle b/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle
new file mode 100644
index 0000000..ca31675
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.2"
+
+    defaultConfig {
+        applicationId "dev.flutter.imitation_game.smiley"
+        minSdkVersion 22
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+            signingConfig signingConfigs.debug
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'androidx.appcompat:appcompat:1.0.2'
+    implementation 'androidx.core:core-ktx:1.0.2'
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro b/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt b/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..f276ddb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/androidTest/java/dev/flutter/imitation_game/smiley/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package dev.flutter.imitation_game.smiley
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("dev.flutter.imitation_game.smiley", appContext.packageName)
+    }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0a91cd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="dev.flutter.imitation_game.smiley">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt
new file mode 100644
index 0000000..7d4ef8a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/java/dev/flutter/imitation_game/smiley/MainActivity.kt
@@ -0,0 +1,15 @@
+package dev.flutter.imitation_game.smiley
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+
+class MainActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
+        actionBar?.hide()
+        setContentView(R.layout.activity_main)
+    }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/drawable/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..4820781
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <ImageView
+        android:id="@+id/imageView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitCenter"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/smiley" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..030098f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#6200EE</color>
+    <color name="colorPrimaryDark">#3700B3</color>
+    <color name="colorAccent">#03DAC5</color>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1c84e3b
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">Smiley</string>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7af3eba
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+        <item name="windowNoTitle">true</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/android/build.gradle b/packages/imitation_game/imitation_tests/smiley/android/build.gradle
new file mode 100644
index 0000000..067a362
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    ext.kotlin_version = '1.3.61'
+    repositories {
+        google()
+        jcenter()
+        
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.6.2'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+        
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle.properties b/packages/imitation_game/imitation_tests/smiley/android/gradle.properties
new file mode 100644
index 0000000..23339e0
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..67a69c7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 18 13:30:11 PDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradlew b/packages/imitation_game/imitation_tests/smiley/android/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat b/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/imitation_game/imitation_tests/smiley/android/run_android.sh b/packages/imitation_game/imitation_tests/smiley/android/run_android.sh
new file mode 100755
index 0000000..de66f41
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/run_android.sh
@@ -0,0 +1,12 @@
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+source ../../../helper/run_android_helper.sh
+
+readonly BUNDLE="dev.flutter.imitation_game.smiley"
+readonly TEST_NAME="smiley"
+
+./gradlew assembleRelease
+launch_apk ./app/build/outputs/apk/release/app-release.apk $BUNDLE "MainActivity"
+sleep 10
+MEMORY_TOTAL=`read_app_total_memory $BUNDLE`
+report_results $TEST_NAME "android" "{\"adb_memory_total\":$MEMORY_TOTAL.0}"
diff --git a/packages/imitation_game/imitation_tests/smiley/android/settings.gradle b/packages/imitation_game/imitation_tests/smiley/android/settings.gradle
new file mode 100644
index 0000000..21c96b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/android/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name='Smiley'
+include ':app'
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh b/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh
new file mode 100755
index 0000000..cd22623
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/run_android.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+source ../../../helper/run_android_helper.sh
+
+readonly BUNDLE="com.example.smiley"
+readonly TEST_NAME="smiley"
+
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+flutter build apk
+launch_apk ./build/app/outputs/flutter-apk/app-release.apk $BUNDLE "MainActivity"
+sleep 10
+MEMORY_TOTAL=`read_app_total_memory $BUNDLE`
+report_results $TEST_NAME "flutter" "{\"adb_memory_total\":$MEMORY_TOTAL.0}"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh b/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh
new file mode 100755
index 0000000..decb485
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/run_ios.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+flutter run --release
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.metadata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.metadata
new file mode 100644
index 0000000..3f3cbf6
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 227990ab308574184e06944710bae000330412d0
+  channel: unknown
+
+project_type: app
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md
new file mode 100644
index 0000000..683f68d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/README.md
@@ -0,0 +1,16 @@
+# smiley
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle
new file mode 100644
index 0000000..01823ad
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/build.gradle
@@ -0,0 +1,63 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.smiley"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    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 {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..8177932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.smiley">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d377ac5
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.smiley">
+    <!-- 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="smiley"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <!-- Displays an Android View that continues showing the launch screen
+                 Drawable until Flutter paints its first frame, then this splash
+                 screen fades out. A splash screen is useful to avoid any visual
+                 gap between the end of Android's launch screen and the painting of
+                 Flutter's first frame. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.SplashScreenDrawable"
+              android:resource="@drawable/launch_background"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt
new file mode 100644
index 0000000..ccd14ef
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/kotlin/com/example/smiley/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.smiley
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..1f83a33
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">@android:color/white</item>
+    </style>
+</resources>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..8177932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.smiley">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/settings.gradle b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt
new file mode 100644
index 0000000..d331e21
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/assets/ip.txt
@@ -0,0 +1 @@
+192.168.0.18:4040
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/images/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..e8efba1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..399e934
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile
new file mode 100644
index 0000000..6697f0a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Podfile
@@ -0,0 +1,87 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def parse_KV_file(file, separator='=')
+  file_abs_path = File.expand_path(file)
+  if !File.exists? file_abs_path
+    return [];
+  end
+  generated_key_values = {}
+  skip_line_start_symbols = ["#", "/"]
+  File.foreach(file_abs_path) do |line|
+    next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
+    plugin = line.split(pattern=separator)
+    if plugin.length == 2
+      podname = plugin[0].strip()
+      path = plugin[1].strip()
+      podpath = File.expand_path("#{path}", file_abs_path)
+      generated_key_values[podname] = podpath
+    else
+      puts "Invalid plugin specification: #{line}"
+    end
+  end
+  generated_key_values
+end
+
+target 'Runner' do
+  use_frameworks!
+  use_modular_headers!
+
+  # Flutter Pod
+
+  copied_flutter_dir = File.join(__dir__, 'Flutter')
+  copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
+  copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
+  unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
+    # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
+    # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
+    # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
+
+    generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
+    unless File.exist?(generated_xcode_build_settings_path)
+      raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+    end
+    generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
+    cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
+
+    unless File.exist?(copied_framework_path)
+      FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
+    end
+    unless File.exist?(copied_podspec_path)
+      FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
+    end
+  end
+
+  # Keep pod path relative so it can be checked into Podfile.lock.
+  pod 'Flutter', :path => 'Flutter'
+
+  # Plugin Pods
+
+  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
+  # referring to absolute paths on developers' machines.
+  system('rm -rf .symlinks')
+  system('mkdir -p .symlinks/plugins')
+  plugin_pods = parse_KV_file('../.flutter-plugins')
+  plugin_pods.each do |name, path|
+    symlink = File.join('.symlinks', 'plugins', name)
+    File.symlink(path, symlink)
+    pod name, :path => File.join(symlink, 'ios')
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    target.build_configurations.each do |config|
+      config.build_settings['ENABLE_BITCODE'] = 'NO'
+    end
+  end
+end
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..24067aa
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,576 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 50;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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 */; };
+		E871A0843F25069A572B8976 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ED58CA389D243E9D25A3BCF /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			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>"; };
+		1653BF8CF7D70B08243DF6A2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+		1A3DE4B0B684E0679C64523E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		6ED58CA389D243E9D25A3BCF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; 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>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		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>"; };
+		C4C62015C2EE2797ABF091BA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				E871A0843F25069A572B8976 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		2BE839568BFB70043910F5F6 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				6ED58CA389D243E9D25A3BCF /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		8E5CD0CA74B3FF8D250A4283 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				1A3DE4B0B684E0679C64523E /* Pods-Runner.debug.xcconfig */,
+				C4C62015C2EE2797ABF091BA /* Pods-Runner.release.xcconfig */,
+				1653BF8CF7D70B08243DF6A2 /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				8E5CD0CA74B3FF8D250A4283 /* Pods */,
+				2BE839568BFB70043910F5F6 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				CAB15821656924BCC3058D97 /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				DBD650350FBE96B42D816E77 /* [CP] Embed Pods Frameworks */,
+			);
+			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 = 1020;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			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 */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets 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\" embed_and_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";
+		};
+		CAB15821656924BCC3058D97 /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		DBD650350FBE96B42D816E77 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift 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 */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				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.smiley;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				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.smiley;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				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.smiley;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				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 */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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"
+      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"
+      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 = "Profile"
+      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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist
new file mode 100644
index 0000000..9907fcd
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>smiley</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart
new file mode 100644
index 0000000..adec8c1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/lib/main.dart
@@ -0,0 +1,116 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show jsonEncode;
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:http/http.dart' as http;
+import 'package:poll_ios_stats/poll_ios_stats.dart';
+
+String _hostIp;
+
+Future<String> _getHostIp() async {
+  return rootBundle.loadString('assets/ip.txt');
+}
+
+Future<void> _sendResult(double result) async {
+  assert(_hostIp != null);
+  print('sending result $result...');
+  final String measurementName =
+      '${Platform.isAndroid ? "android_" : "ios_"}startup_time';
+  final http.Response response = await http.post(
+    'http://$_hostIp',
+    headers: <String, String>{
+      'Content-Type': 'application/json; charset=UTF-8',
+    },
+    body: jsonEncode(<String, dynamic>{
+      'test': 'smiley',
+      'platform': 'flutter',
+      'results': <String, double>{measurementName: result},
+    }),
+  );
+  if (response.statusCode != 200) {
+    print('error when posting results:${response.statusCode}');
+  }
+}
+
+void main() {
+  runApp(const MyApp());
+  // Hide status bar.
+  SystemChrome.setEnabledSystemUIOverlays(<SystemUiOverlay>[]);
+  _getHostIp().then((String ip) async {
+    _hostIp = ip;
+  });
+}
+
+/// Top level Material App.
+class MyApp extends StatelessWidget {
+  /// Create top level Material App.
+  const MyApp({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+        visualDensity: VisualDensity.adaptivePlatformDensity,
+      ),
+      home: const MyHomePage(),
+    );
+  }
+}
+
+/// `Scaffold` that has the image widget.
+class MyHomePage extends StatefulWidget {
+  /// Standard constructor for a StatefulWidget.
+  const MyHomePage({Key key}) : super(key: key);
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  final AssetImage _image = const AssetImage('images/smiley.png');
+  bool _loading = true;
+
+  @override
+  void initState() {
+    _image
+        .resolve(const ImageConfiguration())
+        .addListener(ImageStreamListener((_, __) {
+      if (mounted) {
+        setState(() {
+          _loading = false;
+          // This should get called when the image has actually been drawn to the screen.
+          WidgetsBinding.instance.addPostFrameCallback((_) async {
+            final DateTime renderTime = DateTime.now();
+            final PollIosStats _poller = PollIosStats();
+            final StartupTime startupTime = await _poller.pollStartupTime();
+            final Duration diff = renderTime.difference(
+                DateTime.fromMicrosecondsSinceEpoch(startupTime.startupTime));
+            _sendResult(diff.inMicroseconds / 1000000.0);
+          });
+        });
+      }
+    }));
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            if (_loading) Container() else Image(image: _image),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml
new file mode 100644
index 0000000..61a4149
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/flutter/smiley/pubspec.yaml
@@ -0,0 +1,74 @@
+name: smiley
+description: A new Flutter project.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^0.1.3
+  flutter:
+    sdk: flutter
+  http: ^0.12.2
+  poll_ios_stats: ^0.0.1
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/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 material Icons class.
+  uses-material-design: true
+
+  # To add assets to your application, add an assets section, like this:
+  assets:
+    - images/smiley.png
+    - assets/ip.txt
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # 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: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/run_ios.sh b/packages/imitation_game/imitation_tests/smiley/uikit/run_ios.sh
new file mode 100755
index 0000000..794db72
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/run_ios.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+# TODO: What about customizing the DEVELOPMENT_TEAM?
+set -e
+cd $( dirname "${BASH_SOURCE[0]}" )
+cd smiley
+xcodebuild -scheme smiley -configuration Release -project smiley.xcodeproj -archivePath ./smiley.xcarchive archive
+xcodebuild -exportArchive -archivePath ./smiley.xcarchive/ -exportPath . -exportOptionsPlist exportOptions.plist -allowProvisioningUpdates
+ios-deploy --justlaunch --bundle ./smiley.xcarchive/Products/Applications/smiley.app
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist
new file mode 100644
index 0000000..687b403
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/exportOptions.plist
@@ -0,0 +1,18 @@
+<?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>compileBitcode</key>
+	<true/>
+	<key>method</key>
+	<string>development</string>
+	<key>signingStyle</key>
+	<string>automatic</string>
+	<key>stripSwiftSymbols</key>
+	<true/>
+	<key>teamID</key>
+	<string>S8QB4VV633</string>
+	<key>thinning</key>
+	<string>&lt;none&gt;</string>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..aca7bdb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.pbxproj
@@ -0,0 +1,350 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 50;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		0D313B6D24C6613900D7044B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B6C24C6613900D7044B /* AppDelegate.m */; };
+		0D313B7024C6613900D7044B /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B6F24C6613900D7044B /* SceneDelegate.m */; };
+		0D313B7324C6613900D7044B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B7224C6613900D7044B /* ViewController.m */; };
+		0D313B7624C6613900D7044B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7424C6613900D7044B /* Main.storyboard */; };
+		0D313B7824C6613A00D7044B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7724C6613A00D7044B /* Assets.xcassets */; };
+		0D313B7B24C6613A00D7044B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */; };
+		0D313B7E24C6613A00D7044B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D313B7D24C6613A00D7044B /* main.m */; };
+		0D313B8524C66AB700D7044B /* ip.txt in Resources */ = {isa = PBXBuildFile; fileRef = 0D313B8424C66AB700D7044B /* ip.txt */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		0D313B6824C6613900D7044B /* smiley.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = smiley.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		0D313B6B24C6613900D7044B /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		0D313B6C24C6613900D7044B /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		0D313B6E24C6613900D7044B /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
+		0D313B6F24C6613900D7044B /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
+		0D313B7124C6613900D7044B /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
+		0D313B7224C6613900D7044B /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
+		0D313B7524C6613900D7044B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		0D313B7724C6613A00D7044B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		0D313B7A24C6613A00D7044B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		0D313B7C24C6613A00D7044B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		0D313B7D24C6613A00D7044B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		0D313B8424C66AB700D7044B /* ip.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ip.txt; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		0D313B6524C6613900D7044B /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		0D313B5F24C6613900D7044B = {
+			isa = PBXGroup;
+			children = (
+				0D313B6A24C6613900D7044B /* smiley */,
+				0D313B6924C6613900D7044B /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		0D313B6924C6613900D7044B /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				0D313B6824C6613900D7044B /* smiley.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		0D313B6A24C6613900D7044B /* smiley */ = {
+			isa = PBXGroup;
+			children = (
+				0D313B6B24C6613900D7044B /* AppDelegate.h */,
+				0D313B6C24C6613900D7044B /* AppDelegate.m */,
+				0D313B6E24C6613900D7044B /* SceneDelegate.h */,
+				0D313B6F24C6613900D7044B /* SceneDelegate.m */,
+				0D313B7124C6613900D7044B /* ViewController.h */,
+				0D313B7224C6613900D7044B /* ViewController.m */,
+				0D313B7424C6613900D7044B /* Main.storyboard */,
+				0D313B7724C6613A00D7044B /* Assets.xcassets */,
+				0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */,
+				0D313B7C24C6613A00D7044B /* Info.plist */,
+				0D313B7D24C6613A00D7044B /* main.m */,
+				0D313B8424C66AB700D7044B /* ip.txt */,
+			);
+			path = smiley;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		0D313B6724C6613900D7044B /* smiley */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 0D313B8124C6613A00D7044B /* Build configuration list for PBXNativeTarget "smiley" */;
+			buildPhases = (
+				0D313B6424C6613900D7044B /* Sources */,
+				0D313B6524C6613900D7044B /* Frameworks */,
+				0D313B6624C6613900D7044B /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = smiley;
+			productName = smiley;
+			productReference = 0D313B6824C6613900D7044B /* smiley.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		0D313B6024C6613900D7044B /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 1150;
+				ORGANIZATIONNAME = "Aaron Clarke";
+				TargetAttributes = {
+					0D313B6724C6613900D7044B = {
+						CreatedOnToolsVersion = 11.5;
+					};
+				};
+			};
+			buildConfigurationList = 0D313B6324C6613900D7044B /* Build configuration list for PBXProject "smiley" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 0D313B5F24C6613900D7044B;
+			productRefGroup = 0D313B6924C6613900D7044B /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				0D313B6724C6613900D7044B /* smiley */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		0D313B6624C6613900D7044B /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				0D313B7B24C6613A00D7044B /* LaunchScreen.storyboard in Resources */,
+				0D313B8524C66AB700D7044B /* ip.txt in Resources */,
+				0D313B7824C6613A00D7044B /* Assets.xcassets in Resources */,
+				0D313B7624C6613900D7044B /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		0D313B6424C6613900D7044B /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				0D313B7324C6613900D7044B /* ViewController.m in Sources */,
+				0D313B6D24C6613900D7044B /* AppDelegate.m in Sources */,
+				0D313B7E24C6613A00D7044B /* main.m in Sources */,
+				0D313B7024C6613900D7044B /* SceneDelegate.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		0D313B7424C6613900D7044B /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				0D313B7524C6613900D7044B /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		0D313B7924C6613A00D7044B /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				0D313B7A24C6613A00D7044B /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		0D313B7F24C6613A00D7044B /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_IMPLICIT_RETAIN_SELF = 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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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 = 13.5;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+			};
+			name = Debug;
+		};
+		0D313B8024C6613A00D7044B /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_IMPLICIT_RETAIN_SELF = 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_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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 = 13.5;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		0D313B8224C6613A00D7044B /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				INFOPLIST_FILE = smiley/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.imitation-game.smiley";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		0D313B8324C6613A00D7044B /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = S8QB4VV633;
+				INFOPLIST_FILE = smiley/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.imitation-game.smiley";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		0D313B6324C6613900D7044B /* Build configuration list for PBXProject "smiley" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				0D313B7F24C6613A00D7044B /* Debug */,
+				0D313B8024C6613A00D7044B /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		0D313B8124C6613A00D7044B /* Build configuration list for PBXNativeTarget "smiley" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				0D313B8224C6613A00D7044B /* Debug */,
+				0D313B8324C6613A00D7044B /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 0D313B6024C6613900D7044B /* Project object */;
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1ba2d6e
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:smiley.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h
new file mode 100644
index 0000000..9a7d7f2
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.h
@@ -0,0 +1,9 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : UIResponder<UIApplicationDelegate>
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m
new file mode 100644
index 0000000..cac844f
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/AppDelegate.m
@@ -0,0 +1,38 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "AppDelegate.h"
+
+@interface AppDelegate ()
+
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  // Override point for customization after application launch.
+  return YES;
+}
+
+#pragma mark - UISceneSession lifecycle
+
+- (UISceneConfiguration *)application:(UIApplication *)application
+    configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
+                                   options:(UISceneConnectionOptions *)options {
+  // Called when a new scene session is being created.
+  // Use this method to select a configuration to create the new scene with.
+  return [[UISceneConfiguration alloc] initWithName:@"Default Configuration"
+                                        sessionRole:connectingSceneSession.role];
+}
+
+- (void)application:(UIApplication *)application
+    didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
+  // Called when the user discards a scene session.
+  // If any sessions were discarded while the application was not running, this will be called
+  // shortly after application:didFinishLaunchingWithOptions. Use this method to release any
+  // resources that were specific to the discarded scenes, as they will not return.
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..9221b9bb
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,98 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "60x60"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "20x20"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "40x40"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "76x76"
+    },
+    {
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json
new file mode 100644
index 0000000..79308c1
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "smiley.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png
new file mode 100644
index 0000000..441b66a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Assets.xcassets/smiley.imageset/smiley.png
Binary files differ
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..865e932
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..4f0a58a
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Base.lproj/Main.storyboard
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
+        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="lef-PC-56A">
+                                <rect key="frame" x="0.0" y="241" width="414" height="414"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" secondItem="lef-PC-56A" secondAttribute="height" multiplier="1:1" id="f3c-4F-4K7"/>
+                                </constraints>
+                            </imageView>
+                        </subviews>
+                        <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
+                        <constraints>
+                            <constraint firstItem="lef-PC-56A" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="Pf2-cI-sL8"/>
+                            <constraint firstItem="lef-PC-56A" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="WPF-Fb-Wds"/>
+                            <constraint firstItem="lef-PC-56A" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="WXd-85-JCf"/>
+                        </constraints>
+                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
+                    </view>
+                    <connections>
+                        <outlet property="imageView" destination="lef-PC-56A" id="rfq-xo-kDP"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="131.8840579710145" y="138.61607142857142"/>
+        </scene>
+    </scenes>
+</document>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist
new file mode 100644
index 0000000..7b6037c
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/Info.plist
@@ -0,0 +1,64 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UIApplicationSceneManifest</key>
+	<dict>
+		<key>UIApplicationSupportsMultipleScenes</key>
+		<false/>
+		<key>UISceneConfigurations</key>
+		<dict>
+			<key>UIWindowSceneSessionRoleApplication</key>
+			<array>
+				<dict>
+					<key>UISceneConfigurationName</key>
+					<string>Default Configuration</string>
+					<key>UISceneDelegateClassName</key>
+					<string>SceneDelegate</string>
+					<key>UISceneStoryboardFile</key>
+					<string>Main</string>
+				</dict>
+			</array>
+		</dict>
+	</dict>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<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>
+</dict>
+</plist>
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h
new file mode 100644
index 0000000..bc46a07
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.h
@@ -0,0 +1,11 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface SceneDelegate : UIResponder<UIWindowSceneDelegate>
+
+@property(strong, nonatomic) UIWindow* window;
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m
new file mode 100644
index 0000000..061f723
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/SceneDelegate.m
@@ -0,0 +1,52 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "SceneDelegate.h"
+
+@interface SceneDelegate ()
+
+@end
+
+@implementation SceneDelegate
+
+- (void)scene:(UIScene *)scene
+    willConnectToSession:(UISceneSession *)session
+                 options:(UISceneConnectionOptions *)connectionOptions {
+  // Use this method to optionally configure and attach the UIWindow `window` to the provided
+  // UIWindowScene `scene`. If using a storyboard, the `window` property will automatically be
+  // initialized and attached to the scene. This delegate does not imply the connecting scene or
+  // session are new (see `application:configurationForConnectingSceneSession` instead).
+}
+
+- (void)sceneDidDisconnect:(UIScene *)scene {
+  // Called as the scene is being released by the system.
+  // This occurs shortly after the scene enters the background, or when its session is discarded.
+  // Release any resources associated with this scene that can be re-created the next time the scene
+  // connects. The scene may re-connect later, as its session was not neccessarily discarded (see
+  // `application:didDiscardSceneSessions` instead).
+}
+
+- (void)sceneDidBecomeActive:(UIScene *)scene {
+  // Called when the scene has moved from an inactive state to an active state.
+  // Use this method to restart any tasks that were paused (or not yet started) when the scene was
+  // inactive.
+}
+
+- (void)sceneWillResignActive:(UIScene *)scene {
+  // Called when the scene will move from an active state to an inactive state.
+  // This may occur due to temporary interruptions (ex. an incoming phone call).
+}
+
+- (void)sceneWillEnterForeground:(UIScene *)scene {
+  // Called as the scene transitions from the background to the foreground.
+  // Use this method to undo the changes made on entering the background.
+}
+
+- (void)sceneDidEnterBackground:(UIScene *)scene {
+  // Called as the scene transitions from the foreground to the background.
+  // Use this method to save data, release shared resources, and store enough scene-specific state
+  // information to restore the scene back to its current state.
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h
new file mode 100644
index 0000000..a3585bf
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.h
@@ -0,0 +1,10 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+
+@interface ViewController : UIViewController
+@property(nonatomic, strong) IBOutlet UIImageView* imageView;
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m
new file mode 100644
index 0000000..46596b7
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ViewController.m
@@ -0,0 +1,114 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ViewController.h"
+#import <mach/mach.h>
+#import <sys/sysctl.h>
+#import <sys/types.h>
+
+static int64_t loadStartupTime(NSError **error) {
+  pid_t pid = [[NSProcessInfo processInfo] processIdentifier];
+  int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
+  struct kinfo_proc proc;
+  size_t size = sizeof(proc);
+  int err = sysctl(mib, 4, &proc, &size, NULL, 0);
+  if (err != 0) {
+    int errCode = errno;
+    if (error) {
+      *error = [NSError errorWithDomain:@"smiley" code:errCode userInfo:@{}];
+    }
+    return 0;
+  }
+
+  struct timeval startTime = proc.kp_proc.p_starttime;
+  int64_t microsecondsInSecond = 1000000LL;
+  int64_t microsecondsSinceEpoch =
+      (int64_t)(startTime.tv_sec * microsecondsInSecond) + (int64_t)startTime.tv_usec;
+
+  return microsecondsSinceEpoch;
+}
+
+static NSString *loadIpAddress() {
+#if TARGET_IPHONE_SIMULATOR
+  return @"127.0.0.1:4040";
+#else
+  NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
+  NSError *err;
+  NSString *ipAddress =
+      [NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:&err];
+  assert(err == nil);
+  return
+      [ipAddress stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+#endif
+}
+
+static void sendResults(NSTimeInterval result) {
+  NSLog(@"send results:%f", result);
+  NSString *ipAddress = loadIpAddress();
+  NSString *url = [NSString stringWithFormat:@"http://%@", ipAddress];
+  NSMutableURLRequest *urlRequest =
+      [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
+  NSDictionary *payload = @{
+    @"test" : @"smiley",
+    @"platform" : @"uikit",
+    @"results" : @{
+      @"ios_startup_time" : @(result),
+    }
+  };
+  NSError *error;
+  NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
+  assert(error == nil && jsonData);
+  [urlRequest setHTTPMethod:@"POST"];
+  [urlRequest setHTTPBody:jsonData];
+
+  NSURLSession *session = [NSURLSession sharedSession];
+  NSURLSessionDataTask *dataTask =
+      [session dataTaskWithRequest:urlRequest
+                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+                   NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+                   if (httpResponse.statusCode != 200) {
+                     NSLog(@"Error %@ '%@'", error, url);
+                   }
+                 }];
+  [dataTask resume];
+}
+
+@interface ViewController ()
+@property(nonatomic, strong) CADisplayLink *displayLink;
+@end
+
+@implementation ViewController {
+  BOOL _waitingForDraw;
+}
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+}
+
+- (void)loadView {
+  [super loadView];
+  self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTick:)];
+  [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+  self.imageView.image = [UIImage imageNamed:@"smiley"];
+  _waitingForDraw = YES;
+}
+
+- (BOOL)prefersStatusBarHidden {
+  return YES;
+}
+
+- (void)onTick:(CADisplayLink *)sender {
+  if (_waitingForDraw) {
+    int64_t epochTime = loadStartupTime(nil);
+    NSTimeInterval epocTimeSeconds = (double)epochTime / 1000000.0;
+    NSDate *startTime = [NSDate dateWithTimeIntervalSince1970:epocTimeSeconds];
+    NSTimeInterval runTime = [[NSDate now] timeIntervalSinceDate:startTime];
+    sendResults(runTime);
+    _waitingForDraw = NO;
+    [self.displayLink invalidate];
+    self.displayLink = nil;
+  }
+}
+
+@end
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt
new file mode 100644
index 0000000..d331e21
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/ip.txt
@@ -0,0 +1 @@
+192.168.0.18:4040
\ No newline at end of file
diff --git a/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m
new file mode 100644
index 0000000..9098614
--- /dev/null
+++ b/packages/imitation_game/imitation_tests/smiley/uikit/smiley/smiley/main.m
@@ -0,0 +1,15 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  NSString* appDelegateClassName;
+  @autoreleasepool {
+    // Setup code that might create autoreleased objects goes here.
+    appDelegateClassName = NSStringFromClass([AppDelegate class]);
+  }
+  return UIApplicationMain(argc, argv, nil, appDelegateClassName);
+}
diff --git a/packages/imitation_game/last_results.json b/packages/imitation_game/last_results.json
new file mode 100644
index 0000000..5344819
--- /dev/null
+++ b/packages/imitation_game/last_results.json
@@ -0,0 +1,14 @@
+{
+  "smiley": {
+    "flutter": {
+      "ios_startup_time": 0.691717,
+      "adb_memory_total": 43179.0
+    },
+    "uikit": {
+      "ios_startup_time": 0.2632870674133301
+    },
+    "android": {
+      "adb_memory_total": 97941.0
+    }
+  }
+}
\ No newline at end of file
diff --git a/packages/imitation_game/lib/imitation_game.dart b/packages/imitation_game/lib/imitation_game.dart
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/packages/imitation_game/lib/imitation_game.dart
@@ -0,0 +1 @@
+
diff --git a/packages/imitation_game/lib/readme_template.dart b/packages/imitation_game/lib/readme_template.dart
new file mode 100644
index 0000000..fc69cfb
--- /dev/null
+++ b/packages/imitation_game/lib/readme_template.dart
@@ -0,0 +1,111 @@
+/// Mustache template used for generating README.md.
+String readmeTemplate = """
+# Imitation Game
+
+## Description
+
+`imitation_game` is a platform for performing automated tests to compare the
+performance of different UI frameworks.  For example, how much memory does the
+same app written in Flutter and UIKit take?
+
+## Running all the tests
+
+You need a mobile device plugged into your computer and setup for development.
+The mobile device and the computer need to be on the same network, one that
+allows communication between computers since that's how the mobile phone will
+report its results to the computer.
+
+```sh
+dart imitation_game.dart
+```
+
+## Dependencies
+
+In order to run the tests you will need the union of all the platforms being
+tested.  As new tests are added please add to this list:
+
+### iOS
+
+- Flutter
+- Xcode
+- [ios_deploy](https://github.com/ios-control/ios-deploy) - used to launch apps
+  on the attached iOS device.
+
+## Example File Layout
+
+```text
+./
+├─ imitation_game.dart
+└─ imitation_tests/
+   ├─ smiley/
+   │  ├─ README.md
+   │  ├─ flutter/
+   │  │  ├─ run_ios.sh
+   │  │  └─ <flutter project files>
+   │  └─ uikit/
+   │     ├─ run_ios.sh
+   │     └─ <uikit project files>
+   └─ memory/
+      ├─ README.md
+      ├─ flutter/
+      │  ├─ run_ios.sh
+      │  └─ <flutter project files>
+      └─ uikit/
+         ├─ run_ios.sh
+         └─ <uikit project files>
+```
+
+Here there are 2 different tests with 2 different platform implementations.  The
+tests are named `smiley` and `memory`, they are both implemented on the
+platforms `flutter` and `uikit`.
+
+### Adding a test
+
+Tests should comprise of implementations on one or more platform.  The directory
+for the test should be added to `./tests`.  Inside that directory there should
+be a directory of implementations and a `README.md` file that explains the test.
+
+### Adding an implementation to a test
+
+An implementation has to follow these rules:
+
+- It needs to perform the same operations as the other implementations and
+  follow the description in the test's `README.md`.
+- It needs to contain a `run_ios.sh` script that will build and launch the test
+  on the connected device.
+- It should contain a file named `ip.txt` which will be overwritten by
+  `imitation_game.dart` with the ip address and port that should be used to
+  report results to.
+- It needs to report its results to the ip and port in the `ip.txt` via an HTTP
+  POST of JSON data.
+
+## Data format for results
+
+```json
+{
+  "test": "name_of_test",
+  "platform": "name_of_platform",
+  "results": {
+    "some_result_name": 1.23,
+    "some_result_name2": 4.56,
+  }
+}
+```
+
+A single test run can report multiple numbers.
+
+## Results
+Date created: {{date}}
+{{flutterVersion}}
+
+{{#tests}}
+- {{name}}
+  {{#platforms}}
+    - {{name}}
+    {{#measurements}}
+      - {{name}}: {{value}}
+    {{/measurements}}
+  {{/platforms}}
+{{/tests}}
+
+""";
diff --git a/packages/imitation_game/pubspec.yaml b/packages/imitation_game/pubspec.yaml
new file mode 100644
index 0000000..fea3802
--- /dev/null
+++ b/packages/imitation_game/pubspec.yaml
@@ -0,0 +1,9 @@
+name: imitation_game
+version: 0.0.1
+description: Testing framework for comparing multiple frameworks' performance.
+homepage: https://github.com/flutter/packages/tree/master/packages/imitation_game
+dependencies:
+  args: ^1.6.0
+  mustache: ^1.1.1
+environment:
+  sdk: ">=2.7.0 <3.0.0"
diff --git a/packages/imitation_game/run.sh b/packages/imitation_game/run.sh
new file mode 100755
index 0000000..5107632
--- /dev/null
+++ b/packages/imitation_game/run.sh
@@ -0,0 +1,5 @@
+if [ $# -eq 0 ]; then
+  echo "usage: run.sh [android | ios]"
+fi
+
+pub run imitation_game --platform=$1
diff --git a/packages/metrics_center/.gitignore b/packages/metrics_center/.gitignore
new file mode 100644
index 0000000..d97c5ea
--- /dev/null
+++ b/packages/metrics_center/.gitignore
@@ -0,0 +1 @@
+secret
diff --git a/packages/metrics_center/CHANGELOG.md b/packages/metrics_center/CHANGELOG.md
new file mode 100644
index 0000000..4e629f2
--- /dev/null
+++ b/packages/metrics_center/CHANGELOG.md
@@ -0,0 +1,29 @@
+# 0.1.0
+
+- `update` now requires DateTime when commit was merged
+- Removed `github` dependency
+
+# 0.0.9
+
+- Remove legacy datastore and destination.
+
+# 0.0.8
+
+- Allow tests to override LegacyFlutterDestination GCP project id.
+
+# 0.0.7
+
+- Expose constants that were missing since 0.0.4+1.
+
+# 0.0.6
+
+- Allow `datastoreFromCredentialsJson` to specify project id.
+
+# 0.0.5
+
+- `FlutterDestination` writes into both Skia perf GCS and the legacy datastore.
+- `FlutterDestination.makeFromAccessToken` returns a `Future`.
+
+# 0.0.4+1
+
+- Moved to the `flutter/packages` repository
diff --git a/packages/metrics_center/LICENSE b/packages/metrics_center/LICENSE
new file mode 100644
index 0000000..922fc0c
--- /dev/null
+++ b/packages/metrics_center/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2014 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/metrics_center/README.md b/packages/metrics_center/README.md
new file mode 100644
index 0000000..8f1418d
--- /dev/null
+++ b/packages/metrics_center/README.md
@@ -0,0 +1,9 @@
+# Metrics Center
+
+Metrics center is a minimal set of code and services to support multiple perf
+metrics generators (e.g., Cocoon device lab, Cirrus bots, LUCI bots, Firebase
+Test Lab) and destinations (e.g., old Cocoon perf dashboard, Skia perf
+dashboard). The work and maintenance it requires is very close to that of just
+supporting a single generator and destination (e.g., engine bots to Skia perf),
+and the small amount of extra work is designed to make it easy to support more
+generators and destinations in the future.
diff --git a/packages/metrics_center/lib/metrics_center.dart b/packages/metrics_center/lib/metrics_center.dart
new file mode 100644
index 0000000..3790116
--- /dev/null
+++ b/packages/metrics_center/lib/metrics_center.dart
@@ -0,0 +1,9 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/common.dart';
+export 'src/constants.dart';
+export 'src/flutter.dart';
+export 'src/google_benchmark.dart';
+export 'src/skiaperf.dart';
diff --git a/packages/metrics_center/lib/src/common.dart b/packages/metrics_center/lib/src/common.dart
new file mode 100644
index 0000000..ee17c40
--- /dev/null
+++ b/packages/metrics_center/lib/src/common.dart
@@ -0,0 +1,65 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+import 'package:equatable/equatable.dart';
+
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:http/http.dart';
+
+/// Common format of a metric data point.
+class MetricPoint extends Equatable {
+  /// Creates a new data point.
+  MetricPoint(
+    this.value,
+    Map<String, String> tags,
+  ) : _tags = SplayTreeMap<String, String>.from(tags);
+
+  /// Can store integer values.
+  final double value;
+
+  /// Test name, unit, timestamp, configs, git revision, ..., in sorted order.
+  UnmodifiableMapView<String, String> get tags =>
+      UnmodifiableMapView<String, String>(_tags);
+
+  /// Unique identifier for updating existing data point.
+  ///
+  /// We shouldn't have to worry about hash collisions until we have about
+  /// 2^128 points.
+  ///
+  /// This id should stay constant even if the [tags.keys] are reordered.
+  /// (Because we are using an ordered SplayTreeMap to generate the id.)
+  String get id => sha256.convert(utf8.encode('$_tags')).toString();
+
+  @override
+  String toString() {
+    return 'MetricPoint(value=$value, tags=$_tags)';
+  }
+
+  final SplayTreeMap<String, String> _tags;
+
+  @override
+  List<Object> get props => <Object>[value, tags];
+}
+
+/// Interface to write [MetricPoint].
+abstract class MetricDestination {
+  /// Insert new data points or modify old ones with matching id.
+  Future<void> update(List<MetricPoint> points, DateTime commitTime);
+}
+
+/// Create `AuthClient` in case we only have an access token without the full
+/// credentials json. It's currently the case for Chrmoium LUCI bots.
+AuthClient authClientFromAccessToken(String token, List<String> scopes) {
+  final DateTime anHourLater = DateTime.now().add(const Duration(hours: 1));
+  final AccessToken accessToken =
+      AccessToken('Bearer', token, anHourLater.toUtc());
+  final AccessCredentials accessCredentials =
+      AccessCredentials(accessToken, null, scopes);
+  return authenticatedClient(Client(), accessCredentials);
+}
diff --git a/packages/metrics_center/lib/src/constants.dart b/packages/metrics_center/lib/src/constants.dart
new file mode 100644
index 0000000..49c8244
--- /dev/null
+++ b/packages/metrics_center/lib/src/constants.dart
@@ -0,0 +1,51 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Strings used for MetricPoint tag keys
+const String kGithubRepoKey = 'gitRepo';
+
+/// Strings used for MetricPoint tag keys
+const String kGitRevisionKey = 'gitRevision';
+
+/// Strings used for MetricPoint tag keys
+const String kUnitKey = 'unit';
+
+/// Strings used for MetricPoint tag keys
+const String kNameKey = 'name';
+
+/// Strings used for MetricPoint tag keys
+const String kSubResultKey = 'subResult';
+
+/// Flutter repo name.
+const String kFlutterFrameworkRepo = 'flutter/flutter';
+
+/// Engine repo name.
+const String kFlutterEngineRepo = 'flutter/engine';
+
+/// The key for the GCP project id in the credentials json.
+const String kProjectId = 'project_id';
+
+/// Timeline key in JSON.
+const String kSourceTimeMicrosName = 'sourceTimeMicros';
+
+/// The prod bucket name for Flutter's instance of Skia Perf.
+const String kBucketName = 'flutter-skia-perf-prod';
+
+/// The test bucket name for Flutter's instance of Skia Perf.
+const String kTestBucketName = 'flutter-skia-perf-test';
+
+/// JSON key for Skia Perf's git hash entry.
+const String kSkiaPerfGitHashKey = 'gitHash';
+
+/// JSON key for Skia Perf's results entry.
+const String kSkiaPerfResultsKey = 'results';
+
+/// JSON key for Skia Perf's value entry.
+const String kSkiaPerfValueKey = 'value';
+
+/// JSON key for Skia Perf's options entry.
+const String kSkiaPerfOptionsKey = 'options';
+
+/// JSON key for Skia Perf's default config entry.
+const String kSkiaPerfDefaultConfig = 'default';
diff --git a/packages/metrics_center/lib/src/flutter.dart b/packages/metrics_center/lib/src/flutter.dart
new file mode 100644
index 0000000..89e35ae
--- /dev/null
+++ b/packages/metrics_center/lib/src/flutter.dart
@@ -0,0 +1,60 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'common.dart';
+import 'constants.dart';
+import 'skiaperf.dart';
+
+/// Convenient class to capture the benchmarks in the Flutter engine repo.
+class FlutterEngineMetricPoint extends MetricPoint {
+  /// Creates a metric point for the Flutter engine repository.
+  ///
+  /// The `name`, `value`, and `gitRevision` parameters must not be null.
+  FlutterEngineMetricPoint(
+    String name,
+    double value,
+    String gitRevision, {
+    Map<String, String> moreTags = const <String, String>{},
+  }) : super(
+          value,
+          <String, String>{
+            kNameKey: name,
+            kGithubRepoKey: kFlutterEngineRepo,
+            kGitRevisionKey: gitRevision,
+          }..addAll(moreTags),
+        );
+}
+
+/// All Flutter performance metrics (framework, engine, ...) should be written
+/// to this destination.
+class FlutterDestination extends MetricDestination {
+  FlutterDestination._(this._skiaPerfDestination);
+
+  /// Creates a [FlutterDestination] from service account JSON.
+  static Future<FlutterDestination> makeFromCredentialsJson(
+      Map<String, dynamic> json,
+      {bool isTesting = false}) async {
+    final SkiaPerfDestination skiaPerfDestination =
+        await SkiaPerfDestination.makeFromGcpCredentials(json,
+            isTesting: isTesting);
+    return FlutterDestination._(skiaPerfDestination);
+  }
+
+  /// Creates a [FlutterDestination] from an OAuth access token.
+  static Future<FlutterDestination> makeFromAccessToken(
+      String accessToken, String projectId,
+      {bool isTesting = false}) async {
+    final SkiaPerfDestination skiaPerfDestination =
+        await SkiaPerfDestination.makeFromAccessToken(accessToken, projectId,
+            isTesting: isTesting);
+    return FlutterDestination._(skiaPerfDestination);
+  }
+
+  @override
+  Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
+    await _skiaPerfDestination.update(points, commitTime);
+  }
+
+  final SkiaPerfDestination _skiaPerfDestination;
+}
diff --git a/packages/metrics_center/lib/src/gcs_lock.dart b/packages/metrics_center/lib/src/gcs_lock.dart
new file mode 100644
index 0000000..47d0d23
--- /dev/null
+++ b/packages/metrics_center/lib/src/gcs_lock.dart
@@ -0,0 +1,91 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:googleapis/storage/v1.dart';
+import 'package:googleapis_auth/auth.dart';
+
+/// Global (in terms of earth) mutex using Google Cloud Storage.
+class GcsLock {
+  /// Create a lock with an authenticated client and a GCS bucket name.
+  ///
+  /// The client is used to communicate with Google Cloud Storage APIs.
+  GcsLock(this._client, this._bucketName)
+      : assert(_client != null),
+        assert(_bucketName != null) {
+    _api = StorageApi(_client);
+  }
+
+  /// Create a temporary lock file in GCS, and use it as a mutex mechanism to
+  /// run a piece of code exclusively.
+  ///
+  /// There must be no existing lock file with the same name in order to
+  /// proceed. If multiple [GcsLock]s with the same `bucketName` and
+  /// `lockFileName` try [protectedRun] simultaneously, only one will proceed
+  /// and create the lock file. All others will be blocked.
+  ///
+  /// When [protectedRun] finishes, the lock file is deleted, and other blocked
+  /// [protectedRun] may proceed.
+  ///
+  /// If the lock file is stuck (e.g., `_unlock` is interrupted unexpectedly),
+  /// one may need to manually delete the lock file from GCS to unblock any
+  /// [protectedRun] that may depend on it.
+  Future<void> protectedRun(
+      String lockFileName, Future<void> Function() f) async {
+    await _lock(lockFileName);
+    try {
+      await f();
+    } catch (e, stacktrace) {
+      print(stacktrace);
+      rethrow;
+    } finally {
+      await _unlock(lockFileName);
+    }
+  }
+
+  Future<void> _lock(String lockFileName) async {
+    final Object object = Object();
+    object.bucket = _bucketName;
+    object.name = lockFileName;
+    final Media content = Media(const Stream<List<int>>.empty(), 0);
+
+    Duration waitPeriod = const Duration(milliseconds: 10);
+    bool locked = false;
+    while (!locked) {
+      try {
+        await _api.objects.insert(object, _bucketName,
+            ifGenerationMatch: '0', uploadMedia: content);
+        locked = true;
+      } on DetailedApiRequestError catch (e) {
+        if (e.status == 412) {
+          // Status 412 means that the lock file already exists. Wait until
+          // that lock file is deleted.
+          await Future<void>.delayed(waitPeriod);
+          waitPeriod *= 2;
+          if (waitPeriod >= _kWarningThreshold) {
+            print(
+              'The lock is waiting for a long time: $waitPeriod. '
+              'If the lock file $lockFileName in bucket $_bucketName '
+              'seems to be stuck (i.e., it was created a long time ago and '
+              'no one seems to be owning it currently), delete it manually '
+              'to unblock this.',
+            );
+          }
+        } else {
+          rethrow;
+        }
+      }
+    }
+  }
+
+  Future<void> _unlock(String lockFileName) async {
+    await _api.objects.delete(_bucketName, lockFileName);
+  }
+
+  StorageApi _api;
+
+  final String _bucketName;
+  final AuthClient _client;
+
+  static const Duration _kWarningThreshold = Duration(seconds: 10);
+}
diff --git a/packages/metrics_center/lib/src/google_benchmark.dart b/packages/metrics_center/lib/src/google_benchmark.dart
new file mode 100644
index 0000000..ab4ae22
--- /dev/null
+++ b/packages/metrics_center/lib/src/google_benchmark.dart
@@ -0,0 +1,74 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'common.dart';
+import 'constants.dart';
+
+const String _kTimeUnitKey = 'time_unit';
+
+const List<String> _kNonNumericalValueSubResults = <String>[
+  kNameKey,
+  _kTimeUnitKey,
+  'iterations',
+  'big_o',
+];
+
+/// Parse the json result of https://github.com/google/benchmark.
+class GoogleBenchmarkParser {
+  /// Given a Google benchmark json output, parse its content into a list of [MetricPoint].
+  static Future<List<MetricPoint>> parse(String jsonFileName) async {
+    final Map<String, dynamic> jsonResult =
+        jsonDecode(File(jsonFileName).readAsStringSync())
+            as Map<String, dynamic>;
+
+    final Map<String, dynamic> rawContext =
+        jsonResult['context'] as Map<String, dynamic>;
+    final Map<String, String> context = rawContext.map<String, String>(
+      (String k, dynamic v) => MapEntry<String, String>(k, v.toString()),
+    );
+    final List<MetricPoint> points = <MetricPoint>[];
+    for (final dynamic item in jsonResult['benchmarks']) {
+      _parseAnItem(item as Map<String, dynamic>, points, context);
+    }
+    return points;
+  }
+}
+
+void _parseAnItem(
+  Map<String, dynamic> item,
+  List<MetricPoint> points,
+  Map<String, String> context,
+) {
+  final String name = item[kNameKey] as String;
+  final Map<String, String> timeUnitMap = <String, String>{
+    kUnitKey: item[_kTimeUnitKey] as String
+  };
+  for (final String subResult in item.keys) {
+    if (!_kNonNumericalValueSubResults.contains(subResult)) {
+      num rawValue;
+      try {
+        rawValue = item[subResult] as num;
+      } catch (e) {
+        print(
+            '$subResult: ${item[subResult]} (${item[subResult].runtimeType}) is not a number');
+        rethrow;
+      }
+
+      final double value =
+          rawValue is int ? rawValue.toDouble() : rawValue as double;
+      points.add(
+        MetricPoint(
+          value,
+          <String, String>{kNameKey: name, kSubResultKey: subResult}
+            ..addAll(context)
+            ..addAll(
+                subResult.endsWith('time') ? timeUnitMap : <String, String>{}),
+        ),
+      );
+    }
+  }
+}
diff --git a/packages/metrics_center/lib/src/skiaperf.dart b/packages/metrics_center/lib/src/skiaperf.dart
new file mode 100644
index 0000000..2a9c4bd
--- /dev/null
+++ b/packages/metrics_center/lib/src/skiaperf.dart
@@ -0,0 +1,438 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:gcloud/storage.dart';
+import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/auth_io.dart';
+
+import 'common.dart';
+import 'constants.dart';
+import 'gcs_lock.dart';
+
+/// A [MetricPoint] modeled after the format that Skia Perf expects.
+///
+/// Skia Perf Format is a JSON file that looks like:
+/// ```json
+/// {
+///     "gitHash": "fe4a4029a080bc955e9588d05a6cd9eb490845d4",
+///     "key": {
+///         "arch": "x86",
+///         "gpu": "GTX660",
+///         "model": "ShuttleA",
+///         "os": "Ubuntu12"
+///     },
+///     "results": {
+///         "ChunkAlloc_PushPop_640_480": {
+///             "nonrendering": {
+///                 "min_ms": 0.01485466666666667,
+///                 "options": {
+///                     "source_type": "bench"
+///                 }
+///             }
+///         },
+///         "DeferredSurfaceCopy_discardable_640_480": {
+///             "565": {
+///                 "min_ms": 2.215988,
+///                 "options": {
+///                     "source_type": "bench"
+///                 }
+///             }
+///         }
+///      }
+///   }
+/// }
+/// ```
+class SkiaPerfPoint extends MetricPoint {
+  SkiaPerfPoint._(this.githubRepo, this.gitHash, this.testName, this.subResult,
+      double value, this._options, this.jsonUrl)
+      : assert(_options[kGithubRepoKey] == null),
+        assert(_options[kGitRevisionKey] == null),
+        assert(_options[kNameKey] == null),
+        super(
+          value,
+          <String, String>{}
+            ..addAll(_options)
+            ..addAll(<String, String>{
+              kGithubRepoKey: githubRepo,
+              kGitRevisionKey: gitHash,
+              kNameKey: testName,
+              kSubResultKey: subResult,
+            }),
+        ) {
+    assert(tags[kGithubRepoKey] != null);
+    assert(tags[kGitRevisionKey] != null);
+    assert(tags[kNameKey] != null);
+  }
+
+  /// Construct [SkiaPerfPoint] from a well-formed [MetricPoint].
+  ///
+  /// The [MetricPoint] must have [kGithubRepoKey], [kGitRevisionKey],
+  /// [kNameKey] in its tags for this to be successful.
+  ///
+  /// If the [MetricPoint] has a tag 'date', that tag will be removed so Skia
+  /// perf can plot multiple metrics with different date as a single trace.
+  /// Skia perf will use the git revision's date instead of this date tag in
+  /// the time axis.
+  factory SkiaPerfPoint.fromPoint(MetricPoint p) {
+    final String githubRepo = p.tags[kGithubRepoKey];
+    final String gitHash = p.tags[kGitRevisionKey];
+    final String name = p.tags[kNameKey];
+
+    if (githubRepo == null || gitHash == null || name == null) {
+      throw '$kGithubRepoKey, $kGitRevisionKey, $kNameKey must be set in'
+          ' the tags of $p.';
+    }
+
+    final String subResult = p.tags[kSubResultKey] ?? kSkiaPerfValueKey;
+
+    final Map<String, String> options = <String, String>{}..addEntries(
+        p.tags.entries.where(
+          (MapEntry<String, dynamic> entry) =>
+              entry.key != kGithubRepoKey &&
+              entry.key != kGitRevisionKey &&
+              entry.key != kNameKey &&
+              entry.key != kSubResultKey &&
+              // https://github.com/google/benchmark automatically generates a
+              // 'date' field. If it's included in options, the Skia perf won't
+              // be able to connect different points in a single trace because
+              // the date is always different.
+              entry.key != 'date',
+        ),
+      );
+
+    return SkiaPerfPoint._(
+        githubRepo, gitHash, name, subResult, p.value, options, null);
+  }
+
+  /// In the format of '<owner>/<name>' such as 'flutter/flutter' or
+  /// 'flutter/engine'.
+  final String githubRepo;
+
+  /// SHA such as 'ad20d368ffa09559754e4b2b5c12951341ca3b2d'
+  final String gitHash;
+
+  /// For Flutter devicelab, this is the task name (e.g.,
+  /// 'flutter_gallery__transition_perf'); for Google benchmark, this is the
+  /// benchmark name (e.g., 'BM_ShellShutdown').
+  ///
+  /// In Skia perf web dashboard, this value can be queried and filtered by
+  /// "test".
+  final String testName;
+
+  /// The name of "subResult" comes from the special treatment of "sub_result"
+  /// in SkiaPerf. If not provided, its value will be set to kSkiaPerfValueKey.
+  ///
+  /// When Google benchmarks are converted to SkiaPerfPoint, this subResult
+  /// could be "cpu_time" or "real_time".
+  ///
+  /// When devicelab benchmarks are converted to SkiaPerfPoint, this subResult
+  /// is often the metric name such as "average_frame_build_time_millis" whereas
+  /// the [testName] is the benchmark or task name such as
+  /// "flutter_gallery__transition_perf".
+  final String subResult;
+
+  /// The url to the Skia perf json file in the Google Cloud Storage bucket.
+  ///
+  /// This can be null if the point has been stored in the bucket yet.
+  final String jsonUrl;
+
+  Map<String, dynamic> _toSubResultJson() {
+    return <String, dynamic>{
+      subResult: value,
+      kSkiaPerfOptionsKey: _options,
+    };
+  }
+
+  /// Convert a list of SkiaPoints with the same git repo and git revision into
+  /// a single json file in the Skia perf format.
+  ///
+  /// The list must be non-empty.
+  static Map<String, dynamic> toSkiaPerfJson(List<SkiaPerfPoint> points) {
+    assert(points.isNotEmpty);
+    assert(() {
+      for (final SkiaPerfPoint p in points) {
+        if (p.githubRepo != points[0].githubRepo ||
+            p.gitHash != points[0].gitHash) {
+          return false;
+        }
+      }
+      return true;
+    }(), 'All points must have same githubRepo and gitHash');
+
+    final Map<String, dynamic> results = <String, dynamic>{};
+    for (final SkiaPerfPoint p in points) {
+      final Map<String, dynamic> subResultJson = p._toSubResultJson();
+      if (results[p.testName] == null) {
+        results[p.testName] = <String, dynamic>{
+          kSkiaPerfDefaultConfig: subResultJson,
+        };
+      } else {
+        // Flutter currently doesn't support having the same name but different
+        // options/configurations. If this actually happens in the future, we
+        // probably can use different values of config (currently there's only
+        // one kSkiaPerfDefaultConfig) to resolve the conflict.
+        assert(results[p.testName][kSkiaPerfDefaultConfig][kSkiaPerfOptionsKey]
+                .toString() ==
+            subResultJson[kSkiaPerfOptionsKey].toString());
+        assert(
+            results[p.testName][kSkiaPerfDefaultConfig][p.subResult] == null);
+        results[p.testName][kSkiaPerfDefaultConfig][p.subResult] = p.value;
+      }
+    }
+
+    return <String, dynamic>{
+      kSkiaPerfGitHashKey: points[0].gitHash,
+      kSkiaPerfResultsKey: results,
+    };
+  }
+
+  // Equivalent to tags without git repo, git hash, and name because those two
+  // are already stored somewhere else.
+  final Map<String, String> _options;
+}
+
+/// Handle writing and updates of Skia perf GCS buckets.
+class SkiaPerfGcsAdaptor {
+  /// Construct the adaptor given the associated GCS bucket where the data is
+  /// read from and written to.
+  SkiaPerfGcsAdaptor(this._gcsBucket) : assert(_gcsBucket != null);
+
+  /// Used by Skia to differentiate json file format versions.
+  static const int version = 1;
+
+  /// Write a list of SkiaPerfPoint into a GCS file with name `objectName` in
+  /// the proper json format that's understandable by Skia perf services.
+  ///
+  /// The `objectName` must be a properly formatted string returned by
+  /// [computeObjectName].
+  ///
+  /// The read may retry multiple times if transient network errors with code
+  /// 504 happens.
+  Future<void> writePoints(
+      String objectName, List<SkiaPerfPoint> points) async {
+    final String jsonString = jsonEncode(SkiaPerfPoint.toSkiaPerfJson(points));
+    final List<int> content = utf8.encode(jsonString);
+
+    // Retry multiple times as GCS may return 504 timeout.
+    for (int retry = 0; retry < 5; retry += 1) {
+      try {
+        await _gcsBucket.writeBytes(objectName, content);
+        return;
+      } catch (e) {
+        if (e is DetailedApiRequestError && e.status == 504) {
+          continue;
+        }
+        rethrow;
+      }
+    }
+    // Retry one last time and let the exception go through.
+    await _gcsBucket.writeBytes(objectName, content);
+  }
+
+  /// Read a list of `SkiaPerfPoint` that have been previously written to the
+  /// GCS file with name `objectName`.
+  ///
+  /// The Github repo and revision of those points will be inferred from the
+  /// `objectName`.
+  ///
+  /// Return an empty list if the object does not exist in the GCS bucket.
+  ///
+  /// The read may retry multiple times if transient network errors with code
+  /// 504 happens.
+  Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
+    // Retry multiple times as GCS may return 504 timeout.
+    for (int retry = 0; retry < 5; retry += 1) {
+      try {
+        return await _readPointsWithoutRetry(objectName);
+      } catch (e) {
+        if (e is DetailedApiRequestError && e.status == 504) {
+          continue;
+        }
+        rethrow;
+      }
+    }
+    // Retry one last time and let the exception go through.
+    return _readPointsWithoutRetry(objectName);
+  }
+
+  Future<List<SkiaPerfPoint>> _readPointsWithoutRetry(String objectName) async {
+    ObjectInfo info;
+
+    try {
+      info = await _gcsBucket.info(objectName);
+    } catch (e) {
+      if (e.toString().contains('No such object')) {
+        return <SkiaPerfPoint>[];
+      } else {
+        rethrow;
+      }
+    }
+
+    final Stream<List<int>> stream = _gcsBucket.read(objectName);
+    final Stream<int> byteStream = stream.expand((List<int> x) => x);
+    final Map<String, dynamic> decodedJson =
+        jsonDecode(utf8.decode(await byteStream.toList()))
+            as Map<String, dynamic>;
+
+    final List<SkiaPerfPoint> points = <SkiaPerfPoint>[];
+
+    final String firstGcsNameComponent = objectName.split('/')[0];
+    _populateGcsNameToGithubRepoMapIfNeeded();
+    final String githubRepo = _gcsNameToGithubRepo[firstGcsNameComponent];
+    assert(githubRepo != null);
+
+    final String gitHash = decodedJson[kSkiaPerfGitHashKey] as String;
+    final Map<String, dynamic> results =
+        decodedJson[kSkiaPerfResultsKey] as Map<String, dynamic>;
+    for (final String name in results.keys) {
+      final Map<String, dynamic> subResultMap =
+          results[name][kSkiaPerfDefaultConfig] as Map<String, dynamic>;
+      for (final String subResult
+          in subResultMap.keys.where((String s) => s != kSkiaPerfOptionsKey)) {
+        points.add(SkiaPerfPoint._(
+          githubRepo,
+          gitHash,
+          name,
+          subResult,
+          subResultMap[subResult] as double,
+          (subResultMap[kSkiaPerfOptionsKey] as Map<String, dynamic>)
+              .cast<String, String>(),
+          info.downloadLink.toString(),
+        ));
+      }
+    }
+    return points;
+  }
+
+  /// Compute the GCS file name that's used to store metrics for a given commit
+  /// (git revision).
+  ///
+  /// Skia perf needs all directory names to be well formatted. The final name
+  /// of the json file (currently `values.json`) can be arbitrary, and multiple
+  /// json files can be put in that leaf directory. We intend to use multiple
+  /// json files in the future to scale up the system if too many writes are
+  /// competing for the same json file.
+  static Future<String> computeObjectName(
+      String githubRepo, String revision, DateTime commitTime) async {
+    assert(_githubRepoToGcsName[githubRepo] != null);
+    final String topComponent = _githubRepoToGcsName[githubRepo];
+    // [commitTime] is not guranteed to be UTC. Ensure it is so all results
+    // pushed to GCS are the same timezone.
+    final DateTime commitUtcTime = commitTime.toUtc();
+    final String month = commitUtcTime.month.toString().padLeft(2, '0');
+    final String day = commitUtcTime.day.toString().padLeft(2, '0');
+    final String hour = commitUtcTime.hour.toString().padLeft(2, '0');
+    final String dateComponents = '${commitUtcTime.year}/$month/$day/$hour';
+    return '$topComponent/$dateComponents/$revision/values.json';
+  }
+
+  static final Map<String, String> _githubRepoToGcsName = <String, String>{
+    kFlutterFrameworkRepo: 'flutter-flutter',
+    kFlutterEngineRepo: 'flutter-engine',
+  };
+  static final Map<String, String> _gcsNameToGithubRepo = <String, String>{};
+
+  static void _populateGcsNameToGithubRepoMapIfNeeded() {
+    if (_gcsNameToGithubRepo.isEmpty) {
+      for (final String repo in _githubRepoToGcsName.keys) {
+        final String gcsName = _githubRepoToGcsName[repo];
+        assert(_gcsNameToGithubRepo[gcsName] == null);
+        _gcsNameToGithubRepo[gcsName] = repo;
+      }
+    }
+  }
+
+  final Bucket _gcsBucket;
+}
+
+/// A [MetricDestination] that conforms to Skia Perf's protocols.
+class SkiaPerfDestination extends MetricDestination {
+  /// Creates a new [SkiaPerfDestination].
+  SkiaPerfDestination(this._gcs, this._lock);
+
+  /// Create from a full credentials json (of a service account).
+  static Future<SkiaPerfDestination> makeFromGcpCredentials(
+      Map<String, dynamic> credentialsJson,
+      {bool isTesting = false}) async {
+    final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+        ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+    return make(
+      client,
+      credentialsJson[kProjectId] as String,
+      isTesting: isTesting,
+    );
+  }
+
+  /// Create from an access token and its project id.
+  static Future<SkiaPerfDestination> makeFromAccessToken(
+      String token, String projectId,
+      {bool isTesting = false}) async {
+    final AuthClient client = authClientFromAccessToken(token, Storage.SCOPES);
+    return make(client, projectId, isTesting: isTesting);
+  }
+
+  /// Create from an [AuthClient] and a GCP project id.
+  ///
+  /// [AuthClient] can be obtained from functions like `clientViaUserConsent`.
+  static Future<SkiaPerfDestination> make(AuthClient client, String projectId,
+      {bool isTesting = false}) async {
+    final Storage storage = Storage(client, projectId);
+    final String bucketName = isTesting ? kTestBucketName : kBucketName;
+    if (!await storage.bucketExists(bucketName)) {
+      throw 'Bucket $bucketName does not exist.';
+    }
+    final SkiaPerfGcsAdaptor adaptor =
+        SkiaPerfGcsAdaptor(storage.bucket(bucketName));
+    final GcsLock lock = GcsLock(client, bucketName);
+    return SkiaPerfDestination(adaptor, lock);
+  }
+
+  @override
+  Future<void> update(List<MetricPoint> points, DateTime commitTime) async {
+    // 1st, create a map based on git repo, git revision, and point id. Git repo
+    // and git revision are the top level components of the Skia perf GCS object
+    // name.
+    final Map<String, Map<String, Map<String, SkiaPerfPoint>>> pointMap =
+        <String, Map<String, Map<String, SkiaPerfPoint>>>{};
+    for (final SkiaPerfPoint p
+        in points.map((MetricPoint x) => SkiaPerfPoint.fromPoint(x))) {
+      if (p != null) {
+        pointMap[p.githubRepo] ??= <String, Map<String, SkiaPerfPoint>>{};
+        pointMap[p.githubRepo][p.gitHash] ??= <String, SkiaPerfPoint>{};
+        pointMap[p.githubRepo][p.gitHash][p.id] = p;
+      }
+    }
+
+    // 2nd, read existing points from the gcs object and update with new ones.
+    for (final String repo in pointMap.keys) {
+      for (final String revision in pointMap[repo].keys) {
+        final String objectName = await SkiaPerfGcsAdaptor.computeObjectName(
+            repo, revision, commitTime);
+        final Map<String, SkiaPerfPoint> newPoints = pointMap[repo][revision];
+        // If too many bots are writing the metrics of a git revision into this
+        // single json file (with name `objectName`), the contention on the lock
+        // might be too high. In that case, break the json file into multiple
+        // json files according to bot names or task names. Skia perf read all
+        // json files in the directory so one can use arbitrary names for those
+        // sharded json file names.
+        _lock.protectedRun('$objectName.lock', () async {
+          final List<SkiaPerfPoint> oldPoints =
+              await _gcs.readPoints(objectName);
+          for (final SkiaPerfPoint p in oldPoints) {
+            if (newPoints[p.id] == null) {
+              newPoints[p.id] = p;
+            }
+          }
+          await _gcs.writePoints(objectName, newPoints.values.toList());
+        });
+      }
+    }
+  }
+
+  final SkiaPerfGcsAdaptor _gcs;
+  final GcsLock _lock;
+}
diff --git a/packages/metrics_center/pubspec.yaml b/packages/metrics_center/pubspec.yaml
new file mode 100644
index 0000000..cc8fac0
--- /dev/null
+++ b/packages/metrics_center/pubspec.yaml
@@ -0,0 +1,23 @@
+name: metrics_center
+version: 0.1.0
+description:
+  Support multiple performance metrics sources/formats and destinations.
+homepage:
+  https://github.com/flutter/packages/tree/master/packages/metrics_center
+
+environment:
+  sdk: '>=2.10.0 <3.0.0'
+
+dependencies:
+  crypto: ^2.1.5
+  equatable: ^1.2.5
+  gcloud: ^0.7.3
+  googleapis: ^0.56.1
+  googleapis_auth: ^0.2.12
+  http: ^0.12.2
+
+dev_dependencies:
+  fake_async: ^1.2.0-nullsafety.3
+  mockito: ^4.1.1
+  pedantic: ^1.10.0-nullsafety.3
+  test: ^1.16.0-nullsafety.9
diff --git a/packages/metrics_center/test/common.dart b/packages/metrics_center/test/common.dart
new file mode 100644
index 0000000..6ca543f
--- /dev/null
+++ b/packages/metrics_center/test/common.dart
@@ -0,0 +1,27 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+import 'package:test/test.dart' as test_package show TypeMatcher;
+
+export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+// Defines a 'package:test' shim.
+// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed
+
+/// A matcher that compares the type of the actual value to the type argument T.
+test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();
+
+void tryToDelete(Directory directory) {
+  // This should not be necessary, but it turns out that
+  // on Windows it's common for deletions to fail due to
+  // bogus (we think) "access denied" errors.
+  try {
+    directory.deleteSync(recursive: true);
+  } on FileSystemException catch (error) {
+    print('Failed to delete ${directory.path}: $error');
+  }
+}
diff --git a/packages/metrics_center/test/example_google_benchmark.json b/packages/metrics_center/test/example_google_benchmark.json
new file mode 100644
index 0000000..212ce9c
--- /dev/null
+++ b/packages/metrics_center/test/example_google_benchmark.json
@@ -0,0 +1,32 @@
+{
+    "context": {
+        "date": "2019-12-17 15:14:14",
+        "num_cpus": 56,
+        "mhz_per_cpu": 2594,
+        "cpu_scaling_enabled": true,
+        "library_build_type": "release"
+    },
+    "benchmarks": [
+        {
+            "name": "BM_PaintRecordInit",
+            "iterations": 6749079,
+            "real_time": 101,
+            "cpu_time": 101,
+            "time_unit": "ns"
+        },
+        {
+            "name": "BM_ParagraphShortLayout",
+            "iterations": 151761,
+            "real_time": 4460,
+            "cpu_time": 4460,
+            "time_unit": "ns"
+        },
+        {
+            "name": "BM_ParagraphStylesBigO_BigO",
+            "cpu_coefficient": 6548,
+            "real_coefficient": 6548,
+            "big_o": "N",
+            "time_unit": "ns"
+        }
+    ]
+}
diff --git a/packages/metrics_center/test/flutter_test.dart b/packages/metrics_center/test/flutter_test.dart
new file mode 100644
index 0000000..277e400
--- /dev/null
+++ b/packages/metrics_center/test/flutter_test.dart
@@ -0,0 +1,50 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/flutter.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+void main() {
+  const String gitRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
+  final FlutterEngineMetricPoint simplePoint = FlutterEngineMetricPoint(
+    'BM_ParagraphLongLayout',
+    287235,
+    gitRevision,
+  );
+
+  test('FlutterEngineMetricPoint works.', () {
+    expect(simplePoint.value, equals(287235));
+    expect(simplePoint.tags[kGithubRepoKey], kFlutterEngineRepo);
+    expect(simplePoint.tags[kGitRevisionKey], gitRevision);
+    expect(simplePoint.tags[kNameKey], 'BM_ParagraphLongLayout');
+
+    final FlutterEngineMetricPoint detailedPoint = FlutterEngineMetricPoint(
+      'BM_ParagraphLongLayout',
+      287224,
+      'ca799fa8b2254d09664b78ee80c43b434788d112',
+      moreTags: const <String, String>{
+        'executable': 'txt_benchmarks',
+        'sub_result': 'CPU',
+        kUnitKey: 'ns',
+      },
+    );
+    expect(detailedPoint.value, equals(287224));
+    expect(detailedPoint.tags['executable'], equals('txt_benchmarks'));
+    expect(detailedPoint.tags['sub_result'], equals('CPU'));
+    expect(detailedPoint.tags[kUnitKey], equals('ns'));
+  });
+
+  final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+
+  test('FlutterDestination integration test with update.', () async {
+    final FlutterDestination dst =
+        await FlutterDestination.makeFromCredentialsJson(credentialsJson,
+            isTesting: true);
+    dst.update(<FlutterEngineMetricPoint>[simplePoint],
+        DateTime.fromMillisecondsSinceEpoch(123));
+  }, skip: credentialsJson == null);
+}
diff --git a/packages/metrics_center/test/gcs_lock_test.dart b/packages/metrics_center/test/gcs_lock_test.dart
new file mode 100644
index 0000000..d682d95
--- /dev/null
+++ b/packages/metrics_center/test/gcs_lock_test.dart
@@ -0,0 +1,105 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:googleapis/storage/v1.dart';
+import 'package:fake_async/fake_async.dart';
+import 'package:gcloud/storage.dart';
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/gcs_lock.dart';
+import 'package:mockito/mockito.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+enum TestPhase {
+  run1,
+  run2,
+}
+
+class MockClient extends Mock implements AuthClient {}
+
+void main() {
+  const Duration kDelayStep = Duration(milliseconds: 10);
+  final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+
+  test('GcsLock prints warnings for long waits', () {
+    // Capture print to verify error messages.
+    final List<String> prints = <String>[];
+    final ZoneSpecification spec =
+        ZoneSpecification(print: (_, __, ___, String msg) => prints.add(msg));
+
+    Zone.current.fork(specification: spec).run<void>(() {
+      fakeAsync((FakeAsync fakeAsync) {
+        final MockClient mockClient = MockClient();
+        final GcsLock lock = GcsLock(mockClient, 'mockBucket');
+        when(mockClient.send(any)).thenThrow(DetailedApiRequestError(412, ''));
+        final Future<void> runFinished =
+            lock.protectedRun('mock.lock', () async {});
+        fakeAsync.elapse(const Duration(seconds: 10));
+        when(mockClient.send(any)).thenThrow(AssertionError('Stop!'));
+        runFinished.catchError((dynamic e) {
+          final AssertionError error = e as AssertionError;
+          expect(error.message, 'Stop!');
+          print('${error.message}');
+        });
+        fakeAsync.elapse(const Duration(seconds: 20));
+      });
+    });
+
+    const String kExpectedErrorMessage = 'The lock is waiting for a long time: '
+        '0:00:10.240000. If the lock file mock.lock in bucket mockBucket '
+        'seems to be stuck (i.e., it was created a long time ago and no one '
+        'seems to be owning it currently), delete it manually to unblock this.';
+    expect(prints, equals(<String>[kExpectedErrorMessage, 'Stop!']));
+  });
+
+  test('GcsLock integration test: single protectedRun is successful', () async {
+    final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+        ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+    final GcsLock lock = GcsLock(client, kTestBucketName);
+    int testValue = 0;
+    await lock.protectedRun('test.lock', () async {
+      testValue = 1;
+    });
+    expect(testValue, 1);
+  }, skip: credentialsJson == null);
+
+  test('GcsLock integration test: protectedRun is exclusive', () async {
+    final AutoRefreshingAuthClient client = await clientViaServiceAccount(
+        ServiceAccountCredentials.fromJson(credentialsJson), Storage.SCOPES);
+    final GcsLock lock1 = GcsLock(client, kTestBucketName);
+    final GcsLock lock2 = GcsLock(client, kTestBucketName);
+
+    TestPhase phase = TestPhase.run1;
+    final Completer<void> started1 = Completer<void>();
+    final Future<void> finished1 = lock1.protectedRun('test.lock', () async {
+      started1.complete();
+      while (phase == TestPhase.run1) {
+        await Future<void>.delayed(kDelayStep);
+      }
+    });
+
+    await started1.future;
+
+    final Completer<void> started2 = Completer<void>();
+    final Future<void> finished2 = lock2.protectedRun('test.lock', () async {
+      started2.complete();
+    });
+
+    // started2 should not be set even after a long wait because lock1 is
+    // holding the GCS lock file.
+    await Future<void>.delayed(kDelayStep * 10);
+    expect(started2.isCompleted, false);
+
+    // When phase is switched to run2, lock1 should be released soon and
+    // lock2 should soon be able to proceed its protectedRun.
+    phase = TestPhase.run2;
+    await started2.future;
+    await finished1;
+    await finished2;
+  }, skip: credentialsJson == null);
+}
diff --git a/packages/metrics_center/test/google_benchmark_test.dart b/packages/metrics_center/test/google_benchmark_test.dart
new file mode 100644
index 0000000..11883ae
--- /dev/null
+++ b/packages/metrics_center/test/google_benchmark_test.dart
@@ -0,0 +1,39 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/common.dart';
+import 'package:metrics_center/src/google_benchmark.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+void main() {
+  test('GoogleBenchmarkParser parses example json.', () async {
+    final List<MetricPoint> points =
+        await GoogleBenchmarkParser.parse('test/example_google_benchmark.json');
+    expect(points.length, 6);
+    expectSetMatch(
+      points.map((MetricPoint p) => p.value),
+      <int>[101, 101, 4460, 4460, 6548, 6548],
+    );
+    expectSetMatch(
+      points.map((MetricPoint p) => p.tags[kSubResultKey]),
+      <String>[
+        'cpu_time',
+        'real_time',
+        'cpu_coefficient',
+        'real_coefficient',
+      ],
+    );
+    expectSetMatch(
+      points.map((MetricPoint p) => p.tags[kNameKey]),
+      <String>[
+        'BM_PaintRecordInit',
+        'BM_ParagraphShortLayout',
+        'BM_ParagraphStylesBigO_BigO',
+      ],
+    );
+  });
+}
diff --git a/packages/metrics_center/test/skiaperf_test.dart b/packages/metrics_center/test/skiaperf_test.dart
new file mode 100644
index 0000000..d815711
--- /dev/null
+++ b/packages/metrics_center/test/skiaperf_test.dart
@@ -0,0 +1,583 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@Timeout(Duration(seconds: 3600))
+
+import 'dart:convert';
+
+import 'package:gcloud/storage.dart';
+import 'package:googleapis/storage/v1.dart' show DetailedApiRequestError;
+import 'package:googleapis_auth/auth_io.dart';
+import 'package:metrics_center/metrics_center.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:metrics_center/src/constants.dart';
+import 'package:metrics_center/src/gcs_lock.dart';
+
+import 'common.dart';
+import 'utility.dart';
+
+class MockBucket extends Mock implements Bucket {}
+
+class MockObjectInfo extends Mock implements ObjectInfo {}
+
+class MockGcsLock implements GcsLock {
+  @override
+  Future<void> protectedRun(
+      String exclusiveObjectName, Future<void> Function() f) async {
+    await f();
+  }
+}
+
+class MockSkiaPerfGcsAdaptor implements SkiaPerfGcsAdaptor {
+  @override
+  Future<List<SkiaPerfPoint>> readPoints(String objectName) async {
+    return _storage[objectName] ?? <SkiaPerfPoint>[];
+  }
+
+  @override
+  Future<void> writePoints(
+      String objectName, List<SkiaPerfPoint> points) async {
+    _storage[objectName] = points.toList();
+  }
+
+  // Map from the object name to the list of SkiaPoint that mocks the GCS.
+  final Map<String, List<SkiaPerfPoint>> _storage =
+      <String, List<SkiaPerfPoint>>{};
+}
+
+Future<void> main() async {
+  const double kValue1 = 1.0;
+  const double kValue2 = 2.0;
+  const double kValue3 = 3.0;
+
+  const String kFrameworkRevision1 = '9011cece2595447eea5dd91adaa241c1c9ef9a33';
+  const String kFrameworkRevision2 = '372fe290e4d4f3f97cbf02a57d235771a9412f10';
+  const String kEngineRevision1 = '617938024315e205f26ed72ff0f0647775fa6a71';
+  const String kEngineRevision2 = '5858519139c22484aaff1cf5b26bdf7951259344';
+  const String kTaskName = 'analyzer_benchmark';
+  const String kMetric1 = 'flutter_repo_batch_maximum';
+  const String kMetric2 = 'flutter_repo_watch_maximum';
+
+  final MetricPoint cocoonPointRev1Metric1 = MetricPoint(
+    kValue1,
+    const <String, String>{
+      kGithubRepoKey: kFlutterFrameworkRepo,
+      kGitRevisionKey: kFrameworkRevision1,
+      kNameKey: kTaskName,
+      kSubResultKey: kMetric1,
+      kUnitKey: 's',
+    },
+  );
+
+  final MetricPoint cocoonPointRev1Metric2 = MetricPoint(
+    kValue2,
+    const <String, String>{
+      kGithubRepoKey: kFlutterFrameworkRepo,
+      kGitRevisionKey: kFrameworkRevision1,
+      kNameKey: kTaskName,
+      kSubResultKey: kMetric2,
+      kUnitKey: 's',
+    },
+  );
+
+  final MetricPoint cocoonPointRev2Metric1 = MetricPoint(
+    kValue3,
+    const <String, String>{
+      kGithubRepoKey: kFlutterFrameworkRepo,
+      kGitRevisionKey: kFrameworkRevision2,
+      kNameKey: kTaskName,
+      kSubResultKey: kMetric1,
+      kUnitKey: 's',
+    },
+  );
+
+  final MetricPoint cocoonPointBetaRev1Metric1 = MetricPoint(
+    kValue1,
+    const <String, String>{
+      kGithubRepoKey: kFlutterFrameworkRepo,
+      kGitRevisionKey: kFrameworkRevision1,
+      kNameKey: 'beta/$kTaskName',
+      kSubResultKey: kMetric1,
+      kUnitKey: 's',
+      'branch': 'beta',
+    },
+  );
+
+  final MetricPoint cocoonPointBetaRev1Metric1BadBranch = MetricPoint(
+    kValue1,
+    const <String, String>{
+      kGithubRepoKey: kFlutterFrameworkRepo,
+      kGitRevisionKey: kFrameworkRevision1,
+      kNameKey: kTaskName,
+      kSubResultKey: kMetric1,
+      kUnitKey: 's',
+
+      // If we only add this 'branch' tag without changing the test or sub-result name, an exception
+      // would be thrown as Skia Perf currently only supports the same set of tags for a pair of
+      // kNameKey and kSubResultKey values. So to support branches, one also has to add the branch
+      // name to the test name.
+      'branch': 'beta',
+    },
+  );
+
+  const String engineMetricName = 'BM_PaintRecordInit';
+  const String engineRevision = 'ca799fa8b2254d09664b78ee80c43b434788d112';
+  const double engineValue1 = 101;
+  const double engineValue2 = 102;
+
+  final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
+    engineMetricName,
+    engineValue1,
+    engineRevision,
+    moreTags: const <String, String>{
+      kSubResultKey: 'cpu_time',
+      kUnitKey: 'ns',
+      'date': '2019-12-17 15:14:14',
+      'num_cpus': '56',
+      'mhz_per_cpu': '2594',
+      'cpu_scaling_enabled': 'true',
+      'library_build_type': 'release',
+    },
+  );
+
+  final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
+    engineMetricName,
+    engineValue2,
+    engineRevision,
+    moreTags: const <String, String>{
+      kSubResultKey: 'real_time',
+      kUnitKey: 'ns',
+      'date': '2019-12-17 15:14:14',
+      'num_cpus': '56',
+      'mhz_per_cpu': '2594',
+      'cpu_scaling_enabled': 'true',
+      'library_build_type': 'release',
+    },
+  );
+
+  test('Throw if invalid points are converted to SkiaPoint', () {
+    final MetricPoint noGithubRepoPoint = MetricPoint(
+      kValue1,
+      const <String, String>{
+        kGitRevisionKey: kFrameworkRevision1,
+        kNameKey: kTaskName,
+      },
+    );
+
+    final MetricPoint noGitRevisionPoint = MetricPoint(
+      kValue1,
+      const <String, String>{
+        kGithubRepoKey: kFlutterFrameworkRepo,
+        kNameKey: kTaskName,
+      },
+    );
+
+    final MetricPoint noTestNamePoint = MetricPoint(
+      kValue1,
+      const <String, String>{
+        kGithubRepoKey: kFlutterFrameworkRepo,
+        kGitRevisionKey: kFrameworkRevision1,
+      },
+    );
+
+    expect(() => SkiaPerfPoint.fromPoint(noGithubRepoPoint), throwsA(anything));
+    expect(
+        () => SkiaPerfPoint.fromPoint(noGitRevisionPoint), throwsA(anything));
+    expect(() => SkiaPerfPoint.fromPoint(noTestNamePoint), throwsA(anything));
+  });
+
+  test('Correctly convert a metric point from cocoon to SkiaPoint', () {
+    final SkiaPerfPoint skiaPoint1 =
+        SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+    expect(skiaPoint1, isNotNull);
+    expect(skiaPoint1.testName, equals(kTaskName));
+    expect(skiaPoint1.subResult, equals(kMetric1));
+    expect(skiaPoint1.value, equals(cocoonPointRev1Metric1.value));
+    expect(skiaPoint1.jsonUrl, isNull); // Not inserted yet
+  });
+
+  test('Cocoon points correctly encode into Skia perf json format', () {
+    final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+    final SkiaPerfPoint p2 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2);
+    final SkiaPerfPoint p3 =
+        SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1);
+
+    const JsonEncoder encoder = JsonEncoder.withIndent('  ');
+
+    expect(
+        encoder
+            .convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2, p3])),
+        equals('''
+{
+  "gitHash": "9011cece2595447eea5dd91adaa241c1c9ef9a33",
+  "results": {
+    "analyzer_benchmark": {
+      "default": {
+        "flutter_repo_batch_maximum": 1.0,
+        "options": {
+          "unit": "s"
+        },
+        "flutter_repo_watch_maximum": 2.0
+      }
+    },
+    "beta/analyzer_benchmark": {
+      "default": {
+        "flutter_repo_batch_maximum": 1.0,
+        "options": {
+          "branch": "beta",
+          "unit": "s"
+        }
+      }
+    }
+  }
+}'''));
+  });
+
+  test('Engine metric points correctly encode into Skia perf json format', () {
+    const JsonEncoder encoder = JsonEncoder.withIndent('  ');
+    expect(
+      encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
+        SkiaPerfPoint.fromPoint(enginePoint1),
+        SkiaPerfPoint.fromPoint(enginePoint2),
+      ])),
+      equals(
+        '''
+{
+  "gitHash": "ca799fa8b2254d09664b78ee80c43b434788d112",
+  "results": {
+    "BM_PaintRecordInit": {
+      "default": {
+        "cpu_time": 101.0,
+        "options": {
+          "cpu_scaling_enabled": "true",
+          "library_build_type": "release",
+          "mhz_per_cpu": "2594",
+          "num_cpus": "56",
+          "unit": "ns"
+        },
+        "real_time": 102.0
+      }
+    }
+  }
+}''',
+      ),
+    );
+  });
+
+  test(
+      'Throw if engine points with the same test name but different options are converted to '
+      'Skia perf points', () {
+    final FlutterEngineMetricPoint enginePoint1 = FlutterEngineMetricPoint(
+      'BM_PaintRecordInit',
+      101,
+      'ca799fa8b2254d09664b78ee80c43b434788d112',
+      moreTags: const <String, String>{
+        kSubResultKey: 'cpu_time',
+        kUnitKey: 'ns',
+        'cpu_scaling_enabled': 'true',
+      },
+    );
+    final FlutterEngineMetricPoint enginePoint2 = FlutterEngineMetricPoint(
+      'BM_PaintRecordInit',
+      102,
+      'ca799fa8b2254d09664b78ee80c43b434788d112',
+      moreTags: const <String, String>{
+        kSubResultKey: 'real_time',
+        kUnitKey: 'ns',
+        'cpu_scaling_enabled': 'false',
+      },
+    );
+
+    const JsonEncoder encoder = JsonEncoder.withIndent('  ');
+    expect(
+      () => encoder.convert(SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[
+        SkiaPerfPoint.fromPoint(enginePoint1),
+        SkiaPerfPoint.fromPoint(enginePoint2),
+      ])),
+      throwsA(anything),
+    );
+  });
+
+  test(
+      'Throw if two Cocoon metric points with the same name and subResult keys '
+      'but different options are converted to Skia perf points', () {
+    final SkiaPerfPoint p1 = SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1);
+    final SkiaPerfPoint p2 =
+        SkiaPerfPoint.fromPoint(cocoonPointBetaRev1Metric1BadBranch);
+
+    expect(
+      () => SkiaPerfPoint.toSkiaPerfJson(<SkiaPerfPoint>[p1, p2]),
+      throwsA(anything),
+    );
+  });
+
+  test('SkiaPerfGcsAdaptor computes name correctly', () async {
+    expect(
+      await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterFrameworkRepo,
+        kFrameworkRevision1,
+        DateTime.utc(2019, 12, 04, 23),
+      ),
+      equals('flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
+    );
+    expect(
+      await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterEngineRepo,
+        kEngineRevision1,
+        DateTime.utc(2019, 12, 03, 20),
+      ),
+      equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
+    );
+    expect(
+      await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterEngineRepo,
+        kEngineRevision2,
+        DateTime.utc(2020, 01, 03, 15),
+      ),
+      equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
+    );
+  });
+
+  test('Successfully read mock GCS that fails 1st time with 504', () async {
+    final MockBucket testBucket = MockBucket();
+    final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+    final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterFrameworkRepo,
+        kFrameworkRevision1,
+        DateTime.fromMillisecondsSinceEpoch(123));
+
+    final List<SkiaPerfPoint> writePoints = <SkiaPerfPoint>[
+      SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
+    ];
+    final String skiaPerfJson =
+        jsonEncode(SkiaPerfPoint.toSkiaPerfJson(writePoints));
+    await skiaPerfGcs.writePoints(testObjectName, writePoints);
+    verify(testBucket.writeBytes(testObjectName, utf8.encode(skiaPerfJson)));
+
+    // Emulate the first network request to fail with 504.
+    when(testBucket.info(testObjectName))
+        .thenThrow(DetailedApiRequestError(504, 'Test Failure'));
+
+    final MockObjectInfo mockObjectInfo = MockObjectInfo();
+    when(mockObjectInfo.downloadLink)
+        .thenReturn(Uri.https('test.com', 'mock.json'));
+    when(testBucket.info(testObjectName))
+        .thenAnswer((_) => Future<ObjectInfo>.value(mockObjectInfo));
+    when(testBucket.read(testObjectName))
+        .thenAnswer((_) => Stream<List<int>>.value(utf8.encode(skiaPerfJson)));
+
+    final List<SkiaPerfPoint> readPoints =
+        await skiaPerfGcs.readPoints(testObjectName);
+    expect(readPoints.length, equals(1));
+    expect(readPoints[0].testName, kTaskName);
+    expect(readPoints[0].subResult, kMetric1);
+    expect(readPoints[0].value, kValue1);
+    expect(readPoints[0].githubRepo, kFlutterFrameworkRepo);
+    expect(readPoints[0].gitHash, kFrameworkRevision1);
+    expect(readPoints[0].jsonUrl, 'https://test.com/mock.json');
+  });
+
+  test('Return empty list if the GCS file does not exist', () async {
+    final MockBucket testBucket = MockBucket();
+    final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+    final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterFrameworkRepo,
+        kFrameworkRevision1,
+        DateTime.fromMillisecondsSinceEpoch(123));
+    when(testBucket.info(testObjectName))
+        .thenThrow(Exception('No such object'));
+    expect((await skiaPerfGcs.readPoints(testObjectName)).length, 0);
+  });
+
+  // The following is for integration tests.
+  Bucket testBucket;
+  GcsLock testLock;
+  final Map<String, dynamic> credentialsJson = getTestGcpCredentialsJson();
+  if (credentialsJson != null) {
+    final ServiceAccountCredentials credentials =
+        ServiceAccountCredentials.fromJson(credentialsJson);
+
+    final AutoRefreshingAuthClient client =
+        await clientViaServiceAccount(credentials, Storage.SCOPES);
+    final Storage storage =
+        Storage(client, credentialsJson['project_id'] as String);
+
+    const String kTestBucketName = 'flutter-skia-perf-test';
+
+    assert(await storage.bucketExists(kTestBucketName));
+    testBucket = storage.bucket(kTestBucketName);
+    testLock = GcsLock(client, kTestBucketName);
+  }
+
+  Future<void> skiaPerfGcsAdapterIntegrationTest() async {
+    final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+    final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterFrameworkRepo,
+        kFrameworkRevision1,
+        DateTime.fromMillisecondsSinceEpoch(123));
+
+    await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
+      SkiaPerfPoint.fromPoint(cocoonPointRev1Metric1),
+      SkiaPerfPoint.fromPoint(cocoonPointRev1Metric2),
+    ]);
+
+    final List<SkiaPerfPoint> points =
+        await skiaPerfGcs.readPoints(testObjectName);
+    expect(points.length, equals(2));
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
+    expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
+        <String>[kMetric1, kMetric2]);
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
+    expectSetMatch(points.map((SkiaPerfPoint p) => p.githubRepo),
+        <String>[kFlutterFrameworkRepo]);
+    expectSetMatch(points.map((SkiaPerfPoint p) => p.gitHash),
+        <String>[kFrameworkRevision1]);
+    for (int i = 0; i < 2; i += 1) {
+      expect(points[0].jsonUrl, startsWith('https://'));
+    }
+  }
+
+  Future<void> skiaPerfGcsIntegrationTestWithEnginePoints() async {
+    final SkiaPerfGcsAdaptor skiaPerfGcs = SkiaPerfGcsAdaptor(testBucket);
+
+    final String testObjectName = await SkiaPerfGcsAdaptor.computeObjectName(
+        kFlutterEngineRepo,
+        engineRevision,
+        DateTime.fromMillisecondsSinceEpoch(123));
+
+    await skiaPerfGcs.writePoints(testObjectName, <SkiaPerfPoint>[
+      SkiaPerfPoint.fromPoint(enginePoint1),
+      SkiaPerfPoint.fromPoint(enginePoint2),
+    ]);
+
+    final List<SkiaPerfPoint> points =
+        await skiaPerfGcs.readPoints(testObjectName);
+    expect(points.length, equals(2));
+    expectSetMatch(
+      points.map((SkiaPerfPoint p) => p.testName),
+      <String>[engineMetricName, engineMetricName],
+    );
+    expectSetMatch(
+      points.map((SkiaPerfPoint p) => p.value),
+      <double>[engineValue1, engineValue2],
+    );
+    expectSetMatch(
+      points.map((SkiaPerfPoint p) => p.githubRepo),
+      <String>[kFlutterEngineRepo],
+    );
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.gitHash), <String>[engineRevision]);
+    for (int i = 0; i < 2; i += 1) {
+      expect(points[0].jsonUrl, startsWith('https://'));
+    }
+  }
+
+  // To run the following integration tests, there must be a valid Google Cloud
+  // Project service account credentials in secret/test_gcp_credentials.json so
+  // `testBucket` won't be null. Currently, these integration tests are skipped
+  // in the CI, and only verified locally.
+  test(
+    'SkiaPerfGcsAdaptor passes integration test with Google Cloud Storage',
+    skiaPerfGcsAdapterIntegrationTest,
+    skip: testBucket == null,
+  );
+
+  test(
+    'SkiaPerfGcsAdaptor integration test with engine points',
+    skiaPerfGcsIntegrationTestWithEnginePoints,
+    skip: testBucket == null,
+  );
+
+  // `SkiaPerfGcsAdaptor.computeObjectName` uses `GithubHelper` which requires
+  // network connections. Hence we put them as integration tests instead of unit
+  // tests.
+  test(
+    'SkiaPerfGcsAdaptor integration test for name computations',
+    () async {
+      expect(
+        await SkiaPerfGcsAdaptor.computeObjectName(
+          kFlutterFrameworkRepo,
+          kFrameworkRevision1,
+          DateTime.utc(2019, 12, 04, 23),
+        ),
+        equals(
+            'flutter-flutter/2019/12/04/23/$kFrameworkRevision1/values.json'),
+      );
+      expect(
+        await SkiaPerfGcsAdaptor.computeObjectName(
+          kFlutterEngineRepo,
+          kEngineRevision1,
+          DateTime.utc(2019, 12, 03, 20),
+        ),
+        equals('flutter-engine/2019/12/03/20/$kEngineRevision1/values.json'),
+      );
+      expect(
+        await SkiaPerfGcsAdaptor.computeObjectName(
+          kFlutterEngineRepo,
+          kEngineRevision2,
+          DateTime.utc(2020, 01, 03, 15),
+        ),
+        equals('flutter-engine/2020/01/03/15/$kEngineRevision2/values.json'),
+      );
+    },
+    skip: testBucket == null,
+  );
+
+  test('SkiaPerfDestination correctly updates points', () async {
+    final SkiaPerfGcsAdaptor mockGcs = MockSkiaPerfGcsAdaptor();
+    final GcsLock mockLock = MockGcsLock();
+    final SkiaPerfDestination dst = SkiaPerfDestination(mockGcs, mockLock);
+    await dst.update(<MetricPoint>[cocoonPointRev1Metric1],
+        DateTime.fromMillisecondsSinceEpoch(123));
+    await dst.update(<MetricPoint>[cocoonPointRev1Metric2],
+        DateTime.fromMillisecondsSinceEpoch(123));
+    List<SkiaPerfPoint> points = await mockGcs.readPoints(
+        await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+            kFrameworkRevision1, DateTime.fromMillisecondsSinceEpoch(123)));
+    expect(points.length, equals(2));
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.testName), <String>[kTaskName]);
+    expectSetMatch(points.map((SkiaPerfPoint p) => p.subResult),
+        <String>[kMetric1, kMetric2]);
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.value), <double>[kValue1, kValue2]);
+
+    final MetricPoint updated =
+        MetricPoint(kValue3, cocoonPointRev1Metric1.tags);
+
+    await dst.update(<MetricPoint>[updated, cocoonPointRev2Metric1],
+        DateTime.fromMillisecondsSinceEpoch(123));
+
+    points = await mockGcs.readPoints(
+        await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+            kFrameworkRevision2, DateTime.fromMillisecondsSinceEpoch(123)));
+    expect(points.length, equals(1));
+    expect(points[0].gitHash, equals(kFrameworkRevision2));
+    expect(points[0].value, equals(kValue3));
+
+    points = await mockGcs.readPoints(
+        await SkiaPerfGcsAdaptor.computeObjectName(kFlutterFrameworkRepo,
+            kFrameworkRevision1, DateTime.fromMillisecondsSinceEpoch(123)));
+    expectSetMatch(
+        points.map((SkiaPerfPoint p) => p.value), <double>[kValue2, kValue3]);
+  });
+
+  Future<void> skiaPerfDestinationIntegrationTest() async {
+    final SkiaPerfDestination destination =
+        SkiaPerfDestination(SkiaPerfGcsAdaptor(testBucket), testLock);
+    await destination.update(<MetricPoint>[cocoonPointRev1Metric1],
+        DateTime.fromMillisecondsSinceEpoch(123));
+  }
+
+  test(
+    'SkiaPerfDestination integration test',
+    skiaPerfDestinationIntegrationTest,
+    skip: testBucket == null,
+  );
+}
diff --git a/packages/metrics_center/test/utility.dart b/packages/metrics_center/test/utility.dart
new file mode 100644
index 0000000..6dacd48
--- /dev/null
+++ b/packages/metrics_center/test/utility.dart
@@ -0,0 +1,23 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'common.dart';
+
+// This will be used in many of our unit tests.
+void expectSetMatch<T>(Iterable<T> actual, Iterable<T> expected) {
+  expect(Set<T>.from(actual), equals(Set<T>.from(expected)));
+}
+
+// May return null if the credentials file doesn't exist.
+Map<String, dynamic> getTestGcpCredentialsJson() {
+  final File f = File('secret/test_gcp_credentials.json');
+  if (!f.existsSync()) {
+    return null;
+  }
+  return jsonDecode(File('secret/test_gcp_credentials.json').readAsStringSync())
+      as Map<String, dynamic>;
+}
diff --git a/packages/multicast_dns/CHANGELOG.md b/packages/multicast_dns/CHANGELOG.md
new file mode 100644
index 0000000..fa64e26
--- /dev/null
+++ b/packages/multicast_dns/CHANGELOG.md
@@ -0,0 +1,22 @@
+## 0.3.0
+
+* Migrate package to null safety.
+
+## 0.2.2
+* Fixes parsing of TXT records. Continues parsing on non-utf8 strings.
+
+## 0.2.1
+* Fixes the handling of packets containing non-utf8 strings.
+
+## 0.2.0
+* Allow configuration of the port and address the mdns query is performed on.
+
+## 0.1.1
+
+* Fixes [flutter/issue/31854](https://github.com/flutter/flutter/issues/31854) where `decodeMDnsResponse` advanced to incorrect code points and ignored some records.
+
+## 0.1.0
+
+* Initial Open Source release.
+* Migrates the dartino-sdk's mDNS client to Dart 2.0 and Flutter's analysis rules
+* Breaks from original Dartino code, as it does not use native libraries for macOS and overhauls the `ResourceRecord` class.
diff --git a/packages/multicast_dns/LICENSE b/packages/multicast_dns/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/multicast_dns/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/multicast_dns/README.md b/packages/multicast_dns/README.md
new file mode 100644
index 0000000..47f769c
--- /dev/null
+++ b/packages/multicast_dns/README.md
@@ -0,0 +1,21 @@
+# Multicast DNS package
+
+[![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](
+https://pub.dartlang.org/packages/multicast_dns)
+
+A Dart package to do service discovery over multicast DNS (mDNS), Bonjour, and Avahi.
+
+## Usage
+To use this package, add `multicast_dns` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+``` dart
+import 'package:multicast_dns/multicast_dns.dart';
+```
+
+Then use the `MDnsClient` Dart class in your code. To see how this is done,
+check out the [example app](example/main.dart) or the sample implementations in
+the [bin](bin/) directory.
diff --git a/packages/multicast_dns/example/main.dart b/packages/multicast_dns/example/main.dart
new file mode 100644
index 0000000..d15f665
--- /dev/null
+++ b/packages/multicast_dns/example/main.dart
@@ -0,0 +1,37 @@
+// Copyright 2018, the Flutter 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.
+
+// Example script to illustrate how to use the mdns package to discover the port
+// of a Dart observatory over mDNS.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main() async {
+  // Parse the command line arguments.
+
+  const String name = '_dartobservatory._tcp.local';
+  final MDnsClient client = MDnsClient();
+  // Start the client with default options.
+  await client.start();
+
+  // Get the PTR record for the service.
+  await for (final PtrResourceRecord ptr in client
+      .lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer(name))) {
+    // Use the domainName from the PTR record to get the SRV record,
+    // which will have the port and local hostname.
+    // Note that duplicate messages may come through, especially if any
+    // other mDNS queries are running elsewhere on the machine.
+    await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
+        ResourceRecordQuery.service(ptr.domainName))) {
+      // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local"
+      final String bundleId =
+          ptr.domainName; //.substring(0, ptr.domainName.indexOf('@'));
+      print('Dart observatory instance found at '
+          '${srv.target}:${srv.port} for "$bundleId".');
+    }
+  }
+  client.stop();
+
+  print('Done.');
+}
diff --git a/packages/multicast_dns/example/mdns_resolve.dart b/packages/multicast_dns/example/mdns_resolve.dart
new file mode 100644
index 0000000..13441d6
--- /dev/null
+++ b/packages/multicast_dns/example/mdns_resolve.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Example script to illustrate how to use the mdns package to lookup names
+// on the local network.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main(List<String> args) async {
+  if (args.length != 1) {
+    print('''
+Please provide an address as argument.
+
+For example:
+  dart mdns_resolve.dart dartino.local''');
+    return;
+  }
+
+  final String name = args[0];
+
+  final MDnsClient client = MDnsClient();
+  await client.start();
+  await for (final IPAddressResourceRecord record in client
+      .lookup<IPAddressResourceRecord>(ResourceRecordQuery.addressIPv4(name))) {
+    print('Found address (${record.address}).');
+  }
+
+  await for (final IPAddressResourceRecord record in client
+      .lookup<IPAddressResourceRecord>(ResourceRecordQuery.addressIPv6(name))) {
+    print('Found address (${record.address}).');
+  }
+  client.stop();
+}
diff --git a/packages/multicast_dns/example/mdns_sd.dart b/packages/multicast_dns/example/mdns_sd.dart
new file mode 100644
index 0000000..b4d05ad
--- /dev/null
+++ b/packages/multicast_dns/example/mdns_sd.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Example script to illustrate how to use the mdns package to discover services
+// on the local network.
+
+import 'package:multicast_dns/multicast_dns.dart';
+
+Future<void> main(List<String> args) async {
+  if (args.isEmpty) {
+    print('''
+Please provide the name of a service as argument.
+
+For example:
+  dart mdns_sd.dart [--verbose] _workstation._tcp.local''');
+    return;
+  }
+
+  final bool verbose = args.contains('--verbose') || args.contains('-v');
+  final String name = args.last;
+  final MDnsClient client = MDnsClient();
+  await client.start();
+
+  await for (final PtrResourceRecord ptr in client
+      .lookup<PtrResourceRecord>(ResourceRecordQuery.serverPointer(name))) {
+    if (verbose) {
+      print(ptr);
+    }
+    await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
+        ResourceRecordQuery.service(ptr.domainName))) {
+      if (verbose) {
+        print(srv);
+      }
+      if (verbose) {
+        await client
+            .lookup<TxtResourceRecord>(ResourceRecordQuery.text(ptr.domainName))
+            .forEach(print);
+      }
+      await for (final IPAddressResourceRecord ip
+          in client.lookup<IPAddressResourceRecord>(
+              ResourceRecordQuery.addressIPv4(srv.target))) {
+        if (verbose) {
+          print(ip);
+        }
+        print('Service instance found at '
+            '${srv.target}:${srv.port} with ${ip.address}.');
+      }
+      await for (final IPAddressResourceRecord ip
+          in client.lookup<IPAddressResourceRecord>(
+              ResourceRecordQuery.addressIPv6(srv.target))) {
+        if (verbose) {
+          print(ip);
+        }
+        print('Service instance found at '
+            '${srv.target}:${srv.port} with ${ip.address}.');
+      }
+    }
+  }
+  client.stop();
+}
diff --git a/packages/multicast_dns/lib/multicast_dns.dart b/packages/multicast_dns/lib/multicast_dns.dart
new file mode 100644
index 0000000..0b014cf
--- /dev/null
+++ b/packages/multicast_dns/lib/multicast_dns.dart
@@ -0,0 +1,235 @@
+// Copyright (c) 2015, the Dartino 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 'dart:io';
+
+import 'package:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/lookup_resolver.dart';
+import 'package:multicast_dns/src/native_protocol_client.dart';
+import 'package:multicast_dns/src/packet.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+export 'package:multicast_dns/src/resource_record.dart';
+
+/// A callback type for [MDnsQuerier.start] to iterate available network
+/// interfaces.
+///
+/// Implementations must ensure they return interfaces appropriate for the
+/// [type] parameter.
+///
+/// See also:
+///   * [MDnsQuerier.allInterfacesFactory]
+typedef NetworkInterfacesFactory = Future<Iterable<NetworkInterface>> Function(
+    InternetAddressType type);
+
+/// A factory for construction of datagram sockets.
+///
+/// This can be injected into the [MDnsClient] to provide alternative
+/// implementations of [RawDatagramSocket.bind].
+typedef RawDatagramSocketFactory = Future<RawDatagramSocket> Function(
+    dynamic host, int port,
+    {bool reuseAddress, bool reusePort, int ttl});
+
+/// Client for DNS lookup and publishing using the mDNS protocol.
+///
+/// Users should call [MDnsQuerier.start] when ready to start querying and
+/// listening. [MDnsQuerier.stop] must be called when done to clean up
+/// resources.
+///
+/// This client only supports "One-Shot Multicast DNS Queries" as described in
+/// section 5.1 of [RFC 6762](https://tools.ietf.org/html/rfc6762).
+class MDnsClient {
+  /// Create a new [MDnsClient].
+  MDnsClient({
+    RawDatagramSocketFactory rawDatagramSocketFactory = RawDatagramSocket.bind,
+  }) : _rawDatagramSocketFactory = rawDatagramSocketFactory;
+
+  bool _starting = false;
+  bool _started = false;
+  final List<RawDatagramSocket> _sockets = <RawDatagramSocket>[];
+  final LookupResolver _resolver = LookupResolver();
+  final ResourceRecordCache _cache = ResourceRecordCache();
+  final RawDatagramSocketFactory _rawDatagramSocketFactory;
+
+  InternetAddress? _mDnsAddress;
+  int? _mDnsPort;
+
+  /// Find all network interfaces with an the [InternetAddressType] specified.
+  Future<Iterable<NetworkInterface>> allInterfacesFactory(
+      InternetAddressType type) {
+    return NetworkInterface.list(
+      includeLinkLocal: true,
+      type: type,
+      includeLoopback: true,
+    );
+  }
+
+  /// Start the mDNS client.
+  ///
+  /// With no arguments, this method will listen on the IPv4 multicast address
+  /// on all IPv4 network interfaces.
+  ///
+  /// The [listenAddress] parameter must be either [InternetAddress.anyIPv4] or
+  /// [InternetAddress.anyIPv6], and will default to anyIPv4.
+  ///
+  /// The [interfaceFactory] defaults to [allInterfacesFactory].
+  ///
+  /// The [mDnsPort] allows configuring what port is used for the mDNS
+  /// query. If not provided, defaults to `5353`.
+  ///
+  /// The [mDnsAddress] allows configuring what internet address is used
+  /// for the mDNS query. If not provided, defaults to either `224.0.0.251` or
+  /// or `FF02::FB`.
+  Future<void> start({
+    InternetAddress? listenAddress,
+    NetworkInterfacesFactory? interfacesFactory,
+    int mDnsPort = mDnsPort,
+    InternetAddress? mDnsAddress,
+  }) async {
+    listenAddress ??= InternetAddress.anyIPv4;
+    interfacesFactory ??= allInterfacesFactory;
+    final int selectedMDnsPort = _mDnsPort = mDnsPort;
+    _mDnsAddress = mDnsAddress;
+
+    assert(listenAddress.address == InternetAddress.anyIPv4.address ||
+        listenAddress.address == InternetAddress.anyIPv6.address);
+
+    if (_started || _starting) {
+      return;
+    }
+    _starting = true;
+
+    // Listen on all addresses.
+    final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
+      listenAddress.address,
+      selectedMDnsPort,
+      reuseAddress: true,
+      reusePort: true,
+      ttl: 255,
+    );
+
+    // Can't send to IPv6 any address.
+    if (incoming.address != InternetAddress.anyIPv6) {
+      _sockets.add(incoming);
+    }
+
+    _mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
+        ? mDnsAddressIPv4
+        : mDnsAddressIPv6;
+
+    final List<NetworkInterface> interfaces =
+        (await interfacesFactory(listenAddress.type)).toList();
+
+    for (final NetworkInterface interface in interfaces) {
+      // Create a socket for sending on each adapter.
+      final InternetAddress targetAddress = interface.addresses[0];
+      final RawDatagramSocket socket = await _rawDatagramSocketFactory(
+        targetAddress,
+        selectedMDnsPort,
+        reuseAddress: true,
+        reusePort: true,
+        ttl: 255,
+      );
+      _sockets.add(socket);
+      // Ensure that we're using this address/interface for multicast.
+      if (targetAddress.type == InternetAddressType.IPv4) {
+        socket.setRawOption(RawSocketOption(
+          RawSocketOption.levelIPv4,
+          RawSocketOption.IPv4MulticastInterface,
+          targetAddress.rawAddress,
+        ));
+      } else {
+        socket.setRawOption(RawSocketOption.fromInt(
+          RawSocketOption.levelIPv6,
+          RawSocketOption.IPv6MulticastInterface,
+          interface.index,
+        ));
+      }
+      // Join multicast on this interface.
+      incoming.joinMulticast(_mDnsAddress!, interface);
+    }
+    incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
+    _started = true;
+    _starting = false;
+  }
+
+  /// Stop the client and close any associated sockets.
+  void stop() {
+    if (!_started) {
+      return;
+    }
+    if (_starting) {
+      throw StateError('Cannot stop mDNS client while it is starting.');
+    }
+
+    for (final RawDatagramSocket socket in _sockets) {
+      socket.close();
+    }
+
+    _resolver.clearPendingRequests();
+
+    _started = false;
+  }
+
+  /// Lookup a [ResourceRecord], potentially from the cache.
+  ///
+  /// The [type] parameter must be a valid [ResourceRecordType].  The [fullyQualifiedName]
+  /// parameter is the name of the service to lookup, and must not be null. The
+  /// [timeout] parameter specifies how long the internal cache should hold on
+  /// to the record.  The [multicast] parameter specifies whether the query
+  /// should be sent as unicast (QU) or multicast (QM).
+  ///
+  /// Some publishers have been observed to not respond to unicast requests
+  /// properly, so the default is true.
+  Stream<T> lookup<T extends ResourceRecord>(
+    ResourceRecordQuery query, {
+    Duration timeout = const Duration(seconds: 5),
+  }) {
+    final int? selectedMDnsPort = _mDnsPort;
+    if (!_started || selectedMDnsPort == null) {
+      throw StateError('mDNS client must be started before calling lookup.');
+    }
+    // Look for entries in the cache.
+    final List<T> cached = <T>[];
+    _cache.lookup<T>(
+        query.fullyQualifiedName, query.resourceRecordType, cached);
+    if (cached.isNotEmpty) {
+      final StreamController<T> controller = StreamController<T>();
+      cached.forEach(controller.add);
+      controller.close();
+      return controller.stream;
+    }
+
+    // Add the pending request before sending the query.
+    final Stream<T> results = _resolver.addPendingRequest<T>(
+        query.resourceRecordType, query.fullyQualifiedName, timeout);
+
+    // Send the request on all interfaces.
+    final List<int> packet = query.encode();
+    for (final RawDatagramSocket socket in _sockets) {
+      socket.send(packet, _mDnsAddress!, selectedMDnsPort);
+    }
+    return results;
+  }
+
+  // Process incoming datagrams.
+  void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
+    if (event == RawSocketEvent.read) {
+      final Datagram? datagram = incoming.receive();
+      if (datagram == null) {
+        return;
+      }
+
+      // Check for published responses.
+      final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
+      if (response != null) {
+        _cache.updateRecords(response);
+        _resolver.handleResponse(response);
+        return;
+      }
+      // TODO(dnfield): Support queries coming in for published entries.
+    }
+  }
+}
diff --git a/packages/multicast_dns/lib/src/constants.dart b/packages/multicast_dns/lib/src/constants.dart
new file mode 100644
index 0000000..061329d
--- /dev/null
+++ b/packages/multicast_dns/lib/src/constants.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2015, the Dartino 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:io';
+
+/// The IPv4 mDNS Address.
+final InternetAddress mDnsAddressIPv4 = InternetAddress('224.0.0.251');
+
+/// The IPv6 mDNS Address.
+final InternetAddress mDnsAddressIPv6 = InternetAddress('FF02::FB');
+
+/// The mDNS port.
+const int mDnsPort = 5353;
+
+/// Enumeration of supported resource record class types.
+abstract class ResourceRecordClass {
+  // This class is intended to be used as a namespace, and should not be
+  // extended directly.
+  ResourceRecordClass._();
+
+  /// Internet address class ("IN").
+  static const int internet = 1;
+}
+
+/// Enumeration of DNS question types.
+abstract class QuestionType {
+  // This class is intended to be used as a namespace, and should not be
+  // extended directly.
+  QuestionType._();
+
+  /// "QU" Question.
+  static const int unicast = 0x8000;
+
+  /// "QM" Question.
+  static const int multicast = 0x0000;
+}
diff --git a/packages/multicast_dns/lib/src/lookup_resolver.dart b/packages/multicast_dns/lib/src/lookup_resolver.dart
new file mode 100644
index 0000000..a25e691
--- /dev/null
+++ b/packages/multicast_dns/lib/src/lookup_resolver.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2015, the Dartino 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 'dart:collection';
+
+import 'package:multicast_dns/src/resource_record.dart';
+
+/// Class for maintaining state about pending mDNS requests.
+class PendingRequest extends LinkedListEntry<PendingRequest> {
+  /// Creates a new PendingRequest.
+  PendingRequest(this.type, this.domainName, this.controller);
+
+  /// The [ResourceRecordType] of the request.
+  final int type;
+
+  /// The domain name to look up via mDNS.
+  ///
+  /// For example, `'_http._tcp.local` to look up HTTP services on the local
+  /// domain.
+  final String domainName;
+
+  /// A StreamController managing the request.
+  final StreamController<ResourceRecord> controller;
+
+  /// The timer for the request.
+  Timer? timer;
+}
+
+/// Class for keeping track of pending lookups and processing incoming
+/// query responses.
+class LookupResolver {
+  final LinkedList<PendingRequest> _pendingRequests =
+      LinkedList<PendingRequest>();
+
+  /// Adds a request and returns a [Stream] of [ResourceRecord] responses.
+  Stream<T> addPendingRequest<T extends ResourceRecord>(
+      int type, String name, Duration timeout) {
+    final StreamController<T> controller = StreamController<T>();
+    final PendingRequest request = PendingRequest(type, name, controller);
+    final Timer timer = Timer(timeout, () {
+      request.unlink();
+      controller.close();
+    });
+    request.timer = timer;
+    _pendingRequests.add(request);
+    return controller.stream;
+  }
+
+  /// Parses [ResoureRecord]s received and delivers them to the appropriate
+  /// listener(s) added via [addPendingRequest].
+  void handleResponse(List<ResourceRecord> response) {
+    for (final ResourceRecord r in response) {
+      final int type = r.resourceRecordType;
+      String name = r.name.toLowerCase();
+      if (name.endsWith('.')) {
+        name = name.substring(0, name.length - 1);
+      }
+
+      bool responseMatches(PendingRequest request) {
+        String requestName = request.domainName.toLowerCase();
+        // make, e.g. "_http" become "_http._tcp.local".
+        if (!requestName.endsWith('local')) {
+          if (!requestName.endsWith('._tcp.local') &&
+              !requestName.endsWith('._udp.local') &&
+              !requestName.endsWith('._tcp') &&
+              !requestName.endsWith('.udp')) {
+            requestName += '._tcp';
+          }
+          requestName += '.local';
+        }
+        return requestName == name && request.type == type;
+      }
+
+      for (final PendingRequest pendingRequest in _pendingRequests) {
+        if (responseMatches(pendingRequest)) {
+          if (pendingRequest.controller.isClosed) {
+            return;
+          }
+          pendingRequest.controller.add(r);
+        }
+      }
+    }
+  }
+
+  /// Removes any pending requests and ends processing.
+  void clearPendingRequests() {
+    while (_pendingRequests.isNotEmpty) {
+      final PendingRequest request = _pendingRequests.first;
+      request.unlink();
+      request.timer?.cancel();
+      request.controller.close();
+    }
+  }
+}
diff --git a/packages/multicast_dns/lib/src/native_protocol_client.dart b/packages/multicast_dns/lib/src/native_protocol_client.dart
new file mode 100644
index 0000000..15e70c9
--- /dev/null
+++ b/packages/multicast_dns/lib/src/native_protocol_client.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2015, the Dartino 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:collection';
+
+import 'package:multicast_dns/src/resource_record.dart';
+
+/// Cache for resource records that have been received.
+///
+/// There can be multiple entries for the same name and type.
+///
+/// The cache is updated with a list of records, because it needs to remove
+/// all entries that correspond to the name and type of the name/type
+/// combinations of records that should be updated.  For example, a host may
+/// remove one of its IP addresses and report the remaining address as a
+/// response - then we need to clear all previous entries for that host before
+/// updating the cache.
+class ResourceRecordCache {
+  /// Creates a new ResourceRecordCache.
+  ResourceRecordCache();
+
+  final Map<int, SplayTreeMap<String, List<ResourceRecord>>> _cache =
+      <int, SplayTreeMap<String, List<ResourceRecord>>>{};
+
+  /// The number of entries in the cache.
+  int get entryCount {
+    int count = 0;
+    for (final SplayTreeMap<String, List<ResourceRecord>> map
+        in _cache.values) {
+      for (final List<ResourceRecord> records in map.values) {
+        count += records.length;
+      }
+    }
+    return count;
+  }
+
+  /// Update the records in this cache.
+  void updateRecords(List<ResourceRecord> records) {
+    // TODO(karlklose): include flush bit in the record and only flush if
+    // necessary.
+    // Clear the cache for all name/type combinations to be updated.
+    final Map<int, Set<String>> seenRecordTypes = <int, Set<String>>{};
+    for (final ResourceRecord record in records) {
+      // TODO(dnfield): Update this to use set literal syntax when we're able to bump the SDK constraint.
+      seenRecordTypes[record.resourceRecordType] ??=
+          Set<String>(); // ignore: prefer_collection_literals
+      if (seenRecordTypes[record.resourceRecordType]!.add(record.name)) {
+        _cache[record.resourceRecordType] ??=
+            SplayTreeMap<String, List<ResourceRecord>>();
+
+        _cache[record.resourceRecordType]![record.name] = <ResourceRecord>[
+          record
+        ];
+      } else {
+        _cache[record.resourceRecordType]![record.name]!.add(record);
+      }
+    }
+  }
+
+  /// Get a record from this cache.
+  void lookup<T extends ResourceRecord>(
+      String name, int type, List<T> results) {
+    assert(ResourceRecordType.debugAssertValid(type));
+    final int time = DateTime.now().millisecondsSinceEpoch;
+    final SplayTreeMap<String, List<ResourceRecord>>? candidates = _cache[type];
+    if (candidates == null) {
+      return;
+    }
+
+    final List<ResourceRecord>? candidateRecords = candidates[name];
+    if (candidateRecords == null) {
+      return;
+    }
+    candidateRecords
+        .removeWhere((ResourceRecord candidate) => candidate.validUntil < time);
+    results.addAll(candidateRecords.cast<T>());
+  }
+}
diff --git a/packages/multicast_dns/lib/src/packet.dart b/packages/multicast_dns/lib/src/packet.dart
new file mode 100644
index 0000000..9905689
--- /dev/null
+++ b/packages/multicast_dns/lib/src/packet.dart
@@ -0,0 +1,397 @@
+// 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.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+// Offsets into the header. See https://tools.ietf.org/html/rfc1035.
+const int _kIdOffset = 0;
+const int _kFlagsOffset = 2;
+const int _kQdcountOffset = 4;
+const int _kAncountOffset = 6;
+const int _kNscountOffset = 8;
+const int _kArcountOffset = 10;
+const int _kHeaderSize = 12;
+
+/// Processes a DNS query name into a list of parts.
+///
+/// Will attempt to append 'local' if the name is something like '_http._tcp',
+/// and '._tcp.local' if name is something like '_http'.
+List<String> processDnsNameParts(String name) {
+  final List<String> parts = name.split('.');
+  if (parts.length == 1) {
+    return <String>[parts[0], '_tcp', 'local'];
+  } else if (parts.length == 2 && parts[1].startsWith('_')) {
+    return <String>[parts[0], parts[1], 'local'];
+  }
+
+  return parts;
+}
+
+/// Encode an mDNS query packet.
+///
+/// The [type] parameter must be a valid [ResourceRecordType] value. The
+/// [multicast] parameter must not be null.
+///
+/// This is a low level API; most consumers should prefer
+/// [ResourceRecordQuery.encode], which offers some convenience wrappers around
+/// selecting the correct [type] and setting the [name] parameter correctly.
+List<int> encodeMDnsQuery(
+  String name, {
+  int type = ResourceRecordType.addressIPv4,
+  bool multicast = true,
+}) {
+  assert(ResourceRecordType.debugAssertValid(type));
+
+  final List<String> nameParts = processDnsNameParts(name);
+  final List<List<int>> rawNameParts =
+      nameParts.map<List<int>>((String part) => utf8.encode(part)).toList();
+
+  // Calculate the size of the packet.
+  int size = _kHeaderSize;
+  for (int i = 0; i < rawNameParts.length; i++) {
+    size += 1 + rawNameParts[i].length;
+  }
+
+  size += 1; // End with empty part
+  size += 4; // Trailer (QTYPE and QCLASS).
+  final Uint8List data = Uint8List(size);
+  final ByteData packetByteData = ByteData.view(data.buffer);
+  // Query identifier - just use 0.
+  packetByteData.setUint16(_kIdOffset, 0);
+  // Flags - 0 for query.
+  packetByteData.setUint16(_kFlagsOffset, 0);
+  // Query count.
+  packetByteData.setUint16(_kQdcountOffset, 1);
+  // Number of answers - 0 for query.
+  packetByteData.setUint16(_kAncountOffset, 0);
+  // Number of name server records - 0 for query.
+  packetByteData.setUint16(_kNscountOffset, 0);
+  // Number of resource records - 0 for query.
+  packetByteData.setUint16(_kArcountOffset, 0);
+  int offset = _kHeaderSize;
+  for (int i = 0; i < rawNameParts.length; i++) {
+    data[offset++] = rawNameParts[i].length;
+    data.setRange(offset, offset + rawNameParts[i].length, rawNameParts[i]);
+    offset += rawNameParts[i].length;
+  }
+
+  data[offset] = 0; // Empty part.
+  offset++;
+  packetByteData.setUint16(offset, type); // QTYPE.
+  offset += 2;
+  packetByteData.setUint16(
+      offset,
+      ResourceRecordClass.internet |
+          (multicast ? QuestionType.multicast : QuestionType.unicast));
+
+  return data;
+}
+
+/// Result of reading a Fully Qualified Domain Name (FQDN).
+class _FQDNReadResult {
+  /// Creates a new FQDN read result.
+  _FQDNReadResult(this.fqdnParts, this.bytesRead);
+
+  /// The raw parts of the FQDN.
+  final List<String> fqdnParts;
+
+  /// The bytes consumed from the packet for this FQDN.
+  final int bytesRead;
+
+  /// Returns the Fully Qualified Domain Name.
+  String get fqdn => fqdnParts.join('.');
+
+  @override
+  String toString() => fqdn;
+}
+
+/// Reads a FQDN from raw packet data.
+String readFQDN(List<int> packet, [int offset = 0]) {
+  final Uint8List data =
+      packet is Uint8List ? packet : Uint8List.fromList(packet);
+  final ByteData byteData = ByteData.view(data.buffer);
+
+  return _readFQDN(data, byteData, offset, data.length).fqdn;
+}
+
+// Read a FQDN at the given offset. Returns a pair with the FQDN
+// parts and the number of bytes consumed.
+//
+// If decoding fails (e.g. due to an invalid packet) `null` is returned.
+_FQDNReadResult _readFQDN(
+    Uint8List data, ByteData byteData, int offset, int length) {
+  void checkLength(int required) {
+    if (length < required) {
+      throw MDnsDecodeException(required);
+    }
+  }
+
+  final List<String> parts = <String>[];
+  final int prevOffset = offset;
+  while (true) {
+    // At least one byte is required.
+    checkLength(offset + 1);
+
+    // Check for compressed.
+    if (data[offset] & 0xc0 == 0xc0) {
+      // At least two bytes are required for a compressed FQDN.
+      checkLength(offset + 2);
+
+      // A compressed FQDN has a new offset in the lower 14 bits.
+      final _FQDNReadResult result = _readFQDN(
+          data, byteData, byteData.getUint16(offset) & ~0xc000, length);
+      parts.addAll(result.fqdnParts);
+      offset += 2;
+      break;
+    } else {
+      // A normal FQDN part has a length and a UTF-8 encoded name
+      // part. If the length is 0 this is the end of the FQDN.
+      final int partLength = data[offset];
+      offset++;
+      if (partLength > 0) {
+        checkLength(offset + partLength);
+        final Uint8List partBytes =
+            Uint8List.view(data.buffer, offset, partLength);
+        offset += partLength;
+        // According to the RFC, this is supposed to be utf-8 encoded, but
+        // we should continue decoding even if it isn't to avoid dropping the
+        // rest of the data, which might still be useful.
+        parts.add(utf8.decode(partBytes, allowMalformed: true));
+      } else {
+        break;
+      }
+    }
+  }
+  return _FQDNReadResult(parts, offset - prevOffset);
+}
+
+/// Decode an mDNS query packet.
+///
+/// If decoding fails (e.g. due to an invalid packet), `null` is returned.
+///
+/// See https://tools.ietf.org/html/rfc1035 for format.
+ResourceRecordQuery? decodeMDnsQuery(List<int> packet) {
+  final int length = packet.length;
+  if (length < _kHeaderSize) {
+    return null;
+  }
+
+  final Uint8List data =
+      packet is Uint8List ? packet : Uint8List.fromList(packet);
+  final ByteData packetBytes = ByteData.view(data.buffer);
+
+  // Check whether it's a query.
+  final int flags = packetBytes.getUint16(_kFlagsOffset);
+  if (flags != 0) {
+    return null;
+  }
+  final int questionCount = packetBytes.getUint16(_kQdcountOffset);
+  if (questionCount == 0) {
+    return null;
+  }
+
+  final _FQDNReadResult fqdn =
+      _readFQDN(data, packetBytes, _kHeaderSize, data.length);
+
+  int offset = _kHeaderSize + fqdn.bytesRead;
+  final int type = packetBytes.getUint16(offset);
+  offset += 2;
+  final int queryType = packetBytes.getUint16(offset) & 0x8000;
+  return ResourceRecordQuery(type, fqdn.fqdn, queryType);
+}
+
+/// Decode an mDNS response packet.
+///
+/// If decoding fails (e.g. due to an invalid packet) `null` is returned.
+///
+/// See https://tools.ietf.org/html/rfc1035 for the format.
+List<ResourceRecord>? decodeMDnsResponse(List<int> packet) {
+  final int length = packet.length;
+  if (length < _kHeaderSize) {
+    return null;
+  }
+
+  final Uint8List data =
+      packet is Uint8List ? packet : Uint8List.fromList(packet);
+  final ByteData packetBytes = ByteData.view(data.buffer);
+
+  final int answerCount = packetBytes.getUint16(_kAncountOffset);
+  final int authorityCount = packetBytes.getUint16(_kNscountOffset);
+  final int additionalCount = packetBytes.getUint16(_kArcountOffset);
+  final int remainingCount = answerCount + authorityCount + additionalCount;
+
+  if (remainingCount == 0) {
+    return null;
+  }
+
+  final int questionCount = packetBytes.getUint16(_kQdcountOffset);
+  int offset = _kHeaderSize;
+
+  void checkLength(int required) {
+    if (length < required) {
+      throw MDnsDecodeException(required);
+    }
+  }
+
+  ResourceRecord? readResourceRecord() {
+    // First read the FQDN.
+    final _FQDNReadResult result = _readFQDN(data, packetBytes, offset, length);
+    final String fqdn = result.fqdn;
+    offset += result.bytesRead;
+    checkLength(offset + 2);
+    final int type = packetBytes.getUint16(offset);
+    offset += 2;
+    // The first bit of the rrclass field is set to indicate that the answer is
+    // unique and the querier should flush the cached answer for this name
+    // (RFC 6762, Sec. 10.2). We ignore it for now since we don't cache answers.
+    checkLength(offset + 2);
+    final int resourceRecordClass = packetBytes.getUint16(offset) & 0x7fff;
+
+    if (resourceRecordClass != ResourceRecordClass.internet) {
+      // We do not support other classes.
+      return null;
+    }
+
+    offset += 2;
+    checkLength(offset + 4);
+    final int ttl = packetBytes.getInt32(offset);
+    offset += 4;
+
+    checkLength(offset + 2);
+    final int readDataLength = packetBytes.getUint16(offset);
+    offset += 2;
+    final int validUntil = DateTime.now().millisecondsSinceEpoch + ttl * 1000;
+    switch (type) {
+      case ResourceRecordType.addressIPv4:
+        checkLength(offset + readDataLength);
+        final StringBuffer addr = StringBuffer();
+        final int stop = offset + readDataLength;
+        addr.write(packetBytes.getUint8(offset));
+        offset++;
+        for (; offset < stop; offset++) {
+          addr.write('.');
+          addr.write(packetBytes.getUint8(offset));
+        }
+        return IPAddressResourceRecord(fqdn, validUntil,
+            address: InternetAddress(addr.toString()));
+      case ResourceRecordType.addressIPv6:
+        checkLength(offset + readDataLength);
+        final StringBuffer addr = StringBuffer();
+        final int stop = offset + readDataLength;
+        addr.write(packetBytes.getUint16(offset).toRadixString(16));
+        offset += 2;
+        for (; offset < stop; offset += 2) {
+          addr.write(':');
+          addr.write(packetBytes.getUint16(offset).toRadixString(16));
+        }
+        return IPAddressResourceRecord(
+          fqdn,
+          validUntil,
+          address: InternetAddress(addr.toString()),
+        );
+      case ResourceRecordType.service:
+        checkLength(offset + 2);
+        final int priority = packetBytes.getUint16(offset);
+        offset += 2;
+        checkLength(offset + 2);
+        final int weight = packetBytes.getUint16(offset);
+        offset += 2;
+        checkLength(offset + 2);
+        final int port = packetBytes.getUint16(offset);
+        offset += 2;
+        final _FQDNReadResult result =
+            _readFQDN(data, packetBytes, offset, length);
+        offset += result.bytesRead;
+        return SrvResourceRecord(
+          fqdn,
+          validUntil,
+          target: result.fqdn,
+          port: port,
+          priority: priority,
+          weight: weight,
+        );
+      case ResourceRecordType.serverPointer:
+        checkLength(offset + readDataLength);
+        final _FQDNReadResult result =
+            _readFQDN(data, packetBytes, offset, length);
+        offset += readDataLength;
+        return PtrResourceRecord(
+          fqdn,
+          validUntil,
+          domainName: result.fqdn,
+        );
+      case ResourceRecordType.text:
+        checkLength(offset + readDataLength);
+        // The first byte of the buffer is the length of the first string of
+        // the TXT record. Further length-prefixed strings may follow. We
+        // concatenate them with newlines.
+        final StringBuffer strings = StringBuffer();
+        int index = 0;
+        while (index < readDataLength) {
+          final int txtLength = data[offset + index];
+          index++;
+          if (txtLength == 0) {
+            continue;
+          }
+          final String text = utf8.decode(
+            Uint8List.view(data.buffer, offset + index, txtLength),
+            allowMalformed: true,
+          );
+          strings.writeln(text);
+          index += txtLength;
+        }
+        offset += readDataLength;
+        return TxtResourceRecord(fqdn, validUntil, text: strings.toString());
+      default:
+        checkLength(offset + readDataLength);
+        offset += readDataLength;
+        return null;
+    }
+  }
+
+  // This list can't be fixed length right now because we might get
+  // resource record types we don't support, and consumers expect this list
+  // to not have null entries.
+  final List<ResourceRecord> result = <ResourceRecord>[];
+
+  try {
+    for (int i = 0; i < questionCount; i++) {
+      final _FQDNReadResult result =
+          _readFQDN(data, packetBytes, offset, length);
+      offset += result.bytesRead;
+      checkLength(offset + 4);
+      offset += 4;
+    }
+    for (int i = 0; i < remainingCount; i++) {
+      final ResourceRecord? record = readResourceRecord();
+      if (record != null) {
+        result.add(record);
+      }
+    }
+  } on MDnsDecodeException {
+    // If decoding fails return null.
+    return null;
+  }
+  return result;
+}
+
+/// This exception is thrown by the decoder when the packet is invalid.
+class MDnsDecodeException implements Exception {
+  /// Creates a new MDnsDecodeException, indicating an error in decoding at the
+  /// specified [offset].
+  ///
+  /// The [offset] parameter should not be null.
+  const MDnsDecodeException(this.offset);
+
+  /// The offset in the packet at which the exception occurred.
+  final int offset;
+
+  @override
+  String toString() => 'Decoding error at $offset';
+}
diff --git a/packages/multicast_dns/lib/src/resource_record.dart b/packages/multicast_dns/lib/src/resource_record.dart
new file mode 100644
index 0000000..5a752b7
--- /dev/null
+++ b/packages/multicast_dns/lib/src/resource_record.dart
@@ -0,0 +1,394 @@
+// Copyright 2018 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+import 'package:multicast_dns/src/constants.dart';
+import 'package:multicast_dns/src/packet.dart';
+
+// TODO(dnfield): Probably should go with a real hashing function here
+// when https://github.com/dart-lang/sdk/issues/11617 is figured out.
+const int _seedHashPrime = 2166136261;
+const int _multipleHashPrime = 16777619;
+
+int _combineHash(int current, int hash) =>
+    (current & _multipleHashPrime) ^ hash;
+
+int _hashValues(List<int> values) {
+  assert(values.isNotEmpty);
+
+  return values.fold(
+    _seedHashPrime,
+    (int current, int next) => _combineHash(current, next),
+  );
+}
+
+/// Enumeration of support resource record types.
+abstract class ResourceRecordType {
+  // This class is intended to be used as a namespace, and should not be
+  // extended directly.
+  ResourceRecordType._();
+
+  /// An IPv4 Address record, also known as an "A" record. It has a value of 1.
+  static const int addressIPv4 = 1;
+
+  /// An IPv6 Address record, also known as an "AAAA" record.  It has a vaule of
+  /// 28.
+  static const int addressIPv6 = 28;
+
+  /// An IP Address reverse map record, also known as a "PTR" recored. It has a
+  /// value of 12.
+  static const int serverPointer = 12;
+
+  /// An available service record, also known as an "SRV" record.  It has a
+  /// value of 33.
+  static const int service = 33;
+
+  /// A text record, also known as a "TXT" record.  It has a value of 16.
+  static const int text = 16;
+
+  // TODO(dnfield): Support ANY in some meaningful way.  Might be server only.
+  // /// A query for all records of all types known to the name server.
+  // static const int any = 255;
+
+  /// Checks that a given int is a valid ResourceRecordType.
+  ///
+  /// This method is intended to be called only from an `assert()`.
+  static bool debugAssertValid(int resourceRecordType) {
+    return resourceRecordType == addressIPv4 ||
+        resourceRecordType == addressIPv6 ||
+        resourceRecordType == serverPointer ||
+        resourceRecordType == service ||
+        resourceRecordType == text;
+  }
+
+  /// Prints a debug-friendly version of the resource record type value.
+  static String toDebugString(int resourceRecordType) {
+    switch (resourceRecordType) {
+      case addressIPv4:
+        return 'A (IPv4 Address)';
+      case addressIPv6:
+        return 'AAAA (IPv6 Address)';
+      case serverPointer:
+        return 'PTR (Domain Name Pointer)';
+      case service:
+        return 'SRV (Service record)';
+      case text:
+        return 'TXT (Text)';
+    }
+    return 'Unknown ($resourceRecordType)';
+  }
+}
+
+/// Represents a DNS query.
+@immutable
+class ResourceRecordQuery {
+  /// Creates a new ResourceRecordQuery.
+  ///
+  /// Most callers should prefer one of the named constructors.
+  ResourceRecordQuery(
+    this.resourceRecordType,
+    this.fullyQualifiedName,
+    this.questionType,
+  ) : assert(ResourceRecordType.debugAssertValid(resourceRecordType));
+
+  /// An A (IPv4) query.
+  ResourceRecordQuery.addressIPv4(
+    String name, {
+    bool isMulticast = true,
+  }) : this(
+          ResourceRecordType.addressIPv4,
+          name,
+          isMulticast ? QuestionType.multicast : QuestionType.unicast,
+        );
+
+  /// An AAAA (IPv6) query.
+  ResourceRecordQuery.addressIPv6(
+    String name, {
+    bool isMulticast = true,
+  }) : this(
+          ResourceRecordType.addressIPv6,
+          name,
+          isMulticast ? QuestionType.multicast : QuestionType.unicast,
+        );
+
+  /// A PTR (Server pointer) query.
+  ResourceRecordQuery.serverPointer(
+    String name, {
+    bool isMulticast = true,
+  }) : this(
+          ResourceRecordType.serverPointer,
+          name,
+          isMulticast ? QuestionType.multicast : QuestionType.unicast,
+        );
+
+  /// An SRV (Service) query.
+  ResourceRecordQuery.service(
+    String name, {
+    bool isMulticast = true,
+  }) : this(
+          ResourceRecordType.service,
+          name,
+          isMulticast ? QuestionType.multicast : QuestionType.unicast,
+        );
+
+  /// A TXT (Text record) query.
+  ResourceRecordQuery.text(
+    String name, {
+    bool isMulticast = true,
+  }) : this(
+          ResourceRecordType.text,
+          name,
+          isMulticast ? QuestionType.multicast : QuestionType.unicast,
+        );
+
+  /// Tye type of resource record - one of [ResourceRecordType]'s values.
+  final int resourceRecordType;
+
+  /// The Fully Qualified Domain Name associated with the request.
+  final String fullyQualifiedName;
+
+  /// The [QuestionType], i.e. multicast or unicast.
+  final int questionType;
+
+  /// Convenience accessor to determine whether the question type is multicast.
+  bool get isMulticast => questionType == QuestionType.multicast;
+
+  /// Convenience accessor to determine whether the question type is unicast.
+  bool get isUnicast => questionType == QuestionType.unicast;
+
+  /// Encodes this query to the raw wire format.
+  List<int> encode() {
+    return encodeMDnsQuery(
+      fullyQualifiedName,
+      type: resourceRecordType,
+      multicast: isMulticast,
+    );
+  }
+
+  @override
+  int get hashCode => _hashValues(
+      <int>[resourceRecordType, fullyQualifiedName.hashCode, questionType]);
+
+  @override
+  bool operator ==(Object other) {
+    return other is ResourceRecordQuery &&
+        other.resourceRecordType == resourceRecordType &&
+        other.fullyQualifiedName == fullyQualifiedName &&
+        other.questionType == questionType;
+  }
+
+  @override
+  String toString() =>
+      '$runtimeType{$fullyQualifiedName, type: ${ResourceRecordType.toDebugString(resourceRecordType)}, isMulticast: $isMulticast}';
+}
+
+/// Base implementation of DNS resource records (RRs).
+@immutable
+abstract class ResourceRecord {
+  /// Creates a new ResourceRecord.
+  const ResourceRecord(this.resourceRecordType, this.name, this.validUntil);
+
+  /// The FQDN for this record.
+  final String name;
+
+  /// The epoch time at which point this record is valid for in the cache.
+  final int validUntil;
+
+  /// The raw resource record value.  See [ResourceRecordType] for supported values.
+  final int resourceRecordType;
+
+  String get _additionalInfo;
+
+  @override
+  String toString() =>
+      '$runtimeType{$name, validUntil: ${DateTime.fromMillisecondsSinceEpoch(validUntil)}, $_additionalInfo}';
+
+  @override
+  bool operator ==(Object other) {
+    return other is ResourceRecord && _equals(other);
+  }
+
+  bool _equals(ResourceRecord other) {
+    return other.name == name &&
+        other.validUntil == validUntil &&
+        other.resourceRecordType == resourceRecordType;
+  }
+
+  @override
+  int get hashCode {
+    return _hashValues(<int>[
+      name.hashCode,
+      validUntil.hashCode,
+      resourceRecordType.hashCode,
+      _hashCode,
+    ]);
+  }
+
+  // Subclasses of this class should use _hashValues to create a hash code
+  // that will then get hashed in with the common values on this class.
+  int get _hashCode;
+
+  /// Low level method for encoding this record into an mDNS packet.
+  ///
+  /// Subclasses should provide the packet format of their encapsulated data
+  /// into a `Uint8List`, which could then be used to write a pakcet to send
+  /// as a response for this record type.
+  Uint8List encodeResponseRecord();
+}
+
+/// A Service Pointer for reverse mapping an IP address (DNS "PTR").
+class PtrResourceRecord extends ResourceRecord {
+  /// Creates a new PtrResourceRecord.
+  const PtrResourceRecord(
+    String name,
+    int validUntil, {
+    required this.domainName,
+  }) : super(ResourceRecordType.serverPointer, name, validUntil);
+
+  /// The FQDN for this record.
+  final String domainName;
+
+  @override
+  String get _additionalInfo => 'domainName: $domainName';
+
+  @override
+  bool _equals(ResourceRecord other) {
+    return other is PtrResourceRecord &&
+        other.domainName == domainName &&
+        super._equals(other);
+  }
+
+  @override
+  int get _hashCode => _combineHash(_seedHashPrime, domainName.hashCode);
+
+  @override
+  Uint8List encodeResponseRecord() {
+    return Uint8List.fromList(utf8.encode(domainName));
+  }
+}
+
+/// An IP Address record for IPv4 (DNS "A") or IPv6 (DNS "AAAA") records.
+class IPAddressResourceRecord extends ResourceRecord {
+  /// Creates a new IPAddressResourceRecord.
+  IPAddressResourceRecord(
+    String name,
+    int validUntil, {
+    required this.address,
+  }) : super(
+            address.type == InternetAddressType.IPv4
+                ? ResourceRecordType.addressIPv4
+                : ResourceRecordType.addressIPv6,
+            name,
+            validUntil);
+
+  /// The [InternetAddress] for this record.
+  final InternetAddress address;
+
+  @override
+  String get _additionalInfo => 'address: $address';
+
+  @override
+  bool _equals(ResourceRecord other) {
+    return other is IPAddressResourceRecord && other.address == address;
+  }
+
+  @override
+  int get _hashCode => _combineHash(_seedHashPrime, address.hashCode);
+
+  @override
+  Uint8List encodeResponseRecord() {
+    return Uint8List.fromList(address.rawAddress);
+  }
+}
+
+/// A Service record, capturing a host target and port (DNS "SRV").
+class SrvResourceRecord extends ResourceRecord {
+  /// Creates a new service record.
+  const SrvResourceRecord(
+    String name,
+    int validUntil, {
+    required this.target,
+    required this.port,
+    required this.priority,
+    required this.weight,
+  }) : super(ResourceRecordType.service, name, validUntil);
+
+  /// The hostname for this record.
+  final String target;
+
+  /// The port for this record.
+  final int port;
+
+  /// The relative priority of this service.
+  final int priority;
+
+  /// The weight (used when multiple services have the same priority).
+  final int weight;
+
+  @override
+  String get _additionalInfo =>
+      'target: $target, port: $port, priority: $priority, weight: $weight';
+
+  @override
+  bool _equals(ResourceRecord other) {
+    return other is SrvResourceRecord &&
+        other.target == target &&
+        other.port == port &&
+        other.priority == priority &&
+        other.weight == weight;
+  }
+
+  @override
+  int get _hashCode => _hashValues(<int>[
+        target.hashCode,
+        port.hashCode,
+        priority.hashCode,
+        weight.hashCode,
+      ]);
+
+  @override
+  Uint8List encodeResponseRecord() {
+    final List<int> data = utf8.encode(target);
+    final Uint8List result = Uint8List(data.length + 7);
+    final ByteData resultData = ByteData.view(result.buffer);
+    resultData.setUint16(0, priority);
+    resultData.setUint16(2, weight);
+    resultData.setUint16(4, port);
+    result[6] = data.length;
+    return result..setRange(7, data.length, data);
+  }
+}
+
+/// A Text record, contianing additional textual data (DNS "TXT").
+class TxtResourceRecord extends ResourceRecord {
+  /// Creates a new text record.
+  const TxtResourceRecord(
+    String name,
+    int validUntil, {
+    required this.text,
+  }) : super(ResourceRecordType.text, name, validUntil);
+
+  /// The raw text from this record.
+  final String text;
+
+  @override
+  String get _additionalInfo => 'text: $text';
+
+  @override
+  bool _equals(ResourceRecord other) {
+    return other is TxtResourceRecord && other.text == text;
+  }
+
+  @override
+  int get _hashCode => _combineHash(_seedHashPrime, text.hashCode);
+
+  @override
+  Uint8List encodeResponseRecord() {
+    return Uint8List.fromList(utf8.encode(text));
+  }
+}
diff --git a/packages/multicast_dns/pubspec.yaml b/packages/multicast_dns/pubspec.yaml
new file mode 100644
index 0000000..058d0c4
--- /dev/null
+++ b/packages/multicast_dns/pubspec.yaml
@@ -0,0 +1,13 @@
+name: multicast_dns
+description: Dart package for mDNS queries (e.g. Bonjour, Avahi).
+homepage: https://github.com/flutter/packages/tree/master/packages/multicast_dns
+version: 0.3.0
+
+dependencies:
+  meta: ^1.3.0
+
+dev_dependencies:
+  test: "^1.16.5"
+
+environment:
+  sdk: ">=2.12.0-0 <3.0.0"
diff --git a/packages/multicast_dns/test/client_test.dart b/packages/multicast_dns/test/client_test.dart
new file mode 100644
index 0000000..85ab0b1
--- /dev/null
+++ b/packages/multicast_dns/test/client_test.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2015, the Dartino 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 'dart:io';
+
+import 'package:multicast_dns/multicast_dns.dart';
+import 'package:test/fake.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('Can inject datagram socket factory and configure mdns port', () async {
+    late int lastPort;
+    final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
+    final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
+        (dynamic host, int port,
+            {bool reuseAddress = true,
+            bool reusePort = true,
+            int ttl = 1}) async {
+      lastPort = port;
+      return datagramSocket;
+    });
+
+    await client.start(
+        mDnsPort: 1234,
+        interfacesFactory: (InternetAddressType type) async =>
+            <NetworkInterface>[]);
+
+    expect(lastPort, 1234);
+  });
+}
+
+class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
+  @override
+  InternetAddress get address => InternetAddress.anyIPv4;
+
+  @override
+  StreamSubscription<RawSocketEvent> listen(
+      void Function(RawSocketEvent event)? onData,
+      {Function? onError,
+      void Function()? onDone,
+      bool? cancelOnError}) {
+    return const Stream<RawSocketEvent>.empty().listen(onData,
+        onError: onError, cancelOnError: cancelOnError, onDone: onDone);
+  }
+}
diff --git a/packages/multicast_dns/test/decode_test.dart b/packages/multicast_dns/test/decode_test.dart
new file mode 100644
index 0000000..f6c5e88
--- /dev/null
+++ b/packages/multicast_dns/test/decode_test.dart
@@ -0,0 +1,1601 @@
+// Copyright (c) 2015, the Dartino 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:io';
+
+import 'package:test/test.dart';
+import 'package:multicast_dns/src/packet.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+const int _kSrvHeaderSize = 6;
+
+void main() {
+  testValidPackages();
+  testBadPackages();
+  testNonUtf8DomainName();
+  // testHexDumpList();
+  testPTRRData();
+  testSRVRData();
+}
+
+void testValidPackages() {
+  test('Can decode valid packets', () {
+    List<ResourceRecord> result = decodeMDnsResponse(package1)!;
+    expect(result, isNotNull);
+    expect(result.length, 1);
+    IPAddressResourceRecord ipResult = result[0] as IPAddressResourceRecord;
+    expect(ipResult.name, 'raspberrypi.local');
+    expect(ipResult.address.address, '192.168.1.191');
+
+    result = decodeMDnsResponse(package2)!;
+    expect(result.length, 2);
+    ipResult = result[0] as IPAddressResourceRecord;
+    expect(ipResult.name, 'raspberrypi.local');
+    expect(ipResult.address.address, '192.168.1.191');
+    ipResult = result[1] as IPAddressResourceRecord;
+    expect(ipResult.name, 'raspberrypi.local');
+    expect(ipResult.address.address, '169.254.95.83');
+
+    result = decodeMDnsResponse(package3)!;
+    expect(result.length, 8);
+    expect(result, <ResourceRecord>[
+      TxtResourceRecord(
+        'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+        result[0].validUntil,
+        text: '',
+      ),
+      PtrResourceRecord(
+        '_udisks-ssh._tcp.local',
+        result[1].validUntil,
+        domainName: 'raspberrypi._udisks-ssh._tcp.local',
+      ),
+      SrvResourceRecord(
+        'raspberrypi._udisks-ssh._tcp.local',
+        result[2].validUntil,
+        target: 'raspberrypi.local',
+        port: 22,
+        priority: 0,
+        weight: 0,
+      ),
+      TxtResourceRecord(
+        'raspberrypi._udisks-ssh._tcp.local',
+        result[3].validUntil,
+        text: '',
+      ),
+      PtrResourceRecord('_services._dns-sd._udp.local', result[4].validUntil,
+          domainName: '_udisks-ssh._tcp.local'),
+      PtrResourceRecord(
+        '_workstation._tcp.local',
+        result[5].validUntil,
+        domainName: 'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+      ),
+      SrvResourceRecord(
+        'raspberrypi [b8:27:eb:03:92:4b]._workstation._tcp.local',
+        result[6].validUntil,
+        target: 'raspberrypi.local',
+        port: 9,
+        priority: 0,
+        weight: 0,
+      ),
+      PtrResourceRecord(
+        '_services._dns-sd._udp.local',
+        result[7].validUntil,
+        domainName: '_workstation._tcp.local',
+      ),
+    ]);
+
+    result = decodeMDnsResponse(packagePtrResponse)!;
+    expect(6, result.length);
+    expect(result, <ResourceRecord>[
+      PtrResourceRecord(
+        '_fletch_agent._tcp.local',
+        result[0].validUntil,
+        domainName: 'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+      ),
+      TxtResourceRecord(
+        'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+        result[1].validUntil,
+        text: '',
+      ),
+      SrvResourceRecord(
+        'fletch-agent on raspberrypi._fletch_agent._tcp.local',
+        result[2].validUntil,
+        target: 'raspberrypi.local',
+        port: 12121,
+        priority: 0,
+        weight: 0,
+      ),
+      IPAddressResourceRecord(
+        'raspberrypi.local',
+        result[3].validUntil,
+        address: InternetAddress('fe80:0000:0000:0000:ba27:ebff:fe69:6e3a'),
+      ),
+      IPAddressResourceRecord(
+        'raspberrypi.local',
+        result[4].validUntil,
+        address: InternetAddress('192.168.1.1'),
+      ),
+      IPAddressResourceRecord(
+        'raspberrypi.local',
+        result[5].validUntil,
+        address: InternetAddress('169.254.167.172'),
+      ),
+    ]);
+  });
+
+  // Fixes https://github.com/flutter/flutter/issues/31854
+  test('Can decode packages with question, answer and additional', () {
+    final List<ResourceRecord> result =
+        decodeMDnsResponse(packetWithQuestionAnArCount)!;
+    expect(result, isNotNull);
+    expect(result.length, 2);
+    expect(result, <ResourceRecord>[
+      PtrResourceRecord(
+        '_______________.____._____',
+        result[0].validUntil,
+        domainName: '_______________________._______________.____._____',
+      ),
+      PtrResourceRecord(
+        '_______________.____._____',
+        result[1].validUntil,
+        domainName: '____________________________._______________.____._____',
+      ),
+    ]);
+  });
+
+  // Fixes https://github.com/flutter/flutter/issues/31854
+  test('Can decode packages without question and with answer and additional',
+      () {
+    final List<ResourceRecord> result =
+        decodeMDnsResponse(packetWithoutQuestionWithAnArCount)!;
+    expect(result, isNotNull);
+    expect(result.length, 2);
+    expect(result, <ResourceRecord>[
+      PtrResourceRecord(
+        '_______________.____._____',
+        result[0].validUntil,
+        domainName: '______________________._______________.____._____',
+      ),
+      TxtResourceRecord(
+        '_______________.____._____',
+        result[1].validUntil,
+        text: 'model=MacBookPro14,3\nosxvers=18\necolor=225,225,223\n',
+      ),
+    ]);
+  });
+
+  test('Can decode packages with a long text resource', () {
+    final List<ResourceRecord> result = decodeMDnsResponse(packetWithLongTxt)!;
+    expect(result, isNotNull);
+    expect(result.length, 2);
+    expect(result, <ResourceRecord>[
+      PtrResourceRecord(
+        '_______________.____._____',
+        result[0].validUntil,
+        domainName: '______________________._______________.____._____',
+      ),
+      TxtResourceRecord(
+        '_______________.____._____',
+        result[1].validUntil,
+        text: (')' * 129) + '\n',
+      ),
+    ]);
+  });
+}
+
+void testBadPackages() {
+  test('Returns null for invalid packets', () {
+    for (final List<int> p in <List<int>>[package1, package2, package3]) {
+      for (int i = 0; i < p.length; i++) {
+        expect(decodeMDnsResponse(p.sublist(0, i)), isNull);
+      }
+    }
+  });
+}
+
+void testPTRRData() {
+  test('Can read FQDN from PTR data', () {
+    expect('sgjesse-macbookpro2 [78:31:c1:b8:55:38]._workstation._tcp.local',
+        readFQDN(ptrRData));
+    expect('fletch-agent._fletch_agent._tcp.local', readFQDN(ptrRData2));
+  });
+}
+
+void testSRVRData() {
+  test('Can read FQDN from SRV data', () {
+    expect('fletch.local', readFQDN(srvRData, _kSrvHeaderSize));
+  });
+}
+
+void testNonUtf8DomainName() {
+  test('Returns non-null for non-utf8 domain name', () {
+    final List<ResourceRecord> result = decodeMDnsResponse(nonUtf8Package)!;
+    expect(result, isNotNull);
+    expect(result[0] is TxtResourceRecord, isTrue);
+    final TxtResourceRecord txt = result[0] as TxtResourceRecord;
+    expect(txt.name, contains('�'));
+  });
+}
+
+// One address.
+const List<int> package1 = <int>[
+  0x00,
+  0x00,
+  0x84,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00,
+  0x00,
+  0x01,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x04,
+  0xc0,
+  0xa8,
+  0x01,
+  0xbf
+];
+
+// Two addresses.
+const List<int> package2 = <int>[
+  0x00,
+  0x00,
+  0x84,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x02,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00,
+  0x00,
+  0x01,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x04,
+  0xc0,
+  0xa8,
+  0x01,
+  0xbf,
+  0xc0,
+  0x0c,
+  0x00,
+  0x01,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x04,
+  0xa9,
+  0xfe,
+  0x5f,
+  0x53
+];
+
+// Eight mixed answers.
+const List<int> package3 = <int>[
+  0x00,
+  0x00,
+  0x84,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x08,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x1f,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0x20,
+  0x5b,
+  0x62,
+  0x38,
+  0x3a,
+  0x32,
+  0x37,
+  0x3a,
+  0x65,
+  0x62,
+  0x3a,
+  0x30,
+  0x33,
+  0x3a,
+  0x39,
+  0x32,
+  0x3a,
+  0x34,
+  0x62,
+  0x5d,
+  0x0c,
+  0x5f,
+  0x77,
+  0x6f,
+  0x72,
+  0x6b,
+  0x73,
+  0x74,
+  0x61,
+  0x74,
+  0x69,
+  0x6f,
+  0x6e,
+  0x04,
+  0x5f,
+  0x74,
+  0x63,
+  0x70,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00,
+  0x00,
+  0x10,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x01,
+  0x00,
+  0x0b,
+  0x5f,
+  0x75,
+  0x64,
+  0x69,
+  0x73,
+  0x6b,
+  0x73,
+  0x2d,
+  0x73,
+  0x73,
+  0x68,
+  0xc0,
+  0x39,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x0e,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x50,
+  0xc0,
+  0x68,
+  0x00,
+  0x21,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x14,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x16,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x3e,
+  0xc0,
+  0x68,
+  0x00,
+  0x10,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x01,
+  0x00,
+  0x09,
+  0x5f,
+  0x73,
+  0x65,
+  0x72,
+  0x76,
+  0x69,
+  0x63,
+  0x65,
+  0x73,
+  0x07,
+  0x5f,
+  0x64,
+  0x6e,
+  0x73,
+  0x2d,
+  0x73,
+  0x64,
+  0x04,
+  0x5f,
+  0x75,
+  0x64,
+  0x70,
+  0xc0,
+  0x3e,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x50,
+  0xc0,
+  0x2c,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x0c,
+  0xc0,
+  0x0c,
+  0x00,
+  0x21,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x08,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x09,
+  0xc0,
+  0x88,
+  0xc0,
+  0xa3,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x2c
+];
+
+const List<int> packagePtrResponse = <int>[
+  0x00,
+  0x00,
+  0x84,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x06,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x0d,
+  0x5f,
+  0x66,
+  0x6c,
+  0x65,
+  0x74,
+  0x63,
+  0x68,
+  0x5f,
+  0x61,
+  0x67,
+  0x65,
+  0x6e,
+  0x74,
+  0x04,
+  0x5f,
+  0x74,
+  0x63,
+  0x70,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x1e,
+  0x1b,
+  0x66,
+  0x6c,
+  0x65,
+  0x74,
+  0x63,
+  0x68,
+  0x2d,
+  0x61,
+  0x67,
+  0x65,
+  0x6e,
+  0x74,
+  0x20,
+  0x6f,
+  0x6e,
+  0x20,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x0c,
+  0xc0,
+  0x30,
+  0x00,
+  0x10,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x01,
+  0x00,
+  0xc0,
+  0x30,
+  0x00,
+  0x21,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x14,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x2f,
+  0x59,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x1f,
+  0xc0,
+  0x6d,
+  0x00,
+  0x1c,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x10,
+  0xfe,
+  0x80,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0xba,
+  0x27,
+  0xeb,
+  0xff,
+  0xfe,
+  0x69,
+  0x6e,
+  0x3a,
+  0xc0,
+  0x6d,
+  0x00,
+  0x01,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x04,
+  0xc0,
+  0xa8,
+  0x01,
+  0x01,
+  0xc0,
+  0x6d,
+  0x00,
+  0x01,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x04,
+  0xa9,
+  0xfe,
+  0xa7,
+  0xac
+];
+
+const List<int> ptrRData = <int>[
+  0x27,
+  0x73,
+  0x67,
+  0x6a,
+  0x65,
+  0x73,
+  0x73,
+  0x65,
+  0x2d,
+  0x6d,
+  0x61,
+  0x63,
+  0x62,
+  0x6f,
+  0x6f,
+  0x6b,
+  0x70,
+  0x72,
+  0x6f,
+  0x32,
+  0x20,
+  0x5b,
+  0x37,
+  0x38,
+  0x3a,
+  0x33,
+  0x31,
+  0x3a,
+  0x63,
+  0x31,
+  0x3a,
+  0x62,
+  0x38,
+  0x3a,
+  0x35,
+  0x35,
+  0x3a,
+  0x33,
+  0x38,
+  0x5d,
+  0x0c,
+  0x5f,
+  0x77,
+  0x6f,
+  0x72,
+  0x6b,
+  0x73,
+  0x74,
+  0x61,
+  0x74,
+  0x69,
+  0x6f,
+  0x6e,
+  0x04,
+  0x5f,
+  0x74,
+  0x63,
+  0x70,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00
+];
+
+const List<int> ptrRData2 = <int>[
+  0x0c,
+  0x66,
+  0x6c,
+  0x65,
+  0x74,
+  0x63,
+  0x68,
+  0x2d,
+  0x61,
+  0x67,
+  0x65,
+  0x6e,
+  0x74,
+  0x0d,
+  0x5f,
+  0x66,
+  0x6c,
+  0x65,
+  0x74,
+  0x63,
+  0x68,
+  0x5f,
+  0x61,
+  0x67,
+  0x65,
+  0x6e,
+  0x74,
+  0x04,
+  0x5f,
+  0x74,
+  0x63,
+  0x70,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00
+];
+
+const List<int> srvRData = <int>[
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x2f,
+  0x59,
+  0x06,
+  0x66,
+  0x6c,
+  0x65,
+  0x74,
+  0x63,
+  0x68,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00
+];
+
+const List<int> packetWithQuestionAnArCount = <int>[
+  0,
+  0,
+  2,
+  0,
+  0,
+  1,
+  0,
+  1,
+  0,
+  0,
+  0,
+  1,
+  15,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  4,
+  95,
+  95,
+  95,
+  95,
+  5,
+  95,
+  95,
+  95,
+  95,
+  95,
+  0,
+  0,
+  12,
+  0,
+  1,
+  192,
+  12,
+  0,
+  12,
+  0,
+  1,
+  0,
+  0,
+  14,
+  13,
+  0,
+  26,
+  23,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  12,
+  192,
+  12,
+  0,
+  12,
+  0,
+  1,
+  0,
+  0,
+  14,
+  13,
+  0,
+  31,
+  28,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  12,
+];
+
+const List<int> packetWithoutQuestionWithAnArCount = <int>[
+  0,
+  0,
+  132,
+  0,
+  0,
+  0,
+  0,
+  1,
+  0,
+  0,
+  0,
+  1,
+  15,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  4,
+  95,
+  95,
+  95,
+  95,
+  5,
+  95,
+  95,
+  95,
+  95,
+  95,
+  0,
+  0,
+  12,
+  0,
+  1,
+  0,
+  0,
+  17,
+  148,
+  0,
+  25,
+  22,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  12,
+  22,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  12,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  28,
+  0,
+  16,
+  0,
+  1,
+  0,
+  0,
+  17,
+  148,
+  0,
+  51,
+  20,
+  109,
+  111,
+  100,
+  101,
+  108,
+  61,
+  77,
+  97,
+  99,
+  66,
+  111,
+  111,
+  107,
+  80,
+  114,
+  111,
+  49,
+  52,
+  44,
+  51,
+  10,
+  111,
+  115,
+  120,
+  118,
+  101,
+  114,
+  115,
+  61,
+  49,
+  56,
+  18,
+  101,
+  99,
+  111,
+  108,
+  111,
+  114,
+  61,
+  50,
+  50,
+  53,
+  44,
+  50,
+  50,
+  53,
+  44,
+  50,
+  50,
+  51,
+];
+
+// This is the same as packetWithoutQuestionWithAnArCount, but the text
+// resource just has a single long string. If the length isn't decoded
+// separately from the string, there will be utf8 decoding failures.
+const List<int> packetWithLongTxt = <int>[
+  0,
+  0,
+  132,
+  0,
+  0,
+  0,
+  0,
+  1,
+  0,
+  0,
+  0,
+  1,
+  15,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  4,
+  95,
+  95,
+  95,
+  95,
+  5,
+  95,
+  95,
+  95,
+  95,
+  95,
+  0,
+  0,
+  12,
+  0,
+  1,
+  0,
+  0,
+  17,
+  148,
+  0,
+  25,
+  22,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  12,
+  22,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  12,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  95,
+  192,
+  28,
+  0,
+  16,
+  0,
+  1,
+  0,
+  0,
+  17,
+  148,
+  0,
+  51,
+  // Long string starts here.
+  129,
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 16
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 32
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 64
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, //
+  41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, // 128,
+  41, // 129
+];
+
+// Package with a domain name that is not valid utf-8.
+const List<int> nonUtf8Package = <int>[
+  0x00,
+  0x00,
+  0x84,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x08,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x1f,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0x20,
+  0x5b,
+  0x62,
+  0x38,
+  0x3a,
+  0x32,
+  0x37,
+  0x3a,
+  0x65,
+  0x62,
+  0xd2,
+  0x30,
+  0x33,
+  0x3a,
+  0x39,
+  0x32,
+  0x3a,
+  0x34,
+  0x62,
+  0x5d,
+  0x0c,
+  0x5f,
+  0x77,
+  0x6f,
+  0x72,
+  0x6b,
+  0x73,
+  0x74,
+  0x61,
+  0x74,
+  0x69,
+  0x6f,
+  0x6e,
+  0x04,
+  0x5f,
+  0x74,
+  0x63,
+  0x70,
+  0x05,
+  0x6c,
+  0x6f,
+  0x63,
+  0x61,
+  0x6c,
+  0x00,
+  0x00,
+  0x10,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x01,
+  0x00,
+  0x0b,
+  0x5f,
+  0x75,
+  0x64,
+  0x69,
+  0x73,
+  0x6b,
+  0x73,
+  0x2d,
+  0x73,
+  0x73,
+  0x68,
+  0xc0,
+  0x39,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x0e,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x50,
+  0xc0,
+  0x68,
+  0x00,
+  0x21,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x14,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x16,
+  0x0b,
+  0x72,
+  0x61,
+  0x73,
+  0x70,
+  0x62,
+  0x65,
+  0x72,
+  0x72,
+  0x79,
+  0x70,
+  0x69,
+  0xc0,
+  0x3e,
+  0xc0,
+  0x68,
+  0x00,
+  0x10,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x01,
+  0x00,
+  0x09,
+  0x5f,
+  0x73,
+  0x65,
+  0x72,
+  0x76,
+  0x69,
+  0x63,
+  0x65,
+  0x73,
+  0x07,
+  0x5f,
+  0x64,
+  0x6e,
+  0x73,
+  0x2d,
+  0x73,
+  0x64,
+  0x04,
+  0x5f,
+  0x75,
+  0x64,
+  0x70,
+  0xc0,
+  0x3e,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x50,
+  0xc0,
+  0x2c,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x0c,
+  0xc0,
+  0x0c,
+  0x00,
+  0x21,
+  0x80,
+  0x01,
+  0x00,
+  0x00,
+  0x00,
+  0x78,
+  0x00,
+  0x08,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x00,
+  0x09,
+  0xc0,
+  0x88,
+  0xc0,
+  0xa3,
+  0x00,
+  0x0c,
+  0x00,
+  0x01,
+  0x00,
+  0x00,
+  0x11,
+  0x94,
+  0x00,
+  0x02,
+  0xc0,
+  0x2c
+];
diff --git a/packages/multicast_dns/test/lookup_resolver_test.dart b/packages/multicast_dns/test/lookup_resolver_test.dart
new file mode 100644
index 0000000..3423a68
--- /dev/null
+++ b/packages/multicast_dns/test/lookup_resolver_test.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2015, the Dartino 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:io';
+
+import 'package:test/test.dart';
+import 'package:multicast_dns/src/lookup_resolver.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+
+void main() {
+  testTimeout();
+  testResult();
+  testResult2();
+  testResult3();
+}
+
+ResourceRecord ip4Result(String name, InternetAddress address) {
+  final int validUntil = DateTime.now().millisecondsSinceEpoch + 2000;
+  return IPAddressResourceRecord(name, validUntil, address: address);
+}
+
+void testTimeout() {
+  test('Resolver does not return with short timeout', () async {
+    const Duration shortTimeout = Duration(milliseconds: 1);
+    final LookupResolver resolver = LookupResolver();
+    final Stream<ResourceRecord> result = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'xxx', shortTimeout);
+    expect(await result.isEmpty, isTrue);
+  });
+}
+
+// One pending request and one response.
+void testResult() {
+  test('One pending request and one response', () async {
+    const Duration noTimeout = Duration(days: 1);
+    final LookupResolver resolver = LookupResolver();
+    final Stream<ResourceRecord> futureResult = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+    final ResourceRecord response =
+        ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+    resolver.handleResponse(<ResourceRecord>[response]);
+    final IPAddressResourceRecord result =
+        await futureResult.first as IPAddressResourceRecord;
+    expect('1.2.3.4', result.address.address);
+    resolver.clearPendingRequests();
+  });
+}
+
+void testResult2() {
+  test('Two requests', () async {
+    const Duration noTimeout = Duration(days: 1);
+    final LookupResolver resolver = LookupResolver();
+    final Stream<ResourceRecord> futureResult1 = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+    final Stream<ResourceRecord> futureResult2 = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'yyy.local', noTimeout);
+    final ResourceRecord response1 =
+        ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+    final ResourceRecord response2 =
+        ip4Result('yyy.local', InternetAddress('2.3.4.5'));
+    resolver.handleResponse(<ResourceRecord>[response2, response1]);
+    final IPAddressResourceRecord result1 =
+        await futureResult1.first as IPAddressResourceRecord;
+    final IPAddressResourceRecord result2 =
+        await futureResult2.first as IPAddressResourceRecord;
+    expect('1.2.3.4', result1.address.address);
+    expect('2.3.4.5', result2.address.address);
+    resolver.clearPendingRequests();
+  });
+}
+
+void testResult3() {
+  test('Multiple requests', () async {
+    const Duration noTimeout = Duration(days: 1);
+    final LookupResolver resolver = LookupResolver();
+    final ResourceRecord response0 =
+        ip4Result('zzz.local', InternetAddress('2.3.4.5'));
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    final Stream<ResourceRecord> futureResult1 = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'xxx.local', noTimeout);
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    final Stream<ResourceRecord> futureResult2 = resolver.addPendingRequest(
+        ResourceRecordType.addressIPv4, 'yyy.local', noTimeout);
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    final ResourceRecord response1 =
+        ip4Result('xxx.local', InternetAddress('1.2.3.4'));
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    final ResourceRecord response2 =
+        ip4Result('yyy.local', InternetAddress('2.3.4.5'));
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    resolver.handleResponse(<ResourceRecord>[response2, response1]);
+    resolver.handleResponse(<ResourceRecord>[response0]);
+    final IPAddressResourceRecord result1 =
+        await futureResult1.first as IPAddressResourceRecord;
+    final IPAddressResourceRecord result2 =
+        await futureResult2.first as IPAddressResourceRecord;
+    expect('1.2.3.4', result1.address.address);
+    expect('2.3.4.5', result2.address.address);
+    resolver.clearPendingRequests();
+  });
+}
diff --git a/packages/multicast_dns/test/resource_record_cache_test.dart b/packages/multicast_dns/test/resource_record_cache_test.dart
new file mode 100644
index 0000000..1b9130f
--- /dev/null
+++ b/packages/multicast_dns/test/resource_record_cache_test.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Test that the resource record cache works correctly.  In particular, make
+// sure that it removes all entries for a name before insertingrecords
+// of that name.
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:multicast_dns/src/resource_record.dart';
+import 'package:multicast_dns/src/native_protocol_client.dart'
+    show ResourceRecordCache;
+
+void main() {
+  testOverwrite();
+  testTimeout();
+}
+
+void testOverwrite() {
+  test('Cache can overwrite entries', () {
+    final InternetAddress ip1 = InternetAddress('192.168.1.1');
+    final InternetAddress ip2 = InternetAddress('192.168.1.2');
+    final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000;
+
+    final ResourceRecordCache cache = ResourceRecordCache();
+
+    // Add two different records.
+    cache.updateRecords(<ResourceRecord>[
+      IPAddressResourceRecord('hest', valid, address: ip1),
+      IPAddressResourceRecord('fisk', valid, address: ip2)
+    ]);
+    expect(cache.entryCount, 2);
+
+    // Update these records.
+    cache.updateRecords(<ResourceRecord>[
+      IPAddressResourceRecord('hest', valid, address: ip1),
+      IPAddressResourceRecord('fisk', valid, address: ip2)
+    ]);
+    expect(cache.entryCount, 2);
+
+    // Add two records with the same name (should remove the old one
+    // with that name only.)
+    cache.updateRecords(<ResourceRecord>[
+      IPAddressResourceRecord('hest', valid, address: ip1),
+      IPAddressResourceRecord('hest', valid, address: ip2)
+    ]);
+    expect(cache.entryCount, 3);
+
+    // Overwrite the two cached entries with one with the same name.
+    cache.updateRecords(<ResourceRecord>[
+      IPAddressResourceRecord('hest', valid, address: ip1),
+    ]);
+    expect(cache.entryCount, 2);
+  });
+}
+
+void testTimeout() {
+  test('Cache can evict records after timeout', () {
+    final InternetAddress ip1 = InternetAddress('192.168.1.1');
+    final int valid = DateTime.now().millisecondsSinceEpoch + 86400 * 1000;
+    final int notValid = DateTime.now().millisecondsSinceEpoch - 1;
+
+    final ResourceRecordCache cache = ResourceRecordCache();
+
+    cache.updateRecords(
+        <ResourceRecord>[IPAddressResourceRecord('hest', valid, address: ip1)]);
+    expect(cache.entryCount, 1);
+
+    cache.updateRecords(<ResourceRecord>[
+      IPAddressResourceRecord('fisk', notValid, address: ip1)
+    ]);
+
+    List<ResourceRecord> results = <ResourceRecord>[];
+    cache.lookup('hest', ResourceRecordType.addressIPv4, results);
+    expect(results.isEmpty, isFalse);
+
+    results = <ResourceRecord>[];
+    cache.lookup('fisk', ResourceRecordType.addressIPv4, results);
+    expect(results.isEmpty, isTrue);
+    expect(cache.entryCount, 1);
+  });
+}
diff --git a/packages/multicast_dns/tool/packet_gen.dart b/packages/multicast_dns/tool/packet_gen.dart
new file mode 100644
index 0000000..36486be
--- /dev/null
+++ b/packages/multicast_dns/tool/packet_gen.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2015, the Dartino 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.
+
+// Support code to generate the hex-lists in test/decode_test.dart from
+// a hex-stream.
+import 'dart:io';
+
+void formatHexStream(String hexStream) {
+  String s = '';
+  for (int i = 0; i < hexStream.length / 2; i++) {
+    if (s.isNotEmpty) {
+      s += ', ';
+    }
+    s += '0x';
+    final String x = hexStream.substring(i * 2, i * 2 + 2);
+    s += x;
+    if (((i + 1) % 8) == 0) {
+      s += ',';
+      print(s);
+      s = '';
+    }
+  }
+  if (s.isNotEmpty) {
+    print(s);
+  }
+}
+
+// Support code for generating the hex-lists in test/decode_test.dart.
+void hexDumpList(List<int> package) {
+  String s = '';
+  for (int i = 0; i < package.length; i++) {
+    if (s.isNotEmpty) {
+      s += ', ';
+    }
+    s += '0x';
+    final String x = package[i].toRadixString(16);
+    if (x.length == 1) {
+      s += '0';
+    }
+    s += x;
+    if (((i + 1) % 8) == 0) {
+      s += ',';
+      print(s);
+      s = '';
+    }
+  }
+  if (s.isNotEmpty) {
+    print(s);
+  }
+}
+
+void dumpDatagram(Datagram datagram) {
+  String _toHex(List<int> ints) {
+    final StringBuffer buffer = StringBuffer();
+    for (int i = 0; i < ints.length; i++) {
+      buffer.write(ints[i].toRadixString(16).padLeft(2, '0'));
+      if ((i + 1) % 10 == 0) {
+        buffer.writeln();
+      } else {
+        buffer.write(' ');
+      }
+    }
+    return buffer.toString();
+  }
+
+  print('${datagram.address.address}:${datagram.port}:');
+  print(_toHex(datagram.data));
+  print('');
+}
diff --git a/packages/palette_generator/CHANGELOG.md b/packages/palette_generator/CHANGELOG.md
new file mode 100644
index 0000000..786dcc7
--- /dev/null
+++ b/packages/palette_generator/CHANGELOG.md
@@ -0,0 +1,35 @@
+## 0.3.0
+
+* Migrated to null safety.
+
+## 0.2.4+1
+
+* Removed a `dart:async` import that isn't required for \>=Dart 2.1.
+
+## 0.2.4
+
+* Fix PaletteGenerator.fromImageProvider region not scaled to fit image size.
+
+## 0.2.3
+
+* Bumped minimum Flutter version to 1.15.21 to pick up Diagnosticable as a mixin and remove DiagnosticableMixin.
+
+## 0.2.2
+
+* Bumped minimum Flutter version to 1.6.7 to pick up DiagnosticableMixin.
+
+## 0.2.1
+
+* PaletteGenerator: performance improvements.
+
+## 0.2.0
+
+* Updated code to be compatible with Flutter breaking change.
+
+## 0.1.1
+
+* Fixed a problem with a listener that wasn't being unregistered properly.
+
+## 0.1.0
+
+* Initial Open Source release.
diff --git a/packages/palette_generator/LICENSE b/packages/palette_generator/LICENSE
new file mode 100644
index 0000000..8211a02
--- /dev/null
+++ b/packages/palette_generator/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/palette_generator/README.md b/packages/palette_generator/README.md
new file mode 100644
index 0000000..3d37881
--- /dev/null
+++ b/packages/palette_generator/README.md
@@ -0,0 +1,22 @@
+# Palette Generator package
+
+[![pub package](https://img.shields.io/pub/v/palette_generator.svg)](https://pub.dartlang.org/packages/palette_generator)
+
+A Flutter package to extract prominent colors from an Image, typically used to
+find colors for a user interface.
+
+## Usage
+
+To use this package, add `palette_generator` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+
+```dart
+import 'package:palette_generator/palette_generator.dart';
+```
+
+Then use the `PaletteGenerator` Dart class in your code. To see how this is done,
+check out the [image_colors example app](example/).
diff --git a/packages/palette_generator/example/.gitignore b/packages/palette_generator/example/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/palette_generator/example/.gitignore
@@ -0,0 +1,37 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/palette_generator/example/.metadata b/packages/palette_generator/example/.metadata
new file mode 100644
index 0000000..96bf554
--- /dev/null
+++ b/packages/palette_generator/example/.metadata
@@ -0,0 +1,8 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: bb23a110e0e4c2b378cec38fcd5d9b7bef10ec63
+  channel: unknown
diff --git a/packages/palette_generator/example/README.md b/packages/palette_generator/example/README.md
new file mode 100644
index 0000000..cfba2a8
--- /dev/null
+++ b/packages/palette_generator/example/README.md
@@ -0,0 +1,6 @@
+# image_colors
+
+A sample app for demonstrating the PaletteGenerator
+
+This app will show you what kinds of palettes the generator creates, and one
+way to create them from existing image providers.
\ No newline at end of file
diff --git a/packages/palette_generator/example/android/.gitignore b/packages/palette_generator/example/android/.gitignore
new file mode 100644
index 0000000..65b7315
--- /dev/null
+++ b/packages/palette_generator/example/android/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+*.class
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+GeneratedPluginRegistrant.java
diff --git a/packages/palette_generator/example/android/app/build.gradle b/packages/palette_generator/example/android/app/build.gradle
new file mode 100644
index 0000000..640a8a2
--- /dev/null
+++ b/packages/palette_generator/example/android/app/build.gradle
@@ -0,0 +1,56 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.")
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "io.flutter.packages.palettegenerator.imagecolors"
+        minSdkVersion 16
+        targetSdkVersion 27
+        versionCode 1
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.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 {
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test:runner:1.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
+}
diff --git a/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..7b31ebf
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.packages.palettegenerator.imagecolors">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a3cd38c
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.packages.palettegenerator.imagecolors">
+
+    <!-- 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="image_colors"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- This keeps the window background of the activity showing
+                 until Flutter renders its first frame. It can be removed if
+                 there is no splash screen (such as the default splash screen
+                 defined in @style/LaunchTheme). -->
+            <meta-data
+                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
+                android:value="true" />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java b/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java
new file mode 100644
index 0000000..2ce2e49
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/java/io/flutter/packages/palettegenerator/imagecolors/MainActivity.java
@@ -0,0 +1,13 @@
+package io.flutter.packages.palettegenerator.imagecolors;
+
+import android.os.Bundle;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class MainActivity extends FlutterActivity {
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    GeneratedPluginRegistrant.registerWith(this);
+  }
+}
diff --git a/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt b/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt
new file mode 100644
index 0000000..e121e6c
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/kotlin/io/flutter/packages/palettegenerator/example/MainActivity.kt
@@ -0,0 +1,12 @@
+package io.flutter.packages.palettegenerator.imagecolors
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+        GeneratedPluginRegistrant.registerWith(flutterEngine);
+    }
+}
diff --git a/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml b/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/palette_generator/example/android/app/src/main/res/values/styles.xml b/packages/palette_generator/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+</resources>
diff --git a/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml b/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..7b31ebf
--- /dev/null
+++ b/packages/palette_generator/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.packages.palettegenerator.imagecolors">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/palette_generator/example/android/build.gradle b/packages/palette_generator/example/android/build.gradle
new file mode 100644
index 0000000..e0d7ae2
--- /dev/null
+++ b/packages/palette_generator/example/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+    }
+}
+
+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/packages/palette_generator/example/android/gradle.properties b/packages/palette_generator/example/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..380a29b
--- /dev/null
+++ b/packages/palette_generator/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 07 08:46:39 PST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/packages/palette_generator/example/android/gradlew b/packages/palette_generator/example/android/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/packages/palette_generator/example/android/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/packages/palette_generator/example/android/gradlew.bat b/packages/palette_generator/example/android/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/packages/palette_generator/example/android/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/palette_generator/example/android/settings.gradle b/packages/palette_generator/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/palette_generator/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/palette_generator/example/assets/landscape.png b/packages/palette_generator/example/assets/landscape.png
new file mode 100644
index 0000000..815f599
--- /dev/null
+++ b/packages/palette_generator/example/assets/landscape.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/.gitignore b/packages/palette_generator/example/ios/.gitignore
new file mode 100644
index 0000000..79cc4da
--- /dev/null
+++ b/packages/palette_generator/example/ios/.gitignore
@@ -0,0 +1,45 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+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/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist b/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..9367d48
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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/packages/palette_generator/example/ios/Flutter/Debug.xcconfig b/packages/palette_generator/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/palette_generator/example/ios/Flutter/Release.xcconfig b/packages/palette_generator/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh b/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh
new file mode 100755
index 0000000..d0df73c
--- /dev/null
+++ b/packages/palette_generator/example/ios/Flutter/flutter_export_environment.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# This is a generated file; do not edit or check into version control.
+export "FLUTTER_ROOT=/Users/gspencer/code/flutter"
+export "FLUTTER_APPLICATION_PATH=/Users/gspencer/code/packages/packages/palette_generator/example"
+export "FLUTTER_TARGET=lib/main.dart"
+export "FLUTTER_BUILD_DIR=build"
+export "SYMROOT=${SOURCE_ROOT}/../build/ios"
+export "FLUTTER_FRAMEWORK_DIR=/Users/gspencer/code/flutter/bin/cache/artifacts/engine/ios"
+export "FLUTTER_BUILD_NAME=0.1.0"
+export "FLUTTER_BUILD_NUMBER=0.1.0"
diff --git a/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj b/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d78d489
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,438 @@
+// !$*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 = "$(FLUTTER_BUILD_NUMBER)";
+				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 = io.flutter.packages.palette_generator.imageColors;
+				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 = "$(FLUTTER_BUILD_NUMBER)";
+				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 = io.flutter.packages.palette_generator.imageColors;
+				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/packages/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..1263ac8
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,93 @@
+<?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/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/ios/Runner/AppDelegate.h b/packages/palette_generator/example/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/palette_generator/example/ios/Runner/AppDelegate.m b/packages/palette_generator/example/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..59a72e9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#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/packages/palette_generator/example/ios/Runner/AppDelegate.swift b/packages/palette_generator/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "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/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..3d43d11
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "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/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard b/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/palette_generator/example/ios/Runner/Info.plist b/packages/palette_generator/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..cdf4f69
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>image_colors</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h b/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/palette_generator/example/ios/Runner/main.m b/packages/palette_generator/example/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/palette_generator/example/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#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/packages/palette_generator/example/lib/main.dart b/packages/palette_generator/example/lib/main.dart
new file mode 100644
index 0000000..9e01739
--- /dev/null
+++ b/packages/palette_generator/example/lib/main.dart
@@ -0,0 +1,316 @@
+// 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:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+import 'package:palette_generator/palette_generator.dart';
+
+void main() => runApp(const MyApp());
+
+const Color _kBackgroundColor = Color(0xffa0a0a0);
+const Color _kSelectionRectangleBackground = Color(0x15000000);
+const Color _kSelectionRectangleBorder = Color(0x80000000);
+const Color _kPlaceholderColor = Color(0x80404040);
+
+/// The main Application class.
+class MyApp extends StatelessWidget {
+  /// Creates the main Application class.
+  const MyApp({Key? key}) : super(key: key);
+
+  // This widget is the root of your application.
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Image Colors',
+      theme: ThemeData(
+        primarySwatch: Colors.green,
+      ),
+      home: const ImageColors(
+        title: 'Image Colors',
+        image: AssetImage('assets/landscape.png'),
+        imageSize: Size(256.0, 170.0),
+      ),
+    );
+  }
+}
+
+/// The home page for this example app.
+@immutable
+class ImageColors extends StatefulWidget {
+  /// Creates the home page.
+  const ImageColors({
+    Key? key,
+    this.title,
+    required this.image,
+    this.imageSize,
+  }) : super(key: key);
+
+  /// The title that is shown at the top of the page.
+  final String? title;
+
+  /// This is the image provider that is used to load the colors from.
+  final ImageProvider image;
+
+  /// The dimensions of the image.
+  final Size? imageSize;
+
+  @override
+  _ImageColorsState createState() {
+    return _ImageColorsState();
+  }
+}
+
+class _ImageColorsState extends State<ImageColors> {
+  Rect? region;
+  Rect? dragRegion;
+  Offset? startDrag;
+  Offset? currentDrag;
+  PaletteGenerator? paletteGenerator;
+
+  final GlobalKey imageKey = GlobalKey();
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.imageSize != null) {
+      region = Offset.zero & widget.imageSize!;
+    }
+    _updatePaletteGenerator(region);
+  }
+
+  Future<void> _updatePaletteGenerator(Rect? newRegion) async {
+    paletteGenerator = await PaletteGenerator.fromImageProvider(
+      widget.image,
+      size: widget.imageSize,
+      region: newRegion,
+      maximumColorCount: 20,
+    );
+    setState(() {});
+  }
+
+  // Called when the user starts to drag
+  void _onPanDown(DragDownDetails details) {
+    final RenderBox box =
+        imageKey.currentContext!.findRenderObject()! as RenderBox;
+    final Offset localPosition = box.globalToLocal(details.globalPosition);
+    setState(() {
+      startDrag = localPosition;
+      currentDrag = localPosition;
+      dragRegion = Rect.fromPoints(localPosition, localPosition);
+    });
+  }
+
+  // Called as the user drags: just updates the region, not the colors.
+  void _onPanUpdate(DragUpdateDetails details) {
+    setState(() {
+      currentDrag = currentDrag! + details.delta;
+      dragRegion = Rect.fromPoints(startDrag!, currentDrag!);
+    });
+  }
+
+  // Called if the drag is canceled (e.g. by rotating the device or switching
+  // apps)
+  void _onPanCancel() {
+    setState(() {
+      dragRegion = null;
+      startDrag = null;
+    });
+  }
+
+  // Called when the drag ends. Sets the region, and updates the colors.
+  Future<void> _onPanEnd(DragEndDetails details) async {
+    final Size? imageSize = imageKey.currentContext?.size;
+    Rect? newRegion;
+
+    if (imageSize != null) {
+      newRegion = (Offset.zero & imageSize).intersect(dragRegion!);
+      if (newRegion.size.width < 4 && newRegion.size.width < 4) {
+        newRegion = Offset.zero & imageSize;
+      }
+    }
+
+    await _updatePaletteGenerator(newRegion);
+    setState(() {
+      region = newRegion;
+      dragRegion = null;
+      startDrag = null;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: _kBackgroundColor,
+      appBar: AppBar(
+        title: Text(widget.title ?? ''),
+      ),
+      body: Column(
+        mainAxisSize: MainAxisSize.max,
+        mainAxisAlignment: MainAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: <Widget>[
+          Padding(
+            padding: const EdgeInsets.all(20.0),
+            // GestureDetector is used to handle the selection rectangle.
+            child: GestureDetector(
+              onPanDown: _onPanDown,
+              onPanUpdate: _onPanUpdate,
+              onPanCancel: _onPanCancel,
+              onPanEnd: _onPanEnd,
+              child: Stack(children: <Widget>[
+                Image(
+                  key: imageKey,
+                  image: widget.image,
+                  width: widget.imageSize?.width,
+                  height: widget.imageSize?.height,
+                ),
+                // This is the selection rectangle.
+                Positioned.fromRect(
+                    rect: dragRegion ?? region ?? Rect.zero,
+                    child: Container(
+                      decoration: BoxDecoration(
+                          color: _kSelectionRectangleBackground,
+                          border: Border.all(
+                            width: 1.0,
+                            color: _kSelectionRectangleBorder,
+                            style: BorderStyle.solid,
+                          )),
+                    )),
+              ]),
+            ),
+          ),
+          // Use a FutureBuilder so that the palettes will be displayed when
+          // the palette generator is done generating its data.
+          PaletteSwatches(generator: paletteGenerator),
+        ],
+      ),
+    );
+  }
+}
+
+/// A widget that draws the swatches for the [PaletteGenerator] it is given,
+/// and shows the selected target colors.
+class PaletteSwatches extends StatelessWidget {
+  /// Create a Palette swatch.
+  ///
+  /// The [generator] is optional. If it is null, then the display will
+  /// just be an empty container.
+  const PaletteSwatches({Key? key, this.generator}) : super(key: key);
+
+  /// The [PaletteGenerator] that contains all of the swatches that we're going
+  /// to display.
+  final PaletteGenerator? generator;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> swatches = <Widget>[];
+    final PaletteGenerator? paletteGen = generator;
+    if (paletteGen == null || paletteGen.colors.isEmpty) {
+      return Container();
+    }
+    for (final Color color in paletteGen.colors) {
+      swatches.add(PaletteSwatch(color: color));
+    }
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      mainAxisSize: MainAxisSize.min,
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: <Widget>[
+        Wrap(
+          children: swatches,
+        ),
+        Container(height: 30.0),
+        PaletteSwatch(
+            label: 'Dominant', color: paletteGen.dominantColor?.color),
+        PaletteSwatch(
+            label: 'Light Vibrant', color: paletteGen.lightVibrantColor?.color),
+        PaletteSwatch(label: 'Vibrant', color: paletteGen.vibrantColor?.color),
+        PaletteSwatch(
+            label: 'Dark Vibrant', color: paletteGen.darkVibrantColor?.color),
+        PaletteSwatch(
+            label: 'Light Muted', color: paletteGen.lightMutedColor?.color),
+        PaletteSwatch(label: 'Muted', color: paletteGen.mutedColor?.color),
+        PaletteSwatch(
+            label: 'Dark Muted', color: paletteGen.darkMutedColor?.color),
+      ],
+    );
+  }
+}
+
+/// A small square of color with an optional label.
+@immutable
+class PaletteSwatch extends StatelessWidget {
+  /// Creates a PaletteSwatch.
+  ///
+  /// If the [paletteColor] has property `isTargetColorFound` as `false`,
+  /// then the swatch will show a placeholder instead, to indicate
+  /// that there is no color.
+  const PaletteSwatch({
+    Key? key,
+    this.color,
+    this.label,
+  }) : super(key: key);
+
+  /// The color of the swatch.
+  final Color? color;
+
+  /// The optional label to display next to the swatch.
+  final String? label;
+
+  @override
+  Widget build(BuildContext context) {
+    // Compute the "distance" of the color swatch and the background color
+    // so that we can put a border around those color swatches that are too
+    // close to the background's saturation and lightness. We ignore hue for
+    // the comparison.
+    final HSLColor hslColor = HSLColor.fromColor(color ?? Colors.transparent);
+    final HSLColor backgroundAsHsl = HSLColor.fromColor(_kBackgroundColor);
+    final double colorDistance = math.sqrt(
+        math.pow(hslColor.saturation - backgroundAsHsl.saturation, 2.0) +
+            math.pow(hslColor.lightness - backgroundAsHsl.lightness, 2.0));
+
+    Widget swatch = Padding(
+      padding: const EdgeInsets.all(2.0),
+      child: color == null
+          ? const Placeholder(
+              fallbackWidth: 34.0,
+              fallbackHeight: 20.0,
+              color: Color(0xff404040),
+              strokeWidth: 2.0,
+            )
+          : Container(
+              decoration: BoxDecoration(
+                  color: color,
+                  border: Border.all(
+                    width: 1.0,
+                    color: _kPlaceholderColor,
+                    style: colorDistance < 0.2
+                        ? BorderStyle.solid
+                        : BorderStyle.none,
+                  )),
+              width: 34.0,
+              height: 20.0,
+            ),
+    );
+
+    if (label != null) {
+      swatch = ConstrainedBox(
+        constraints: const BoxConstraints(maxWidth: 130.0, minWidth: 130.0),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.start,
+          children: <Widget>[
+            swatch,
+            Container(width: 5.0),
+            Text(label!),
+          ],
+        ),
+      );
+    }
+    return swatch;
+  }
+}
diff --git a/packages/palette_generator/example/macos/.gitignore b/packages/palette_generator/example/macos/.gitignore
new file mode 100644
index 0000000..d2fd377
--- /dev/null
+++ b/packages/palette_generator/example/macos/.gitignore
@@ -0,0 +1,6 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/xcuserdata/
diff --git a/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig b/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000..c2efd0b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1 @@
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000..cccf817
--- /dev/null
+++ b/packages/palette_generator/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,10 @@
+//
+//  Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+}
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..331479d
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,596 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 51;
+	objects = {
+
+/* Begin PBXAggregateTarget section */
+		33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+			isa = PBXAggregateTarget;
+			buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+			buildPhases = (
+				33CC111E2044C6BF0003C045 /* ShellScript */,
+			);
+			dependencies = (
+			);
+			name = "Flutter Assemble";
+			productName = FLX;
+		};
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+		335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+		33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+		33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; };
+		33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; };
+		D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+			remoteInfo = FLX;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		33CC110E2044A8840003C045 /* Bundle Framework */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */,
+				33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */,
+			);
+			name = "Bundle Framework";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
+		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
+		33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
+		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
+		33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
+		33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
+		33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
+		33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
+		33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
+		33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; };
+		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
+		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
+		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
+		D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		33CC10EA2044A3C60003C045 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				D73912F022F37F9E000D13A0 /* App.framework in Frameworks */,
+				33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		33BA886A226E78AF003329D5 /* Configs */ = {
+			isa = PBXGroup;
+			children = (
+				33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+			);
+			path = Configs;
+			sourceTree = "<group>";
+		};
+		33CC10E42044A3C60003C045 = {
+			isa = PBXGroup;
+			children = (
+				33FAB671232836740065AC1E /* Runner */,
+				33CEB47122A05771004F2AC0 /* Flutter */,
+				33CC10EE2044A3C60003C045 /* Products */,
+				D73912EC22F37F3D000D13A0 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		33CC10EE2044A3C60003C045 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10ED2044A3C60003C045 /* example.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		33CC11242044D66E0003C045 /* Resources */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F22044A3C60003C045 /* Assets.xcassets */,
+				33CC10F42044A3C60003C045 /* MainMenu.xib */,
+				33CC10F72044A3C60003C045 /* Info.plist */,
+			);
+			name = Resources;
+			path = ..;
+			sourceTree = "<group>";
+		};
+		33CEB47122A05771004F2AC0 /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+				33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+				33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+				33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+				D73912EF22F37F9E000D13A0 /* App.framework */,
+				33D1A10322148B71006C7A3E /* FlutterMacOS.framework */,
+			);
+			path = Flutter;
+			sourceTree = "<group>";
+		};
+		33FAB671232836740065AC1E /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+				33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+				33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+				33E51914231749380026EE4D /* Release.entitlements */,
+				33CC11242044D66E0003C045 /* Resources */,
+				33BA886A226E78AF003329D5 /* Configs */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		33CC10EC2044A3C60003C045 /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				33CC10E92044A3C60003C045 /* Sources */,
+				33CC10EA2044A3C60003C045 /* Frameworks */,
+				33CC10EB2044A3C60003C045 /* Resources */,
+				33CC110E2044A8840003C045 /* Bundle Framework */,
+				3399D490228B24CF009A79C7 /* ShellScript */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				33CC11202044C79F0003C045 /* PBXTargetDependency */,
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		33CC10E52044A3C60003C045 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0920;
+				LastUpgradeCheck = 0930;
+				ORGANIZATIONNAME = "The Flutter Authors";
+				TargetAttributes = {
+					33CC10EC2044A3C60003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						LastSwiftMigration = 1100;
+						ProvisioningStyle = Automatic;
+						SystemCapabilities = {
+							com.apple.Sandbox = {
+								enabled = 1;
+							};
+						};
+					};
+					33CC111A2044C6BA0003C045 = {
+						CreatedOnToolsVersion = 9.2;
+						ProvisioningStyle = Manual;
+					};
+				};
+			};
+			buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 8.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 33CC10E42044A3C60003C045;
+			productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				33CC10EC2044A3C60003C045 /* Runner */,
+				33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		33CC10EB2044A3C60003C045 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+				33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3399D490228B24CF009A79C7 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+			);
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n";
+		};
+		33CC111E2044C6BF0003C045 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				Flutter/ephemeral/FlutterInputs.xcfilelist,
+			);
+			inputPaths = (
+				Flutter/ephemeral/tripwire,
+			);
+			outputFileListPaths = (
+				Flutter/ephemeral/FlutterOutputs.xcfilelist,
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		33CC10E92044A3C60003C045 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+				33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+				335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+			targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+			isa = PBXVariantGroup;
+			children = (
+				33CC10F52044A3C60003C045 /* Base */,
+			);
+			name = MainMenu.xib;
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		338D0CE9231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Profile;
+		};
+		338D0CEA231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter/ephemeral",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Profile;
+		};
+		338D0CEB231458BD00FA5F75 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Profile;
+		};
+		33CC10F92044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		33CC10FA2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				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_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				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_SUSPICIOUS_MOVE = YES;
+				CODE_SIGN_IDENTITY = "-";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = macosx;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+			};
+			name = Release;
+		};
+		33CC10FC2044A3C60003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter/ephemeral",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		33CC10FD2044A3C60003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter/ephemeral",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+				);
+				PROVISIONING_PROFILE_SPECIFIER = "";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+		33CC111C2044C6BA0003C045 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Manual;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		33CC111D2044C6BA0003C045 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10F92044A3C60003C045 /* Debug */,
+				33CC10FA2044A3C60003C045 /* Release */,
+				338D0CE9231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC10FC2044A3C60003C045 /* Debug */,
+				33CC10FD2044A3C60003C045 /* Release */,
+				338D0CEA231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				33CC111C2044C6BA0003C045 /* Debug */,
+				33CC111D2044C6BA0003C045 /* Release */,
+				338D0CEB231458BD00FA5F75 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..764c74b
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:/Users/stuartmorgan/src/embedder-opensource/flutter-desktop-embedding/example/macos/Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..df12c33
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1000"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+               BuildableName = "example.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "00380F9121DF178D00097171"
+               BuildableName = "RunnerUITests.xctest"
+               BlueprintName = "RunnerUITests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "example.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"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "33CC10EC2044A3C60003C045"
+            BuildableName = "example.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 = "33CC10EC2044A3C60003C045"
+            BuildableName = "example.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/AppDelegate.swift b/packages/palette_generator/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000..d53ef64
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+    return true
+  }
+}
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a2ec33f
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+  "images" : [
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_16.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "16x16",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_32.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "32x32",
+      "idiom" : "mac",
+      "filename" : "app_icon_64.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_128.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "128x128",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_256.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "256x256",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_512.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "512x512",
+      "idiom" : "mac",
+      "filename" : "app_icon_1024.png",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000..3c4935a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000..ed4cc16
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000..483be61
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000..bcbf36d
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000..9c0a652
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000..e71a726
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000..8a31fe2
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
Binary files differ
diff --git a/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000..537341a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,339 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+    <dependencies>
+        <deployment identifier="macosx"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+            <connections>
+                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
+            </connections>
+        </customObject>
+        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+        <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
+            <connections>
+                <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
+                <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
+            </connections>
+        </customObject>
+        <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
+        <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
+            <items>
+                <menuItem title="APP_NAME" id="1Xt-HY-uBw">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
+                        <items>
+                            <menuItem title="About APP_NAME" id="5kV-Vb-QxS">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
+                            <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
+                            <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
+                            <menuItem title="Services" id="NMo-om-nkz">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
+                            <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
+                                <connections>
+                                    <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Show All" id="Kd2-mp-pUS">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
+                            <menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
+                                <connections>
+                                    <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Edit" id="5QF-Oa-p0T">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Edit" id="W48-6f-4Dl">
+                        <items>
+                            <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
+                                <connections>
+                                    <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
+                                <connections>
+                                    <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
+                            <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
+                                <connections>
+                                    <action selector="cut:" target="-1" id="YJe-68-I9s"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
+                                <connections>
+                                    <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
+                                <connections>
+                                    <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
+                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                <connections>
+                                    <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Delete" id="pa3-QI-u2k">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
+                                <connections>
+                                    <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
+                            <menuItem title="Find" id="4EN-yA-p0u">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Find" id="1b7-l0-nxx">
+                                    <items>
+                                        <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
+                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
+                                            <connections>
+                                                <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
+                                            <connections>
+                                                <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
+                                    <items>
+                                        <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
+                                            <connections>
+                                                <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
+                                            <connections>
+                                                <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
+                                        <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Substitutions" id="9ic-FL-obx">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
+                                    <items>
+                                        <menuItem title="Show Substitutions" id="z6F-FW-3nz">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
+                                        <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Quotes" id="hQb-2v-fYv">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Dashes" id="rgM-f4-ycn">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Smart Links" id="cwL-P1-jid">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Data Detectors" id="tRr-pd-1PS">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Text Replacement" id="HFQ-gK-NFA">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Transformations" id="2oI-Rn-ZJC">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
+                                    <items>
+                                        <menuItem title="Make Upper Case" id="vmV-6d-7jI">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Make Lower Case" id="d9M-CD-aMd">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Capitalize" id="UEZ-Bs-lqG">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                            <menuItem title="Speech" id="xrE-MZ-jX0">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
+                                    <items>
+                                        <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
+                                            </connections>
+                                        </menuItem>
+                                        <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
+                                            <modifierMask key="keyEquivalentModifierMask"/>
+                                            <connections>
+                                                <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
+                                            </connections>
+                                        </menuItem>
+                                    </items>
+                                </menu>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="View" id="H8h-7b-M4v">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="View" id="HyV-fh-RgO">
+                        <items>
+                            <menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
+                                <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+                                <connections>
+                                    <action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+                <menuItem title="Window" id="aUF-d1-5bR">
+                    <modifierMask key="keyEquivalentModifierMask"/>
+                    <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
+                        <items>
+                            <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
+                                <connections>
+                                    <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem title="Zoom" id="R4o-n2-Eq4">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
+                                </connections>
+                            </menuItem>
+                            <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
+                            <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
+                                <modifierMask key="keyEquivalentModifierMask"/>
+                                <connections>
+                                    <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
+                                </connections>
+                            </menuItem>
+                        </items>
+                    </menu>
+                </menuItem>
+            </items>
+            <point key="canvasLocation" x="142" y="-258"/>
+        </menu>
+        <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
+            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
+            <rect key="contentRect" x="335" y="390" width="800" height="600"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
+            <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
+                <rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
+                <autoresizingMask key="autoresizingMask"/>
+            </view>
+        </window>
+    </objects>
+</document>
diff --git a/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000..380aa95
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = io.flutter.packages.palettegenerator.imagecolors
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2020 io.flutter.packages.palettegenerator. All rights reserved.
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000..36b0fd9
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000..dff4f49
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig b/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000..42bcbf4
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements b/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000..dddb8a3
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+<?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>com.apple.security.app-sandbox</key>
+	<true/>
+	<key>com.apple.security.cs.allow-jit</key>
+	<true/>
+	<key>com.apple.security.network.server</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/Info.plist b/packages/palette_generator/example/macos/Runner/Info.plist
new file mode 100644
index 0000000..4789daa
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIconFile</key>
+	<string></string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</string>
+	<key>LSMinimumSystemVersion</key>
+	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+	<key>NSHumanReadableCopyright</key>
+	<string>$(PRODUCT_COPYRIGHT)</string>
+	<key>NSMainNibFile</key>
+	<string>MainMenu</string>
+	<key>NSPrincipalClass</key>
+	<string>NSApplication</string>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift b/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000..2722837
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+  override func awakeFromNib() {
+    let flutterViewController = FlutterViewController.init()
+    let windowFrame = self.frame
+    self.contentViewController = flutterViewController
+    self.setFrame(windowFrame, display: true)
+
+    RegisterGeneratedPlugins(registry: flutterViewController)
+
+    super.awakeFromNib()
+  }
+}
diff --git a/packages/palette_generator/example/macos/Runner/Release.entitlements b/packages/palette_generator/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/packages/palette_generator/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+<?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>com.apple.security.app-sandbox</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/palette_generator/example/pubspec.yaml b/packages/palette_generator/example/pubspec.yaml
new file mode 100644
index 0000000..b2b0cbd
--- /dev/null
+++ b/packages/palette_generator/example/pubspec.yaml
@@ -0,0 +1,22 @@
+name: image_colors
+description: A simple example of how to use the PaletteGenerator to load the palette from an image file.
+publish_to: none
+version: 0.1.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  palette_generator:
+    path: ../
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
+  assets:
+    - assets/landscape.png
diff --git a/packages/palette_generator/lib/palette_generator.dart b/packages/palette_generator/lib/palette_generator.dart
new file mode 100644
index 0000000..ed721f5
--- /dev/null
+++ b/packages/palette_generator/lib/palette_generator.dart
@@ -0,0 +1,1274 @@
+// 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.
+
+// TODO(goderbauer): Fix this warning for the classes in this file.
+// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:math' as math;
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+import 'dart:ui' show Color;
+
+import 'package:collection/collection.dart'
+    show PriorityQueue, HeapPriorityQueue;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/painting.dart';
+
+/// A class to extract prominent colors from an image for use as user interface
+/// colors.
+///
+/// To create a new [PaletteGenerator], use the asynchronous
+/// [PaletteGenerator.fromImage] static function.
+///
+/// A number of color paletteColors with different profiles are chosen from the
+/// image:
+///
+///   * [vibrantColor]
+///   * [darkVibrantColor]
+///   * [lightVibrantColor]
+///   * [mutedColor]
+///   * [darkMutedColor]
+///   * [lightMutedColor]
+///
+/// You may add your own target palette color types by supplying them to the
+/// `targets` parameter for [PaletteGenerator.fromImage].
+///
+/// In addition, the population-sorted list of discovered [colors] is available,
+/// and a [paletteColors] list providing contrasting title text and body text
+/// colors for each palette color.
+///
+/// The palette is created using a color quantizer based on the Median-cut
+/// algorithm, but optimized for picking out distinct colors rather than
+/// representative colors.
+///
+/// The color space is represented as a 3-dimensional cube with each dimension
+/// being one component of an RGB image. The cube is then repeatedly divided
+/// until the color space is reduced to the requested number of colors. An
+/// average color is then generated from each cube.
+///
+/// What makes this different from a median-cut algorithm is that median-cut
+/// divides cubes so that all of the cubes have roughly the same population,
+/// where the quantizer that is used to create the palette divides cubes based
+/// on their color volume. This means that the color space is divided into
+/// distinct colors, rather than representative colors.
+///
+/// See also:
+///
+///   * [PaletteColor], to contain various pieces of metadata about a chosen
+///     palette color.
+///   * [PaletteTarget], to be able to create your own target color types.
+///   * [PaletteFilter], a function signature for filtering the allowed colors
+///     in the palette.
+class PaletteGenerator with Diagnosticable {
+  /// Create a [PaletteGenerator] from a set of paletteColors and targets.
+  ///
+  /// The usual way to create a [PaletteGenerator] is to use the asynchronous
+  /// [PaletteGenerator.fromImage] static function. This constructor is mainly
+  /// used for cases when you have your own source of color information and
+  /// would like to use the target selection and scoring methods here.
+  PaletteGenerator.fromColors(
+    this.paletteColors, {
+    this.targets = const <PaletteTarget>[],
+  }) : selectedSwatches = <PaletteTarget, PaletteColor>{} {
+    _sortSwatches();
+    _selectSwatches();
+  }
+
+  /// Create a [PaletteGenerator] from an [dart:ui.Image] asynchronously.
+  ///
+  /// The [region] specifies the part of the image to inspect for color
+  /// candidates. By default it uses the entire image. Must not be equal to
+  /// [Rect.zero], and must not be larger than the image dimensions.
+  ///
+  /// The [maximumColorCount] sets the maximum number of colors that will be
+  /// returned in the [PaletteGenerator]. The default is 16 colors.
+  ///
+  /// The [filters] specify a lost of [PaletteFilter] instances that can be used
+  /// to include certain colors in the list of colors. The default filter is
+  /// an instance of [AvoidRedBlackWhitePaletteFilter], which stays away from
+  /// whites, blacks, and low-saturation reds.
+  ///
+  /// The [targets] are a list of target color types, specified by creating
+  /// custom [PaletteTarget]s. By default, this is the list of targets in
+  /// [PaletteTarget.baseTargets].
+  static Future<PaletteGenerator> fromImage(
+    ui.Image image, {
+    Rect? region,
+    int maximumColorCount = _defaultCalculateNumberColors,
+    List<PaletteFilter> filters = const <PaletteFilter>[
+      avoidRedBlackWhitePaletteFilter
+    ],
+    List<PaletteTarget> targets = const <PaletteTarget>[],
+  }) async {
+    assert(region == null || region != Rect.zero);
+    assert(
+        region == null ||
+            (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
+        'Region $region is outside the image ${image.width}x${image.height}');
+    assert(
+        region == null ||
+            (region.bottomRight.dx <= image.width &&
+                region.bottomRight.dy <= image.height),
+        'Region $region is outside the image ${image.width}x${image.height}');
+
+    final _ColorCutQuantizer quantizer = _ColorCutQuantizer(
+      image,
+      maxColors: maximumColorCount,
+      filters: filters,
+      region: region,
+    );
+    final List<PaletteColor> colors = await quantizer.quantizedColors;
+    return PaletteGenerator.fromColors(
+      colors,
+      targets: targets,
+    );
+  }
+
+  /// Create a [PaletteGenerator] from an [ImageProvider], like [FileImage], or
+  /// [AssetImage], asynchronously.
+  ///
+  /// The [size] is the desired size of the image. The image will be resized to
+  /// this size before creating the [PaletteGenerator] from it.
+  ///
+  /// The [region] specifies the part of the (resized) image to inspect for
+  /// color candidates. By default it uses the entire image. Must not be equal
+  /// to [Rect.zero], and must not be larger than the image dimensions.
+  ///
+  /// The [maximumColorCount] sets the maximum number of colors that will be
+  /// returned in the [PaletteGenerator]. The default is 16 colors.
+  ///
+  /// The [filters] specify a lost of [PaletteFilter] instances that can be used
+  /// to include certain colors in the list of colors. The default filter is
+  /// an instance of [AvoidRedBlackWhitePaletteFilter], which stays away from
+  /// whites, blacks, and low-saturation reds.
+  ///
+  /// The [targets] are a list of target color types, specified by creating
+  /// custom [PaletteTarget]s. By default, this is the list of targets in
+  /// [PaletteTarget.baseTargets].
+  ///
+  /// The [timeout] describes how long to wait for the image to load before
+  /// giving up on it. A value of Duration.zero implies waiting forever. The
+  /// default timeout is 15 seconds.
+  static Future<PaletteGenerator> fromImageProvider(
+    ImageProvider imageProvider, {
+    Size? size,
+    Rect? region,
+    int maximumColorCount = _defaultCalculateNumberColors,
+    List<PaletteFilter> filters = const <PaletteFilter>[
+      avoidRedBlackWhitePaletteFilter
+    ],
+    List<PaletteTarget> targets = const <PaletteTarget>[],
+    Duration timeout = const Duration(seconds: 15),
+  }) async {
+    assert(region == null || size != null);
+    assert(region == null || region != Rect.zero);
+    assert(
+        region == null ||
+            (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
+        'Region $region is outside the image ${size!.width}x${size.height}');
+    assert(region == null || size!.contains(region.topLeft),
+        'Region $region is outside the image $size');
+    assert(
+        region == null ||
+            (region.bottomRight.dx <= size!.width &&
+                region.bottomRight.dy <= size.height),
+        'Region $region is outside the image $size');
+    final ImageStream stream = imageProvider.resolve(
+      ImageConfiguration(size: size, devicePixelRatio: 1.0),
+    );
+    final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
+    Timer? loadFailureTimeout;
+    late ImageStreamListener listener;
+    listener = ImageStreamListener((ImageInfo info, bool synchronousCall) {
+      loadFailureTimeout?.cancel();
+      stream.removeListener(listener);
+      imageCompleter.complete(info.image);
+    });
+
+    if (timeout != Duration.zero) {
+      loadFailureTimeout = Timer(timeout, () {
+        stream.removeListener(listener);
+        imageCompleter.completeError(
+          TimeoutException(
+              'Timeout occurred trying to load from $imageProvider'),
+        );
+      });
+    }
+    stream.addListener(listener);
+    final ui.Image image = await imageCompleter.future;
+    ui.Rect? newRegion = region;
+    if (size != null && region != null) {
+      final double scale = image.width / size.width;
+      newRegion = Rect.fromLTRB(
+        region.left * scale,
+        region.top * scale,
+        region.right * scale,
+        region.bottom * scale,
+      );
+    }
+    return PaletteGenerator.fromImage(
+      image,
+      region: newRegion,
+      maximumColorCount: maximumColorCount,
+      filters: filters,
+      targets: targets,
+    );
+  }
+
+  static const int _defaultCalculateNumberColors = 16;
+
+  /// Provides a map of the selected paletteColors for each target in [targets].
+  final Map<PaletteTarget, PaletteColor> selectedSwatches;
+
+  /// The list of [PaletteColor]s that make up the palette, sorted from most
+  /// dominant color to least dominant color.
+  final List<PaletteColor> paletteColors;
+
+  /// The list of targets that the palette uses for custom color selection.
+  ///
+  /// By default, this contains the entire list of predefined targets in
+  /// [PaletteTarget.baseTargets].
+  final List<PaletteTarget> targets;
+
+  /// Returns a list of colors in the [paletteColors], sorted from most
+  /// dominant to least dominant color.
+  Iterable<Color> get colors sync* {
+    for (final PaletteColor paletteColor in paletteColors) {
+      yield paletteColor.color;
+    }
+  }
+
+  /// Returns a vibrant color from the palette. Might be null if an appropriate
+  /// target color could not be found.
+  PaletteColor? get vibrantColor => selectedSwatches[PaletteTarget.vibrant];
+
+  /// Returns a light and vibrant color from the palette. Might be null if an
+  /// appropriate target color could not be found.
+  PaletteColor? get lightVibrantColor =>
+      selectedSwatches[PaletteTarget.lightVibrant];
+
+  /// Returns a dark and vibrant color from the palette. Might be null if an
+  /// appropriate target color could not be found.
+  PaletteColor? get darkVibrantColor =>
+      selectedSwatches[PaletteTarget.darkVibrant];
+
+  /// Returns a muted color from the palette. Might be null if an appropriate
+  /// target color could not be found.
+  PaletteColor? get mutedColor => selectedSwatches[PaletteTarget.muted];
+
+  /// Returns a muted and light color from the palette. Might be null if an
+  /// appropriate target color could not be found.
+  PaletteColor? get lightMutedColor =>
+      selectedSwatches[PaletteTarget.lightMuted];
+
+  /// Returns a muted and dark color from the palette. Might be null if an
+  /// appropriate target color could not be found.
+  PaletteColor? get darkMutedColor => selectedSwatches[PaletteTarget.darkMuted];
+
+  /// The dominant color (the color with the largest population).
+  PaletteColor? get dominantColor => _dominantColor;
+  PaletteColor? _dominantColor;
+
+  void _sortSwatches() {
+    if (paletteColors.isEmpty) {
+      _dominantColor = null;
+      return;
+    }
+    // Sort from most common to least common.
+    paletteColors.sort((PaletteColor a, PaletteColor b) {
+      return b.population.compareTo(a.population);
+    });
+    _dominantColor = paletteColors[0];
+  }
+
+  void _selectSwatches() {
+    final Set<PaletteTarget> allTargets =
+        Set<PaletteTarget>.from(targets + PaletteTarget.baseTargets);
+    final Set<Color> usedColors = <Color>{};
+    for (final PaletteTarget target in allTargets) {
+      target._normalizeWeights();
+      final PaletteColor? targetScore =
+          _generateScoredTarget(target, usedColors);
+      if (targetScore != null) {
+        selectedSwatches[target] = targetScore;
+      }
+    }
+  }
+
+  PaletteColor? _generateScoredTarget(
+      PaletteTarget target, Set<Color> usedColors) {
+    final PaletteColor? maxScoreSwatch =
+        _getMaxScoredSwatchForTarget(target, usedColors);
+    if (maxScoreSwatch != null && target.isExclusive) {
+      // If we have a color, and the target is exclusive, add the color to the
+      // used list.
+      usedColors.add(maxScoreSwatch.color);
+    }
+    return maxScoreSwatch;
+  }
+
+  PaletteColor? _getMaxScoredSwatchForTarget(
+      PaletteTarget target, Set<Color> usedColors) {
+    double maxScore = 0.0;
+    PaletteColor? maxScoreSwatch;
+    for (final PaletteColor paletteColor in paletteColors) {
+      if (_shouldBeScoredForTarget(paletteColor, target, usedColors)) {
+        final double score = _generateScore(paletteColor, target);
+        if (maxScoreSwatch == null || score > maxScore) {
+          maxScoreSwatch = paletteColor;
+          maxScore = score;
+        }
+      }
+    }
+    return maxScoreSwatch;
+  }
+
+  bool _shouldBeScoredForTarget(
+      PaletteColor paletteColor, PaletteTarget target, Set<Color> usedColors) {
+    // Check whether the HSL lightness is within the correct range, and that
+    // this color hasn't been used yet.
+    final HSLColor hslColor = HSLColor.fromColor(paletteColor.color);
+    return hslColor.saturation >= target.minimumSaturation &&
+        hslColor.saturation <= target.maximumSaturation &&
+        hslColor.lightness >= target.minimumLightness &&
+        hslColor.lightness <= target.maximumLightness &&
+        !usedColors.contains(paletteColor.color);
+  }
+
+  double _generateScore(PaletteColor paletteColor, PaletteTarget target) {
+    final HSLColor hslColor = HSLColor.fromColor(paletteColor.color);
+
+    double saturationScore = 0.0;
+    double valueScore = 0.0;
+    double populationScore = 0.0;
+
+    if (target.saturationWeight > 0.0) {
+      saturationScore = target.saturationWeight *
+          (1.0 - (hslColor.saturation - target.targetSaturation).abs());
+    }
+    if (target.lightnessWeight > 0.0) {
+      valueScore = target.lightnessWeight *
+          (1.0 - (hslColor.lightness - target.targetLightness).abs());
+    }
+    if (_dominantColor != null && target.populationWeight > 0.0) {
+      populationScore = target.populationWeight *
+          (paletteColor.population / _dominantColor!.population);
+    }
+
+    return saturationScore + valueScore + populationScore;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(IterableProperty<PaletteColor>(
+        'paletteColors', paletteColors,
+        defaultValue: <PaletteColor>[]));
+    properties.add(IterableProperty<PaletteTarget>('targets', targets,
+        defaultValue: PaletteTarget.baseTargets));
+  }
+}
+
+/// A class which allows custom selection of colors when a [PaletteGenerator] is
+/// generated.
+///
+/// To add a target, supply it to the `targets` list in
+/// [PaletteGenerator.fromImage] or [PaletteGenerator..fromColors].
+///
+/// See also:
+///
+///   * [PaletteGenerator], a class for selecting color palettes from images.
+class PaletteTarget with Diagnosticable {
+  /// Creates a [PaletteTarget] for custom palette selection.
+  PaletteTarget({
+    this.minimumSaturation = 0.0,
+    this.targetSaturation = 0.5,
+    this.maximumSaturation = 1.0,
+    this.minimumLightness = 0.0,
+    this.targetLightness = 0.5,
+    this.maximumLightness = 1.0,
+    this.isExclusive = true,
+  });
+
+  /// The minimum saturation value for this target.
+  final double minimumSaturation;
+
+  /// The target saturation value for this target.
+  final double targetSaturation;
+
+  /// The maximum saturation value for this target.
+  final double maximumSaturation;
+
+  /// The minimum lightness value for this target.
+  final double minimumLightness;
+
+  /// The target lightness value for this target.
+  final double targetLightness;
+
+  /// The maximum lightness value for this target.
+  final double maximumLightness;
+
+  /// Returns whether any color selected for this target is exclusive for this
+  /// target only.
+  ///
+  /// If false, then the color can also be selected for other targets. Defaults
+  /// to true.
+  final bool isExclusive;
+
+  /// The weight of importance that a color's saturation value has on selection.
+  double saturationWeight = _weightSaturation;
+
+  /// The weight of importance that a color's lightness value has on selection.
+  double lightnessWeight = _weightLightness;
+
+  /// The weight of importance that a color's population value has on selection.
+  double populationWeight = _weightPopulation;
+
+  static const double _targetDarkLightness = 0.26;
+  static const double _maxDarkLightness = 0.45;
+
+  static const double _minLightLightness = 0.55;
+  static const double _targetLightLightness = 0.74;
+
+  static const double _minNormalLightness = 0.3;
+  static const double _targetNormalLightness = 0.5;
+  static const double _maxNormalLightness = 0.7;
+
+  static const double _targetMutedSaturation = 0.3;
+  static const double _maxMutedSaturation = 0.4;
+
+  static const double _targetVibrantSaturation = 1.0;
+  static const double _minVibrantSaturation = 0.35;
+
+  static const double _weightSaturation = 0.24;
+  static const double _weightLightness = 0.52;
+  static const double _weightPopulation = 0.24;
+
+  /// A target which has the characteristics of a vibrant color which is light
+  /// in luminance.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget lightVibrant = PaletteTarget(
+    targetLightness: _targetLightLightness,
+    minimumLightness: _minLightLightness,
+    minimumSaturation: _minVibrantSaturation,
+    targetSaturation: _targetVibrantSaturation,
+  );
+
+  /// A target which has the characteristics of a vibrant color which is neither
+  /// light or dark.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget vibrant = PaletteTarget(
+    minimumLightness: _minNormalLightness,
+    targetLightness: _targetNormalLightness,
+    maximumLightness: _maxNormalLightness,
+    minimumSaturation: _minVibrantSaturation,
+    targetSaturation: _targetVibrantSaturation,
+  );
+
+  /// A target which has the characteristics of a vibrant color which is dark in
+  /// luminance.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget darkVibrant = PaletteTarget(
+    targetLightness: _targetDarkLightness,
+    maximumLightness: _maxDarkLightness,
+    minimumSaturation: _minVibrantSaturation,
+    targetSaturation: _targetVibrantSaturation,
+  );
+
+  /// A target which has the characteristics of a muted color which is light in
+  /// luminance.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget lightMuted = PaletteTarget(
+    targetLightness: _targetLightLightness,
+    minimumLightness: _minLightLightness,
+    targetSaturation: _targetMutedSaturation,
+    maximumSaturation: _maxMutedSaturation,
+  );
+
+  /// A target which has the characteristics of a muted color which is neither
+  /// light or dark.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget muted = PaletteTarget(
+    minimumLightness: _minNormalLightness,
+    targetLightness: _targetNormalLightness,
+    maximumLightness: _maxNormalLightness,
+    targetSaturation: _targetMutedSaturation,
+    maximumSaturation: _maxMutedSaturation,
+  );
+
+  /// A target which has the characteristics of a muted color which is dark in
+  /// luminance.
+  ///
+  /// One of the base set of `targets` for [PaletteGenerator.fromImage], in [baseTargets].
+  static final PaletteTarget darkMuted = PaletteTarget(
+    targetLightness: _targetDarkLightness,
+    maximumLightness: _maxDarkLightness,
+    targetSaturation: _targetMutedSaturation,
+    maximumSaturation: _maxMutedSaturation,
+  );
+
+  /// A list of all the available predefined targets.
+  ///
+  /// The base set of `targets` for [PaletteGenerator.fromImage].
+  static final List<PaletteTarget> baseTargets = <PaletteTarget>[
+    lightVibrant,
+    vibrant,
+    darkVibrant,
+    lightMuted,
+    muted,
+    darkMuted,
+  ];
+
+  void _normalizeWeights() {
+    final double sum = saturationWeight + lightnessWeight + populationWeight;
+    if (sum != 0.0) {
+      saturationWeight /= sum;
+      lightnessWeight /= sum;
+      populationWeight /= sum;
+    }
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return minimumSaturation == other.minimumSaturation &&
+        targetSaturation == other.targetSaturation &&
+        maximumSaturation == other.maximumSaturation &&
+        minimumLightness == other.minimumLightness &&
+        targetLightness == other.targetLightness &&
+        maximumLightness == other.maximumLightness &&
+        saturationWeight == other.saturationWeight &&
+        lightnessWeight == other.lightnessWeight &&
+        populationWeight == other.populationWeight;
+  }
+
+  @override
+  int get hashCode {
+    return hashValues(
+      minimumSaturation,
+      targetSaturation,
+      maximumSaturation,
+      minimumLightness,
+      targetLightness,
+      maximumLightness,
+      saturationWeight,
+      lightnessWeight,
+      populationWeight,
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    final PaletteTarget defaultTarget = PaletteTarget();
+    properties.add(DoubleProperty('minimumSaturation', minimumSaturation,
+        defaultValue: defaultTarget.minimumSaturation));
+    properties.add(DoubleProperty('targetSaturation', targetSaturation,
+        defaultValue: defaultTarget.targetSaturation));
+    properties.add(DoubleProperty('maximumSaturation', maximumSaturation,
+        defaultValue: defaultTarget.maximumSaturation));
+    properties.add(DoubleProperty('minimumLightness', minimumLightness,
+        defaultValue: defaultTarget.minimumLightness));
+    properties.add(DoubleProperty('targetLightness', targetLightness,
+        defaultValue: defaultTarget.targetLightness));
+    properties.add(DoubleProperty('maximumLightness', maximumLightness,
+        defaultValue: defaultTarget.maximumLightness));
+    properties.add(DoubleProperty('saturationWeight', saturationWeight,
+        defaultValue: defaultTarget.saturationWeight));
+    properties.add(DoubleProperty('lightnessWeight', lightnessWeight,
+        defaultValue: defaultTarget.lightnessWeight));
+    properties.add(DoubleProperty('populationWeight', populationWeight,
+        defaultValue: defaultTarget.populationWeight));
+  }
+}
+
+typedef _ContrastCalculator = double Function(Color a, Color b, int alpha);
+
+/// A color palette color generated by the [PaletteGenerator].
+///
+/// This palette color represents a dominant [color] in an image, and has a
+/// [population] of how many pixels in the source image it represents. It picks
+/// a [titleTextColor] and a [bodyTextColor] that contrast sufficiently with the
+/// source [color] for comfortable reading.
+///
+/// See also:
+///
+///   * [PaletteGenerator], a class for selecting color palettes from images.
+class PaletteColor with Diagnosticable {
+  /// Generate a [PaletteColor].
+  PaletteColor(this.color, this.population);
+
+  static const double _minContrastTitleText = 3.0;
+  static const double _minContrastBodyText = 4.5;
+
+  /// The color that this palette color represents.
+  final Color color;
+
+  /// The number of pixels in the source image that this palette color
+  /// represents.
+  final int population;
+
+  /// The color of title text for use with this palette color.
+  Color get titleTextColor {
+    if (_titleTextColor == null) {
+      _ensureTextColorsGenerated();
+    }
+    return _titleTextColor!;
+  }
+
+  Color? _titleTextColor;
+
+  /// The color of body text for use with this palette color.
+  Color get bodyTextColor {
+    if (_bodyTextColor == null) {
+      _ensureTextColorsGenerated();
+    }
+    return _bodyTextColor!;
+  }
+
+  Color? _bodyTextColor;
+
+  void _ensureTextColorsGenerated() {
+    if (_titleTextColor == null || _bodyTextColor == null) {
+      const Color white = Color(0xffffffff);
+      const Color black = Color(0xff000000);
+      // First check white, as most colors will be dark
+      final int? lightBodyAlpha =
+          _calculateMinimumAlpha(white, color, _minContrastBodyText);
+      final int? lightTitleAlpha =
+          _calculateMinimumAlpha(white, color, _minContrastTitleText);
+
+      if (lightBodyAlpha != null && lightTitleAlpha != null) {
+        // If we found valid light values, use them and return
+        _bodyTextColor = white.withAlpha(lightBodyAlpha);
+        _titleTextColor = white.withAlpha(lightTitleAlpha);
+        return;
+      }
+
+      final int? darkBodyAlpha =
+          _calculateMinimumAlpha(black, color, _minContrastBodyText);
+      final int? darkTitleAlpha =
+          _calculateMinimumAlpha(black, color, _minContrastTitleText);
+
+      if (darkBodyAlpha != null && darkTitleAlpha != null) {
+        // If we found valid dark values, use them and return
+        _bodyTextColor = black.withAlpha(darkBodyAlpha);
+        _titleTextColor = black.withAlpha(darkTitleAlpha);
+        return;
+      }
+
+      // If we reach here then we can not find title and body values which use
+      // the same lightness, we need to use mismatched values
+      _bodyTextColor = lightBodyAlpha != null
+          ? white.withAlpha(lightBodyAlpha)
+          : black.withAlpha(darkBodyAlpha ?? 255);
+      _titleTextColor = lightTitleAlpha != null
+          ? white.withAlpha(lightTitleAlpha)
+          : black.withAlpha(darkTitleAlpha ?? 255);
+    }
+  }
+
+  /// Returns the contrast ratio between [foreground] and [background].
+  /// [background] must be opaque.
+  ///
+  /// Formula defined [here](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef).
+  static double _calculateContrast(Color foreground, Color background) {
+    assert(background.alpha == 0xff,
+        'background can not be translucent: $background.');
+    if (foreground.alpha < 0xff) {
+      // If the foreground is translucent, composite the foreground over the
+      // background
+      foreground = Color.alphaBlend(foreground, background);
+    }
+    final double lightness1 = foreground.computeLuminance() + 0.05;
+    final double lightness2 = background.computeLuminance() + 0.05;
+    return math.max(lightness1, lightness2) / math.min(lightness1, lightness2);
+  }
+
+  // Calculates the minimum alpha value which can be applied to foreground that
+  // would have a contrast value of at least [minContrastRatio] when compared to
+  // background.
+  //
+  // The background must be opaque (alpha of 255).
+  //
+  // Returns the alpha value in the range 0-255, or null if no value could be
+  // calculated.
+  static int? _calculateMinimumAlpha(
+      Color foreground, Color background, double minContrastRatio) {
+    assert(background.alpha == 0xff,
+        'The background cannot be translucent: $background.');
+    double contrastCalculator(Color fg, Color bg, int alpha) {
+      final Color testForeground = fg.withAlpha(alpha);
+      return _calculateContrast(testForeground, bg);
+    }
+
+    // First lets check that a fully opaque foreground has sufficient contrast
+    final double testRatio = contrastCalculator(foreground, background, 0xff);
+    if (testRatio < minContrastRatio) {
+      // Fully opaque foreground does not have sufficient contrast, return error
+      return null;
+    }
+    foreground = foreground.withAlpha(0xff);
+    return _binaryAlphaSearch(
+        foreground, background, minContrastRatio, contrastCalculator);
+  }
+
+  // Calculates the alpha value using binary search based on a given contrast
+  // evaluation function and target contrast that needs to be satisfied.
+  //
+  // The background must be opaque (alpha of 255).
+  //
+  // Returns the alpha value in the range [0, 255].
+  static int _binaryAlphaSearch(
+    Color foreground,
+    Color background,
+    double minContrastRatio,
+    _ContrastCalculator calculator,
+  ) {
+    assert(background.alpha == 0xff,
+        'The background cannot be translucent: $background.');
+    const int minAlphaSearchMaxIterations = 10;
+    const int minAlphaSearchPrecision = 1;
+
+    // Binary search to find a value with the minimum value which provides
+    // sufficient contrast
+    int numIterations = 0;
+    int minAlpha = 0;
+    int maxAlpha = 0xff;
+    while (numIterations <= minAlphaSearchMaxIterations &&
+        (maxAlpha - minAlpha) > minAlphaSearchPrecision) {
+      final int testAlpha = (minAlpha + maxAlpha) ~/ 2;
+      final double testRatio = calculator(foreground, background, testAlpha);
+      if (testRatio < minContrastRatio) {
+        minAlpha = testAlpha;
+      } else {
+        maxAlpha = testAlpha;
+      }
+      numIterations++;
+    }
+    // Conservatively return the max of the range of possible alphas, which is
+    // known to pass.
+    return maxAlpha;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<Color>('color', color));
+    properties
+        .add(DiagnosticsProperty<Color>('titleTextColor', titleTextColor));
+    properties.add(DiagnosticsProperty<Color>('bodyTextColor', bodyTextColor));
+    properties.add(IntProperty('population', population, defaultValue: 0));
+  }
+
+  @override
+  int get hashCode {
+    return hashValues(color, population);
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return color == other.color && population == other.population;
+  }
+}
+
+/// Hook to allow clients to be able filter colors from selected in a
+/// [PaletteGenerator]. Returns true if the [color] is allowed.
+///
+/// See also:
+///
+///   * [PaletteGenerator.fromImage], which takes a list of these for its
+///    `filters` parameter.
+///   * [avoidRedBlackWhitePaletteFilter], the default filter for
+///     [PaletteGenerator].
+typedef PaletteFilter = bool Function(HSLColor color);
+
+/// A basic [PaletteFilter], which rejects colors near black, white and low
+/// saturation red.
+///
+/// Use this as an element in the `filters` list given to [PaletteGenerator.fromImage].
+///
+/// See also:
+///  * [PaletteGenerator], a class for selecting color palettes from images.
+bool avoidRedBlackWhitePaletteFilter(HSLColor color) {
+  bool _isBlack(HSLColor hslColor) {
+    const double _blackMaxLightness = 0.05;
+    return hslColor.lightness <= _blackMaxLightness;
+  }
+
+  bool _isWhite(HSLColor hslColor) {
+    const double _whiteMinLightness = 0.95;
+    return hslColor.lightness >= _whiteMinLightness;
+  }
+
+  // Returns true if the color is close to the red side of the I line.
+  bool _isNearRedILine(HSLColor hslColor) {
+    const double redLineMinHue = 10.0;
+    const double redLineMaxHue = 37.0;
+    const double redLineMaxSaturation = 0.82;
+    return hslColor.hue >= redLineMinHue &&
+        hslColor.hue <= redLineMaxHue &&
+        hslColor.saturation <= redLineMaxSaturation;
+  }
+
+  return !_isWhite(color) && !_isBlack(color) && !_isNearRedILine(color);
+}
+
+enum _ColorComponent {
+  red,
+  green,
+  blue,
+}
+
+/// A box that represents a volume in the RGB color space.
+class _ColorVolumeBox {
+  _ColorVolumeBox(
+      this._lowerIndex, this._upperIndex, this.histogram, this.colors) {
+    _fitMinimumBox();
+  }
+
+  final _ColorHistogram histogram;
+  final List<Color> colors;
+
+  // The lower and upper index are inclusive.
+  final int _lowerIndex;
+  int _upperIndex;
+
+  // The population of colors within this box.
+  late int _population;
+
+  // Bounds in each of the dimensions.
+  late int _minRed;
+  late int _maxRed;
+  late int _minGreen;
+  late int _maxGreen;
+  late int _minBlue;
+  late int _maxBlue;
+
+  int getVolume() {
+    return (_maxRed - _minRed + 1) *
+        (_maxGreen - _minGreen + 1) *
+        (_maxBlue - _minBlue + 1);
+  }
+
+  bool canSplit() {
+    return getColorCount() > 1;
+  }
+
+  int getColorCount() {
+    return 1 + _upperIndex - _lowerIndex;
+  }
+
+  /// Recomputes the boundaries of this box to tightly fit the colors within the
+  /// box.
+  void _fitMinimumBox() {
+    // Reset the min and max to opposite values
+    int minRed = 256;
+    int minGreen = 256;
+    int minBlue = 256;
+    int maxRed = -1;
+    int maxGreen = -1;
+    int maxBlue = -1;
+    int count = 0;
+    for (int i = _lowerIndex; i <= _upperIndex; i++) {
+      final Color color = colors[i];
+      count += histogram[color]!.value;
+      if (color.red > maxRed) {
+        maxRed = color.red;
+      }
+      if (color.red < minRed) {
+        minRed = color.red;
+      }
+      if (color.green > maxGreen) {
+        maxGreen = color.green;
+      }
+      if (color.green < minGreen) {
+        minGreen = color.green;
+      }
+      if (color.blue > maxBlue) {
+        maxBlue = color.blue;
+      }
+      if (color.blue < minBlue) {
+        minBlue = color.blue;
+      }
+    }
+    _minRed = minRed;
+    _maxRed = maxRed;
+    _minGreen = minGreen;
+    _maxGreen = maxGreen;
+    _minBlue = minBlue;
+    _maxBlue = maxBlue;
+    _population = count;
+  }
+
+  /// Split this color box at the mid-point along it's longest dimension.
+  ///
+  /// Returns the new ColorBox.
+  _ColorVolumeBox splitBox() {
+    assert(canSplit(), "Can't split a box with only 1 color");
+    // find median along the longest dimension
+    final int splitPoint = _findSplitPoint();
+    final _ColorVolumeBox newBox =
+        _ColorVolumeBox(splitPoint + 1, _upperIndex, histogram, colors);
+    // Now change this box's upperIndex and recompute the color boundaries
+    _upperIndex = splitPoint;
+    _fitMinimumBox();
+    return newBox;
+  }
+
+  /// Returns the largest dimension of this color box.
+  _ColorComponent _getLongestColorDimension() {
+    final int redLength = _maxRed - _minRed;
+    final int greenLength = _maxGreen - _minGreen;
+    final int blueLength = _maxBlue - _minBlue;
+    if (redLength >= greenLength && redLength >= blueLength) {
+      return _ColorComponent.red;
+    } else if (greenLength >= redLength && greenLength >= blueLength) {
+      return _ColorComponent.green;
+    } else {
+      return _ColorComponent.blue;
+    }
+  }
+
+  // Finds where to split this box between _lowerIndex and _upperIndex.
+  //
+  // The split point is calculated by finding the longest color dimension, and
+  // then sorting the sub-array based on that dimension value in each color.
+  // The colors are then iterated over until a color is found with the
+  // midpoint closest to the whole box's dimension midpoint.
+  //
+  // Returns the index of the split point in the colors array.
+  int _findSplitPoint() {
+    final _ColorComponent longestDimension = _getLongestColorDimension();
+    int compareColors(Color a, Color b) {
+      int makeValue(int first, int second, int third) {
+        return first << 16 | second << 8 | third;
+      }
+
+      switch (longestDimension) {
+        case _ColorComponent.red:
+          final int aValue = makeValue(a.red, a.green, a.blue);
+          final int bValue = makeValue(b.red, b.green, b.blue);
+          return aValue.compareTo(bValue);
+        case _ColorComponent.green:
+          final int aValue = makeValue(a.green, a.red, a.blue);
+          final int bValue = makeValue(b.green, b.red, b.blue);
+          return aValue.compareTo(bValue);
+        case _ColorComponent.blue:
+          final int aValue = makeValue(a.blue, a.green, a.red);
+          final int bValue = makeValue(b.blue, b.green, b.red);
+          return aValue.compareTo(bValue);
+      }
+    }
+
+    // We need to sort the colors in this box based on the longest color
+    // dimension.
+    final List<Color> colorSubset =
+        colors.sublist(_lowerIndex, _upperIndex + 1);
+    colorSubset.sort(compareColors);
+    colors.replaceRange(_lowerIndex, _upperIndex + 1, colorSubset);
+    final int median = (_population / 2).round();
+    for (int i = 0, count = 0; i <= colorSubset.length; i++) {
+      count += histogram[colorSubset[i]]!.value;
+      if (count >= median) {
+        // We never want to split on the upperIndex, as this will result in the
+        // same box.
+        return math.min(_upperIndex - 1, i + _lowerIndex);
+      }
+    }
+    return _lowerIndex;
+  }
+
+  PaletteColor getAverageColor() {
+    int redSum = 0;
+    int greenSum = 0;
+    int blueSum = 0;
+    int totalPopulation = 0;
+    for (int i = _lowerIndex; i <= _upperIndex; i++) {
+      final Color color = colors[i];
+      final int colorPopulation = histogram[color]!.value;
+      totalPopulation += colorPopulation;
+      redSum += colorPopulation * color.red;
+      greenSum += colorPopulation * color.green;
+      blueSum += colorPopulation * color.blue;
+    }
+    final int redMean = (redSum / totalPopulation).round();
+    final int greenMean = (greenSum / totalPopulation).round();
+    final int blueMean = (blueSum / totalPopulation).round();
+    return PaletteColor(
+      Color.fromARGB(0xff, redMean, greenMean, blueMean),
+      totalPopulation,
+    );
+  }
+}
+
+/// Holds mutable count for a color.
+// Using a mutable count rather than replacing value in the histogram
+// in the _ColorCutQuantizer speeds up building the histogram significantly.
+class _ColorCount {
+  int value = 0;
+}
+
+class _ColorHistogram {
+  final Map<int, Map<int, Map<int, _ColorCount>>> _hist =
+      <int, Map<int, Map<int, _ColorCount>>>{};
+  final DoubleLinkedQueue<Color> _keys = DoubleLinkedQueue<Color>();
+
+  _ColorCount? operator [](Color color) {
+    final Map<int, Map<int, _ColorCount>>? redMap = _hist[color.red];
+    if (redMap == null) {
+      return null;
+    }
+    final Map<int, _ColorCount>? blueMap = redMap[color.blue];
+    if (blueMap == null) {
+      return null;
+    }
+    return blueMap[color.green];
+  }
+
+  void operator []=(Color key, _ColorCount value) {
+    final int red = key.red;
+    final int blue = key.blue;
+    final int green = key.green;
+
+    bool newColor = false;
+
+    Map<int, Map<int, _ColorCount>>? redMap = _hist[red];
+    if (redMap == null) {
+      _hist[red] = redMap = <int, Map<int, _ColorCount>>{};
+      newColor = true;
+    }
+
+    Map<int, _ColorCount>? blueMap = redMap[blue];
+    if (blueMap == null) {
+      redMap[blue] = blueMap = <int, _ColorCount>{};
+      newColor = true;
+    }
+
+    if (blueMap[green] == null) {
+      newColor = true;
+    }
+    blueMap[green] = value;
+
+    if (newColor) {
+      _keys.add(key);
+    }
+  }
+
+  void removeWhere(bool Function(Color key) predicate) {
+    for (final Color key in _keys) {
+      if (predicate(key)) {
+        _hist[key.red]?[key.blue]?.remove(key.green);
+      }
+    }
+    _keys.removeWhere((Color color) => predicate(color));
+  }
+
+  Iterable<Color> get keys {
+    return _keys;
+  }
+
+  int get length {
+    return _keys.length;
+  }
+}
+
+class _ColorCutQuantizer {
+  _ColorCutQuantizer(
+    this.image, {
+    this.maxColors = PaletteGenerator._defaultCalculateNumberColors,
+    this.region,
+    this.filters = const <PaletteFilter>[avoidRedBlackWhitePaletteFilter],
+  })  : assert(region == null || region != Rect.zero),
+        _paletteColors = <PaletteColor>[];
+
+  FutureOr<List<PaletteColor>> get quantizedColors async {
+    if (_paletteColors.isNotEmpty) {
+      return _paletteColors;
+    } else {
+      return _quantizeColors(image);
+    }
+  }
+
+  final ui.Image image;
+  final List<PaletteColor> _paletteColors;
+
+  final int maxColors;
+  final Rect? region;
+  final List<PaletteFilter> filters;
+
+  Iterable<Color> _getImagePixels(ByteData pixels, int width, int height,
+      {Rect? region}) sync* {
+    final int rowStride = width * 4;
+    int rowStart;
+    int rowEnd;
+    int colStart;
+    int colEnd;
+    if (region != null) {
+      rowStart = region.top.floor();
+      rowEnd = region.bottom.floor();
+      colStart = region.left.floor();
+      colEnd = region.right.floor();
+      assert(rowStart >= 0);
+      assert(rowEnd <= height);
+      assert(colStart >= 0);
+      assert(colEnd <= width);
+    } else {
+      rowStart = 0;
+      rowEnd = height;
+      colStart = 0;
+      colEnd = width;
+    }
+    int byteCount = 0;
+    for (int row = rowStart; row < rowEnd; ++row) {
+      for (int col = colStart; col < colEnd; ++col) {
+        final int position = row * rowStride + col * 4;
+        // Convert from RGBA to ARGB.
+        final int pixel = pixels.getUint32(position);
+        final Color result = Color((pixel << 24) | (pixel >> 8));
+        byteCount += 4;
+        yield result;
+      }
+    }
+    assert(byteCount == ((rowEnd - rowStart) * (colEnd - colStart) * 4));
+  }
+
+  bool _shouldIgnoreColor(Color color) {
+    final HSLColor hslColor = HSLColor.fromColor(color);
+    if (filters.isNotEmpty) {
+      for (final PaletteFilter filter in filters) {
+        if (!filter(hslColor)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  Future<List<PaletteColor>> _quantizeColors(ui.Image image) async {
+    const int quantizeWordWidth = 5;
+    const int quantizeChannelWidth = 8;
+    const int quantizeShift = quantizeChannelWidth - quantizeWordWidth;
+    const int quantizeWordMask =
+        ((1 << quantizeWordWidth) - 1) << quantizeShift;
+
+    Color quantizeColor(Color color) {
+      return Color.fromARGB(
+        color.alpha,
+        color.red & quantizeWordMask,
+        color.green & quantizeWordMask,
+        color.blue & quantizeWordMask,
+      );
+    }
+
+    final ByteData? imageData =
+        await image.toByteData(format: ui.ImageByteFormat.rawRgba);
+    if (imageData == null) {
+      throw 'Failed to encode the image.';
+    }
+    final Iterable<Color> pixels =
+        _getImagePixels(imageData, image.width, image.height, region: region);
+    final _ColorHistogram hist = _ColorHistogram();
+    Color? currentColor;
+    _ColorCount? currentColorCount;
+
+    for (final Color pixel in pixels) {
+      // Update the histogram, but only for non-zero alpha values, and for the
+      // ones we do add, make their alphas opaque so that we can use a Color as
+      // the histogram key.
+      final Color quantizedColor = quantizeColor(pixel);
+      final Color colorKey = quantizedColor.withAlpha(0xff);
+      // Skip pixels that are entirely transparent.
+      if (quantizedColor.alpha == 0x0) {
+        continue;
+      }
+      if (currentColor != colorKey) {
+        currentColor = colorKey;
+        currentColorCount = hist[colorKey];
+        if (currentColorCount == null) {
+          hist[colorKey] = currentColorCount = _ColorCount();
+        }
+      }
+      currentColorCount!.value = currentColorCount.value + 1;
+    }
+    // Now let's remove any colors that the filters want to ignore.
+    hist.removeWhere((Color color) {
+      return _shouldIgnoreColor(color);
+    });
+    if (hist.length <= maxColors) {
+      // The image has fewer colors than the maximum requested, so just return
+      // the colors.
+      _paletteColors.clear();
+      for (final Color color in hist.keys) {
+        _paletteColors.add(PaletteColor(color, hist[color]!.value));
+      }
+    } else {
+      // We need use quantization to reduce the number of colors
+      _paletteColors.clear();
+      _paletteColors.addAll(_quantizePixels(maxColors, hist));
+    }
+    return _paletteColors;
+  }
+
+  List<PaletteColor> _quantizePixels(
+    int maxColors,
+    _ColorHistogram histogram,
+  ) {
+    int volumeComparator(_ColorVolumeBox a, _ColorVolumeBox b) {
+      return b.getVolume().compareTo(a.getVolume());
+    }
+
+    // Create the priority queue which is sorted by volume descending. This
+    // means we always split the largest box in the queue
+    final PriorityQueue<_ColorVolumeBox> priorityQueue =
+        HeapPriorityQueue<_ColorVolumeBox>(volumeComparator);
+    // To start, offer a box which contains all of the colors
+    priorityQueue.add(_ColorVolumeBox(
+        0, histogram.length - 1, histogram, histogram.keys.toList()));
+    // Now go through the boxes, splitting them until we have reached maxColors
+    // or there are no more boxes to split
+    _splitBoxes(priorityQueue, maxColors);
+    // Finally, return the average colors of the color boxes.
+    return _generateAverageColors(priorityQueue);
+  }
+
+  // Iterate through the [PriorityQueue], popping [_ColorVolumeBox] objects
+  // from the queue and splitting them. Once split, the new box and the
+  // remaining box are offered back to the queue.
+  //
+  // The `maxSize` is the maximum number of boxes to split.
+  void _splitBoxes(PriorityQueue<_ColorVolumeBox> queue, final int maxSize) {
+    while (queue.length < maxSize) {
+      final _ColorVolumeBox colorVolumeBox = queue.removeFirst();
+      if (colorVolumeBox.canSplit()) {
+        // First split the box, and offer the result
+        queue.add(colorVolumeBox.splitBox());
+        // Then offer the box back
+        queue.add(colorVolumeBox);
+      } else {
+        // If we get here then there are no more boxes to split, so return
+        return;
+      }
+    }
+  }
+
+  // Generates the average colors from each of the boxes in the queue.
+  List<PaletteColor> _generateAverageColors(
+      PriorityQueue<_ColorVolumeBox> colorVolumeBoxes) {
+    final List<PaletteColor> colors = <PaletteColor>[];
+    for (final _ColorVolumeBox colorVolumeBox in colorVolumeBoxes.toList()) {
+      final PaletteColor paletteColor = colorVolumeBox.getAverageColor();
+      if (!_shouldIgnoreColor(paletteColor.color)) {
+        colors.add(paletteColor);
+      }
+    }
+    return colors;
+  }
+}
diff --git a/packages/palette_generator/palette_generator.iml b/packages/palette_generator/palette_generator.iml
new file mode 100644
index 0000000..c9ef74c
--- /dev/null
+++ b/packages/palette_generator/palette_generator.iml
@@ -0,0 +1,18 @@
+<?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" />
+      <excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/android/app/src/main/java/io/flutter/packages" />
+      <excludeFolder url="file://$MODULE_DIR$/example/build" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/packages/palette_generator/pubspec.yaml b/packages/palette_generator/pubspec.yaml
new file mode 100644
index 0000000..8bc401b
--- /dev/null
+++ b/packages/palette_generator/pubspec.yaml
@@ -0,0 +1,18 @@
+name: palette_generator
+description: Flutter package for generating palette colors from a source image.
+homepage: https://github.com/flutter/packages/tree/master/packages/palette_generator
+version: 0.3.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=1.15.21"
+
+dependencies:
+  collection: ^1.15.0
+  flutter:
+    sdk: flutter
+  path: ^1.8.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
diff --git a/packages/palette_generator/test/assets/dominant.png b/packages/palette_generator/test/assets/dominant.png
new file mode 100644
index 0000000..ca54543
--- /dev/null
+++ b/packages/palette_generator/test/assets/dominant.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/landscape.png b/packages/palette_generator/test/assets/landscape.png
new file mode 100644
index 0000000..815f599
--- /dev/null
+++ b/packages/palette_generator/test/assets/landscape.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/tall_blue.png b/packages/palette_generator/test/assets/tall_blue.png
new file mode 100644
index 0000000..6453d79
--- /dev/null
+++ b/packages/palette_generator/test/assets/tall_blue.png
Binary files differ
diff --git a/packages/palette_generator/test/assets/wide_red.png b/packages/palette_generator/test/assets/wide_red.png
new file mode 100644
index 0000000..5130b44
--- /dev/null
+++ b/packages/palette_generator/test/assets/wide_red.png
Binary files differ
diff --git a/packages/palette_generator/test/palette_generator_test.dart b/packages/palette_generator/test/palette_generator_test.dart
new file mode 100644
index 0000000..375b5db
--- /dev/null
+++ b/packages/palette_generator/test/palette_generator_test.dart
@@ -0,0 +1,388 @@
+// 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:io';
+import 'dart:typed_data';
+import 'dart:ui' as ui show Image, Codec, FrameInfo, instantiateImageCodec;
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as path;
+import 'package:palette_generator/palette_generator.dart';
+
+/// An image provider implementation for testing that takes a pre-loaded image.
+/// This avoids handling asynchronous I/O in the test zone, which is
+/// problematic.
+class FakeImageProvider extends ImageProvider<FakeImageProvider> {
+  const FakeImageProvider(this._image, {this.scale = 1.0});
+
+  final ui.Image _image;
+
+  /// The scale to place in the [ImageInfo] object of the image.
+  final double scale;
+
+  @override
+  Future<FakeImageProvider> obtainKey(ImageConfiguration configuration) {
+    return SynchronousFuture<FakeImageProvider>(this);
+  }
+
+  @override
+  ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) {
+    assert(key == this);
+    return OneFrameImageStreamCompleter(
+      SynchronousFuture<ImageInfo>(
+        ImageInfo(image: _image, scale: scale),
+      ),
+    );
+  }
+}
+
+Future<ImageProvider> loadImage(String name) async {
+  File imagePath = File(path.joinAll(<String>['assets', name]));
+  if (path.split(Directory.current.absolute.path).last != 'test') {
+    imagePath = File(path.join('test', imagePath.path));
+  }
+  final Uint8List data = Uint8List.fromList(imagePath.readAsBytesSync());
+  final ui.Codec codec = await ui.instantiateImageCodec(data);
+  final ui.FrameInfo frameInfo = await codec.getNextFrame();
+  return FakeImageProvider(frameInfo.image);
+}
+
+Future<void> main() async {
+  // Load the images outside of the test zone so that IO doesn't get
+  // complicated.
+  final List<String> imageNames = <String>[
+    'tall_blue',
+    'wide_red',
+    'dominant',
+    'landscape'
+  ];
+  final Map<String, ImageProvider> testImages = <String, ImageProvider>{};
+  for (final String name in imageNames) {
+    testImages[name] = await loadImage('$name.png');
+  }
+
+  testWidgets('Initialize the image cache', (WidgetTester tester) async {
+    // We need to have a testWidgets test in order to initialize the image
+    // cache for the other tests, but they timeout if they too are testWidgets
+    // tests.
+    tester.pumpWidget(const Placeholder());
+  });
+
+  test('PaletteGenerator works on 1-pixel wide blue image', () async {
+    final PaletteGenerator palette =
+        await PaletteGenerator.fromImageProvider(testImages['tall_blue']!);
+    expect(palette.paletteColors.length, equals(1));
+    expect(palette.paletteColors[0].color,
+        within<Color>(distance: 8, from: const Color(0xff0000ff)));
+  });
+
+  test('PaletteGenerator works on 1-pixel high red image', () async {
+    final PaletteGenerator palette =
+        await PaletteGenerator.fromImageProvider(testImages['wide_red']!);
+    expect(palette.paletteColors.length, equals(1));
+    expect(palette.paletteColors[0].color,
+        within<Color>(distance: 8, from: const Color(0xffff0000)));
+  });
+
+  test('PaletteGenerator finds dominant color and text colors', () async {
+    final PaletteGenerator palette =
+        await PaletteGenerator.fromImageProvider(testImages['dominant']!);
+    expect(palette.paletteColors.length, equals(3));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff0000ff)));
+    expect(palette.dominantColor!.titleTextColor,
+        within<Color>(distance: 8, from: const Color(0x8affffff)));
+    expect(palette.dominantColor!.bodyTextColor,
+        within<Color>(distance: 8, from: const Color(0xb2ffffff)));
+  });
+
+  test('PaletteGenerator works with regions', () async {
+    final ImageProvider imageProvider = testImages['dominant']!;
+    Rect region = const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0);
+    const Size size = Size(100.0, 100.0);
+    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+        imageProvider,
+        region: region,
+        size: size);
+    expect(palette.paletteColors.length, equals(3));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff0000ff)));
+
+    region = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        region: region, size: size);
+    expect(palette.paletteColors.length, equals(1));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xffff0000)));
+
+    region = const Rect.fromLTRB(0.0, 0.0, 30.0, 20.0);
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        region: region, size: size);
+    expect(palette.paletteColors.length, equals(3));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff00ff00)));
+  });
+
+  test('PaletteGenerator works as expected on a real image', () async {
+    final PaletteGenerator palette =
+        await PaletteGenerator.fromImageProvider(testImages['landscape']!);
+    final List<PaletteColor> expectedSwatches = <PaletteColor>[
+      PaletteColor(const Color(0xff3f630c), 10137),
+      PaletteColor(const Color(0xff3c4b2a), 4773),
+      PaletteColor(const Color(0xff81b2e9), 4762),
+      PaletteColor(const Color(0xffc0d6ec), 4714),
+      PaletteColor(const Color(0xff4c4f50), 2465),
+      PaletteColor(const Color(0xff5c635b), 2463),
+      PaletteColor(const Color(0xff6e80a2), 2421),
+      PaletteColor(const Color(0xff9995a3), 1214),
+      PaletteColor(const Color(0xff676c4d), 1213),
+      PaletteColor(const Color(0xffc4b2b2), 1173),
+      PaletteColor(const Color(0xff445166), 1040),
+      PaletteColor(const Color(0xff475d83), 1019),
+      PaletteColor(const Color(0xff7e7360), 589),
+      PaletteColor(const Color(0xfff6b835), 286),
+      PaletteColor(const Color(0xffb9983d), 152),
+      PaletteColor(const Color(0xffe3ab35), 149),
+    ];
+    final Iterable<Color> expectedColors =
+        expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+    expect(palette.paletteColors, containsAll(expectedSwatches));
+    expect(palette.vibrantColor, isNotNull);
+    expect(palette.lightVibrantColor, isNotNull);
+    expect(palette.darkVibrantColor, isNotNull);
+    expect(palette.mutedColor, isNotNull);
+    expect(palette.lightMutedColor, isNotNull);
+    expect(palette.darkMutedColor, isNotNull);
+    expect(palette.vibrantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xfff6b835)));
+    expect(palette.lightVibrantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff82b2e9)));
+    expect(palette.darkVibrantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff3f630c)));
+    expect(palette.mutedColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff6c7fa2)));
+    expect(palette.lightMutedColor!.color,
+        within<Color>(distance: 8, from: const Color(0xffc4b2b2)));
+    expect(palette.darkMutedColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff3c4b2a)));
+    expect(palette.colors, containsAllInOrder(expectedColors));
+    expect(palette.colors.length, equals(palette.paletteColors.length));
+  });
+
+  test('PaletteGenerator limits max colors', () async {
+    final ImageProvider imageProvider = testImages['landscape']!;
+    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+        imageProvider,
+        maximumColorCount: 32);
+    expect(palette.paletteColors.length, equals(31));
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        maximumColorCount: 1);
+    expect(palette.paletteColors.length, equals(1));
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        maximumColorCount: 15);
+    expect(palette.paletteColors.length, equals(15));
+  });
+
+  test('PaletteGenerator Filters work', () async {
+    final ImageProvider imageProvider = testImages['landscape']!;
+    // First, test that supplying the default filter is the same as not supplying one.
+    List<PaletteFilter> filters = <PaletteFilter>[
+      avoidRedBlackWhitePaletteFilter
+    ];
+    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+        imageProvider,
+        filters: filters);
+    final List<PaletteColor> expectedSwatches = <PaletteColor>[
+      PaletteColor(const Color(0xff3f630c), 10137),
+      PaletteColor(const Color(0xff3c4b2a), 4773),
+      PaletteColor(const Color(0xff81b2e9), 4762),
+      PaletteColor(const Color(0xffc0d6ec), 4714),
+      PaletteColor(const Color(0xff4c4f50), 2465),
+      PaletteColor(const Color(0xff5c635b), 2463),
+      PaletteColor(const Color(0xff6e80a2), 2421),
+      PaletteColor(const Color(0xff9995a3), 1214),
+      PaletteColor(const Color(0xff676c4d), 1213),
+      PaletteColor(const Color(0xffc4b2b2), 1173),
+      PaletteColor(const Color(0xff445166), 1040),
+      PaletteColor(const Color(0xff475d83), 1019),
+      PaletteColor(const Color(0xff7e7360), 589),
+      PaletteColor(const Color(0xfff6b835), 286),
+      PaletteColor(const Color(0xffb9983d), 152),
+      PaletteColor(const Color(0xffe3ab35), 149),
+    ];
+    final Iterable<Color> expectedColors =
+        expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+    expect(palette.paletteColors, containsAll(expectedSwatches));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff3f630c)));
+    expect(palette.colors, containsAllInOrder(expectedColors));
+
+    // A non-default filter works (and the default filter isn't applied too).
+    filters = <PaletteFilter>[onlyBluePaletteFilter];
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        filters: filters);
+    final List<PaletteColor> blueSwatches = <PaletteColor>[
+      PaletteColor(const Color(0xff4c5c75), 1515),
+      PaletteColor(const Color(0xff7483a1), 1505),
+      PaletteColor(const Color(0xff515661), 1476),
+      PaletteColor(const Color(0xff769dd4), 1470),
+      PaletteColor(const Color(0xff3e4858), 777),
+      PaletteColor(const Color(0xff98a3bc), 760),
+      PaletteColor(const Color(0xffb4c7e0), 760),
+      PaletteColor(const Color(0xff99bbe5), 742),
+      PaletteColor(const Color(0xffcbdef0), 701),
+      PaletteColor(const Color(0xff1c212b), 429),
+      PaletteColor(const Color(0xff393c46), 417),
+      PaletteColor(const Color(0xff526483), 394),
+      PaletteColor(const Color(0xff61708b), 372),
+      PaletteColor(const Color(0xff5e8ccc), 345),
+      PaletteColor(const Color(0xff587ab4), 194),
+      PaletteColor(const Color(0xff5584c8), 182),
+    ];
+    final Iterable<Color> expectedBlues =
+        blueSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+
+    expect(palette.paletteColors, containsAll(blueSwatches));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xff4c5c75)));
+    expect(palette.colors, containsAllInOrder(expectedBlues));
+
+    // More than one filter is the intersection of the two filters.
+    filters = <PaletteFilter>[onlyBluePaletteFilter, onlyCyanPaletteFilter];
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        filters: filters);
+    final List<PaletteColor> blueGreenSwatches = <PaletteColor>[
+      PaletteColor(const Color(0xffc8e8f8), 87),
+      PaletteColor(const Color(0xff5c6c74), 73),
+      PaletteColor(const Color(0xff6f8088), 49),
+      PaletteColor(const Color(0xff687880), 49),
+      PaletteColor(const Color(0xff506068), 45),
+      PaletteColor(const Color(0xff485860), 39),
+      PaletteColor(const Color(0xff405058), 21),
+      PaletteColor(const Color(0xffd6ebf3), 11),
+      PaletteColor(const Color(0xff2f3f47), 7),
+      PaletteColor(const Color(0xff0f1f27), 6),
+      PaletteColor(const Color(0xffc0e0f0), 6),
+      PaletteColor(const Color(0xff203038), 3),
+      PaletteColor(const Color(0xff788890), 2),
+      PaletteColor(const Color(0xff384850), 2),
+      PaletteColor(const Color(0xff98a8b0), 1),
+      PaletteColor(const Color(0xffa8b8c0), 1),
+    ];
+    final Iterable<Color> expectedBlueGreens =
+        blueGreenSwatches.map<Color>((PaletteColor swatch) => swatch.color);
+
+    expect(palette.paletteColors, containsAll(blueGreenSwatches));
+    expect(palette.dominantColor, isNotNull);
+    expect(palette.dominantColor!.color,
+        within<Color>(distance: 8, from: const Color(0xffc8e8f8)));
+    expect(palette.colors, containsAllInOrder(expectedBlueGreens));
+
+    // Mutually exclusive filters return an empty palette.
+    filters = <PaletteFilter>[onlyBluePaletteFilter, onlyGreenPaletteFilter];
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        filters: filters);
+    expect(palette.paletteColors, isEmpty);
+    expect(palette.dominantColor, isNull);
+    expect(palette.colors, isEmpty);
+  });
+
+  test('PaletteGenerator targets work', () async {
+    final ImageProvider imageProvider = testImages['landscape']!;
+    // Passing an empty set of targets works the same as passing a null targets
+    // list.
+    PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
+        imageProvider,
+        targets: <PaletteTarget>[]);
+    expect(palette.selectedSwatches, isNotEmpty);
+    expect(palette.vibrantColor, isNotNull);
+    expect(palette.lightVibrantColor, isNotNull);
+    expect(palette.darkVibrantColor, isNotNull);
+    expect(palette.mutedColor, isNotNull);
+    expect(palette.lightMutedColor, isNotNull);
+    expect(palette.darkMutedColor, isNotNull);
+
+    // Passing targets augments the baseTargets, and those targets are found.
+    final List<PaletteTarget> saturationExtremeTargets = <PaletteTarget>[
+      PaletteTarget(minimumSaturation: 0.85),
+      PaletteTarget(maximumSaturation: .25),
+    ];
+    palette = await PaletteGenerator.fromImageProvider(imageProvider,
+        targets: saturationExtremeTargets);
+    expect(palette.vibrantColor, isNotNull);
+    expect(palette.lightVibrantColor, isNotNull);
+    expect(palette.darkVibrantColor, isNotNull);
+    expect(palette.mutedColor, isNotNull);
+    expect(palette.lightMutedColor, isNotNull);
+    expect(palette.darkMutedColor, isNotNull);
+    expect(palette.selectedSwatches.length,
+        equals(PaletteTarget.baseTargets.length + 2));
+    final PaletteColor? selectedSwatchesFirst =
+        palette.selectedSwatches[saturationExtremeTargets[0]];
+    final PaletteColor? selectedSwatchesSecond =
+        palette.selectedSwatches[saturationExtremeTargets[1]];
+    expect(selectedSwatchesFirst, isNotNull);
+    expect(selectedSwatchesSecond, isNotNull);
+    expect(selectedSwatchesFirst!.color, equals(const Color(0xfff6b835)));
+    expect(selectedSwatchesSecond!.color, equals(const Color(0xff6e80a2)));
+  });
+
+  test('PaletteGenerator produces consistent results', () async {
+    final ImageProvider imageProvider = testImages['landscape']!;
+
+    PaletteGenerator lastPalette =
+        await PaletteGenerator.fromImageProvider(imageProvider);
+    for (int i = 0; i < 5; ++i) {
+      final PaletteGenerator palette =
+          await PaletteGenerator.fromImageProvider(imageProvider);
+      expect(palette.paletteColors.length, lastPalette.paletteColors.length);
+      expect(palette.vibrantColor, equals(lastPalette.vibrantColor));
+      expect(palette.lightVibrantColor, equals(lastPalette.lightVibrantColor));
+      expect(palette.darkVibrantColor, equals(lastPalette.darkVibrantColor));
+      expect(palette.mutedColor, equals(lastPalette.mutedColor));
+      expect(palette.lightMutedColor, equals(lastPalette.lightMutedColor));
+      expect(palette.darkMutedColor, equals(lastPalette.darkMutedColor));
+      expect(palette.dominantColor, isNotNull);
+      expect(lastPalette.dominantColor, isNotNull);
+      expect(palette.dominantColor!.color,
+          within<Color>(distance: 8, from: lastPalette.dominantColor!.color));
+      lastPalette = palette;
+    }
+  });
+}
+
+bool onlyBluePaletteFilter(HSLColor hslColor) {
+  const double blueLineMinHue = 185.0;
+  const double blueLineMaxHue = 260.0;
+  const double blueLineMaxSaturation = 0.82;
+  return hslColor.hue >= blueLineMinHue &&
+      hslColor.hue <= blueLineMaxHue &&
+      hslColor.saturation <= blueLineMaxSaturation;
+}
+
+bool onlyCyanPaletteFilter(HSLColor hslColor) {
+  const double cyanLineMinHue = 165.0;
+  const double cyanLineMaxHue = 200.0;
+  const double cyanLineMaxSaturation = 0.82;
+  return hslColor.hue >= cyanLineMinHue &&
+      hslColor.hue <= cyanLineMaxHue &&
+      hslColor.saturation <= cyanLineMaxSaturation;
+}
+
+bool onlyGreenPaletteFilter(HSLColor hslColor) {
+  const double greenLineMinHue = 80.0;
+  const double greenLineMaxHue = 165.0;
+  const double greenLineMaxSaturation = 0.82;
+  return hslColor.hue >= greenLineMinHue &&
+      hslColor.hue <= greenLineMaxHue &&
+      hslColor.saturation <= greenLineMaxSaturation;
+}
diff --git a/packages/pigeon/.gitignore b/packages/pigeon/.gitignore
new file mode 100644
index 0000000..a70e872
--- /dev/null
+++ b/packages/pigeon/.gitignore
@@ -0,0 +1,15 @@
+build/
+e2e_tests/test_objc/ios/Flutter/
+platform_tests/ios_unit_tests/ios/Runner/messages.h
+platform_tests/ios_unit_tests/ios/Runner/messages.m
+xcuserdata/
+.gradle/
+.flutter-plugins
+.flutter-plugins-dependencies
+*.iml
+**/.symlinks/
+gradlew
+gradlew.bat
+local.properties
+gradle-wrapper.jar
+
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
new file mode 100644
index 0000000..0125754
--- /dev/null
+++ b/packages/pigeon/CHANGELOG.md
@@ -0,0 +1,210 @@
+## 0.2.1
+
+* Java: Fixed issue where multiple async HostApis can generate multiple Result interfaces.
+* Dart: Made it so you can specify the BinaryMessenger of the generated APIs.
+
+## 0.2.0
+
+* **BREAKING CHANGE** - Pigeon files must be null-safe now.  That means the
+  fields inside of the classes must be declared nullable (
+  [non-null fields](https://github.com/flutter/flutter/issues/59118) arent't yet
+  supported).  Migration example:
+
+```dart
+// Version 0.1.x
+class Foo {
+  int bar;
+  String baz;
+}
+
+// Version 0.2.x
+class Foo {
+  int? bar;
+  String? baz;
+}
+```
+
+* **BREAKING CHANGE** - The default output from Pigeon is now null-safe.  If you
+  want non-null-safe code you must provide the `--no-dart_null_safety` flag.
+* The Pigeon source code is now null-safe.
+* Fixed niladic non-value returning async functions in the Java generator.
+* Made `runCommandLine` return an the status code.
+
+## 0.1.24
+
+* Moved logic from bin/ to lib/ to help customers wrap up the behavior.
+* Added some more linter ignores for Dart.
+
+## 0.1.23
+
+* More Java linter and linter fixes.
+
+## 0.1.22
+
+* Java code generator enhancements:
+  * Added linter tests to CI.
+  * Fixed some linter issues in the Java code.
+
+## 0.1.21
+
+* Fixed decode method on generated Flutter classes that use null-safety and have
+  null values.
+
+## 0.1.20
+
+* Implemented `@async` HostApi's for iOS.
+* Fixed async FlutterApi methods with void return.
+
+## 0.1.19
+
+* Fixed a bug introduced in 0.1.17 where methods without arguments were
+  no longer being called.
+
+## 0.1.18
+
+* Null safe requires Dart 2.12.
+
+## 0.1.17
+
+* Split out test code generation for Dart into a separate file via the
+  --dart_test_out flag.
+
+## 0.1.16
+
+* Fixed running in certain environments where NNBD is enabled by default.
+
+## 0.1.15
+
+* Added support for running in versions of Dart that support NNBD.
+
+## 0.1.14
+
+* [Windows] Fixed executing from drives other than C:.
+
+## 0.1.13
+
+* Fixed execution on Windows with certain setups where Dart didn't allow
+  backslashes in `import` statements.
+
+## 0.1.12
+
+* Fixed assert failure with creating a PlatformException as a result of an
+  exception in Java handlers.
+
+## 0.1.11
+
+* Added flag to generate null safety annotated Dart code `--dart_null_safety`.
+* Made it so Dart API setup methods can take null.
+
+## 0.1.10+1
+
+* Updated the examples page.
+
+## 0.1.10
+
+* Fixed bug that prevented running `pigeon` on Windows (introduced in `0.1.8`).
+
+## 0.1.9
+
+* Fixed bug where executing pigeon without arguments would crash (introduced in 0.1.8).
+
+## 0.1.8
+
+* Started spawning pigeon_lib in an isolate instead of a subprocess.  The
+  subprocess could have lead to errors if the dart version on $PATH didn't match
+  the one that comes with flutter.
+
+## 0.1.7
+
+* Fixed Dart compilation for later versions that support null safety, opting out
+  of it for now.
+* Fixed nested types in the Java runtime.
+
+## 0.1.6
+
+* Fixed unused variable linter warning in Dart code under certain conditions.
+
+## 0.1.5
+
+* Made array datatypes correctly get imported and exported avoiding the need to
+  add extra imports to generated code.
+
+## 0.1.4
+
+* Fixed nullability for NSError's in generated objc code.
+* Fixed nullability of nested objects in the Dart generator.
+* Added test to make sure the pigeon version is correct in generated code headers.
+
+## 0.1.3
+
+* Added error message if supported datatypes are used as arguments or return
+  types directly, without an enclosing class.
+* Added support for List and Map datatypes in Java and Objective-C targets.
+
+## 0.1.2+1
+
+* Updated the Readme.md.
+
+## 0.1.2
+
+* Removed static analysis warnings from generated Java code.
+
+## 0.1.1
+
+* Fixed issue where nested types didn't work if they weren't present in the Api.
+
+## 0.1.0
+
+* Added pigeon.dart.
+* Fixed some Obj-C linter problems.
+* Added the ability to generate a mock handler in Dart.
+
+## 0.1.0-experimental.11
+
+* Fixed setting an api to null in Java.
+
+## 0.1.0-experimental.10
+
+* Added support for void argument functions.
+* Added nullability annotations to generated objc code.
+
+## 0.1.0-experimental.9
+
+* Added e2e tests for iOS.
+
+## 0.1.0-experimental.8
+
+* Renamed `setupPigeon` to `configurePigeon`.
+
+## 0.1.0-experimental.7
+
+* Suppressed or got rid of warnings in generated Dart code.
+
+## 0.1.0-experimental.6
+
+* Added support for void return types.
+
+## 0.1.0-experimental.5
+
+* Fixed runtime exception in Android with values of ints less than 2^32.
+* Incremented codegen version warning.
+
+## 0.1.0-experimental.4
+
+* Fixed primitive types for Android Java.
+
+## 0.1.0-experimental.3
+
+* Added support for for Android Java.
+
+## 0.1.0-experimental.2
+
+* Added Host->Flutter calls for Objective-C
+
+## 0.1.0-experimental.1
+
+* Fixed warning in the README.md
+
+## 0.1.0-experimental.0
+
+* Initial release.
diff --git a/packages/pigeon/CONTRIBUTING.md b/packages/pigeon/CONTRIBUTING.md
new file mode 100644
index 0000000..d4174c8
--- /dev/null
+++ b/packages/pigeon/CONTRIBUTING.md
@@ -0,0 +1,74 @@
+# Pigeon Contributor's Guide
+
+## Description
+
+Pigeon is a code generation tool that adds type safety to Flutter’s Platform
+Channels.  This document serves as an overview of how it functions to help
+people who would like to contribute to the project.
+
+## State Diagram
+
+Pigeon generates a temporary file in its _LaunchIsolate_, the isolate that is
+spawned to run `main()`, then launches another isolate, _PigeonIsolate_, that
+uses `dart:mirrors` to parse the generated file, creating an
+[AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree), then running code
+generators with that AST.
+
+![State Diagram](./docs/pigeon_state.png)
+
+## Source Index
+
+* [ast.dart](./lib/ast.dart) - The data structure for representing the Abstract Syntax Tree.
+* [dart_generator.dart](./lib/dart_generator.dart) - The Dart code generator.
+* [java_generator.dart](./lib/java_generator.dart) - The Java code generator.
+* [objc_generator.dart](./lib/objc_generator.dart) - The Objective-C code
+  generator (header and source files).
+* [generator_tools.dart](./lib/generator_tools.dart) - Shared code between generators.
+* [pigeon_cl.dart](./lib/pigeon_cl.dart) - The top-level function executed by
+  the command line tool in [bin/][./bin].
+* [pigeon_lib.dart](./lib/pigeon_lib.dart) - The top-level function for the
+  PigeonIsolate and the AST generation code.
+* [pigeon.dart](./lib/pigeon.dart) - A file of exported modules, the intended
+  import for users of Pigeon.
+
+## Testing Overview
+
+Pigeon has 3 types of tests, you'll find them all in [run_tests.sh](./run_tests.sh).
+
+* Unit tests - These are the fastest tests that are just typical unit tests,
+  they may be generating code and checking it against a regular expression to
+  see if it's correct.  Example:
+  [dart_generator_test.dart](./test/dart_generator_test.dart)
+* Compilation tests -  These tests generate code, then attempt to compile that
+  code.  These are tests are much slower than unit tests, but not as slow as
+  integration tests.  These tests are typically run against the Pigeon files in
+  [pigeons](./pigeons).
+* Integration tests - These tests generate code, then compile the generated
+  code, then execute the generated code.  It can be thought of as unit-tests run
+  against the generated code.  Examples: [platform_tests](./platform_tests)
+
+## Generated Source Code Example
+
+This is what the temporary generated code that the _PigeonIsolate_ executes
+looks like (see [State Diagram](#state-diagram)):
+
+```dart
+@dart = 2.12
+import 'path/to/supplied/pigeon/file.dart'
+import 'dart:io';
+import 'dart:isolate';
+import 'package:pigeon/pigeon_lib.dart';
+void main(List<String> args, SendPort sendPort) async {
+  sendPort.send(await Pigeon.run(args));
+}
+```
+
+This is how `dart:mirrors` gets access to the supplied Pigeon file.
+
+## Imminent Plans
+
+* Migrate to Dart Analyzer for AST generation ([issue
+  78818](https://github.com/flutter/flutter/issues/78818)) - We might have
+  reached the limitations of using dart:mirrors for parsing the Dart files.
+  That package has been deprecated and it doesn't support null-safe annotations.
+  We should migrate to using the Dart Analyzer as the front-end parser.
diff --git a/packages/pigeon/LICENSE b/packages/pigeon/LICENSE
new file mode 100644
index 0000000..bc67b8f
--- /dev/null
+++ b/packages/pigeon/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
new file mode 100644
index 0000000..5283dc0
--- /dev/null
+++ b/packages/pigeon/README.md
@@ -0,0 +1,127 @@
+# Pigeon
+
+**Warning: Pigeon is prerelease, breaking changes may occur.**
+
+Pigeon is a code generator tool to make communication between Flutter and the
+host platform type-safe and easier.
+
+## Supported Platforms
+
+Currently Pigeon only supports generating Objective-C code for usage on iOS and
+Java code for Android.  The Objective-C code is
+[accessible to Swift](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift)
+and the Java code is accessible to Kotlin.
+
+## Runtime Requirements
+
+Pigeon generates all the code that is needed to communicate between Flutter and
+the host platform, there is no extra runtime requirement.  A plugin author
+doesn't need to worry about conflicting versions of Pigeon.
+
+## Usage
+
+### Flutter calling into iOS Steps
+
+1) Add Pigeon as a dev_dependency.
+1) Make a ".dart" file outside of your "lib" directory for defining the communication interface.
+1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code:
+   `flutter pub get` then `flutter pub run pigeon` with suitable arguments.
+1) Add the generated Dart code to `lib` for compilation.
+1) Add the generated Objective-C code to your Xcode project for compilation
+   (e.g. `ios/Runner.xcworkspace` or `.podspec`).
+1) Implement the generated iOS protocol for handling the calls on iOS, set it up
+   as the handler for the messages.
+1) Call the generated Dart methods.
+
+### Flutter calling into Android Steps
+
+1) Add Pigeon as a dev_dependency.
+1) Make a ".dart" file outside of your "lib" directory for defining the communication interface.
+1) Run pigeon on your ".dart" file to generate the required Dart and Java code.
+   `flutter pub get` then `flutter pub run pigeon` with suitable arguments.
+1) Add the generated Dart code to `./lib` for compilation.
+1) Add the generated Java code to your `./android/app/src/main/java` directory for compilation.
+1) Implement the generated Java interface for handling the calls on Android, set it up
+   as the handler for the messages.
+1) Call the generated Dart methods.
+
+### Rules for defining your communication interface
+
+1) The file should contain no method or function definitions, only declarations.
+1) Datatypes are defined as classes with fields of the supported datatypes (see
+   the supported Datatypes section).
+1) Api's should be defined as an `abstract class` with either `HostApi()` or
+   `FlutterApi()` as metadata.  The former being for procedures that are defined
+   on the host platform and the latter for procedures that are defined in Dart.
+1) Method declarations on the Api classes should have one argument and a return
+   value whose types are defined in the file or be `void`.
+
+## Example
+
+See the "Example" tab.  A full working example can also be found in the
+[video_player plugin](https://github.com/flutter/plugins/tree/master/packages/video_player).
+
+## Supported Datatypes
+
+Pigeon uses the `StandardMessageCodec` so it supports any data-type platform
+channels supports
+[[documentation](https://flutter.dev/docs/development/platform-integration/platform-channels#codec)].
+Nested data-types are supported, too.
+
+Note: Generics for List and Map aren't supported yet.
+
+## Asynchronous Handlers
+
+By default Pigeon will generate synchronous handlers for messages.  If you want
+to be able to respond to a message asynchronously you can use the `@async`
+annotation as of version 0.1.20.
+
+Example:
+
+```dart
+class Value {
+  int? number;
+}
+
+@HostApi()
+abstract class Api2Host {
+  @async
+  Value calculate(Value value);
+}
+```
+
+Generates:
+
+```objc
+// Objc
+@protocol Api2Host
+-(void)calculate:(nullable Value *)input 
+      completion:(void(^)(Value *_Nullable, FlutterError *_Nullable))completion;
+@end
+```
+
+```java
+// Java
+public interface Result<T> {
+   void success(T result);
+}
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+public interface Api2Host {
+   void calculate(Value arg, Result<Value> result);
+}
+```
+
+## Null Safety (NNBD)
+
+Right now Pigeon supports generating null-safe code, but it doesn't yet support
+[non-null fields](https://github.com/flutter/flutter/issues/59118).
+
+The default is to generate null-safe code but in order to generate non-null-safe
+code run Pigeon with the extra argument `--no-dart_null_safety`. For example:
+`flutter pub run pigeon --input ./pigeons/messages.dart --no-dart_null_safety --dart_out stdout`.
+
+## Feedback
+
+File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the
+word 'pigeon' clearly in the title and cc **@gaaclarke**.
diff --git a/packages/pigeon/bin/pigeon.dart b/packages/pigeon/bin/pigeon.dart
new file mode 100644
index 0000000..3566bad
--- /dev/null
+++ b/packages/pigeon/bin/pigeon.dart
@@ -0,0 +1,13 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @dart = 2.2
+
+import 'dart:io' show exit;
+
+import 'package:pigeon/pigeon_cl.dart';
+
+Future<void> main(List<String> args) async {
+  exit(await runCommandLine(args));
+}
diff --git a/packages/pigeon/ci/.gitignore b/packages/pigeon/ci/.gitignore
new file mode 100644
index 0000000..72e8ffc
--- /dev/null
+++ b/packages/pigeon/ci/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/packages/pigeon/docs/pigeon_state.png b/packages/pigeon/docs/pigeon_state.png
new file mode 100644
index 0000000..79539ad
--- /dev/null
+++ b/packages/pigeon/docs/pigeon_state.png
Binary files differ
diff --git a/packages/pigeon/docs/pigeon_state.puml b/packages/pigeon/docs/pigeon_state.puml
new file mode 100644
index 0000000..4027ac4
--- /dev/null
+++ b/packages/pigeon/docs/pigeon_state.puml
@@ -0,0 +1,60 @@
+@startuml
+
+[*] -> LaunchIsolate
+
+state LaunchIsolate {
+  [*] --> ParseCommandLineArguments
+  ParseCommandLineArguments --> WriteTemporarySourceCode
+  WriteTemporarySourceCode --> SpawnPigeonIsolate
+  SpawnPigeonIsolate --> WaitForPigeonIsolate
+  WaitForPigeonIsolate --> [*]
+}
+
+LaunchIsolate -> [*]
+
+state PigeonIsolate {
+  [*] --> ParseCommandLineArguments2
+  ParseCommandLineArguments2 --> PrintUsage
+  PrintUsage --> [*]
+  ParseCommandLineArguments2 --> ExecuteConfigurePigeon
+  ExecuteConfigurePigeon --> GenerateAST
+  GenerateAST --> RunGenerators
+  RunGenerators --> PrintErrors
+  PrintErrors --> ReturnStatusCode
+  ReturnStatusCode --> [*]
+
+  state GenerateAST {
+    [*] --> CollectAnnotatedClasses
+    CollectAnnotatedClasses --> CollectAnnotatedClassesDependencies
+    CollectAnnotatedClassesDependencies --> BuildAST
+    BuildAST --> [*]
+  }
+
+  state RunGenerators {
+    state DartTestGeneratorFork <<fork>>
+    state DartTestGeneratorJoin <<join>>
+    [*] --> DartTestGeneratorFork
+    DartTestGeneratorFork --> DartTestGeneratorJoin
+    DartTestGeneratorFork --> DartTestGenerator
+    DartTestGenerator --> DartTestGeneratorJoin
+    DartTestGeneratorJoin --> [*]
+    ||
+    state DartGeneratorFork <<fork>>
+    state DartGeneratorJoin <<join>>
+    [*] --> DartGeneratorFork
+    DartGeneratorFork --> DartGeneratorJoin
+    DartGeneratorFork --> DartGenerator
+    DartGenerator --> DartGeneratorJoin
+    DartGeneratorJoin --> [*]
+    ||
+    state JavaGeneratorFork <<fork>>
+    state JavaGeneratorJoin <<join>>
+    [*] --> JavaGeneratorFork
+    JavaGeneratorFork --> JavaGeneratorJoin
+    JavaGeneratorFork --> JavaGenerator
+    JavaGenerator --> JavaGeneratorJoin
+    JavaGeneratorJoin --> [*]
+  }
+}
+
+@enduml
\ No newline at end of file
diff --git a/packages/pigeon/e2e_tests/test_objc/README.md b/packages/pigeon/e2e_tests/test_objc/README.md
new file mode 100644
index 0000000..d9cf13e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/README.md
@@ -0,0 +1,3 @@
+# test_objc
+
+The testbed app for E2E tests.
diff --git a/packages/pigeon/e2e_tests/test_objc/android/.project b/packages/pigeon/e2e_tests/test_objc/android/.project
new file mode 100644
index 0000000..3964dd3
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>android</name>
+	<comment>Project android created by Buildship.</comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+	</natures>
+</projectDescription>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs b/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..861b6d8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.4.jdk/Contents/Home
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.classpath b/packages/pigeon/e2e_tests/test_objc/android/app/.classpath
new file mode 100644
index 0000000..4a04201
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
+	<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
+	<classpathentry kind="output" path="bin/default"/>
+</classpath>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.project b/packages/pigeon/e2e_tests/test_objc/android/app/.project
new file mode 100644
index 0000000..ac485d7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>app</name>
+	<comment>Project app created by Buildship.</comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+	</natures>
+</projectDescription>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs b/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..b1886ad
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=..
+eclipse.preferences.version=1
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle b/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle
new file mode 100644
index 0000000..ec6262c
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/build.gradle
@@ -0,0 +1,67 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 28
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.test_objc"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.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 {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..8172dab
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.test_objc">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b62dd45
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.test_objc">
+    <!-- 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="test_objc"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            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>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java
new file mode 100644
index 0000000..a6962f7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java
@@ -0,0 +1,232 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package dev.flutter.aaclarke.pigeon;
+
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.util.HashMap;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings("unused")
+public class Pigeon {
+
+  /** Generated class from Pigeon that represents data sent in messages. */
+  public static class SearchReply {
+    private String result;
+
+    public String getResult() {
+      return result;
+    }
+
+    public void setResult(String setterArg) {
+      this.result = setterArg;
+    }
+
+    private String error;
+
+    public String getError() {
+      return error;
+    }
+
+    public void setError(String setterArg) {
+      this.error = setterArg;
+    }
+
+    HashMap toMap() {
+      HashMap<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("result", result);
+      toMapResult.put("error", error);
+      return toMapResult;
+    }
+
+    static SearchReply fromMap(HashMap map) {
+      SearchReply fromMapResult = new SearchReply();
+      Object result = map.get("result");
+      fromMapResult.result = (String) result;
+      Object error = map.get("error");
+      fromMapResult.error = (String) error;
+      return fromMapResult;
+    }
+  }
+
+  /** Generated class from Pigeon that represents data sent in messages. */
+  public static class SearchRequest {
+    private String query;
+
+    public String getQuery() {
+      return query;
+    }
+
+    public void setQuery(String setterArg) {
+      this.query = setterArg;
+    }
+
+    private Long anInt;
+
+    public Long getAnInt() {
+      return anInt;
+    }
+
+    public void setAnInt(Long setterArg) {
+      this.anInt = setterArg;
+    }
+
+    private Boolean aBool;
+
+    public Boolean getABool() {
+      return aBool;
+    }
+
+    public void setABool(Boolean setterArg) {
+      this.aBool = setterArg;
+    }
+
+    HashMap toMap() {
+      HashMap<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("query", query);
+      toMapResult.put("anInt", anInt);
+      toMapResult.put("aBool", aBool);
+      return toMapResult;
+    }
+
+    static SearchRequest fromMap(HashMap map) {
+      SearchRequest fromMapResult = new SearchRequest();
+      Object query = map.get("query");
+      fromMapResult.query = (String) query;
+      Object anInt = map.get("anInt");
+      fromMapResult.anInt =
+          (anInt == null) ? null : ((anInt instanceof Integer) ? (Integer) anInt : (Long) anInt);
+      Object aBool = map.get("aBool");
+      fromMapResult.aBool = (Boolean) aBool;
+      return fromMapResult;
+    }
+  }
+
+  /** Generated class from Pigeon that represents data sent in messages. */
+  public static class Nested {
+    private SearchRequest request;
+
+    public SearchRequest getRequest() {
+      return request;
+    }
+
+    public void setRequest(SearchRequest setterArg) {
+      this.request = setterArg;
+    }
+
+    HashMap toMap() {
+      HashMap<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("request", request);
+      return toMapResult;
+    }
+
+    static Nested fromMap(HashMap map) {
+      Nested fromMapResult = new Nested();
+      Object request = map.get("request");
+      fromMapResult.request = (SearchRequest) request;
+      return fromMapResult;
+    }
+  }
+
+  /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
+  public static class FlutterSearchApi {
+    private final BinaryMessenger binaryMessenger;
+
+    public FlutterSearchApi(BinaryMessenger argBinaryMessenger) {
+      this.binaryMessenger = argBinaryMessenger;
+    }
+
+    public interface Reply<T> {
+      void reply(T reply);
+    }
+
+    public void search(SearchRequest argInput, Reply<SearchReply> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger,
+              "dev.flutter.pigeon.FlutterSearchApi.search",
+              new StandardMessageCodec());
+      HashMap inputMap = argInput.toMap();
+      channel.send(
+          inputMap,
+          channelReply -> {
+            HashMap outputMap = (HashMap) channelReply;
+            @SuppressWarnings("ConstantConditions")
+            SearchReply output = SearchReply.fromMap(outputMap);
+            callback.reply(output);
+          });
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface NestedApi {
+    SearchReply search(Nested arg);
+
+    /** Sets up an instance of `NestedApi` to handle messages through the `binaryMessenger` */
+    static void setup(BinaryMessenger binaryMessenger, NestedApi api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.NestedApi.search", new StandardMessageCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                HashMap<String, HashMap> wrapped = new HashMap<>();
+                try {
+                  @SuppressWarnings("ConstantConditions")
+                  Nested input = Nested.fromMap((HashMap) message);
+                  SearchReply output = api.search(input);
+                  wrapped.put("result", output.toMap());
+                } catch (Exception exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface Api {
+    SearchReply search(SearchRequest arg);
+
+    /** Sets up an instance of `Api` to handle messages through the `binaryMessenger` */
+    static void setup(BinaryMessenger binaryMessenger, Api api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.Api.search", new StandardMessageCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                HashMap<String, HashMap> wrapped = new HashMap<>();
+                try {
+                  @SuppressWarnings("ConstantConditions")
+                  SearchRequest input = SearchRequest.fromMap((HashMap) message);
+                  SearchReply output = api.search(input);
+                  wrapped.put("result", output.toMap());
+                } catch (Exception exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+
+  private static HashMap wrapError(Exception exception) {
+    HashMap<String, Object> errorMap = new HashMap<>();
+    errorMap.put("message", exception.toString());
+    errorMap.put("code", null);
+    errorMap.put("details", null);
+    return errorMap;
+  }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt
new file mode 100644
index 0000000..2907dd8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/kotlin/com/example/test_objc/MainActivity.kt
@@ -0,0 +1,12 @@
+package com.example.test_objc
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugins.GeneratedPluginRegistrant
+
+class MainActivity: FlutterActivity() {
+    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+        GeneratedPluginRegistrant.registerWith(flutterEngine);
+    }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+</resources>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..8172dab
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.test_objc">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pigeon/e2e_tests/test_objc/android/build.gradle b/packages/pigeon/e2e_tests/test_objc/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+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/packages/pigeon/e2e_tests/test_objc/android/gradle.properties b/packages/pigeon/e2e_tests/test_objc/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/pigeon/e2e_tests/test_objc/android/settings.gradle b/packages/pigeon/e2e_tests/test_objc/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..e8efba1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..399e934
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Podfile b/packages/pigeon/e2e_tests/test_objc/ios/Podfile
new file mode 100644
index 0000000..c382d62
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Podfile
@@ -0,0 +1,42 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+  use_modular_headers!
+
+  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_ios_build_settings(target)
+  end
+end
+
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d8ec407
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,743 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		0D89130E23C8F951005E0326 /* dartle.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89130D23C8F951005E0326 /* dartle.m */; };
+		0D89131123C8FB14005E0326 /* MyFlutterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89131023C8FB14005E0326 /* MyFlutterViewController.m */; };
+		0D89131423C8FCD2005E0326 /* MyApi.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D89131323C8FCD2005E0326 /* MyApi.m */; };
+		0DA878B02453583800622EF2 /* MyNestedApi.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DA878AF2453583800622EF2 /* MyNestedApi.m */; };
+		0DD149D423C926D900ABB3D6 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DD149D323C926D900ABB3D6 /* RunnerTests.m */; };
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		5F4169D8C45B4C68F36D3EDA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */; };
+		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 PBXContainerItemProxy section */
+		0DD149D623C926D900ABB3D6 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		0D89130C23C8F951005E0326 /* dartle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dartle.h; sourceTree = "<group>"; };
+		0D89130D23C8F951005E0326 /* dartle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = dartle.m; sourceTree = "<group>"; };
+		0D89130F23C8FB14005E0326 /* MyFlutterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyFlutterViewController.h; sourceTree = "<group>"; };
+		0D89131023C8FB14005E0326 /* MyFlutterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyFlutterViewController.m; sourceTree = "<group>"; };
+		0D89131223C8FCD2005E0326 /* MyApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyApi.h; sourceTree = "<group>"; };
+		0D89131323C8FCD2005E0326 /* MyApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyApi.m; sourceTree = "<group>"; };
+		0DA878AE2453583800622EF2 /* MyNestedApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MyNestedApi.h; sourceTree = "<group>"; };
+		0DA878AF2453583800622EF2 /* MyNestedApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyNestedApi.m; sourceTree = "<group>"; };
+		0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		0DD149D323C926D900ABB3D6 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+		0DD149D523C926D900ABB3D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		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>"; };
+		29BF8B7A9D0A8441FBFB29F1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+		3B0FDBA7A0766A60FD7C1D91 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		3DE0496B7DCC7F565F839974 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; 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>"; };
+		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>"; };
+		C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		0DD149CE23C926D900ABB3D6 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F4169D8C45B4C68F36D3EDA /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		0DD149D223C926D900ABB3D6 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				0DD149D323C926D900ABB3D6 /* RunnerTests.m */,
+				0DD149D523C926D900ABB3D6 /* Info.plist */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
+		508082A21EE4605616986B5E /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				C06A312A3EB22F18BB22C39C /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		5ECC4FFF4A77B35E781D0484 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				3DE0496B7DCC7F565F839974 /* Pods-Runner.debug.xcconfig */,
+				3B0FDBA7A0766A60FD7C1D91 /* Pods-Runner.release.xcconfig */,
+				29BF8B7A9D0A8441FBFB29F1 /* Pods-Runner.profile.xcconfig */,
+			);
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				0DD149D223C926D900ABB3D6 /* RunnerTests */,
+				97C146EF1CF9000F007C117D /* Products */,
+				5ECC4FFF4A77B35E781D0484 /* Pods */,
+				508082A21EE4605616986B5E /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+				0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				0D89130C23C8F951005E0326 /* dartle.h */,
+				0D89130D23C8F951005E0326 /* dartle.m */,
+				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 */,
+				0D89130F23C8FB14005E0326 /* MyFlutterViewController.h */,
+				0D89131023C8FB14005E0326 /* MyFlutterViewController.m */,
+				0D89131223C8FCD2005E0326 /* MyApi.h */,
+				0D89131323C8FCD2005E0326 /* MyApi.m */,
+				0DA878AE2453583800622EF2 /* MyNestedApi.h */,
+				0DA878AF2453583800622EF2 /* MyNestedApi.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 */
+		0DD149D023C926D900ABB3D6 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 0DD149DB23C926D900ABB3D6 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				0DD149CD23C926D900ABB3D6 /* Sources */,
+				0DD149CE23C926D900ABB3D6 /* Frameworks */,
+				0DD149CF23C926D900ABB3D6 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				0DD149D723C926D900ABB3D6 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 0DD149D123C926D900ABB3D6 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				C8AD4DD66A8AA54F9893603E /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				2935EF385A5B690A2E773EB2 /* [CP] Embed Pods Frameworks */,
+			);
+			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 = 1020;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					0DD149D023C926D900ABB3D6 = {
+						CreatedOnToolsVersion = 11.3;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+				0DD149D023C926D900ABB3D6 /* RunnerTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		0DD149CF23C926D900ABB3D6 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		2935EF385A5B690A2E773EB2 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		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\" embed_and_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";
+		};
+		C8AD4DD66A8AA54F9893603E /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		0DD149CD23C926D900ABB3D6 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				0DD149D423C926D900ABB3D6 /* RunnerTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				0D89131423C8FCD2005E0326 /* MyApi.m in Sources */,
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+				0D89130E23C8F951005E0326 /* dartle.m in Sources */,
+				0D89131123C8FB14005E0326 /* MyFlutterViewController.m in Sources */,
+				0DA878B02453583800622EF2 /* MyNestedApi.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		0DD149D723C926D900ABB3D6 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 0DD149D623C926D900ABB3D6 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency 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 */
+		0DD149D823C926D900ABB3D6 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Debug;
+		};
+		0DD149D923C926D900ABB3D6 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Release;
+		};
+		0DD149DA23C926D900ABB3D6 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Profile;
+		};
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				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.testObjc;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = 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 = "$(FLUTTER_BUILD_NUMBER)";
+				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.testObjc;
+				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 = "$(FLUTTER_BUILD_NUMBER)";
+				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.testObjc;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		0DD149DB23C926D900ABB3D6 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				0DD149D823C926D900ABB3D6 /* Debug */,
+				0DD149D923C926D900ABB3D6 /* Release */,
+				0DD149DA23C926D900ABB3D6 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..595f1c6
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "0DD149D023C926D900ABB3D6"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      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>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      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/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
new file mode 100644
index 0000000..b56e406
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1130"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "0DD149D023C926D900ABB3D6"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+   <FileRef
+      location = "group:Pods/Pods.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..70e8393
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#import "AppDelegate.h"
+#import "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/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "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/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "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/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..c8b5b90
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--My Flutter View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="MyFlutterViewController" 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="414" height="896"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="25" y="29"/>
+        </scene>
+    </scenes>
+</document>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist
new file mode 100644
index 0000000..58bf245
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>test_objc</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h
new file mode 100644
index 0000000..68940a9
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.h
@@ -0,0 +1,10 @@
+#import <Foundation/Foundation.h>
+#import "dartle.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Implementation of the Pigeon generated interface Api.
+@interface MyApi : NSObject<ACApi>
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m
new file mode 100644
index 0000000..8dccd43
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyApi.m
@@ -0,0 +1,15 @@
+#import "MyApi.h"
+#import <Flutter/Flutter.h>
+
+@implementation MyApi
+- (ACSearchReply*)search:(ACSearchRequest*)input error:(FlutterError**)error {
+  if ([input.query isEqualToString:@"error"]) {
+    *error = [FlutterError errorWithCode:@"somecode" message:@"somemessage" details:nil];
+    return nil;
+  } else {
+    ACSearchReply* reply = [[ACSearchReply alloc] init];
+    reply.result = [NSString stringWithFormat:@"Hello %@!", input.query];
+    return reply;
+  }
+}
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h
new file mode 100644
index 0000000..43bf4ca
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.h
@@ -0,0 +1,8 @@
+#import <Flutter/Flutter.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MyFlutterViewController : FlutterViewController
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m
new file mode 100644
index 0000000..5994306
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyFlutterViewController.m
@@ -0,0 +1,17 @@
+#import "MyFlutterViewController.h"
+#import "MyApi.h"
+#import "MyNestedApi.h"
+#import "dartle.h"
+
+@interface MyFlutterViewController ()
+@end
+
+@implementation MyFlutterViewController
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+  ACApiSetup(self.engine.binaryMessenger, [[MyApi alloc] init]);
+  ACNestedApiSetup(self.engine.binaryMessenger, [[MyNestedApi alloc] init]);
+}
+
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h
new file mode 100644
index 0000000..4dd1e26
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.h
@@ -0,0 +1,11 @@
+#import <Foundation/Foundation.h>
+#import "dartle.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Implementation of the Pigeon generated interface NestedApi.
+@interface MyNestedApi : NSObject<ACNestedApi>
+- (ACSearchReply *)search:(ACNested *)input error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m
new file mode 100644
index 0000000..d563278
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/MyNestedApi.m
@@ -0,0 +1,9 @@
+#import "MyNestedApi.h"
+
+@implementation MyNestedApi
+- (ACSearchReply *)search:(ACNested *)input error:(FlutterError **)error {
+  ACSearchReply *reply = [[ACSearchReply alloc] init];
+  reply.result = [NSString stringWithFormat:@"Hello %@!", input.request.query];
+  return reply;
+}
+@end
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h
new file mode 100644
index 0000000..3844745
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.h
@@ -0,0 +1,47 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class ACSearchReply;
+@class ACSearchRequest;
+@class ACNested;
+
+@interface ACSearchReply : NSObject
+@property(nonatomic, copy, nullable) NSString *result;
+@property(nonatomic, copy, nullable) NSString *error;
+@end
+
+@interface ACSearchRequest : NSObject
+@property(nonatomic, copy, nullable) NSString *query;
+@property(nonatomic, strong, nullable) NSNumber *anInt;
+@property(nonatomic, strong, nullable) NSNumber *aBool;
+@end
+
+@interface ACNested : NSObject
+@property(nonatomic, strong, nullable) ACSearchRequest *request;
+@end
+
+@interface ACFlutterSearchApi : NSObject
+- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
+- (void)search:(ACSearchRequest *)input completion:(void (^)(ACSearchReply *, NSError *))completion;
+@end
+@protocol ACNestedApi
+- (nullable ACSearchReply *)search:(ACNested *)input error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void ACNestedApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
+                             id<ACNestedApi> _Nullable api);
+
+@protocol ACApi
+- (nullable ACSearchReply *)search:(ACSearchRequest *)input
+                             error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void ACApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACApi> _Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m
new file mode 100644
index 0000000..7b52efa
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/dartle.m
@@ -0,0 +1,157 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "dartle.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) {
+  NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+  if (error) {
+    errorDict = [NSDictionary
+        dictionaryWithObjectsAndKeys:(error.code ? error.code : [NSNull null]), @"code",
+                                     (error.message ? error.message : [NSNull null]), @"message",
+                                     (error.details ? error.details : [NSNull null]), @"details",
+                                     nil];
+  }
+  return [NSDictionary dictionaryWithObjectsAndKeys:(result ? result : [NSNull null]), @"result",
+                                                    errorDict, @"error", nil];
+}
+
+@interface ACSearchReply ()
++ (ACSearchReply *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+@interface ACSearchRequest ()
++ (ACSearchRequest *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+@interface ACNested ()
++ (ACNested *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+
+@implementation ACSearchReply
++ (ACSearchReply *)fromMap:(NSDictionary *)dict {
+  ACSearchReply *result = [[ACSearchReply alloc] init];
+  result.result = dict[@"result"];
+  if ((NSNull *)result.result == [NSNull null]) {
+    result.result = nil;
+  }
+  result.error = dict[@"error"];
+  if ((NSNull *)result.error == [NSNull null]) {
+    result.error = nil;
+  }
+  return result;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:(self.result ? self.result : [NSNull null]), @"result",
+                                   (self.error ? self.error : [NSNull null]), @"error", nil];
+}
+@end
+
+@implementation ACSearchRequest
++ (ACSearchRequest *)fromMap:(NSDictionary *)dict {
+  ACSearchRequest *result = [[ACSearchRequest alloc] init];
+  result.query = dict[@"query"];
+  if ((NSNull *)result.query == [NSNull null]) {
+    result.query = nil;
+  }
+  result.anInt = dict[@"anInt"];
+  if ((NSNull *)result.anInt == [NSNull null]) {
+    result.anInt = nil;
+  }
+  result.aBool = dict[@"aBool"];
+  if ((NSNull *)result.aBool == [NSNull null]) {
+    result.aBool = nil;
+  }
+  return result;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:(self.query ? self.query : [NSNull null]), @"query",
+                                   (self.anInt ? self.anInt : [NSNull null]), @"anInt",
+                                   (self.aBool ? self.aBool : [NSNull null]), @"aBool", nil];
+}
+@end
+
+@implementation ACNested
++ (ACNested *)fromMap:(NSDictionary *)dict {
+  ACNested *result = [[ACNested alloc] init];
+  result.request = [ACSearchRequest fromMap:dict[@"request"]];
+  if ((NSNull *)result.request == [NSNull null]) {
+    result.request = nil;
+  }
+  return result;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:(self.request ? [self.request toMap] : [NSNull null]),
+                                   @"request", nil];
+}
+@end
+
+@interface ACFlutterSearchApi ()
+@property(nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;
+@end
+
+@implementation ACFlutterSearchApi
+- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger {
+  self = [super init];
+  if (self) {
+    self.binaryMessenger = binaryMessenger;
+  }
+  return self;
+}
+
+- (void)search:(ACSearchRequest *)input
+    completion:(void (^)(ACSearchReply *, NSError *))completion {
+  FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
+      messageChannelWithName:@"dev.flutter.pigeon.FlutterSearchApi.search"
+             binaryMessenger:self.binaryMessenger];
+  NSDictionary *inputMap = [input toMap];
+  [channel sendMessage:inputMap
+                 reply:^(id reply) {
+                   NSDictionary *outputMap = reply;
+                   ACSearchReply *output = [ACSearchReply fromMap:outputMap];
+                   completion(output, nil);
+                 }];
+}
+@end
+void ACNestedApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACNestedApi> api) {
+  {
+    FlutterBasicMessageChannel *channel =
+        [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NestedApi.search"
+                                           binaryMessenger:binaryMessenger];
+    if (api) {
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        FlutterError *error;
+        ACNested *input = [ACNested fromMap:message];
+        ACSearchReply *output = [api search:input error:&error];
+        callback(wrapResult([output toMap], error));
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
+void ACApiSetup(id<FlutterBinaryMessenger> binaryMessenger, id<ACApi> api) {
+  {
+    FlutterBasicMessageChannel *channel =
+        [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api.search"
+                                           binaryMessenger:binaryMessenger];
+    if (api) {
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        FlutterError *error;
+        ACSearchRequest *input = [ACSearchRequest fromMap:message];
+        ACSearchReply *output = [api search:input error:&error];
+        callback(wrapResult([output toMap], error));
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m b/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#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/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/Info.plist
@@ -0,0 +1,22 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..9614c65
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,4 @@
+#import <XCTest/XCTest.h>
+#import <e2e/E2EIosTest.h>
+
+E2E_IOS_RUNNER(RunnerTests)
diff --git a/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart b/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart
new file mode 100644
index 0000000..9f0db48
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/lib/dartle.dart
@@ -0,0 +1,147 @@
+// Autogenerated from Pigeon (v0.1.2), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import
+import 'dart:async';
+import 'package:flutter/services.dart';
+
+class SearchReply {
+  String result;
+  String error;
+  // ignore: unused_element
+  Map<dynamic, dynamic> _toMap() {
+    final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+    pigeonMap['result'] = result;
+    pigeonMap['error'] = error;
+    return pigeonMap;
+  }
+
+  // ignore: unused_element
+  static SearchReply _fromMap(Map<dynamic, dynamic> pigeonMap) {
+    final SearchReply result = SearchReply();
+    result.result = pigeonMap['result'];
+    result.error = pigeonMap['error'];
+    return result;
+  }
+}
+
+class SearchRequest {
+  String query;
+  int anInt;
+  bool aBool;
+  // ignore: unused_element
+  Map<dynamic, dynamic> _toMap() {
+    final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+    pigeonMap['query'] = query;
+    pigeonMap['anInt'] = anInt;
+    pigeonMap['aBool'] = aBool;
+    return pigeonMap;
+  }
+
+  // ignore: unused_element
+  static SearchRequest _fromMap(Map<dynamic, dynamic> pigeonMap) {
+    final SearchRequest result = SearchRequest();
+    result.query = pigeonMap['query'];
+    result.anInt = pigeonMap['anInt'];
+    result.aBool = pigeonMap['aBool'];
+    return result;
+  }
+}
+
+class Nested {
+  SearchRequest request;
+  // ignore: unused_element
+  Map<dynamic, dynamic> _toMap() {
+    final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
+    pigeonMap['request'] = request._toMap();
+    return pigeonMap;
+  }
+
+  // ignore: unused_element
+  static Nested _fromMap(Map<dynamic, dynamic> pigeonMap) {
+    final Nested result = Nested();
+    result.request = SearchRequest._fromMap(pigeonMap['request']);
+    return result;
+  }
+}
+
+abstract class FlutterSearchApi {
+  SearchReply search(SearchRequest arg);
+  static void setup(FlutterSearchApi api) {
+    {
+      const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+          'dev.flutter.pigeon.FlutterSearchApi.search', StandardMessageCodec());
+      channel.setMessageHandler((dynamic message) async {
+        final Map<dynamic, dynamic> mapMessage =
+            message as Map<dynamic, dynamic>;
+        final SearchRequest input = SearchRequest._fromMap(mapMessage);
+        final SearchReply output = api.search(input);
+        return output._toMap();
+      });
+    }
+  }
+}
+
+class NestedApi {
+  Future<SearchReply> search(Nested arg) async {
+    final Map<dynamic, dynamic> requestMap = arg._toMap();
+    const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+        'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+
+    final Map<dynamic, dynamic> replyMap = await channel.send(requestMap);
+    if (replyMap == null) {
+      throw PlatformException(
+          code: 'channel-error',
+          message: 'Unable to establish connection on channel.',
+          details: null);
+    } else if (replyMap['error'] != null) {
+      final Map<dynamic, dynamic> error = replyMap['error'];
+      throw PlatformException(
+          code: error['code'],
+          message: error['message'],
+          details: error['details']);
+    } else {
+      return SearchReply._fromMap(replyMap['result']);
+    }
+  }
+}
+
+class Api {
+  Future<SearchReply> search(SearchRequest arg) async {
+    final Map<dynamic, dynamic> requestMap = arg._toMap();
+    const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+        'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+
+    final Map<dynamic, dynamic> replyMap = await channel.send(requestMap);
+    if (replyMap == null) {
+      throw PlatformException(
+          code: 'channel-error',
+          message: 'Unable to establish connection on channel.',
+          details: null);
+    } else if (replyMap['error'] != null) {
+      final Map<dynamic, dynamic> error = replyMap['error'];
+      throw PlatformException(
+          code: error['code'],
+          message: error['message'],
+          details: error['details']);
+    } else {
+      return SearchReply._fromMap(replyMap['result']);
+    }
+  }
+}
+
+abstract class TestHostApi {
+  SearchReply search(SearchRequest arg);
+  static void setup(TestHostApi api) {
+    {
+      const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
+          'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+      channel.setMockMessageHandler((dynamic message) async {
+        final Map<dynamic, dynamic> mapMessage =
+            message as Map<dynamic, dynamic>;
+        final SearchRequest input = SearchRequest._fromMap(mapMessage);
+        final SearchReply output = api.search(input);
+        return <dynamic, dynamic>{'result': output._toMap()};
+      });
+    }
+  }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/lib/main.dart b/packages/pigeon/e2e_tests/test_objc/lib/main.dart
new file mode 100644
index 0000000..4e74743
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/lib/main.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'dartle.dart';
+
+class _MyFlutterSearchApi extends FlutterSearchApi {
+  @override
+  SearchReply search(SearchRequest input) {
+    return SearchReply()..result = 'Hello ${input.query}, from Flutter';
+  }
+}
+
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+  FlutterSearchApi.setup(_MyFlutterSearchApi());
+  runApp(const MyApp());
+}
+
+/// Main widget for the tests.
+class MyApp extends StatelessWidget {
+  /// Creates the main widget for the tests.
+  const MyApp({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      home: const _MyHomePage(title: 'Flutter Demo Home Page'),
+    );
+  }
+}
+
+class _MyHomePage extends StatefulWidget {
+  const _MyHomePage({Key key, this.title}) : super(key: key);
+  final String title;
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<_MyHomePage> {
+  String _message = '';
+
+  Future<void> _incrementCounter() async {
+    final SearchRequest request = SearchRequest()..query = 'Aaron';
+    final Api api = Api();
+    final SearchReply reply = await api.search(request);
+    setState(() {
+      _message = reply.result;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            const Text(
+              'Message:',
+            ),
+            Text(
+              _message,
+              style: Theme.of(context).textTheme.headline1,
+            ),
+          ],
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _incrementCounter,
+        tooltip: 'Increment',
+        child: const Icon(Icons.cake),
+      ), // This trailing comma makes auto-formatting nicer for build methods.
+    );
+  }
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/pubspec.yaml b/packages/pigeon/e2e_tests/test_objc/pubspec.yaml
new file mode 100644
index 0000000..b66d931
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/pubspec.yaml
@@ -0,0 +1,71 @@
+name: test_objc
+description: A new Flutter project.
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^0.1.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  e2e: ^0.4.0
+  flutter_test:
+    sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/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 material Icons class.
+  uses-material-design: true
+
+  # To add assets to your application, add an assets section, like this:
+  # assets:
+  #  - images/a_dot_burr.jpeg
+  #  - images/a_dot_ham.jpeg
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # 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: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/packages/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart b/packages/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart
new file mode 100644
index 0000000..e97639a
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/test_driver/e2e_test.dart
@@ -0,0 +1,31 @@
+import 'package:e2e/e2e.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:test_objc/dartle.dart';
+
+void main() {
+  E2EWidgetsFlutterBinding.ensureInitialized();
+  testWidgets('simple call', (WidgetTester tester) async {
+    final SearchRequest request = SearchRequest()..query = 'Aaron';
+    final Api api = Api();
+    final SearchReply reply = await api.search(request);
+    expect(reply.result, equals('Hello Aaron!'));
+  });
+
+  testWidgets('simple nested', (WidgetTester tester) async {
+    final SearchRequest request = SearchRequest()..query = 'Aaron';
+    final Nested nested = Nested()..request = request;
+    final NestedApi api = NestedApi();
+    final SearchReply reply = await api.search(nested);
+    expect(reply.result, equals('Hello Aaron!'));
+  });
+
+  testWidgets('throws', (WidgetTester tester) async {
+    final SearchRequest request = SearchRequest()..query = 'error';
+    final Api api = Api();
+    SearchReply reply;
+    expect(() async {
+      reply = await api.search(request);
+    }, throwsException);
+    expect(reply, isNull);
+  });
+}
diff --git a/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart b/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart
new file mode 100644
index 0000000..300082d
--- /dev/null
+++ b/packages/pigeon/e2e_tests/test_objc/test_driver/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:test_objc/main.dart';
+
+void main() {
+  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    await tester.pumpWidget(const MyApp());
+
+    // Verify that our counter starts at 0.
+    expect(find.text('0'), findsOneWidget);
+    expect(find.text('1'), findsNothing);
+
+    // Tap the '+' icon and trigger a frame.
+    await tester.tap(find.byIcon(Icons.add));
+    await tester.pump();
+
+    // Verify that our counter has incremented.
+    expect(find.text('0'), findsNothing);
+    expect(find.text('1'), findsOneWidget);
+  });
+}
diff --git a/packages/pigeon/example/README.md b/packages/pigeon/example/README.md
new file mode 100644
index 0000000..a6d18e3
--- /dev/null
+++ b/packages/pigeon/example/README.md
@@ -0,0 +1,126 @@
+# Pigeon Examples
+
+## HostApi Example
+
+This example gives an overview of how to use Pigeon to call into the
+host-platform from Flutter.
+
+### message.dart
+
+This is the Pigeon file that describes the interface that will be used to call
+from Flutter to the host-platform.
+
+```dart
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+  String? query;
+}
+
+class SearchReply {
+  String? result;
+}
+
+@HostApi()
+abstract class Api {
+  SearchReply search(SearchRequest request);
+}
+```
+
+### invocation
+
+This is the call to Pigeon that will injest `message.dart` and generate the code
+for iOS and Android.
+
+```sh
+flutter pub run pigeon \
+  --input pigeons/message.dart \
+  --dart_out lib/pigeon.dart \
+  --objc_header_out ios/Runner/pigeon.h \
+  --objc_source_out ios/Runner/pigeon.m \
+  --java_out ./android/app/src/main/java/dev/flutter/pigeon/Pigeon.java \
+  --java_package "dev.flutter.pigeon"
+```
+
+### AppDelegate.m
+
+This is the code that will use the generated Objective-C code to recieve calls
+from Flutter.
+
+```objc
+#import "AppDelegate.h"
+#import <Flutter/Flutter.h>
+#import "pigeon.h"
+
+@interface MyApi : NSObject <Api>
+@end
+
+@implementation MyApi
+-(SearchReply*)search:(SearchRequest*)request error:(FlutterError **)error {
+  SearchReply *reply = [[SearchReply alloc] init];
+  reply.result =
+      [NSString stringWithFormat:@"Hi %@!", request.query];
+  return reply;
+}
+@end
+
+- (BOOL)application:(UIApplication *)application 
+didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
+  MyApi *api = [[MyApi alloc] init];
+  ApiSetup(getFlutterEngine().binaryMessenger, api);
+  return YES;
+}
+```
+
+### StartActivity.java
+
+This is the code that will use the generated Java code to receive calls from Flutter.
+
+```java
+import dev.flutter.pigeon.Pigeon;
+
+public class StartActivity extends Activity {
+  private class MyApi extends Pigeon.Api {
+    Pigeon.SearchReply search(Pigeon.SearchRequest request) {
+      Pigeon.SearchReply reply = new Pigeon.SearchReply();
+      reply.result = String.format("Hi %s!", request.query);
+      return reply;
+    }
+  }
+
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    Pigeon.SetupApi(getBinaryMessenger(), new MyApi());
+  }
+}
+```
+
+### test.dart
+
+This is the Dart code that will call into the host-platform using the generated
+Dart code.
+
+```dart
+import 'pigeon.dart';
+
+void main() {
+  testWidgets("test pigeon", (WidgetTester tester) async {
+    SearchRequest request = SearchRequest()..query = "Aaron";
+    Api api = Api();
+    SearchReply reply = await api.search(request);
+    expect(reply.result, equals("Hi Aaron!"));
+  });
+}
+
+```
+
+## Swift / Kotlin Plugin Example
+
+A downloadable example of using Pigeon to create a Flutter Plugin with Swift and
+Kotlin can be found at
+[gaaclarke/flutter_plugin_example](https://github.com/gaaclarke/pigeon_plugin_example).
+
+## Swift / Kotlin Add-to-app Example
+
+A full example of using Pigeon for add-to-app with Swift on iOS can be found at
+[samples/add_to_app/books](https://github.com/flutter/samples/tree/master/add_to_app/books).
diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart
new file mode 100644
index 0000000..95fb026
--- /dev/null
+++ b/packages/pigeon/lib/ast.dart
@@ -0,0 +1,121 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Enum that represents where an [Api] is located, on the host or Flutter.
+enum ApiLocation {
+  /// The API is for calling functions defined on the host.
+  host,
+
+  /// The API is for calling functions defined in Flutter.
+  flutter,
+}
+
+/// Superclass for all AST nodes.
+class Node {}
+
+/// Represents a method on an [Api].
+class Method extends Node {
+  /// Parametric constructor for [Method].
+  Method({
+    required this.name,
+    required this.returnType,
+    required this.argType,
+    this.isAsynchronous = false,
+  });
+
+  /// The name of the method.
+  String name;
+
+  /// The data-type of the return value.
+  String returnType;
+
+  /// The data-type of the argument.
+  String argType;
+
+  /// Whether the receiver of this method is expected to return synchronously or not.
+  bool isAsynchronous;
+}
+
+/// Represents a collection of [Method]s that are hosted on a given [location].
+class Api extends Node {
+  /// Parametric constructor for [Api].
+  Api({
+    required this.name,
+    required this.location,
+    required this.methods,
+    this.dartHostTestHandler,
+  });
+
+  /// The name of the API.
+  String name;
+
+  /// Where the API's implementation is located, host or Flutter.
+  ApiLocation location;
+
+  /// List of methods inside the API.
+  List<Method> methods;
+
+  /// The name of the Dart test interface to generate to help with testing.
+  String? dartHostTestHandler;
+}
+
+/// Represents a field on a [Class].
+class Field extends Node {
+  /// Parametric constructor for [Field].
+  Field({
+    required this.name,
+    required this.dataType,
+  });
+
+  /// The name of the field.
+  String name;
+
+  /// The data-type of the field (ex 'String' or 'int').
+  String dataType;
+
+  @override
+  String toString() {
+    return '(Field name:$name)';
+  }
+}
+
+/// Represents a class with [Field]s.
+class Class extends Node {
+  /// Parametric constructor for [Class].
+  Class({
+    required this.name,
+    required this.fields,
+  });
+
+  /// The name of the class.
+  String name;
+
+  /// All the fields contained in the class.
+  List<Field> fields;
+
+  @override
+  String toString() {
+    return '(Class name:$name fields:$fields)';
+  }
+}
+
+/// Top-level node for the AST.
+class Root extends Node {
+  /// Parametric constructor for [Root].
+  Root({
+    required this.classes,
+    required this.apis,
+  });
+
+  /// All the classes contained in the AST.
+  List<Class> classes;
+
+  /// All the API's contained in the AST.
+  List<Api> apis;
+
+  @override
+  String toString() {
+    return '(Root classes:$classes apis:$apis)';
+  }
+}
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
new file mode 100644
index 0000000..56fdc22
--- /dev/null
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -0,0 +1,332 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Options that control how Dart code will be generated.
+class DartOptions {
+  /// Constructor for DartOptions.
+  DartOptions({this.isNullSafe = true});
+
+  /// Determines if the generated code has null safety annotations (Dart >=2.12 required).
+  bool isNullSafe;
+}
+
+String _escapeForDartSingleQuotedString(String raw) {
+  return raw
+      .replaceAll(r'\', r'\\')
+      .replaceAll(r'$', r'\$')
+      .replaceAll(r"'", r"\'");
+}
+
+void _writeHostApi(DartOptions opt, Indent indent, Api api) {
+  assert(api.location == ApiLocation.host);
+  final String nullTag = opt.isNullSafe ? '?' : '';
+  final String unwrapOperator = opt.isNullSafe ? '!' : '';
+  bool first = true;
+  indent.write('class ${api.name} ');
+  indent.scoped('{', '}', () {
+    indent.format('''
+/// Constructor for [${api.name}].  The [binaryMessenger] named argument is
+/// available for dependency injection.  If it is left null, the default
+/// BinaryMessenger will be used which routes to the host platform.
+${api.name}({BinaryMessenger$nullTag binaryMessenger}) : _binaryMessenger = binaryMessenger;
+
+final BinaryMessenger$nullTag _binaryMessenger;
+''');
+
+    for (final Method func in api.methods) {
+      if (!first) {
+        indent.writeln('');
+      } else {
+        first = false;
+      }
+      String argSignature = '';
+      String sendArgument = 'null';
+      String? encodedDeclaration;
+      if (func.argType != 'void') {
+        argSignature = '${func.argType} arg';
+        sendArgument = 'encoded';
+        encodedDeclaration = 'final Object encoded = arg.encode();';
+      }
+      indent.write(
+        'Future<${func.returnType}> ${func.name}($argSignature) async ',
+      );
+      indent.scoped('{', '}', () {
+        if (encodedDeclaration != null) {
+          indent.writeln(encodedDeclaration);
+        }
+        final String channelName = makeChannelName(api, func);
+        indent.writeln(
+            'final BasicMessageChannel<Object$nullTag> channel = BasicMessageChannel<Object$nullTag>(');
+        indent.nest(2, () {
+          indent.writeln(
+            '\'$channelName\', const StandardMessageCodec(), binaryMessenger: _binaryMessenger);',
+          );
+        });
+        final String returnStatement = func.returnType == 'void'
+            ? '// noop'
+            : 'return ${func.returnType}.decode(replyMap[\'${Keys.result}\']$unwrapOperator);';
+        indent.format('''
+final Map<Object$nullTag, Object$nullTag>$nullTag replyMap =\n\t\tawait channel.send($sendArgument) as Map<Object$nullTag, Object$nullTag>$nullTag;
+if (replyMap == null) {
+\tthrow PlatformException(
+\t\tcode: 'channel-error',
+\t\tmessage: 'Unable to establish connection on channel.',
+\t\tdetails: null,
+\t);
+} else if (replyMap['error'] != null) {
+\tfinal Map<Object$nullTag, Object$nullTag> error = (replyMap['${Keys.error}'] as Map<Object$nullTag, Object$nullTag>$nullTag)$unwrapOperator;
+\tthrow PlatformException(
+\t\tcode: (error['${Keys.errorCode}'] as String$nullTag)$unwrapOperator,
+\t\tmessage: error['${Keys.errorMessage}'] as String$nullTag,
+\t\tdetails: error['${Keys.errorDetails}'],
+\t);
+} else {
+\t$returnStatement
+}''');
+      });
+    }
+  });
+}
+
+void _writeFlutterApi(
+  DartOptions opt,
+  Indent indent,
+  Api api, {
+  String Function(Method)? channelNameFunc,
+  bool isMockHandler = false,
+}) {
+  assert(api.location == ApiLocation.flutter);
+  final String nullTag = opt.isNullSafe ? '?' : '';
+  final String unwrapOperator = opt.isNullSafe ? '!' : '';
+  indent.write('abstract class ${api.name} ');
+  indent.scoped('{', '}', () {
+    for (final Method func in api.methods) {
+      final bool isAsync = func.isAsynchronous;
+      final String returnType =
+          isAsync ? 'Future<${func.returnType}>' : func.returnType;
+      final String argSignature =
+          func.argType == 'void' ? '' : '${func.argType} arg';
+      indent.writeln('$returnType ${func.name}($argSignature);');
+    }
+    indent.write('static void setup(${api.name}$nullTag api) ');
+    indent.scoped('{', '}', () {
+      for (final Method func in api.methods) {
+        indent.write('');
+        indent.scoped('{', '}', () {
+          indent.writeln(
+            'const BasicMessageChannel<Object$nullTag> channel = BasicMessageChannel<Object$nullTag>(',
+          );
+          final String channelName = channelNameFunc == null
+              ? makeChannelName(api, func)
+              : channelNameFunc(func);
+          indent.nest(2, () {
+            indent.writeln(
+              '\'$channelName\', StandardMessageCodec());',
+            );
+          });
+          final String messageHandlerSetter =
+              isMockHandler ? 'setMockMessageHandler' : 'setMessageHandler';
+          indent.write('if (api == null) ');
+          indent.scoped('{', '}', () {
+            indent.writeln('channel.$messageHandlerSetter(null);');
+          }, addTrailingNewline: false);
+          indent.add(' else ');
+          indent.scoped('{', '}', () {
+            indent.write(
+              'channel.$messageHandlerSetter((Object$nullTag message) async ',
+            );
+            indent.scoped('{', '});', () {
+              final String argType = func.argType;
+              final String returnType = func.returnType;
+              final bool isAsync = func.isAsynchronous;
+              final String emptyReturnStatement = isMockHandler
+                  ? 'return <Object$nullTag, Object$nullTag>{};'
+                  : func.returnType == 'void'
+                      ? 'return;'
+                      : 'return null;';
+              String call;
+              if (argType == 'void') {
+                indent.writeln('// ignore message');
+                call = 'api.${func.name}()';
+              } else {
+                indent.writeln(
+                  'assert(message != null, \'Argument for $channelName was null. Expected $argType.\');',
+                );
+                indent.writeln(
+                  'final $argType input = $argType.decode(message$unwrapOperator);',
+                );
+                call = 'api.${func.name}(input)';
+              }
+              if (returnType == 'void') {
+                if (isAsync) {
+                  indent.writeln('await $call;');
+                } else {
+                  indent.writeln('$call;');
+                }
+                indent.writeln(emptyReturnStatement);
+              } else {
+                if (isAsync) {
+                  indent.writeln('final $returnType output = await $call;');
+                } else {
+                  indent.writeln('final $returnType output = $call;');
+                }
+                const String returnExpression = 'output.encode()';
+                final String returnStatement = isMockHandler
+                    ? 'return <Object$nullTag, Object$nullTag>{\'${Keys.result}\': $returnExpression};'
+                    : 'return $returnExpression;';
+                indent.writeln(returnStatement);
+              }
+            });
+          });
+        });
+      }
+    });
+  });
+}
+
+String _addGenericTypes(String dataType, String nullTag) {
+  switch (dataType) {
+    case 'List':
+      return 'List<Object$nullTag>$nullTag';
+    case 'Map':
+      return 'Map<Object$nullTag, Object$nullTag>$nullTag';
+    default:
+      return '$dataType$nullTag';
+  }
+}
+
+/// Generates Dart source code for the given AST represented by [root],
+/// outputting the code to [sink].
+void generateDart(DartOptions opt, Root root, StringSink sink) {
+  final String nullTag = opt.isNullSafe ? '?' : '';
+  final String unwrapOperator = opt.isNullSafe ? '!' : '';
+  final List<String> customClassNames =
+      root.classes.map((Class x) => x.name).toList();
+  final Indent indent = Indent(sink);
+  indent.writeln('// $generatedCodeWarning');
+  indent.writeln('// $seeAlsoWarning');
+  indent.writeln(
+    '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types',
+  );
+  indent.writeln('// @dart = ${opt.isNullSafe ? '2.12' : '2.8'}');
+  indent.writeln('import \'dart:async\';');
+  indent.writeln(
+    'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;',
+  );
+  indent.writeln('');
+  indent.writeln('import \'package:flutter/services.dart\';');
+  for (final Class klass in root.classes) {
+    indent.writeln('');
+    sink.write('class ${klass.name} ');
+    indent.scoped('{', '}', () {
+      for (final Field field in klass.fields) {
+        final String datatype = _addGenericTypes(field.dataType, nullTag);
+        indent.writeln('$datatype ${field.name};');
+      }
+      if (klass.fields.isNotEmpty) {
+        indent.writeln('');
+      }
+      indent.write('Object encode() ');
+      indent.scoped('{', '}', () {
+        indent.writeln(
+          'final Map<Object$nullTag, Object$nullTag> pigeonMap = <Object$nullTag, Object$nullTag>{};',
+        );
+        for (final Field field in klass.fields) {
+          indent.write('pigeonMap[\'${field.name}\'] = ');
+          if (customClassNames.contains(field.dataType)) {
+            indent.addln(
+              '${field.name} == null ? null : ${field.name}$unwrapOperator.encode();',
+            );
+          } else {
+            indent.addln('${field.name};');
+          }
+        }
+        indent.writeln('return pigeonMap;');
+      });
+      indent.writeln('');
+      indent.write(
+        'static ${klass.name} decode(Object message) ',
+      );
+      indent.scoped('{', '}', () {
+        indent.writeln(
+          'final Map<Object$nullTag, Object$nullTag> pigeonMap = message as Map<Object$nullTag, Object$nullTag>;',
+        );
+        indent.writeln('return ${klass.name}()');
+        indent.nest(1, () {
+          for (int index = 0; index < klass.fields.length; index += 1) {
+            final Field field = klass.fields[index];
+            indent.write('..${field.name} = ');
+            if (customClassNames.contains(field.dataType)) {
+              indent.format('''
+pigeonMap['${field.name}'] != null
+\t\t? ${field.dataType}.decode(pigeonMap['${field.name}']$unwrapOperator)
+\t\t: null''', leadingSpace: false, trailingNewline: false);
+            } else {
+              indent.add(
+                'pigeonMap[\'${field.name}\'] as ${_addGenericTypes(field.dataType, nullTag)}',
+              );
+            }
+            indent.addln(index == klass.fields.length - 1 ? ';' : '');
+          }
+        });
+      });
+    });
+  }
+  for (final Api api in root.apis) {
+    indent.writeln('');
+    if (api.location == ApiLocation.host) {
+      _writeHostApi(opt, indent, api);
+    } else if (api.location == ApiLocation.flutter) {
+      _writeFlutterApi(opt, indent, api);
+    }
+  }
+}
+
+/// Generates Dart source code for test support libraries based on the
+/// given AST represented by [root], outputting the code to [sink].
+void generateTestDart(
+  DartOptions opt,
+  Root root,
+  StringSink sink,
+  String mainDartFile,
+) {
+  final Indent indent = Indent(sink);
+  indent.writeln('// $generatedCodeWarning');
+  indent.writeln('// $seeAlsoWarning');
+  indent.writeln(
+    '// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import',
+  );
+  indent.writeln('// @dart = ${opt.isNullSafe ? '2.12' : '2.8'}');
+  indent.writeln('import \'dart:async\';');
+  indent.writeln(
+    'import \'dart:typed_data\' show Uint8List, Int32List, Int64List, Float64List;',
+  );
+  indent.writeln('import \'package:flutter/services.dart\';');
+  indent.writeln('import \'package:flutter_test/flutter_test.dart\';');
+  indent.writeln('');
+  indent.writeln(
+    'import \'${_escapeForDartSingleQuotedString(mainDartFile)}\';',
+  );
+  for (final Api api in root.apis) {
+    if (api.location == ApiLocation.host && api.dartHostTestHandler != null) {
+      final Api mockApi = Api(
+        name: api.dartHostTestHandler!,
+        methods: api.methods,
+        location: ApiLocation.flutter,
+        dartHostTestHandler: api.dartHostTestHandler,
+      );
+      indent.writeln('');
+      _writeFlutterApi(
+        opt,
+        indent,
+        mockApi,
+        channelNameFunc: (Method func) => makeChannelName(api, func),
+        isMockHandler: true,
+      );
+    }
+  }
+}
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
new file mode 100644
index 0000000..44500fc
--- /dev/null
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -0,0 +1,208 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:mirrors';
+import 'ast.dart';
+
+/// The current version of pigeon. This must match the version in pubspec.yaml.
+const String pigeonVersion = '0.2.1';
+
+/// Read all the content from [stdin] to a String.
+String readStdin() {
+  final List<int> bytes = <int>[];
+  int byte = stdin.readByteSync();
+  while (byte >= 0) {
+    bytes.add(byte);
+    byte = stdin.readByteSync();
+  }
+  return utf8.decode(bytes);
+}
+
+/// A helper class for managing indentation, wrapping a [StringSink].
+class Indent {
+  /// Constructor which takes a [StringSink] [Ident] will wrap.
+  Indent(this._sink);
+
+  int _count = 0;
+  final StringSink _sink;
+
+  /// String used for newlines (ex "\n").
+  final String newline = '\n';
+
+  /// String used to represent a tab.
+  final String tab = '  ';
+
+  /// Increase the indentation level.
+  void inc([int level = 1]) {
+    _count += level;
+  }
+
+  /// Decrement the indentation level.
+  void dec([int level = 1]) {
+    _count -= level;
+  }
+
+  /// Returns the String representing the current indentation.
+  String str() {
+    String result = '';
+    for (int i = 0; i < _count; i++) {
+      result += tab;
+    }
+    return result;
+  }
+
+  /// Replaces the newlines and tabs of input and adds it to the stream.
+  void format(String input,
+      {bool leadingSpace = true, bool trailingNewline = true}) {
+    final List<String> lines = input.split('\n');
+    for (int i = 0; i < lines.length; ++i) {
+      final String line = lines[i];
+      if (i == 0 && !leadingSpace) {
+        addln(line.replaceAll('\t', tab));
+      } else if (i == lines.length - 1 && !trailingNewline) {
+        write(line.replaceAll('\t', tab));
+      } else {
+        writeln(line.replaceAll('\t', tab));
+      }
+    }
+  }
+
+  /// Scoped increase of the ident level.  For the execution of [func] the
+  /// indentation will be incremented.
+  void scoped(
+    String? begin,
+    String? end,
+    Function func, {
+    bool addTrailingNewline = true,
+  }) {
+    if (begin != null) {
+      _sink.write(begin + newline);
+    }
+    nest(1, func);
+    if (end != null) {
+      _sink.write(str() + end);
+      if (addTrailingNewline) {
+        _sink.write(newline);
+      }
+    }
+  }
+
+  /// Like `scoped` but writes the current indentation level.
+  void writeScoped(
+    String? begin,
+    String end,
+    Function func, {
+    bool addTrailingNewline = true,
+  }) {
+    scoped(str() + (begin ?? ''), end, func,
+        addTrailingNewline: addTrailingNewline);
+  }
+
+  /// Scoped increase of the ident level.  For the execution of [func] the
+  /// indentation will be incremented by the given amount.
+  void nest(int count, Function func) {
+    inc(count);
+    func();
+    dec(count);
+  }
+
+  /// Add [text] with indentation and a newline.
+  void writeln(String text) {
+    if (text.isEmpty) {
+      _sink.write(newline);
+    } else {
+      _sink.write(str() + text + newline);
+    }
+  }
+
+  /// Add [text] with indentation.
+  void write(String text) {
+    _sink.write(str() + text);
+  }
+
+  /// Add [text] with a newline.
+  void addln(String text) {
+    _sink.write(text + newline);
+  }
+
+  /// Just adds [text].
+  void add(String text) {
+    _sink.write(text);
+  }
+}
+
+/// Create the generated channel name for a [func] on a [api].
+String makeChannelName(Api api, Method func) {
+  return 'dev.flutter.pigeon.${api.name}.${func.name}';
+}
+
+/// Represents the mapping of a Dart datatype to a Host datatype.
+class HostDatatype {
+  /// Parametric constructor for HostDatatype.
+  HostDatatype({
+    required this.datatype,
+    required this.isBuiltin,
+  });
+
+  /// The [String] that can be printed into host code to represent the type.
+  final String datatype;
+
+  /// `true` if the host datatype is something builtin.
+  final bool isBuiltin;
+}
+
+/// Calculates the [HostDatatype] for the provided [Field].  It will check the
+/// field against the `classes` to check if it is a builtin type.
+/// `builtinResolver` will return the host datatype for the Dart datatype for
+/// builtin types.  `customResolver` can modify the datatype of custom types.
+HostDatatype getHostDatatype(
+    Field field, List<Class> classes, String? Function(String) builtinResolver,
+    {String Function(String)? customResolver}) {
+  final String? datatype = builtinResolver(field.dataType);
+  if (datatype == null) {
+    if (classes.map((Class x) => x.name).contains(field.dataType)) {
+      final String customName = customResolver != null
+          ? customResolver(field.dataType)
+          : field.dataType;
+      return HostDatatype(datatype: customName, isBuiltin: false);
+    } else {
+      throw Exception(
+          'unrecognized datatype for field:"${field.name}" of type:"${field.dataType}"');
+    }
+  } else {
+    return HostDatatype(datatype: datatype, isBuiltin: true);
+  }
+}
+
+/// Warning printed at the top of all generated code.
+const String generatedCodeWarning =
+    'Autogenerated from Pigeon (v$pigeonVersion), do not edit directly.';
+
+/// String to be printed after `generatedCodeWarning`.
+const String seeAlsoWarning = 'See also: https://pub.dev/packages/pigeon';
+
+/// Collection of keys used in dictionaries across generators.
+class Keys {
+  /// The key in the result hash for the 'result' value.
+  static const String result = 'result';
+
+  /// The key in the result hash for the 'error' value.
+  static const String error = 'error';
+
+  /// The key in an error hash for the 'code' value.
+  static const String errorCode = 'code';
+
+  /// The key in an error hash for the 'message' value.
+  static const String errorMessage = 'message';
+
+  /// The key in an error hash for the 'details' value.
+  static const String errorDetails = 'details';
+}
+
+/// Returns true if `type` represents 'void'.
+bool isVoid(TypeMirror type) {
+  return MirrorSystem.getName(type.simpleName) == 'void';
+}
diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart
new file mode 100644
index 0000000..5f8efca
--- /dev/null
+++ b/packages/pigeon/lib/java_generator.dart
@@ -0,0 +1,320 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+const Map<String, String> _javaTypeForDartTypeMap = <String, String>{
+  'bool': 'Boolean',
+  'int': 'Long',
+  'String': 'String',
+  'double': 'Double',
+  'Uint8List': 'byte[]',
+  'Int32List': 'int[]',
+  'Int64List': 'long[]',
+  'Float64List': 'double[]',
+  'List': 'List<Object>',
+  'Map': 'Map<Object, Object>',
+};
+
+/// Options that control how Java code will be generated.
+class JavaOptions {
+  /// Creates a [JavaOptions] object
+  JavaOptions({
+    this.className,
+    this.package,
+  });
+
+  /// The name of the class that will house all the generated classes.
+  String? className;
+
+  /// The package where the generated class will live.
+  String? package;
+}
+
+void _writeHostApi(Indent indent, Api api) {
+  assert(api.location == ApiLocation.host);
+
+  indent.writeln(
+      '/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
+  indent.write('public interface ${api.name} ');
+  indent.scoped('{', '}', () {
+    for (final Method method in api.methods) {
+      final String returnType =
+          method.isAsynchronous ? 'void' : method.returnType;
+      final List<String> argSignature = <String>[];
+      if (method.argType != 'void') {
+        argSignature.add('${method.argType} arg');
+      }
+      if (method.isAsynchronous) {
+        final String returnType =
+            method.returnType == 'void' ? 'Void' : method.returnType;
+        argSignature.add('Result<$returnType> result');
+      }
+      indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
+    }
+    indent.addln('');
+    indent.writeln(
+        '/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */');
+    indent.write(
+        'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
+    indent.scoped('{', '}', () {
+      for (final Method method in api.methods) {
+        final String channelName = makeChannelName(api, method);
+        indent.write('');
+        indent.scoped('{', '}', () {
+          indent.writeln('BasicMessageChannel<Object> channel =');
+          indent.inc();
+          indent.inc();
+          indent.writeln(
+              'new BasicMessageChannel<>(binaryMessenger, "$channelName", new StandardMessageCodec());');
+          indent.dec();
+          indent.dec();
+          indent.write('if (api != null) ');
+          indent.scoped('{', '} else {', () {
+            indent.write('channel.setMessageHandler((message, reply) -> ');
+            indent.scoped('{', '});', () {
+              final String argType = method.argType;
+              final String returnType = method.returnType;
+              indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
+              indent.write('try ');
+              indent.scoped('{', '}', () {
+                final List<String> methodArgument = <String>[];
+                if (argType != 'void') {
+                  indent.writeln('@SuppressWarnings("ConstantConditions")');
+                  indent.writeln(
+                      '$argType input = $argType.fromMap((Map<String, Object>)message);');
+                  methodArgument.add('input');
+                }
+                if (method.isAsynchronous) {
+                  final String resultValue =
+                      method.returnType == 'void' ? 'null' : 'result.toMap()';
+                  methodArgument.add(
+                    'result -> { '
+                    'wrapped.put("${Keys.result}", $resultValue); '
+                    'reply.reply(wrapped); '
+                    '}',
+                  );
+                }
+                final String call =
+                    'api.${method.name}(${methodArgument.join(', ')})';
+                if (method.isAsynchronous) {
+                  indent.writeln('$call;');
+                } else if (method.returnType == 'void') {
+                  indent.writeln('$call;');
+                  indent.writeln('wrapped.put("${Keys.result}", null);');
+                } else {
+                  indent.writeln('$returnType output = $call;');
+                  indent.writeln(
+                      'wrapped.put("${Keys.result}", output.toMap());');
+                }
+              });
+              indent.write('catch (Error | RuntimeException exception) ');
+              indent.scoped('{', '}', () {
+                indent.writeln(
+                    'wrapped.put("${Keys.error}", wrapError(exception));');
+                if (method.isAsynchronous) {
+                  indent.writeln('reply.reply(wrapped);');
+                }
+              });
+              if (!method.isAsynchronous) {
+                indent.writeln('reply.reply(wrapped);');
+              }
+            });
+          });
+          indent.scoped(null, '}', () {
+            indent.writeln('channel.setMessageHandler(null);');
+          });
+        });
+      }
+    });
+  });
+}
+
+void _writeFlutterApi(Indent indent, Api api) {
+  assert(api.location == ApiLocation.flutter);
+  indent.writeln(
+      '/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/');
+  indent.write('public static class ${api.name} ');
+  indent.scoped('{', '}', () {
+    indent.writeln('private final BinaryMessenger binaryMessenger;');
+    indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)');
+    indent.scoped('{', '}', () {
+      indent.writeln('this.binaryMessenger = argBinaryMessenger;');
+    });
+    indent.write('public interface Reply<T> ');
+    indent.scoped('{', '}', () {
+      indent.writeln('void reply(T reply);');
+    });
+    for (final Method func in api.methods) {
+      final String channelName = makeChannelName(api, func);
+      final String returnType =
+          func.returnType == 'void' ? 'Void' : func.returnType;
+      String sendArgument;
+      if (func.argType == 'void') {
+        indent.write('public void ${func.name}(Reply<$returnType> callback) ');
+        sendArgument = 'null';
+      } else {
+        indent.write(
+            'public void ${func.name}(${func.argType} argInput, Reply<$returnType> callback) ');
+        sendArgument = 'inputMap';
+      }
+      indent.scoped('{', '}', () {
+        indent.writeln('BasicMessageChannel<Object> channel =');
+        indent.inc();
+        indent.inc();
+        indent.writeln(
+            'new BasicMessageChannel<>(binaryMessenger, "$channelName", new StandardMessageCodec());');
+        indent.dec();
+        indent.dec();
+        if (func.argType != 'void') {
+          indent.writeln('Map<String, Object> inputMap = argInput.toMap();');
+        }
+        indent.write('channel.send($sendArgument, channelReply -> ');
+        indent.scoped('{', '});', () {
+          if (func.returnType == 'void') {
+            indent.writeln('callback.reply(null);');
+          } else {
+            indent.writeln('Map outputMap = (Map)channelReply;');
+            indent.writeln('@SuppressWarnings("ConstantConditions")');
+            indent.writeln(
+                '${func.returnType} output = ${func.returnType}.fromMap(outputMap);');
+            indent.writeln('callback.reply(output);');
+          }
+        });
+      });
+    }
+  });
+}
+
+String _makeGetter(Field field) {
+  final String uppercased =
+      field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
+  return 'get$uppercased';
+}
+
+String _makeSetter(Field field) {
+  final String uppercased =
+      field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
+  return 'set$uppercased';
+}
+
+String? _javaTypeForDartType(String datatype) {
+  return _javaTypeForDartTypeMap[datatype];
+}
+
+String _castObject(Field field, List<Class> classes, String varName) {
+  final HostDatatype hostDatatype =
+      getHostDatatype(field, classes, _javaTypeForDartType);
+  if (field.dataType == 'int') {
+    return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)';
+  } else if (!hostDatatype.isBuiltin &&
+      classes.map((Class x) => x.name).contains(field.dataType)) {
+    return '${hostDatatype.datatype}.fromMap((Map)$varName)';
+  } else {
+    return '(${hostDatatype.datatype})$varName';
+  }
+}
+
+/// Generates the ".java" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateJava(JavaOptions options, Root root, StringSink sink) {
+  final Set<String> rootClassNameSet =
+      root.classes.map((Class x) => x.name).toSet();
+  final Indent indent = Indent(sink);
+  indent.writeln('// $generatedCodeWarning');
+  indent.writeln('// $seeAlsoWarning');
+  indent.addln('');
+  if (options.package != null) {
+    indent.writeln('package ${options.package};');
+  }
+  indent.addln('');
+  indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
+  indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
+  indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');
+  indent.writeln('import java.util.List;');
+  indent.writeln('import java.util.Map;');
+  indent.writeln('import java.util.HashMap;');
+
+  indent.addln('');
+  indent.writeln('/** Generated class from Pigeon. */');
+  indent.writeln(
+      '@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
+  indent.write('public class ${options.className!} ');
+  indent.scoped('{', '}', () {
+    for (final Class klass in root.classes) {
+      indent.addln('');
+      indent.writeln(
+          '/** Generated class from Pigeon that represents data sent in messages. */');
+      indent.write('public static class ${klass.name} ');
+      indent.scoped('{', '}', () {
+        for (final Field field in klass.fields) {
+          final HostDatatype hostDatatype =
+              getHostDatatype(field, root.classes, _javaTypeForDartType);
+          indent.writeln('private ${hostDatatype.datatype} ${field.name};');
+          indent.writeln(
+              'public ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }');
+          indent.writeln(
+              'public void ${_makeSetter(field)}(${hostDatatype.datatype} setterArg) { this.${field.name} = setterArg; }');
+          indent.addln('');
+        }
+        indent.write('Map<String, Object> toMap() ');
+        indent.scoped('{', '}', () {
+          indent.writeln('Map<String, Object> toMapResult = new HashMap<>();');
+          for (final Field field in klass.fields) {
+            final HostDatatype hostDatatype =
+                getHostDatatype(field, root.classes, _javaTypeForDartType);
+            String toWriteValue = '';
+            if (!hostDatatype.isBuiltin &&
+                rootClassNameSet.contains(field.dataType)) {
+              toWriteValue = '${field.name}.toMap()';
+            } else {
+              toWriteValue = field.name;
+            }
+            indent.writeln('toMapResult.put("${field.name}", $toWriteValue);');
+          }
+          indent.writeln('return toMapResult;');
+        });
+        indent.write('static ${klass.name} fromMap(Map<String, Object> map) ');
+        indent.scoped('{', '}', () {
+          indent.writeln('${klass.name} fromMapResult = new ${klass.name}();');
+          for (final Field field in klass.fields) {
+            indent.writeln('Object ${field.name} = map.get("${field.name}");');
+            indent.writeln(
+                'fromMapResult.${field.name} = ${_castObject(field, root.classes, field.name)};');
+          }
+          indent.writeln('return fromMapResult;');
+        });
+      });
+    }
+
+    if (root.apis.any((Api api) =>
+        api.location == ApiLocation.host &&
+        api.methods.any((Method it) => it.isAsynchronous))) {
+      indent.addln('');
+      indent.write('public interface Result<T> ');
+      indent.scoped('{', '}', () {
+        indent.writeln('void success(T result);');
+      });
+    }
+
+    for (final Api api in root.apis) {
+      indent.addln('');
+      if (api.location == ApiLocation.host) {
+        _writeHostApi(indent, api);
+      } else if (api.location == ApiLocation.flutter) {
+        _writeFlutterApi(indent, api);
+      }
+    }
+
+    indent.format('''
+private static Map<String, Object> wrapError(Throwable exception) {
+\tMap<String, Object> errorMap = new HashMap<>();
+\terrorMap.put("${Keys.errorMessage}", exception.toString());
+\terrorMap.put("${Keys.errorCode}", exception.getClass().getSimpleName());
+\terrorMap.put("${Keys.errorDetails}", null);
+\treturn errorMap;
+}''');
+  });
+}
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
new file mode 100644
index 0000000..caec03d
--- /dev/null
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -0,0 +1,449 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Options that control how Objective-C code will be generated.
+class ObjcOptions {
+  /// Parametric constructor for ObjcOptions.
+  ObjcOptions({
+    this.header,
+    this.prefix,
+  });
+
+  /// The path to the header that will get placed in the source filed (example:
+  /// "foo.h").
+  String? header;
+
+  /// Prefix that will be appended before all generated classes and protocols.
+  String? prefix;
+}
+
+String _className(String? prefix, String className) {
+  if (prefix != null) {
+    return '$prefix$className';
+  } else {
+    return className;
+  }
+}
+
+String _callbackForType(String dartType, String objcType) {
+  return dartType == 'void'
+      ? 'void(^)(NSError* _Nullable)'
+      : 'void(^)($objcType*, NSError* _Nullable)';
+}
+
+const Map<String, String> _objcTypeForDartTypeMap = <String, String>{
+  'bool': 'NSNumber *',
+  'int': 'NSNumber *',
+  'String': 'NSString *',
+  'double': 'NSNumber *',
+  'Uint8List': 'FlutterStandardTypedData *',
+  'Int32List': 'FlutterStandardTypedData *',
+  'Int64List': 'FlutterStandardTypedData *',
+  'Float64List': 'FlutterStandardTypedData *',
+  'List': 'NSArray *',
+  'Map': 'NSDictionary *',
+};
+
+const Map<String, String> _propertyTypeForDartTypeMap = <String, String>{
+  'String': 'copy',
+  'bool': 'strong',
+  'int': 'strong',
+  'double': 'strong',
+  'Uint8List': 'strong',
+  'Int32List': 'strong',
+  'Int64List': 'strong',
+  'Float64List': 'strong',
+  'List': 'strong',
+  'Map': 'strong',
+};
+
+String? _objcTypeForDartType(String type) {
+  return _objcTypeForDartTypeMap[type];
+}
+
+String _propertyTypeForDartType(String type) {
+  final String? result = _propertyTypeForDartTypeMap[type];
+  if (result == null) {
+    return 'assign';
+  } else {
+    return result;
+  }
+}
+
+void _writeClassDeclarations(
+    Indent indent, List<Class> classes, String? prefix) {
+  for (final Class klass in classes) {
+    indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject');
+    for (final Field field in klass.fields) {
+      final HostDatatype hostDatatype = getHostDatatype(
+          field, classes, _objcTypeForDartType,
+          customResolver: (String x) => '${_className(prefix, x)} *');
+      final String propertyType = hostDatatype.isBuiltin
+          ? _propertyTypeForDartType(field.dataType)
+          : 'strong';
+      final String nullability =
+          hostDatatype.datatype.contains('*') ? ', nullable' : '';
+      indent.writeln(
+          '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};');
+    }
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+}
+
+void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) {
+  final String apiName = _className(options.prefix, api.name);
+  indent.writeln('@protocol $apiName');
+  for (final Method func in api.methods) {
+    final String returnTypeName = _className(options.prefix, func.returnType);
+    if (func.isAsynchronous) {
+      if (func.returnType == 'void') {
+        if (func.argType == 'void') {
+          indent.writeln(
+              '-(void)${func.name}:(void(^)(FlutterError *_Nullable))completion;');
+        } else {
+          final String argType = _className(options.prefix, func.argType);
+          indent.writeln(
+              '-(void)${func.name}:(nullable $argType *)input completion:(void(^)(FlutterError *_Nullable))completion;');
+        }
+      } else {
+        if (func.argType == 'void') {
+          indent.writeln(
+              '-(void)${func.name}:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;');
+        } else {
+          final String argType = _className(options.prefix, func.argType);
+          indent.writeln(
+              '-(void)${func.name}:(nullable $argType *)input completion:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;');
+        }
+      }
+    } else {
+      final String returnType =
+          func.returnType == 'void' ? 'void' : 'nullable $returnTypeName *';
+      if (func.argType == 'void') {
+        indent.writeln(
+            '-($returnType)${func.name}:(FlutterError *_Nullable *_Nonnull)error;');
+      } else {
+        final String argType = _className(options.prefix, func.argType);
+        indent.writeln(
+            '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;');
+      }
+    }
+  }
+  indent.writeln('@end');
+  indent.writeln('');
+  indent.writeln(
+      'extern void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, id<$apiName> _Nullable api);');
+  indent.writeln('');
+}
+
+void _writeFlutterApiDeclaration(Indent indent, Api api, ObjcOptions options) {
+  final String apiName = _className(options.prefix, api.name);
+  indent.writeln('@interface $apiName : NSObject');
+  indent.writeln(
+      '- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;');
+  for (final Method func in api.methods) {
+    final String returnType = _className(options.prefix, func.returnType);
+    final String callbackType = _callbackForType(func.returnType, returnType);
+    if (func.argType == 'void') {
+      indent.writeln('- (void)${func.name}:($callbackType)completion;');
+    } else {
+      final String argType = _className(options.prefix, func.argType);
+      indent.writeln(
+          '- (void)${func.name}:($argType*)input completion:($callbackType)completion;');
+    }
+  }
+  indent.writeln('@end');
+}
+
+/// Generates the ".h" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) {
+  final Indent indent = Indent(sink);
+  indent.writeln('// $generatedCodeWarning');
+  indent.writeln('// $seeAlsoWarning');
+  indent.writeln('#import <Foundation/Foundation.h>');
+  indent.writeln('@protocol FlutterBinaryMessenger;');
+  indent.writeln('@class FlutterError;');
+  indent.writeln('@class FlutterStandardTypedData;');
+  indent.writeln('');
+
+  indent.writeln('NS_ASSUME_NONNULL_BEGIN');
+  indent.writeln('');
+
+  for (final Class klass in root.classes) {
+    indent.writeln('@class ${_className(options.prefix, klass.name)};');
+  }
+
+  indent.writeln('');
+
+  _writeClassDeclarations(indent, root.classes, options.prefix);
+
+  for (final Api api in root.apis) {
+    if (api.location == ApiLocation.host) {
+      _writeHostApiDeclaration(indent, api, options);
+    } else if (api.location == ApiLocation.flutter) {
+      _writeFlutterApiDeclaration(indent, api, options);
+    }
+  }
+
+  indent.writeln('NS_ASSUME_NONNULL_END');
+}
+
+String _dictGetter(
+    List<String> classnames, String dict, Field field, String? prefix) {
+  if (classnames.contains(field.dataType)) {
+    String className = field.dataType;
+    if (prefix != null) {
+      className = '$prefix$className';
+    }
+    return '[$className fromMap:$dict[@"${field.name}"]]';
+  } else {
+    return '$dict[@"${field.name}"]';
+  }
+}
+
+String _dictValue(List<String> classnames, Field field) {
+  if (classnames.contains(field.dataType)) {
+    return '(self.${field.name} ? [self.${field.name} toMap] : [NSNull null])';
+  } else {
+    return '(self.${field.name} ? self.${field.name} : [NSNull null])';
+  }
+}
+
+void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) {
+  assert(api.location == ApiLocation.host);
+  final String apiName = _className(options.prefix, api.name);
+  indent.write(
+      'void ${apiName}Setup(id<FlutterBinaryMessenger> binaryMessenger, id<$apiName> api) ');
+  indent.scoped('{', '}', () {
+    for (final Method func in api.methods) {
+      indent.write('');
+      indent.scoped('{', '}', () {
+        indent.writeln('FlutterBasicMessageChannel *channel =');
+        indent.inc();
+        indent.writeln('[FlutterBasicMessageChannel');
+        indent.inc();
+        indent
+            .writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+        indent.writeln('binaryMessenger:binaryMessenger];');
+        indent.dec();
+        indent.dec();
+
+        indent.write('if (api) ');
+        indent.scoped('{', '}', () {
+          indent.write(
+              '[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) ');
+          indent.scoped('{', '}];', () {
+            final String returnType =
+                _className(options.prefix, func.returnType);
+            String syncCall;
+            if (func.argType == 'void') {
+              syncCall = '[api ${func.name}:&error]';
+            } else {
+              final String argType = _className(options.prefix, func.argType);
+              indent.writeln('$argType *input = [$argType fromMap:message];');
+              syncCall = '[api ${func.name}:input error:&error]';
+            }
+            if (func.isAsynchronous) {
+              if (func.returnType == 'void') {
+                const String callback = 'callback(error);';
+                if (func.argType == 'void') {
+                  indent.writeScoped(
+                      '[api ${func.name}:^(FlutterError *_Nullable error) {',
+                      '}];', () {
+                    indent.writeln(callback);
+                  });
+                } else {
+                  indent.writeScoped(
+                      '[api ${func.name}:input completion:^(FlutterError *_Nullable error) {',
+                      '}];', () {
+                    indent.writeln(callback);
+                  });
+                }
+              } else {
+                const String callback =
+                    'callback(wrapResult([output toMap], error));';
+                if (func.argType == 'void') {
+                  indent.writeScoped(
+                      '[api ${func.name}:^($returnType *_Nullable output, FlutterError *_Nullable error) {',
+                      '}];', () {
+                    indent.writeln(callback);
+                  });
+                } else {
+                  indent.writeScoped(
+                      '[api ${func.name}:input completion:^($returnType *_Nullable output, FlutterError *_Nullable error) {',
+                      '}];', () {
+                    indent.writeln(callback);
+                  });
+                }
+              }
+            } else {
+              indent.writeln('FlutterError *error;');
+              if (func.returnType == 'void') {
+                indent.writeln('$syncCall;');
+                indent.writeln('callback(wrapResult(nil, error));');
+              } else {
+                indent.writeln('$returnType *output = $syncCall;');
+                indent.writeln('callback(wrapResult([output toMap], error));');
+              }
+            }
+          });
+        });
+        indent.write('else ');
+        indent.scoped('{', '}', () {
+          indent.writeln('[channel setMessageHandler:nil];');
+        });
+      });
+    }
+  });
+}
+
+void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) {
+  assert(api.location == ApiLocation.flutter);
+  final String apiName = _className(options.prefix, api.name);
+  indent.writeln('@interface $apiName ()');
+  indent.writeln(
+      '@property (nonatomic, strong) NSObject<FlutterBinaryMessenger>* binaryMessenger;');
+  indent.writeln('@end');
+  indent.addln('');
+  indent.writeln('@implementation $apiName');
+  indent.write(
+      '- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger>*)binaryMessenger ');
+  indent.scoped('{', '}', () {
+    indent.writeln('self = [super init];');
+    indent.write('if (self) ');
+    indent.scoped('{', '}', () {
+      indent.writeln('_binaryMessenger = binaryMessenger;');
+    });
+    indent.writeln('return self;');
+  });
+  indent.addln('');
+  for (final Method func in api.methods) {
+    final String returnType = _className(options.prefix, func.returnType);
+    final String callbackType = _callbackForType(func.returnType, returnType);
+
+    String sendArgument;
+    if (func.argType == 'void') {
+      indent.write('- (void)${func.name}:($callbackType)completion ');
+      sendArgument = 'nil';
+    } else {
+      final String argType = _className(options.prefix, func.argType);
+      indent.write(
+          '- (void)${func.name}:($argType*)input completion:($callbackType)completion ');
+      sendArgument = 'inputMap';
+    }
+    indent.scoped('{', '}', () {
+      indent.writeln('FlutterBasicMessageChannel *channel =');
+      indent.inc();
+      indent.writeln('[FlutterBasicMessageChannel');
+      indent.inc();
+      indent.writeln('messageChannelWithName:@"${makeChannelName(api, func)}"');
+      indent.writeln('binaryMessenger:self.binaryMessenger];');
+      indent.dec();
+      indent.dec();
+      if (func.argType != 'void') {
+        indent.writeln('NSDictionary* inputMap = [input toMap];');
+      }
+      indent.write('[channel sendMessage:$sendArgument reply:^(id reply) ');
+      indent.scoped('{', '}];', () {
+        if (func.returnType == 'void') {
+          indent.writeln('completion(nil);');
+        } else {
+          indent.writeln('NSDictionary* outputMap = reply;');
+          indent.writeln(
+              '$returnType * output = [$returnType fromMap:outputMap];');
+          indent.writeln('completion(output, nil);');
+        }
+      });
+    });
+  }
+  indent.writeln('@end');
+}
+
+/// Generates the ".m" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcSource(ObjcOptions options, Root root, StringSink sink) {
+  final Indent indent = Indent(sink);
+  final List<String> classnames =
+      root.classes.map((Class x) => x.name).toList();
+
+  indent.writeln('// $generatedCodeWarning');
+  indent.writeln('// $seeAlsoWarning');
+  indent.writeln('#import "${options.header}"');
+  indent.writeln('#import <Flutter/Flutter.h>');
+  indent.writeln('');
+
+  indent.writeln('#if !__has_feature(objc_arc)');
+  indent.writeln('#error File requires ARC to be enabled.');
+  indent.writeln('#endif');
+  indent.addln('');
+
+  indent.format('''
+static NSDictionary<NSString*, id>* wrapResult(NSDictionary *result, FlutterError *error) {
+\tNSDictionary *errorDict = (NSDictionary *)[NSNull null];
+\tif (error) {
+\t\terrorDict = @{
+\t\t\t\t@"${Keys.errorCode}": (error.code ? error.code : [NSNull null]),
+\t\t\t\t@"${Keys.errorMessage}": (error.message ? error.message : [NSNull null]),
+\t\t\t\t@"${Keys.errorDetails}": (error.details ? error.details : [NSNull null]),
+\t\t\t\t};
+\t}
+\treturn @{
+\t\t\t@"${Keys.result}": (result ? result : [NSNull null]),
+\t\t\t@"${Keys.error}": errorDict,
+\t\t\t};
+}''');
+  indent.addln('');
+
+  for (final Class klass in root.classes) {
+    final String className = _className(options.prefix, klass.name);
+    indent.writeln('@interface $className ()');
+    indent.writeln('+($className*)fromMap:(NSDictionary*)dict;');
+    indent.writeln('-(NSDictionary*)toMap;');
+    indent.writeln('@end');
+  }
+
+  indent.writeln('');
+
+  for (final Class klass in root.classes) {
+    final String className = _className(options.prefix, klass.name);
+    indent.writeln('@implementation $className');
+    indent.write('+($className*)fromMap:(NSDictionary*)dict ');
+    indent.scoped('{', '}', () {
+      const String resultName = 'result';
+      indent.writeln('$className* $resultName = [[$className alloc] init];');
+      for (final Field field in klass.fields) {
+        indent.writeln(
+            '$resultName.${field.name} = ${_dictGetter(classnames, 'dict', field, options.prefix)};');
+        indent.write(
+            'if ((NSNull *)$resultName.${field.name} == [NSNull null]) ');
+        indent.scoped('{', '}', () {
+          indent.writeln('$resultName.${field.name} = nil;');
+        });
+      }
+      indent.writeln('return $resultName;');
+    });
+    indent.write('-(NSDictionary*)toMap ');
+    indent.scoped('{', '}', () {
+      indent.write('return [NSDictionary dictionaryWithObjectsAndKeys:');
+      for (final Field field in klass.fields) {
+        indent.add(_dictValue(classnames, field) + ', @"${field.name}", ');
+      }
+      indent.addln('nil];');
+    });
+    indent.writeln('@end');
+    indent.writeln('');
+  }
+
+  for (final Api api in root.apis) {
+    if (api.location == ApiLocation.host) {
+      _writeHostApiSource(indent, options, api);
+    } else if (api.location == ApiLocation.flutter) {
+      _writeFlutterApiSource(indent, options, api);
+    }
+  }
+}
diff --git a/packages/pigeon/lib/pigeon.dart b/packages/pigeon/lib/pigeon.dart
new file mode 100644
index 0000000..5f19495
--- /dev/null
+++ b/packages/pigeon/lib/pigeon.dart
@@ -0,0 +1,2 @@
+export 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+export 'pigeon_lib.dart';
diff --git a/packages/pigeon/lib/pigeon_cl.dart b/packages/pigeon/lib/pigeon_cl.dart
new file mode 100644
index 0000000..096c482
--- /dev/null
+++ b/packages/pigeon/lib/pigeon_cl.dart
@@ -0,0 +1,75 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as path;
+import 'package:pigeon/pigeon_lib.dart';
+
+/// This creates a relative path from `from` to `input`, the output being a
+/// posix path on all platforms.
+String _posixRelative(String input, {required String from}) {
+  final path.Context context = path.Context(style: path.Style.posix);
+  final String rawInputPath = input;
+  final String absInputPath = File(rawInputPath).absolute.path;
+  // By going through URI's we can make sure paths can go between drives in
+  // Windows.
+  final Uri inputUri = path.toUri(absInputPath);
+  final String posixAbsInputPath = context.fromUri(inputUri);
+  final Uri tempUri = path.toUri(from);
+  final String posixTempPath = context.fromUri(tempUri);
+  return context.relative(posixAbsInputPath, from: posixTempPath);
+}
+
+/// This is the main entrypoint for the command-line tool.  [args] are the
+/// commmand line arguments and there is an optional [packageConfig] to
+/// accomodate users that want to integrate pigeon with other build systems.
+Future<int> runCommandLine(List<String> args, {Uri? packageConfig}) async {
+  final PigeonOptions opts = Pigeon.parseArgs(args);
+  final Directory tempDir = Directory.systemTemp.createTempSync(
+    'flutter_pigeon.',
+  );
+
+  String importLine = '';
+  if (opts.input != null) {
+    final String relInputPath = _posixRelative(opts.input!, from: tempDir.path);
+    importLine = 'import \'$relInputPath\';\n';
+  }
+  final String code = """
+// @dart = 2.12
+$importLine
+import 'dart:io';
+import 'dart:isolate';
+import 'package:pigeon/pigeon_lib.dart';
+void main(List<String> args, SendPort sendPort) async {
+  sendPort.send(await Pigeon.run(args));
+}
+""";
+  final File tempFile = File(path.join(tempDir.path, '_pigeon_temp_.dart'));
+  await tempFile.writeAsString(code);
+  final ReceivePort receivePort = ReceivePort();
+  Isolate.spawnUri(
+    // Using Uri.file instead of Uri.parse in order to parse backslashes as
+    // path segment separator with Windows semantics.
+    Uri.file(tempFile.path),
+    args,
+    receivePort.sendPort,
+    packageConfig: packageConfig,
+  );
+
+  final Completer<int> completer = Completer<int>();
+  receivePort.listen((dynamic message) {
+    try {
+      // ignore: avoid_as
+      completer.complete(message as int);
+    } catch (exception) {
+      completer.completeError(exception);
+    }
+  });
+  final int exitCode = await completer.future;
+  tempDir.deleteSync(recursive: true);
+  return exitCode;
+}
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
new file mode 100644
index 0000000..5228bb9
--- /dev/null
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -0,0 +1,518 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+import 'dart:mirrors';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart' as path;
+import 'package:path/path.dart';
+import 'package:pigeon/java_generator.dart';
+
+import 'ast.dart';
+import 'dart_generator.dart';
+import 'generator_tools.dart';
+import 'objc_generator.dart';
+
+const List<String> _validTypes = <String>[
+  'String',
+  'bool',
+  'int',
+  'double',
+  'Uint8List',
+  'Int32List',
+  'Int64List',
+  'Float64List',
+  'List',
+  'Map',
+];
+
+class _Asynchronous {
+  const _Asynchronous();
+}
+
+/// Metadata to annotate a Api method as asynchronous
+const _Asynchronous async = _Asynchronous();
+
+/// Metadata to annotate a Pigeon API implemented by the host-platform.
+///
+/// The abstract class with this annotation groups a collection of Dart↔host
+/// interop methods. These methods are invoked by Dart and are received by a
+/// host-platform (such as in Android or iOS) by a class implementing the
+/// generated host-platform interface.
+class HostApi {
+  /// Parametric constructor for [HostApi].
+  const HostApi({this.dartHostTestHandler});
+
+  /// The name of an interface generated for tests. Implement this
+  /// interface and invoke `[name of this handler].setup` to receive
+  /// calls from your real [HostApi] class in Dart instead of the host
+  /// platform code, as is typical.
+  ///
+  /// When using this, you must specify the `--out_test_dart` argument
+  /// to specify where to generate the test file.
+  ///
+  /// Prefer to use a mock of the real [HostApi] with a mocking library for unit
+  /// tests.  Generating this Dart handler is sometimes useful in integration
+  /// testing.
+  ///
+  /// Defaults to `null` in which case no handler will be generated.
+  final String? dartHostTestHandler;
+}
+
+/// Metadata to annotate a Pigeon API implemented by Flutter.
+///
+/// The abstract class with this annotation groups a collection of Dart↔host
+/// interop methods. These methods are invoked by the host-platform (such as in
+/// Android or iOS) and are received by Flutter by a class implementing the
+/// generated Dart interface.
+class FlutterApi {
+  /// Parametric constructor for [FlutterApi].
+  const FlutterApi();
+}
+
+/// Represents an error as a result of parsing and generating code.
+class Error {
+  /// Parametric constructor for Error.
+  Error({
+    required this.message,
+    this.filename,
+    this.lineNumber,
+  });
+
+  /// A description of the error.
+  String message;
+
+  /// What file caused the [Error].
+  String? filename;
+
+  /// What line the error happened on.
+  int? lineNumber;
+
+  @override
+  String toString() {
+    return '(Error message:"$message" filename:"$filename" lineNumber:$lineNumber)';
+  }
+}
+
+bool _isApi(ClassMirror classMirror) {
+  return classMirror.isAbstract &&
+      (_getHostApi(classMirror) != null || _isFlutterApi(classMirror));
+}
+
+HostApi? _getHostApi(ClassMirror apiMirror) {
+  for (final InstanceMirror instance in apiMirror.metadata) {
+    if (instance.reflectee is HostApi) {
+      return instance.reflectee;
+    }
+  }
+  return null;
+}
+
+bool _isFlutterApi(ClassMirror apiMirror) {
+  for (final InstanceMirror instance in apiMirror.metadata) {
+    if (instance.reflectee is FlutterApi) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/// Options used when running the code generator.
+class PigeonOptions {
+  /// Creates a instance of PigeonOptions
+  PigeonOptions();
+
+  /// Path to the file which will be processed.
+  String? input;
+
+  /// Path to the dart file that will be generated.
+  String? dartOut;
+
+  /// Path to the dart file that will be generated for test support classes.
+  String? dartTestOut;
+
+  /// Path to the ".h" Objective-C file will be generated.
+  String? objcHeaderOut;
+
+  /// Path to the ".m" Objective-C file will be generated.
+  String? objcSourceOut;
+
+  /// Options that control how Objective-C will be generated.
+  ObjcOptions? objcOptions;
+
+  /// Path to the java file that will be generated.
+  String? javaOut;
+
+  /// Options that control how Java will be generated.
+  JavaOptions? javaOptions;
+
+  /// Options that control how Dart will be generated.
+  DartOptions? dartOptions = DartOptions();
+}
+
+/// A collection of an AST represented as a [Root] and [Error]'s.
+class ParseResults {
+  /// Parametric constructor for [ParseResults].
+  ParseResults({
+    required this.root,
+    required this.errors,
+  });
+
+  /// The resulting AST.
+  final Root root;
+
+  /// Errors generated while parsing input.
+  final List<Error> errors;
+}
+
+/// Tool for generating code to facilitate platform channels usage.
+class Pigeon {
+  /// Create and setup a [Pigeon] instance.
+  static Pigeon setup() {
+    return Pigeon();
+  }
+
+  Class _parseClassMirror(ClassMirror klassMirror) {
+    final List<Field> fields = <Field>[];
+    for (final DeclarationMirror declaration
+        in klassMirror.declarations.values) {
+      if (declaration is VariableMirror) {
+        fields.add(Field(
+          name: MirrorSystem.getName(declaration.simpleName),
+          dataType: MirrorSystem.getName(
+            declaration.type.simpleName,
+          ),
+        ));
+      }
+    }
+    final Class klass = Class(
+      name: MirrorSystem.getName(klassMirror.simpleName),
+      fields: fields,
+    );
+    return klass;
+  }
+
+  Iterable<Class> _parseClassMirrors(Iterable<ClassMirror> mirrors) sync* {
+    for (final ClassMirror mirror in mirrors) {
+      yield _parseClassMirror(mirror);
+      final Iterable<ClassMirror> nestedTypes = mirror.declarations.values
+          .whereType<VariableMirror>()
+          .map((VariableMirror variable) => variable.type)
+          .whereType<ClassMirror>()
+
+          ///note: This will need to be changed if we support generic types.
+          .where((ClassMirror mirror) =>
+              !_validTypes.contains(MirrorSystem.getName(mirror.simpleName)));
+      for (final Class klass in _parseClassMirrors(nestedTypes)) {
+        yield klass;
+      }
+    }
+  }
+
+  Iterable<T> _unique<T, U>(Iterable<T> iter, U Function(T val) getKey) sync* {
+    final Set<U> seen = <U>{};
+    for (final T val in iter) {
+      if (seen.add(getKey(val))) {
+        yield val;
+      }
+    }
+  }
+
+  /// Use reflection to parse the [types] provided.
+  ParseResults parse(List<Type> types) {
+    final Set<ClassMirror> classes = <ClassMirror>{};
+    final List<ClassMirror> apis = <ClassMirror>[];
+
+    for (final Type type in types) {
+      final ClassMirror classMirror = reflectClass(type);
+      if (_isApi(classMirror)) {
+        apis.add(classMirror);
+      } else {
+        classes.add(classMirror);
+      }
+    }
+
+    for (final ClassMirror apiMirror in apis) {
+      for (final DeclarationMirror declaration
+          in apiMirror.declarations.values) {
+        if (declaration is MethodMirror && !declaration.isConstructor) {
+          if (!isVoid(declaration.returnType)) {
+            classes.add(declaration.returnType as ClassMirror);
+          }
+          if (declaration.parameters.isNotEmpty) {
+            classes.add(declaration.parameters[0].type as ClassMirror);
+          }
+        }
+      }
+    }
+    final Root root = Root(
+      classes:
+          _unique(_parseClassMirrors(classes), (Class x) => x.name).toList(),
+      apis: <Api>[],
+    );
+    for (final ClassMirror apiMirror in apis) {
+      final List<Method> functions = <Method>[];
+      for (final DeclarationMirror declaration
+          in apiMirror.declarations.values) {
+        if (declaration is MethodMirror && !declaration.isConstructor) {
+          final bool isAsynchronous =
+              declaration.metadata.any((InstanceMirror it) {
+            return MirrorSystem.getName(it.type.simpleName) ==
+                '${async.runtimeType}';
+          });
+          functions.add(Method(
+            name: MirrorSystem.getName(declaration.simpleName),
+            argType: declaration.parameters.isEmpty
+                ? 'void'
+                : MirrorSystem.getName(
+                    declaration.parameters[0].type.simpleName),
+            returnType: MirrorSystem.getName(declaration.returnType.simpleName),
+            isAsynchronous: isAsynchronous,
+          ));
+        }
+      }
+      final HostApi? hostApi = _getHostApi(apiMirror);
+      root.apis.add(Api(
+        name: MirrorSystem.getName(apiMirror.simpleName),
+        location: hostApi != null ? ApiLocation.host : ApiLocation.flutter,
+        methods: functions,
+        dartHostTestHandler: hostApi?.dartHostTestHandler,
+      ));
+    }
+
+    final List<Error> validateErrors = _validateAst(root);
+    return ParseResults(root: root, errors: validateErrors);
+  }
+
+  /// String that describes how the tool is used.
+  static String get usage {
+    return '''
+
+Pigeon is a tool for generating type-safe communication code between Flutter
+and the host platform.
+
+usage: pigeon --input <pigeon path> --dart_out <dart path> [option]*
+
+options:
+''' +
+        _argParser.usage;
+  }
+
+  static final ArgParser _argParser = ArgParser()
+    ..addOption('input', help: 'REQUIRED: Path to pigeon file.')
+    ..addOption('dart_out',
+        help: 'REQUIRED: Path to generated Dart source file (.dart).')
+    ..addOption('dart_test_out',
+        help: 'Path to generated library for Dart tests, when using '
+            '@HostApi(dartHostTestHandler:).')
+    ..addOption('objc_source_out',
+        help: 'Path to generated Objective-C source file (.m).')
+    ..addOption('java_out', help: 'Path to generated Java file (.java).')
+    ..addOption('java_package',
+        help: 'The package that generated Java code will be in.')
+    ..addFlag('dart_null_safety',
+        help: 'Makes generated Dart code have null safety annotations',
+        defaultsTo: true)
+    ..addOption('objc_header_out',
+        help: 'Path to generated Objective-C header file (.h).')
+    ..addOption('objc_prefix',
+        help: 'Prefix for generated Objective-C classes and protocols.');
+
+  /// Convert command-line arguments to [PigeonOptions].
+  static PigeonOptions parseArgs(List<String> args) {
+    // Note: This function shouldn't perform any logic, just translate the args
+    // to PigeonOptions.  Synthesized values inside of the PigeonOption should
+    // get set in the `run` function to accomodate users that are using the
+    // `configurePigeon` function.
+    final ArgResults results = _argParser.parse(args);
+
+    final PigeonOptions opts = PigeonOptions();
+    opts.input = results['input'];
+    opts.dartOut = results['dart_out'];
+    opts.dartTestOut = results['dart_test_out'];
+    opts.objcHeaderOut = results['objc_header_out'];
+    opts.objcSourceOut = results['objc_source_out'];
+    opts.objcOptions = ObjcOptions(
+      prefix: results['objc_prefix'],
+    );
+    opts.javaOut = results['java_out'];
+    opts.javaOptions = JavaOptions(
+      package: results['java_package'],
+    );
+    opts.dartOptions = DartOptions()..isNullSafe = results['dart_null_safety'];
+    return opts;
+  }
+
+  static Future<void> _runGenerator(
+      String output, void Function(IOSink sink) func) async {
+    IOSink sink;
+    File file;
+    if (output == 'stdout') {
+      sink = stdout;
+    } else {
+      file = File(output);
+      sink = file.openWrite();
+    }
+    func(sink);
+    await sink.flush();
+  }
+
+  List<Error> _validateAst(Root root) {
+    final List<Error> result = <Error>[];
+    final List<String> customClasses =
+        root.classes.map((Class x) => x.name).toList();
+    for (final Class klass in root.classes) {
+      for (final Field field in klass.fields) {
+        if (!(_validTypes.contains(field.dataType) ||
+            customClasses.contains(field.dataType))) {
+          result.add(Error(
+              message:
+                  'Unsupported datatype:"${field.dataType}" in class "${klass.name}".'));
+        }
+      }
+    }
+    for (final Api api in root.apis) {
+      for (final Method method in api.methods) {
+        if (_validTypes.contains(method.argType)) {
+          result.add(Error(
+              message:
+                  'Unsupported argument type: "${method.argType}" in API: "${api.name}" method: "${method.name}'));
+        }
+        if (_validTypes.contains(method.returnType)) {
+          result.add(Error(
+              message:
+                  'Unsupported return type: "${method.returnType}" in API: "${api.name}" method: "${method.name}'));
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /// Crawls through the reflection system looking for a configurePigeon method and
+  /// executing it.
+  static void _executeConfigurePigeon(PigeonOptions options) {
+    for (final LibraryMirror library
+        in currentMirrorSystem().libraries.values) {
+      for (final DeclarationMirror declaration in library.declarations.values) {
+        if (declaration is MethodMirror &&
+            MirrorSystem.getName(declaration.simpleName) == 'configurePigeon') {
+          if (declaration.parameters.length == 1 &&
+              declaration.parameters[0].type == reflectClass(PigeonOptions)) {
+            library.invoke(declaration.simpleName, <dynamic>[options]);
+          } else {
+            print('warning: invalid \'configurePigeon\' method defined.');
+          }
+        }
+      }
+    }
+  }
+
+  static String _posixify(String input) {
+    final path.Context context = path.Context(style: path.Style.posix);
+    return context.fromUri(path.toUri(path.absolute(input)));
+  }
+
+  /// The 'main' entrypoint used by the command-line tool.  [args] are the
+  /// command-line arguments.
+  static Future<int> run(List<String> args) async {
+    final Pigeon pigeon = Pigeon.setup();
+    final PigeonOptions options = Pigeon.parseArgs(args);
+
+    _executeConfigurePigeon(options);
+
+    if (options.input == null || options.dartOut == null) {
+      print(usage);
+      return 0;
+    }
+
+    final List<Error> errors = <Error>[];
+    final List<Type> apis = <Type>[];
+    if (options.objcHeaderOut != null) {
+      options.objcOptions?.header = basename(options.objcHeaderOut!);
+    }
+
+    for (final LibraryMirror library
+        in currentMirrorSystem().libraries.values) {
+      for (final DeclarationMirror declaration in library.declarations.values) {
+        if (declaration is ClassMirror && _isApi(declaration)) {
+          apis.add(declaration.reflectedType);
+        }
+      }
+    }
+
+    if (apis.isNotEmpty) {
+      final ParseResults parseResults = pigeon.parse(apis);
+      for (final Error err in parseResults.errors) {
+        errors.add(Error(message: err.message, filename: options.input));
+      }
+      if (options.dartOut != null) {
+        await _runGenerator(
+            options.dartOut!,
+            (StringSink sink) => generateDart(
+                options.dartOptions ?? DartOptions(), parseResults.root, sink));
+      }
+      if (options.dartTestOut != null && options.dartOut != null) {
+        final String mainPath = context.relative(
+          _posixify(options.dartOut!),
+          from: _posixify(path.dirname(options.dartTestOut!)),
+        );
+        await _runGenerator(
+          options.dartTestOut!,
+          (StringSink sink) => generateTestDart(
+            options.dartOptions ?? DartOptions(),
+            parseResults.root,
+            sink,
+            mainPath,
+          ),
+        );
+      }
+      if (options.objcHeaderOut != null) {
+        await _runGenerator(
+            options.objcHeaderOut!,
+            (StringSink sink) => generateObjcHeader(
+                options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
+      }
+      if (options.objcSourceOut != null) {
+        await _runGenerator(
+            options.objcSourceOut!,
+            (StringSink sink) => generateObjcSource(
+                options.objcOptions ?? ObjcOptions(), parseResults.root, sink));
+      }
+      if (options.javaOut != null) {
+        if (options.javaOptions!.className == null) {
+          options.javaOptions!.className =
+              path.basenameWithoutExtension(options.javaOut!);
+        }
+        await _runGenerator(
+            options.javaOut!,
+            (StringSink sink) => generateJava(
+                options.javaOptions ?? JavaOptions(), parseResults.root, sink));
+      }
+    } else {
+      errors.add(Error(message: 'No pigeon classes found, nothing generated.'));
+    }
+
+    printErrors(errors);
+
+    return errors.isNotEmpty ? 1 : 0;
+  }
+
+  /// Print a list of errors to stderr.
+  static void printErrors(List<Error> errors) {
+    for (final Error err in errors) {
+      if (err.filename != null) {
+        if (err.lineNumber != null) {
+          stderr.writeln(
+              'Error: ${err.filename}:${err.lineNumber}: ${err.message}');
+        } else {
+          stderr.writeln('Error: ${err.filename}: ${err.message}');
+        }
+      } else {
+        stderr.writeln('Error: ${err.message}');
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/mock_handler_tester/.gitignore b/packages/pigeon/mock_handler_tester/.gitignore
new file mode 100644
index 0000000..f3c2053
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/pigeon/mock_handler_tester/.metadata b/packages/pigeon/mock_handler_tester/.metadata
new file mode 100644
index 0000000..bb4f09a
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: e6b697a9df5e9ce933024be3334e86b599c60e71
+  channel: unknown
+
+project_type: app
diff --git a/packages/pigeon/mock_handler_tester/README.md b/packages/pigeon/mock_handler_tester/README.md
new file mode 100644
index 0000000..698b506
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/README.md
@@ -0,0 +1,3 @@
+# mock_handler_tester
+
+A test bed for testing the code generated by `dartHostTestHandler`.
diff --git a/packages/pigeon/mock_handler_tester/lib/main.dart b/packages/pigeon/mock_handler_tester/lib/main.dart
new file mode 100644
index 0000000..e4b5c93
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/lib/main.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/material.dart';
+
+void main() {
+  runApp(Container());
+}
diff --git a/packages/pigeon/mock_handler_tester/pubspec.yaml b/packages/pigeon/mock_handler_tester/pubspec.yaml
new file mode 100644
index 0000000..1826c25
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/pubspec.yaml
@@ -0,0 +1,19 @@
+name: mock_handler_tester
+description: A testbed for testing dartHostTestHandler.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^1.0.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
diff --git a/packages/pigeon/mock_handler_tester/test/message.dart b/packages/pigeon/mock_handler_tester/test/message.dart
new file mode 100644
index 0000000..0896b04
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/message.dart
@@ -0,0 +1,166 @@
+// Autogenerated from Pigeon (v0.2.0-nullsafety.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/services.dart';
+
+class SearchReply {
+  String? result;
+  String? error;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['result'] = result;
+    pigeonMap['error'] = error;
+    return pigeonMap;
+  }
+
+  static SearchReply decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchReply()
+      ..result = pigeonMap['result'] as String?
+      ..error = pigeonMap['error'] as String?;
+  }
+}
+
+class SearchRequest {
+  String? query;
+  int? anInt;
+  bool? aBool;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['query'] = query;
+    pigeonMap['anInt'] = anInt;
+    pigeonMap['aBool'] = aBool;
+    return pigeonMap;
+  }
+
+  static SearchRequest decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchRequest()
+      ..query = pigeonMap['query'] as String?
+      ..anInt = pigeonMap['anInt'] as int?
+      ..aBool = pigeonMap['aBool'] as bool?;
+  }
+}
+
+class Nested {
+  SearchRequest? request;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['request'] = request == null ? null : request!.encode();
+    return pigeonMap;
+  }
+
+  static Nested decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return Nested()
+      ..request = pigeonMap['request'] != null
+          ? SearchRequest.decode(pigeonMap['request']!)
+          : null;
+  }
+}
+
+abstract class FlutterSearchApi {
+  SearchReply search(SearchRequest arg);
+  static void setup(FlutterSearchApi? api) {
+    {
+      const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.FlutterSearchApi.search', StandardMessageCodec());
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.FlutterSearchApi.search was null. Expected SearchRequest.');
+          final SearchRequest input = SearchRequest.decode(message!);
+          final SearchReply output = api.search(input);
+          return output.encode();
+        });
+      }
+    }
+  }
+}
+
+class NestedApi {
+  Future<SearchReply> search(Nested arg) async {
+    final Object encoded = arg.encode();
+    const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(encoded) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return SearchReply.decode(replyMap['result']!);
+    }
+  }
+}
+
+class Api {
+  Future<void> initialize() async {
+    const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.Api.initialize', StandardMessageCodec());
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(null) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      // noop
+    }
+  }
+
+  Future<SearchReply> search(SearchRequest arg) async {
+    final Object encoded = arg.encode();
+    const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(encoded) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return SearchReply.decode(replyMap['result']!);
+    }
+  }
+}
diff --git a/packages/pigeon/mock_handler_tester/test/test.dart b/packages/pigeon/mock_handler_tester/test/test.dart
new file mode 100644
index 0000000..0d8e4c3
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/test.dart
@@ -0,0 +1,66 @@
+// Autogenerated from Pigeon (v0.2.0-nullsafety.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'message.dart';
+
+abstract class TestNestedApi {
+  SearchReply search(Nested arg);
+  static void setup(TestNestedApi? api) {
+    {
+      const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.NestedApi.search', StandardMessageCodec());
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.NestedApi.search was null. Expected Nested.');
+          final Nested input = Nested.decode(message!);
+          final SearchReply output = api.search(input);
+          return <Object?, Object?>{'result': output.encode()};
+        });
+      }
+    }
+  }
+}
+
+abstract class TestHostApi {
+  void initialize();
+  SearchReply search(SearchRequest arg);
+  static void setup(TestHostApi? api) {
+    {
+      const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.Api.initialize', StandardMessageCodec());
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          // ignore message
+          api.initialize();
+          return <Object?, Object?>{};
+        });
+      }
+    }
+    {
+      const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.Api.search', StandardMessageCodec());
+      if (api == null) {
+        channel.setMockMessageHandler(null);
+      } else {
+        channel.setMockMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.Api.search was null. Expected SearchRequest.');
+          final SearchRequest input = SearchRequest.decode(message!);
+          final SearchReply output = api.search(input);
+          return <Object?, Object?>{'result': output.encode()};
+        });
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/mock_handler_tester/test/widget_test.dart b/packages/pigeon/mock_handler_tester/test/widget_test.dart
new file mode 100644
index 0000000..0334051
--- /dev/null
+++ b/packages/pigeon/mock_handler_tester/test/widget_test.dart
@@ -0,0 +1,102 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'message.dart';
+import 'test.dart';
+
+class Mock implements TestHostApi {
+  List<String> log = <String>[];
+
+  @override
+  void initialize() {
+    log.add('initialize');
+  }
+
+  @override
+  SearchReply search(SearchRequest arg) {
+    log.add('search');
+    return SearchReply()..result = arg.query;
+  }
+}
+
+class MockNested implements TestNestedApi {
+  bool didCall = false;
+  @override
+  SearchReply search(Nested arg) {
+    didCall = true;
+    if (arg.request == null) {
+      return SearchReply();
+    } else {
+      return SearchReply()..result = arg.request?.query;
+    }
+  }
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  test('simple', () async {
+    final NestedApi api = NestedApi();
+    final MockNested mock = MockNested();
+    TestNestedApi.setup(mock);
+    final SearchReply reply = await api.search(Nested()..request = null);
+    expect(mock.didCall, true);
+    expect(reply.result, null);
+  });
+
+  test('nested', () async {
+    final Api api = Api();
+    final Mock mock = Mock();
+    TestHostApi.setup(mock);
+    final SearchReply reply = await api.search(SearchRequest()..query = 'foo');
+    expect(mock.log, <String>['search']);
+    expect(reply.result, 'foo');
+  });
+
+  test('no-arg calls', () async {
+    final Api api = Api();
+    final Mock mock = Mock();
+    TestHostApi.setup(mock);
+    await api.initialize();
+    expect(mock.log, <String>['initialize']);
+  });
+
+  test(
+    'calling methods with null',
+    () async {
+      final Mock mock = Mock();
+      TestHostApi.setup(mock);
+      expect(
+        await const BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.Api.initialize',
+          StandardMessageCodec(),
+        ).send(null),
+        isEmpty,
+      );
+      try {
+        await const BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.Api.search',
+          StandardMessageCodec(),
+        ).send(null) as Map<Object?, Object?>?;
+        expect(true, isFalse); // should not reach here
+      } catch (error) {
+        expect(error, isAssertionError);
+        expect(
+          error.toString(),
+          contains(
+            'Argument for dev.flutter.pigeon.Api.search was null. Expected SearchRequest.',
+          ),
+        );
+      }
+      expect(mock.log, <String>['initialize']);
+    },
+    // TODO(ianh): skip can be removed after first stable release in 2021
+    skip: Platform.environment['CHANNEL'] == 'stable',
+  );
+}
diff --git a/packages/pigeon/pigeons/all_datatypes.dart b/packages/pigeon/pigeons/all_datatypes.dart
new file mode 100644
index 0000000..95a2886
--- /dev/null
+++ b/packages/pigeon/pigeons/all_datatypes.dart
@@ -0,0 +1,30 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Everything {
+  bool? aBool;
+  int? anInt;
+  double? aDouble;
+  String? aString;
+  Uint8List? aByteArray;
+  Int32List? a4ByteArray;
+  Int64List? a8ByteArray;
+  Float64List? aFloatArray;
+  // ignore: always_specify_types
+  List? aList;
+  // ignore: always_specify_types
+  Map? aMap;
+}
+
+@HostApi()
+abstract class HostEverything {
+  Everything giveMeEverything();
+}
+
+@FlutterApi()
+abstract class FlutterEverything {
+  Everything giveMeEverything();
+}
diff --git a/packages/pigeon/pigeons/android_unittests.dart b/packages/pigeon/pigeons/android_unittests.dart
new file mode 100644
index 0000000..baaa69b
--- /dev/null
+++ b/packages/pigeon/pigeons/android_unittests.dart
@@ -0,0 +1,24 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+  int? value;
+}
+
+class NestedRequest {
+  String? context;
+  SetRequest? request;
+}
+
+@HostApi()
+abstract class Api {
+  void setValue(SetRequest request);
+}
+
+@HostApi()
+abstract class NestedApi {
+  void setValueWithContext(NestedRequest request);
+}
diff --git a/packages/pigeon/pigeons/async_handlers.dart b/packages/pigeon/pigeons/async_handlers.dart
new file mode 100644
index 0000000..6e2cb02
--- /dev/null
+++ b/packages/pigeon/pigeons/async_handlers.dart
@@ -0,0 +1,23 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Value {
+  int? number;
+}
+
+@HostApi()
+abstract class Api2Host {
+  @async
+  Value calculate(Value value);
+  @async
+  void voidVoid();
+}
+
+@FlutterApi()
+abstract class Api2Flutter {
+  @async
+  Value calculate(Value value);
+}
diff --git a/packages/pigeon/pigeons/flutter_unittests.dart b/packages/pigeon/pigeons/flutter_unittests.dart
new file mode 100644
index 0000000..acc28b8
--- /dev/null
+++ b/packages/pigeon/pigeons/flutter_unittests.dart
@@ -0,0 +1,30 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+  String? query;
+}
+
+class SearchReply {
+  String? result;
+  String? error;
+}
+
+class SearchRequests {
+  // ignore: always_specify_types
+  List? requests;
+}
+
+class SearchReplies {
+  // ignore: always_specify_types
+  List? replies;
+}
+
+@HostApi()
+abstract class Api {
+  SearchReply search(SearchRequest request);
+  SearchReplies doSearches(SearchRequests request);
+}
diff --git a/packages/pigeon/pigeons/host2flutter.dart b/packages/pigeon/pigeons/host2flutter.dart
new file mode 100644
index 0000000..5a45975
--- /dev/null
+++ b/packages/pigeon/pigeons/host2flutter.dart
@@ -0,0 +1,24 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+  String? query;
+}
+
+class SearchReply {
+  String? result;
+}
+
+@FlutterApi()
+abstract class Api {
+  SearchReply search(SearchRequest request);
+}
+
+void configurePigeon(PigeonOptions options) {
+  options.objcOptions ??= ObjcOptions();
+  options.objcOptions?.prefix = 'AC';
+}
diff --git a/packages/pigeon/pigeons/java_double_host_api.dart b/packages/pigeon/pigeons/java_double_host_api.dart
new file mode 100644
index 0000000..b220947
--- /dev/null
+++ b/packages/pigeon/pigeons/java_double_host_api.dart
@@ -0,0 +1,17 @@
+import 'package:pigeon/pigeon.dart';
+
+class Response {
+  int? result;
+}
+
+@HostApi()
+abstract class BridgeApi1 {
+  @async
+  Response call();
+}
+
+@HostApi()
+abstract class BridgeApi2 {
+  @async
+  Response call();
+}
diff --git a/packages/pigeon/pigeons/list.dart b/packages/pigeon/pigeons/list.dart
new file mode 100644
index 0000000..e370ae8
--- /dev/null
+++ b/packages/pigeon/pigeons/list.dart
@@ -0,0 +1,15 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class TestMessage {
+  // ignore: always_specify_types
+  List? testList;
+}
+
+@HostApi()
+abstract class TestApi {
+  void test(TestMessage msg);
+}
diff --git a/packages/pigeon/pigeons/message.dart b/packages/pigeon/pigeons/message.dart
new file mode 100644
index 0000000..449debf
--- /dev/null
+++ b/packages/pigeon/pigeons/message.dart
@@ -0,0 +1,48 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file is an example pigeon file that is used in compilation, unit, mock
+// handler, and e2e tests.
+
+import 'package:pigeon/java_generator.dart';
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/pigeon.dart';
+
+class SearchRequest {
+  String? query;
+  int? anInt;
+  bool? aBool;
+}
+
+class SearchReply {
+  String? result;
+  String? error;
+}
+
+@HostApi(dartHostTestHandler: 'TestHostApi')
+abstract class Api {
+  void initialize();
+  SearchReply search(SearchRequest request);
+}
+
+class Nested {
+  SearchRequest? request;
+}
+
+@HostApi(dartHostTestHandler: 'TestNestedApi')
+abstract class NestedApi {
+  SearchReply search(Nested nested);
+}
+
+void configurePigeon(PigeonOptions options) {
+  options.objcOptions ??= ObjcOptions();
+  options.objcOptions?.prefix = 'AC';
+  options.javaOptions ??= JavaOptions(className: 'Pigeon');
+  options.javaOptions?.package = 'dev.flutter.aaclarke.pigeon';
+}
+
+@FlutterApi()
+abstract class FlutterSearchApi {
+  SearchReply search(SearchRequest request);
+}
diff --git a/packages/pigeon/pigeons/void_arg_flutter.dart b/packages/pigeon/pigeons/void_arg_flutter.dart
new file mode 100644
index 0000000..d6fbd54
--- /dev/null
+++ b/packages/pigeon/pigeons/void_arg_flutter.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Result {
+  int? code;
+}
+
+@FlutterApi()
+abstract class Api {
+  Result getCode();
+}
diff --git a/packages/pigeon/pigeons/void_arg_host.dart b/packages/pigeon/pigeons/void_arg_host.dart
new file mode 100644
index 0000000..fec04ab
--- /dev/null
+++ b/packages/pigeon/pigeons/void_arg_host.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class Result {
+  int? code;
+}
+
+@HostApi()
+abstract class Api {
+  Result getCode();
+}
diff --git a/packages/pigeon/pigeons/voidflutter.dart b/packages/pigeon/pigeons/voidflutter.dart
new file mode 100644
index 0000000..eb2faa9
--- /dev/null
+++ b/packages/pigeon/pigeons/voidflutter.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+  int? value;
+}
+
+@FlutterApi()
+abstract class Api {
+  void setValue(SetRequest request);
+}
diff --git a/packages/pigeon/pigeons/voidhost.dart b/packages/pigeon/pigeons/voidhost.dart
new file mode 100644
index 0000000..cdbbebd
--- /dev/null
+++ b/packages/pigeon/pigeons/voidhost.dart
@@ -0,0 +1,14 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+class SetRequest {
+  int? value;
+}
+
+@HostApi()
+abstract class Api {
+  void setValue(SetRequest request);
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/pigeon/platform_tests/android_unit_tests/.metadata b/packages/pigeon/platform_tests/android_unit_tests/.metadata
new file mode 100644
index 0000000..62b61c3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 891511d58f6550ce9e9b03b8d7c6a602caa97488
+  channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/android_unit_tests/README.md b/packages/pigeon/platform_tests/android_unit_tests/README.md
new file mode 100644
index 0000000..e109aa5
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/README.md
@@ -0,0 +1,3 @@
+# android_unit_tests
+
+Unit-tests for Pigeon generated Java code.  See [../../run_tests.sh](../../run_tests.sh).
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore b/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
new file mode 100644
index 0000000..f45bc0b
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/build.gradle
@@ -0,0 +1,65 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 30
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.android_unit_tests"
+        minSdkVersion 16
+        targetSdkVersion 30
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    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
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    testImplementation 'junit:junit:4.+'
+    testImplementation "org.mockito:mockito-core:3.+"
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a945186
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+   <application
+        android:label="android_unit_tests"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <!-- Displays an Android View that continues showing the launch screen
+                 Drawable until Flutter paints its first frame, then this splash
+                 screen fades out. A splash screen is useful to avoid any visual
+                 gap between the end of Android's launch screen and the painting of
+                 Flutter's first frame. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.SplashScreenDrawable"
+              android:resource="@drawable/launch_background"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
new file mode 100644
index 0000000..6219796
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java
@@ -0,0 +1,81 @@
+// Autogenerated from Pigeon (v0.2.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.example.android_unit_tests;
+
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class Pigeon {
+
+  /** Generated class from Pigeon that represents data sent in messages. */
+  public static class SetRequest {
+    private Long value;
+
+    public Long getValue() {
+      return value;
+    }
+
+    public void setValue(Long setterArg) {
+      this.value = setterArg;
+    }
+
+    Map<String, Object> toMap() {
+      Map<String, Object> toMapResult = new HashMap<>();
+      toMapResult.put("value", value);
+      return toMapResult;
+    }
+
+    static SetRequest fromMap(Map<String, Object> map) {
+      SetRequest fromMapResult = new SetRequest();
+      Object value = map.get("value");
+      fromMapResult.value =
+          (value == null) ? null : ((value instanceof Integer) ? (Integer) value : (Long) value);
+      return fromMapResult;
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface Api {
+    void setValue(SetRequest arg);
+
+    /** Sets up an instance of `Api` to handle messages through the `binaryMessenger`. */
+    static void setup(BinaryMessenger binaryMessenger, Api api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.Api.setValue", new StandardMessageCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                Map<String, Object> wrapped = new HashMap<>();
+                try {
+                  @SuppressWarnings("ConstantConditions")
+                  SetRequest input = SetRequest.fromMap((Map<String, Object>) message);
+                  api.setValue(input);
+                  wrapped.put("result", null);
+                } catch (Error | RuntimeException exception) {
+                  wrapped.put("error", wrapError(exception));
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+
+  private static Map<String, Object> wrapError(Throwable exception) {
+    Map<String, Object> errorMap = new HashMap<>();
+    errorMap.put("message", exception.toString());
+    errorMap.put("code", exception.getClass().getSimpleName());
+    errorMap.put("details", null);
+    return errorMap;
+  }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
new file mode 100644
index 0000000..eb1afff
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/kotlin/com/example/android_unit_tests/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.android_unit_tests
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a60e5e3
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..0102266
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android_unit_tests">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
new file mode 100644
index 0000000..1efa547
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/app/src/test/java/com/example/android_unit_tests/PigeonTest.java
@@ -0,0 +1,98 @@
+package com.example.android_unit_tests;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class PigeonTest {
+  private MessageCodec<Object> codec = StandardMessageCodec.INSTANCE;
+
+  @Test
+  public void toMapAndBack() {
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    Map<String, Object> map = request.toMap();
+    Pigeon.SetRequest readRequest = Pigeon.SetRequest.fromMap(map);
+    assertEquals(request.getValue(), readRequest.getValue());
+  }
+
+  @Test
+  public void toMapAndBackNested() {
+    Pigeon.NestedRequest nested = new Pigeon.NestedRequest();
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    nested.setRequest(request);
+    Map<String, Object> map = nested.toMap();
+    Pigeon.NestedRequest readNested = Pigeon.NestedRequest.fromMap(map);
+    assertEquals(nested.getRequest().getValue(), readNested.getRequest().getValue());
+  }
+
+  @Test
+  public void clearsHandler() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<String> channelName = ArgumentCaptor.forClass(String.class);
+    verify(binaryMessenger).setMessageHandler(channelName.capture(), isNotNull());
+    Pigeon.Api.setup(binaryMessenger, null);
+    verify(binaryMessenger).setMessageHandler(eq(channelName.getValue()), isNull());
+  }
+
+  /** Causes an exception in the handler by passing in null when a SetRequest is expected. */
+  @Test
+  public void errorMessage() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+    ByteBuffer message = codec.encodeMessage(null);
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("error"));
+            });
+  }
+
+  @Test
+  public void callsVoidMethod() {
+    Pigeon.Api mockApi = mock(Pigeon.Api.class);
+    BinaryMessenger binaryMessenger = mock(BinaryMessenger.class);
+    Pigeon.Api.setup(binaryMessenger, mockApi);
+    ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> handler =
+        ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+    verify(binaryMessenger).setMessageHandler(anyString(), handler.capture());
+    Pigeon.SetRequest request = new Pigeon.SetRequest();
+    request.setValue(1234l);
+    ByteBuffer message = codec.encodeMessage(request.toMap());
+    message.rewind();
+    handler
+        .getValue()
+        .onMessage(
+            message,
+            (bytes) -> {
+              bytes.rewind();
+              @SuppressWarnings("unchecked")
+              Map<String, Object> wrapped = (Map<String, Object>) codec.decodeMessage(bytes);
+              assertTrue(wrapped.containsKey("result"));
+              assertNull(wrapped.get("result"));
+            });
+    ArgumentCaptor<Pigeon.SetRequest> receivedRequest =
+        ArgumentCaptor.forClass(Pigeon.SetRequest.class);
+    verify(mockApi).setValue(receivedRequest.capture());
+    assertEquals(request.getValue(), receivedRequest.getValue().getValue());
+  }
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
new file mode 100644
index 0000000..8a8ae3d
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/build.gradle
@@ -0,0 +1,34 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+    gradle.projectsEvaluated {
+        tasks.withType(JavaCompile) {
+            options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+        }
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle b/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
new file mode 100644
index 0000000..5e49fc1
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/lib/main.dart
@@ -0,0 +1,5 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+void main() {}
diff --git a/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..febf7cf
--- /dev/null
+++ b/packages/pigeon/platform_tests/android_unit_tests/pubspec.yaml
@@ -0,0 +1,20 @@
+name: android_unit_tests
+description: Unit tests for Pigeon generated Java code.
+
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^1.0.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.gitignore b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.metadata b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.metadata
new file mode 100644
index 0000000..5db57c1
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 6db3d61ed4a58ba89140d7fe1fd294b598cc29c5
+  channel: master
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md
new file mode 100644
index 0000000..3c52fc9
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/README.md
@@ -0,0 +1,3 @@
+# flutter_unit_tests
+
+Unit test scaffold for null safe Flutter projects.
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
new file mode 100644
index 0000000..5154847
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
@@ -0,0 +1,133 @@
+// Autogenerated from Pigeon (v0.2.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/services.dart';
+
+class SearchReply {
+  String? result;
+  String? error;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['result'] = result;
+    pigeonMap['error'] = error;
+    return pigeonMap;
+  }
+
+  static SearchReply decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchReply()
+      ..result = pigeonMap['result'] as String?
+      ..error = pigeonMap['error'] as String?;
+  }
+}
+
+class SearchRequest {
+  String? query;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['query'] = query;
+    return pigeonMap;
+  }
+
+  static SearchRequest decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchRequest()..query = pigeonMap['query'] as String?;
+  }
+}
+
+class SearchReplies {
+  List<Object?>? replies;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['replies'] = replies;
+    return pigeonMap;
+  }
+
+  static SearchReplies decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchReplies()..replies = pigeonMap['replies'] as List<Object?>?;
+  }
+}
+
+class SearchRequests {
+  List<Object?>? requests;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['requests'] = requests;
+    return pigeonMap;
+  }
+
+  static SearchRequests decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return SearchRequests()..requests = pigeonMap['requests'] as List<Object?>?;
+  }
+}
+
+class Api {
+  /// Constructor for [Api].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  Api({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  Future<SearchReply> search(SearchRequest arg) async {
+    final Object encoded = arg.encode();
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.Api.search', const StandardMessageCodec(),
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(encoded) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return SearchReply.decode(replyMap['result']!);
+    }
+  }
+
+  Future<SearchReplies> doSearches(SearchRequests arg) async {
+    final Object encoded = arg.encode();
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.Api.doSearches', const StandardMessageCodec(),
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(encoded) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+        details: null,
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return SearchReplies.decode(replyMap['result']!);
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..9b21234
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/pubspec.yaml
@@ -0,0 +1,19 @@
+name: flutter_unit_tests
+description: Unit test scaffold for null safe Flutter projects.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  build_runner: ^1.11.0
+  flutter_test:
+    sdk: flutter
+  mockito: ^5.0.4
+
+flutter:
+  uses-material-design: true
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
new file mode 100644
index 0000000..c407fd9
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.dart
@@ -0,0 +1,74 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_unit_tests/null_safe_pigeon.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'null_safe_test.mocks.dart';
+
+@GenerateMocks(<Type>[BinaryMessenger])
+void main() {
+  test('with values filled', () {
+    final SearchReply reply = SearchReply()
+      ..result = 'foo'
+      ..error = 'bar';
+    final Object encoded = reply.encode();
+    final SearchReply decoded = SearchReply.decode(encoded);
+    expect(reply.result, decoded.result);
+    expect(reply.error, decoded.error);
+  });
+
+  test('with null value', () {
+    final SearchReply reply = SearchReply()
+      ..result = 'foo'
+      ..error = null;
+    final Object encoded = reply.encode();
+    final SearchReply decoded = SearchReply.decode(encoded);
+    expect(reply.result, decoded.result);
+    expect(reply.error, decoded.error);
+  });
+
+  test('send/receive', () async {
+    final SearchRequest request = SearchRequest()..query = 'hey';
+    final SearchReply reply = SearchReply()..result = 'ho';
+    final BinaryMessenger mockMessenger = MockBinaryMessenger();
+    const MessageCodec<Object?> codec = StandardMessageCodec();
+    final Completer<ByteData?> completer = Completer<ByteData?>();
+    completer.complete(
+        codec.encodeMessage(<String, Object>{'result': reply.encode()}));
+    final Future<ByteData?> sendResult = completer.future;
+    when(mockMessenger.send('dev.flutter.pigeon.Api.search', any))
+        .thenAnswer((Invocation realInvocation) => sendResult);
+    final Api api = Api(binaryMessenger: mockMessenger);
+    final SearchReply readReply = await api.search(request);
+    expect(readReply, isNotNull);
+    expect(reply.result, readReply.result);
+  });
+
+  // TODO(gaaclarke): This test is a companion for the fix to https://github.com/flutter/flutter/issues/80538
+  // test('send/receive list classes', () async {
+  //   final SearchRequest request = SearchRequest()
+  //       ..query = 'hey';
+  //   final SearchReply reply = SearchReply()
+  //       ..result = 'ho';
+  //   final SearchRequests requests = SearchRequests()
+  //       ..requests = <SearchRequest>[request];
+  //   final SearchReplies replies = SearchReplies()
+  //       ..replies = <SearchReply>[reply];
+  //   final BinaryMessenger mockMessenger = MockBinaryMessenger();
+  //   const MessageCodec<Object?> codec = StandardMessageCodec();
+  //   final Completer<ByteData?> completer = Completer<ByteData?>();
+  //   completer.complete(codec.encodeMessage(<String, Object>{'result' : replies.encode()}));
+  //   final Future<ByteData?> sendResult = completer.future;
+  //   when(mockMessenger.send('dev.flutter.pigeon.Api.search', any)).thenAnswer((Invocation realInvocation) => sendResult);
+  //   final Api api = Api(binaryMessenger: mockMessenger);
+  //   final SearchReplies readReplies = await api.doSearches(requests);
+  //   expect(readReplies, isNotNull);
+  //   expect(reply.result, (readReplies.replies![0] as SearchReply?)!.result);
+  // });
+}
diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
new file mode 100644
index 0000000..5a326b5
--- /dev/null
+++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/test/null_safe_test.mocks.dart
@@ -0,0 +1,55 @@
+// Mocks generated by Mockito 5.0.4 from annotations
+// in flutter_unit_tests/test/null_safe_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i3;
+import 'dart:typed_data' as _i4;
+import 'dart:ui' as _i5;
+
+import 'package:flutter/src/services/binary_messenger.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: comment_references
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: always_specify_types
+
+/// A class which mocks [BinaryMessenger].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBinaryMessenger extends _i1.Mock implements _i2.BinaryMessenger {
+  MockBinaryMessenger() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i3.Future<void> handlePlatformMessage(String? channel, _i4.ByteData? data,
+          _i5.PlatformMessageResponseCallback? callback) =>
+      (super.noSuchMethod(
+          Invocation.method(#handlePlatformMessage, [channel, data, callback]),
+          returnValue: Future<void>.value(null),
+          returnValueForMissingStub:
+              Future<dynamic>.value()) as _i3.Future<void>);
+  @override
+  _i3.Future<_i4.ByteData?>? send(String? channel, _i4.ByteData? message) =>
+      (super.noSuchMethod(Invocation.method(#send, [channel, message]))
+          as _i3.Future<_i4.ByteData?>?);
+  @override
+  void setMessageHandler(String? channel, _i2.MessageHandler? handler) => super
+      .noSuchMethod(Invocation.method(#setMessageHandler, [channel, handler]),
+          returnValueForMissingStub: null);
+  @override
+  bool checkMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+      (super.noSuchMethod(
+          Invocation.method(#checkMessageHandler, [channel, handler]),
+          returnValue: false) as bool);
+  @override
+  void setMockMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+      super.noSuchMethod(
+          Invocation.method(#setMockMessageHandler, [channel, handler]),
+          returnValueForMissingStub: null);
+  @override
+  bool checkMockMessageHandler(String? channel, _i2.MessageHandler? handler) =>
+      (super.noSuchMethod(
+          Invocation.method(#checkMockMessageHandler, [channel, handler]),
+          returnValue: false) as bool);
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/.gitignore b/packages/pigeon/platform_tests/ios_unit_tests/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/.gitignore
@@ -0,0 +1,37 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/.metadata b/packages/pigeon/platform_tests/ios_unit_tests/.metadata
new file mode 100644
index 0000000..01d2dcb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 0b8abb4724aa590dd0f429683339b1e045a1594d
+  channel: stable
+
+project_type: app
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/README.md b/packages/pigeon/platform_tests/ios_unit_tests/README.md
new file mode 100644
index 0000000..3b17696
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/README.md
@@ -0,0 +1,16 @@
+# ios_unit_tests
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/.gitignore b/packages/pigeon/platform_tests/ios_unit_tests/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..cca797a
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,667 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		0D50126D23FF759100CD5B95 /* messages.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50126B23FF759100CD5B95 /* messages.m */; };
+		0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; };
+		0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35E725D45A3000B76435 /* async_handlers.m */; };
+		0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */; };
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		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 PBXContainerItemProxy section */
+		0D50127723FF75B100CD5B95 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+			remoteInfo = Runner;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		0D50126B23FF759100CD5B95 /* messages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = messages.m; sourceTree = "<group>"; };
+		0D50126C23FF759100CD5B95 /* messages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = messages.h; sourceTree = "<group>"; };
+		0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+		0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		0D8C35E625D45A3000B76435 /* async_handlers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_handlers.h; sourceTree = "<group>"; };
+		0D8C35E725D45A3000B76435 /* async_handlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = async_handlers.m; sourceTree = "<group>"; };
+		0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncHandlersTest.m; sourceTree = "<group>"; };
+		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>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; 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>"; };
+		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 */
+		0D50126F23FF75B100CD5B95 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		0D50127323FF75B100CD5B95 /* RunnerTests */ = {
+			isa = PBXGroup;
+			children = (
+				0D50127423FF75B100CD5B95 /* RunnerTests.m */,
+				0D50127623FF75B100CD5B95 /* Info.plist */,
+				0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */,
+			);
+			path = RunnerTests;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				0D50127323FF75B100CD5B95 /* RunnerTests */,
+				97C146EF1CF9000F007C117D /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+				0D50127223FF75B100CD5B95 /* RunnerTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				0D8C35E625D45A3000B76435 /* async_handlers.h */,
+				0D8C35E725D45A3000B76435 /* async_handlers.m */,
+				0D50126C23FF759100CD5B95 /* messages.h */,
+				0D50126B23FF759100CD5B95 /* messages.m */,
+				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 */
+		0D50127123FF75B100CD5B95 /* RunnerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 0D50127923FF75B100CD5B95 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+			buildPhases = (
+				0D50126E23FF75B100CD5B95 /* Sources */,
+				0D50126F23FF75B100CD5B95 /* Frameworks */,
+				0D50127023FF75B100CD5B95 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				0D50127823FF75B100CD5B95 /* PBXTargetDependency */,
+			);
+			name = RunnerTests;
+			productName = RunnerTests;
+			productReference = 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		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 = 1020;
+				ORGANIZATIONNAME = "The Chromium Authors";
+				TargetAttributes = {
+					0D50127123FF75B100CD5B95 = {
+						CreatedOnToolsVersion = 11.3;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 97C146ED1CF9000F007C117D;
+					};
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+				0D50127123FF75B100CD5B95 /* RunnerTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		0D50127023FF75B100CD5B95 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets 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\" embed_and_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 */
+		0D50126E23FF75B100CD5B95 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */,
+				0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
+				0D50126D23FF759100CD5B95 /* messages.m in Sources */,
+				97C146F31CF9000F007C117D /* main.m in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+				0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		0D50127823FF75B100CD5B95 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 97C146ED1CF9000F007C117D /* Runner */;
+			targetProxy = 0D50127723FF75B100CD5B95 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency 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 */
+		0D50127A23FF75B100CD5B95 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Debug;
+		};
+		0D50127B23FF75B100CD5B95 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Release;
+		};
+		0D50127C23FF75B100CD5B95 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = RunnerTests/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 13.2;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = com.google.aaclarke.RunnerTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
+			};
+			name = Profile;
+		};
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				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.iosUnitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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;
+				SUPPORTED_PLATFORMS = 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 = "$(FLUTTER_BUILD_NUMBER)";
+				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.iosUnitTests;
+				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 = "$(FLUTTER_BUILD_NUMBER)";
+				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.iosUnitTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		0D50127923FF75B100CD5B95 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				0D50127A23FF75B100CD5B95 /* Debug */,
+				0D50127B23FF75B100CD5B95 /* Release */,
+				0D50127C23FF75B100CD5B95 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..97e0ecb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "0D50127123FF75B100CD5B95"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      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>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
new file mode 100644
index 0000000..606dad1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerTests.xcscheme
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1130"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "0D50127123FF75B100CD5B95"
+               BuildableName = "RunnerTests.xctest"
+               BlueprintName = "RunnerTests"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h
new file mode 100644
index 0000000..36e21bb
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.h
@@ -0,0 +1,6 @@
+#import <Flutter/Flutter.h>
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : FlutterAppDelegate
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m
new file mode 100644
index 0000000..70e8393
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/AppDelegate.m
@@ -0,0 +1,13 @@
+#import "AppDelegate.h"
+#import "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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..ec8c2c5
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,104 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "3x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-App-60x60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "83.5x83.5",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "size" : "1024x1024",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..3efa7d1
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "LaunchImage.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}
\ No newline at end of file
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist
new file mode 100644
index 0000000..444570d
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>ios_unit_tests</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
new file mode 100644
index 0000000..a69bdb6
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
@@ -0,0 +1,27 @@
+// Autogenerated from Pigeon (v0.1.20), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class Value;
+
+@interface Value : NSObject
+@property(nonatomic, strong, nullable) NSNumber *number;
+@end
+
+@interface Api2Flutter : NSObject
+- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
+- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion;
+@end
+@protocol Api2Host
+- (void)calculate:(nullable Value *)input
+       completion:(void (^)(Value *_Nullable, FlutterError *_Nullable))completion;
+@end
+
+extern void Api2HostSetup(id<FlutterBinaryMessenger> binaryMessenger, id<Api2Host> _Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
new file mode 100644
index 0000000..73f062e
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
@@ -0,0 +1,88 @@
+// Autogenerated from Pigeon (v0.1.20), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "async_handlers.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary<NSString *, id> *wrapResult(NSDictionary *result, FlutterError *error) {
+  NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+  if (error) {
+    errorDict = @{
+      @"code" : (error.code ? error.code : [NSNull null]),
+      @"message" : (error.message ? error.message : [NSNull null]),
+      @"details" : (error.details ? error.details : [NSNull null]),
+    };
+  }
+  return @{
+    @"result" : (result ? result : [NSNull null]),
+    @"error" : errorDict,
+  };
+}
+
+@interface Value ()
++ (Value *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
+
+@implementation Value
++ (Value *)fromMap:(NSDictionary *)dict {
+  Value *result = [[Value alloc] init];
+  result.number = dict[@"number"];
+  if ((NSNull *)result.number == [NSNull null]) {
+    result.number = nil;
+  }
+  return result;
+}
+- (NSDictionary *)toMap {
+  return [NSDictionary
+      dictionaryWithObjectsAndKeys:(self.number ? self.number : [NSNull null]), @"number", nil];
+}
+@end
+
+@interface Api2Flutter ()
+@property(nonatomic, strong) NSObject<FlutterBinaryMessenger> *binaryMessenger;
+@end
+
+@implementation Api2Flutter
+- (instancetype)initWithBinaryMessenger:(NSObject<FlutterBinaryMessenger> *)binaryMessenger {
+  self = [super init];
+  if (self) {
+    _binaryMessenger = binaryMessenger;
+  }
+  return self;
+}
+
+- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion {
+  FlutterBasicMessageChannel *channel =
+      [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Flutter.calculate"
+                                         binaryMessenger:self.binaryMessenger];
+  NSDictionary *inputMap = [input toMap];
+  [channel sendMessage:inputMap
+                 reply:^(id reply) {
+                   NSDictionary *outputMap = reply;
+                   Value *output = [Value fromMap:outputMap];
+                   completion(output, nil);
+                 }];
+}
+@end
+void Api2HostSetup(id<FlutterBinaryMessenger> binaryMessenger, id<Api2Host> api) {
+  {
+    FlutterBasicMessageChannel *channel =
+        [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Host.calculate"
+                                           binaryMessenger:binaryMessenger];
+    if (api) {
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        Value *input = [Value fromMap:message];
+        [api calculate:input
+            completion:^(Value *_Nullable output, FlutterError *_Nullable error) {
+              callback(wrapResult([output toMap], error));
+            }];
+      }];
+    } else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#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/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m
new file mode 100644
index 0000000..f32e046
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m
@@ -0,0 +1,139 @@
+#import <Flutter/Flutter.h>
+#import <XCTest/XCTest.h>
+#import "async_handlers.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface Value ()
++ (Value*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface MockBinaryMessenger : NSObject<FlutterBinaryMessenger>
+@property(nonatomic, copy) NSNumber* result;
+@property(nonatomic, retain) FlutterStandardMessageCodec* codec;
+@property(nonatomic, retain) NSMutableDictionary<NSString*, FlutterBinaryMessageHandler>* handlers;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation MockBinaryMessenger
+
+- (instancetype)init {
+  self = [super init];
+  if (self) {
+    _codec = [FlutterStandardMessageCodec sharedInstance];
+    _handlers = [[NSMutableDictionary alloc] init];
+  }
+  return self;
+}
+
+- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection {
+}
+
+- (void)sendOnChannel:(nonnull NSString*)channel message:(NSData* _Nullable)message {
+}
+
+- (void)sendOnChannel:(nonnull NSString*)channel
+              message:(NSData* _Nullable)message
+          binaryReply:(FlutterBinaryReply _Nullable)callback {
+  if (self.result) {
+    Value* output = [[Value alloc] init];
+    output.number = self.result;
+    NSDictionary* outputDictionary = [output toMap];
+    callback([_codec encode:outputDictionary]);
+  }
+}
+
+- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString*)channel
+                                          binaryMessageHandler:
+                                              (FlutterBinaryMessageHandler _Nullable)handler {
+  _handlers[channel] = [handler copy];
+  return _handlers.count;
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface MockApi2Host : NSObject<Api2Host>
+@property(nonatomic, copy) NSNumber* output;
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation MockApi2Host
+
+- (void)calculate:(Value* _Nullable)input
+       completion:(nonnull void (^)(Value* _Nullable, FlutterError* _Nullable))completion {
+  if (self.output) {
+    Value* output = [[Value alloc] init];
+    output.number = self.output;
+    completion(output, nil);
+  } else {
+    completion(nil, [FlutterError errorWithCode:@"hey" message:@"ho" details:nil]);
+  }
+}
+
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@interface AsyncHandlersTest : XCTestCase
+@end
+
+///////////////////////////////////////////////////////////////////////////////////////////
+@implementation AsyncHandlersTest
+
+- (void)testAsyncHost2Flutter {
+  MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+  binaryMessenger.result = @(2);
+  Api2Flutter* api2Flutter = [[Api2Flutter alloc] initWithBinaryMessenger:binaryMessenger];
+  Value* input = [[Value alloc] init];
+  input.number = @(1);
+  XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+  [api2Flutter calculate:input
+              completion:^(Value* _Nonnull output, NSError* _Nullable error) {
+                XCTAssertEqual(output.number.intValue, 2);
+                [expectation fulfill];
+              }];
+  [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+- (void)testAsyncFlutter2Host {
+  MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+  MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init];
+  mockApi2Host.output = @(2);
+  Api2HostSetup(binaryMessenger, mockApi2Host);
+  NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate";
+  XCTAssertNotNil(binaryMessenger.handlers[channelName]);
+
+  Value* input = [[Value alloc] init];
+  input.number = @(1);
+  NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]];
+  XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+  binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) {
+    NSDictionary* outputMap = [binaryMessenger.codec decode:data];
+    Value* output = [Value fromMap:outputMap[@"result"]];
+    XCTAssertEqual(output.number.intValue, 2);
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+- (void)testAsyncFlutter2HostError {
+  MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init];
+  MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init];
+  Api2HostSetup(binaryMessenger, mockApi2Host);
+  NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate";
+  XCTAssertNotNil(binaryMessenger.handlers[channelName]);
+
+  Value* input = [[Value alloc] init];
+  input.number = @(1);
+  NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]];
+  XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"];
+  binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) {
+    NSDictionary* outputMap = [binaryMessenger.codec decode:data];
+    XCTAssertNotNil(outputMap[@"error"]);
+    [expectation fulfill];
+  });
+  [self waitForExpectationsWithTimeout:1.0 handler:nil];
+}
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist
new file mode 100644
index 0000000..64d65ca
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/Info.plist
@@ -0,0 +1,22 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..f077fd4
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,39 @@
+#import <XCTest/XCTest.h>
+#import "messages.h"
+
+@interface ACSearchReply ()
++ (ACSearchReply*)fromMap:(NSDictionary*)dict;
+- (NSDictionary*)toMap;
+@end
+
+@interface RunnerTests : XCTestCase
+
+@end
+
+@implementation RunnerTests
+
+- (void)testToMapAndBack {
+  ACSearchReply* reply = [[ACSearchReply alloc] init];
+  reply.result = @"foobar";
+  NSDictionary* dict = [reply toMap];
+  ACSearchReply* copy = [ACSearchReply fromMap:dict];
+  XCTAssertEqual(reply.result, copy.result);
+}
+
+- (void)testHandlesNull {
+  ACSearchReply* reply = [[ACSearchReply alloc] init];
+  reply.result = nil;
+  NSDictionary* dict = [reply toMap];
+  ACSearchReply* copy = [ACSearchReply fromMap:dict];
+  XCTAssertNil(copy.result);
+}
+
+- (void)testHandlesNullFirst {
+  ACSearchReply* reply = [[ACSearchReply alloc] init];
+  reply.error = @"foobar";
+  NSDictionary* dict = [reply toMap];
+  ACSearchReply* copy = [ACSearchReply fromMap:dict];
+  XCTAssertEqual(reply.error, copy.error);
+}
+
+@end
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart b/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart
new file mode 100644
index 0000000..e89b0e7
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/lib/main.dart
@@ -0,0 +1,14 @@
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+/// An empty app.
+class MyApp extends StatelessWidget {
+  /// Creates an empty app.
+  const MyApp({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+}
diff --git a/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml b/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml
new file mode 100644
index 0000000..e5e7de4
--- /dev/null
+++ b/packages/pigeon/platform_tests/ios_unit_tests/pubspec.yaml
@@ -0,0 +1,69 @@
+name: ios_unit_tests
+description: A new Flutter project.
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^0.1.2
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/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 material Icons class.
+  uses-material-design: true
+
+  # To add assets to your application, add an assets section, like this:
+  # assets:
+  #  - images/a_dot_burr.jpeg
+  #  - images/a_dot_ham.jpeg
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # 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: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
new file mode 100644
index 0000000..06278fc
--- /dev/null
+++ b/packages/pigeon/pubspec.yaml
@@ -0,0 +1,11 @@
+name: pigeon
+version: 0.2.1 # This must match the version in lib/generator_tools.dart
+description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
+homepage: https://github.com/flutter/packages/tree/master/packages/pigeon
+dependencies:
+  args: ^2.0.0
+  path: ^1.8.0
+dev_dependencies:
+  test: ^1.11.1
+environment:
+  sdk: '>=2.12.0 <3.0.0'
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
new file mode 100755
index 0000000..07d38bb
--- /dev/null
+++ b/packages/pigeon/run_tests.sh
@@ -0,0 +1,454 @@
+#!/bin/bash
+###############################################################################
+# run_tests.sh
+#
+# This runs all the different types of tests for pigeon.  It should be run from
+# the directory that contains the script.
+###############################################################################
+
+# exit when any command fails
+set -e
+
+# TODO(blasten): Enable on stable when possible.
+# https://github.com/flutter/flutter/issues/75187
+if [[ "$CHANNEL" == "stable" ]]; then
+  exit 0
+fi
+
+###############################################################################
+# Variables
+###############################################################################
+flutter=$(which flutter)
+flutter_bin=$(dirname $flutter)
+framework_path="$flutter_bin/cache/artifacts/engine/ios/"
+
+java_linter=checkstyle-8.41-all.jar
+java_formatter=google-java-format-1.3-all-deps.jar
+google_checks=google_checks.xml
+google_checks_version=7190c47ca5515ad8cb827bc4065ae7664d2766c1
+java_error_prone=error_prone_core-2.5.1-with-dependencies.jar
+dataflow_shaded=dataflow-shaded-3.7.1.jar
+jformat_string=jFormatString-3.0.0.jar
+java_version=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1)
+javac_jar=javac-9+181-r4173-1.jar
+if [ $java_version == "8" ]; then
+  javac_bootclasspath="-J-Xbootclasspath/p:ci/$javac_jar"
+else
+  javac_bootclasspath=
+fi
+
+###############################################################################
+# Helper Functions
+###############################################################################
+
+# Create a temporary directory in a way that works on both Linux and macOS.
+#
+# The mktemp commands have slighly semantics on the BSD systems vs GNU systems.
+mktmpdir() {
+  mktemp -d flutter_pigeon.XXXXXX 2>/dev/null || mktemp -d -t flutter_pigeon.
+}
+
+# test_pigeon_ios(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to compile the objc
+# code.
+test_pigeon_ios() {
+  echo "test_pigeon_ios($1)"
+  temp_dir=$(mktmpdir)
+
+  pub run pigeon \
+    --no-dart_null_safety \
+    --input $1 \
+    --dart_out $temp_dir/pigeon.dart \
+    --objc_header_out $temp_dir/pigeon.h \
+    --objc_source_out $temp_dir/pigeon.m
+
+  xcrun clang \
+    -arch arm64 \
+    -isysroot $(xcrun --sdk iphoneos --show-sdk-path) \
+    -F $framework_path \
+    -F $framework_path/Flutter.xcframework/ios-armv7_arm64 \
+    -Werror \
+    -fobjc-arc \
+    -c $temp_dir/pigeon.m \
+    -o $temp_dir/pigeon.o
+
+  rm -rf $temp_dir
+}
+
+# test_pigeon_android(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to compile the java
+# code.
+test_pigeon_android() {
+  echo "test_pigeon_android($1)"
+  temp_dir=$(mktmpdir)
+
+  pub run pigeon \
+    --input $1 \
+    --dart_out $temp_dir/pigeon.dart \
+    --java_out $temp_dir/Pigeon.java \
+    --java_package foo
+
+  java -jar ci/$java_formatter --replace "$temp_dir/Pigeon.java"
+  java -jar ci/$java_linter -c "ci/$google_checks" "$temp_dir/Pigeon.java"
+  if ! javac \
+    $javac_bootclasspath \
+    -XDcompilePolicy=simple \
+    -processorpath "ci/$java_error_prone:ci/$dataflow_shaded:ci/$jformat_string" \
+    '-Xplugin:ErrorProne -Xep:CatchingUnchecked:ERROR' \
+    -classpath "$flutter_bin/cache/artifacts/engine/android-x64/flutter.jar" \
+    $temp_dir/Pigeon.java; then
+    echo "javac $temp_dir/Pigeon.java failed"
+    exit 1
+  fi
+
+  rm -rf $temp_dir
+}
+
+# test_null_safe_dart(<path to pigeon file>)
+#
+# Compiles the pigeon file to a temp directory and attempts to run the dart
+# analyzer on it with and without null safety turned on.
+test_pigeon_dart() {
+  echo "test_pigeon_dart($1)"
+  temp_dir_1=$(mktmpdir)
+  temp_dir_2=$(mktmpdir)
+
+  pub run pigeon \
+    --input $1 \
+    --dart_out $temp_dir_1/pigeon.dart &
+  null_safe_gen_pid=$!
+
+  pub run pigeon \
+    --no-dart_null_safety \
+    --input $1 \
+    --dart_out $temp_dir_2/pigeon.dart &
+  non_null_safe_gen_pid=$!
+
+  wait $null_safe_gen_pid
+  wait $non_null_safe_gen_pid
+
+  # `./e2e_tests/test_objc/.packages` is used to get access to Flutter since
+  # Pigeon doesn't depend on Flutter.
+  dartanalyzer $temp_dir_1/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages &
+  null_safe_analyze_pid=$!
+  dartanalyzer $temp_dir_2/pigeon.dart --fatal-infos --fatal-warnings --packages ./e2e_tests/test_objc/.packages &
+  non_null_safe_analyze_pid=$!
+
+  wait $null_safe_analyze_pid
+  wait $non_null_safe_analyze_pid
+
+  rm -rf $temp_dir_1
+  rm -rf $temp_dir_2
+}
+
+print_usage() {
+  echo "usage: ./run_tests.sh [-l] [-t test_name]
+
+flags:
+  -t test_name: Run only specified test.
+  -l          : List available tests.
+"
+}
+
+###############################################################################
+# Stages
+###############################################################################
+get_java_linter_formatter() {
+  if [ ! -f "ci/$java_linter" ]; then
+    curl -L https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.41/$java_linter >"ci/$java_linter"
+  fi
+  if [ ! -f "ci/$java_formatter" ]; then
+    curl -L https://github.com/google/google-java-format/releases/download/google-java-format-1.3/$java_formatter >"ci/$java_formatter"
+  fi
+  if [ ! -f "ci/$google_checks" ]; then
+    curl -L https://raw.githubusercontent.com/checkstyle/checkstyle/$google_checks_version/src/main/resources/$google_checks >"ci/$google_checks"
+  fi
+  if [ ! -f "ci/$java_error_prone" ]; then
+    curl https://repo1.maven.org/maven2/com/google/errorprone/error_prone_core/2.5.1/$java_error_prone >"ci/$java_error_prone"
+  fi
+  if [ ! -f "ci/$dataflow_shaded" ]; then
+    curl https://repo1.maven.org/maven2/org/checkerframework/dataflow-shaded/3.7.1/$dataflow_shaded >"ci/$dataflow_shaded"
+  fi
+  if [ ! -f "ci/$jformat_string" ]; then
+    curl https://repo1.maven.org/maven2/com/google/code/findbugs/jFormatString/3.0.0/$jformat_string >"ci/$jformat_string"
+  fi
+  if [ ! -f "ci/$javac_jar" ]; then
+    curl https://repo1.maven.org/maven2/com/google/errorprone/javac/9+181-r4173-1/$javac_jar >"ci/$javac_jar"
+  fi
+}
+
+run_dart_unittests() {
+  dart analyze bin
+  dart analyze lib
+  dart test
+}
+
+test_running_without_arguments() {
+  pub run pigeon 1>/dev/null
+}
+
+run_flutter_unittests() {
+  pushd $PWD
+  pub run pigeon \
+    --input pigeons/flutter_unittests.dart \
+    --dart_out platform_tests/flutter_null_safe_unit_tests/lib/null_safe_pigeon.dart
+  cd platform_tests/flutter_null_safe_unit_tests
+  flutter pub get
+  flutter test test/null_safe_test.dart
+  popd
+}
+
+run_mock_handler_tests() {
+  pushd $PWD
+  pub run pigeon \
+    --input pigeons/message.dart \
+    --dart_out mock_handler_tester/test/message.dart \
+    --dart_test_out mock_handler_tester/test/test.dart
+  dartfmt -w mock_handler_tester/test/message.dart
+  dartfmt -w mock_handler_tester/test/test.dart
+  cd mock_handler_tester
+  flutter test
+  popd
+}
+
+run_dart_compilation_tests() {
+  # Make sure the artifacts are present.
+  flutter precache
+  # Make sure flutter dependencies are available.
+  pushd $PWD
+  cd e2e_tests/test_objc/
+  flutter pub get
+  popd
+  test_pigeon_dart ./pigeons/all_datatypes.dart
+  test_pigeon_dart ./pigeons/async_handlers.dart
+  test_pigeon_dart ./pigeons/host2flutter.dart
+  test_pigeon_dart ./pigeons/list.dart
+  test_pigeon_dart ./pigeons/message.dart
+  test_pigeon_dart ./pigeons/void_arg_flutter.dart
+  test_pigeon_dart ./pigeons/void_arg_host.dart
+  test_pigeon_dart ./pigeons/voidflutter.dart
+  test_pigeon_dart ./pigeons/voidhost.dart
+}
+
+run_java_compilation_tests() {
+  # Make sure the artifacts are present.
+  flutter precache
+  # Make sure flutter dependencies are available.
+  pushd $PWD
+  # We use e2e_tests/test_objc in order to get access to Flutter.
+  cd e2e_tests/test_objc/
+  flutter pub get
+  popd
+  test_pigeon_android ./pigeons/all_datatypes.dart
+  test_pigeon_android ./pigeons/async_handlers.dart
+  test_pigeon_android ./pigeons/host2flutter.dart
+  test_pigeon_android ./pigeons/java_double_host_api.dart
+  test_pigeon_android ./pigeons/list.dart
+  test_pigeon_android ./pigeons/message.dart
+  test_pigeon_android ./pigeons/void_arg_flutter.dart
+  test_pigeon_android ./pigeons/void_arg_host.dart
+  test_pigeon_android ./pigeons/voidflutter.dart
+  test_pigeon_android ./pigeons/voidhost.dart
+}
+
+run_objc_compilation_tests() {
+  # Make sure the artifacts are present.
+  flutter precache
+
+  test_pigeon_ios ./pigeons/all_datatypes.dart
+  test_pigeon_ios ./pigeons/async_handlers.dart
+  test_pigeon_ios ./pigeons/host2flutter.dart
+  test_pigeon_ios ./pigeons/list.dart
+  test_pigeon_ios ./pigeons/message.dart
+  test_pigeon_ios ./pigeons/void_arg_flutter.dart
+  test_pigeon_ios ./pigeons/void_arg_host.dart
+  test_pigeon_ios ./pigeons/voidflutter.dart
+  test_pigeon_ios ./pigeons/voidhost.dart
+}
+
+run_ios_unittests() {
+  pub run pigeon \
+    --no-dart_null_safety \
+    --input pigeons/message.dart \
+    --dart_out /dev/null \
+    --objc_header_out platform_tests/ios_unit_tests/ios/Runner/messages.h \
+    --objc_source_out platform_tests/ios_unit_tests/ios/Runner/messages.m
+  pub run pigeon \
+    --no-dart_null_safety \
+    --input pigeons/async_handlers.dart \
+    --dart_out /dev/null \
+    --objc_header_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.h \
+    --objc_source_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
+  clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.h
+  clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.m
+  clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.h
+  clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.m
+  pushd $PWD
+  cd platform_tests/ios_unit_tests
+  flutter build ios --simulator
+  cd ios
+  xcodebuild \
+    -workspace Runner.xcworkspace \
+    -scheme RunnerTests \
+    -sdk iphonesimulator \
+    -destination 'platform=iOS Simulator,name=iPhone 8' \
+    test
+  popd
+}
+
+run_ios_e2e_tests() {
+  DARTLE_H="e2e_tests/test_objc/ios/Runner/dartle.h"
+  DARTLE_M="e2e_tests/test_objc/ios/Runner/dartle.m"
+  DARTLE_DART="e2e_tests/test_objc/lib/dartle.dart"
+  PIGEON_JAVA="e2e_tests/test_objc/android/app/src/main/java/io/flutter/plugins/Pigeon.java"
+  pub run pigeon \
+    --input pigeons/message.dart \
+    --dart_out $DARTLE_DART \
+    --objc_header_out $DARTLE_H \
+    --objc_source_out $DARTLE_M \
+    --java_out $PIGEON_JAVA
+  dartfmt -w $DARTLE_DART
+
+  pushd $PWD
+  cd e2e_tests/test_objc
+  flutter build ios -t test_driver/e2e_test.dart --simulator
+  cd ios
+  xcodebuild \
+    -workspace Runner.xcworkspace \
+    -scheme RunnerTests \
+    -sdk iphonesimulator \
+    -destination 'platform=iOS Simulator,name=iPhone 8' \
+    test
+  popd
+}
+
+run_formatter() {
+  cd ../..
+  pub global activate flutter_plugin_tools && pub global run flutter_plugin_tools format 2>/dev/null
+}
+
+run_android_unittests() {
+  pushd $PWD
+  pub run pigeon \
+    --input pigeons/android_unittests.dart \
+    --dart_out /dev/null \
+    --java_out platform_tests/android_unit_tests/android/app/src/main/java/com/example/android_unit_tests/Pigeon.java \
+    --java_package "com.example.android_unit_tests"
+  
+  cd platform_tests/android_unit_tests
+  if [ ! -f "android/gradlew" ]; then
+    flutter build apk --debug
+  fi
+  cd android
+  ./gradlew test
+  popd
+}
+
+###############################################################################
+# main
+###############################################################################
+should_run_android_unittests=true
+should_run_dart_compilation_tests=true
+should_run_dart_unittests=true
+should_run_flutter_unittests=true
+should_run_formatter=true
+should_run_ios_e2e_tests=true
+should_run_ios_unittests=true
+should_run_java_compilation_tests=true
+should_run_mock_handler_tests=true
+should_run_objc_compilation_tests=true
+while getopts "t:l?h" opt; do
+  case $opt in
+  t)
+    should_run_android_unittests=false
+    should_run_dart_compilation_tests=false
+    should_run_dart_unittests=false
+    should_run_flutter_unittests=false
+    should_run_formatter=false
+    should_run_ios_e2e_tests=false
+    should_run_ios_unittests=false
+    should_run_java_compilation_tests=false
+    should_run_mock_handler_tests=false
+    should_run_objc_compilation_tests=false
+    case $OPTARG in
+    android_unittests) should_run_android_unittests=true ;;
+    dart_compilation_tests) should_run_dart_compilation_tests=true ;;
+    dart_unittests) should_run_dart_unittests=true ;;
+    flutter_unittests) should_run_flutter_unittests=true ;;
+    ios_e2e_tests) should_run_ios_e2e_tests=true ;;
+    ios_unittests) should_run_ios_unittests=true ;;
+    java_compilation_tests) should_run_java_compilation_tests=true ;;
+    mock_handler_tests) should_run_mock_handler_tests=true ;;
+    objc_compilation_tests) should_run_objc_compilation_tests=true ;;
+    *)
+      echo "unrecognized test: $OPTARG"
+      exit 1
+      ;;
+    esac
+    ;;
+  l)
+    echo "available tests for -t:
+  android_unittests      - Unit tests on generated Java code.
+  dart_compilation_tests - Compilation tests on generated Dart code.
+  dart_unittests         - Unit tests on and analysis on Pigeon's implementation.
+  flutter_unittests      - Unit tests on generated Dart code.
+  ios_e2e_tests          - End-to-end objc tests run on iOS Simulator
+  ios_unittests          - Unit tests on generated Objc code.
+  java_compilation_tests - Compilation tests on generated Java code.
+  mock_handler_tests     - Unit tests on generated Dart mock handler code.
+  objc_compilation_tests - Compilation tests on generated Objc code.
+  "
+    exit 1
+    ;;
+  \h)
+    print_usage
+    exit 1
+    ;;
+  \?)
+    print_usage
+    exit 1
+    ;;
+  ?)
+    print_usage
+    exit 1
+    ;;
+  esac
+done
+
+if [ "$should_run_java_compilation_tests" = true ]; then
+  get_java_linter_formatter
+fi
+pub get
+test_running_without_arguments
+if [ "$should_run_dart_unittests" = true ]; then
+  run_dart_unittests
+fi
+if [ "$should_run_flutter_unittests" = true ]; then
+  run_flutter_unittests
+fi
+if [ "$should_run_mock_handler_tests" = true ]; then
+  run_mock_handler_tests
+fi
+if [ "$should_run_dart_compilation_tests" = true ]; then
+  run_dart_compilation_tests
+fi
+if [ "$should_run_java_compilation_tests" = true ]; then
+  run_java_compilation_tests
+fi
+if [ "$should_run_objc_compilation_tests" = true ]; then
+  run_objc_compilation_tests
+fi
+if [ "$should_run_ios_unittests" = true ]; then
+  run_ios_unittests
+fi
+if [ "$should_run_ios_e2e_tests" = true ]; then
+  run_ios_e2e_tests
+fi
+if [ "$should_run_android_unittests" = true ]; then
+  run_android_unittests
+fi
+if [ "$should_run_formatter" = true ]; then
+  run_formatter
+fi
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
new file mode 100644
index 0000000..5463793
--- /dev/null
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -0,0 +1,368 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/ast.dart';
+import 'package:pigeon/dart_generator.dart';
+import 'package:pigeon/generator_tools.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('gen one class', () {
+    final Class klass = Class(
+      name: 'Foobar',
+      fields: <Field>[
+        Field(
+          name: 'field1',
+          dataType: 'dataType1',
+        ),
+      ],
+    );
+    final Root root = Root(
+      apis: <Api>[],
+      classes: <Class>[klass],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('class Foobar'));
+    expect(code, contains('  dataType1 field1;'));
+  });
+
+  test('gen one host api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('class Api'));
+    expect(code, matches('Output.*doSomething.*Input'));
+  });
+
+  test('nested class', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+        name: 'Input',
+        fields: <Field>[Field(name: 'input', dataType: 'String')],
+      ),
+      Class(
+        name: 'Nested',
+        fields: <Field>[Field(name: 'nested', dataType: 'Input')],
+      )
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(
+      code,
+      contains(
+        'pigeonMap[\'nested\'] = nested == null ? null : nested.encode()',
+      ),
+    );
+    expect(
+      code.replaceAll('\n', ' ').replaceAll('  ', ''),
+      contains(
+        '..nested = pigeonMap[\'nested\'] != null ? Input.decode(pigeonMap[\'nested\']) : null;',
+      ),
+    );
+  });
+
+  test('flutterapi', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('abstract class Api'));
+    expect(code, contains('static void setup(Api'));
+  });
+
+  test('host void', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'void',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Future<void> doSomething'));
+    expect(code, contains('// noop'));
+  });
+
+  test('flutter void return', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'void',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    // The next line verifies that we're not setting a variable to the value of "doSomething", but
+    // ignores the line where we assert the value of the argument isn't null, since on that line
+    // we mention "doSomething" in the assertion message.
+    expect(code, isNot(matches('[^!]=.*doSomething')));
+    expect(code, contains('doSomething('));
+    expect(code, isNot(contains('.encode()')));
+  });
+
+  test('flutter void argument', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'void',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('output.*=.*doSomething[(][)]'));
+    expect(code, contains('Output doSomething();'));
+  });
+
+  test('host void argument', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'void',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('channel.send[(]null[)]'));
+  });
+
+  test('mock dart handler', () {
+    final Root root = Root(apis: <Api>[
+      Api(
+          name: 'Api',
+          location: ApiLocation.host,
+          dartHostTestHandler: 'ApiMock',
+          methods: <Method>[
+            Method(
+              name: 'doSomething',
+              argType: 'Input',
+              returnType: 'Output',
+              isAsynchronous: false,
+            ),
+            Method(
+              name: 'voidReturner',
+              argType: 'Input',
+              returnType: 'void',
+              isAsynchronous: false,
+            )
+          ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer mainCodeSink = StringBuffer();
+    final StringBuffer testCodeSink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, mainCodeSink);
+    final String mainCode = mainCodeSink.toString();
+    expect(mainCode, isNot(contains('import \'fo\\\'o.dart\';')));
+    expect(mainCode, contains('class Api {'));
+    expect(mainCode, isNot(contains('abstract class ApiMock')));
+    expect(mainCode, isNot(contains('.ApiMock.doSomething')));
+    expect(mainCode, isNot(contains('\'${Keys.result}\': output.encode()')));
+    expect(mainCode, isNot(contains('return <Object, Object>{};')));
+    generateTestDart(
+        DartOptions(isNullSafe: false), root, testCodeSink, "fo'o.dart");
+    final String testCode = testCodeSink.toString();
+    expect(testCode, contains('import \'fo\\\'o.dart\';'));
+    expect(testCode, isNot(contains('class Api {')));
+    expect(testCode, contains('abstract class ApiMock'));
+    expect(testCode, isNot(contains('.ApiMock.doSomething')));
+    expect(testCode, contains('\'${Keys.result}\': output.encode()'));
+    expect(testCode, contains('return <Object, Object>{};'));
+  });
+
+  test('opt out of nndb', () {
+    final Class klass = Class(
+      name: 'Foobar',
+      fields: <Field>[
+        Field(
+          name: 'field1',
+          dataType: 'dataType1',
+        ),
+      ],
+    );
+    final Root root = Root(
+      apis: <Api>[],
+      classes: <Class>[klass],
+    );
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('// @dart = 2.8'));
+  });
+
+  test('gen one async Flutter Api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('abstract class Api'));
+    expect(code, contains('Future<Output> doSomething(Input arg);'));
+    expect(
+        code, contains('final Output output = await api.doSomething(input);'));
+  });
+
+  test('gen one async Flutter Api with void return', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'void',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, isNot(matches('=.s*doSomething')));
+    expect(code, contains('await api.doSomething('));
+    expect(code, isNot(contains('._toMap()')));
+  });
+
+  test('gen one async Host Api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('class Api'));
+    expect(code, matches('Output.*doSomething.*Input'));
+  });
+
+  test('async host void argument', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'void',
+          returnType: 'Output',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateDart(DartOptions(isNullSafe: false), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('channel.send[(]null[)]'));
+  });
+}
diff --git a/packages/pigeon/test/java_generator_test.dart b/packages/pigeon/test/java_generator_test.dart
new file mode 100644
index 0000000..053049b
--- /dev/null
+++ b/packages/pigeon/test/java_generator_test.dart
@@ -0,0 +1,351 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/ast.dart';
+import 'package:pigeon/java_generator.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('gen one class', () {
+    final Class klass = Class(
+      name: 'Foobar',
+      fields: <Field>[
+        Field(
+          name: 'field1',
+          dataType: 'int',
+        ),
+      ],
+    );
+    final Root root = Root(
+      apis: <Api>[],
+      classes: <Class>[klass],
+    );
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public class Messages'));
+    expect(code, contains('public static class Foobar'));
+    expect(code, contains('private Long field1;'));
+  });
+
+  test('package', () {
+    final Class klass = Class(
+      name: 'Foobar',
+      fields: <Field>[
+        Field(
+          name: 'field1',
+          dataType: 'int',
+        )
+      ],
+    );
+    final Root root = Root(
+      apis: <Api>[],
+      classes: <Class>[klass],
+    );
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages')
+      ..package = 'com.google.foobar';
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('package com.google.foobar;'));
+    expect(code, contains('Map<String, Object> toMap()'));
+  });
+
+  test('gen one host api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public interface Api'));
+    expect(code, matches('Output.*doSomething.*Input'));
+    expect(code, contains('channel.setMessageHandler(null)'));
+  });
+
+  test('all the simple datatypes header', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(name: 'Foobar', fields: <Field>[
+        Field(name: 'aBool', dataType: 'bool'),
+        Field(name: 'aInt', dataType: 'int'),
+        Field(name: 'aDouble', dataType: 'double'),
+        Field(name: 'aString', dataType: 'String'),
+        Field(name: 'aUint8List', dataType: 'Uint8List'),
+        Field(name: 'aInt32List', dataType: 'Int32List'),
+        Field(name: 'aInt64List', dataType: 'Int64List'),
+        Field(name: 'aFloat64List', dataType: 'Float64List'),
+      ]),
+    ]);
+
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('private Boolean aBool;'));
+    expect(code, contains('private Long aInt;'));
+    expect(code, contains('private Double aDouble;'));
+    expect(code, contains('private String aString;'));
+    expect(code, contains('private byte[] aUint8List;'));
+    expect(code, contains('private int[] aInt32List;'));
+    expect(code, contains('private long[] aInt64List;'));
+    expect(code, contains('private double[] aFloat64List;'));
+  });
+
+  test('gen one flutter api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public static class Api'));
+    expect(code, matches('doSomething.*Input.*Output'));
+  });
+
+  test('gen host void api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'void',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, isNot(matches('=.*doSomething')));
+    expect(code, contains('doSomething('));
+  });
+
+  test('gen flutter void return api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'void',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Reply<Void>'));
+    expect(code, isNot(contains('.fromMap(')));
+    expect(code, contains('callback.reply(null)'));
+  });
+
+  test('gen host void argument api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'void',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('Output doSomething()'));
+    expect(code, contains('api.doSomething()'));
+  });
+
+  test('gen flutter void argument api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'void',
+          returnType: 'Output',
+          isAsynchronous: false,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('doSomething(Reply<Output>'));
+    expect(code, contains('channel.send(null'));
+  });
+
+  test('gen list', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'List')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public static class Foobar'));
+    expect(code, contains('private List<Object> field1;'));
+  });
+
+  test('gen map', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'Map')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public static class Foobar'));
+    expect(code, contains('private Map<Object, Object> field1;'));
+  });
+
+  test('gen nested', () {
+    final Class klass = Class(
+      name: 'Outer',
+      fields: <Field>[
+        Field(
+          name: 'nested',
+          dataType: 'Nested',
+        )
+      ],
+    );
+    final Class nestedClass = Class(
+      name: 'Nested',
+      fields: <Field>[
+        Field(
+          name: 'data',
+          dataType: 'int',
+        )
+      ],
+    );
+    final Root root = Root(
+      apis: <Api>[],
+      classes: <Class>[klass, nestedClass],
+    );
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public class Messages'));
+    expect(code, contains('public static class Outer'));
+    expect(code, contains('public static class Nested'));
+    expect(code, contains('private Nested nested;'));
+    expect(code, contains('Nested.fromMap((Map)nested);'));
+    expect(code, contains('put("nested", nested.toMap());'));
+  });
+
+  test('gen one async Host Api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public interface Api'));
+    expect(code, contains('public interface Result<T> {'));
+    expect(
+        code, contains('void doSomething(Input arg, Result<Output> result);'));
+    expect(
+        code,
+        contains(
+            'api.doSomething(input, result -> { wrapped.put("result", result.toMap()); reply.reply(wrapped); });'));
+    expect(code, contains('channel.setMessageHandler(null)'));
+  });
+
+  test('gen one async Flutter Api', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(
+          name: 'doSomething',
+          argType: 'Input',
+          returnType: 'Output',
+          isAsynchronous: true,
+        )
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    final JavaOptions javaOptions = JavaOptions(className: 'Messages');
+    generateJava(javaOptions, root, sink);
+    final String code = sink.toString();
+    expect(code, contains('public static class Api'));
+    expect(code, matches('doSomething.*Input.*Output'));
+  });
+}
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
new file mode 100644
index 0000000..329f151
--- /dev/null
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -0,0 +1,619 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/ast.dart';
+
+void main() {
+  test('gen one class header', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Foobar'));
+    expect(code, matches('@property.*NSString.*field1'));
+  });
+
+  test('gen one class source', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('#import "foo.h"'));
+    expect(code, contains('@implementation Foobar'));
+  });
+
+  test('gen one api header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Input'));
+    expect(code, contains('@interface Output'));
+    expect(code, contains('@protocol Api'));
+    expect(code, matches('nullable Output.*doSomething.*Input.*FlutterError'));
+    expect(code, matches('ApiSetup.*<Api>.*_Nullable'));
+  });
+
+  test('gen one api source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('#import "foo.h"'));
+    expect(code, contains('@implementation Input'));
+    expect(code, contains('@implementation Output'));
+    expect(code, contains('ApiSetup('));
+  });
+
+  test('all the simple datatypes header', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(name: 'Foobar', fields: <Field>[
+        Field(name: 'aBool', dataType: 'bool'),
+        Field(name: 'aInt', dataType: 'int'),
+        Field(name: 'aDouble', dataType: 'double'),
+        Field(name: 'aString', dataType: 'String'),
+        Field(name: 'aUint8List', dataType: 'Uint8List'),
+        Field(name: 'aInt32List', dataType: 'Int32List'),
+        Field(name: 'aInt64List', dataType: 'Int64List'),
+        Field(name: 'aFloat64List', dataType: 'Float64List'),
+      ]),
+    ]);
+
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Foobar'));
+    expect(code, contains('@class FlutterStandardTypedData;'));
+    expect(code, matches('@property.*strong.*NSNumber.*aBool'));
+    expect(code, matches('@property.*strong.*NSNumber.*aInt'));
+    expect(code, matches('@property.*strong.*NSNumber.*aDouble'));
+    expect(code, matches('@property.*copy.*NSString.*aString'));
+    expect(code,
+        matches('@property.*strong.*FlutterStandardTypedData.*aUint8List'));
+    expect(code,
+        matches('@property.*strong.*FlutterStandardTypedData.*aInt32List'));
+    expect(code,
+        matches('@property.*strong.*FlutterStandardTypedData.*Int64List'));
+    expect(code,
+        matches('@property.*strong.*FlutterStandardTypedData.*Float64List'));
+  });
+
+  test('bool source', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(name: 'Foobar', fields: <Field>[
+        Field(name: 'aBool', dataType: 'bool'),
+      ]),
+    ]);
+
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@implementation Foobar'));
+    expect(code, contains('result.aBool = dict[@"aBool"];'));
+  });
+
+  test('nested class header', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Nested',
+          fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code,
+        contains('@property(nonatomic, strong, nullable) Input * nested;'));
+  });
+
+  test('nested class source', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Nested',
+          fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('result.nested = [Input fromMap:dict[@"nested"]];'));
+    expect(code, matches('[self.nested toMap].*@"nested"'));
+  });
+
+  test('prefix class header', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface ABCFoobar'));
+  });
+
+  test('prefix class source', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@implementation ABCFoobar'));
+  });
+
+  test('prefix nested class header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Nested',
+          fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('property.*ABCInput'));
+    expect(code, matches('ABCNested.*doSomething.*ABCInput'));
+    expect(code, contains('@protocol ABCApi'));
+  });
+
+  test('prefix nested class source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Nested',
+          fields: <Field>[Field(name: 'nested', dataType: 'Input')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('ABCInput fromMap'));
+    expect(code, matches('ABCInput.*=.*ABCInput fromMap'));
+    expect(code, contains('void ABCApiSetup('));
+  });
+
+  test('gen flutter api header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Api : NSObject'));
+    expect(
+        code,
+        contains(
+            'initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;'));
+    expect(code, matches('void.*doSomething.*Input.*Output'));
+  });
+
+  test('gen flutter api source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')])
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@implementation Api'));
+    expect(code, matches('void.*doSomething.*Input.*Output.*{'));
+  });
+
+  test('gen host void header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('(void)doSomething:'));
+  });
+
+  test('gen host void source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, isNot(matches('=.*doSomething')));
+    expect(code, matches('[.*doSomething:.*]'));
+    expect(code, contains('callback(wrapResult(nil, error))'));
+  });
+
+  test('gen flutter void return header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('completion:(void(^)(NSError* _Nullable))'));
+  });
+
+  test('gen flutter void return source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'Input', returnType: 'void')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('completion:(void(^)(NSError* _Nullable))'));
+    expect(code, contains('completion(nil)'));
+  });
+
+  test('gen host void arg header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('ABCOutput.*doSomething:[(]FlutterError'));
+  });
+
+  test('gen host void arg source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(code, matches('output.*=.*api doSomething:&error'));
+  });
+
+  test('gen flutter void arg header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(void(^)(ABCOutput*, NSError* _Nullable))completion'));
+  });
+
+  test('gen flutter void arg header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.flutter, methods: <Method>[
+        Method(name: 'doSomething', argType: 'void', returnType: 'Output')
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(void(^)(ABCOutput*, NSError* _Nullable))completion'));
+    expect(code, contains('channel sendMessage:nil'));
+  });
+
+  test('gen list', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'List')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Foobar'));
+    expect(code, matches('@property.*NSArray.*field1'));
+  });
+
+  test('gen map', () {
+    final Root root = Root(apis: <Api>[], classes: <Class>[
+      Class(
+          name: 'Foobar',
+          fields: <Field>[Field(name: 'field1', dataType: 'Map')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(), root, sink);
+    final String code = sink.toString();
+    expect(code, contains('@interface Foobar'));
+    expect(code, matches('@property.*NSDictionary.*field1'));
+  });
+
+  test('async void(input) HostApi header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'Input',
+            returnType: 'void',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(FlutterError *_Nullable))completion'));
+  });
+
+  test('async output(input) HostApi header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'Input',
+            returnType: 'Output',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion'));
+  });
+
+  test('async output(void) HostApi header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'void',
+            returnType: 'Output',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion'));
+  });
+
+  test('async void(void) HostApi header', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'void',
+            returnType: 'void',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '(void)doSomething:(void(^)(FlutterError *_Nullable))completion'));
+  });
+
+  test('async output(input) HostApi source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'Input',
+            returnType: 'Output',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '[api doSomething:input completion:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {'));
+  });
+
+  test('async void(input) HostApi source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'Input',
+            returnType: 'void',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Input',
+          fields: <Field>[Field(name: 'input', dataType: 'String')]),
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '[api doSomething:input completion:^(FlutterError *_Nullable error) {'));
+  });
+
+  test('async void(void) HostApi source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'void',
+            returnType: 'void',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code, contains('[api doSomething:^(FlutterError *_Nullable error) {'));
+  });
+
+  test('async output(void) HostApi source', () {
+    final Root root = Root(apis: <Api>[
+      Api(name: 'Api', location: ApiLocation.host, methods: <Method>[
+        Method(
+            name: 'doSomething',
+            argType: 'void',
+            returnType: 'Output',
+            isAsynchronous: true)
+      ])
+    ], classes: <Class>[
+      Class(
+          name: 'Output',
+          fields: <Field>[Field(name: 'output', dataType: 'String')]),
+    ]);
+    final StringBuffer sink = StringBuffer();
+    generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink);
+    final String code = sink.toString();
+    expect(
+        code,
+        contains(
+            '[api doSomething:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {'));
+  });
+}
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
new file mode 100644
index 0000000..f92117d
--- /dev/null
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -0,0 +1,246 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/pigeon_lib.dart';
+import 'package:pigeon/ast.dart';
+
+class Input1 {
+  String? input;
+}
+
+class Output1 {
+  String? output;
+}
+
+@HostApi()
+abstract class Api1 {
+  Output1 doit(Input1 input);
+}
+
+class InvalidDatatype {
+  dynamic something;
+}
+
+@HostApi()
+abstract class ApiTwoMethods {
+  Output1 method1(Input1 input);
+  Output1 method2(Input1 input);
+}
+
+class Nested {
+  Input1? input;
+}
+
+@FlutterApi()
+abstract class AFlutterApi {
+  Output1 doit(Input1 input);
+}
+
+@HostApi()
+abstract class VoidApi {
+  void doit(Input1 input);
+}
+
+@HostApi()
+abstract class VoidArgApi {
+  Output1 doit();
+}
+
+@HostApi(dartHostTestHandler: 'ApiWithMockDartClassMock')
+abstract class ApiWithMockDartClass {
+  Output1 doit();
+}
+
+class OnlyVisibleFromNesting {
+  String? foo;
+}
+
+class Nestor {
+  OnlyVisibleFromNesting? nested;
+}
+
+@HostApi()
+abstract class NestorApi {
+  Nestor getit();
+}
+
+@HostApi()
+abstract class InvalidArgTypeApi {
+  void doit(bool value);
+}
+
+@HostApi()
+abstract class InvalidReturnTypeApi {
+  bool doit();
+}
+
+void main() {
+  test('parse args - input', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--input', 'foo.dart']);
+    expect(opts.input, equals('foo.dart'));
+  });
+
+  test('parse args - dart_out', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--dart_out', 'foo.dart']);
+    expect(opts.dartOut, equals('foo.dart'));
+  });
+
+  test('parse args - java_package', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--java_package', 'com.google.foo']);
+    expect(opts.javaOptions?.package, equals('com.google.foo'));
+  });
+
+  test('parse args - input', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--java_out', 'foo.java']);
+    expect(opts.javaOut, equals('foo.java'));
+  });
+
+  test('parse args - objc_header_out', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--objc_header_out', 'foo.h']);
+    expect(opts.objcHeaderOut, equals('foo.h'));
+  });
+
+  test('parse args - objc_source_out', () {
+    final PigeonOptions opts =
+        Pigeon.parseArgs(<String>['--objc_source_out', 'foo.m']);
+    expect(opts.objcSourceOut, equals('foo.m'));
+  });
+
+  test('simple parse api', () {
+    final Pigeon dartle = Pigeon.setup();
+    final ParseResults parseResult = dartle.parse(<Type>[Api1]);
+    expect(parseResult.errors.length, equals(0));
+    final Root root = parseResult.root;
+    expect(root.classes.length, equals(2));
+    expect(root.apis.length, equals(1));
+    expect(root.apis[0].name, equals('Api1'));
+    expect(root.apis[0].methods.length, equals(1));
+    expect(root.apis[0].methods[0].name, equals('doit'));
+    expect(root.apis[0].methods[0].argType, equals('Input1'));
+    expect(root.apis[0].methods[0].returnType, equals('Output1'));
+
+    Class? input;
+    Class? output;
+    for (final Class klass in root.classes) {
+      if (klass.name == 'Input1') {
+        input = klass;
+      } else if (klass.name == 'Output1') {
+        output = klass;
+      }
+    }
+    expect(input, isNotNull);
+    expect(output, isNotNull);
+
+    expect(input?.fields.length, equals(1));
+    expect(input?.fields[0].name, equals('input'));
+    expect(input?.fields[0].dataType, equals('String'));
+
+    expect(output?.fields.length, equals(1));
+    expect(output?.fields[0].name, equals('output'));
+    expect(output?.fields[0].dataType, equals('String'));
+  });
+
+  test('invalid datatype', () {
+    final Pigeon dartle = Pigeon.setup();
+    final ParseResults results = dartle.parse(<Type>[InvalidDatatype]);
+    expect(results.errors.length, 1);
+    expect(results.errors[0].message, contains('InvalidDatatype'));
+    expect(results.errors[0].message, contains('dynamic'));
+  });
+
+  test('two methods', () {
+    final Pigeon dartle = Pigeon.setup();
+    final ParseResults results = dartle.parse(<Type>[ApiTwoMethods]);
+    expect(results.errors.length, 0);
+    expect(results.root.apis.length, 1);
+    expect(results.root.apis[0].methods.length, equals(2));
+    expect(results.root.apis[0].methods[0].name, equals('method1'));
+    expect(results.root.apis[0].methods[1].name, equals('method2'));
+  });
+
+  test('nested', () {
+    final Pigeon dartle = Pigeon.setup();
+    final ParseResults results = dartle.parse(<Type>[Nested, Input1]);
+    expect(results.errors.length, equals(0));
+    expect(results.root.classes.length, equals(2));
+    expect(results.root.classes[0].name, equals('Nested'));
+    expect(results.root.classes[0].fields.length, equals(1));
+    expect(results.root.classes[0].fields[0].dataType, equals('Input1'));
+  });
+
+  test('flutter api', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[AFlutterApi]);
+    expect(results.errors.length, equals(0));
+    expect(results.root.apis.length, equals(1));
+    expect(results.root.apis[0].name, equals('AFlutterApi'));
+    expect(results.root.apis[0].location, equals(ApiLocation.flutter));
+  });
+
+  test('void host api', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[VoidApi]);
+    expect(results.errors.length, equals(0));
+    expect(results.root.apis.length, equals(1));
+    expect(results.root.apis[0].methods.length, equals(1));
+    expect(results.root.apis[0].name, equals('VoidApi'));
+    expect(results.root.apis[0].methods[0].returnType, equals('void'));
+  });
+
+  test('void arg host api', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[VoidArgApi]);
+    expect(results.errors.length, equals(0));
+    expect(results.root.apis.length, equals(1));
+    expect(results.root.apis[0].methods.length, equals(1));
+    expect(results.root.apis[0].name, equals('VoidArgApi'));
+    expect(results.root.apis[0].methods[0].returnType, equals('Output1'));
+    expect(results.root.apis[0].methods[0].argType, equals('void'));
+  });
+
+  test('mockDartClass', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[ApiWithMockDartClass]);
+    expect(results.errors.length, equals(0));
+    expect(results.root.apis.length, equals(1));
+    expect(results.root.apis[0].dartHostTestHandler,
+        equals('ApiWithMockDartClassMock'));
+  });
+
+  test('only visible from nesting', () {
+    final Pigeon dartle = Pigeon.setup();
+    final ParseResults results = dartle.parse(<Type>[NestorApi]);
+    expect(results.errors.length, 0);
+    expect(results.root.apis.length, 1);
+    final List<String> classNames =
+        results.root.classes.map((Class x) => x.name).toList();
+    expect(classNames.length, 2);
+    expect(classNames.contains('Nestor'), true);
+    expect(classNames.contains('OnlyVisibleFromNesting'), true);
+  });
+
+  test('invalid datatype for argument', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[InvalidArgTypeApi]);
+    expect(results.errors.length, 1);
+  });
+
+  test('invalid datatype for argument', () {
+    final Pigeon pigeon = Pigeon.setup();
+    final ParseResults results = pigeon.parse(<Type>[InvalidReturnTypeApi]);
+    expect(results.errors.length, 1);
+  });
+
+  test('null safety flag', () {
+    final PigeonOptions results =
+        Pigeon.parseArgs(<String>['--dart_null_safety']);
+    expect(results.dartOptions?.isNullSafe, isTrue);
+  });
+}
diff --git a/packages/pigeon/test/version_test.dart b/packages/pigeon/test/version_test.dart
new file mode 100644
index 0000000..1487b90
--- /dev/null
+++ b/packages/pigeon/test/version_test.dart
@@ -0,0 +1,19 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:pigeon/generator_tools.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('pigeon version matches pubspec', () {
+    final String pubspecPath = '${Directory.current.path}/pubspec.yaml';
+    final String pubspec = File(pubspecPath).readAsStringSync();
+    final RegExp regex = RegExp(r'version:\s*(.*?) #');
+    final RegExpMatch? match = regex.firstMatch(pubspec);
+    expect(match, isNotNull);
+    expect(pigeonVersion, match?.group(1)?.trim());
+  });
+}
diff --git a/packages/pointer_interceptor/.gitignore b/packages/pointer_interceptor/.gitignore
new file mode 100644
index 0000000..1985397
--- /dev/null
+++ b/packages/pointer_interceptor/.gitignore
@@ -0,0 +1,74 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/pointer_interceptor/.metadata b/packages/pointer_interceptor/.metadata
new file mode 100644
index 0000000..b533074
--- /dev/null
+++ b/packages/pointer_interceptor/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: e6bd95bc5caa5e34c5b0285a559673984374b7ea
+  channel: master
+
+project_type: package
diff --git a/packages/pointer_interceptor/CHANGELOG.md b/packages/pointer_interceptor/CHANGELOG.md
new file mode 100644
index 0000000..06e0c6e
--- /dev/null
+++ b/packages/pointer_interceptor/CHANGELOG.md
@@ -0,0 +1,15 @@
+## 0.9.0
+
+* Migrates to null safety.
+
+## 0.8.0+2
+
+* Use `ElevatedButton` instead of the deprecated `RaisedButton` in example and docs.
+
+## 0.8.0+1
+
+* Update README.md so images render in pub.dev
+
+## 0.8.0
+
+* Initial release of the `PointerInterceptor` widget.
diff --git a/packages/pointer_interceptor/LICENSE b/packages/pointer_interceptor/LICENSE
new file mode 100644
index 0000000..73e6b6e
--- /dev/null
+++ b/packages/pointer_interceptor/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/pointer_interceptor/README.md b/packages/pointer_interceptor/README.md
new file mode 100644
index 0000000..bcd3b06
--- /dev/null
+++ b/packages/pointer_interceptor/README.md
@@ -0,0 +1,71 @@
+# pointer_interceptor
+
+`PointerInterceptor` is a widget that prevents mouse events (in web) from being captured by an underlying [`HtmlElementView`](https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html).
+
+You can use this widget in a cross-platform app freely. In mobile, where the issue that this plugin fixes does not exist, the widget acts as a pass-through of its `children`, without adding anything to the render tree.
+
+## What is the problem?
+
+When overlaying Flutter widgets on top of `HtmlElementView` widgets that respond to mouse gestures (handle clicks, for example), the clicks will be consumed by the `HtmlElementView`, and not relayed to Flutter.
+
+The result is that Flutter widget's `onTap` (and other) handlers won't fire as expected, but they'll affect the underlying webview.
+
+|The problem...|
+|:-:|
+|![Depiction of problematic areas](https://raw.githubusercontent.com/flutter/packages/master/packages/pointer_interceptor/doc/img/affected-areas.png)|
+|_In the dashed areas, mouse events won't work as expected. The `HtmlElementView` will consume them before Flutter sees them._|
+
+
+## How does this work?
+
+`PointerInterceptor` creates a platform view consisting of an empty HTML element. The element has the size of its `child` widget, and is inserted in the layer tree _behind_ its child in paint order.
+
+This empty platform view doesn't do anything with mouse events, other than preventing them from reaching other platform views underneath it.
+
+This gives an opportunity to the Flutter framework to handle the click, as expected:
+
+|The solution...|
+|:-:|
+|![Depiction of the solution](https://raw.githubusercontent.com/flutter/packages/master/packages/pointer_interceptor/doc/img/fixed-areas.png)|
+|_Each `PointerInterceptor` (green) renders between Flutter widgets and the underlying `HtmlElementView`. Mouse events now can't reach the background HtmlElementView, and work as expected._|
+
+## How to use
+
+Some common scenarios where this widget may come in handy:
+
+* [FAB](https://api.flutter.dev/flutter/material/FloatingActionButton-class.html) unclickable in an app that renders a full-screen background Map
+* Custom Play/Pause buttons on top of a video element don't work
+* Drawer contents not interactive when it overlaps an iframe element
+* ...
+
+All the cases above have in common that they attempt to render Flutter widgets *on top* of platform views that handle pointer events.
+
+There's two ways that the `PointerInterceptor` widget can be used to solve the problems above:
+
+1. Wrapping your button element directly (FAB, Custom Play/Pause button...):
+
+    ```dart
+    PointerInterceptor(
+      child: ElevatedButton(...),
+    )
+    ```
+
+2. As a root container for a "layout" element, wrapping a bunch of other elements (like a Drawer):
+
+    ```dart
+    Scaffold(
+      ...
+      drawer: PointerInterceptor(
+        child: Drawer(
+          child: ...
+        ),
+      ),
+      ...
+    )
+    ```
+
+### `debug`
+
+The `PointerInterceptor` widget has a `debug` property, that will render it visibly on the screen (similar to the images above).
+
+This may be useful to see what the widget is actually covering when used as a layout element.
diff --git a/packages/pointer_interceptor/doc/img/affected-areas.png b/packages/pointer_interceptor/doc/img/affected-areas.png
new file mode 100644
index 0000000..40592db
--- /dev/null
+++ b/packages/pointer_interceptor/doc/img/affected-areas.png
Binary files differ
diff --git a/packages/pointer_interceptor/doc/img/fixed-areas.png b/packages/pointer_interceptor/doc/img/fixed-areas.png
new file mode 100644
index 0000000..489d376
--- /dev/null
+++ b/packages/pointer_interceptor/doc/img/fixed-areas.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/.gitignore b/packages/pointer_interceptor/example/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/packages/pointer_interceptor/example/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/pointer_interceptor/example/.metadata b/packages/pointer_interceptor/example/.metadata
new file mode 100644
index 0000000..d2885b8
--- /dev/null
+++ b/packages/pointer_interceptor/example/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: e6bd95bc5caa5e34c5b0285a559673984374b7ea
+  channel: master
+
+project_type: app
diff --git a/packages/pointer_interceptor/example/README.md b/packages/pointer_interceptor/example/README.md
new file mode 100644
index 0000000..9fddf8c
--- /dev/null
+++ b/packages/pointer_interceptor/example/README.md
@@ -0,0 +1,17 @@
+# pointer_interceptor_example
+
+An example for the PointerInterceptor widget.
+
+## Getting Started
+
+`flutter run -d chrome` to run the sample. You can tweak some code in the `lib/main.dart`, but be careful, changes there can break integration tests!
+
+## Running tests
+
+`flutter drive --target integration_test/widget_test.dart --driver test_driver/integration_test.dart --show-web-server-device -d web-server --web-renderer=html`
+
+The command above will run the integration tests for this package.
+
+Make sure that you have `chromedriver` running in port `4444`.
+
+Read more on: [flutter.dev > Docs > Testing & debugging > Integration testing](https://flutter.dev/docs/testing/integration-tests).
diff --git a/packages/pointer_interceptor/example/android/.gitignore b/packages/pointer_interceptor/example/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/.gitignore
@@ -0,0 +1,11 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
diff --git a/packages/pointer_interceptor/example/android/app/build.gradle b/packages/pointer_interceptor/example/android/app/build.gradle
new file mode 100644
index 0000000..3932aa9
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/build.gradle
@@ -0,0 +1,63 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+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.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 29
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.example"
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    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 {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..c208884
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.example">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..34dd77e
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.example">
+   <application
+        android:label="example"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <!-- Displays an Android View that continues showing the launch screen
+                 Drawable until Flutter paints its first frame, then this splash
+                 screen fades out. A splash screen is useful to avoid any visual
+                 gap between the end of Android's launch screen and the painting of
+                 Flutter's first frame. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.SplashScreenDrawable"
+              android:resource="@drawable/launch_background"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
new file mode 100644
index 0000000..e793a00
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml b/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml b/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml b/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+         
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>
diff --git a/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml b/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..c208884
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.example">
+    <!-- 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"/>
+</manifest>
diff --git a/packages/pointer_interceptor/example/android/build.gradle b/packages/pointer_interceptor/example/android/build.gradle
new file mode 100644
index 0000000..3100ad2
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+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/packages/pointer_interceptor/example/android/gradle.properties b/packages/pointer_interceptor/example/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/packages/pointer_interceptor/example/android/settings.gradle b/packages/pointer_interceptor/example/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/packages/pointer_interceptor/example/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/packages/pointer_interceptor/example/integration_test/widget_test.dart b/packages/pointer_interceptor/example/integration_test/widget_test.dart
new file mode 100644
index 0000000..72b02ad
--- /dev/null
+++ b/packages/pointer_interceptor/example/integration_test/widget_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:html' as html;
+
+// Imports the Flutter Driver API.
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'package:pointer_interceptor_example/main.dart' as app;
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('Widget', () {
+    final Finder nonClickableButtonFinder =
+        find.byKey(const Key('transparent-button'));
+    final Finder clickableButtonFinder =
+        find.byKey(const Key('clickable-button'));
+
+    testWidgets(
+        'on wrapped elements, the browser hits the interceptor (and not the background-html-view)',
+        (WidgetTester tester) async {
+      app.main();
+      await tester.pumpAndSettle();
+
+      final html.Element? element =
+          _getHtmlElementFromFinder(clickableButtonFinder, tester);
+      expect(element?.tagName.toLowerCase(), 'flt-platform-view');
+
+      final html.Element? platformViewRoot =
+          element?.shadowRoot?.getElementById('background-html-view');
+      expect(platformViewRoot, isNull);
+    });
+
+    testWidgets(
+        'on unwrapped elements, the browser hits the background-html-view',
+        (WidgetTester tester) async {
+      app.main();
+      await tester.pumpAndSettle();
+
+      final html.Element? element =
+          _getHtmlElementFromFinder(nonClickableButtonFinder, tester);
+      expect(element?.tagName.toLowerCase(), 'flt-platform-view');
+
+      final html.Element? platformViewRoot =
+          element?.shadowRoot?.getElementById('background-html-view');
+      expect(platformViewRoot, isNotNull);
+    });
+  });
+}
+
+// This functions locates a widget from a Finder, and asks the browser what's the
+// DOM element in the center of the coordinates of the widget. (Returns *which*
+// DOM element will handle Mouse interactions first at those coordinates.)
+html.Element? _getHtmlElementFromFinder(Finder finder, WidgetTester tester) {
+  final Offset point = tester.getCenter(finder);
+  return html.document.elementFromPoint(point.dx.toInt(), point.dy.toInt());
+}
diff --git a/packages/pointer_interceptor/example/ios/.gitignore b/packages/pointer_interceptor/example/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist b/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig b/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig b/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..c6759a6
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,471 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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 = (
+			);
+			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>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; 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>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		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 = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			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 = 1020;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			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 */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets 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\" embed_and_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 = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift 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 */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 9.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				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 */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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"
+      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"
+      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 = "Profile"
+      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/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?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>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?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>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift b/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+  override func application(
+    _ application: UIApplication,
+    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+  ) -> Bool {
+    GeneratedPluginRegistrant.register(with: self)
+    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+  }
+}
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+  "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/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "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/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+<?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/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+<?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/packages/pointer_interceptor/example/ios/Runner/Info.plist b/packages/pointer_interceptor/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..a060db6
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+<?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>$(DEVELOPMENT_LANGUAGE)</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>example</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(FLUTTER_BUILD_NAME)</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>$(FLUTTER_BUILD_NUMBER)</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/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h b/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/packages/pointer_interceptor/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/packages/pointer_interceptor/example/lib/main.dart b/packages/pointer_interceptor/example/lib/main.dart
new file mode 100644
index 0000000..619380c
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/main.dart
@@ -0,0 +1,210 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html;
+
+import 'package:flutter/material.dart';
+import 'package:pointer_interceptor/pointer_interceptor.dart';
+
+import 'src/shim/dart_ui.dart' as ui;
+
+const String _htmlElementViewType = '_htmlElementViewType';
+const double _videoWidth = 640;
+const double _videoHeight = 480;
+
+/// The html.Element that will be rendered underneath the flutter UI.
+html.Element htmlElement = html.DivElement()
+  ..style.width = '100%'
+  ..style.height = '100%'
+  ..style.backgroundColor = '#fabada'
+  ..style.cursor = 'auto'
+  ..id = 'background-html-view';
+
+// See other examples commented out below...
+
+// html.Element htmlElement = html.VideoElement()
+//   ..style.width = '100%'
+//   ..style.height = '100%'
+//   ..style.cursor = 'auto'
+//   ..style.backgroundColor = 'black'
+//   ..id = 'background-html-view'
+//   ..src = 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4'
+//   ..poster = 'https://peach.blender.org/wp-content/uploads/title_anouncement.jpg?x11217'
+//   ..controls = true;
+
+// html.Element htmlElement = html.IFrameElement()
+//       ..width = '100%'
+//       ..height = '100%'
+//       ..id = 'background-html-view'
+//       ..src = 'https://www.youtube.com/embed/IyFZznAk69U'
+//       ..style.border = 'none';
+
+void main() {
+  runApp(const MyApp());
+}
+
+/// Main app
+class MyApp extends StatelessWidget {
+  /// Creates main app.
+  const MyApp({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    ui.platformViewRegistry.registerViewFactory(_htmlElementViewType,
+        (int viewId) {
+      final html.Element wrapper = html.DivElement();
+      wrapper.append(htmlElement);
+      return wrapper;
+    });
+
+    return const MaterialApp(
+      title: 'Stopping Clicks with some DOM',
+      home: MyHomePage(),
+    );
+  }
+}
+
+/// First page
+class MyHomePage extends StatefulWidget {
+  /// Creates first page.
+  const MyHomePage({Key? key}) : super(key: key);
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  String _lastClick = 'none';
+
+  void _clickedOn(String key) {
+    setState(() {
+      _lastClick = key;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('PointerInterceptor demo'),
+        actions: <Widget>[
+          PointerInterceptor(
+            // debug: true,
+            child: IconButton(
+              icon: const Icon(Icons.add_alert),
+              tooltip: 'AppBar Icon',
+              onPressed: () {
+                _clickedOn('appbar-icon');
+              },
+            ),
+          ),
+        ],
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Text(
+              'Last click on: $_lastClick',
+              key: const Key('last-clicked'),
+            ),
+            Container(
+              color: Colors.black,
+              width: _videoWidth,
+              height: _videoHeight,
+              child: Stack(
+                alignment: Alignment.center,
+                children: <Widget>[
+                  HtmlElement(
+                    onClick: () {
+                      _clickedOn('html-element');
+                    },
+                  ),
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                    children: <Widget>[
+                      ElevatedButton(
+                        key: const Key('transparent-button'),
+                        child: const Text('Never calls onPressed'),
+                        onPressed: () {
+                          _clickedOn('transparent-button');
+                        },
+                      ),
+                      PointerInterceptor(
+                        child: ElevatedButton(
+                          key: const Key('clickable-button'),
+                          child: const Text('Works As Expected'),
+                          onPressed: () {
+                            _clickedOn('clickable-button');
+                          },
+                        ),
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+      floatingActionButton: Row(
+        mainAxisAlignment: MainAxisAlignment.end,
+        children: <Widget>[
+          PointerInterceptor(
+            // debug: true,
+            child: FloatingActionButton(
+              child: const Icon(Icons.navigation),
+              onPressed: () {
+                _clickedOn('fab-1');
+              },
+            ),
+          ),
+        ],
+      ),
+      drawer: Drawer(
+        child: PointerInterceptor(
+          // debug: true, // Enable this to "see" the interceptor covering the column.
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: <Widget>[
+              ListTile(
+                title: const Text('Item 1'),
+                onTap: () {
+                  _clickedOn('drawer-item-1');
+                },
+              ),
+              ListTile(
+                title: const Text('Item 2'),
+                onTap: () {
+                  _clickedOn('drawer-item-2');
+                },
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// Initialize the videoPlayer, then render the corresponding view...
+class HtmlElement extends StatelessWidget {
+  /// Constructor
+  const HtmlElement({Key? key, required this.onClick}) : super(key: key);
+
+  /// A function to run when the element is clicked
+  final Function onClick;
+
+  @override
+  Widget build(BuildContext context) {
+    htmlElement.onClick.listen((_) {
+      onClick();
+    });
+
+    return const HtmlElementView(
+      viewType: _htmlElementViewType,
+    );
+  }
+}
diff --git a/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart
new file mode 100644
index 0000000..442115e
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// This file shims dart:ui in web-only scenarios, getting rid of the need to
+/// suppress analyzer warnings.
+
+// TODO(dit): flutter/flutter#55000 Remove this file once web-only dart:ui APIs
+// are exposed from a dedicated place.
+export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart';
diff --git a/packages/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart
new file mode 100644
index 0000000..787e349
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_fake.dart
@@ -0,0 +1,30 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Fake interface for the logic that this package needs from (web-only) dart:ui.
+// This is conditionally exported so the analyzer sees these methods as available.
+
+/// Shim for web_ui engine.PlatformViewRegistry
+/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
+// ignore: camel_case_types
+class platformViewRegistry {
+  /// Shim for registerViewFactory
+  /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
+  static void registerViewFactory(
+      String viewTypeId, dynamic Function(int viewId) viewFactory) {}
+}
+
+/// Shim for web_ui engine.AssetManager.
+/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12
+// ignore: camel_case_types
+class webOnlyAssetManager {
+  /// Shim for getAssetUrl.
+  /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45
+  static String getAssetUrl(String asset) {
+    return '';
+  }
+}
+
+/// Signature of callbacks that have no arguments and return no data.
+typedef VoidCallback = void Function();
diff --git a/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart
new file mode 100644
index 0000000..90ecd7d
--- /dev/null
+++ b/packages/pointer_interceptor/example/lib/src/shim/dart_ui_real.dart
@@ -0,0 +1,5 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'dart:ui';
diff --git a/packages/pointer_interceptor/example/pubspec.yaml b/packages/pointer_interceptor/example/pubspec.yaml
new file mode 100644
index 0000000..2ed4117
--- /dev/null
+++ b/packages/pointer_interceptor/example/pubspec.yaml
@@ -0,0 +1,30 @@
+name: pointer_interceptor_example
+description: An example app for the pointer_interceptor package.
+publish_to: 'none'
+version: 1.0.0
+
+environment:
+  sdk: ">=2.12.0-0 <3.0.0"
+  flutter: ">=1.26.0-0" # For integration_test from sdk
+
+dependencies:
+  flutter:
+    sdk: flutter
+  pointer_interceptor:
+    path: ../
+
+dev_dependencies:
+  flutter_driver:
+    sdk: flutter
+  flutter_test:
+    sdk: flutter
+  integration_test:
+    sdk: flutter
+
+# 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 material Icons class.
+  uses-material-design: true
diff --git a/packages/pointer_interceptor/example/test_driver/integration_test.dart b/packages/pointer_interceptor/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..64e2248
--- /dev/null
+++ b/packages/pointer_interceptor/example/test_driver/integration_test.dart
@@ -0,0 +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.
+
+import 'package:integration_test/integration_test_driver.dart';
+
+Future<void> main() async => integrationDriver();
diff --git a/packages/pointer_interceptor/example/web/favicon.png b/packages/pointer_interceptor/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/favicon.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/icons/Icon-192.png b/packages/pointer_interceptor/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/icons/Icon-512.png b/packages/pointer_interceptor/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/pointer_interceptor/example/web/index.html b/packages/pointer_interceptor/example/web/index.html
new file mode 100644
index 0000000..1460b5e
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/index.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <!--
+    If you are serving your web app in a path other than the root, change the
+    href value below to reflect the base path you are serving from.
+
+    The path provided below has to start and end with a slash "/" in order for
+    it to work correctly.
+
+    Fore more details:
+    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
+  -->
+  <base href="/">
+
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="A new Flutter project.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="example">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="icon" type="image/png" href="favicon.png"/>
+
+  <title>example</title>
+  <link rel="manifest" href="manifest.json">
+</head>
+<body>
+  <!-- This script installs service_worker.js to provide PWA functionality to
+       application. For more information, see:
+       https://developers.google.com/web/fundamentals/primers/service-workers -->
+  <script>
+    if ('serviceWorker' in navigator) {
+      window.addEventListener('flutter-first-frame', function () {
+        navigator.serviceWorker.register('flutter_service_worker.js');
+      });
+    }
+  </script>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/pointer_interceptor/example/web/manifest.json b/packages/pointer_interceptor/example/web/manifest.json
new file mode 100644
index 0000000..8c01291
--- /dev/null
+++ b/packages/pointer_interceptor/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+    "name": "example",
+    "short_name": "example",
+    "start_url": ".",
+    "display": "standalone",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "A new Flutter project.",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "icons/Icon-192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ]
+}
diff --git a/packages/pointer_interceptor/lib/pointer_interceptor.dart b/packages/pointer_interceptor/lib/pointer_interceptor.dart
new file mode 100644
index 0000000..c760403
--- /dev/null
+++ b/packages/pointer_interceptor/lib/pointer_interceptor.dart
@@ -0,0 +1,7 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library pointer_interceptor;
+
+export 'src/mobile.dart' if (dart.library.html) 'src/web.dart';
diff --git a/packages/pointer_interceptor/lib/src/mobile.dart b/packages/pointer_interceptor/lib/src/mobile.dart
new file mode 100644
index 0000000..1f561ac
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/mobile.dart
@@ -0,0 +1,29 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+
+/// A [Widget] that prevents clicks from being swallowed by [HtmlElementView]s.
+class PointerInterceptor extends StatelessWidget {
+  /// Create a `PointerInterceptor` wrapping a `child`.
+  const PointerInterceptor({
+    required this.child,
+    this.debug = false,
+    Key? key,
+  }) : super(key: key);
+
+  /// The `Widget` that is being wrapped by this `PointerInterceptor`.
+  final Widget child;
+
+  /// When true, the widget renders with a semi-transparent red background, for debug purposes.
+  ///
+  /// This is useful when rendering this as a "layout" widget, like the root child
+  /// of a `Drawer`.
+  final bool debug;
+
+  @override
+  Widget build(BuildContext context) {
+    return child;
+  }
+}
diff --git a/packages/pointer_interceptor/lib/src/shim/dart_ui.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui.dart
new file mode 100644
index 0000000..442115e
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui.dart
@@ -0,0 +1,10 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// This file shims dart:ui in web-only scenarios, getting rid of the need to
+/// suppress analyzer warnings.
+
+// TODO(dit): flutter/flutter#55000 Remove this file once web-only dart:ui APIs
+// are exposed from a dedicated place.
+export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart';
diff --git a/packages/pointer_interceptor/lib/src/shim/dart_ui_fake.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui_fake.dart
new file mode 100644
index 0000000..787e349
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui_fake.dart
@@ -0,0 +1,30 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Fake interface for the logic that this package needs from (web-only) dart:ui.
+// This is conditionally exported so the analyzer sees these methods as available.
+
+/// Shim for web_ui engine.PlatformViewRegistry
+/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
+// ignore: camel_case_types
+class platformViewRegistry {
+  /// Shim for registerViewFactory
+  /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
+  static void registerViewFactory(
+      String viewTypeId, dynamic Function(int viewId) viewFactory) {}
+}
+
+/// Shim for web_ui engine.AssetManager.
+/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12
+// ignore: camel_case_types
+class webOnlyAssetManager {
+  /// Shim for getAssetUrl.
+  /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L45
+  static String getAssetUrl(String asset) {
+    return '';
+  }
+}
+
+/// Signature of callbacks that have no arguments and return no data.
+typedef VoidCallback = void Function();
diff --git a/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart b/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart
new file mode 100644
index 0000000..90ecd7d
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/shim/dart_ui_real.dart
@@ -0,0 +1,5 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'dart:ui';
diff --git a/packages/pointer_interceptor/lib/src/web.dart b/packages/pointer_interceptor/lib/src/web.dart
new file mode 100644
index 0000000..f59c4a3
--- /dev/null
+++ b/packages/pointer_interceptor/lib/src/web.dart
@@ -0,0 +1,89 @@
+// Copyright 2019 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html;
+
+import 'package:flutter/widgets.dart';
+
+import 'shim/dart_ui.dart' as ui;
+
+const String _viewType = '__webPointerInterceptorViewType__';
+const String _debug = 'debug__';
+
+// Computes a "view type" for different configurations of the widget.
+String _getViewType({bool debug = false}) {
+  return debug ? _viewType + _debug : _viewType;
+}
+
+// Registers a viewFactory for this widget.
+void _registerFactory({bool debug = false}) {
+  final String viewType = _getViewType(debug: debug);
+  ui.platformViewRegistry.registerViewFactory(viewType, (int viewId) {
+    final html.Element htmlElement = html.DivElement()
+      ..style.top = '0'
+      ..style.right = '0'
+      ..style.bottom = '0'
+      ..style.left = '0'
+      ..style.position = 'relative';
+    if (debug) {
+      htmlElement.style.backgroundColor = 'rgba(255, 0, 0, .5)';
+    }
+    return htmlElement;
+  });
+}
+
+/// The web implementation of the `PointerInterceptor` widget.
+///
+/// A `Widget` that prevents clicks from being swallowed by [HtmlElementView]s.
+class PointerInterceptor extends StatelessWidget {
+  /// Creates a PointerInterceptor for the web.
+  PointerInterceptor({
+    required this.child,
+    this.debug = false,
+    Key? key,
+  }) : super(key: key) {
+    if (!_registered) {
+      _register();
+    }
+  }
+
+  /// The `Widget` that is being wrapped by this `PointerInterceptor`.
+  final Widget child;
+
+  /// When true, the widget renders with a semi-transparent red background, for debug purposes.
+  ///
+  /// This is useful when rendering this as a "layout" widget, like the root child
+  /// of a [Drawer].
+  final bool debug;
+
+  // Keeps track if this widget has already registered its view factories or not.
+  static bool _registered = false;
+
+  // Registers the view factories for the interceptor widgets.
+  static void _register() {
+    assert(!_registered);
+
+    _registerFactory();
+    _registerFactory(debug: true);
+
+    _registered = true;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final String viewType = _getViewType(debug: debug);
+    return Stack(
+      alignment: Alignment.center,
+      children: <Widget>[
+        Positioned.fill(
+          child: HtmlElementView(
+            viewType: viewType,
+          ),
+        ),
+        child,
+      ],
+    );
+  }
+}
diff --git a/packages/pointer_interceptor/pubspec.yaml b/packages/pointer_interceptor/pubspec.yaml
new file mode 100644
index 0000000..5bea65f
--- /dev/null
+++ b/packages/pointer_interceptor/pubspec.yaml
@@ -0,0 +1,16 @@
+name: pointer_interceptor
+description: A widget to prevent clicks from being swallowed by underlying HtmlElementViews on the web.
+repository: https://github.com/flutter/packages
+version: 0.9.0
+
+environment:
+  sdk: ">=2.12.0-0 <3.0.0"
+  flutter: ">=1.17.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
diff --git a/packages/pointer_interceptor/test/README.md b/packages/pointer_interceptor/test/README.md
new file mode 100644
index 0000000..7c5b4ad
--- /dev/null
+++ b/packages/pointer_interceptor/test/README.md
@@ -0,0 +1,5 @@
+## test
+
+This package uses integration tests for testing.
+
+See `example/README.md` for more info.
diff --git a/packages/pointer_interceptor/test/tests_exist_elsewhere_test.dart b/packages/pointer_interceptor/test/tests_exist_elsewhere_test.dart
new file mode 100644
index 0000000..334f521
--- /dev/null
+++ b/packages/pointer_interceptor/test/tests_exist_elsewhere_test.dart
@@ -0,0 +1,10 @@
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('Tell the user where to find the real tests', () {
+    print('---');
+    print('This package uses integration_test for its tests.');
+    print('See `example/README.md` for more info.');
+    print('---');
+  });
+}
diff --git a/packages/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md
new file mode 100644
index 0000000..e243d25
--- /dev/null
+++ b/packages/web_benchmarks/CHANGELOG.md
@@ -0,0 +1,19 @@
+## 0.0.5
+
+* Updated dependencies to allow broader versions for upstream packages.
+
+## 0.0.4
+
+* Updated dependencies to allow broader versions for upstream packages.
+
+## 0.0.3
+
+* Fixed benchmarks failing due to trace format change for begin frame.
+
+## 0.0.2
+
+* Improve console messages.
+
+## 0.0.1 - Initial release.
+
+* Provide a benchmark server (host-side) and a benchmark client (browser-side).
diff --git a/packages/web_benchmarks/LICENSE b/packages/web_benchmarks/LICENSE
new file mode 100644
index 0000000..4611350
--- /dev/null
+++ b/packages/web_benchmarks/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/web_benchmarks/README.md b/packages/web_benchmarks/README.md
new file mode 100644
index 0000000..ba6a1cb
--- /dev/null
+++ b/packages/web_benchmarks/README.md
@@ -0,0 +1,15 @@
+# web_benchmarks
+
+A benchmark harness for Flutter Web apps. Currently only supports running
+benchmarks in Chrome.
+
+# Writing a benchmark
+
+An example benchmark can be found in [test/web_benchmark_test.dart][1].
+
+A web benchmark is made of two parts: a client and a server. The client is code
+that runs in the browser together with the benchmark code. The server serves the
+app's code and assets. Additionally, the server communicates with the browser to
+extract the performance traces.
+
+[1]: https://github.com/flutter/packages/blob/master/packages/web_benchmarks/test/web_benchmarks_test.dart
diff --git a/packages/web_benchmarks/lib/client.dart b/packages/web_benchmarks/lib/client.dart
new file mode 100644
index 0000000..bb1fc76
--- /dev/null
+++ b/packages/web_benchmarks/lib/client.dart
@@ -0,0 +1,383 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json;
+import 'dart:html' as html;
+import 'dart:math' as math;
+
+import 'package:meta/meta.dart';
+
+import 'src/common.dart';
+import 'src/recorder.dart';
+export 'src/recorder.dart';
+
+/// Signature for a function that creates a [Recorder].
+typedef RecorderFactory = Recorder Function();
+
+/// List of all benchmarks that run in the devicelab.
+///
+/// When adding a new benchmark, add it to this map. Make sure that the name
+/// of your benchmark is unique.
+Map<String, RecorderFactory> _benchmarks;
+
+final LocalBenchmarkServerClient _client = LocalBenchmarkServerClient();
+
+/// Starts a local benchmark client to run [benchmarks].
+///
+/// Usually used in combination with a benchmark server, which orders the
+/// client to run each benchmark in order.
+///
+/// When used without a server, prompts the user to select a benchmark to
+/// run next.
+Future<void> runBenchmarks(Map<String, RecorderFactory> benchmarks) async {
+  assert(benchmarks != null);
+
+  // Set local benchmarks.
+  _benchmarks = benchmarks;
+
+  // Check if the benchmark server wants us to run a specific benchmark.
+  final String nextBenchmark = await _client.requestNextBenchmark();
+
+  if (nextBenchmark == LocalBenchmarkServerClient.kManualFallback) {
+    _fallbackToManual(
+        'The server did not tell us which benchmark to run next.');
+    return;
+  }
+
+  await _runBenchmark(nextBenchmark);
+  html.window.location.reload();
+}
+
+Future<void> _runBenchmark(String benchmarkName) async {
+  final RecorderFactory recorderFactory = _benchmarks[benchmarkName];
+
+  if (recorderFactory == null) {
+    _fallbackToManual('Benchmark $benchmarkName not found.');
+    return;
+  }
+
+  await runZoned<Future<void>>(
+    () async {
+      final Recorder recorder = recorderFactory();
+      final Runner runner = recorder.isTracingEnabled && !_client.isInManualMode
+          ? Runner(
+              recorder: recorder,
+              setUpAllDidRun: () =>
+                  _client.startPerformanceTracing(benchmarkName),
+              tearDownAllWillRun: _client.stopPerformanceTracing,
+            )
+          : Runner(recorder: recorder);
+
+      final Profile profile = await runner.run();
+      if (!_client.isInManualMode) {
+        await _client.sendProfileData(profile);
+      } else {
+        _printResultsToScreen(profile);
+        print(profile);
+      }
+    },
+    zoneSpecification: ZoneSpecification(
+      print: (Zone self, ZoneDelegate parent, Zone zone, String line) async {
+        if (_client.isInManualMode) {
+          parent.print(zone, '[$benchmarkName] $line');
+        } else {
+          await _client.printToConsole(line);
+        }
+      },
+      handleUncaughtError: (
+        Zone self,
+        ZoneDelegate parent,
+        Zone zone,
+        Object error,
+        StackTrace stackTrace,
+      ) async {
+        if (_client.isInManualMode) {
+          parent.print(zone, '[$benchmarkName] $error, $stackTrace');
+          parent.handleUncaughtError(zone, error, stackTrace);
+        } else {
+          await _client.reportError(error, stackTrace);
+        }
+      },
+    ),
+  );
+}
+
+void _fallbackToManual(String error) {
+  html.document.body.appendHtml('''
+    <div id="manual-panel">
+      <h3>$error</h3>
+
+      <p>Choose one of the following benchmarks:</p>
+
+      <!-- Absolutely position it so it receives the clicks and not the glasspane -->
+      <ul style="position: absolute">
+        ${_benchmarks.keys.map((String name) => '<li><button id="$name">$name</button></li>').join('\n')}
+      </ul>
+    </div>
+  ''',
+      validator: html.NodeValidatorBuilder()
+        ..allowHtml5()
+        ..allowInlineStyles());
+
+  for (final String benchmarkName in _benchmarks.keys) {
+    final html.Element button = html.document.querySelector('#$benchmarkName');
+    button.addEventListener('click', (_) {
+      final html.Element manualPanel =
+          html.document.querySelector('#manual-panel');
+      manualPanel?.remove();
+      _runBenchmark(benchmarkName);
+    });
+  }
+}
+
+/// Visualizes results on the Web page for manual inspection.
+void _printResultsToScreen(Profile profile) {
+  html.document.body.innerHtml = '<h2>${profile.name}</h2>';
+
+  profile.scoreData.forEach((String scoreKey, Timeseries timeseries) {
+    html.document.body.appendHtml('<h2>$scoreKey</h2>');
+    html.document.body.appendHtml('<pre>${timeseries.computeStats()}</pre>');
+    html.document.body.append(TimeseriesVisualization(timeseries).render());
+  });
+}
+
+/// Draws timeseries data and statistics on a canvas.
+class TimeseriesVisualization {
+  /// Creates a visualization for a [Timeseries].
+  TimeseriesVisualization(this._timeseries) {
+    _stats = _timeseries.computeStats();
+    _canvas = html.CanvasElement();
+    _screenWidth = html.window.screen.width;
+    _canvas.width = _screenWidth;
+    _canvas.height = (_kCanvasHeight * html.window.devicePixelRatio).round();
+    _canvas.style
+      ..width = '100%'
+      ..height = '${_kCanvasHeight}px'
+      ..outline = '1px solid green';
+    _ctx = _canvas.context2D;
+
+    // The amount of vertical space available on the chart. Because some
+    // outliers can be huge they can dwarf all the useful values. So we
+    // limit it to 1.5 x the biggest non-outlier.
+    _maxValueChartRange = 1.5 *
+        _stats.samples
+            .where((AnnotatedSample sample) => !sample.isOutlier)
+            .map<double>((AnnotatedSample sample) => sample.magnitude)
+            .fold<double>(0, math.max);
+  }
+
+  static const double _kCanvasHeight = 200;
+
+  final Timeseries _timeseries;
+  TimeseriesStats _stats;
+  html.CanvasElement _canvas;
+  html.CanvasRenderingContext2D _ctx;
+  int _screenWidth;
+
+  // Used to normalize benchmark values to chart height.
+  double _maxValueChartRange;
+
+  /// Converts a sample value to vertical canvas coordinates.
+  ///
+  /// This does not work for horizontal coordinates.
+  double _normalized(double value) {
+    return _kCanvasHeight * value / _maxValueChartRange;
+  }
+
+  /// A utility for drawing lines.
+  void drawLine(num x1, num y1, num x2, num y2) {
+    _ctx.beginPath();
+    _ctx.moveTo(x1, y1);
+    _ctx.lineTo(x2, y2);
+    _ctx.stroke();
+  }
+
+  /// Renders the timeseries into a `<canvas>` and returns the canvas element.
+  html.CanvasElement render() {
+    _ctx.translate(0, _kCanvasHeight * html.window.devicePixelRatio);
+    _ctx.scale(1, -html.window.devicePixelRatio);
+
+    final double barWidth = _screenWidth / _stats.samples.length;
+    double xOffset = 0;
+    for (int i = 0; i < _stats.samples.length; i++) {
+      final AnnotatedSample sample = _stats.samples[i];
+
+      if (sample.isWarmUpValue) {
+        // Put gray background behing warm-up samples.
+        _ctx.fillStyle = 'rgba(200,200,200,1)';
+        _ctx.fillRect(xOffset, 0, barWidth, _normalized(_maxValueChartRange));
+      }
+
+      if (sample.magnitude > _maxValueChartRange) {
+        // The sample value is so big it doesn't fit on the chart. Paint it purple.
+        _ctx.fillStyle = 'rgba(100,50,100,0.8)';
+      } else if (sample.isOutlier) {
+        // The sample is an outlier, color it light red.
+        _ctx.fillStyle = 'rgba(255,50,50,0.6)';
+      } else {
+        // A non-outlier sample, color it light blue.
+        _ctx.fillStyle = 'rgba(50,50,255,0.6)';
+      }
+
+      _ctx.fillRect(xOffset, 0, barWidth - 1, _normalized(sample.magnitude));
+      xOffset += barWidth;
+    }
+
+    // Draw a horizontal solid line corresponding to the average.
+    _ctx.lineWidth = 1;
+    drawLine(0, _normalized(_stats.average), _screenWidth,
+        _normalized(_stats.average));
+
+    // Draw a horizontal dashed line corresponding to the outlier cut off.
+    _ctx.setLineDash(<num>[5, 5]);
+    drawLine(0, _normalized(_stats.outlierCutOff), _screenWidth,
+        _normalized(_stats.outlierCutOff));
+
+    // Draw a light red band that shows the noise (1 stddev in each direction).
+    _ctx.fillStyle = 'rgba(255,50,50,0.3)';
+    _ctx.fillRect(
+      0,
+      _normalized(_stats.average * (1 - _stats.noise)),
+      _screenWidth,
+      _normalized(2 * _stats.average * _stats.noise),
+    );
+
+    return _canvas;
+  }
+}
+
+/// Implements the client REST API for the local benchmark server.
+///
+/// The local server is optional. If it is not available the benchmark UI must
+/// implement a manual fallback. This allows debugging benchmarks using plain
+/// `flutter run`.
+class LocalBenchmarkServerClient {
+  /// This value is returned by [requestNextBenchmark].
+  static const String kManualFallback = '__manual_fallback__';
+
+  /// Whether we fell back to manual mode.
+  ///
+  /// This happens when you run benchmarks using plain `flutter run` rather than
+  /// devicelab test harness. The test harness spins up a special server that
+  /// provides API for automatically picking the next benchmark to run.
+  bool isInManualMode;
+
+  /// Asks the local server for the name of the next benchmark to run.
+  ///
+  /// Returns [kManualFallback] if local server is not available (uses 404 as a
+  /// signal).
+  Future<String> requestNextBenchmark() async {
+    final html.HttpRequest request = await _requestXhr(
+      '/next-benchmark',
+      method: 'POST',
+      mimeType: 'application/json',
+      sendData: json.encode(_benchmarks.keys.toList()),
+    );
+
+    // `kEndOfBenchmarks` is expected when the benchmark server is telling us there are no more benchmarks to run.
+    // 404 is expected when the benchmark is run using plain `flutter run`, which does not provide "next-benchmark" handler.
+    if (request.responseText == kEndOfBenchmarks || request.status == 404) {
+      isInManualMode = true;
+      return kManualFallback;
+    }
+
+    isInManualMode = false;
+    return request.responseText;
+  }
+
+  void _checkNotManualMode() {
+    if (isInManualMode) {
+      throw StateError('Operation not supported in manual fallback mode.');
+    }
+  }
+
+  /// Asks the local server to begin tracing performance.
+  ///
+  /// This uses the chrome://tracing tracer, which is not available from within
+  /// the page itself, and therefore must be controlled from outside using the
+  /// DevTools Protocol.
+  Future<void> startPerformanceTracing(String benchmarkName) async {
+    _checkNotManualMode();
+    await html.HttpRequest.request(
+      '/start-performance-tracing?label=$benchmarkName',
+      method: 'POST',
+      mimeType: 'application/json',
+    );
+  }
+
+  /// Stops the performance tracing session started by [startPerformanceTracing].
+  Future<void> stopPerformanceTracing() async {
+    _checkNotManualMode();
+    await html.HttpRequest.request(
+      '/stop-performance-tracing',
+      method: 'POST',
+      mimeType: 'application/json',
+    );
+  }
+
+  /// Sends the profile data collected by the benchmark to the local benchmark
+  /// server.
+  Future<void> sendProfileData(Profile profile) async {
+    _checkNotManualMode();
+    final html.HttpRequest request = await html.HttpRequest.request(
+      '/profile-data',
+      method: 'POST',
+      mimeType: 'application/json',
+      sendData: json.encode(profile.toJson()),
+    );
+    if (request.status != 200) {
+      throw Exception('Failed to report profile data to benchmark server. '
+          'The server responded with status code ${request.status}.');
+    }
+  }
+
+  /// Reports an error to the benchmark server.
+  ///
+  /// The server will halt the devicelab task and log the error.
+  Future<void> reportError(dynamic error, StackTrace stackTrace) async {
+    _checkNotManualMode();
+    await html.HttpRequest.request(
+      '/on-error',
+      method: 'POST',
+      mimeType: 'application/json',
+      sendData: json.encode(<String, dynamic>{
+        'error': '$error',
+        'stackTrace': '$stackTrace',
+      }),
+    );
+  }
+
+  /// Reports a message about the demo to the benchmark server.
+  Future<void> printToConsole(String report) async {
+    _checkNotManualMode();
+    await html.HttpRequest.request(
+      '/print-to-console',
+      method: 'POST',
+      mimeType: 'text/plain',
+      sendData: report,
+    );
+  }
+
+  /// This is the same as calling [html.HttpRequest.request] but it doesn't
+  /// crash on 404, which we use to detect `flutter run`.
+  Future<html.HttpRequest> _requestXhr(
+    String url, {
+    @required String method,
+    @required String mimeType,
+    @required dynamic sendData,
+  }) {
+    final Completer<html.HttpRequest> completer = Completer<html.HttpRequest>();
+    final html.HttpRequest xhr = html.HttpRequest();
+    method ??= 'GET';
+    xhr.open(method, url, async: true);
+    xhr.overrideMimeType(mimeType);
+    xhr.onLoad.listen((html.ProgressEvent e) {
+      completer.complete(xhr);
+    });
+    xhr.onError.listen(completer.completeError);
+    xhr.send(sendData);
+    return completer.future;
+  }
+}
diff --git a/packages/web_benchmarks/lib/server.dart b/packages/web_benchmarks/lib/server.dart
new file mode 100644
index 0000000..77300c0
--- /dev/null
+++ b/packages/web_benchmarks/lib/server.dart
@@ -0,0 +1,62 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+
+import 'src/benchmark_result.dart';
+import 'src/runner.dart';
+
+export 'src/benchmark_result.dart';
+
+/// The default port number used by the local benchmark server.
+const int defaultBenchmarkServerPort = 9999;
+
+/// The default port number used for Chrome DevTool Protocol.
+const int defaultChromeDebugPort = 10000;
+
+/// Builds and serves a Flutter Web app, collects raw benchmark data and
+/// summarizes the result as a [BenchmarkResult].
+///
+/// [benchmarkAppDirectory] is the directory containing the app that's being
+/// benchmarked. The app is expected to use `package:web_benchmarks/client.dart`
+/// and call the `runBenchmarks` function to run the benchmarks.
+///
+/// [entryPoint] is the path to the main app file that runs the benchmark. It
+/// can be different (and typically is) from the production entry point of the
+/// app.
+///
+/// If [useCanvasKit] is true, builds the app in CanvasKit mode.
+///
+/// [benchmarkServerPort] is the port this benchmark server serves the app on.
+/// By default uses [defaultBenchmarkServerPort].
+///
+/// [chromeDebugPort] is the port Chrome uses for DevTool Protocol used to
+/// extract tracing data. By default uses [defaultChromeDebugPort].
+///
+/// If [headless] is true, runs Chrome without UI. In particular, this is
+/// useful in environments (e.g. CI) that doesn't have a display.
+Future<BenchmarkResults> serveWebBenchmark({
+  @required io.Directory benchmarkAppDirectory,
+  @required String entryPoint,
+  @required bool useCanvasKit,
+  int benchmarkServerPort = defaultBenchmarkServerPort,
+  int chromeDebugPort = defaultChromeDebugPort,
+  bool headless = true,
+}) async {
+  // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
+  Logger.root.level = Level.INFO;
+
+  return BenchmarkServer(
+    benchmarkAppDirectory: benchmarkAppDirectory,
+    entryPoint: entryPoint,
+    useCanvasKit: useCanvasKit,
+    benchmarkServerPort: benchmarkServerPort,
+    chromeDebugPort: chromeDebugPort,
+    headless: headless,
+  ).run();
+}
diff --git a/packages/web_benchmarks/lib/src/benchmark_result.dart b/packages/web_benchmarks/lib/src/benchmark_result.dart
new file mode 100644
index 0000000..4f6c458
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/benchmark_result.dart
@@ -0,0 +1,59 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+
+/// A single benchmark score value collected from the benchmark.
+class BenchmarkScore {
+  /// Creates a benchmark score.
+  ///
+  /// [metric] and [value] must not be null.
+  BenchmarkScore({
+    @required this.metric,
+    @required this.value,
+  }) : assert(metric != null && value != null);
+
+  /// The name of the metric that this score is categorized under.
+  ///
+  /// Scores collected over time under the same name can be visualized as a
+  /// timeline.
+  final String metric;
+
+  /// The result of measuring a particular metric in this benchmark run.
+  final num value;
+
+  /// Serializes the benchmark metric to a JSON object.
+  Map<String, dynamic> toJson() {
+    return <String, dynamic>{
+      'metric': metric,
+      'value': value,
+    };
+  }
+}
+
+/// The result of running a benchmark.
+class BenchmarkResults {
+  /// Constructs a result containing scores from a single run benchmark run.
+  BenchmarkResults(this.scores);
+
+  /// Scores collected in a benchmark run.
+  final Map<String, List<BenchmarkScore>> scores;
+
+  /// Serializes benchmark metrics to JSON.
+  Map<String, List<Map<String, dynamic>>> toJson() {
+    return scores.map<String, List<Map<String, dynamic>>>(
+        (String benchmarkName, List<BenchmarkScore> scores) {
+      return MapEntry<String, List<Map<String, dynamic>>>(
+        benchmarkName,
+        scores
+            .map<Map<String, dynamic>>(
+                (BenchmarkScore score) => <String, dynamic>{
+                      'metric': score.metric,
+                      'value': score.value,
+                    })
+            .toList(),
+      );
+    });
+  }
+}
diff --git a/packages/web_benchmarks/lib/src/browser.dart b/packages/web_benchmarks/lib/src/browser.dart
new file mode 100644
index 0000000..413adfe
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/browser.dart
@@ -0,0 +1,612 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json, utf8, LineSplitter, JsonEncoder;
+import 'dart:io' as io;
+import 'dart:math' as math;
+
+import 'package:path/path.dart' as path;
+import 'package:meta/meta.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'common.dart';
+
+/// Options passed to Chrome when launching it.
+class ChromeOptions {
+  /// Creates chrome options.
+  ///
+  /// [windowWidth], [windowHeight], and [headless] must not be null.
+  ChromeOptions({
+    this.userDataDirectory,
+    this.url,
+    this.windowWidth = 1024,
+    this.windowHeight = 1024,
+    this.headless,
+    this.debugPort,
+  });
+
+  /// If not null passed as `--user-data-dir`.
+  final String userDataDirectory;
+
+  /// If not null launches a Chrome tab at this URL.
+  final String url;
+
+  /// The width of the Chrome window.
+  ///
+  /// This is important for screenshots and benchmarks.
+  final int windowWidth;
+
+  /// The height of the Chrome window.
+  ///
+  /// This is important for screenshots and benchmarks.
+  final int windowHeight;
+
+  /// Launches code in "headless" mode, which allows running Chrome in
+  /// environments without a display, such as LUCI and Cirrus.
+  final bool headless;
+
+  /// The port Chrome will use for its debugging protocol.
+  ///
+  /// If null, Chrome is launched without debugging. When running in headless
+  /// mode without a debug port, Chrome quits immediately. For most tests it is
+  /// typical to set [headless] to true and set a non-null debug port.
+  final int debugPort;
+}
+
+/// A function called when the Chrome process encounters an error.
+typedef ChromeErrorCallback = void Function(String);
+
+/// Manages a single Chrome process.
+class Chrome {
+  Chrome._(this._chromeProcess, this._onError, this._debugConnection,
+      bool headless) {
+    if (headless) {
+      // In headless mode, if the Chrome process quits before it was asked to
+      // quit, notify the error listener. If it's not running headless, the
+      // developer may close the browser any time, so it's not considered to
+      // be an error.
+      _chromeProcess.exitCode.then((int exitCode) {
+        if (!_isStopped) {
+          _onError(
+              'Chrome process exited prematurely with exit code $exitCode');
+        }
+      });
+    }
+  }
+
+  /// Launches Chrome with the give [options].
+  ///
+  /// The [onError] callback is called with an error message when the Chrome
+  /// process encounters an error. In particular, [onError] is called when the
+  /// Chrome process exits prematurely, i.e. before [stop] is called.
+  static Future<Chrome> launch(ChromeOptions options,
+      {String workingDirectory, @required ChromeErrorCallback onError}) async {
+    if (!io.Platform.isWindows) {
+      final io.ProcessResult versionResult = io.Process.runSync(
+          _findSystemChromeExecutable(), const <String>['--version']);
+      print('Launching ${versionResult.stdout}');
+    } else {
+      print('Launching Chrome...');
+    }
+
+    final bool withDebugging = options.debugPort != null;
+    final List<String> args = <String>[
+      if (options.userDataDirectory != null)
+        '--user-data-dir=${options.userDataDirectory}',
+      if (options.url != null) options.url,
+      if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true')
+        '--no-sandbox',
+      if (options.headless) '--headless',
+      if (withDebugging) '--remote-debugging-port=${options.debugPort}',
+      '--window-size=${options.windowWidth},${options.windowHeight}',
+      '--disable-extensions',
+      '--disable-popup-blocking',
+      // Indicates that the browser is in "browse without sign-in" (Guest session) mode.
+      '--bwsi',
+      '--no-first-run',
+      '--no-default-browser-check',
+      '--disable-default-apps',
+      '--disable-translate',
+    ];
+    final io.Process chromeProcess = await io.Process.start(
+      _findSystemChromeExecutable(),
+      args,
+      workingDirectory: workingDirectory,
+    );
+
+    WipConnection debugConnection;
+    if (withDebugging) {
+      debugConnection =
+          await _connectToChromeDebugPort(chromeProcess, options.debugPort);
+    }
+
+    return Chrome._(chromeProcess, onError, debugConnection, options.headless);
+  }
+
+  final io.Process _chromeProcess;
+  final ChromeErrorCallback _onError;
+  final WipConnection _debugConnection;
+  bool _isStopped = false;
+
+  Completer<void> _tracingCompleter;
+  StreamSubscription<WipEvent> _tracingSubscription;
+  List<Map<String, dynamic>> _tracingData;
+
+  /// Starts recording a performance trace.
+  ///
+  /// If there is already a tracing session in progress, throws an error. Call
+  /// [endRecordingPerformance] before starting a new tracing session.
+  ///
+  /// The [label] is for debugging convenience.
+  Future<void> beginRecordingPerformance(String label) async {
+    if (_tracingCompleter != null) {
+      throw StateError(
+          'Cannot start a new performance trace. A tracing session labeled '
+          '"$label" is already in progress.');
+    }
+    _tracingCompleter = Completer<void>();
+    _tracingData = <Map<String, dynamic>>[];
+
+    // Subscribe to tracing events prior to calling "Tracing.start". Otherwise,
+    // we'll miss tracing data.
+    _tracingSubscription =
+        _debugConnection.onNotification.listen((WipEvent event) {
+      // We receive data as a sequence of "Tracing.dataCollected" followed by
+      // "Tracing.tracingComplete" at the end. Until "Tracing.tracingComplete"
+      // is received, the data may be incomplete.
+      if (event.method == 'Tracing.tracingComplete') {
+        _tracingCompleter.complete();
+        _tracingSubscription.cancel();
+        _tracingSubscription = null;
+      } else if (event.method == 'Tracing.dataCollected') {
+        final dynamic value = event.params['value'];
+        if (value is! List) {
+          throw FormatException(
+              '"Tracing.dataCollected" returned malformed data. '
+              'Expected a List but got: ${value.runtimeType}');
+        }
+        _tracingData.addAll(event.params['value'].cast<Map<String, dynamic>>());
+      }
+    });
+    await _debugConnection.sendCommand('Tracing.start', <String, dynamic>{
+      // The choice of categories is as follows:
+      //
+      // blink:
+      //   provides everything on the UI thread, including scripting,
+      //   style recalculations, layout, painting, and some compositor
+      //   work.
+      // blink.user_timing:
+      //   provides marks recorded using window.performance. We use marks
+      //   to find frames that the benchmark cares to measure.
+      // gpu:
+      //   provides tracing data from the GPU data
+      //   disabled due to https://bugs.chromium.org/p/chromium/issues/detail?id=1068259
+      // TODO(yjbanov): extract useful GPU data
+      'categories': 'blink,blink.user_timing',
+      'transferMode': 'SendAsStream',
+    });
+  }
+
+  /// Stops a performance tracing session started by [beginRecordingPerformance].
+  ///
+  /// Returns all the collected tracing data unfiltered.
+  Future<List<Map<String, dynamic>>> endRecordingPerformance() async {
+    await _debugConnection.sendCommand('Tracing.end');
+    await _tracingCompleter.future;
+    final List<Map<String, dynamic>> data = _tracingData;
+    _tracingCompleter = null;
+    _tracingData = null;
+    return data;
+  }
+
+  /// Stops the Chrome process.
+  void stop() {
+    _isStopped = true;
+    _chromeProcess.kill();
+  }
+
+  /// Resolves when the Chrome process exits.
+  Future<void> get whenExits async {
+    await _chromeProcess.exitCode;
+  }
+}
+
+String _findSystemChromeExecutable() {
+  // On some environments, such as the Dart HHH tester, Chrome resides in a
+  // non-standard location and is provided via the following environment
+  // variable.
+  final String envExecutable = io.Platform.environment['CHROME_EXECUTABLE'];
+  if (envExecutable != null) {
+    return envExecutable;
+  }
+
+  if (io.Platform.isLinux) {
+    final io.ProcessResult which =
+        io.Process.runSync('which', <String>['google-chrome']);
+
+    if (which.exitCode != 0) {
+      throw Exception('Failed to locate system Chrome installation.');
+    }
+
+    final String output = which.stdout;
+    return output.trim();
+  } else if (io.Platform.isMacOS) {
+    return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+  } else if (io.Platform.isWindows) {
+    const String kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
+    final List<String> kWindowsPrefixes = <String>[
+      io.Platform.environment['LOCALAPPDATA'],
+      io.Platform.environment['PROGRAMFILES'],
+      io.Platform.environment['PROGRAMFILES(X86)'],
+    ];
+    final String windowsPrefix = kWindowsPrefixes.firstWhere((String prefix) {
+      if (prefix == null) {
+        return false;
+      }
+      final String expectedPath = path.join(prefix, kWindowsExecutable);
+      return io.File(expectedPath).existsSync();
+    }, orElse: () => '.');
+    return path.join(windowsPrefix, kWindowsExecutable);
+  } else {
+    throw Exception(
+        'Web benchmarks cannot run on ${io.Platform.operatingSystem}.');
+  }
+}
+
+/// Waits for Chrome to print DevTools URI and connects to it.
+Future<WipConnection> _connectToChromeDebugPort(
+    io.Process chromeProcess, int port) async {
+  chromeProcess.stdout
+      .transform(utf8.decoder)
+      .transform(const LineSplitter())
+      .listen((String line) {
+    print('[CHROME]: $line');
+  });
+
+  await chromeProcess.stderr
+      .transform(utf8.decoder)
+      .transform(const LineSplitter())
+      .map((String line) {
+    print('[CHROME]: $line');
+    return line;
+  }).firstWhere((String line) => line.startsWith('DevTools listening'),
+          orElse: () {
+    throw Exception('Expected Chrome to print "DevTools listening" string '
+        'with DevTools URL, but the string was never printed.');
+  });
+
+  final Uri devtoolsUri =
+      await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
+  print('Connecting to DevTools: $devtoolsUri');
+  final ChromeConnection chromeConnection = ChromeConnection('localhost', port);
+  final Iterable<ChromeTab> tabs =
+      (await chromeConnection.getTabs()).where((ChromeTab tab) {
+    return tab.url.startsWith('http://localhost');
+  });
+  final ChromeTab tab = tabs.single;
+  final WipConnection debugConnection = await tab.connect();
+  print('Connected to Chrome tab: ${tab.title} (${tab.url})');
+  return debugConnection;
+}
+
+/// Gets the Chrome debugger URL for the web page being benchmarked.
+Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
+  final io.HttpClient client = io.HttpClient();
+  final io.HttpClientRequest request =
+      await client.getUrl(base.resolve('/json/list'));
+  final io.HttpClientResponse response = await request.close();
+  final List<dynamic> jsonObject =
+      await json.fuse(utf8).decoder.bind(response).single;
+  if (jsonObject == null || jsonObject.isEmpty) {
+    return base;
+  }
+  return base.resolve(jsonObject.first['webSocketDebuggerUrl']);
+}
+
+/// Summarizes a Blink trace down to a few interesting values.
+class BlinkTraceSummary {
+  BlinkTraceSummary._({
+    @required this.averageBeginFrameTime,
+    @required this.averageUpdateLifecyclePhasesTime,
+  }) : averageTotalUIFrameTime =
+            averageBeginFrameTime + averageUpdateLifecyclePhasesTime;
+
+  /// Summarizes Blink trace from the raw JSON trace.
+  static BlinkTraceSummary fromJson(List<Map<String, dynamic>> traceJson) {
+    try {
+      // Convert raw JSON data to BlinkTraceEvent objects sorted by timestamp.
+      List<BlinkTraceEvent> events = traceJson
+          .map<BlinkTraceEvent>(BlinkTraceEvent.fromJson)
+          .toList()
+            ..sort((BlinkTraceEvent a, BlinkTraceEvent b) => a.ts - b.ts);
+
+      Exception noMeasuredFramesFound() => Exception(
+            'No measured frames found in benchmark tracing data. This likely '
+            'indicates a bug in the benchmark. For example, the benchmark failed '
+            'to pump enough frames. It may also indicate a change in Chrome\'s '
+            'tracing data format. Check if Chrome version changed recently and '
+            'adjust the parsing code accordingly.',
+          );
+
+      // Use the pid from the first "measured_frame" event since the event is
+      // emitted by the script running on the process we're interested in.
+      //
+      // We previously tried using the "CrRendererMain" event. However, for
+      // reasons unknown, Chrome in the devicelab refuses to emit this event
+      // sometimes, causing to flakes.
+      final BlinkTraceEvent firstMeasuredFrameEvent = events.firstWhere(
+        (BlinkTraceEvent event) => event.isBeginMeasuredFrame,
+        orElse: () => throw noMeasuredFramesFound(),
+      );
+
+      if (firstMeasuredFrameEvent == null) {
+        // This happens in benchmarks that do not measure frames, such as some
+        // of the text layout benchmarks.
+        return null;
+      }
+
+      final int tabPid = firstMeasuredFrameEvent.pid;
+
+      // Filter out data from unrelated processes
+      events = events
+          .where((BlinkTraceEvent element) => element.pid == tabPid)
+          .toList();
+
+      // Extract frame data.
+      final List<BlinkFrame> frames = <BlinkFrame>[];
+      int skipCount = 0;
+      BlinkFrame frame = BlinkFrame();
+      for (final BlinkTraceEvent event in events) {
+        if (event.isBeginFrame) {
+          frame.beginFrame = event;
+        } else if (event.isUpdateAllLifecyclePhases) {
+          frame.updateAllLifecyclePhases = event;
+          if (frame.endMeasuredFrame != null) {
+            frames.add(frame);
+          } else {
+            skipCount += 1;
+          }
+          frame = BlinkFrame();
+        } else if (event.isBeginMeasuredFrame) {
+          frame.beginMeasuredFrame = event;
+        } else if (event.isEndMeasuredFrame) {
+          frame.endMeasuredFrame = event;
+        }
+      }
+
+      print('Extracted ${frames.length} measured frames.');
+      print('Skipped $skipCount non-measured frames.');
+
+      if (frames.isEmpty) {
+        throw noMeasuredFramesFound();
+      }
+
+      // Compute averages and summarize.
+      return BlinkTraceSummary._(
+        averageBeginFrameTime: _computeAverageDuration(
+            frames.map((BlinkFrame frame) => frame.beginFrame).toList()),
+        averageUpdateLifecyclePhasesTime: _computeAverageDuration(frames
+            .map((BlinkFrame frame) => frame.updateAllLifecyclePhases)
+            .toList()),
+      );
+    } catch (_, __) {
+      final io.File traceFile = io.File('./chrome-trace.json');
+      io.stderr.writeln(
+          'Failed to interpret the Chrome trace contents. The trace was saved in ${traceFile.path}');
+      traceFile.writeAsStringSync(
+          const JsonEncoder.withIndent('  ').convert(traceJson));
+      rethrow;
+    }
+  }
+
+  /// The average duration of "WebViewImpl::beginFrame" events.
+  ///
+  /// This event contains all of scripting time of an animation frame, plus an
+  /// unknown small amount of work browser does before and after scripting.
+  final Duration averageBeginFrameTime;
+
+  /// The average duration of "WebViewImpl::updateAllLifecyclePhases" events.
+  ///
+  /// This event contains style, layout, painting, and compositor computations,
+  /// which are not included in the scripting time. This event does not
+  /// include GPU time, which happens on a separate thread.
+  final Duration averageUpdateLifecyclePhasesTime;
+
+  /// The average sum of [averageBeginFrameTime] and
+  /// [averageUpdateLifecyclePhasesTime].
+  ///
+  /// This value contains the vast majority of work the UI thread performs in
+  /// any given animation frame.
+  final Duration averageTotalUIFrameTime;
+
+  @override
+  String toString() => '$BlinkTraceSummary('
+      'averageBeginFrameTime: ${averageBeginFrameTime.inMicroseconds / 1000}ms, '
+      'averageUpdateLifecyclePhasesTime: ${averageUpdateLifecyclePhasesTime.inMicroseconds / 1000}ms)';
+}
+
+/// Contains events pertaining to a single frame in the Blink trace data.
+class BlinkFrame {
+  /// Corresponds to 'WebViewImpl::beginFrame' event.
+  BlinkTraceEvent beginFrame;
+
+  /// Corresponds to 'WebViewImpl::updateAllLifecyclePhases' event.
+  BlinkTraceEvent updateAllLifecyclePhases;
+
+  /// Corresponds to 'measured_frame' begin event.
+  BlinkTraceEvent beginMeasuredFrame;
+
+  /// Corresponds to 'measured_frame' end event.
+  BlinkTraceEvent endMeasuredFrame;
+}
+
+/// Takes a list of events that have non-null [BlinkTraceEvent.tdur] computes
+/// their average as a [Duration] value.
+Duration _computeAverageDuration(List<BlinkTraceEvent> events) {
+  // Compute the sum of "tdur" fields of the last kMeasuredSampleCount events.
+  final double sum = events
+      .skip(math.max(events.length - kMeasuredSampleCount, 0))
+      .fold(0.0, (double previousValue, BlinkTraceEvent event) {
+    if (event.tdur == null) {
+      throw FormatException('Trace event lacks "tdur" field: $event');
+    }
+    return previousValue + event.tdur;
+  });
+  final int sampleCount = math.min(events.length, kMeasuredSampleCount);
+  return Duration(microseconds: sum ~/ sampleCount);
+}
+
+/// An event collected by the Blink tracer (in Chrome accessible using chrome://tracing).
+///
+/// See also:
+///  * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
+class BlinkTraceEvent {
+  BlinkTraceEvent._({
+    @required this.args,
+    @required this.cat,
+    @required this.name,
+    @required this.ph,
+    @required this.pid,
+    @required this.tid,
+    @required this.ts,
+    @required this.tts,
+    @required this.tdur,
+  });
+
+  /// Parses an event from its JSON representation.
+  ///
+  /// Sample event encoded as JSON (the data is bogus, this just shows the format):
+  ///
+  /// ```
+  /// {
+  ///   "name": "myName",
+  ///   "cat": "category,list",
+  ///   "ph": "B",
+  ///   "ts": 12345,
+  ///   "pid": 123,
+  ///   "tid": 456,
+  ///   "args": {
+  ///     "someArg": 1,
+  ///     "anotherArg": {
+  ///       "value": "my value"
+  ///     }
+  ///   }
+  /// }
+  /// ```
+  ///
+  /// For detailed documentation of the format see:
+  ///
+  /// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
+  static BlinkTraceEvent fromJson(Map<String, dynamic> json) {
+    return BlinkTraceEvent._(
+      args: json['args'],
+      cat: json['cat'],
+      name: json['name'],
+      ph: json['ph'],
+      pid: _readInt(json, 'pid'),
+      tid: _readInt(json, 'tid'),
+      ts: _readInt(json, 'ts'),
+      tts: _readInt(json, 'tts'),
+      tdur: _readInt(json, 'tdur'),
+    );
+  }
+
+  /// Event-specific data.
+  final Map<String, dynamic> args;
+
+  /// Event category.
+  final String cat;
+
+  /// Event name.
+  final String name;
+
+  /// Event "phase".
+  final String ph;
+
+  /// Process ID of the process that emitted the event.
+  final int pid;
+
+  /// Thread ID of the thread that emitted the event.
+  final int tid;
+
+  /// Timestamp in microseconds using tracer clock.
+  final int ts;
+
+  /// Timestamp in microseconds using thread clock.
+  final int tts;
+
+  /// Event duration in microseconds.
+  final int tdur;
+
+  /// A "begin frame" event contains all of the scripting time of an animation
+  /// frame (JavaScript, WebAssembly), plus a negligible amount of internal
+  /// browser overhead.
+  ///
+  /// This event does not include non-UI thread scripting, such as web workers,
+  /// service workers, and CSS Paint paintlets.
+  ///
+  /// WebViewImpl::beginFrame was used in earlier versions of Chrome, kept
+  /// for compatibility.
+  ///
+  /// This event is a duration event that has its `tdur` populated.
+  bool get isBeginFrame =>
+      ph == 'X' &&
+      (name == 'WebViewImpl::beginFrame' ||
+          name == 'WebFrameWidgetBase::BeginMainFrame');
+
+  /// An "update all lifecycle phases" event contains UI thread computations
+  /// related to an animation frame that's outside the scripting phase.
+  ///
+  /// This event includes style recalculation, layer tree update, layout,
+  /// painting, and parts of compositing work.
+  ///
+  /// This event is a duration event that has its `tdur` populated.
+  bool get isUpdateAllLifecyclePhases =>
+      ph == 'X' && name == 'WebViewImpl::updateAllLifecyclePhases';
+
+  /// Whether this is the beginning of a "measured_frame" event.
+  ///
+  /// This event is a custom event emitted by our benchmark test harness.
+  ///
+  /// See also:
+  ///  * `recorder.dart`, which emits this event.
+  bool get isBeginMeasuredFrame => ph == 'b' && name == 'measured_frame';
+
+  /// Whether this is the end of a "measured_frame" event.
+  ///
+  /// This event is a custom event emitted by our benchmark test harness.
+  ///
+  /// See also:
+  ///  * `recorder.dart`, which emits this event.
+  bool get isEndMeasuredFrame => ph == 'e' && name == 'measured_frame';
+
+  @override
+  String toString() => '$BlinkTraceEvent('
+      'args: ${json.encode(args)}, '
+      'cat: $cat, '
+      'name: $name, '
+      'ph: $ph, '
+      'pid: $pid, '
+      'tid: $tid, '
+      'ts: $ts, '
+      'tts: $tts, '
+      'tdur: $tdur)';
+}
+
+/// Read an integer out of [json] stored under [key].
+///
+/// Since JSON does not distinguish between `int` and `double`, extra
+/// validation and conversion is needed.
+///
+/// Returns null if the value is null.
+int _readInt(Map<String, dynamic> json, String key) {
+  final num jsonValue = json[key];
+
+  if (jsonValue == null) {
+    return null;
+  }
+
+  return jsonValue.toInt();
+}
diff --git a/packages/web_benchmarks/lib/src/common.dart b/packages/web_benchmarks/lib/src/common.dart
new file mode 100644
index 0000000..7795287
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/common.dart
@@ -0,0 +1,16 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// This library contains code that's common between the client and the server.
+///
+/// The code must be compilable both as a command-line program and as a web
+/// program.
+library web_benchmarks.common;
+
+/// The number of samples we use to collect statistics from.
+const int kMeasuredSampleCount = 100;
+
+/// A special value returned by the `/next-benchmark` HTTP POST request when
+/// all benchmarks have run and there are no more benchmarks to run.
+const String kEndOfBenchmarks = '__end_of_benchmarks__';
diff --git a/packages/web_benchmarks/lib/src/recorder.dart b/packages/web_benchmarks/lib/src/recorder.dart
new file mode 100644
index 0000000..f9e6df5
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/recorder.dart
@@ -0,0 +1,1243 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:html' as html;
+import 'dart:js_util' as js_util;
+import 'dart:math' as math;
+import 'dart:ui';
+
+import 'package:meta/meta.dart';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+
+import 'common.dart';
+
+/// The number of samples from warm-up iterations.
+///
+/// We warm-up the benchmark prior to measuring to allow JIT and caches to settle.
+const int _kWarmUpSampleCount = 200;
+
+/// The total number of samples collected by a benchmark.
+const int kTotalSampleCount = _kWarmUpSampleCount + kMeasuredSampleCount;
+
+/// A benchmark metric that includes frame-related computations prior to
+/// submitting layer and picture operations to the underlying renderer, such as
+/// HTML and CanvasKit. During this phase we compute transforms, clips, and
+/// other information needed for rendering.
+const String kProfilePrerollFrame = 'preroll_frame';
+
+/// A benchmark metric that includes submitting layer and picture information
+/// to the renderer.
+const String kProfileApplyFrame = 'apply_frame';
+
+/// Measures the amount of time [action] takes.
+Duration timeAction(VoidCallback action) {
+  final Stopwatch stopwatch = Stopwatch()..start();
+  action();
+  stopwatch.stop();
+  return stopwatch.elapsed;
+}
+
+/// A function that performs asynchronous work.
+typedef AsyncVoidCallback = Future<void> Function();
+
+/// An [AsyncVoidCallback] that doesn't do anything.
+///
+/// This is used just so we don't have to deal with null all over the place.
+Future<void> _dummyAsyncVoidCallback() async {}
+
+/// Runs the benchmark using the given [recorder].
+///
+/// Notifies about "set up" and "tear down" events via the [setUpAllDidRun]
+/// and [tearDownAllWillRun] callbacks.
+@sealed
+class Runner {
+  /// Creates a runner for the [recorder].
+  ///
+  /// All arguments must not be null.
+  Runner({
+    @required this.recorder,
+    this.setUpAllDidRun = _dummyAsyncVoidCallback,
+    this.tearDownAllWillRun = _dummyAsyncVoidCallback,
+  });
+
+  /// The recorder that will run and record the benchmark.
+  final Recorder recorder;
+
+  /// Called immediately after [Recorder.setUpAll] future is resolved.
+  ///
+  /// This is useful, for example, to kick off a profiler or a tracer such that
+  /// the "set up" computations are not included in the metrics.
+  final AsyncVoidCallback setUpAllDidRun;
+
+  /// Called just before calling [Recorder.tearDownAll].
+  ///
+  /// This is useful, for example, to stop a profiler or a tracer such that
+  /// the "tear down" computations are not included in the metrics.
+  final AsyncVoidCallback tearDownAllWillRun;
+
+  /// Runs the benchmark and reports the results.
+  Future<Profile> run() async {
+    await recorder.setUpAll();
+    await setUpAllDidRun();
+    final Profile profile = await recorder.run();
+    await tearDownAllWillRun();
+    await recorder.tearDownAll();
+    return profile;
+  }
+}
+
+/// Base class for benchmark recorders.
+///
+/// Each benchmark recorder has a [name] and a [run] method at a minimum.
+abstract class Recorder {
+  Recorder._(this.name, this.isTracingEnabled);
+
+  /// Whether this recorder requires tracing using Chrome's DevTools Protocol's
+  /// "Tracing" API.
+  final bool isTracingEnabled;
+
+  /// The name of the benchmark.
+  ///
+  /// The results displayed in the Flutter Dashboard will use this name as a
+  /// prefix.
+  final String name;
+
+  /// Returns the recorded profile.
+  ///
+  /// This value is only available while the benchmark is running.
+  Profile get profile;
+
+  /// Whether the benchmark should continue running.
+  ///
+  /// Returns `false` if the benchmark collected enough data and it's time to
+  /// stop.
+  bool shouldContinue() => profile.shouldContinue();
+
+  /// Called once before all runs of this benchmark recorder.
+  ///
+  /// This is useful for doing one-time setup work that's needed for the
+  /// benchmark.
+  Future<void> setUpAll() async {}
+
+  /// The implementation of the benchmark that will produce a [Profile].
+  Future<Profile> run();
+
+  /// Called once after all runs of this benchmark recorder.
+  ///
+  /// This is useful for doing one-time clean up work after the benchmark is
+  /// complete.
+  Future<void> tearDownAll() async {}
+}
+
+/// A recorder for benchmarking raw execution of Dart code.
+///
+/// This is useful for benchmarks that don't need frames or widgets.
+///
+/// Example:
+///
+/// ```
+/// class BenchForLoop extends RawRecorder {
+///   BenchForLoop() : super(name: benchmarkName);
+///
+///   static const String benchmarkName = 'for_loop';
+///
+///   @override
+///   void body(Profile profile) {
+///     profile.record('loop', () {
+///       double x = 0;
+///       for (int i = 0; i < 10000000; i++) {
+///         x *= 1.5;
+///       }
+///     });
+///   }
+/// }
+/// ```
+abstract class RawRecorder extends Recorder {
+  /// Creates a raw benchmark recorder with a name.
+  ///
+  /// [name] must not be null.
+  RawRecorder({@required String name}) : super._(name, false);
+
+  /// The body of the benchmark.
+  ///
+  /// This is the part that records measurements of the benchmark.
+  void body(Profile profile);
+
+  @override
+  Profile get profile => _profile;
+  Profile _profile;
+
+  @override
+  @nonVirtual
+  Future<Profile> run() async {
+    _profile = Profile(name: name);
+    do {
+      await Future<void>.delayed(Duration.zero);
+      body(_profile);
+    } while (shouldContinue());
+    return _profile;
+  }
+}
+
+/// A recorder for benchmarking interactions with the engine without the
+/// framework by directly exercising [SceneBuilder].
+///
+/// To implement a benchmark, extend this class and implement [onDrawFrame].
+///
+/// Example:
+///
+/// ```
+/// class BenchDrawCircle extends SceneBuilderRecorder {
+///   BenchDrawCircle() : super(name: benchmarkName);
+///
+///   static const String benchmarkName = 'draw_circle';
+///
+///   @override
+///   void onDrawFrame(SceneBuilder sceneBuilder) {
+///     final PictureRecorder pictureRecorder = PictureRecorder();
+///     final Canvas canvas = Canvas(pictureRecorder);
+///     final Paint paint = Paint()..color = const Color.fromARGB(255, 255, 0, 0);
+///     final Size windowSize = window.physicalSize;
+///     canvas.drawCircle(windowSize.center(Offset.zero), 50.0, paint);
+///     final Picture picture = pictureRecorder.endRecording();
+///     sceneBuilder.addPicture(picture);
+///   }
+/// }
+/// ```
+abstract class SceneBuilderRecorder extends Recorder {
+  /// Creates a [SceneBuilder] benchmark recorder.
+  ///
+  /// [name] must not be null.
+  SceneBuilderRecorder({@required String name}) : super._(name, true);
+
+  @override
+  Profile get profile => _profile;
+  Profile _profile;
+
+  /// Called from [Window.onBeginFrame].
+  @mustCallSuper
+  void onBeginFrame() {}
+
+  /// Called on every frame.
+  ///
+  /// An implementation should exercise the [sceneBuilder] to build a frame.
+  /// However, it must not call [SceneBuilder.build] or [Window.render].
+  /// Instead the benchmark harness will call them and time them appropriately.
+  void onDrawFrame(SceneBuilder sceneBuilder);
+
+  @override
+  Future<Profile> run() {
+    final Completer<Profile> profileCompleter = Completer<Profile>();
+    _profile = Profile(name: name);
+
+    window.onBeginFrame = (_) {
+      try {
+        startMeasureFrame(profile);
+        onBeginFrame();
+      } catch (error, stackTrace) {
+        profileCompleter.completeError(error, stackTrace);
+        rethrow;
+      }
+    };
+    window.onDrawFrame = () {
+      try {
+        _profile.record('drawFrameDuration', () {
+          final SceneBuilder sceneBuilder = SceneBuilder();
+          onDrawFrame(sceneBuilder);
+          _profile.record('sceneBuildDuration', () {
+            final Scene scene = sceneBuilder.build();
+            _profile.record('windowRenderDuration', () {
+              window.render(scene);
+            }, reported: false);
+          }, reported: false);
+        }, reported: true);
+        endMeasureFrame();
+
+        if (shouldContinue()) {
+          window.scheduleFrame();
+        } else {
+          profileCompleter.complete(_profile);
+        }
+      } catch (error, stackTrace) {
+        profileCompleter.completeError(error, stackTrace);
+        rethrow;
+      }
+    };
+    window.scheduleFrame();
+    return profileCompleter.future;
+  }
+}
+
+/// A recorder for benchmarking interactions with the framework by creating
+/// widgets.
+///
+/// To implement a benchmark, extend this class and implement [createWidget].
+///
+/// Example:
+///
+/// ```
+/// class BenchListView extends WidgetRecorder {
+///   BenchListView() : super(name: benchmarkName);
+///
+///   static const String benchmarkName = 'bench_list_view';
+///
+///   @override
+///   Widget createWidget() {
+///     return Directionality(
+///       textDirection: TextDirection.ltr,
+///       child: _TestListViewWidget(),
+///     );
+///   }
+/// }
+///
+/// class _TestListViewWidget extends StatefulWidget {
+///   @override
+///   State<StatefulWidget> createState() {
+///     return _TestListViewWidgetState();
+///   }
+/// }
+///
+/// class _TestListViewWidgetState extends State<_TestListViewWidget> {
+///   ScrollController scrollController;
+///
+///   @override
+///   void initState() {
+///     super.initState();
+///     scrollController = ScrollController();
+///     Timer.run(() async {
+///       bool forward = true;
+///       while (true) {
+///         await scrollController.animateTo(
+///           forward ? 300 : 0,
+///           curve: Curves.linear,
+///           duration: const Duration(seconds: 1),
+///         );
+///         forward = !forward;
+///       }
+///     });
+///   }
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     return ListView.builder(
+///       controller: scrollController,
+///       itemCount: 10000,
+///       itemBuilder: (BuildContext context, int index) {
+///         return Text('Item #$index');
+///       },
+///     );
+///   }
+/// }
+/// ```
+abstract class WidgetRecorder extends Recorder implements FrameRecorder {
+  /// Creates a widget benchmark recorder.
+  ///
+  /// [name] must not be null.
+  ///
+  /// If [useCustomWarmUp] is true, delegates the benchmark warm-up to the
+  /// benchmark implementation instead of using a built-in strategy. The
+  /// benchmark is expected to call [Profile.stopWarmingUp] to signal that
+  /// the warm-up phase is finished.
+  WidgetRecorder({
+    @required String name,
+    this.useCustomWarmUp = false,
+  }) : super._(name, true);
+
+  /// Creates a widget to be benchmarked.
+  ///
+  /// The widget must create its own animation to drive the benchmark. The
+  /// animation should continue indefinitely. The benchmark harness will stop
+  /// pumping frames automatically.
+  Widget createWidget();
+
+  final List<VoidCallback> _didStopCallbacks = <VoidCallback>[];
+  @override
+  void registerDidStop(VoidCallback fn) {
+    _didStopCallbacks.add(fn);
+  }
+
+  @override
+  Profile profile;
+  Completer<void> _runCompleter;
+
+  /// Whether to delimit warm-up frames in a custom way.
+  final bool useCustomWarmUp;
+
+  Stopwatch _drawFrameStopwatch;
+
+  @override
+  @mustCallSuper
+  void frameWillDraw() {
+    startMeasureFrame(profile);
+    _drawFrameStopwatch = Stopwatch()..start();
+  }
+
+  @override
+  @mustCallSuper
+  void frameDidDraw() {
+    endMeasureFrame();
+    profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed,
+        reported: true);
+
+    if (shouldContinue()) {
+      window.scheduleFrame();
+    } else {
+      for (final VoidCallback fn in _didStopCallbacks) {
+        fn();
+      }
+      _runCompleter.complete();
+    }
+  }
+
+  @override
+  void _onError(dynamic error, StackTrace stackTrace) {
+    _runCompleter.completeError(error, stackTrace);
+  }
+
+  @override
+  Future<Profile> run() async {
+    _runCompleter = Completer<void>();
+    final Profile localProfile =
+        profile = Profile(name: name, useCustomWarmUp: useCustomWarmUp);
+    final _RecordingWidgetsBinding binding =
+        _RecordingWidgetsBinding.ensureInitialized();
+    final Widget widget = createWidget();
+
+    registerEngineBenchmarkValueListener(kProfilePrerollFrame, (num value) {
+      localProfile.addDataPoint(
+        kProfilePrerollFrame,
+        Duration(microseconds: value.toInt()),
+        reported: false,
+      );
+    });
+    registerEngineBenchmarkValueListener(kProfileApplyFrame, (num value) {
+      localProfile.addDataPoint(
+        kProfileApplyFrame,
+        Duration(microseconds: value.toInt()),
+        reported: false,
+      );
+    });
+
+    binding._beginRecording(this, widget);
+
+    try {
+      await _runCompleter.future;
+      return localProfile;
+    } finally {
+      stopListeningToEngineBenchmarkValues(kProfilePrerollFrame);
+      stopListeningToEngineBenchmarkValues(kProfileApplyFrame);
+      _runCompleter = null;
+      profile = null;
+    }
+  }
+}
+
+/// A recorder for measuring the performance of building a widget from scratch
+/// starting from an empty frame.
+///
+/// The recorder will call [createWidget] and render it, then it will pump
+/// another frame that clears the screen. It repeats this process, measuring the
+/// performance of frames that render the widget and ignoring the frames that
+/// clear the screen.
+abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
+  /// Creates a widget build benchmark recorder.
+  ///
+  /// [name] must not be null.
+  WidgetBuildRecorder({@required String name}) : super._(name, true);
+
+  /// Creates a widget to be benchmarked.
+  ///
+  /// The widget is not expected to animate as we only care about construction
+  /// of the widget. If you are interested in benchmarking an animation,
+  /// consider using [WidgetRecorder].
+  Widget createWidget();
+
+  final List<VoidCallback> _didStopCallbacks = <VoidCallback>[];
+  @override
+  void registerDidStop(VoidCallback fn) {
+    _didStopCallbacks.add(fn);
+  }
+
+  @override
+  Profile profile;
+  Completer<void> _runCompleter;
+
+  Stopwatch _drawFrameStopwatch;
+
+  /// Whether in this frame we should call [createWidget] and render it.
+  ///
+  /// If false, then this frame will clear the screen.
+  bool showWidget = true;
+
+  /// The state that hosts the widget under test.
+  _WidgetBuildRecorderHostState _hostState;
+
+  Widget _getWidgetForFrame() {
+    if (showWidget) {
+      return createWidget();
+    } else {
+      return null;
+    }
+  }
+
+  @override
+  @mustCallSuper
+  void frameWillDraw() {
+    if (showWidget) {
+      startMeasureFrame(profile);
+      _drawFrameStopwatch = Stopwatch()..start();
+    }
+  }
+
+  @override
+  @mustCallSuper
+  void frameDidDraw() {
+    // Only record frames that show the widget.
+    if (showWidget) {
+      endMeasureFrame();
+      profile.addDataPoint('drawFrameDuration', _drawFrameStopwatch.elapsed,
+          reported: true);
+    }
+
+    if (shouldContinue()) {
+      showWidget = !showWidget;
+      _hostState._setStateTrampoline();
+    } else {
+      for (final VoidCallback fn in _didStopCallbacks) {
+        fn();
+      }
+      _runCompleter.complete();
+    }
+  }
+
+  @override
+  void _onError(dynamic error, StackTrace stackTrace) {
+    _runCompleter.completeError(error, stackTrace);
+  }
+
+  @override
+  Future<Profile> run() async {
+    _runCompleter = Completer<void>();
+    final Profile localProfile = profile = Profile(name: name);
+    final _RecordingWidgetsBinding binding =
+        _RecordingWidgetsBinding.ensureInitialized();
+    binding._beginRecording(this, _WidgetBuildRecorderHost(this));
+
+    try {
+      await _runCompleter.future;
+      return localProfile;
+    } finally {
+      _runCompleter = null;
+      profile = null;
+    }
+  }
+}
+
+/// Hosts widgets created by [WidgetBuildRecorder].
+class _WidgetBuildRecorderHost extends StatefulWidget {
+  const _WidgetBuildRecorderHost(this.recorder);
+
+  final WidgetBuildRecorder recorder;
+
+  @override
+  State<StatefulWidget> createState() => _WidgetBuildRecorderHostState();
+}
+
+class _WidgetBuildRecorderHostState extends State<_WidgetBuildRecorderHost> {
+  @override
+  void initState() {
+    super.initState();
+    widget.recorder._hostState = this;
+  }
+
+  // This is just to bypass the @protected on setState.
+  void _setStateTrampoline() {
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox.expand(
+      child: widget.recorder._getWidgetForFrame(),
+    );
+  }
+}
+
+/// Series of time recordings indexed in time order.
+///
+/// It can calculate [average], [standardDeviation] and [noise]. If the amount
+/// of data collected is higher than [_kMeasuredSampleCount], then these
+/// calculations will only apply to the latest [_kMeasuredSampleCount] data
+/// points.
+class Timeseries {
+  /// Creates an empty timeseries.
+  ///
+  /// [name], [isReported], and [useCustomWarmUp] must not be null.
+  Timeseries(this.name, this.isReported, {this.useCustomWarmUp = false})
+      : _warmUpFrameCount = useCustomWarmUp ? 0 : null;
+
+  /// The label of this timeseries used for debugging and result inspection.
+  final String name;
+
+  /// Whether this timeseries is reported to the benchmark dashboard.
+  ///
+  /// If `true` a new benchmark card is created for the timeseries and is
+  /// visible on the dashboard.
+  ///
+  /// If `false` the data is stored but it does not show up on the dashboard.
+  /// Use unreported metrics for metrics that are useful for manual inspection
+  /// but that are too fine-grained to be useful for tracking on the dashboard.
+  final bool isReported;
+
+  /// Whether to delimit warm-up frames in a custom way.
+  final bool useCustomWarmUp;
+
+  /// The number of frames ignored as warm-up frames, used only
+  /// when [useCustomWarmUp] is true.
+  int _warmUpFrameCount;
+
+  /// The number of frames ignored as warm-up frames.
+  int get warmUpFrameCount =>
+      useCustomWarmUp ? _warmUpFrameCount : count - kMeasuredSampleCount;
+
+  /// List of all the values that have been recorded.
+  ///
+  /// This list has no limit.
+  final List<double> _allValues = <double>[];
+
+  /// The total amount of data collected, including ones that were dropped
+  /// because of the sample size limit.
+  int get count => _allValues.length;
+
+  /// Extracts useful statistics out of this timeseries.
+  ///
+  /// See [TimeseriesStats] for more details.
+  TimeseriesStats computeStats() {
+    final int finalWarmUpFrameCount = warmUpFrameCount;
+
+    assert(finalWarmUpFrameCount >= 0 && finalWarmUpFrameCount < count);
+
+    // The first few values we simply discard and never look at. They're from the warm-up phase.
+    final List<double> warmUpValues =
+        _allValues.sublist(0, finalWarmUpFrameCount);
+
+    // Values we analyze.
+    final List<double> candidateValues =
+        _allValues.sublist(finalWarmUpFrameCount);
+
+    // The average that includes outliers.
+    final double dirtyAverage = _computeAverage(name, candidateValues);
+
+    // The standard deviation that includes outliers.
+    final double dirtyStandardDeviation =
+        _computeStandardDeviationForPopulation(name, candidateValues);
+
+    // Any value that's higher than this is considered an outlier.
+    final double outlierCutOff = dirtyAverage + dirtyStandardDeviation;
+
+    // Candidates with outliers removed.
+    final Iterable<double> cleanValues =
+        candidateValues.where((double value) => value <= outlierCutOff);
+
+    // Outlier candidates.
+    final Iterable<double> outliers =
+        candidateValues.where((double value) => value > outlierCutOff);
+
+    // Final statistics.
+    final double cleanAverage = _computeAverage(name, cleanValues);
+    final double standardDeviation =
+        _computeStandardDeviationForPopulation(name, cleanValues);
+    final double noise =
+        cleanAverage > 0.0 ? standardDeviation / cleanAverage : 0.0;
+
+    // Compute outlier average. If there are no outliers the outlier average is
+    // the same as clean value average. In other words, in a perfect benchmark
+    // with no noise the difference between average and outlier average is zero,
+    // which the best possible outcome. Noise produces a positive difference
+    // between the two.
+    final double outlierAverage =
+        outliers.isNotEmpty ? _computeAverage(name, outliers) : cleanAverage;
+
+    final List<AnnotatedSample> annotatedValues = <AnnotatedSample>[
+      for (final double warmUpValue in warmUpValues)
+        AnnotatedSample(
+          magnitude: warmUpValue,
+          isOutlier: warmUpValue > outlierCutOff,
+          isWarmUpValue: true,
+        ),
+      for (final double candidate in candidateValues)
+        AnnotatedSample(
+          magnitude: candidate,
+          isOutlier: candidate > outlierCutOff,
+          isWarmUpValue: false,
+        ),
+    ];
+
+    return TimeseriesStats(
+      name: name,
+      average: cleanAverage,
+      outlierCutOff: outlierCutOff,
+      outlierAverage: outlierAverage,
+      standardDeviation: standardDeviation,
+      noise: noise,
+      cleanSampleCount: cleanValues.length,
+      outlierSampleCount: outliers.length,
+      samples: annotatedValues,
+    );
+  }
+
+  /// Adds a value to this timeseries.
+  void add(double value, {@required bool isWarmUpValue}) {
+    if (value < 0.0) {
+      throw StateError(
+        'Timeseries $name: negative metric values are not supported. Got: $value',
+      );
+    }
+    _allValues.add(value);
+    if (useCustomWarmUp && isWarmUpValue) {
+      _warmUpFrameCount += 1;
+    }
+  }
+}
+
+/// Various statistics about a [Timeseries].
+///
+/// See the docs on the individual fields for more details.
+@sealed
+class TimeseriesStats {
+  /// Creates statistics for a time series.
+  const TimeseriesStats({
+    @required this.name,
+    @required this.average,
+    @required this.outlierCutOff,
+    @required this.outlierAverage,
+    @required this.standardDeviation,
+    @required this.noise,
+    @required this.cleanSampleCount,
+    @required this.outlierSampleCount,
+    @required this.samples,
+  });
+
+  /// The label used to refer to the corresponding timeseries.
+  final String name;
+
+  /// The average value of the measured samples without outliers.
+  final double average;
+
+  /// The standard deviation in the measured samples without outliers.
+  final double standardDeviation;
+
+  /// The noise as a multiple of the [average] value takes from clean samples.
+  ///
+  /// This value can be multiplied by 100.0 to get noise as a percentage of
+  /// the average.
+  ///
+  /// If [average] is zero, treats the result as perfect score, returns zero.
+  final double noise;
+
+  /// The maximum value a sample can have without being considered an outlier.
+  ///
+  /// See [Timeseries.computeStats] for details on how this value is computed.
+  final double outlierCutOff;
+
+  /// The average of outlier samples.
+  ///
+  /// This value can be used to judge how badly we jank, when we jank.
+  ///
+  /// Another useful metrics is the difference between [outlierAverage] and
+  /// [average]. The smaller the value the more predictable is the performance
+  /// of the corresponding benchmark.
+  final double outlierAverage;
+
+  /// The number of measured samples after outlier are removed.
+  final int cleanSampleCount;
+
+  /// The number of outliers.
+  final int outlierSampleCount;
+
+  /// All collected samples, annotated with statistical information.
+  ///
+  /// See [AnnotatedSample] for more details.
+  final List<AnnotatedSample> samples;
+
+  /// Outlier average divided by clean average.
+  ///
+  /// This is a measure of performance consistency. The higher this number the
+  /// worse is jank when it happens. Smaller is better, with 1.0 being the
+  /// perfect score. If [average] is zero, this value defaults to 1.0.
+  double get outlierRatio => average > 0.0
+      ? outlierAverage / average
+      : 1.0; // this can only happen in perfect benchmark that reports only zeros
+
+  @override
+  String toString() {
+    final StringBuffer buffer = StringBuffer();
+    buffer.writeln(
+      '$name: (samples: $cleanSampleCount clean/$outlierSampleCount '
+      'outliers/${cleanSampleCount + outlierSampleCount} '
+      'measured/${samples.length} total)',
+    );
+    buffer.writeln(' | average: $average μs');
+    buffer.writeln(' | outlier average: $outlierAverage μs');
+    buffer.writeln(' | outlier/clean ratio: ${outlierRatio}x');
+    buffer.writeln(' | noise: ${_ratioToPercent(noise)}');
+    return buffer.toString();
+  }
+}
+
+/// Annotates a single measurement with statistical information.
+@sealed
+class AnnotatedSample {
+  /// Creates an annotated measurement sample.
+  const AnnotatedSample({
+    @required this.magnitude,
+    @required this.isOutlier,
+    @required this.isWarmUpValue,
+  });
+
+  /// The non-negative raw result of the measurement.
+  final double magnitude;
+
+  /// Whether this sample was considered an outlier.
+  final bool isOutlier;
+
+  /// Whether this sample was taken during the warm-up phase.
+  ///
+  /// If this value is `true`, this sample does not participate in
+  /// statistical computations. However, the sample would still be
+  /// shown in the visualization of results so that the benchmark
+  /// can be inspected manually to make sure there's a predictable
+  /// warm-up regression slope.
+  final bool isWarmUpValue;
+}
+
+/// Base class for a profile collected from running a benchmark.
+class Profile {
+  /// Creates an empty profile.
+  ///
+  /// [name] and [useCustomWarmUp] must not be null.
+  Profile({@required this.name, this.useCustomWarmUp = false})
+      : assert(name != null),
+        _isWarmingUp = useCustomWarmUp;
+
+  /// The name of the benchmark that produced this profile.
+  final String name;
+
+  /// Whether to delimit warm-up frames in a custom way.
+  final bool useCustomWarmUp;
+
+  /// Whether we are measuring warm-up frames currently.
+  bool get isWarmingUp => _isWarmingUp;
+
+  bool _isWarmingUp;
+
+  /// Stop the warm-up phase.
+  ///
+  /// Call this method only when [useCustomWarmUp] and [isWarmingUp] are both
+  /// true.
+  /// Call this method only once for each profile.
+  void stopWarmingUp() {
+    if (!useCustomWarmUp) {
+      throw Exception(
+          '`stopWarmingUp` should be used only when `useCustomWarmUp` is true.');
+    } else if (!_isWarmingUp) {
+      throw Exception('Warm-up already stopped.');
+    } else {
+      _isWarmingUp = false;
+    }
+  }
+
+  /// This data will be used to display cards in the Flutter Dashboard.
+  final Map<String, Timeseries> scoreData = <String, Timeseries>{};
+
+  /// This data isn't displayed anywhere. It's stored for completeness purposes.
+  final Map<String, dynamic> extraData = <String, dynamic>{};
+
+  /// Invokes [callback] and records the duration of its execution under [key].
+  Duration record(String key, VoidCallback callback,
+      {@required bool reported}) {
+    final Duration duration = timeAction(callback);
+    addDataPoint(key, duration, reported: reported);
+    return duration;
+  }
+
+  /// Adds a timed sample to the timeseries corresponding to [key].
+  ///
+  /// Set [reported] to `true` to report the timeseries to the dashboard UI.
+  ///
+  /// Set [reported] to `false` to store the data, but not show it on the
+  /// dashboard UI.
+  void addDataPoint(String key, Duration duration, {@required bool reported}) {
+    scoreData
+        .putIfAbsent(
+          key,
+          () => Timeseries(key, reported, useCustomWarmUp: useCustomWarmUp),
+        )
+        .add(duration.inMicroseconds.toDouble(), isWarmUpValue: isWarmingUp);
+  }
+
+  /// Decides whether the data collected so far is sufficient to stop, or
+  /// whether the benchmark should continue collecting more data.
+  ///
+  /// The signals used are sample size, noise, and duration.
+  ///
+  /// If any of the timeseries doesn't satisfy the noise requirements, this
+  /// method will return true (asking the benchmark to continue collecting
+  /// data).
+  bool shouldContinue() {
+    // If there are no `Timeseries` in the `scoreData`, then we haven't
+    // recorded anything yet. Don't stop.
+    if (scoreData.isEmpty) {
+      return true;
+    }
+
+    // We have recorded something, but do we have enough samples? If every
+    // timeseries has collected enough samples, stop the benchmark.
+    return !scoreData.keys
+        .every((String key) => scoreData[key].count >= kTotalSampleCount);
+  }
+
+  /// Returns a JSON representation of the profile that will be sent to the
+  /// server.
+  Map<String, dynamic> toJson() {
+    final List<String> scoreKeys = <String>[];
+    final Map<String, dynamic> json = <String, dynamic>{
+      'name': name,
+      'scoreKeys': scoreKeys,
+    };
+
+    for (final String key in scoreData.keys) {
+      final Timeseries timeseries = scoreData[key];
+
+      if (timeseries.isReported) {
+        scoreKeys.add('$key.average');
+        // Report `outlierRatio` rather than `outlierAverage`, because
+        // the absolute value of outliers is less interesting than the
+        // ratio.
+        scoreKeys.add('$key.outlierRatio');
+      }
+
+      final TimeseriesStats stats = timeseries.computeStats();
+      json['$key.average'] = stats.average;
+      json['$key.outlierAverage'] = stats.outlierAverage;
+      json['$key.outlierRatio'] = stats.outlierRatio;
+      json['$key.noise'] = stats.noise;
+    }
+
+    json.addAll(extraData);
+
+    return json;
+  }
+
+  @override
+  String toString() {
+    final StringBuffer buffer = StringBuffer();
+    buffer.writeln('name: $name');
+    for (final String key in scoreData.keys) {
+      final Timeseries timeseries = scoreData[key];
+      final TimeseriesStats stats = timeseries.computeStats();
+      buffer.writeln(stats.toString());
+    }
+    for (final String key in extraData.keys) {
+      final dynamic value = extraData[key];
+      if (value is List) {
+        buffer.writeln('$key:');
+        for (final dynamic item in value) {
+          buffer.writeln(' - $item');
+        }
+      } else {
+        buffer.writeln('$key: $value');
+      }
+    }
+    return buffer.toString();
+  }
+}
+
+/// Computes the arithmetic mean (or average) of given [values].
+double _computeAverage(String label, Iterable<double> values) {
+  if (values.isEmpty) {
+    throw StateError(
+        '$label: attempted to compute an average of an empty value list.');
+  }
+
+  final double sum = values.reduce((double a, double b) => a + b);
+  return sum / values.length;
+}
+
+/// Computes population standard deviation.
+///
+/// Unlike sample standard deviation, which divides by N - 1, this divides by N.
+///
+/// See also:
+///
+/// * https://en.wikipedia.org/wiki/Standard_deviation
+double _computeStandardDeviationForPopulation(
+    String label, Iterable<double> population) {
+  if (population.isEmpty) {
+    throw StateError(
+        '$label: attempted to compute the standard deviation of empty population.');
+  }
+  final double mean = _computeAverage(label, population);
+  final double sumOfSquaredDeltas = population.fold<double>(
+    0.0,
+    (double previous, double value) => previous += math.pow(value - mean, 2),
+  );
+  return math.sqrt(sumOfSquaredDeltas / population.length);
+}
+
+String _ratioToPercent(double value) {
+  return '${(value * 100).toStringAsFixed(2)}%';
+}
+
+/// Implemented by recorders that use [_RecordingWidgetsBinding] to receive
+/// frame life-cycle calls.
+abstract class FrameRecorder {
+  /// Add a callback that will be called by the recorder when it stops recording.
+  void registerDidStop(VoidCallback cb);
+
+  /// Called just before calling [SchedulerBinding.handleDrawFrame].
+  void frameWillDraw();
+
+  /// Called immediately after calling [SchedulerBinding.handleDrawFrame].
+  void frameDidDraw();
+
+  /// Reports an error.
+  ///
+  /// The implementation is expected to halt benchmark execution as soon as possible.
+  void _onError(dynamic error, StackTrace stackTrace);
+}
+
+/// A variant of [WidgetsBinding] that collaborates with a [Recorder] to decide
+/// when to stop pumping frames.
+///
+/// A normal [WidgetsBinding] typically always pumps frames whenever a widget
+/// instructs it to do so by calling [scheduleFrame] (transitively via
+/// `setState`). This binding will stop pumping new frames as soon as benchmark
+/// parameters are satisfactory (e.g. when the metric noise levels become low
+/// enough).
+class _RecordingWidgetsBinding extends BindingBase
+    with
+        GestureBinding,
+        SchedulerBinding,
+        ServicesBinding,
+        PaintingBinding,
+        SemanticsBinding,
+        RendererBinding,
+        WidgetsBinding {
+  /// Makes an instance of [_RecordingWidgetsBinding] the current binding.
+  static _RecordingWidgetsBinding ensureInitialized() {
+    if (WidgetsBinding.instance == null) {
+      _RecordingWidgetsBinding();
+    }
+    return WidgetsBinding.instance;
+  }
+
+  FrameRecorder _recorder;
+  bool _hasErrored = false;
+
+  /// To short-circuit all frame lifecycle methods when the benchmark has
+  /// stopped collecting data.
+  bool _benchmarkStopped = false;
+
+  void _beginRecording(FrameRecorder recorder, Widget widget) {
+    if (_recorder != null) {
+      throw Exception(
+        'Cannot call _RecordingWidgetsBinding._beginRecording more than once',
+      );
+    }
+    final FlutterExceptionHandler originalOnError = FlutterError.onError;
+
+    recorder.registerDidStop(() {
+      _benchmarkStopped = true;
+    });
+
+    // Fail hard and fast on errors. Benchmarks should not have any errors.
+    FlutterError.onError = (FlutterErrorDetails details) {
+      _haltBenchmarkWithError(details.exception, details.stack);
+      originalOnError(details);
+    };
+    _recorder = recorder;
+    runApp(widget);
+  }
+
+  void _haltBenchmarkWithError(dynamic error, StackTrace stackTrace) {
+    if (_hasErrored) {
+      return;
+    }
+    _recorder._onError(error, stackTrace);
+    _hasErrored = true;
+  }
+
+  @override
+  void handleBeginFrame(Duration rawTimeStamp) {
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
+      return;
+    }
+    try {
+      super.handleBeginFrame(rawTimeStamp);
+    } catch (error, stackTrace) {
+      _haltBenchmarkWithError(error, stackTrace);
+      rethrow;
+    }
+  }
+
+  @override
+  void scheduleFrame() {
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
+      return;
+    }
+    super.scheduleFrame();
+  }
+
+  @override
+  void handleDrawFrame() {
+    // Don't keep on truckin' if there's an error or the benchmark has stopped.
+    if (_hasErrored || _benchmarkStopped) {
+      return;
+    }
+    try {
+      _recorder.frameWillDraw();
+      super.handleDrawFrame();
+      _recorder.frameDidDraw();
+    } catch (error, stackTrace) {
+      _haltBenchmarkWithError(error, stackTrace);
+      rethrow;
+    }
+  }
+}
+
+int _currentFrameNumber = 1;
+
+/// If [_calledStartMeasureFrame] is true, we have called [startMeasureFrame]
+/// but have not its pairing [endMeasureFrame] yet.
+///
+/// This flag ensures that [startMeasureFrame] and [endMeasureFrame] are always
+/// called in pairs, with [startMeasureFrame] followed by [endMeasureFrame].
+bool _calledStartMeasureFrame = false;
+
+/// Whether we are recording a measured frame.
+///
+/// This flag ensures that we always stop measuring a frame if we
+/// have started one. Because we want to skip warm-up frames, this flag
+/// is necessary.
+bool _isMeasuringFrame = false;
+
+/// Adds a marker indication the beginning of frame rendering.
+///
+/// This adds an event to the performance trace used to find measured frames in
+/// Chrome tracing data. The tracing data contains all frames, but some
+/// benchmarks are only interested in a subset of frames. For example,
+/// [WidgetBuildRecorder] only measures frames that build widgets, and ignores
+/// frames that clear the screen.
+///
+/// Warm-up frames are not measured. If [profile.isWarmingUp] is true,
+/// this function does nothing.
+void startMeasureFrame(Profile profile) {
+  if (_calledStartMeasureFrame) {
+    throw Exception('`startMeasureFrame` called twice in a row.');
+  }
+
+  _calledStartMeasureFrame = true;
+
+  if (!profile.isWarmingUp) {
+    // Tell the browser to mark the beginning of the frame.
+    html.window.performance.mark('measured_frame_start#$_currentFrameNumber');
+
+    _isMeasuringFrame = true;
+  }
+}
+
+/// Signals the end of a measured frame.
+///
+/// See [startMeasureFrame] for details on what this instrumentation is used
+/// for.
+///
+/// Warm-up frames are not measured. If [profile.isWarmingUp] was true
+/// when the corresponding [startMeasureFrame] was called,
+/// this function does nothing.
+void endMeasureFrame() {
+  if (!_calledStartMeasureFrame) {
+    throw Exception(
+        '`startMeasureFrame` has not been called before calling `endMeasureFrame`');
+  }
+
+  _calledStartMeasureFrame = false;
+
+  if (_isMeasuringFrame) {
+    // Tell the browser to mark the end of the frame, and measure the duration.
+    html.window.performance.mark('measured_frame_end#$_currentFrameNumber');
+    html.window.performance.measure(
+      'measured_frame',
+      'measured_frame_start#$_currentFrameNumber',
+      'measured_frame_end#$_currentFrameNumber',
+    );
+
+    // Increment the current frame number.
+    _currentFrameNumber += 1;
+
+    _isMeasuringFrame = false;
+  }
+}
+
+/// A function that receives a benchmark value from the framework.
+typedef EngineBenchmarkValueListener = void Function(num value);
+
+// Maps from a value label name to a listener.
+final Map<String, EngineBenchmarkValueListener> _engineBenchmarkListeners =
+    <String, EngineBenchmarkValueListener>{};
+
+/// Registers a [listener] for engine benchmark values labeled by [name].
+///
+/// If another listener is already registered, overrides it.
+void registerEngineBenchmarkValueListener(
+    String name, EngineBenchmarkValueListener listener) {
+  if (listener == null) {
+    throw ArgumentError(
+      'Listener must not be null. To stop listening to engine benchmark values '
+      'under label "$name", call stopListeningToEngineBenchmarkValues(\'$name\').',
+    );
+  }
+
+  if (_engineBenchmarkListeners.containsKey(name)) {
+    throw StateError('A listener for "$name" is already registered.\n'
+        'Call `stopListeningToEngineBenchmarkValues` to unregister the previous '
+        'listener before registering a new one.');
+  }
+
+  if (_engineBenchmarkListeners.isEmpty) {
+    // The first listener is being registered. Register the global listener.
+    js_util.setProperty(html.window, '_flutter_internal_on_benchmark',
+        _dispatchEngineBenchmarkValue);
+  }
+
+  _engineBenchmarkListeners[name] = listener;
+}
+
+/// Stops listening to engine benchmark values under labeled by [name].
+void stopListeningToEngineBenchmarkValues(String name) {
+  _engineBenchmarkListeners.remove(name);
+  if (_engineBenchmarkListeners.isEmpty) {
+    // The last listener unregistered. Remove the global listener.
+    js_util.setProperty(html.window, '_flutter_internal_on_benchmark', null);
+  }
+}
+
+// Dispatches a benchmark value reported by the engine to the relevant listener.
+//
+// If there are no listeners registered for [name], ignores the value.
+void _dispatchEngineBenchmarkValue(String name, double value) {
+  final EngineBenchmarkValueListener listener = _engineBenchmarkListeners[name];
+  if (listener != null) {
+    listener(value);
+  }
+}
diff --git a/packages/web_benchmarks/lib/src/runner.dart b/packages/web_benchmarks/lib/src/runner.dart
new file mode 100644
index 0000000..278d6be
--- /dev/null
+++ b/packages/web_benchmarks/lib/src/runner.dart
@@ -0,0 +1,318 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json;
+import 'dart:io' as io;
+
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_static/shelf_static.dart';
+
+import 'benchmark_result.dart';
+import 'browser.dart';
+import 'common.dart';
+
+/// The default port number used by the local benchmark server.
+const int defaultBenchmarkServerPort = 9999;
+
+/// The default port number used for Chrome DevTool Protocol.
+const int defaultChromeDebugPort = 10000;
+
+/// Builds and serves a Flutter Web app, collects raw benchmark data and
+/// summarizes the result as a [BenchmarkResult].
+class BenchmarkServer {
+  /// Creates a benchmark server.
+  ///
+  /// [benchmarkAppDirectory] is the directory containing the app that's being
+  /// benchmarked. The app is expected to use `package:web_benchmarks/client.dart`
+  /// and call the `runBenchmarks` function to run the benchmarks.
+  ///
+  /// [entryPoint] is the path to the main app file that runs the benchmark. It
+  /// can be different (and typically is) from the production entry point of the
+  /// app.
+  ///
+  /// If [useCanvasKit] is true, builds the app in CanvasKit mode.
+  ///
+  /// [benchmarkServerPort] is the port this benchmark server serves the app on.
+  ///
+  /// [chromeDebugPort] is the port Chrome uses for DevTool Protocol used to
+  /// extract tracing data.
+  ///
+  /// If [headless] is true, runs Chrome without UI. In particular, this is
+  /// useful in environments (e.g. CI) that doesn't have a display.
+  BenchmarkServer({
+    @required this.benchmarkAppDirectory,
+    @required this.entryPoint,
+    @required this.useCanvasKit,
+    @required this.benchmarkServerPort,
+    @required this.chromeDebugPort,
+    @required this.headless,
+  });
+
+  final ProcessManager _processManager = const LocalProcessManager();
+
+  /// The directory containing the app that's being benchmarked.
+  ///
+  /// The app is expected to use `package:web_benchmarks/client.dart`
+  /// and call the `runBenchmarks` function to run the benchmarks.
+  final io.Directory benchmarkAppDirectory;
+
+  /// The path to the main app file that runs the benchmark.
+  ///
+  /// It can be different (and typically is) from the production entry point of
+  /// the app.
+  final String entryPoint;
+
+  /// Whether to build the app in CanvasKit mode.
+  final bool useCanvasKit;
+
+  /// The port this benchmark server serves the app on.
+  final int benchmarkServerPort;
+
+  /// The port Chrome uses for DevTool Protocol used to extract tracing data.
+  final int chromeDebugPort;
+
+  /// Whether to run Chrome without UI.
+  ///
+  /// This is useful in environments (e.g. CI) that doesn't have a display.
+  final bool headless;
+
+  /// Builds and serves the benchmark app, and collects benchmark results.
+  Future<BenchmarkResults> run() async {
+    // Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
+    Logger.root.level = Level.INFO;
+
+    if (!_processManager.canRun('flutter')) {
+      throw Exception(
+          'flutter executable is not runnable. Make sure it\'s in the PATH.');
+    }
+
+    final io.ProcessResult buildResult = await _processManager.run(
+      <String>[
+        'flutter',
+        'build',
+        'web',
+        '--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
+        if (useCanvasKit) '--dart-define=FLUTTER_WEB_USE_SKIA=true',
+        '--profile',
+        '-t',
+        entryPoint,
+      ],
+      workingDirectory: benchmarkAppDirectory.path,
+    );
+
+    if (buildResult.exitCode != 0) {
+      io.stderr.writeln(buildResult.stdout);
+      io.stderr.writeln(buildResult.stderr);
+      throw Exception('Failed to build the benchmark.');
+    }
+
+    final Completer<List<Map<String, dynamic>>> profileData =
+        Completer<List<Map<String, dynamic>>>();
+    final List<Map<String, dynamic>> collectedProfiles =
+        <Map<String, dynamic>>[];
+    List<String> benchmarks;
+    Iterator<String> benchmarkIterator;
+
+    // This future fixes a race condition between the web-page loading and
+    // asking to run a benchmark, and us connecting to Chrome's DevTools port.
+    // Sometime one wins. Other times, the other wins.
+    Future<Chrome> whenChromeIsReady;
+    Chrome chrome;
+    io.HttpServer server;
+    List<Map<String, dynamic>> latestPerformanceTrace;
+    Cascade cascade = Cascade();
+
+    // Serves the static files built for the app (html, js, images, fonts, etc)
+    cascade = cascade.add(createStaticHandler(
+      path.join(benchmarkAppDirectory.path, 'build', 'web'),
+      defaultDocument: 'index.html',
+    ));
+
+    // Serves the benchmark server API used by the benchmark app to coordinate
+    // the running of benchmarks.
+    cascade = cascade.add((Request request) async {
+      try {
+        chrome ??= await whenChromeIsReady;
+        if (request.requestedUri.path.endsWith('/profile-data')) {
+          final Map<String, dynamic> profile =
+              json.decode(await request.readAsString());
+          final String benchmarkName = profile['name'];
+          if (benchmarkName != benchmarkIterator.current) {
+            profileData.completeError(Exception(
+              'Browser returned benchmark results from a wrong benchmark.\n'
+              'Requested to run bechmark ${benchmarkIterator.current}, but '
+              'got results for $benchmarkName.',
+            ));
+            server.close();
+          }
+
+          // Trace data is null when the benchmark is not frame-based, such as RawRecorder.
+          if (latestPerformanceTrace != null) {
+            final BlinkTraceSummary traceSummary =
+                BlinkTraceSummary.fromJson(latestPerformanceTrace);
+            profile['totalUiFrame.average'] =
+                traceSummary.averageTotalUIFrameTime.inMicroseconds;
+            profile['scoreKeys'] ??=
+                <dynamic>[]; // using dynamic for consistency with JSON
+            profile['scoreKeys'].add('totalUiFrame.average');
+            latestPerformanceTrace = null;
+          }
+          collectedProfiles.add(profile);
+          return Response.ok('Profile received');
+        } else if (request.requestedUri.path
+            .endsWith('/start-performance-tracing')) {
+          latestPerformanceTrace = null;
+          await chrome.beginRecordingPerformance(
+              request.requestedUri.queryParameters['label']);
+          return Response.ok('Started performance tracing');
+        } else if (request.requestedUri.path
+            .endsWith('/stop-performance-tracing')) {
+          latestPerformanceTrace = await chrome.endRecordingPerformance();
+          return Response.ok('Stopped performance tracing');
+        } else if (request.requestedUri.path.endsWith('/on-error')) {
+          final Map<String, dynamic> errorDetails =
+              json.decode(await request.readAsString());
+          server.close();
+          // Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
+          final String errorMessage =
+              'Caught browser-side error: ${errorDetails['error']}\n${errorDetails['stackTrace']}';
+          if (!profileData.isCompleted) {
+            profileData.completeError(errorMessage);
+          } else {
+            io.stderr.writeln(errorMessage);
+          }
+          return Response.ok('');
+        } else if (request.requestedUri.path.endsWith('/next-benchmark')) {
+          if (benchmarks == null) {
+            benchmarks =
+                (json.decode(await request.readAsString())).cast<String>();
+            benchmarkIterator = benchmarks.iterator;
+          }
+          if (benchmarkIterator.moveNext()) {
+            final String nextBenchmark = benchmarkIterator.current;
+            print('Launching benchmark "$nextBenchmark"');
+            return Response.ok(nextBenchmark);
+          } else {
+            profileData.complete(collectedProfiles);
+            return Response.ok(kEndOfBenchmarks);
+          }
+        } else if (request.requestedUri.path.endsWith('/print-to-console')) {
+          // A passthrough used by
+          // `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
+          // to print information.
+          final String message = await request.readAsString();
+          print('[APP] $message');
+          return Response.ok('Reported.');
+        } else {
+          return Response.notFound(
+              'This request is not handled by the profile-data handler.');
+        }
+      } catch (error, stackTrace) {
+        if (!profileData.isCompleted) {
+          profileData.completeError(error, stackTrace);
+        } else {
+          io.stderr.writeln('Caught error: $error');
+          io.stderr.writeln('$stackTrace');
+        }
+        return Response.internalServerError(body: '$error');
+      }
+    });
+
+    // If all previous handlers returned HTTP 404, this is the last handler
+    // that simply warns about the unrecognized path.
+    cascade = cascade.add((Request request) {
+      io.stderr.writeln('Unrecognized URL path: ${request.requestedUri.path}');
+      return Response.notFound('Not found: ${request.requestedUri.path}');
+    });
+
+    server = await io.HttpServer.bind('localhost', benchmarkServerPort);
+    try {
+      shelf_io.serveRequests(server, cascade.handler);
+
+      final String dartToolDirectory =
+          path.join(benchmarkAppDirectory.path, '.dart_tool');
+      final String userDataDir = io.Directory(dartToolDirectory)
+          .createTempSync('chrome_user_data_')
+          .path;
+
+      final ChromeOptions options = ChromeOptions(
+        url: 'http://localhost:$benchmarkServerPort/index.html',
+        userDataDirectory: userDataDir,
+        windowHeight: 1024,
+        windowWidth: 1024,
+        headless: headless,
+        debugPort: chromeDebugPort,
+      );
+
+      print('Launching Chrome.');
+      whenChromeIsReady = Chrome.launch(
+        options,
+        onError: (String error) {
+          if (!profileData.isCompleted) {
+            profileData.completeError(Exception(error));
+          } else {
+            io.stderr.writeln('Chrome error: $error');
+          }
+        },
+        workingDirectory: benchmarkAppDirectory.path,
+      );
+
+      print('Waiting for the benchmark to report benchmark profile.');
+      final List<Map<String, dynamic>> profiles = await profileData.future;
+
+      print('Received profile data');
+      final Map<String, List<BenchmarkScore>> results =
+          <String, List<BenchmarkScore>>{};
+      for (final Map<String, dynamic> profile in profiles) {
+        final String benchmarkName = profile['name'];
+        if (benchmarkName.isEmpty) {
+          throw 'Benchmark name is empty';
+        }
+
+        final List<String> scoreKeys = List<String>.from(profile['scoreKeys']);
+        if (scoreKeys == null || scoreKeys.isEmpty) {
+          throw 'No score keys in benchmark "$benchmarkName"';
+        }
+        for (final String scoreKey in scoreKeys) {
+          if (scoreKey == null || scoreKey.isEmpty) {
+            throw 'Score key is empty in benchmark "$benchmarkName". '
+                'Received [${scoreKeys.join(', ')}]';
+          }
+        }
+
+        final List<BenchmarkScore> scores = <BenchmarkScore>[];
+        for (final String key in profile.keys) {
+          if (key == 'name' || key == 'scoreKeys') {
+            continue;
+          }
+          scores.add(BenchmarkScore(
+            metric: key,
+            value: profile[key],
+          ));
+        }
+        results[benchmarkName] = scores;
+      }
+      return BenchmarkResults(results);
+    } finally {
+      if (headless) {
+        chrome?.stop();
+      } else {
+        // In non-headless mode wait for the developer to close Chrome
+        // manually. Otherwise, they won't get a chance to debug anything.
+        print(
+          'Benchmark finished. Chrome running in windowed mode. Close '
+          'Chrome manually to continue.',
+        );
+        await chrome?.whenExits;
+      }
+      server?.close();
+    }
+  }
+}
diff --git a/packages/web_benchmarks/pubspec.yaml b/packages/web_benchmarks/pubspec.yaml
new file mode 100644
index 0000000..7397ed1
--- /dev/null
+++ b/packages/web_benchmarks/pubspec.yaml
@@ -0,0 +1,23 @@
+name: web_benchmarks
+description: A benchmark harness for performance-testing Flutter apps in Chrome.
+version: 0.0.5
+homepage: https://github.com/flutter/packages/tree/master/packages/web_benchmarks
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+  flutter: ">=1.17.0"
+
+# Using +2 upper limit on some packages to allow null-safe versions
+dependencies:
+  flutter:
+    sdk: flutter
+  flutter_test:
+    sdk: flutter
+  logging: ">=0.11.4 <2.0.0"
+  meta: ">=1.0.0 <2.0.0"
+  path: ">=1.7.0 <2.0.0"
+  process: ">=3.0.13 <5.0.0"
+  shelf: ">=0.7.5 <2.0.0"
+  shelf_static: ">=0.2.8 <2.0.0"
+  test: ">=1.15.0 <3.0.0"
+  webkit_inspection_protocol: ">=0.7.3 <2.0.0"
diff --git a/packages/web_benchmarks/testing/test_app/.gitignore b/packages/web_benchmarks/testing/test_app/.gitignore
new file mode 100644
index 0000000..9d532b1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/.gitignore
@@ -0,0 +1,41 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
diff --git a/packages/web_benchmarks/testing/test_app/.metadata b/packages/web_benchmarks/testing/test_app/.metadata
new file mode 100644
index 0000000..5e875f2
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: d26268bb9e6d713a73d6148da7fa75936d442741
+  channel: master
+
+project_type: app
diff --git a/packages/web_benchmarks/testing/test_app/README.md b/packages/web_benchmarks/testing/test_app/README.md
new file mode 100644
index 0000000..c3bb8e4
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/README.md
@@ -0,0 +1,3 @@
+# test_app
+
+An example app for web benchmarks testing.
diff --git a/packages/web_benchmarks/testing/test_app/analysis_options.yaml b/packages/web_benchmarks/testing/test_app/analysis_options.yaml
new file mode 100644
index 0000000..2597fd1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/analysis_options.yaml
@@ -0,0 +1,7 @@
+include: ../../../../analysis_options.yaml
+
+linter:
+  rules:
+    # This is test code. Do not enforce docs.
+    package_api_docs: false
+    public_member_api_docs: false
diff --git a/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart b/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart
new file mode 100644
index 0000000..5844dfb
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/aboutpage.dart
@@ -0,0 +1,29 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+const ValueKey<String> backKey = ValueKey<String>('backKey');
+
+class AboutPage extends StatelessWidget {
+  const AboutPage({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        leading: BackButton(
+          key: backKey,
+          onPressed: () => Navigator.of(context).pop(),
+        ),
+      ),
+      body: Center(
+        child: Text(
+          'This is a sample app.',
+          style: Theme.of(context).textTheme.headline3,
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart b/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart
new file mode 100644
index 0000000..13daba1
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/benchmarks/runner.dart
@@ -0,0 +1,99 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:web_benchmarks/client.dart';
+
+import '../aboutpage.dart' show backKey;
+import '../homepage.dart' show textKey, aboutPageKey;
+import '../main.dart';
+
+/// A recorder that measures frame building durations.
+abstract class AppRecorder extends WidgetRecorder {
+  AppRecorder({@required this.benchmarkName}) : super(name: benchmarkName);
+
+  final String benchmarkName;
+
+  Future<void> automate();
+
+  @override
+  Widget createWidget() {
+    Future<void>.delayed(const Duration(milliseconds: 400), automate);
+    return const MyApp();
+  }
+
+  Future<void> animationStops() async {
+    while (WidgetsBinding.instance.hasScheduledFrame) {
+      await Future<void>.delayed(const Duration(milliseconds: 200));
+    }
+  }
+}
+
+class ScrollRecorder extends AppRecorder {
+  ScrollRecorder() : super(benchmarkName: 'scroll');
+
+  @override
+  Future<void> automate() async {
+    final ScrollableState scrollable =
+        Scrollable.of(find.byKey(textKey).evaluate().single);
+    await scrollable.position.animateTo(
+      30000,
+      curve: Curves.linear,
+      duration: const Duration(seconds: 20),
+    );
+  }
+}
+
+class PageRecorder extends AppRecorder {
+  PageRecorder() : super(benchmarkName: 'page');
+
+  bool _completed = false;
+
+  @override
+  bool shouldContinue() => profile.shouldContinue() || !_completed;
+
+  @override
+  Future<void> automate() async {
+    final LiveWidgetController controller =
+        LiveWidgetController(WidgetsBinding.instance);
+    for (int i = 0; i < 10; ++i) {
+      print('Testing round $i...');
+      await controller.tap(find.byKey(aboutPageKey));
+      await animationStops();
+      await controller.tap(find.byKey(backKey));
+      await animationStops();
+    }
+    _completed = true;
+  }
+}
+
+class TapRecorder extends AppRecorder {
+  TapRecorder() : super(benchmarkName: 'tap');
+
+  bool _completed = false;
+
+  @override
+  bool shouldContinue() => profile.shouldContinue() || !_completed;
+
+  @override
+  Future<void> automate() async {
+    final LiveWidgetController controller =
+        LiveWidgetController(WidgetsBinding.instance);
+    for (int i = 0; i < 10; ++i) {
+      print('Testing round $i...');
+      await controller.tap(find.byIcon(Icons.add));
+      await animationStops();
+    }
+    _completed = true;
+  }
+}
+
+Future<void> main() async {
+  await runBenchmarks(<String, RecorderFactory>{
+    'scroll': () => ScrollRecorder(),
+    'page': () => PageRecorder(),
+    'tap': () => TapRecorder(),
+  });
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/homepage.dart b/packages/web_benchmarks/testing/test_app/lib/homepage.dart
new file mode 100644
index 0000000..7c7ac21
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/homepage.dart
@@ -0,0 +1,91 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+const ValueKey<String> textKey = ValueKey<String>('textKey');
+const ValueKey<String> aboutPageKey = ValueKey<String>('aboutPageKey');
+
+class HomePage extends StatefulWidget {
+  const HomePage({Key key, this.title}) : super(key: key);
+
+  final String title;
+
+  @override
+  _HomePageState createState() => _HomePageState();
+}
+
+class _HomePageState extends State<HomePage> {
+  int _counter = 0;
+
+  void _incrementCounter() {
+    setState(() {
+      _counter++;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(widget.title),
+        actions: <Widget>[
+          IconButton(
+            key: aboutPageKey,
+            icon: const Icon(Icons.alternate_email),
+            onPressed: () => Navigator.of(context).pushNamed('about'),
+          ),
+        ],
+      ),
+      body: Center(
+        child: ListView.builder(
+          itemExtent: 80,
+          itemBuilder: (BuildContext context, int index) {
+            if (index == 0) {
+              return Column(
+                key: textKey,
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: <Widget>[
+                  const Text('You have pushed the button this many times:'),
+                  Text(
+                    '$_counter',
+                    style: Theme.of(context).textTheme.headline4,
+                  ),
+                ],
+              );
+            } else {
+              return SizedBox(
+                height: 80,
+                child: Padding(
+                  padding: const EdgeInsets.all(12),
+                  child: Card(
+                    elevation: 8,
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      crossAxisAlignment: CrossAxisAlignment.center,
+                      children: <Widget>[
+                        Text(
+                          'Line $index',
+                          style: Theme.of(context).textTheme.headline5,
+                        ),
+                        Expanded(child: Container()),
+                        const Icon(Icons.camera),
+                        const Icon(Icons.face),
+                      ],
+                    ),
+                  ),
+                ),
+              );
+            }
+          },
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _incrementCounter,
+        tooltip: 'Increment',
+        child: const Icon(Icons.add),
+      ),
+    );
+  }
+}
diff --git a/packages/web_benchmarks/testing/test_app/lib/main.dart b/packages/web_benchmarks/testing/test_app/lib/main.dart
new file mode 100644
index 0000000..ad775c9
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/lib/main.dart
@@ -0,0 +1,32 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'aboutpage.dart';
+import 'homepage.dart';
+
+void main() {
+  runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+  const MyApp({Key key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+        visualDensity: VisualDensity.adaptivePlatformDensity,
+      ),
+      initialRoute: 'home',
+      routes: <String, WidgetBuilder>{
+        'home': (_) => const HomePage(title: 'Flutter Demo Home Page'),
+        'about': (_) => const AboutPage(),
+      },
+    );
+  }
+}
diff --git a/packages/web_benchmarks/testing/test_app/pubspec.yaml b/packages/web_benchmarks/testing/test_app/pubspec.yaml
new file mode 100644
index 0000000..4d9a9f6
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/pubspec.yaml
@@ -0,0 +1,23 @@
+name: test_app
+description: An example app for web benchmarks testing.
+
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+  cupertino_icons: ^0.1.3
+  flutter:
+    sdk: flutter
+  web_benchmarks:
+    path: ../../
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
diff --git a/packages/web_benchmarks/testing/test_app/web/favicon.png b/packages/web_benchmarks/testing/test_app/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/favicon.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png b/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png b/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/web_benchmarks/testing/test_app/web/index.html b/packages/web_benchmarks/testing/test_app/web/index.html
new file mode 100644
index 0000000..c52c352
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="A new Flutter project.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="test_app">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="icon" type="image/png" href="favicon.png"/>
+
+  <title>test_app</title>
+  <link rel="manifest" href="manifest.json">
+</head>
+<body>
+  <!-- This script installs service_worker.js to provide PWA functionality to
+       application. For more information, see:
+       https://developers.google.com/web/fundamentals/primers/service-workers -->
+  <script>
+    if ('serviceWorker' in navigator) {
+      window.addEventListener('load', function () {
+        navigator.serviceWorker.register('flutter_service_worker.js');
+      });
+    }
+  </script>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
diff --git a/packages/web_benchmarks/testing/test_app/web/manifest.json b/packages/web_benchmarks/testing/test_app/web/manifest.json
new file mode 100644
index 0000000..13a23690
--- /dev/null
+++ b/packages/web_benchmarks/testing/test_app/web/manifest.json
@@ -0,0 +1,23 @@
+{
+    "name": "test_app",
+    "short_name": "test_app",
+    "start_url": ".",
+    "display": "standalone",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "A new Flutter project.",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "icons/Icon-192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ]
+}
diff --git a/packages/web_benchmarks/testing/web_benchmarks_test.dart b/packages/web_benchmarks/testing/web_benchmarks_test.dart
new file mode 100644
index 0000000..2b88121
--- /dev/null
+++ b/packages/web_benchmarks/testing/web_benchmarks_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show JsonEncoder;
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+import 'package:web_benchmarks/server.dart';
+
+Future<void> main() async {
+  test('Can run a web benchmark', () async {
+    final BenchmarkResults taskResult = await serveWebBenchmark(
+      benchmarkAppDirectory: Directory('testing/test_app'),
+      entryPoint: 'lib/benchmarks/runner.dart',
+      useCanvasKit: false,
+    );
+
+    for (final String benchmarkName in <String>['scroll', 'page', 'tap']) {
+      for (final String metricName in <String>[
+        'preroll_frame',
+        'apply_frame',
+        'drawFrameDuration',
+      ]) {
+        for (final String valueName in <String>[
+          'average',
+          'outlierAverage',
+          'outlierRatio',
+          'noise',
+        ]) {
+          expect(
+            taskResult.scores[benchmarkName].where((BenchmarkScore score) =>
+                score.metric == '$metricName.$valueName'),
+            hasLength(1),
+          );
+        }
+      }
+      expect(
+        taskResult.scores[benchmarkName].where(
+            (BenchmarkScore score) => score.metric == 'totalUiFrame.average'),
+        hasLength(1),
+      );
+    }
+
+    expect(
+      const JsonEncoder.withIndent('  ').convert(taskResult.toJson()),
+      isA<String>(),
+    );
+  }, timeout: Timeout.none);
+}
diff --git a/packages/xdg_directories/.gitignore b/packages/xdg_directories/.gitignore
new file mode 100644
index 0000000..bb431f0
--- /dev/null
+++ b/packages/xdg_directories/.gitignore
@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/xdg_directories/.metadata b/packages/xdg_directories/.metadata
new file mode 100644
index 0000000..990be38
--- /dev/null
+++ b/packages/xdg_directories/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: d8c0deb1b6c116be79ceeca005f893be34ab5df2
+  channel: master
+
+project_type: package
diff --git a/packages/xdg_directories/CHANGELOG.md b/packages/xdg_directories/CHANGELOG.md
new file mode 100644
index 0000000..1a525f0
--- /dev/null
+++ b/packages/xdg_directories/CHANGELOG.md
@@ -0,0 +1,15 @@
+## 0.2.0
+
+* Migrated to null safety.
+
+## 0.1.2
+
+* Broaden dependencies to allow nullsafety version of process, meta, and path to be OK.
+
+## 0.1.1
+
+* Remove flutter, flutter_test from pubspec dependencies.
+
+## 0.1.0
+
+* Initial release includes all the features described in the README.md
diff --git a/packages/xdg_directories/LICENSE b/packages/xdg_directories/LICENSE
new file mode 100644
index 0000000..4be0666
--- /dev/null
+++ b/packages/xdg_directories/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/xdg_directories/README.md b/packages/xdg_directories/README.md
new file mode 100644
index 0000000..d7fa16a
--- /dev/null
+++ b/packages/xdg_directories/README.md
@@ -0,0 +1,50 @@
+# `xdg_directories`
+
+A Dart package for reading XDG directory configuration information on Linux.
+
+## Getting Started
+
+On Linux, `xdg` is a system developed by [freedesktop.org](freedesktop.org), a
+project to work on interoperability and shared base technology for free software
+desktop environments for Linux.
+
+This Dart package can be used to determine the directory configuration
+information defined by `xdg`, such as where the Documents or Desktop directories
+are. These are called "user directories" and are defined in configuration file
+in the user's home directory.
+
+See [this wiki](https://wiki.archlinux.org/index.php/XDG_Base_Directory) for
+more details of the XDG Base Directory implementation.
+
+To use this package, the basic XDG values for the following are available via a Dart API:
+
+ - `dataHome` - The single base directory relative to which user-specific data
+   files should be written. (Corresponds to `$XDG_DATA_HOME`).
+
+ - `configHome` - The a single base directory relative to which user-specific
+   configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`).
+
+ - `dataDirs` - The list of preference-ordered base directories relative to
+   which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`).
+
+ - `configDirs` - The list of preference-ordered base directories relative to
+   which configuration files should be searched. (Corresponds to
+   `$XDG_CONFIG_DIRS`).
+
+ - `cacheHome` - The base directory relative to which user-specific
+   non-essential (cached) data should be written. (Corresponds to
+   `$XDG_CACHE_HOME`).
+
+ - `runtimeDir` - The base directory relative to which user-specific runtime
+   files and other file objects should be placed. (Corresponds to
+   `$XDG_RUNTIME_DIR`).
+
+ - `getUserDirectoryNames()` - Returns a set of the names of user directories
+   defined in the `xdg` configuration files.
+
+ - `getUserDirectory(String dirName)` - Gets the value of the user dir with the
+   given name. Requesting a user dir that doesn't exist returns `null`. The
+   `dirName` argument is case-insensitive. See [this
+   wiki](https://wiki.archlinux.org/index.php/XDG_user_directories) for more
+   details and what values of `dirName` might be available.
+
diff --git a/packages/xdg_directories/lib/xdg_directories.dart b/packages/xdg_directories/lib/xdg_directories.dart
new file mode 100644
index 0000000..fc5d515
--- /dev/null
+++ b/packages/xdg_directories/lib/xdg_directories.dart
@@ -0,0 +1,188 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library xdg_directories;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+
+/// An override function used by the tests to override the environment variable
+/// lookups using [xdgEnvironmentOverride].
+typedef EnvironmentAccessor = String? Function(String envVar);
+
+/// A testing setter that replaces the real environment lookups with an override.
+///
+/// Set to null to stop overriding.
+///
+/// Only available to tests.
+@visibleForTesting
+set xdgEnvironmentOverride(EnvironmentAccessor? override) {
+  _xdgEnvironmentOverride = override;
+  _getenv = _xdgEnvironmentOverride ?? _productionGetEnv;
+}
+
+/// A testing getter that returns the current value of the override that
+/// replaces the real environment lookups with an override.
+///
+/// Only available to tests.
+@visibleForTesting
+EnvironmentAccessor? get xdgEnvironmentOverride => _xdgEnvironmentOverride;
+EnvironmentAccessor? _xdgEnvironmentOverride;
+EnvironmentAccessor _getenv = _productionGetEnv;
+String? _productionGetEnv(String value) => Platform.environment[value];
+
+/// A testing function that replaces the process manager used to run xdg-user-path
+/// with the one supplied.
+///
+/// Only available to tests.
+@visibleForTesting
+set xdgProcessManager(ProcessManager processManager) {
+  _processManager = processManager;
+}
+
+ProcessManager _processManager = const LocalProcessManager();
+
+List<Directory> _directoryListFromEnvironment(
+    String envVar, List<Directory> fallback) {
+  ArgumentError.checkNotNull(envVar);
+  ArgumentError.checkNotNull(fallback);
+  final String? value = _getenv(envVar);
+  if (value == null || value.isEmpty) {
+    return fallback;
+  }
+  return value.split(':').where((String value) {
+    return value.isNotEmpty;
+  }).map<Directory>((String entry) {
+    return Directory(entry);
+  }).toList();
+}
+
+Directory? _directoryFromEnvironment(String envVar) {
+  ArgumentError.checkNotNull(envVar);
+  final String? value = _getenv(envVar);
+  if (value == null || value.isEmpty) {
+    return null;
+  }
+  return Directory(value);
+}
+
+Directory _directoryFromEnvironmentWithFallback(
+    String envVar, String fallback) {
+  ArgumentError.checkNotNull(envVar);
+  final String? value = _getenv(envVar);
+  if (value == null || value.isEmpty) {
+    return _getDirectory(fallback);
+  }
+  return Directory(value);
+}
+
+// Creates a Directory from a fallback path.
+Directory _getDirectory(String subdir) {
+  ArgumentError.checkNotNull(subdir);
+  assert(subdir.isNotEmpty);
+  final String? homeDir = _getenv('HOME');
+  if (homeDir == null || homeDir.isEmpty) {
+    throw StateError(
+        'The "HOME" environment variable is not set. This package (and POSIX) '
+        'requires that HOME be set.');
+  }
+  return Directory(path.joinAll(<String>[homeDir, subdir]));
+}
+
+/// The base directory relative to which user-specific
+/// non-essential (cached) data should be written. (Corresponds to
+/// `$XDG_CACHE_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get cacheHome =>
+    _directoryFromEnvironmentWithFallback('XDG_CACHE_HOME', '.cache');
+
+/// The list of preference-ordered base directories relative to
+/// which configuration files should be searched. (Corresponds to
+/// `$XDG_CONFIG_DIRS`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+List<Directory> get configDirs {
+  return _directoryListFromEnvironment(
+    'XDG_CONFIG_DIRS',
+    <Directory>[Directory('/etc/xdg')],
+  );
+}
+
+/// The a single base directory relative to which user-specific
+/// configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get configHome =>
+    _directoryFromEnvironmentWithFallback('XDG_CONFIG_HOME', '.config');
+
+/// The list of preference-ordered base directories relative to
+/// which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+List<Directory> get dataDirs {
+  return _directoryListFromEnvironment(
+    'XDG_DATA_DIRS',
+    <Directory>[Directory('/usr/local/share'), Directory('/usr/share')],
+  );
+}
+
+/// The base directory relative to which user-specific data files should be
+/// written. (Corresponds to `$XDG_DATA_HOME`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory get dataHome =>
+    _directoryFromEnvironmentWithFallback('XDG_DATA_HOME', '.local/share');
+
+/// The base directory relative to which user-specific runtime
+/// files and other file objects should be placed. (Corresponds to
+/// `$XDG_RUNTIME_DIR`).
+///
+/// Throws [StateError] if the HOME environment variable is not set.
+Directory? get runtimeDir => _directoryFromEnvironment('XDG_RUNTIME_DIR');
+
+/// Gets the xdg user directory named by `dirName`.
+///
+/// Use [getUserDirectoryNames] to find out the list of available names.
+Directory? getUserDirectory(String dirName) {
+  final ProcessResult result = _processManager.runSync(
+    <String>['xdg-user-dir', dirName],
+    includeParentEnvironment: true,
+    stdoutEncoding: utf8,
+  );
+  final String path = (result.stdout as String).split('\n')[0];
+  return Directory(path);
+}
+
+/// Gets the set of user directory names that xdg knows about.
+///
+/// These are not paths, they are names of xdg values.  Call [getUserDirectory]
+/// to get the associated directory.
+///
+/// These are the names of the variables in "[configHome]/user-dirs.dirs", with
+/// the `XDG_` prefix removed and the `_DIR` suffix removed.
+Set<String> getUserDirectoryNames() {
+  final File configFile = File(path.join(configHome.path, 'user-dirs.dirs'));
+  List<String> contents;
+  try {
+    contents = configFile.readAsLinesSync();
+  } on FileSystemException {
+    return const <String>{};
+  }
+  final Set<String> result = <String>{};
+  final RegExp dirRegExp =
+      RegExp(r'^\s*XDG_(?<dirname>[^=]*)_DIR\s*=\s*(?<dir>.*)\s*$');
+  for (final String line in contents) {
+    final RegExpMatch? match = dirRegExp.firstMatch(line);
+    if (match == null) {
+      continue;
+    }
+    result.add(match.namedGroup('dirname')!);
+  }
+  return result;
+}
diff --git a/packages/xdg_directories/pubspec.yaml b/packages/xdg_directories/pubspec.yaml
new file mode 100644
index 0000000..197813e
--- /dev/null
+++ b/packages/xdg_directories/pubspec.yaml
@@ -0,0 +1,15 @@
+name: xdg_directories
+description: A Dart package for reading XDG directory configuration information on Linux.
+version: 0.2.0
+homepage: https://github.com/flutter/packages/tree/master/packages/xdg_directories
+
+environment:
+  sdk: ">=2.12.0-0 <3.0.0"
+
+dependencies:
+  meta: ^1.3.0
+  path: ^1.8.0
+  process: ^4.0.0
+
+dev_dependencies:
+  test: ^1.16.0
diff --git a/packages/xdg_directories/test/xdg_directories_test.dart b/packages/xdg_directories/test/xdg_directories_test.dart
new file mode 100644
index 0000000..9bf5c0f
--- /dev/null
+++ b/packages/xdg_directories/test/xdg_directories_test.dart
@@ -0,0 +1,133 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test/fake.dart';
+import 'package:test/test.dart';
+import 'package:path/path.dart' as path;
+import 'package:process/process.dart';
+
+import 'package:xdg_directories/xdg_directories.dart' as xdg;
+
+void main() {
+  final Map<String, String> fakeEnv = <String, String>{};
+  late Directory tmpDir;
+
+  String testPath(String subdir) => path.join(tmpDir.path, subdir);
+
+  setUp(() {
+    tmpDir = Directory.systemTemp.createTempSync('xdg_test');
+    fakeEnv.clear();
+    fakeEnv['HOME'] = tmpDir.path;
+    fakeEnv['XDG_CACHE_HOME'] = testPath('.test_cache');
+    fakeEnv['XDG_CONFIG_DIRS'] = testPath('etc/test_xdg');
+    fakeEnv['XDG_CONFIG_HOME'] = testPath('.test_config');
+    fakeEnv['XDG_DATA_DIRS'] =
+        '${testPath('usr/local/test_share')}:${testPath('usr/test_share')}';
+    fakeEnv['XDG_DATA_HOME'] = testPath('.local/test_share');
+    fakeEnv['XDG_RUNTIME_DIR'] = testPath('.local/test_runtime');
+    Directory(fakeEnv['XDG_CONFIG_HOME']!).createSync(recursive: true);
+    Directory(fakeEnv['XDG_CACHE_HOME']!).createSync(recursive: true);
+    Directory(fakeEnv['XDG_DATA_HOME']!).createSync(recursive: true);
+    Directory(fakeEnv['XDG_RUNTIME_DIR']!).createSync(recursive: true);
+    File(path.join(fakeEnv['XDG_CONFIG_HOME']!, 'user-dirs.dirs'))
+        .writeAsStringSync(r'''
+XDG_DESKTOP_DIR="$HOME/Desktop"
+XDG_DOCUMENTS_DIR="$HOME/Documents"
+XDG_DOWNLOAD_DIR="$HOME/Downloads"
+XDG_MUSIC_DIR="$HOME/Music"
+XDG_PICTURES_DIR="$HOME/Pictures"
+XDG_PUBLICSHARE_DIR="$HOME/Public"
+XDG_TEMPLATES_DIR="$HOME/Templates"
+XDG_VIDEOS_DIR="$HOME/Videos"
+''');
+    xdg.xdgEnvironmentOverride = (String key) => fakeEnv[key];
+  });
+
+  tearDown(() {
+    tmpDir.deleteSync(recursive: true);
+    // Stop overriding the environment accessor.
+    xdg.xdgEnvironmentOverride = null;
+  });
+  void expectDirList(List<Directory> values, List<String> expected) {
+    final List<String> valueStr =
+        values.map<String>((Directory directory) => directory.path).toList();
+    expect(valueStr, orderedEquals(expected));
+  }
+
+  test('Default fallback values work', () {
+    fakeEnv.clear();
+    fakeEnv['HOME'] = tmpDir.path;
+    expect(xdg.cacheHome.path, equals(testPath('.cache')));
+    expect(xdg.configHome.path, equals(testPath('.config')));
+    expect(xdg.dataHome.path, equals(testPath('.local/share')));
+    expect(xdg.runtimeDir, isNull);
+
+    expectDirList(xdg.configDirs, <String>['/etc/xdg']);
+    expectDirList(xdg.dataDirs, <String>['/usr/local/share', '/usr/share']);
+  });
+
+  test('Values pull from environment', () {
+    expect(xdg.cacheHome.path, equals(testPath('.test_cache')));
+    expect(xdg.configHome.path, equals(testPath('.test_config')));
+    expect(xdg.dataHome.path, equals(testPath('.local/test_share')));
+    expect(xdg.runtimeDir, isNotNull);
+    expect(xdg.runtimeDir!.path, equals(testPath('.local/test_runtime')));
+
+    expectDirList(xdg.configDirs, <String>[testPath('etc/test_xdg')]);
+    expectDirList(xdg.dataDirs, <String>[
+      testPath('usr/local/test_share'),
+      testPath('usr/test_share'),
+    ]);
+  });
+
+  test('Can get userDirs', () {
+    final Map<String, String> expected = <String, String>{
+      'DESKTOP': testPath('Desktop'),
+      'DOCUMENTS': testPath('Documents'),
+      'DOWNLOAD': testPath('Downloads'),
+      'MUSIC': testPath('Music'),
+      'PICTURES': testPath('Pictures'),
+      'PUBLICSHARE': testPath('Public'),
+      'TEMPLATES': testPath('Templates'),
+      'VIDEOS': testPath('Videos'),
+    };
+    xdg.xdgProcessManager = FakeProcessManager(expected);
+    final Set<String> userDirs = xdg.getUserDirectoryNames();
+    expect(userDirs, equals(expected.keys.toSet()));
+    for (final String key in userDirs) {
+      expect(xdg.getUserDirectory(key)!.path, equals(expected[key]),
+          reason: 'Path $key value not correct');
+    }
+    xdg.xdgProcessManager = const LocalProcessManager();
+  });
+
+  test('Throws StateError when HOME not set', () {
+    fakeEnv.clear();
+    expect(() {
+      xdg.configHome;
+    }, throwsStateError);
+  });
+}
+
+class FakeProcessManager extends Fake implements ProcessManager {
+  FakeProcessManager(this.expected);
+
+  Map<String, String> expected;
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    return ProcessResult(0, 0, expected[command[1]]!, '');
+  }
+}
diff --git a/script/check_publish.sh b/script/check_publish.sh
new file mode 100755
index 0000000..40e0dcc
--- /dev/null
+++ b/script/check_publish.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -e
+
+# This script checks to make sure that each of the plugins *could* be published.
+# It doesn't actually publish anything.
+
+# So that users can run this script from anywhere and it will work as expected.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+
+source "$SCRIPT_DIR/common.sh"
+
+function check_publish() {
+  local failures=()
+  for package_name in "$@"; do
+    local dir="$REPO_DIR/packages/$package_name"
+    echo "Checking that $package_name can be published."
+    if (cd "$dir" && flutter pub publish --dry-run > /dev/null); then
+      echo "Package $package_name is able to be published."
+    else
+      error "Unable to publish $package_name"
+      failures=("${failures[@]}" "$package_name")
+    fi
+  done
+  if [[ "${#failures[@]}" != 0 ]]; then
+    error "WARNING: The following ${#failures[@]} package(s) failed the publishing check:"
+    for failure in "${failures[@]}"; do
+      error "$failure"
+    done
+  fi
+  return 0
+}
+
+# Sets CHANGED_PACKAGE_LIST
+check_changed_packages
+
+if [[ "${#CHANGED_PACKAGE_LIST[@]}" != 0 ]]; then
+  check_publish "${CHANGED_PACKAGE_LIST[@]}"
+fi
diff --git a/script/common.sh b/script/common.sh
new file mode 100644
index 0000000..eaf94c7
--- /dev/null
+++ b/script/common.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+function error() {
+  echo "$@" 1>&2
+}
+
+function check_changed_packages() {
+  # Try get a merge base for the branch and calculate affected packages.
+  # We need this check because some CIs can do a single branch clones with a limited history of commits.
+  local packages
+  local branch_base_sha="$(git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD)"
+  if [[ "$?" == 0 ]]; then
+    echo "Checking for changed packages from $branch_base_sha"
+    IFS=$'\n' packages=( $(git diff --name-only "$branch_base_sha" HEAD | grep -o "packages/[^/]*" | sed -e "s/packages\///g" | sort | uniq) )
+  else
+    error "Cannot find a merge base for the current branch to run an incremental build..."
+    error "Please rebase your branch onto the latest master!"
+    return 1
+  fi
+
+  # Filter out any packages that don't have a pubspec.yaml: they have probably
+  # been deleted in this PR.
+  CHANGED_PACKAGES=""
+  CHANGED_PACKAGE_LIST=()
+  for package in "${packages[@]}"; do
+    if [[ -f "$REPO_DIR/packages/$package/pubspec.yaml" ]]; then
+      CHANGED_PACKAGES="${CHANGED_PACKAGES},$package"
+      CHANGED_PACKAGE_LIST=("${CHANGED_PACKAGE_LIST[@]}" "$package")
+    fi
+  done
+
+  if [[ "${#CHANGED_PACKAGE_LIST[@]}" == 0 ]]; then
+    echo "No changes detected in packages."
+  else
+    echo "Detected changes in the following ${#CHANGED_PACKAGE_LIST[@]} package(s):"
+    for package in "${CHANGED_PACKAGE_LIST[@]}"; do
+      echo "$package"
+    done
+    echo ""
+  fi
+  return 0
+}
diff --git a/script/incremental_build.sh b/script/incremental_build.sh
new file mode 100755
index 0000000..283c9a5
--- /dev/null
+++ b/script/incremental_build.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+
+source "$SCRIPT_DIR/common.sh"
+
+# Set some default actions if run without arguments.
+ACTIONS=("$@")
+if [[ "${#ACTIONS[@]}" == 0 ]]; then
+  ACTIONS=("test" "analyze" "java-test")
+fi
+
+BRANCH_NAME="${BRANCH_NAME:-"$(git rev-parse --abbrev-ref HEAD)"}"
+if [[ "${BRANCH_NAME}" == "master" ]]; then
+  echo "Running for all packages"
+  (cd "$REPO_DIR" && pub global run flutter_plugin_tools "${ACTIONS[@]}" $BUILD_SHARDING)
+else
+  # Sets CHANGED_PACKAGES
+  check_changed_packages
+
+  if [[ "$CHANGED_PACKAGES" == "" ]]; then
+    echo "Running for all packages"
+    (cd "$REPO_DIR" && pub global run flutter_plugin_tools "${ACTIONS[@]}" $BUILD_SHARDING)
+  else
+    (cd "$REPO_DIR" && pub global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" $BUILD_SHARDING)
+  fi
+fi
diff --git a/script/install_chromium.sh b/script/install_chromium.sh
new file mode 100755
index 0000000..0ad8682
--- /dev/null
+++ b/script/install_chromium.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+set -e
+set -x
+
+# The build of Chromium used to test web functionality.
+#
+# Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
+CHROMIUM_BUILD=768968
+
+mkdir .chromium
+wget "https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2Fchrome-linux.zip?alt=media" -O .chromium/chromium.zip
+unzip .chromium/chromium.zip -d .chromium/
+export CHROME_EXECUTABLE=$(pwd)/.chromium/chrome-linux/chrome
+echo $CHROME_EXECUTABLE
+$CHROME_EXECUTABLE --version
diff --git a/script/local_tests.sh b/script/local_tests.sh
new file mode 100755
index 0000000..ca12e4d
--- /dev/null
+++ b/script/local_tests.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
+REPO_DIR="$(dirname "$SCRIPT_DIR")"
+TEST_SCRIPT_NAME="run_tests.sh"
+
+source "$SCRIPT_DIR/common.sh"
+
+check_changed_packages
+
+for PACKAGE in $CHANGED_PACKAGE_LIST; do
+  PACKAGE_PATH=./packages/$PACKAGE
+  TEST_SCRIPT=$PACKAGE_PATH/$TEST_SCRIPT_NAME
+  if [ -e $TEST_SCRIPT ]; then
+    pushd $PWD
+    cd $PACKAGE_PATH
+    ls
+    ./$TEST_SCRIPT_NAME
+    popd
+  fi
+done
diff --git a/third_party/packages/bsdiff/CHANGELOG.md b/third_party/packages/bsdiff/CHANGELOG.md
new file mode 100644
index 0000000..5703fc8
--- /dev/null
+++ b/third_party/packages/bsdiff/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+* Initial Open Source release.
diff --git a/third_party/packages/bsdiff/LICENSE b/third_party/packages/bsdiff/LICENSE
new file mode 100644
index 0000000..d04a529
--- /dev/null
+++ b/third_party/packages/bsdiff/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2003-2005 Colin Percival. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/packages/bsdiff/README.md b/third_party/packages/bsdiff/README.md
new file mode 100644
index 0000000..18752b4
--- /dev/null
+++ b/third_party/packages/bsdiff/README.md
@@ -0,0 +1,29 @@
+# BSDiff
+
+[![pub package](https://img.shields.io/pub/v/bsdiff.svg)](
+https://pub.dartlang.org/packages/bsdiff)
+
+Binary diff/patch algorithm based on
+[bsdiff 4.3](http://www.daemonology.net/bsdiff/) by Colin Percival.
+It's very effective at compressesing incremental changes in binaries.
+
+This implementation has the following differences from Colin's code,
+to make it easier to read and apply patches in Java and Objective C:
+
+* Using gzip instead of bzip2 because gzip is included in JDK by default
+* Using big- instead of little-endian serialization to simplify Java code
+* Using two's complement instead of high-bit coding for negatives numbers
+
+## Usage
+To use this package, add `bsdiff` as a
+[dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/).
+
+## Example
+
+Import the library via
+``` dart
+import 'package:bsdiff/bsdiff.dart';
+```
+
+Then use the `bsdiff` and `bspatch` Dart functions in your code. To see how this is done,
+check out the [example app](example/main.dart).
diff --git a/third_party/packages/bsdiff/example/main.dart b/third_party/packages/bsdiff/example/main.dart
new file mode 100644
index 0000000..5e2eca7
--- /dev/null
+++ b/third_party/packages/bsdiff/example/main.dart
@@ -0,0 +1,25 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Example script to illustrate how to use the bsdiff package to generate and apply patches.
+
+import 'dart:typed_data';
+
+import 'package:bsdiff/bsdiff.dart';
+
+void main() {
+  final Uint8List originalData =
+      Uint8List.fromList(List<int>.generate(1000, (int index) => index));
+  final Uint8List modifiedData =
+      Uint8List.fromList(List<int>.generate(2000, (int index) => 2 * index));
+
+  print('Original data size ${originalData.length} bytes');
+  print('Modified data size ${modifiedData.length} bytes');
+
+  final Uint8List generatedPatch = bsdiff(originalData, modifiedData);
+  final Uint8List restoredData = bspatch(originalData, generatedPatch);
+
+  print('Generated patch is ${generatedPatch.length} bytes');
+  print('Restored data size ${restoredData.length} bytes');
+}
diff --git a/third_party/packages/bsdiff/example/pubspec.yaml b/third_party/packages/bsdiff/example/pubspec.yaml
new file mode 100644
index 0000000..d5c9099
--- /dev/null
+++ b/third_party/packages/bsdiff/example/pubspec.yaml
@@ -0,0 +1,10 @@
+name: main
+description: A simple example of how to use bsdiff to generate and apply patches.
+version: 0.1.0
+
+dependencies:
+  bsdiff:
+    path: ..
+
+environment:
+  sdk: ">=2.1.1-dev.2.0 <3.0.0"
diff --git a/third_party/packages/bsdiff/lib/bsdiff.dart b/third_party/packages/bsdiff/lib/bsdiff.dart
new file mode 100644
index 0000000..7af6acc
--- /dev/null
+++ b/third_party/packages/bsdiff/lib/bsdiff.dart
@@ -0,0 +1,412 @@
+// Copyright 2003-2005 Colin Percival. 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:io';
+import 'dart:math';
+import 'dart:typed_data';
+
+void _split(List<int> idata, List<int> vdata, int start, int len, int h) {
+  if (len < 16) {
+    for (int j, k = start; k < start + len; k += j) {
+      j = 1;
+      int x = vdata[idata[k] + h];
+      for (int i = 1; k + i < start + len; i++) {
+        if (vdata[idata[k + i] + h] < x) {
+          x = vdata[idata[k + i] + h];
+          j = 0;
+        }
+        if (vdata[idata[k + i] + h] == x) {
+          final int tmp = idata[k + j];
+          idata[k + j] = idata[k + i];
+          idata[k + i] = tmp;
+          j++;
+        }
+      }
+      for (int i = 0; i < j; i++) {
+        vdata[idata[k + i]] = k + j - 1;
+      }
+      if (j == 1) {
+        idata[k] = -1;
+      }
+    }
+    return;
+  }
+
+  final int x = vdata[idata[start + len ~/ 2] + h];
+  int jj = 0;
+  int kk = 0;
+  for (int i = start; i < start + len; i++) {
+    if (vdata[idata[i] + h] < x) {
+      jj++;
+    }
+    if (vdata[idata[i] + h] == x) {
+      kk++;
+    }
+  }
+  jj += start;
+  kk += jj;
+
+  int i = start;
+  int j = 0;
+  int k = 0;
+  while (i < jj) {
+    if (vdata[idata[i] + h] < x) {
+      i++;
+    } else if (vdata[idata[i] + h] == x) {
+      final int tmp = idata[i];
+      idata[i] = idata[jj + j];
+      idata[jj + j] = tmp;
+      j++;
+    } else {
+      final int tmp = idata[i];
+      idata[i] = idata[kk + k];
+      idata[kk + k] = tmp;
+      k++;
+    }
+  }
+
+  while (jj + j < kk) {
+    if (vdata[idata[jj + j] + h] == x) {
+      j++;
+    } else {
+      final int tmp = idata[jj + j];
+      idata[jj + j] = idata[kk + k];
+      idata[kk + k] = tmp;
+      k++;
+    }
+  }
+
+  if (jj > start) {
+    _split(idata, vdata, start, jj - start, h);
+  }
+
+  for (i = 0; i < kk - jj; i++) {
+    vdata[idata[jj + i]] = kk - 1;
+  }
+  if (jj == kk - 1) {
+    idata[jj] = -1;
+  }
+
+  if (start + len > kk) {
+    _split(idata, vdata, kk, start + len - kk, h);
+  }
+}
+
+void _qsufsort(List<int> idata, List<int> vdata, Uint8List olddata) {
+  final int oldsize = olddata.length;
+  final List<int> buckets = List<int>(256);
+
+  for (int i = 0; i < 256; i++) {
+    buckets[i] = 0;
+  }
+  for (int i = 0; i < oldsize; i++) {
+    buckets[olddata[i]]++;
+  }
+  for (int i = 1; i < 256; i++) {
+    buckets[i] += buckets[i - 1];
+  }
+
+  for (int i = 255; i > 0; i--) {
+    buckets[i] = buckets[i - 1];
+  }
+  buckets[0] = 0;
+
+  for (int i = 0; i < oldsize; i++) {
+    idata[++buckets[olddata[i]]] = i;
+  }
+  idata[0] = oldsize;
+
+  for (int i = 0; i < oldsize; i++) {
+    vdata[i] = buckets[olddata[i]];
+  }
+  vdata[oldsize] = 0;
+
+  for (int i = 1; i < 256; i++) {
+    if (buckets[i] == buckets[i - 1] + 1) {
+      idata[buckets[i]] = -1;
+    }
+  }
+  idata[0] = -1;
+
+  for (int h = 1; idata[0] != -(oldsize + 1); h += h) {
+    int len = 0;
+    int i;
+    for (i = 0; i < oldsize + 1;) {
+      if (idata[i] < 0) {
+        len -= idata[i];
+        i -= idata[i];
+      } else {
+        if (len != 0) {
+          idata[i - len] = -len;
+        }
+        len = vdata[idata[i]] + 1 - i;
+        _split(idata, vdata, i, len, h);
+        i += len;
+        len = 0;
+      }
+    }
+    if (len != 0) {
+      idata[i - len] = -len;
+    }
+  }
+
+  for (int i = 0; i < oldsize + 1; i++) {
+    idata[vdata[i]] = i;
+  }
+}
+
+int _matchlen(Uint8List olddata, int oldskip, Uint8List newdata, int newskip) {
+  final int n = min(olddata.length - oldskip, newdata.length - newskip);
+  for (int i = 0; i < n; i++) {
+    if (olddata[oldskip + i] != newdata[newskip + i]) {
+      return i;
+    }
+  }
+  return n;
+}
+
+int _memcmp(Uint8List data1, int skip1, Uint8List data2, int skip2) {
+  final int n = min(data1.length - skip1, data2.length - skip2);
+  for (int i = 0; i < n; i++) {
+    if (data1[i + skip1] != data2[i + skip2]) {
+      return data1[i + skip1] < data2[i + skip2] ? -1 : 1;
+    }
+  }
+  return 0;
+}
+
+class _Ref<T> {
+  T value;
+}
+
+int _search(List<int> idata, Uint8List olddata, Uint8List newdata, int newskip,
+    int start, int end, _Ref<int> pos) {
+  if (end - start < 2) {
+    final int x = _matchlen(olddata, idata[start], newdata, newskip);
+    final int y = _matchlen(olddata, idata[end], newdata, newskip);
+
+    if (x > y) {
+      pos.value = idata[start];
+      return x;
+    } else {
+      pos.value = idata[end];
+      return y;
+    }
+  }
+
+  final int x = start + (end - start) ~/ 2;
+  if (_memcmp(olddata, idata[x], newdata, newskip) < 0) {
+    return _search(idata, olddata, newdata, newskip, x, end, pos);
+  } else {
+    return _search(idata, olddata, newdata, newskip, start, x, pos);
+  }
+}
+
+List<int> _int64bytes(int i) =>
+    (ByteData(8)..setInt64(0, i)).buffer.asUint8List();
+
+Uint8List bsdiff(List<int> olddata, List<int> newdata) {
+  final int oldsize = olddata.length;
+  final int newsize = newdata.length;
+
+  final List<int> idata = List<int>(oldsize + 1);
+  _qsufsort(idata, List<int>(oldsize + 1), olddata);
+
+  final Uint8List db = Uint8List(newsize + 1);
+  final Uint8List eb = Uint8List(newsize + 1);
+
+  int dblen = 0;
+  int eblen = 0;
+
+  BytesBuilder buf = BytesBuilder();
+  final _Ref<int> pos = _Ref<int>();
+
+  for (int scan = 0, len = 0, lastscan = 0, lastpos = 0, lastoffset = 0;
+      scan < newsize;) {
+    int oldscore = 0;
+
+    for (int scsc = scan += len; scan < newsize; scan++) {
+      len = _search(idata, olddata, newdata, scan, 0, oldsize, pos);
+
+      for (; scsc < scan + len; scsc++) {
+        if ((scsc + lastoffset < oldsize) &&
+            (olddata[scsc + lastoffset] == newdata[scsc])) {
+          oldscore++;
+        }
+      }
+      if (((len == oldscore) && (len != 0)) || (len > oldscore + 8)) {
+        break;
+      }
+      if ((scan + lastoffset < oldsize) &&
+          (olddata[scan + lastoffset] == newdata[scan])) {
+        oldscore--;
+      }
+    }
+
+    if ((len != oldscore) || (scan == newsize)) {
+      int lenf = 0;
+      int lenb = 0;
+
+      for (int sf = 0, s = 0, i = 0;
+          (lastscan + i < scan) && (lastpos + i < oldsize);) {
+        if (olddata[lastpos + i] == newdata[lastscan + i]) {
+          s++;
+        }
+        i++;
+        if (s * 2 - i > sf * 2 - lenf) {
+          sf = s;
+          lenf = i;
+        }
+      }
+
+      if (scan < newsize) {
+        for (int sb = 0, s = 0, i = 1;
+            (scan >= lastscan + i) && (pos.value >= i);
+            i++) {
+          if (olddata[pos.value - i] == newdata[scan - i]) {
+            s++;
+          }
+          if (s * 2 - i > sb * 2 - lenb) {
+            sb = s;
+            lenb = i;
+          }
+        }
+      }
+
+      if (lastscan + lenf > scan - lenb) {
+        final int overlap = (lastscan + lenf) - (scan - lenb);
+        int lens = 0;
+        for (int ss = 0, s = 0, i = 0; i < overlap; i++) {
+          if (newdata[lastscan + lenf - overlap + i] ==
+              olddata[lastpos + lenf - overlap + i]) {
+            s++;
+          }
+          if (newdata[scan - lenb + i] == olddata[pos.value - lenb + i]) {
+            s--;
+          }
+          if (s > ss) {
+            ss = s;
+            lens = i + 1;
+          }
+        }
+
+        lenf += lens - overlap;
+        lenb -= lens;
+      }
+
+      for (int i = 0; i < lenf; i++) {
+        db[dblen + i] = newdata[lastscan + i] - olddata[lastpos + i];
+      }
+
+      for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++) {
+        eb[eblen + i] = newdata[lastscan + lenf + i];
+      }
+
+      dblen += lenf;
+      eblen += (scan - lenb) - (lastscan + lenf);
+
+      buf.add(_int64bytes(lenf));
+      buf.add(_int64bytes((scan - lenb) - (lastscan + lenf)));
+      buf.add(_int64bytes((pos.value - lenb) - (lastpos + lenf)));
+
+      lastscan = scan - lenb;
+      lastpos = pos.value - lenb;
+      lastoffset = pos.value - scan;
+    }
+  }
+
+  final BytesBuilder out = BytesBuilder();
+
+  out.add(const AsciiCodec().encode('BZDIFF40').toList());
+  out.add(_int64bytes(0));
+  out.add(_int64bytes(0));
+  out.add(_int64bytes(newsize));
+
+  out.add(gzip.encoder.convert(buf.takeBytes()));
+
+  final int len1 = out.length;
+
+  buf = BytesBuilder();
+  buf.add(db.sublist(0, dblen));
+  out.add(gzip.encoder.convert(buf.takeBytes()));
+
+  final int len2 = out.length;
+
+  buf = BytesBuilder();
+  buf.add(eb.sublist(0, eblen));
+  out.add(gzip.encoder.convert(buf.takeBytes()));
+
+  final Uint8List bytes = Uint8List.fromList(out.takeBytes());
+  final ByteData data = ByteData.view(bytes.buffer);
+  data.setUint64(8, len1 - 32);
+  data.setUint64(16, len2 - len1);
+
+  return bytes;
+}
+
+Uint8List bspatch(List<int> olddata, List<int> diffdata) {
+  final List<int> magic = diffdata.sublist(0, 8);
+  if (const AsciiCodec().decode(magic) != 'BZDIFF40') {
+    throw Exception('Invalid magic');
+  }
+
+  final ByteData header =
+      ByteData.view(Uint8List.fromList(diffdata.sublist(0, 32)).buffer);
+
+  final int ctrllen = header.getInt64(8);
+  final int datalen = header.getInt64(16);
+  final int newsize = header.getInt64(24);
+
+  final List<int> cpf =
+      gzip.decoder.convert(diffdata.sublist(32, 32 + ctrllen));
+  final List<int> dpf = gzip.decoder
+      .convert(diffdata.sublist(32 + ctrllen, 32 + ctrllen + datalen));
+  final List<int> epf = gzip.decoder
+      .convert(diffdata.sublist(32 + ctrllen + datalen, diffdata.length));
+
+  final ByteData cpfdata = ByteData.view(Uint8List.fromList(cpf).buffer);
+
+  final Uint8List newdata = Uint8List(newsize);
+
+  int cpfpos = 0;
+  int dpfpos = 0;
+  int epfpos = 0;
+  int oldpos = 0;
+  int newpos = 0;
+
+  while (newpos < newsize) {
+    final List<int> ctrl = List<int>(3);
+    for (int i = 0; i <= 2; i++) {
+      ctrl[i] = cpfdata.getInt64(8 * cpfpos++);
+    }
+    if (newpos + ctrl[0] > newsize) {
+      throw Exception('Invalid ctrl[0]');
+    }
+
+    newdata.setRange(newpos, newpos + ctrl[0], dpf, dpfpos);
+
+    for (int i = 0; i < ctrl[0]; i++) {
+      if ((oldpos + i >= 0) && (oldpos + i < olddata.length)) {
+        newdata[newpos + i] += olddata[oldpos + i];
+      }
+    }
+
+    dpfpos += ctrl[0];
+    newpos += ctrl[0];
+    oldpos += ctrl[0];
+
+    if (newpos + ctrl[1] > newsize) {
+      throw Exception('Invalid ctrl[0]');
+    }
+
+    newdata.setRange(newpos, newpos + ctrl[1], epf, epfpos);
+
+    epfpos += ctrl[1];
+    newpos += ctrl[1];
+    oldpos += ctrl[2];
+  }
+
+  return newdata;
+}
diff --git a/third_party/packages/bsdiff/pubspec.yaml b/third_party/packages/bsdiff/pubspec.yaml
new file mode 100644
index 0000000..1472d12
--- /dev/null
+++ b/third_party/packages/bsdiff/pubspec.yaml
@@ -0,0 +1,11 @@
+name: bsdiff
+description: Binary diff/patch algorithm based on bsdiff by Colin Percival.
+author: Flutter Team <flutter-dev@googlegroups.com>
+homepage: https://github.com/flutter/packages/tree/master/third_party/packages/bsdiff
+version: 0.1.0
+
+dev_dependencies:
+  test: "^1.3.4"
+
+environment:
+  sdk: ">=2.1.1-dev.2.0 <3.0.0"
diff --git a/third_party/packages/bsdiff/test/bsdiff_test.dart b/third_party/packages/bsdiff/test/bsdiff_test.dart
new file mode 100644
index 0000000..235933a
--- /dev/null
+++ b/third_party/packages/bsdiff/test/bsdiff_test.dart
@@ -0,0 +1,17 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'package:bsdiff/bsdiff.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('roundtrip', () {
+    final Uint8List a = Uint8List.fromList('Hello'.runes.toList());
+    final Uint8List b = Uint8List.fromList('Hello World'.runes.toList());
+    final Uint8List c = bsdiff(a, b);
+    expect(bspatch(a, c), equals(b));
+  });
+}