[espresso] Adds EspressoFlutter as a first-party plugin (#2369)

* Initial open source release of Espresso bindings for Flutter as a new first-party plugin, espresso.
diff --git a/packages/espresso/.gitignore b/packages/espresso/.gitignore
new file mode 100644
index 0000000..e9dc58d
--- /dev/null
+++ b/packages/espresso/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+.dart_tool/
+
+.packages
+.pub/
+
+build/
diff --git a/packages/espresso/.metadata b/packages/espresso/.metadata
new file mode 100644
index 0000000..e6c63f5
--- /dev/null
+++ b/packages/espresso/.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: 0190e40457d43e17bdfaf046dfa634cbc5bf28b9
+  channel: unknown
+
+project_type: plugin
diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md
new file mode 100644
index 0000000..116f3aa
--- /dev/null
+++ b/packages/espresso/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* Initial open-source release of Espresso bindings for Flutter.
diff --git a/packages/espresso/LICENSE b/packages/espresso/LICENSE
new file mode 100644
index 0000000..0c382ce
--- /dev/null
+++ b/packages/espresso/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/espresso/README.md b/packages/espresso/README.md
new file mode 100644
index 0000000..c0678e9
--- /dev/null
+++ b/packages/espresso/README.md
@@ -0,0 +1,125 @@
+# espresso
+
+Provides bindings for Espresso tests of Flutter Android apps.
+
+## Installation
+
+Add the `espresso` package as a `dev_dependency` in your app's pubspec.yaml. If you're testing the example app of a package, add it as a dev_dependency of the main package as well.
+
+Add ```android:usesCleartextTraffic="true"``` in the ```<application>``` in the AndroidManifest.xml
+of the Android app used for testing. It's best to put this in a debug or androidTest
+AndroidManifest.xml so that you don't ship it to end users. (See the example app of this package.)
+
+Add dependencies to your build.gradle:
+
+```groovy
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    testImplementation "com.google.truth:truth:1.0"
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+
+    // Core library
+    api 'androidx.test:core:1.2.0'
+
+    // AndroidJUnitRunner and JUnit Rules
+    androidTestImplementation 'androidx.test:runner:1.1.0'
+    androidTestImplementation 'androidx.test:rules:1.1.0'
+
+    // Assertions
+    androidTestImplementation 'androidx.test.ext:junit:1.0.0'
+    androidTestImplementation 'androidx.test.ext:truth:1.0.0'
+    androidTestImplementation 'com.google.truth:truth:0.42'
+
+    // Espresso dependencies
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
+    androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.1.0'
+
+    // The following Espresso dependency can be either "implementation"
+    // or "androidTestImplementation", depending on whether you want the
+    // dependency to appear on your APK's compile classpath or the test APK
+    // classpath.
+    androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.1.0'
+}
+```
+
+Create an `android/app/src/androidTest` folder and put a test file in a package-appropriate subfolder, e.g. `android/app/src/androidTest/java/com/example/MainActivityTest.java`:
+
+```java
+package com.example.espresso_example;
+
+import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget;
+import static androidx.test.espresso.flutter.action.FlutterActions.click;
+import static androidx.test.espresso.flutter.action.FlutterActions.syntheticClick;
+import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isDescendantOf;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withTooltip;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withType;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.flutter.EspressoFlutter.WidgetInteraction;
+import androidx.test.espresso.flutter.assertion.FlutterAssertions;
+import androidx.test.espresso.flutter.matcher.FlutterMatchers;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link EspressoFlutter}. */
+@RunWith(AndroidJUnit4.class)
+public class MainActivityTest {
+
+    @Before
+    public void setUp() throws Exception {
+        ActivityScenario.launch(MainActivity.class);
+    }
+
+    @Test
+    public void performClick() {
+        onFlutterWidget(withTooltip("Increment")).perform(click());
+        onFlutterWidget(withValueKey("CountText")).check(matches(withText("Button tapped 1 time.")));
+    }
+ ```
+
+You'll need to create a test app that enables the Flutter driver extension.
+You can put this in your test_driver/ folder, e.g. test_driver/example.dart.
+
+```dart
+import 'package:flutter_driver/driver_extension.dart';
+import '../lib/main.dart' as app;
+
+void main() {
+  enableFlutterDriverExtension();
+  app.main();
+}
+```
+
+The following command line command runs the test locally:
+
+```
+./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/example.dart
+```
+
+Espresso tests can also be run on [Firebase Test Lab](https://firebase.google.com/docs/test-lab):
+
+```
+./gradlew app:assembleAndroidTest
+./gradlew app:assembleDebug -Ptarget=<path_to_test>.dart
+gcloud auth activate-service-account --key-file=<PATH_TO_KEY_FILE>
+gcloud --quiet config set project <PROJECT_NAME>
+gcloud firebase test android run --type instrumentation \
+  --app build/app/outputs/apk/debug/app-debug.apk \
+  --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk\
+  --timeout 2m \
+  --results-bucket=<RESULTS_BUCKET> \
+  --results-dir=<RESULTS_DIRECTORY>
+```
+
diff --git a/packages/espresso/android/.gitignore b/packages/espresso/android/.gitignore
new file mode 100644
index 0000000..c6cbe56
--- /dev/null
+++ b/packages/espresso/android/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle
new file mode 100644
index 0000000..4af1d3e
--- /dev/null
+++ b/packages/espresso/android/build.gradle
@@ -0,0 +1,74 @@
+group 'com.example.espresso'
+version '1.0'
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 16
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
+
+dependencies {
+    implementation 'com.google.guava:guava:28.1-android'
+    implementation 'com.squareup.okhttp3:okhttp:3.12.1'
+    implementation 'com.google.code.gson:gson:2.8.6'
+    androidTestImplementation 'org.hamcrest:hamcrest:2.2'
+
+    testImplementation 'junit:junit:4.12'
+    testImplementation "com.google.truth:truth:1.0"
+    api 'androidx.test:runner:1.1.1'
+    api 'androidx.test.espresso:espresso-core:3.1.1'
+
+    // Core library
+    api 'androidx.test:core:1.0.0'
+
+    // AndroidJUnitRunner and JUnit Rules
+    api 'androidx.test:runner:1.1.0'
+    api 'androidx.test:rules:1.1.0'
+
+    // Assertions
+    api 'androidx.test.ext:junit:1.0.0'
+    api 'androidx.test.ext:truth:1.0.0'
+    api 'com.google.truth:truth:0.42'
+
+    // Espresso dependencies
+    api 'androidx.test.espresso:espresso-core:3.1.0'
+    api 'androidx.test.espresso:espresso-contrib:3.1.0'
+    api 'androidx.test.espresso:espresso-intents:3.1.0'
+    api 'androidx.test.espresso:espresso-accessibility:3.1.0'
+    api 'androidx.test.espresso:espresso-web:3.1.0'
+    api 'androidx.test.espresso.idling:idling-concurrent:3.1.0'
+
+    // The following Espresso dependency can be either "implementation"
+    // or "androidTestImplementation", depending on whether you want the
+    // dependency to appear on your APK's compile classpath or the test APK
+    // classpath.
+    api 'androidx.test.espresso:espresso-idling-resource:3.1.0'
+}
+
+
diff --git a/packages/espresso/android/gradle.properties b/packages/espresso/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/espresso/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/packages/espresso/android/gradle/wrapper/gradle-wrapper.properties b/packages/espresso/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4751774
--- /dev/null
+++ b/packages/espresso/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Nov 26 13:04:21 PST 2019
+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/espresso/android/settings.gradle b/packages/espresso/android/settings.gradle
new file mode 100644
index 0000000..46643c1
--- /dev/null
+++ b/packages/espresso/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'espresso'
diff --git a/packages/espresso/android/src/main/AndroidManifest.xml b/packages/espresso/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a70b4d1
--- /dev/null
+++ b/packages/espresso/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.example.espresso">
+</manifest>
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java
new file mode 100644
index 0000000..106436f
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java
@@ -0,0 +1,198 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.flutter.common.Constants.DEFAULT_INTERACTION_TIMEOUT;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isFlutterView;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.any;
+
+import android.util.Log;
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.flutter.action.FlutterViewAction;
+import androidx.test.espresso.flutter.action.WidgetInfoFetcher;
+import androidx.test.espresso.flutter.api.FlutterAction;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetAssertion;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.assertion.FlutterViewAssertion;
+import androidx.test.espresso.flutter.common.Duration;
+import androidx.test.espresso.flutter.exception.NoMatchingWidgetException;
+import androidx.test.espresso.flutter.internal.idgenerator.IdGenerator;
+import androidx.test.espresso.flutter.internal.idgenerator.IdGenerators;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.annotation.Nonnull;
+import okhttp3.OkHttpClient;
+import org.hamcrest.Matcher;
+
+/** Entry point to the Espresso testing APIs on Flutter. */
+public final class EspressoFlutter {
+
+  private static final String TAG = EspressoFlutter.class.getSimpleName();
+
+  private static final OkHttpClient okHttpClient;
+  private static final IdGenerator<Integer> idGenerator;
+  private static final ExecutorService taskExecutor;
+
+  static {
+    okHttpClient = new OkHttpClient();
+    idGenerator = IdGenerators.newIntegerIdGenerator();
+    taskExecutor = Executors.newCachedThreadPool();
+  }
+
+  /**
+   * Creates a {@link WidgetInteraction} for the Flutter widget matched by the given {@code
+   * widgetMatcher}, which is an entry point to perform actions or asserts.
+   *
+   * @param widgetMatcher the matcher used to uniquely match a Flutter widget on the screen.
+   */
+  public static WidgetInteraction onFlutterWidget(@Nonnull WidgetMatcher widgetMatcher) {
+    return new WidgetInteraction(isFlutterView(), widgetMatcher);
+  }
+
+  /**
+   * Provides fluent testing APIs for test authors to perform actions or asserts on Flutter widgets,
+   * similar to {@code ViewInteraction} and {@code WebInteraction}.
+   */
+  public static final class WidgetInteraction {
+
+    /**
+     * Adds a little delay to the interaction timeout so that we make sure not to time out before
+     * the action or assert does.
+     */
+    private static final Duration INTERACTION_TIMEOUT_DELAY = new Duration(1, TimeUnit.SECONDS);
+
+    private final Matcher<View> flutterViewMatcher;
+    private final WidgetMatcher widgetMatcher;
+    private final Duration timeout;
+
+    private WidgetInteraction(Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher) {
+      this(
+          flutterViewMatcher,
+          widgetMatcher,
+          DEFAULT_INTERACTION_TIMEOUT.plus(INTERACTION_TIMEOUT_DELAY));
+    }
+
+    private WidgetInteraction(
+        Matcher<View> flutterViewMatcher, WidgetMatcher widgetMatcher, Duration timeout) {
+      this.flutterViewMatcher = checkNotNull(flutterViewMatcher);
+      this.widgetMatcher = checkNotNull(widgetMatcher);
+      this.timeout = checkNotNull(timeout);
+    }
+
+    /**
+     * Executes the given action(s) with synchronization guarantees: Espresso ensures Flutter's in
+     * an idle state before interacting with the Flutter UI.
+     *
+     * <p>If more than one action is provided, actions are executed in the order provided.
+     *
+     * @param widgetActions one or more actions that shall be performed. Cannot be {@code null}.
+     * @return this interaction for further perform/verification calls.
+     */
+    public WidgetInteraction perform(@Nonnull final WidgetAction... widgetActions) {
+      checkNotNull(widgetActions);
+      for (WidgetAction widgetAction : widgetActions) {
+        // If any error occurred, an unchecked exception will be thrown that stops execution of
+        // following actions.
+        performInternal(widgetAction);
+      }
+      return this;
+    }
+
+    /**
+     * Evaluates the given widget assertion.
+     *
+     * @param assertion a widget assertion that shall be made on the matched Flutter widget. Cannot
+     *     be {@code null}.
+     */
+    public WidgetInteraction check(@Nonnull WidgetAssertion assertion) {
+      checkNotNull(
+          assertion,
+          "Assertion cannot be null. You must specify an assertion on the matched Flutter widget.");
+      WidgetInfo widgetInfo = performInternal(new WidgetInfoFetcher());
+      if (widgetInfo == null) {
+        Log.w(TAG, String.format("Widget info that matches %s is null.", widgetMatcher));
+        throw new NoMatchingWidgetException(
+            String.format("Widget info that matches %s is null.", widgetMatcher));
+      }
+      FlutterViewAssertion flutterViewAssertion = new FlutterViewAssertion(assertion, widgetInfo);
+      onView(flutterViewMatcher).check(flutterViewAssertion);
+      return this;
+    }
+
+    private <T> T performInternal(FlutterAction<T> flutterAction) {
+      checkNotNull(
+          flutterAction,
+          "The action cannot be null. You must specify an action to perform on the matched"
+              + " Flutter widget.");
+      FlutterViewAction<T> flutterViewAction =
+          new FlutterViewAction(
+              widgetMatcher, flutterAction, okHttpClient, idGenerator, taskExecutor);
+      onView(flutterViewMatcher).perform(flutterViewAction);
+      T result;
+      try {
+        if (timeout != null && timeout.getQuantity() > 0) {
+          result = flutterViewAction.waitUntilCompleted(timeout.getQuantity(), timeout.getUnit());
+        } else {
+          result = flutterViewAction.waitUntilCompleted();
+        }
+        return result;
+      } catch (ExecutionException e) {
+        propagateException(e.getCause());
+      } catch (InterruptedException | TimeoutException | RuntimeException e) {
+        propagateException(e);
+      }
+      return null;
+    }
+
+    /**
+     * Propagates exception through #onView so that it get a chance to be handled by the registered
+     * {@code FailureHandler}.
+     */
+    private void propagateException(Throwable t) {
+      onView(flutterViewMatcher).perform(new ExceptionPropagator(t));
+    }
+
+    /**
+     * An exception wrapper that propagates an exception through {@code #onView}, so that it can be
+     * handled by the registered {@code FailureHandler} for the underlying {@code ViewInteraction}.
+     */
+    static class ExceptionPropagator implements ViewAction {
+      private final RuntimeException exception;
+
+      public ExceptionPropagator(RuntimeException exception) {
+        this.exception = checkNotNull(exception);
+      }
+
+      public ExceptionPropagator(Throwable t) {
+        this(new RuntimeException(t));
+      }
+
+      @Override
+      public String getDescription() {
+        return "Propagate: " + exception;
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        throw exception;
+      }
+
+      @SuppressWarnings("unchecked")
+      @Override
+      public Matcher<View> getConstraints() {
+        return any(View.class);
+      }
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java
new file mode 100644
index 0000000..7dcb05b
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java
@@ -0,0 +1,114 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import android.os.Looper;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.espresso.UiController;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+
+/** Utils for the Flutter actions. */
+final class ActionUtil {
+
+  /**
+   * Loops the main thread until the given future task has been done. Users could use this method to
+   * "synchronize" between the main thread and {@code Future} instances running on its own thread
+   * (e.g. methods of the {@code FlutterTestingProtocol}), without blocking the main thread.
+   *
+   * <p>Usage:
+   *
+   * <pre>{@code
+   * Future<T> fooFuture = flutterTestingProtocol.callFoo();
+   * T fooResult = loopUntilCompletion("fooTask", androidUiController, fooFuture, executor);
+   * // Then consumes the fooResult on main thread.
+   * }</pre>
+   *
+   * @param taskName the name that shall be used when registering the task as an {@link
+   *     IdlingResource}. Espresso ignores {@link IdlingResource} with the same name, so always uses
+   *     a unique name if you don't want Espresso to ignore your task.
+   * @param androidUiController the controller to use to interact with the Android UI.
+   * @param futureTask the future task that main thread should wait for a completion signal.
+   * @param executor the executor to use for running async tasks within the method.
+   * @param <T> the return value type.
+   * @return the result of the future task.
+   * @throws ExecutionException if any error occurs during executing the future task.
+   * @throws InterruptedException when any internal thread is interrupted.
+   */
+  public static <T> T loopUntilCompletion(
+      String taskName,
+      UiController androidUiController,
+      Future<T> futureTask,
+      ExecutorService executor)
+      throws ExecutionException, InterruptedException {
+
+    checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
+
+    FutureIdlingResource<T> idlingResourceFuture = new FutureIdlingResource<>(taskName, futureTask);
+    IdlingRegistry.getInstance().register(idlingResourceFuture);
+    try {
+      // It's fine to ignore this {@code Future} handler, since {@code idlingResourceFuture} should
+      // give us the result/error any way.
+      @SuppressWarnings("unused")
+      Future<?> possiblyIgnoredError = executor.submit(idlingResourceFuture);
+      androidUiController.loopMainThreadUntilIdle();
+      checkState(idlingResourceFuture.isDone(), "Future task signaled - but it wasn't done.");
+      return idlingResourceFuture.get();
+    } finally {
+      IdlingRegistry.getInstance().unregister(idlingResourceFuture);
+    }
+  }
+
+  /**
+   * An {@code IdlingResource} implementation that takes in a {@code Future}, and sends the idle
+   * signal to the main thread when the given {@code Future} is done.
+   *
+   * @param <T> the return value type of this {@code FutureTask}.
+   */
+  private static class FutureIdlingResource<T> extends FutureTask<T> implements IdlingResource {
+
+    private final String taskName;
+    // Written from main thread, read from any thread.
+    private volatile ResourceCallback resourceCallback;
+
+    public FutureIdlingResource(String taskName, final Future<T> future) {
+      super(
+          new Callable<T>() {
+            @Override
+            public T call() throws Exception {
+              return future.get();
+            }
+          });
+      this.taskName = checkNotNull(taskName);
+    }
+
+    @Override
+    public String getName() {
+      return taskName;
+    }
+
+    @Override
+    public void done() {
+      resourceCallback.onTransitionToIdle();
+    }
+
+    @Override
+    public boolean isIdleNow() {
+      return isDone();
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(ResourceCallback callback) {
+      this.resourceCallback = callback;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java
new file mode 100644
index 0000000..5da56fd
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java
@@ -0,0 +1,83 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import static androidx.test.espresso.flutter.action.ActionUtil.loopUntilCompletion;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import android.graphics.Rect;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.GeneralClickAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Tap;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** A click on the given Flutter widget by issuing gesture events to the Android system. */
+public final class ClickAction implements WidgetAction {
+
+  private static final String GET_LOCAL_RECT_TASK_NAME = "ClickAction#getLocalRect";
+
+  private final ExecutorService executor;
+
+  public ClickAction(@Nonnull ExecutorService executor) {
+    this.executor = checkNotNull(executor);
+  }
+
+  @Override
+  public ListenableFuture<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+
+    try {
+      Future<Rect> widgetRectFuture = flutterTestingProtocol.getLocalRect(targetWidget);
+      Rect widgetRectInDp =
+          loopUntilCompletion(
+              GET_LOCAL_RECT_TASK_NAME, androidUiController, widgetRectFuture, executor);
+      WidgetCoordinatesCalculator coordinatesCalculator =
+          new WidgetCoordinatesCalculator(widgetRectInDp);
+      // Clicks at the center of the Flutter widget (with no visibility check), with all the default
+      // settings of a native View's click action.
+      ViewAction clickAction =
+          new GeneralClickAction(
+              Tap.SINGLE,
+              coordinatesCalculator,
+              Press.FINGER,
+              InputDevice.SOURCE_UNKNOWN,
+              MotionEvent.BUTTON_PRIMARY);
+      clickAction.perform(androidUiController, flutterView);
+
+      // Espresso will wait for the main thread to finish, so nothing else to wait for in the
+      // testing thread.
+      return immediateFuture(null);
+    } catch (InterruptedException ie) {
+      return immediateFailedFuture(ie);
+    } catch (ExecutionException ee) {
+      return immediateFailedFuture(ee.getCause());
+    } finally {
+      androidUiController.loopMainThreadUntilIdle();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "click";
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java
new file mode 100644
index 0000000..258daf6
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java
@@ -0,0 +1,74 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import androidx.test.espresso.flutter.api.WidgetAction;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.annotation.Nonnull;
+
+/** A collection of actions that can be performed on {@code FlutterView}s or Flutter widgets. */
+public final class FlutterActions {
+
+  private static final ExecutorService taskExecutor = Executors.newCachedThreadPool();
+
+  // Do not initialize.
+  private FlutterActions() {}
+
+  /**
+   * Returns a click action that can be performed on a Flutter widget.
+   *
+   * <p>The current implementation simply clicks at the center of the widget (with no visibility
+   * checks yet). Internally, it calculates the coordinates to click on screen based on the position
+   * of the matched Flutter widget and also its outer Flutter view, and injects gesture events to
+   * the Android system to mimic a human's click.
+   *
+   * <p>Try {@link #syntheticClick()} only when this action cannot handle your case properly, e.g.
+   * Flutter's internal state (only accessible within Flutter) affects how the action should
+   * performed.
+   */
+  public static WidgetAction click() {
+    return new ClickAction(taskExecutor);
+  }
+
+  /**
+   * Returns a synthetic click action that can be performed on a Flutter widget.
+   *
+   * <p>Note, this is not a real click gesture event issued from Android system. Espresso delegates
+   * to Flutter engine to perform the action.
+   *
+   * <p>Always prefer {@link #click()} as it exercises the entire Flutter stack and your Flutter app
+   * by directly injecting key events to the Android system. Uses this {@link #syntheticClick()}
+   * only when there are special cases that {@link #click()} cannot handle properly.
+   */
+  public static WidgetAction syntheticClick() {
+    return new SyntheticClickAction();
+  }
+
+  /**
+   * Returns an action that focuses on the widget (by clicking on it) and types the provided string
+   * into the widget. Appending a \n to the end of the string translates to a ENTER key event. Note:
+   * this method performs a tap on the widget before typing to force the widget into focus, if the
+   * widget already contains text this tap may place the cursor at an arbitrary position within the
+   * text.
+   *
+   * <p>The Flutter widget must support input methods.
+   *
+   * @param stringToBeTyped the text String that shall be input to the matched widget. Cannot be
+   *     {@code null}.
+   */
+  public static WidgetAction typeText(@Nonnull String stringToBeTyped) {
+    return new FlutterTypeTextAction(stringToBeTyped, taskExecutor);
+  }
+
+  /**
+   * Returns an action that scrolls to the widget.
+   *
+   * <p>The widget must be a descendant of a scrollable widget like SingleChildScrollView.
+   */
+  public static WidgetAction scrollTo() {
+    return new FlutterScrollToAction();
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java
new file mode 100644
index 0000000..b97252a
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java
@@ -0,0 +1,51 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import com.google.gson.annotations.Expose;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An action that scrolls the Scrollable ancestor of the widget until the widget is completely
+ * visible.
+ */
+public final class FlutterScrollToAction implements WidgetAction {
+
+  @Override
+  public Future<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+    return flutterTestingProtocol.perform(targetWidget, new ScrollIntoViewAction());
+  }
+
+  @Override
+  public String toString() {
+    return "scrollTo";
+  }
+
+  static class ScrollIntoViewAction extends SyntheticAction {
+
+    @Expose private final double alignment;
+
+    public ScrollIntoViewAction() {
+      this(0.0);
+    }
+
+    public ScrollIntoViewAction(double alignment) {
+      super("scrollIntoView");
+      this.alignment = alignment;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java
new file mode 100644
index 0000000..bb62250
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java
@@ -0,0 +1,185 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import static androidx.test.espresso.flutter.action.ActionUtil.loopUntilCompletion;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.util.concurrent.Futures.allAsList;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.GeneralClickAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Tap;
+import androidx.test.espresso.action.TypeTextAction;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import com.google.common.util.concurrent.JdkFutureAdapters;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.annotations.Expose;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** An action that types text on a Flutter widget. */
+public final class FlutterTypeTextAction implements WidgetAction {
+
+  private static final String TAG = FlutterTypeTextAction.class.getSimpleName();
+
+  private static final String GET_LOCAL_RECT_TASK_NAME = "FlutterTypeTextAction#getLocalRect";
+  private static final String FLUTTER_IDLE_TASK_NAME = "FlutterTypeTextAction#flutterIsIdle";
+
+  private final String stringToBeTyped;
+  private final boolean tapToFocus;
+  private final ExecutorService executor;
+
+  /**
+   * Constructs with the given input string. If the string is empty it results in no-op (nothing is
+   * typed). By default this action sends a tap event to the center of the widget to attain focus
+   * before typing.
+   *
+   * @param stringToBeTyped String To be typed in.
+   */
+  FlutterTypeTextAction(@Nonnull String stringToBeTyped, @Nonnull ExecutorService executor) {
+    this(stringToBeTyped, executor, true);
+  }
+
+  /**
+   * Constructs with the given input string. If the string is empty it results in no-op (nothing is
+   * typed). By default this action sends a tap event to the center of the widget to attain focus
+   * before typing.
+   *
+   * @param stringToBeTyped String To be typed in.
+   * @param tapToFocus indicates whether a tap should be sent to the underlying widget before
+   *     typing.
+   */
+  FlutterTypeTextAction(
+      @Nonnull String stringToBeTyped, @Nonnull ExecutorService executor, boolean tapToFocus) {
+    this.stringToBeTyped = checkNotNull(stringToBeTyped, "The text to type in cannot be null.");
+    this.executor = checkNotNull(executor);
+    this.tapToFocus = tapToFocus;
+  }
+
+  @Override
+  public ListenableFuture<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+
+    // No-op if string is empty.
+    if (stringToBeTyped.length() == 0) {
+      Log.w(TAG, "Text string is empty resulting in no-op (nothing is typed).");
+      return immediateFuture(null);
+    }
+
+    try {
+      ListenableFuture<Void> setTextEntryEmulationFuture =
+          JdkFutureAdapters.listenInPoolThread(
+              flutterTestingProtocol.perform(null, new SetTextEntryEmulationAction(false)));
+      ListenableFuture<Rect> widgetRectFuture =
+          JdkFutureAdapters.listenInPoolThread(flutterTestingProtocol.getLocalRect(targetWidget));
+      // Waits until both Futures return and then proceeds.
+      Rect widgetRectInDp =
+          (Rect)
+              loopUntilCompletion(
+                      GET_LOCAL_RECT_TASK_NAME,
+                      androidUiController,
+                      allAsList(widgetRectFuture, setTextEntryEmulationFuture),
+                      executor)
+                  .get(0);
+
+      // Clicks at the center of the Flutter widget (with no visibility check).
+      //
+      // Calls the click action separately so we get a chance to ensure Flutter is idle before
+      // typing text.
+      WidgetCoordinatesCalculator coordinatesCalculator =
+          new WidgetCoordinatesCalculator(widgetRectInDp);
+      if (tapToFocus) {
+        GeneralClickAction clickAction =
+            new GeneralClickAction(
+                Tap.SINGLE,
+                coordinatesCalculator,
+                Press.FINGER,
+                InputDevice.SOURCE_UNKNOWN,
+                MotionEvent.BUTTON_PRIMARY);
+        clickAction.perform(androidUiController, flutterView);
+        loopUntilCompletion(
+            FLUTTER_IDLE_TASK_NAME,
+            androidUiController,
+            flutterTestingProtocol.waitUntilIdle(),
+            executor);
+      }
+
+      // Then types in text.
+      ViewAction typeTextAction = new TypeTextAction(stringToBeTyped, false);
+      typeTextAction.perform(androidUiController, flutterView);
+
+      // Espresso will wait for the main thread to finish, so nothing else to wait for in the
+      // testing thread.
+      return immediateFuture(null);
+    } catch (InterruptedException ie) {
+      return immediateFailedFuture(ie);
+    } catch (ExecutionException ee) {
+      return immediateFailedFuture(ee.getCause());
+    } finally {
+      androidUiController.loopMainThreadUntilIdle();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return String.format(Locale.ROOT, "type text(%s)", stringToBeTyped);
+  }
+
+  /**
+   * The {@link SyntheticAction} that configures text entry emulation.
+   *
+   * <p>If the text entry emulation is enabled, the operating system's configured keyboard will not
+   * be invoked when the widget is focused. Explicitly disables the text entry emulation when text
+   * input is supposed to be sent using the system's keyboard.
+   *
+   * <p>By default, the text entry emulation is enabled in the Flutter testing protocol.
+   */
+  private static final class SetTextEntryEmulationAction extends SyntheticAction {
+
+    @Expose private final boolean enabled;
+
+    /**
+     * Constructs with the given text entry emulation setting.
+     *
+     * @param enabled whether the text entry emulation is enabled. When {@code enabled} is {@code
+     *     true}, the system's configured keyboard will not be invoked when the widget is focused.
+     */
+    public SetTextEntryEmulationAction(boolean enabled) {
+      super("set_text_entry_emulation");
+      this.enabled = enabled;
+    }
+
+    /**
+     * Constructs with the given text entry emulation setting and also a timeout setting for this
+     * action.
+     *
+     * @param enabled whether the text entry emulation is enabled. When {@code enabled} is {@code
+     *     true}, the system's configured keyboard will not be invoked when the widget is focused.
+     * @param timeOutInMillis the timeout setting of this action.
+     */
+    public SetTextEntryEmulationAction(boolean enabled, long timeOutInMillis) {
+      super("set_text_entry_emulation", timeOutInMillis);
+      this.enabled = enabled;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java
new file mode 100644
index 0000000..7864b43
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java
@@ -0,0 +1,224 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import static androidx.test.espresso.flutter.action.ActionUtil.loopUntilCompletion;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isFlutterView;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.util.concurrent.Futures.transformAsync;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.os.Looper;
+import android.view.View;
+import androidx.test.annotation.Beta;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.flutter.api.FlutterAction;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.internal.idgenerator.IdGenerator;
+import androidx.test.espresso.flutter.internal.jsonrpc.JsonRpcClient;
+import androidx.test.espresso.flutter.internal.protocol.impl.DartVmService;
+import androidx.test.espresso.flutter.internal.protocol.impl.DartVmServiceUtil;
+import androidx.test.espresso.flutter.internal.protocol.impl.FlutterProtocolException;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.JdkFutureAdapters;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import io.flutter.embedding.android.FlutterView;
+import io.flutter.view.FlutterNativeView;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import okhttp3.OkHttpClient;
+import org.hamcrest.Matcher;
+
+/**
+ * A {@code ViewAction} which performs an action on the given {@code FlutterView}.
+ *
+ * <p>This class acts as a bridge to perform {@code WidgetAction} on a Flutter widget on the given
+ * {@code FlutterView}.
+ */
+@Beta
+public final class FlutterViewAction<T> implements ViewAction {
+
+  private static final String FLUTTER_IDLE_TASK_NAME = "flutterIdlingResource";
+
+  private final SettableFuture<T> resultFuture = SettableFuture.create();
+  private final WidgetMatcher widgetMatcher;
+  private final FlutterAction<T> widgetAction;
+  private final OkHttpClient webSocketClient;
+  private final IdGenerator<Integer> messageIdGenerator;
+  private final ExecutorService taskExecutor;
+
+  /**
+   * Constructs an instance based on the given params.
+   *
+   * @param widgetMatcher the matcher that uniquely matches a widget on the {@code FlutterView}.
+   *     Could be {@code null} if this is a universal action that doesn't apply to any specific
+   *     widget.
+   * @param widgetAction the action to be performed on the matched Flutter widget.
+   * @param webSocketClient the WebSocket client that shall be used in the {@code
+   *     FlutterTestingProtocol}.
+   * @param messageIdGenerator an ID generator that shall be used in the {@code
+   *     FlutterTestingProtocol}.
+   * @param taskExecutor the task executor that shall be used in the {@code WidgetAction}.
+   */
+  public FlutterViewAction(
+      WidgetMatcher widgetMatcher,
+      FlutterAction<T> widgetAction,
+      OkHttpClient webSocketClient,
+      IdGenerator<Integer> messageIdGenerator,
+      ExecutorService taskExecutor) {
+    this.widgetMatcher = widgetMatcher;
+    this.widgetAction = checkNotNull(widgetAction);
+    this.webSocketClient = checkNotNull(webSocketClient);
+    this.messageIdGenerator = checkNotNull(messageIdGenerator);
+    this.taskExecutor = checkNotNull(taskExecutor);
+  }
+
+  @Override
+  public Matcher<View> getConstraints() {
+    return isFlutterView();
+  }
+
+  @Override
+  public String getDescription() {
+    return String.format(
+        "Perform a %s action on the Flutter widget matched %s.", widgetAction, widgetMatcher);
+  }
+
+  @Override
+  public void perform(UiController uiController, View flutterView) {
+    // There could be a gap between when the Flutter view is available in the view hierarchy and the
+    // engine & Dart isolates are actually up and running. Check whether the first frame has been
+    // rendered before proceeding in an unblocking way.
+    loopUntilFlutterViewRendered(flutterView, uiController);
+    // The url {@code FlutterNativeView} returns is the http url that the Dart VM Observatory http
+    // server serves at. Need to convert to the one that the WebSocket uses.
+    URI dartVmServiceProtocolUrl =
+        DartVmServiceUtil.getServiceProtocolUri(FlutterNativeView.getObservatoryUri());
+    String isolateId = DartVmServiceUtil.getDartIsolateId(flutterView);
+    final FlutterTestingProtocol flutterTestingProtocol =
+        new DartVmService(
+            isolateId,
+            new JsonRpcClient(webSocketClient, dartVmServiceProtocolUrl),
+            messageIdGenerator,
+            taskExecutor);
+
+    try {
+      // First checks the testing protocol is ready for use and then waits until the Flutter app is
+      // idle before executing the action.
+      ListenableFuture<Void> testingProtocolReadyFuture =
+          JdkFutureAdapters.listenInPoolThread(flutterTestingProtocol.connect());
+      AsyncFunction<Void, Void> flutterIdleFunc =
+          new AsyncFunction<Void, Void>() {
+            public ListenableFuture<Void> apply(Void readyResult) {
+              return JdkFutureAdapters.listenInPoolThread(flutterTestingProtocol.waitUntilIdle());
+            }
+          };
+      ListenableFuture<Void> flutterIdleFuture =
+          transformAsync(testingProtocolReadyFuture, flutterIdleFunc, taskExecutor);
+      loopUntilCompletion(FLUTTER_IDLE_TASK_NAME, uiController, flutterIdleFuture, taskExecutor);
+      perform(flutterView, flutterTestingProtocol, uiController);
+    } catch (ExecutionException ee) {
+      resultFuture.setException(ee.getCause());
+    } catch (InterruptedException ie) {
+      resultFuture.setException(ie);
+    }
+  }
+
+  @VisibleForTesting
+  void perform(
+      View flutterView, FlutterTestingProtocol flutterTestingProtocol, UiController uiController) {
+    final ListenableFuture<T> actionResultFuture =
+        JdkFutureAdapters.listenInPoolThread(
+            widgetAction.perform(widgetMatcher, flutterView, flutterTestingProtocol, uiController));
+    actionResultFuture.addListener(
+        new Runnable() {
+          @Override
+          public void run() {
+            try {
+              resultFuture.set(actionResultFuture.get());
+            } catch (ExecutionException | InterruptedException e) {
+              resultFuture.setException(e);
+            }
+          }
+        },
+        directExecutor());
+  }
+
+  /** Blocks until this action has completed execution. */
+  public T waitUntilCompleted() throws ExecutionException, InterruptedException {
+    checkState(Looper.myLooper() != Looper.getMainLooper(), "On main thread!");
+    return resultFuture.get();
+  }
+
+  /** Blocks until this action has completed execution with a configurable timeout. */
+  public T waitUntilCompleted(long timeout, TimeUnit unit)
+      throws InterruptedException, ExecutionException, TimeoutException {
+    checkState(Looper.myLooper() != Looper.getMainLooper(), "On main thread!");
+    return resultFuture.get(timeout, unit);
+  }
+
+  private static void loopUntilFlutterViewRendered(View flutterView, UiController uiController) {
+    FlutterViewRenderedIdlingResource idlingResource =
+        new FlutterViewRenderedIdlingResource(flutterView);
+    try {
+      IdlingRegistry.getInstance().register(idlingResource);
+      uiController.loopMainThreadUntilIdle();
+    } finally {
+      IdlingRegistry.getInstance().unregister(idlingResource);
+    }
+  }
+
+  /**
+   * An {@link IdlingResource} that checks whether the Flutter view's first frame has been rendered
+   * in an unblocking way.
+   */
+  static final class FlutterViewRenderedIdlingResource implements IdlingResource {
+
+    private final View flutterView;
+    // Written from main thread, read from any thread.
+    private volatile ResourceCallback resourceCallback;
+
+    FlutterViewRenderedIdlingResource(View flutterView) {
+      this.flutterView = checkNotNull(flutterView);
+    }
+
+    @Override
+    public String getName() {
+      return FlutterViewRenderedIdlingResource.class.getSimpleName();
+    }
+
+    @Override
+    public boolean isIdleNow() {
+      boolean isIdle = false;
+      if (flutterView instanceof FlutterView) {
+        isIdle = ((FlutterView) flutterView).hasRenderedFirstFrame();
+      } else if (flutterView instanceof io.flutter.view.FlutterView) {
+        isIdle = ((io.flutter.view.FlutterView) flutterView).hasRenderedFirstFrame();
+      } else {
+        throw new FlutterProtocolException(
+            String.format("This is not a Flutter View instance [id: %d].", flutterView.getId()));
+      }
+      if (isIdle) {
+        resourceCallback.onTransitionToIdle();
+      }
+      return isIdle;
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(ResourceCallback callback) {
+      resourceCallback = callback;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java
new file mode 100644
index 0000000..5036be1
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java
@@ -0,0 +1,47 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import android.view.View;
+import androidx.test.annotation.Beta;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * A synthetic click on a Flutter widget.
+ *
+ * <p>Note, this is not a real click gesture event issued from Android system. Espresso delegates to
+ * Flutter engine to perform the {@link SyntheticClick} action.
+ */
+@Beta
+public final class SyntheticClickAction implements WidgetAction {
+
+  @Override
+  public Future<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+    return flutterTestingProtocol.perform(targetWidget, new SyntheticClick());
+  }
+
+  @Override
+  public String toString() {
+    return "click";
+  }
+
+  static class SyntheticClick extends SyntheticAction {
+
+    public SyntheticClick() {
+      super("tap");
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java
new file mode 100644
index 0000000..b83e29b
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java
@@ -0,0 +1,32 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.WidgetAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** An action that ensures Flutter is in an idle state. */
+public final class WaitUntilIdleAction implements WidgetAction {
+
+  @Override
+  public Future<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+    return flutterTestingProtocol.waitUntilIdle();
+  }
+
+  @Override
+  public String toString() {
+    return "action that waits until Flutter's idle.";
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java
new file mode 100644
index 0000000..8d541ae
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java
@@ -0,0 +1,68 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import androidx.test.espresso.action.CoordinatesProvider;
+import java.util.Arrays;
+
+/** Provides coordinates of a Flutter widget. */
+final class WidgetCoordinatesCalculator implements CoordinatesProvider {
+
+  private static final String TAG = WidgetCoordinatesCalculator.class.getSimpleName();
+
+  private final Rect widgetRectInDp;
+
+  /**
+   * Constructs with the local (as relative to the outer Flutter view) coordinates of a Flutter
+   * widget in the unit of dp.
+   *
+   * @param widgetRectInDp the local widget coordinates in dp.
+   */
+  public WidgetCoordinatesCalculator(Rect widgetRectInDp) {
+    this.widgetRectInDp = checkNotNull(widgetRectInDp);
+  }
+
+  @Override
+  public float[] calculateCoordinates(View flutterView) {
+    int deviceDensityDpi = flutterView.getContext().getResources().getDisplayMetrics().densityDpi;
+    Rect widgetRectInPixel = convertDpToPixel(widgetRectInDp, deviceDensityDpi);
+    float widgetCenterX = (widgetRectInPixel.left + widgetRectInPixel.right) / 2;
+    float widgetCenterY = (widgetRectInPixel.top + widgetRectInPixel.bottom) / 2;
+    int[] viewCords = new int[] {0, 0};
+    flutterView.getLocationOnScreen(viewCords);
+    float[] coords = new float[] {viewCords[0] + widgetCenterX, viewCords[1] + widgetCenterY};
+    Log.d(
+        TAG,
+        String.format(
+            "Clicks on widget[%s] on Flutter View[%d, %d][width:%d, height:%d] at coordinates"
+                + " [%s] on screen",
+            widgetRectInPixel,
+            viewCords[0],
+            viewCords[1],
+            flutterView.getWidth(),
+            flutterView.getHeight(),
+            Arrays.toString(coords)));
+    return coords;
+  }
+
+  private static Rect convertDpToPixel(Rect rectInDp, int densityDpi) {
+    checkNotNull(rectInDp);
+    int left = (int) convertDpToPixel(rectInDp.left, densityDpi);
+    int top = (int) convertDpToPixel(rectInDp.top, densityDpi);
+    int right = (int) convertDpToPixel(rectInDp.right, densityDpi);
+    int bottom = (int) convertDpToPixel(rectInDp.bottom, densityDpi);
+    return new Rect(left, top, right, bottom);
+  }
+
+  private static float convertDpToPixel(float dp, int densityDpi) {
+    return dp * ((float) densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java
new file mode 100644
index 0000000..90d494e
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java
@@ -0,0 +1,28 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.action;
+
+import android.view.View;
+import androidx.test.espresso.UiController;
+import androidx.test.espresso.flutter.api.FlutterAction;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** A {@link FlutterAction} that retrieves the {@code WidgetInfo} of the matched Flutter widget. */
+public final class WidgetInfoFetcher implements FlutterAction<WidgetInfo> {
+
+  @Override
+  public Future<WidgetInfo> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController) {
+    return flutterTestingProtocol.matchWidget(targetWidget);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java
new file mode 100644
index 0000000..24b264c
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java
@@ -0,0 +1,30 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import android.view.View;
+import androidx.test.espresso.UiController;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a Flutter widget action.
+ *
+ * <p>This interface is part of Espresso-Flutter testing framework. Users should usually expect no
+ * return value for an action and use the {@code WidgetAction} for customizing an action on a
+ * Flutter widget.
+ *
+ * @param <R> The type of the action result.
+ */
+public interface FlutterAction<R> {
+
+  /** Performs an action on the given Flutter widget and gets its return value. */
+  Future<R> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController);
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java
new file mode 100644
index 0000000..d01aaf5
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java
@@ -0,0 +1,77 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import android.graphics.Rect;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.common.annotations.Beta;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** Defines the testing protocol/semantics between Espresso and Flutter. */
+@Beta
+public interface FlutterTestingProtocol {
+
+  /** Returns a future that waits until the Flutter testing protocol is in a usable state. */
+  public Future<Void> connect();
+
+  /**
+   * Performs a synthetic action on the Flutter widget that matches the given {@code widgetMatcher}.
+   *
+   * <p>If failed to perform the given {@code action}, returns a {@code Future} containing an {@code
+   * ExecutionException} that wraps the following exception:
+   *
+   * <ul>
+   *   <li>{@code AmbiguousWidgetMatcherException} if the given {@code widgetMatcher} matched
+   *       multiple widgets in the hierarchy when only one widget was expected.
+   *   <li>{@code NoMatchingWidgetException} if the given {@code widgetMatcher} did not match any
+   *       widget in the Flutter UI hierarchy.
+   *   <li>{@code ConnectException} if connection error occurred.
+   * </ul>
+   *
+   * @param widgetMatcher the matcher to match a Flutter widget. If {@code null}, {@code action} is
+   *     not performed on a specific widget.
+   * @param action the action to be performed on the widget.
+   * @return a {@code Future} representing pending completion of performing the action, or yields an
+   *     exception if the action was failed to perform.
+   */
+  Future<Void> perform(@Nullable WidgetMatcher widgetMatcher, @Nonnull SyntheticAction action);
+
+  /**
+   * Returns a Java representation of the Flutter widget that matches the given widget matcher.
+   *
+   * <p>If failed to find a matching widget, returns a {@code Future} containing an {@code
+   * ExecutionException} that wraps the following exception:
+   *
+   * <ul>
+   *   <li>{@code AmbiguousWidgetMatcherException} if the given {@code widgetMatcher} matched
+   *       multiple widgets in the hierarchy when only one widget was expected.
+   *   <li>{@code NoMatchingWidgetException} if the given {@code widgetMatcher} did not match any
+   *       widget in the Flutter UI hierarchy.
+   *   <li>{@code ConnectException} if connection error occurred.
+   * </ul>
+   *
+   * @param widgetMatcher the matcher to match a Flutter widget. Cannot be {@code null}.
+   * @return a {@code Future} representing pending completion of the matching operation.
+   */
+  Future<WidgetInfo> matchWidget(@Nonnull WidgetMatcher widgetMatcher);
+
+  /**
+   * Returns the local (as relative to its outer Flutter View) rectangle area of a widget that
+   * matches the given widget matcher.
+   *
+   * @param widgetMatcher the matcher to match a Flutter widget. Cannot be {@code null}.
+   * @return a rectangle area where the matched widget lives, in the unit of dp (Density-independent
+   *     Pixel).
+   */
+  Future<Rect> getLocalRect(@Nonnull WidgetMatcher widgetMatcher);
+
+  /** Waits until the Flutter frame is in a stable state. */
+  Future<Void> waitUntilIdle();
+
+  /** Releases all the resources associated with this testing protocol connection. */
+  void close();
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java
new file mode 100644
index 0000000..aed0c4b
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java
@@ -0,0 +1,66 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import static androidx.test.espresso.flutter.common.Constants.DEFAULT_INTERACTION_TIMEOUT;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.Beta;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * Base Flutter synthetic action.
+ *
+ * <p>A synthetic action is not a real gesture event issued to the Android system, rather it's an
+ * action that's performed via Flutter engine. It's supposed to be used for complex interactions or
+ * those that are brittle if performed through Android system. Most of the actions should be
+ * associated with a {@link WidgetMatcher}, but some may not, e.g. an action that checks the
+ * rendering status of the entire {@link io.flutter.view.FlutterView}.
+ */
+@Beta
+public abstract class SyntheticAction {
+
+  @Expose
+  @SerializedName("command")
+  protected String actionId;
+
+  @Expose
+  @SerializedName("timeout")
+  protected long timeOutInMillis;
+
+  protected SyntheticAction(@Nonnull String actionId) {
+    this(actionId, DEFAULT_INTERACTION_TIMEOUT.toMillis());
+  }
+
+  protected SyntheticAction(@Nonnull String actionId, long timeOutInMillis) {
+    this.actionId = checkNotNull(actionId);
+    this.timeOutInMillis = timeOutInMillis;
+  }
+
+  @Override
+  public String toString() {
+    return actionId;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == null) {
+      return false;
+    } else if (obj instanceof SyntheticAction) {
+      SyntheticAction otherAction = (SyntheticAction) obj;
+      return Objects.equals(actionId, otherAction.actionId);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(actionId);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java
new file mode 100644
index 0000000..e49d3ef
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java
@@ -0,0 +1,43 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import android.view.View;
+import androidx.test.espresso.UiController;
+import com.google.common.annotations.Beta;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Responsible for performing an interaction on the given Flutter widget.
+ *
+ * <p>This is part of the Espresso-Flutter test framework public API - developers are free to write
+ * their own {@code WidgetAction} implementation when necessary.
+ */
+@Beta
+public interface WidgetAction extends FlutterAction<Void> {
+
+  /**
+   * Performs this action on the given Flutter widget.
+   *
+   * <p>If the given {@code targetWidget} is {@code null}, this action shall be performed on the
+   * entire {@code FlutterView} in context.
+   *
+   * @param targetWidget the matcher that uniquely identifies a Flutter widget on the given {@code
+   *     FlutterView}. {@code Null} if it's a global action on the {@code FlutterView} in context.
+   * @param flutterView the Flutter view that this widget lives in.
+   * @param flutterTestingProtocol the channel for talking to Flutter app directly.
+   * @param androidUiController the interface for issuing UI operations to the Android system.
+   * @return a {@code Future} representing pending completion of performing the action, or yields an
+   *     exception if the action failed to perform.
+   */
+  @Override
+  Future<Void> perform(
+      @Nullable WidgetMatcher targetWidget,
+      @Nonnull View flutterView,
+      @Nonnull FlutterTestingProtocol flutterTestingProtocol,
+      @Nonnull UiController androidUiController);
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java
new file mode 100644
index 0000000..313dd26
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java
@@ -0,0 +1,25 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import android.view.View;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.common.annotations.Beta;
+
+/**
+ * Similar to a {@code ViewAssertion}, a {@link WidgetAssertion} is responsible for performing an
+ * assertion on a Flutter widget.
+ */
+@Beta
+public interface WidgetAssertion {
+
+  /**
+   * Checks the state of the Flutter widget.
+   *
+   * @param flutterView the Flutter view that this widget lives in.
+   * @param widgetInfo the instance that represents a Flutter widget.
+   */
+  void check(View flutterView, WidgetInfo widgetInfo);
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java
new file mode 100644
index 0000000..9f47e0b
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java
@@ -0,0 +1,41 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.api;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.common.annotations.Beta;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import javax.annotation.Nonnull;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Base matcher for Flutter widgets.
+ *
+ * <p>A widget matcher's function is two-fold:
+ *
+ * <ul>
+ *   <li>A matcher that can be passed into Flutter for selecting a Flutter widget.
+ *   <li>Works with the {@code MatchesWidgetAssertion} to assert on a widget's properties.
+ * </ul>
+ */
+@Beta
+public abstract class WidgetMatcher extends TypeSafeMatcher<WidgetInfo> {
+
+  @Expose
+  @SerializedName("finderType")
+  protected String matcherId;
+
+  /**
+   * Constructs a {@code WidgetMatcher} instance with the given {@code matcherId}.
+   *
+   * @param matcherId the matcher id that represents this widget matcher.
+   */
+  public WidgetMatcher(@Nonnull String matcherId) {
+    this.matcherId = checkNotNull(matcherId);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java
new file mode 100644
index 0000000..63ec0f6
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java
@@ -0,0 +1,41 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.assertion;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.view.View;
+import androidx.test.espresso.flutter.api.WidgetAssertion;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import javax.annotation.Nonnull;
+import org.hamcrest.Matcher;
+
+/** Collection of common {@link WidgetAssertion} instances. */
+public final class FlutterAssertions {
+
+  /**
+   * Returns a generic {@link WidgetAssertion} that asserts that a Flutter widget exists and is
+   * matched by the given widget matcher.
+   */
+  public static WidgetAssertion matches(@Nonnull Matcher<WidgetInfo> widgetMatcher) {
+    return new MatchesWidgetAssertion(checkNotNull(widgetMatcher, "Matcher cannot be null."));
+  }
+
+  /** A widget assertion that checks whether a widget is matched by the given matcher. */
+  static class MatchesWidgetAssertion implements WidgetAssertion {
+
+    private final Matcher<WidgetInfo> widgetMatcher;
+
+    private MatchesWidgetAssertion(Matcher<WidgetInfo> widgetMatcher) {
+      this.widgetMatcher = checkNotNull(widgetMatcher);
+    }
+
+    @Override
+    public void check(View flutterView, WidgetInfo widgetInfo) {
+      assertThat(widgetInfo, widgetMatcher);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java
new file mode 100644
index 0000000..5f4697d
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java
@@ -0,0 +1,45 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.assertion;
+
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isFlutterView;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.view.View;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.espresso.ViewAssertion;
+import androidx.test.espresso.flutter.api.WidgetAssertion;
+import androidx.test.espresso.flutter.exception.InvalidFlutterViewException;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import androidx.test.espresso.util.HumanReadables;
+
+/**
+ * A {@code ViewAssertion} which performs an action on the given Flutter view.
+ *
+ * <p>This class acts as a bridge to perform {@code WidgetAssertion} on a Flutter widget on the
+ * given Flutter view.
+ */
+public final class FlutterViewAssertion implements ViewAssertion {
+
+  private final WidgetAssertion assertion;
+  private final WidgetInfo widgetInfo;
+
+  public FlutterViewAssertion(WidgetAssertion assertion, WidgetInfo widgetInfo) {
+    this.assertion = checkNotNull(assertion, "Widget assertion cannot be null.");
+    this.widgetInfo = checkNotNull(widgetInfo, "The widget info to be asserted on cannot be null.");
+  }
+
+  @Override
+  public void check(View view, NoMatchingViewException noViewFoundException) {
+    if (view == null) {
+      throw noViewFoundException;
+    } else if (!isFlutterView().matches(view)) {
+      throw new InvalidFlutterViewException(
+          String.format("Not a valid Flutter view:%s", HumanReadables.describe(view)));
+    } else {
+      assertion.check(view, widgetInfo);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java
new file mode 100644
index 0000000..c47f8df
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java
@@ -0,0 +1,17 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.common;
+
+import java.util.concurrent.TimeUnit;
+
+/** A utility class to hold various constants used by the Espresso-Flutter library. */
+public final class Constants {
+
+  // Do not initialize.
+  private Constants() {}
+
+  /** Default timeout for actions and asserts like {@code WidgetAction}. */
+  public static final Duration DEFAULT_INTERACTION_TIMEOUT = new Duration(10, TimeUnit.SECONDS);
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java
new file mode 100644
index 0000000..d620153
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java
@@ -0,0 +1,61 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.common;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+
+/**
+ * A simple implementation of a time duration, supposed to be used within the Espresso-Flutter
+ * library.
+ *
+ * <p>This class is immutable.
+ */
+public final class Duration {
+
+  private final long quantity;
+  private final TimeUnit unit;
+
+  /**
+   * Initializes a Duration instance.
+   *
+   * @param quantity the amount of time in the given unit.
+   * @param unit the time unit. Cannot be null.
+   */
+  public Duration(long quantity, TimeUnit unit) {
+    this.quantity = quantity;
+    this.unit = checkNotNull(unit, "Time unit cannot be null.");
+  }
+
+  /** Returns the amount of time. */
+  public long getQuantity() {
+    return quantity;
+  }
+
+  /** Returns the time unit. */
+  public TimeUnit getUnit() {
+    return unit;
+  }
+
+  /** Returns the amount of time in milliseconds. */
+  public long toMillis() {
+    return TimeUnit.MILLISECONDS.convert(quantity, unit);
+  }
+
+  /**
+   * Returns a new Duration instance that adds this instance to the given {@code duration}. If the
+   * given {@code duration} is null, this method simply returns this instance.
+   */
+  public Duration plus(@Nullable Duration duration) {
+    if (duration == null) {
+      return this;
+    }
+    long add = unit.convert(duration.quantity, duration.unit);
+    long newQuantity = quantity + add;
+    return new Duration(newQuantity, unit);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java
new file mode 100644
index 0000000..24d495f
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java
@@ -0,0 +1,19 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.exception;
+
+import androidx.test.espresso.EspressoException;
+
+/**
+ * Indicates that a {@code WidgetMatcher} matched multiple widgets in the Flutter UI hierarchy when
+ * only one widget was expected.
+ */
+public final class AmbiguousWidgetMatcherException extends RuntimeException
+    implements EspressoException {
+
+  public AmbiguousWidgetMatcherException(String message) {
+    super(message);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java
new file mode 100644
index 0000000..ca69e39
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java
@@ -0,0 +1,17 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.exception;
+
+import androidx.test.espresso.EspressoException;
+
+/** Indicates that the {@code View} that Espresso operates on is not a valid Flutter View. */
+public final class InvalidFlutterViewException extends RuntimeException
+    implements EspressoException {
+
+  /** Constructs with an error message. */
+  public InvalidFlutterViewException(String message) {
+    super(message);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java
new file mode 100644
index 0000000..49c949a
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java
@@ -0,0 +1,18 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.exception;
+
+import androidx.test.espresso.EspressoException;
+
+/**
+ * Indicates that a given {@code WidgetMatcher} did not match any widgets in the Flutter UI
+ * hierarchy.
+ */
+public final class NoMatchingWidgetException extends RuntimeException implements EspressoException {
+
+  public NoMatchingWidgetException(String message) {
+    super(message);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java
new file mode 100644
index 0000000..1a3666e
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.idgenerator;
+
+/** Thrown if an ID cannot be generated. */
+public final class IdException extends RuntimeException {
+
+  private static final long serialVersionUID = 0L;
+
+  public IdException() {
+    super();
+  }
+
+  public IdException(String message) {
+    super(message);
+  }
+
+  public IdException(String message, Throwable throwable) {
+    super(message, throwable);
+  }
+
+  public IdException(Throwable throwable) {
+    super(throwable);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java
new file mode 100644
index 0000000..b69d8f6
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java
@@ -0,0 +1,19 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.idgenerator;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+/** Generates unique IDs of the parameterized type. */
+public interface IdGenerator<T> {
+
+  /**
+   * Returns a new, unique ID.
+   *
+   * @throws IdException if there were any errors in getting an ID.
+   */
+  @CanIgnoreReturnValue
+  T next();
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java
new file mode 100644
index 0000000..f8f72dc
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java
@@ -0,0 +1,65 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.idgenerator;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Some simple in-memory ID generators. */
+public final class IdGenerators {
+
+  private IdGenerators() {}
+
+  private static final IdGenerator<String> UUID_STRING_GENERATOR =
+      new IdGenerator<String>() {
+        @Override
+        public String next() {
+          return UUID.randomUUID().toString();
+        }
+      };
+
+  /**
+   * Returns a {@code Integer} ID generator whose next value is the value passed in. The value
+   * returned increases by one each time until {@code Integer.MAX_VALUE}. After that an {@code
+   * IdException} is thrown. This IdGenerator is threadsafe.
+   */
+  public static IdGenerator<Integer> newIntegerIdGenerator(int nextValue) {
+    checkArgument(nextValue >= 0, "ID values must be non-negative");
+    final AtomicInteger nextInt = new AtomicInteger(nextValue);
+    return new IdGenerator<Integer>() {
+      @Override
+      public Integer next() {
+        int value = nextInt.getAndIncrement();
+        if (value >= 0) {
+          return value;
+        }
+
+        // Make sure that all subsequent calls throw by setting to the most
+        // negative value possible.
+        nextInt.set(Integer.MIN_VALUE);
+        throw new IdException("Returned the last integer value available");
+      }
+    };
+  }
+
+  /**
+   * Returns a {@code Integer} ID generator whose next value is one. The value returned increases by
+   * one each time until {@code Integer.MAX_VALUE}. After that an {@code IdException} is thrown.
+   * This IdGenerator is threadsafe.
+   */
+  public static IdGenerator<Integer> newIntegerIdGenerator() {
+    return newIntegerIdGenerator(1);
+  }
+
+  /**
+   * Returns a {@code String} ID generator that passes ID requests to {@link UUID#randomUUID()},
+   * thereby generating type-4 (pseudo-randomly generated) UUIDs.
+   */
+  public static IdGenerator<String> randomUuidStringGenerator() {
+    return UUID_STRING_GENERATOR;
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java
new file mode 100644
index 0000000..028a780
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java
@@ -0,0 +1,145 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.jsonrpc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import android.util.Log;
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcRequest;
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcResponse;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import java.net.ConnectException;
+import java.net.URI;
+import java.util.concurrent.ConcurrentMap;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.WebSocket;
+import okhttp3.WebSocketListener;
+
+/**
+ * A client that can be used to talk to a WebSocket-based JSON-RPC server.
+ *
+ * <p>One {@code JsonRpcClient} instance is not supposed to be shared between multiple threads.
+ * Always create a new instance of {@code JsonRpcClient} for connecting to a new JSON-RPC URI, but
+ * try to reuse the {@link OkHttpClient} instance, which is thread-safe and maintains a thread pool
+ * in handling requests and responses.
+ */
+public class JsonRpcClient {
+
+  private static final String TAG = JsonRpcClient.class.getSimpleName();
+  private static final int NORMAL_CLOSURE_STATUS = 1000;
+
+  private final URI webSocketUri;
+  private final ConcurrentMap<String, SettableFuture<JsonRpcResponse>> responseFutures;
+  private WebSocket webSocketConn;
+
+  /** {@code client} can be shared between multiple {@code JsonRpcClient}s. */
+  public JsonRpcClient(OkHttpClient client, URI webSocketUri) {
+    this.webSocketUri = checkNotNull(webSocketUri, "WebSocket URL can't be null.");
+    responseFutures = Maps.newConcurrentMap();
+    connect(checkNotNull(client, "OkHttpClient can't be null."), webSocketUri);
+  }
+
+  private void connect(OkHttpClient client, URI webSocketUri) {
+    Request request = new Request.Builder().url(webSocketUri.toString()).build();
+    WebSocketListener webSocketListener = new WebSocketListenerImpl();
+    webSocketConn = client.newWebSocket(request, webSocketListener);
+  }
+
+  /** Closes the web socket connection. Non-blocking, and will return immediately. */
+  public void disconnect() {
+    if (webSocketConn != null) {
+      webSocketConn.close(NORMAL_CLOSURE_STATUS, "Client request closing. All requests handled.");
+    }
+  }
+
+  /**
+   * Sends a JSON-RPC request and returns a {@link ListenableFuture} with which the client could
+   * wait on response. If the {@code request} is a JSON-RPC notification, this method returns
+   * immediately with a {@code null} response.
+   *
+   * @param request the JSON-RPC request to be sent.
+   * @return a {@code ListenableFuture} representing pending completion of the request, or yields an
+   *     {@code ExecutionException}, which wraps a {@code ConnectException} if failed to send the
+   *     request.
+   */
+  public ListenableFuture<JsonRpcResponse> request(JsonRpcRequest request) {
+    checkNotNull(request, "JSON-RPC request shouldn't be null.");
+    if (Log.isLoggable(TAG, Log.DEBUG)) {
+      Log.d(
+          TAG,
+          String.format("JSON-RPC Request sent to uri %s: %s.", webSocketUri, request.toJson()));
+    }
+    if (webSocketConn == null) {
+      ConnectException e =
+          new ConnectException("WebSocket connection was not initiated correctly.");
+      return immediateFailedFuture(e);
+    }
+    synchronized (responseFutures) {
+      // Holding the lock of responseFutures for send-and-add operations, so that we could make sure
+      // to add its ListenableFuture to the responseFutures map before the thread of
+      // {@code WebSocketListenerImpl#onMessage} method queries the map.
+      boolean succeeded = webSocketConn.send(request.toJson());
+      if (!succeeded) {
+        ConnectException e = new ConnectException("Failed to send request: " + request);
+        return immediateFailedFuture(e);
+      }
+      if (isNullOrEmpty(request.getId())) {
+        // Request id is null or empty. This is a notification request, so returns immediately.
+        return immediateFuture(null);
+      } else {
+        SettableFuture<JsonRpcResponse> responseFuture = SettableFuture.create();
+        responseFutures.put(request.getId(), responseFuture);
+        return responseFuture;
+      }
+    }
+  }
+
+  /** A callback listener that handles incoming web socket messages. */
+  private class WebSocketListenerImpl extends WebSocketListener {
+    @Override
+    public void onMessage(WebSocket webSocket, String response) {
+      if (Log.isLoggable(TAG, Log.DEBUG)) {
+        Log.d(TAG, String.format("JSON-RPC response received: %s.", response));
+      }
+      JsonRpcResponse responseObj = JsonRpcResponse.fromJson(response);
+      synchronized (responseFutures) {
+        if (isNullOrEmpty(responseObj.getId())
+            || !responseFutures.containsKey(responseObj.getId())) {
+          Log.w(
+              TAG,
+              String.format(
+                  "Received a message with empty or unknown ID: %s. Drop the message.",
+                  responseObj.getId()));
+          return;
+        }
+        SettableFuture<JsonRpcResponse> responseFuture =
+            responseFutures.remove(responseObj.getId());
+        responseFuture.set(responseObj);
+      }
+    }
+
+    @Override
+    public void onClosing(WebSocket webSocket, int code, String reason) {
+      Log.d(
+          TAG,
+          String.format(
+              "Server requested connection close with code %d, reason: %s", code, reason));
+      webSocket.close(NORMAL_CLOSURE_STATUS, "Server requested closing connection.");
+    }
+
+    @Override
+    public void onFailure(WebSocket webSocket, Throwable t, Response response) {
+      Log.w(TAG, String.format("Failed to deliver message with error: %s.", t.getMessage()));
+      throw new RuntimeException("WebSocket request failure.", t);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java
new file mode 100644
index 0000000..af5c68e
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java
@@ -0,0 +1,60 @@
+package androidx.test.espresso.flutter.internal.jsonrpc.message;
+
+import com.google.gson.JsonObject;
+import java.util.Objects;
+
+/**
+ * A class for holding the error object in {@code JsonRpcResponse}.
+ *
+ * <p>See https://www.jsonrpc.org/specification#error_object for detailed specification.
+ */
+public class ErrorObject {
+  private final int code;
+  private final String message;
+  private final JsonObject data;
+
+  public ErrorObject(int code, String message) {
+    this(code, message, null);
+  }
+
+  public ErrorObject(int code, String message, JsonObject data) {
+    this.code = code;
+    this.message = message;
+    this.data = data;
+  }
+
+  /** Gets the error code. */
+  public int getCode() {
+    return code;
+  }
+
+  /** Gets the error message. */
+  public String getMessage() {
+    return message;
+  }
+
+  /** Gets the additional information about the error. Could be null. */
+  public JsonObject getData() {
+    return data;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ErrorObject) {
+      ErrorObject errorObject = (ErrorObject) obj;
+      return errorObject.code == this.code
+          && Objects.equals(errorObject.message, this.message)
+          && Objects.equals(errorObject.data, this.data);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = code;
+    hash = hash * 31 + Objects.hashCode(message);
+    hash = hash * 31 + Objects.hashCode(data);
+    return hash;
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java
new file mode 100644
index 0000000..fa03340
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java
@@ -0,0 +1,221 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.jsonrpc.message;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * JSON-RPC 2.0 request object.
+ *
+ * <p>See https://www.jsonrpc.org/specification for detailed specification.
+ */
+public final class JsonRpcRequest {
+
+  private static final Gson gson = new Gson();
+
+  private static final String JSON_RPC_VERSION = "2.0";
+
+  /** Specifying the version of the JSON-RPC protocol. Must be "2.0". */
+  @SerializedName("jsonrpc")
+  private final String version;
+
+  /**
+   * An identifier of the request. Could be String, a number, or null. In this implementation, we
+   * always use String as the type. If null, this is a notification and no response is required.
+   */
+  @Nullable private final String id;
+
+  /** A String containing the name of the method to be invoked. */
+  private final String method;
+
+  /** Parameter values to be used during the invocation of the method. */
+  private JsonObject params;
+
+  /**
+   * Deserializes the given Json string to a {@code JsonRpcRequest} object.
+   *
+   * @param jsonString the string from which the object is to be deserialized.
+   * @return the deserialized object.
+   */
+  public static JsonRpcRequest fromJson(String jsonString) {
+    checkArgument(!isNullOrEmpty(jsonString), "Json string cannot be null or empty.");
+    JsonRpcRequest request = gson.fromJson(jsonString, JsonRpcRequest.class);
+    checkState(JSON_RPC_VERSION.equals(request.getVersion()), "JSON-RPC version must be 2.0.");
+    checkState(
+        !isNullOrEmpty(request.getMethod()), "JSON-RPC request must contain the method field.");
+    return request;
+  }
+
+  /**
+   * Constructs with the given method name. The JSON-RPC version will be defaulted to "2.0".
+   *
+   * @param method the method name of this request.
+   */
+  private JsonRpcRequest(String method) {
+    this(null, method);
+  }
+
+  /**
+   * Constructs with the given id and method name. The JSON-RPC version will be defaulted to "2.0".
+   *
+   * @param id the id of this request.
+   * @param method the method name of this request.
+   */
+  private JsonRpcRequest(@Nullable String id, String method) {
+    this.version = JSON_RPC_VERSION;
+    this.id = id;
+    this.method = checkNotNull(method, "JSON-RPC request method cannot be null.");
+  }
+
+  /**
+   * Gets the JSON-RPC version.
+   *
+   * @return the JSON-RPC version. Should always be "2.0".
+   */
+  public String getVersion() {
+    return version;
+  }
+
+  /**
+   * Gets the id of this JSON-RPC request.
+   *
+   * @return the id of this request. Returns null if this is a notification request.
+   */
+  public String getId() {
+    return id;
+  }
+
+  /**
+   * Gets the method name of this JSON-RPC request.
+   *
+   * @return the method name.
+   */
+  public String getMethod() {
+    return method;
+  }
+
+  /** Gets the params used in this request. */
+  public JsonObject getParams() {
+    return params;
+  }
+
+  /**
+   * Serializes this object to its equivalent Json representation.
+   *
+   * @return the Json representation of this object.
+   */
+  public String toJson() {
+    return gson.toJson(this);
+  }
+
+  /**
+   * Equivalent to {@link #toJson()}.
+   *
+   * @return the Json representation of this object.
+   */
+  @Override
+  public String toString() {
+    return toJson();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof JsonRpcRequest) {
+      JsonRpcRequest objRequest = (JsonRpcRequest) obj;
+      return Objects.equals(objRequest.id, this.id)
+          && Objects.equals(objRequest.method, this.method)
+          && Objects.equals(objRequest.params, this.params);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = Objects.hashCode(id);
+    hash = hash * 31 + Objects.hashCode(method);
+    hash = hash * 31 + Objects.hashCode(params);
+    return hash;
+  }
+
+  /** Builder for {@link JsonRpcRequest}. */
+  public static class Builder {
+
+    /** The request id. Could be null if the request is a notification. */
+    @Nullable private String id;
+
+    /** A String containing the name of the method to be invoked. */
+    private String method;
+
+    /** Parameter values to be used during the invocation of the method. */
+    private JsonObject params = new JsonObject();
+
+    /** Empty constructor. */
+    public Builder() {}
+
+    /**
+     * Constructs an instance with the given method name.
+     *
+     * @param method the method name of this request builder.
+     */
+    public Builder(String method) {
+      this.method = method;
+    }
+
+    /** Sets the id of this request builder. */
+    public Builder setId(@Nullable String id) {
+      this.id = id;
+      return this;
+    }
+
+    /** Sets the method name of this request builder. */
+    public Builder setMethod(String method) {
+      this.method = method;
+      return this;
+    }
+
+    /** Sets the params of this request builder. */
+    public Builder setParams(JsonObject params) {
+      this.params = params;
+      return this;
+    }
+
+    /** Sugar method to add a {@code String} param to this request builder. */
+    public Builder addParam(String tag, String value) {
+      params.addProperty(tag, value);
+      return this;
+    }
+
+    /** Sugar method to add an integer param to this request builder. */
+    public Builder addParam(String tag, int value) {
+      params.addProperty(tag, value);
+      return this;
+    }
+
+    /** Sugar method to add a {@code boolean} param to this request builder. */
+    public Builder addParam(String tag, boolean value) {
+      params.addProperty(tag, value);
+      return this;
+    }
+
+    /** Builds and returns a {@code JsonRpcRequest} instance out of this builder. */
+    public JsonRpcRequest build() {
+      JsonRpcRequest request = new JsonRpcRequest(id, method);
+      if (params != null && params.size() != 0) {
+        request.params = this.params;
+      }
+      return request;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java
new file mode 100644
index 0000000..f845765
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java
@@ -0,0 +1,156 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.jsonrpc.message;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.SerializedName;
+import java.util.Objects;
+
+/**
+ * JSON-RPC 2.0 response object.
+ *
+ * <p>See https://www.jsonrpc.org/specification for detailed specification.
+ */
+public final class JsonRpcResponse {
+
+  private static final Gson gson = new Gson();
+
+  private static final String JSON_RPC_VERSION = "2.0";
+
+  /** Specifying the version of the JSON-RPC protocol. Must be "2.0". */
+  @SerializedName("jsonrpc")
+  private final String version;
+
+  /**
+   * Required. Must be the same as the value of the id in the corresponding JsonRpcRequest object.
+   */
+  private String id;
+
+  /** The result of the JSON-RPC call. Required on success. */
+  private JsonObject result;
+
+  /** Error occurred in the JSON-RPC call. Required on error. */
+  private ErrorObject error;
+
+  /**
+   * Deserializes the given Json string to a {@code JsonRpcResponse} object.
+   *
+   * @param jsonString the string from which the object is to be deserialized.
+   * @return the deserialized object.
+   */
+  public static JsonRpcResponse fromJson(String jsonString) {
+    checkArgument(!isNullOrEmpty(jsonString), "Json string cannot be null or empty.");
+    JsonRpcResponse response = gson.fromJson(jsonString, JsonRpcResponse.class);
+    checkState(!isNullOrEmpty(response.getId()));
+    checkState(JSON_RPC_VERSION.equals(response.getVersion()), "JSON-RPC version must be 2.0.");
+    return response;
+  }
+
+  /**
+   * Constructs with the given id and. The JSON-RPC version will be defaulted to "2.0".
+   *
+   * @param id the id of this response. Should be the same as the corresponding request.
+   */
+  public JsonRpcResponse(String id) {
+    this.version = JSON_RPC_VERSION;
+    setId(id);
+  }
+
+  /**
+   * Gets the JSON-RPC version.
+   *
+   * @return the JSON-RPC version. Should always be "2.0".
+   */
+  public String getVersion() {
+    return version;
+  }
+
+  /** Gets the id of this JSON-RPC response. */
+  public String getId() {
+    return id;
+  }
+
+  /**
+   * Sets the id of this JSON-RPC response.
+   *
+   * @param id the id to be set. Cannot be null.
+   */
+  public void setId(String id) {
+    this.id = checkNotNull(id);
+  }
+
+  /** Gets the result of this JSON-RPC response. Should be present on success. */
+  public JsonObject getResult() {
+    return result;
+  }
+
+  /**
+   * Sets the result of this JSON-RPC response.
+   *
+   * @param result
+   */
+  public void setResult(JsonObject result) {
+    this.result = result;
+  }
+
+  /** Gets the error object of this JSON-RPC response. Should be present on error. */
+  public ErrorObject getError() {
+    return error;
+  }
+
+  /**
+   * Sets the error object of this JSON-RPC response.
+   *
+   * @param error the error to be set.
+   */
+  public void setError(ErrorObject error) {
+    this.error = error;
+  }
+
+  /**
+   * Serializes this object to its equivalent Json representation.
+   *
+   * @return the Json representation of this object.
+   */
+  public String toJson() {
+    return gson.toJson(this);
+  }
+
+  /**
+   * Equivalent to {@link #toJson()}.
+   *
+   * @return the Json representation of this object.
+   */
+  @Override
+  public String toString() {
+    return toJson();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof JsonRpcResponse) {
+      JsonRpcResponse objResponse = (JsonRpcResponse) obj;
+      return Objects.equals(objResponse.id, this.id)
+          && Objects.equals(objResponse.result, this.result)
+          && Objects.equals(objResponse.error, this.error);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = Objects.hashCode(id);
+    hash = hash * 31 + Objects.hashCode(result);
+    hash = hash * 31 + Objects.hashCode(error);
+    return hash;
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java
new file mode 100644
index 0000000..da11fcc
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java
@@ -0,0 +1,377 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.util.concurrent.Futures.transform;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.graphics.Rect;
+import android.util.Log;
+import androidx.test.espresso.flutter.api.FlutterTestingProtocol;
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.internal.idgenerator.IdGenerator;
+import androidx.test.espresso.flutter.internal.jsonrpc.JsonRpcClient;
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcRequest;
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcResponse;
+import androidx.test.espresso.flutter.internal.protocol.impl.GetOffsetAction.OffsetType;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An implementation of the Espresso-Flutter testing protocol by using the testing APIs exposed by
+ * Dart VM service protocol.
+ *
+ * @see <a href="https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md">Dart VM
+ *     Service Protocol</a>.
+ */
+public final class DartVmService implements FlutterTestingProtocol {
+
+  private static final String TAG = DartVmService.class.getSimpleName();
+
+  private static final Gson gson =
+      new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+
+  /** Prefix to be attached to the JSON-RPC message id. */
+  private static final String MESSAGE_ID_PREFIX = "message-";
+
+  /** The JSON-RPC method for testing extension APIs. */
+  private static final String TESTING_EXTENSION_METHOD = "ext.flutter.driver";
+  /** The JSON-RPC method for retrieving Dart isolate info. */
+  private static final String GET_ISOLATE_METHOD = "getIsolate";
+  /** The JSON-RPC method for retrieving Dart VM info. */
+  private static final String GET_VM_METHOD = "getVM";
+
+  /** Json property name for the Dart VM isolate id. */
+  private static final String ISOLATE_ID_TAG = "isolateId";
+
+  private final JsonRpcClient client;
+  private final IdGenerator<Integer> messageIdGenerator;
+  private final String isolateId;
+  private final ListeningExecutorService taskExecutor;
+
+  /**
+   * Constructs a {@code DartVmService} instance that can be used to talk to the testing protocol
+   * exposed by Dart VM service extension protocol. It uses the given {@code isolateId} in all the
+   * JSON-RPC requests. It waits until the service extension protocol is in a usable state before
+   * returning.
+   *
+   * @param isolateId the Dart isolate ID to be used in the JSON-RPC requests sent to Dart VM
+   *     service protocol.
+   * @param jsonRpcClient a JSON-RPC web socket connection to send requests to the Dart VM service
+   *     protocol.
+   * @param messageIdGenerator an ID generator for generating the JSON-RPC request IDs.
+   * @param taskExecutor an executor for running async tasks.
+   */
+  public DartVmService(
+      String isolateId,
+      JsonRpcClient jsonRpcClient,
+      IdGenerator<Integer> messageIdGenerator,
+      ExecutorService taskExecutor) {
+    this.isolateId =
+        checkNotNull(
+            isolateId, "The ID of the Dart isolate that draws the Flutter UI shouldn't be null.");
+    this.client =
+        checkNotNull(
+            jsonRpcClient,
+            "The JsonRpcClient used to talk to Dart VM service protocol shouldn't be null.");
+    this.messageIdGenerator =
+        checkNotNull(
+            messageIdGenerator, "The id generator for generating request IDs shouldn't be null.");
+    this.taskExecutor = MoreExecutors.listeningDecorator(checkNotNull(taskExecutor));
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>This method ensures the Dart VM service is ready for use by checking:
+   *
+   * <ul>
+   *   <li>Dart VM Observatory is up and running.
+   *   <li>The Flutter testing API is registered with the running Dart VM service protocol.
+   * </ul>
+   */
+  @Override
+  @SuppressWarnings("unchecked")
+  public Future<Void> connect() {
+    return (Future<Void>) taskExecutor.submit(new IsDartVmServiceReady(isolateId, this));
+  }
+
+  @Override
+  public Future<Void> perform(
+      @Nullable final WidgetMatcher widgetMatcher, final SyntheticAction action) {
+    // Assumes all the actions require a response.
+    ListenableFuture<JsonRpcResponse> responseFuture =
+        client.request(getActionRequest(widgetMatcher, action));
+    Function<JsonRpcResponse, Void> resultTransformFunc =
+        new Function<JsonRpcResponse, Void>() {
+          public Void apply(JsonRpcResponse response) {
+            if (response.getError() == null) {
+              return null;
+            } else {
+              // TODO(https://github.com/android/android-test/issues/251): Update error case handling
+              // like
+              // AmbiguousWidgetMatcherException, NoMatchingWidgetException after nailing down the
+              // design with
+              // Flutter team.
+              throw new RuntimeException(
+                  String.format(
+                      "Error occurred when performing the given action %s on widget matched %s",
+                      action, widgetMatcher));
+            }
+          }
+        };
+    return transform(responseFuture, resultTransformFunc, directExecutor());
+  }
+
+  @Override
+  public Future<WidgetInfo> matchWidget(@Nonnull WidgetMatcher widgetMatcher) {
+    JsonRpcRequest request = getActionRequest(widgetMatcher, new GetWidgetDiagnosticsAction());
+    ListenableFuture<JsonRpcResponse> jsonResponseFuture = client.request(request);
+
+    Function<JsonRpcResponse, WidgetInfo> widgetInfoTransformer =
+        new Function<JsonRpcResponse, WidgetInfo>() {
+          public WidgetInfo apply(JsonRpcResponse jsonResponse) {
+            GetWidgetDiagnosticsResponse widgetDiagnostics =
+                GetWidgetDiagnosticsResponse.fromJsonRpcResponse(jsonResponse);
+            return WidgetInfoFactory.createWidgetInfo(widgetDiagnostics);
+          }
+        };
+    return transform(jsonResponseFuture, widgetInfoTransformer, directExecutor());
+  }
+
+  @Override
+  public Future<Rect> getLocalRect(@Nonnull WidgetMatcher widgetMatcher) {
+    ListenableFuture<JsonRpcResponse> topLeftFuture =
+        client.request(getActionRequest(widgetMatcher, new GetOffsetAction(OffsetType.TOP_LEFT)));
+    ListenableFuture<JsonRpcResponse> bottomRightFuture =
+        client.request(
+            getActionRequest(widgetMatcher, new GetOffsetAction(OffsetType.BOTTOM_RIGHT)));
+    ListenableFuture<List<JsonRpcResponse>> responses =
+        Futures.allAsList(topLeftFuture, bottomRightFuture);
+    Function<List<JsonRpcResponse>, Rect> rectTransformer =
+        new Function<List<JsonRpcResponse>, Rect>() {
+          public Rect apply(List<JsonRpcResponse> jsonResponses) {
+            GetOffsetResponse topLeft = GetOffsetResponse.fromJsonRpcResponse(jsonResponses.get(0));
+            GetOffsetResponse bottomRight =
+                GetOffsetResponse.fromJsonRpcResponse(jsonResponses.get(1));
+            checkState(
+                topLeft.getX() >= 0 && topLeft.getY() >= 0,
+                String.format(
+                    "The relative coordinates [%.1f, %.1f] of a widget's top left vertex cannot be"
+                        + " negative (negative means it's off the outer Flutter view)!",
+                    topLeft.getX(), topLeft.getY()));
+            checkState(
+                bottomRight.getX() >= 0 && bottomRight.getY() >= 0,
+                String.format(
+                    "The relative coordinates [%.1f, %.1f] of a widget's bottom right vertex cannot"
+                        + " be negative (negative means it's off the outer Flutter view)!",
+                    bottomRight.getX(), bottomRight.getY()));
+            checkState(
+                topLeft.getX() <= bottomRight.getX() && topLeft.getY() <= bottomRight.getY(),
+                String.format(
+                    "The coordinates of the bottom right vertex [%.1f, %.1f] are not actually to the"
+                        + " bottom right of the top left vertex [%.1f, %.1f]!",
+                    topLeft.getX(), topLeft.getY(), bottomRight.getX(), bottomRight.getY()));
+            return new Rect(
+                (int) topLeft.getX(),
+                (int) topLeft.getY(),
+                (int) bottomRight.getX(),
+                (int) bottomRight.getY());
+          }
+        };
+    return transform(responses, rectTransformer, directExecutor());
+  }
+
+  @Override
+  public Future<Void> waitUntilIdle() {
+    return perform(
+        null,
+        new WaitForConditionAction(
+            new NoPendingPlatformMessagesCondition(),
+            new NoTransientCallbacksCondition(),
+            new NoPendingFrameCondition()));
+  }
+
+  @Override
+  public void close() {
+    if (client != null) {
+      client.disconnect();
+    }
+  }
+
+  /** Queries the Dart isolate information. */
+  public ListenableFuture<JsonRpcResponse> getIsolateInfo() {
+    JsonRpcRequest getIsolateReq =
+        new JsonRpcRequest.Builder(GET_ISOLATE_METHOD)
+            .setId(getNextMessageId())
+            .addParam(ISOLATE_ID_TAG, isolateId)
+            .build();
+    return client.request(getIsolateReq);
+  }
+
+  /** Queries the Dart VM information. */
+  public ListenableFuture<GetVmResponse> getVmInfo() {
+    JsonRpcRequest getVmReq =
+        new JsonRpcRequest.Builder(GET_VM_METHOD).setId(getNextMessageId()).build();
+    ListenableFuture<JsonRpcResponse> jsonGetVmResp = client.request(getVmReq);
+    Function<JsonRpcResponse, GetVmResponse> jsonToResponse =
+        new Function<JsonRpcResponse, GetVmResponse>() {
+          public GetVmResponse apply(JsonRpcResponse jsonResp) {
+            return GetVmResponse.fromJsonRpcResponse(jsonResp);
+          }
+        };
+    return transform(jsonGetVmResp, jsonToResponse, directExecutor());
+  }
+
+  /** Gets the next usable message id. */
+  private String getNextMessageId() {
+    return MESSAGE_ID_PREFIX + messageIdGenerator.next();
+  }
+
+  /** Constructs a {@code JsonRpcRequest} based on the given matcher and action. */
+  private JsonRpcRequest getActionRequest(WidgetMatcher widgetMatcher, SyntheticAction action) {
+    checkNotNull(action, "Action cannot be null.");
+    // Assumes all the actions require a response.
+    return new JsonRpcRequest.Builder(TESTING_EXTENSION_METHOD)
+        .setId(getNextMessageId())
+        .setParams(constructParams(isolateId, widgetMatcher, action))
+        .build();
+  }
+
+  /** Constructs the JSON-RPC request params. */
+  private static JsonObject constructParams(
+      String isolateId, WidgetMatcher widgetMatcher, SyntheticAction action) {
+    JsonObject paramObject = new JsonObject();
+    paramObject.addProperty(ISOLATE_ID_TAG, isolateId);
+    if (widgetMatcher != null) {
+      paramObject = merge(paramObject, (JsonObject) gson.toJsonTree(widgetMatcher));
+    }
+    paramObject = merge(paramObject, (JsonObject) gson.toJsonTree(action));
+    return paramObject;
+  }
+
+  /**
+   * Returns a merged {@code JsonObject} of the two given {@code JsonObject}s, or an empty {@code
+   * JsonObject} if both of the objects to be merged are null.
+   */
+  private static JsonObject merge(@Nullable JsonObject obj1, @Nullable JsonObject obj2) {
+    JsonObject result = new JsonObject();
+    mergeTo(result, obj1);
+    mergeTo(result, obj2);
+    return result;
+  }
+
+  private static void mergeTo(JsonObject obj, @Nullable JsonObject toBeMerged) {
+    if (toBeMerged != null) {
+      for (Map.Entry<String, JsonElement> entry : toBeMerged.entrySet()) {
+        obj.add(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+
+  /** A {@link Runnable} that waits until the Dart VM testing extension is ready for use. */
+  static class IsDartVmServiceReady implements Runnable {
+
+    /** Maximum number of retries for checking extension APIs' availability. */
+    private static final int EXTENSION_API_CHECKING_RETRIES = 5;
+
+    /** Json param name for retrieving all the available extension APIs. */
+    private static final String EXTENSION_RPCS_TAG = "extensionRPCs";
+
+    private final String isolateId;
+    private final DartVmService dartVmService;
+
+    IsDartVmServiceReady(String isolateId, DartVmService dartVmService) {
+      this.isolateId = checkNotNull(isolateId);
+      this.dartVmService = checkNotNull(dartVmService);
+    }
+
+    @Override
+    public void run() {
+      waitForTestingApiRegistered();
+    }
+
+    /**
+     * Blocks until the Flutter testing/driver API is registered with the running Dart VM service
+     * protocol by querying whether it's listed in the isolate's 'extensionRPCs'.
+     */
+    @VisibleForTesting
+    void waitForTestingApiRegistered() {
+      int retries = EXTENSION_API_CHECKING_RETRIES;
+      boolean isApiRegistered = false;
+      do {
+        retries--;
+        try {
+          JsonRpcResponse isolateResp = dartVmService.getIsolateInfo().get();
+          isApiRegistered = isTestingApiRegistered(isolateResp);
+        } catch (ExecutionException e) {
+          Log.d(
+              TAG,
+              "Error occurred during retrieving Dart isolate information. Retry.",
+              e.getCause());
+          continue;
+        } catch (InterruptedException e) {
+          Log.d(
+              TAG,
+              "InterruptedException occurred during retrieving Dart isolate information. Retry.",
+              e);
+          Thread.currentThread().interrupt(); // Restores the interrupted status.
+          continue;
+        }
+      } while (!isApiRegistered && retries > 0);
+
+      if (!isApiRegistered) {
+        throw new FlutterProtocolException(
+            String.format("Flutter testing APIs not registered with Dart isolate %s.", isolateId));
+      }
+    }
+
+    @VisibleForTesting
+    boolean isTestingApiRegistered(JsonRpcResponse isolateInfoResp) {
+      if (isolateInfoResp == null
+          || isolateInfoResp.getError() != null
+          || isolateInfoResp.getResult() == null) {
+        Log.w(
+            TAG,
+            String.format(
+                "Error occurred in JSON-RPC response when querying isolate info for %s: %s.",
+                isolateId, isolateInfoResp.getError()));
+        return false;
+      }
+      Iterator<JsonElement> extensions =
+          isolateInfoResp.getResult().get(EXTENSION_RPCS_TAG).getAsJsonArray().iterator();
+      while (extensions.hasNext()) {
+        String extensionApi = extensions.next().getAsString();
+        if (TESTING_EXTENSION_METHOD.equals(extensionApi)) {
+          Log.d(
+              TAG,
+              String.format("Flutter testing API registered with Dart isolate %s.", isolateId));
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java
new file mode 100644
index 0000000..2cf41f1
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java
@@ -0,0 +1,94 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import android.util.Log;
+import android.view.View;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/** Util class for dealing with Dart VM service protocols. */
+public final class DartVmServiceUtil {
+  private static final String TAG = DartVmServiceUtil.class.getSimpleName();
+
+  /**
+   * Converts the Dart VM observatory http server URL to the service protocol WebSocket URL.
+   *
+   * @param observatoryUrl The Dart VM http server URL that can be converted to a service protocol
+   *     URI.
+   */
+  public static URI getServiceProtocolUri(String observatoryUrl) {
+    if (isNullOrEmpty(observatoryUrl)) {
+      throw new RuntimeException(
+          "Dart VM Observatory is not enabled. "
+              + "Please make sure your Flutter app is running under debug mode.");
+    }
+
+    try {
+      new URL(observatoryUrl);
+    } catch (MalformedURLException e) {
+      throw new RuntimeException(
+          String.format("Dart VM Observatory url %s is malformed.", observatoryUrl), e);
+    }
+
+    // Constructs the service protocol URL based on the Observatory http url.
+    // For example, http://127.0.0.1:39694/qsnVeidc78Y=/ -> ws://127.0.0.1:39694/qsnVeidc78Y=/ws.
+    int schemaIndex = observatoryUrl.indexOf(":");
+    String serviceProtocolUri = "ws" + observatoryUrl.substring(schemaIndex);
+    if (!observatoryUrl.endsWith("/")) {
+      serviceProtocolUri += "/";
+    }
+    serviceProtocolUri += "ws";
+
+    Log.i(TAG, "Dart VM service protocol runs at uri: " + serviceProtocolUri);
+    try {
+      return new URI(serviceProtocolUri);
+    } catch (URISyntaxException e) {
+      // Should never happen.
+      throw new RuntimeException("Illegal Dart VM service protocol URI: " + serviceProtocolUri, e);
+    }
+  }
+
+  /** Gets the Dart isolate ID for the given {@code flutterView}. */
+  public static String getDartIsolateId(View flutterView) {
+    checkNotNull(flutterView, "The Flutter View instance cannot be null.");
+    String uiIsolateId = getDartExecutor(flutterView).getIsolateServiceId();
+    Log.d(
+        TAG,
+        String.format(
+            "Dart isolate ID for the Flutter View [id: %d]: %s.",
+            flutterView.getId(), uiIsolateId));
+    return uiIsolateId;
+  }
+
+  /** Gets the Dart executor for the given {@code flutterView}. */
+  public static DartExecutor getDartExecutor(View flutterView) {
+    checkNotNull(flutterView, "The Flutter View instance cannot be null.");
+    // Flutter's embedding is in the phase of rewriting/refactoring. Let's be compatible with both
+    // the old and the new FlutterView classes.
+    if (flutterView instanceof io.flutter.view.FlutterView) {
+      return ((io.flutter.view.FlutterView) flutterView).getDartExecutor();
+    } else if (flutterView instanceof io.flutter.embedding.android.FlutterView) {
+      FlutterEngine flutterEngine =
+          ((io.flutter.embedding.android.FlutterView) flutterView).getAttachedFlutterEngine();
+      if (flutterEngine == null) {
+        throw new FlutterProtocolException(
+            String.format(
+                "No Flutter engine attached to the Flutter view [id: %d].", flutterView.getId()));
+      }
+      return flutterEngine.getDartExecutor();
+    } else {
+      throw new FlutterProtocolException(
+          String.format("This is not a Flutter View instance [id: %d].", flutterView.getId()));
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java
new file mode 100644
index 0000000..71cdb26
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java
@@ -0,0 +1,21 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+/** Represents an exception/error relevant to Dart VM service. */
+public final class FlutterProtocolException extends RuntimeException {
+
+  public FlutterProtocolException(String message) {
+    super(message);
+  }
+
+  public FlutterProtocolException(Throwable t) {
+    super(t);
+  }
+
+  public FlutterProtocolException(String message, Throwable t) {
+    super(message, t);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java
new file mode 100644
index 0000000..9b92f67
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java
@@ -0,0 +1,69 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import com.google.common.base.Ascii;
+import com.google.gson.annotations.Expose;
+
+/** An action that retrieves the widget offset coordinates to the outer Flutter view. */
+final class GetOffsetAction extends SyntheticAction {
+
+  /** The position of the offset coordinates. */
+  public enum OffsetType {
+    TOP_LEFT("topLeft"),
+    TOP_RIGHT("topRight"),
+    BOTTOM_LEFT("bottomLeft"),
+    BOTTOM_RIGHT("bottomRight");
+
+    private OffsetType(String type) {
+      this.type = type;
+    }
+
+    private final String type;
+
+    @Override
+    public String toString() {
+      return type;
+    }
+
+    public static OffsetType fromString(String typeString) {
+      if (typeString == null) {
+        return null;
+      }
+      for (OffsetType offsetType : OffsetType.values()) {
+        if (Ascii.equalsIgnoreCase(offsetType.type, typeString)) {
+          return offsetType;
+        }
+      }
+      return null;
+    }
+  }
+
+  @Expose private final String offsetType;
+
+  /**
+   * Constructor.
+   *
+   * @param type the vertex position.
+   */
+  public GetOffsetAction(OffsetType type) {
+    super("get_offset");
+    this.offsetType = checkNotNull(type).toString();
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param type the vertex position.
+   * @param timeOutInMillis action's timeout setting in milliseconds.
+   */
+  public GetOffsetAction(OffsetType type, long timeOutInMillis) {
+    super("get_offset", timeOutInMillis);
+    this.offsetType = checkNotNull(type).toString();
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java
new file mode 100644
index 0000000..52fcd4c
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java
@@ -0,0 +1,140 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcResponse;
+import androidx.test.espresso.flutter.internal.protocol.impl.GetOffsetAction.OffsetType;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.Expose;
+
+/**
+ * Represents the {@code result} section in a {@code JsonRpcResponse} that's the response of a
+ * {@code GetOffsetAction}.
+ */
+final class GetOffsetResponse {
+
+  private static final Gson gson = new Gson();
+
+  @Expose private boolean isError;
+  @Expose private Coordinates response;
+  @Expose private String type;
+
+  private GetOffsetResponse() {}
+
+  /**
+   * Builds the {@code GetOffsetResponse} out of the JSON-RPC response.
+   *
+   * @param jsonRpcResponse the JSON-RPC response. Cannot be {@code null}.
+   * @return a {@code GetOffsetResponse} instance that's parsed out from the JSON-RPC response.
+   */
+  public static GetOffsetResponse fromJsonRpcResponse(JsonRpcResponse jsonRpcResponse) {
+    checkNotNull(jsonRpcResponse, "The JSON-RPC response cannot be null.");
+    if (jsonRpcResponse.getResult() == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving a Flutter widget's geometry info. Response"
+                  + " received: %s.",
+              jsonRpcResponse));
+    }
+    try {
+      return gson.fromJson(jsonRpcResponse.getResult(), GetOffsetResponse.class);
+    } catch (JsonSyntaxException e) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving a Flutter widget's geometry info. Response"
+                  + " received: %s.",
+              jsonRpcResponse),
+          e);
+    }
+  }
+
+  /** Returns whether this is an error response. */
+  public boolean isError() {
+    return isError;
+  }
+
+  /** Returns the vertex position. */
+  public OffsetType getType() {
+    return OffsetType.fromString(type);
+  }
+
+  /** Returns the X-Coordinate. */
+  public float getX() {
+    if (response == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving a Flutter widget's geometry info. Response"
+                  + " received: %s",
+              this));
+    } else {
+      return response.dx;
+    }
+  }
+
+  /** Returns the Y-Coordinate. */
+  public float getY() {
+    if (response == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving a Flutter widget's geometry info. Response"
+                  + " received: %s",
+              this));
+    } else {
+      return response.dy;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return gson.toJson(this);
+  }
+
+  static class Coordinates {
+
+    @Expose private float dx;
+    @Expose private float dy;
+
+    Coordinates() {}
+
+    Coordinates(float dx, float dy) {
+      this.dx = dx;
+      this.dy = dy;
+    }
+  }
+
+  static class Builder {
+    private boolean isError;
+    private Coordinates coordinate;
+    private OffsetType type;
+
+    public Builder() {}
+
+    public Builder setIsError(boolean isError) {
+      this.isError = isError;
+      return this;
+    }
+
+    public Builder setCoordinates(float dx, float dy) {
+      this.coordinate = new Coordinates(dx, dy);
+      return this;
+    }
+
+    public Builder setType(OffsetType type) {
+      this.type = checkNotNull(type);
+      return this;
+    }
+
+    public GetOffsetResponse build() {
+      GetOffsetResponse response = new GetOffsetResponse();
+      response.isError = this.isError;
+      response.response = this.coordinate;
+      response.type = checkNotNull(type).toString();
+      return response;
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java
new file mode 100644
index 0000000..2fe0d44
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetVmResponse.java
@@ -0,0 +1,127 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcResponse;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.Expose;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a response of a <a
+ * href="https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getvm">getVM()</a>
+ * request.
+ */
+public class GetVmResponse {
+
+  private static final Gson gson = new Gson();
+
+  @Expose private List<Isolate> isolates;
+
+  private GetVmResponse() {}
+
+  /**
+   * Builds the {@code GetVmResponse} out of the JSON-RPC response.
+   *
+   * @param jsonRpcResponse the JSON-RPC response. Cannot be {@code null}.
+   * @return a {@code GetVmResponse} instance that's parsed out from the JSON-RPC response.
+   */
+  public static GetVmResponse fromJsonRpcResponse(JsonRpcResponse jsonRpcResponse) {
+    checkNotNull(jsonRpcResponse, "The JSON-RPC response cannot be null.");
+    if (jsonRpcResponse.getResult() == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving Dart VM info. Response received: %s.",
+              jsonRpcResponse));
+    }
+    try {
+      return gson.fromJson(jsonRpcResponse.getResult(), GetVmResponse.class);
+    } catch (JsonSyntaxException e) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving Dart VM info. Response received: %s.",
+              jsonRpcResponse),
+          e);
+    }
+  }
+
+  /** Returns the number of isolates living in the Dart VM. */
+  public int getIsolateNum() {
+    return isolates == null ? 0 : isolates.size();
+  }
+
+  /** Returns the Dart isolate listed at the given index. */
+  public Isolate getIsolate(int index) {
+    if (isolates == null) {
+      return null;
+    } else if (index < 0 || index >= isolates.size()) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Illegal Dart isolate index: %d. Should be in the range [%d, %d]",
+              index, 0, isolates.size() - 1));
+    } else {
+      return isolates.get(index);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return gson.toJson(this);
+  }
+
+  /** Represents a Dart isolate. */
+  static class Isolate {
+
+    @Expose private String id;
+    @Expose private boolean runnable;
+    @Expose private List<String> extensionRpcList;
+
+    Isolate() {}
+
+    Isolate(String id, boolean runnable) {
+      this.id = id;
+      this.runnable = runnable;
+    }
+
+    /** Gets the Dart isolate ID. */
+    public String getId() {
+      return id;
+    }
+
+    /**
+     * Checks whether the Dart isolate is in a runnable state. True if it's runnable, false
+     * otherwise.
+     */
+    public boolean isRunnable() {
+      return runnable;
+    }
+
+    /** Gets the list of extension RPCs registered at this Dart isolate. Could be {@code null}. */
+    public List<String> getExtensionRpcList() {
+      return extensionRpcList;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof Isolate) {
+        Isolate isolate = (Isolate) obj;
+        return Objects.equals(isolate.id, this.id)
+            && Objects.equals(isolate.runnable, this.runnable)
+            && Objects.equals(isolate.extensionRpcList, this.extensionRpcList);
+      } else {
+        return false;
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, runnable, extensionRpcList);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java
new file mode 100644
index 0000000..5982ee4
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import com.google.gson.annotations.Expose;
+
+/** Represents an action that retrieves the Flutter widget's diagnostics information. */
+final class GetWidgetDiagnosticsAction extends SyntheticAction {
+
+  @Expose private final String diagnosticsType = "widget";
+
+  /**
+   * Sets the depth of the retrieved diagnostics tree as 0. This means only the information of the
+   * root widget will be retrieved.
+   */
+  @Expose private final int subtreeDepth = 0;
+
+  /** Always includes the diagnostics properties of this widget. */
+  @Expose private final boolean includeProperties = true;
+
+  GetWidgetDiagnosticsAction() {
+    super("get_diagnostics_tree");
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java
new file mode 100644
index 0000000..65a456c
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java
@@ -0,0 +1,189 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.util.Log;
+import androidx.test.espresso.flutter.internal.jsonrpc.message.JsonRpcResponse;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Ascii;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents a response of the {@code GetWidgetDiagnosticsAction}. */
+final class GetWidgetDiagnosticsResponse {
+
+  private static final String TAG = GetWidgetDiagnosticsResponse.class.getSimpleName();
+  private static final Gson gson = new Gson();
+
+  @Expose private boolean isError;
+
+  @Expose
+  @SerializedName("response")
+  private DiagnosticNodeInfo widgetInfo;
+
+  private GetWidgetDiagnosticsResponse() {}
+
+  /**
+   * Builds the {@code GetWidgetDiagnosticsResponse} out of the JSON-RPC response.
+   *
+   * @param jsonRpcResponse the JSON-RPC response. Cannot be {@code null}.
+   * @return a {@code GetWidgetDiagnosticsResponse} instance that's parsed out from the JSON-RPC
+   *     response.
+   */
+  public static GetWidgetDiagnosticsResponse fromJsonRpcResponse(JsonRpcResponse jsonRpcResponse) {
+    checkNotNull(jsonRpcResponse, "The JSON-RPC response cannot be null.");
+    if (jsonRpcResponse.getResult() == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving widget's diagnostics info. Response received: %s.",
+              jsonRpcResponse));
+    }
+    try {
+      return gson.fromJson(jsonRpcResponse.getResult(), GetWidgetDiagnosticsResponse.class);
+    } catch (JsonSyntaxException e) {
+      throw new FlutterProtocolException(
+          String.format(
+              "Error occurred during retrieving widget's diagnostics info. Response received: %s.",
+              jsonRpcResponse),
+          e);
+    }
+  }
+
+  /** Returns whether this is an error response. */
+  public boolean isError() {
+    return isError;
+  }
+
+  /** Returns the runtime type of this widget, or {@code null} if the type info is not available. */
+  public String getRuntimeType() {
+    if (widgetInfo == null) {
+      Log.w(TAG, "Widget info is null.");
+      return null;
+    } else {
+      return widgetInfo.runtimeType;
+    }
+  }
+
+  /**
+   * Gets the widget property by its name, or null if the property doesn't exist.
+   *
+   * @param propertyName the property name. Cannot be {@code null}.
+   */
+  public WidgetProperty getPropertyByName(String propertyName) {
+    checkNotNull(propertyName, "Widget property name cannot be null.");
+    if (widgetInfo == null) {
+      Log.w(TAG, "Widget info is null.");
+      return null;
+    }
+    return widgetInfo.getPropertyByName(propertyName);
+  }
+
+  /**
+   * Returns the description of this widget, or {@code null} if the diagnostics info is not
+   * available.
+   */
+  public String getDescription() {
+    if (widgetInfo == null) {
+      Log.w(TAG, "Widget info is null.");
+      return null;
+    }
+    return widgetInfo.description;
+  }
+
+  /**
+   * Returns whether this widget has children, or {@code false} if the diagnostics info is not
+   * available.
+   */
+  public boolean isHasChildren() {
+    if (widgetInfo == null) {
+      Log.w(TAG, "Widget info is null.");
+      return false;
+    }
+    return widgetInfo.hasChildren;
+  }
+
+  @Override
+  public String toString() {
+    return gson.toJson(this);
+  }
+
+  /** A data structure that holds a widget's diagnostics info. */
+  static class DiagnosticNodeInfo {
+
+    @Expose
+    @SerializedName("widgetRuntimeType")
+    private String runtimeType;
+
+    @Expose private List<WidgetProperty> properties;
+    @Expose private String description;
+    @Expose private boolean hasChildren;
+
+    WidgetProperty getPropertyByName(String propertyName) {
+      checkNotNull(propertyName, "Widget property name cannot be null.");
+      if (properties == null) {
+        Log.w(TAG, "Widget property list is null.");
+        return null;
+      }
+      for (WidgetProperty property : properties) {
+        if (Ascii.equalsIgnoreCase(propertyName, property.getName())) {
+          return property;
+        }
+      }
+      return null;
+    }
+  }
+
+  /** Represents a widget property. */
+  static class WidgetProperty {
+    @Expose private final String name;
+    @Expose private final String value;
+    @Expose private final String description;
+
+    @VisibleForTesting
+    WidgetProperty(String name, String value, String description) {
+      this.name = name;
+      this.value = value;
+      this.description = description;
+    }
+
+    /** Returns the name of this widget property. */
+    public String getName() {
+      return name;
+    }
+
+    /** Returns the value of this widget property. */
+    public String getValue() {
+      return value;
+    }
+
+    /** Returns the description of this widget property. */
+    public String getDescription() {
+      return description;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof WidgetProperty)) {
+        return false;
+      } else {
+        WidgetProperty widgetProperty = (WidgetProperty) obj;
+        return Objects.equals(this.name, widgetProperty.name)
+            && Objects.equals(this.value, widgetProperty.value)
+            && Objects.equals(this.description, widgetProperty.description);
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, value, description);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java
new file mode 100644
index 0000000..7e7739b
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java
@@ -0,0 +1,15 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+/**
+ * Represents a condition that waits until no pending frame is scheduled in the Flutter framework.
+ */
+class NoPendingFrameCondition extends WaitCondition {
+
+  public NoPendingFrameCondition() {
+    super("NoPendingFrameCondition");
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java
new file mode 100644
index 0000000..8430ee2
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java
@@ -0,0 +1,16 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+/**
+ * Represents a condition that waits until there are no pending platform messages in the Flutter's
+ * platform channels.
+ */
+class NoPendingPlatformMessagesCondition extends WaitCondition {
+
+  public NoPendingPlatformMessagesCondition() {
+    super("NoPendingPlatformMessagesCondition");
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java
new file mode 100644
index 0000000..4548b28
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java
@@ -0,0 +1,13 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+/** Represents a condition that waits until no transient callbacks in the Flutter framework. */
+class NoTransientCallbacksCondition extends WaitCondition {
+
+  public NoTransientCallbacksCondition() {
+    super("NoTransientCallbacksCondition");
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java
new file mode 100644
index 0000000..7017e88
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java
@@ -0,0 +1,18 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/** The base class that represents a wait condition in the Flutter app. */
+abstract class WaitCondition {
+  // Used in JSON serialization.
+  @SuppressWarnings("unused")
+  private final String conditionName;
+
+  public WaitCondition(String conditionName) {
+    this.conditionName = checkNotNull(conditionName);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java
new file mode 100644
index 0000000..efbe588
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java
@@ -0,0 +1,33 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.SyntheticAction;
+import com.google.gson.Gson;
+import com.google.gson.annotations.Expose;
+
+/**
+ * Represents an action that waits until the specified conditions have been met in the Flutter app.
+ */
+final class WaitForConditionAction extends SyntheticAction {
+
+  private static final Gson gson = new Gson();
+
+  @Expose private final String conditionName = "CombinedCondition";
+
+  @Expose private final String conditions;
+
+  /**
+   * Creates with the given wait conditions.
+   *
+   * @param waitConditions the conditions that this action shall wait for. Cannot be null.
+   */
+  public WaitForConditionAction(WaitCondition... waitConditions) {
+    super("waitForCondition");
+    conditions = gson.toJson(checkNotNull(waitConditions));
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java
new file mode 100644
index 0000000..2353577
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java
@@ -0,0 +1,91 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.internal.protocol.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.util.Log;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import androidx.test.espresso.flutter.model.WidgetInfoBuilder;
+
+/** A factory that creates {@link WidgetInfo} instances. */
+final class WidgetInfoFactory {
+
+  private static final String TAG = WidgetInfoFactory.class.getSimpleName();
+
+  private enum WidgetRuntimeType {
+    TEXT("Text"),
+    RICH_TEXT("RichText"),
+    UNKNOWN("Unknown");
+
+    private WidgetRuntimeType(String typeString) {
+      this.type = typeString;
+    }
+
+    private final String type;
+
+    @Override
+    public String toString() {
+      return type;
+    }
+
+    public static WidgetRuntimeType getType(String typeString) {
+      for (WidgetRuntimeType widgetType : WidgetRuntimeType.values()) {
+        if (widgetType.type.equals(typeString)) {
+          return widgetType;
+        }
+      }
+      return UNKNOWN;
+    }
+  }
+
+  /**
+   * Creates a {@code WidgetInfo} instance based on the given diagnostics info.
+   *
+   * <p>The current implementation is ugly. As the widget's properties are serialized out as JSON
+   * strings, we have to inspect the content based on the widget type.
+   *
+   * @throws FlutterProtocolException when the given {@code widgetDiagnostics} is invalid.
+   */
+  public static WidgetInfo createWidgetInfo(GetWidgetDiagnosticsResponse widgetDiagnostics) {
+    checkNotNull(widgetDiagnostics, "The widget diagnostics instance is null.");
+    WidgetInfoBuilder widgetInfo = new WidgetInfoBuilder();
+    if (widgetDiagnostics.getRuntimeType() == null) {
+      throw new FlutterProtocolException(
+          String.format(
+              "The widget diagnostics info must contain the runtime type of the widget. Illegal"
+                  + " widget diagnostics info: %s.",
+              widgetDiagnostics));
+    }
+    widgetInfo.setRuntimeType(widgetDiagnostics.getRuntimeType());
+
+    // Ugly, but let's figure out a better way as this evolves.
+    switch (WidgetRuntimeType.getType(widgetDiagnostics.getRuntimeType())) {
+      case TEXT:
+        // Flutter Text Widget's "data" field stores the text info.
+        if (widgetDiagnostics.getPropertyByName("data") != null) {
+          String text = widgetDiagnostics.getPropertyByName("data").getValue();
+          widgetInfo.setText(text);
+        }
+        break;
+      case RICH_TEXT:
+        if (widgetDiagnostics.getPropertyByName("text") != null) {
+          String richText = widgetDiagnostics.getPropertyByName("text").getValue();
+          widgetInfo.setText(richText);
+        }
+        break;
+      default:
+        // Let's be silent when we know little about the widget's type.
+        // The widget's fields will be mostly empty but it can be used for checking the existence
+        // of the widget.
+        Log.i(
+            TAG,
+            String.format(
+                "Unknown widget type: %s. Widget diagnostics info: %s.",
+                widgetDiagnostics.getRuntimeType(), widgetDiagnostics));
+    }
+    return widgetInfo.build();
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java
new file mode 100644
index 0000000..5a272f2
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java
@@ -0,0 +1,105 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import android.view.View;
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import io.flutter.embedding.android.FlutterView;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/** A collection of matchers that match a Flutter view or Flutter widgets. */
+public final class FlutterMatchers {
+
+  /**
+   * Returns a matcher that matches a {@link FlutterView} or a legacy {@code
+   * io.flutter.view.FlutterView}.
+   */
+  public static Matcher<View> isFlutterView() {
+    return new IsFlutterViewMatcher();
+  }
+
+  /**
+   * Returns a matcher that matches a Flutter widget's tooltip.
+   *
+   * @param tooltip the tooltip String to match. Cannot be {@code null}.
+   */
+  public static WidgetMatcher withTooltip(@Nonnull String tooltip) {
+    return new WithTooltipMatcher(tooltip);
+  }
+
+  /**
+   * Returns a matcher that matches a Flutter widget's value key.
+   *
+   * @param valueKey the value key String to match. Cannot be {@code null}.
+   */
+  public static WidgetMatcher withValueKey(@Nonnull String valueKey) {
+    return new WithValueKeyMatcher(valueKey);
+  }
+
+  /**
+   * Returns a matcher that matches a Flutter widget's runtime type.
+   *
+   * <p>Usage:
+   *
+   * <p>{@code withType("TextField")} can be used to match a Flutter <a
+   * href="https://api.flutter.dev/flutter/material/TextField-class.html">TextField</a> widget.
+   *
+   * @param type the type String to match. Cannot be {@code null}.
+   */
+  public static WidgetMatcher withType(@Nonnull String type) {
+    return new WithTypeMatcher(type);
+  }
+
+  /**
+   * Returns a matcher that matches a Flutter widget's text.
+   *
+   * @param text the text String to match. Cannot be {@code null}.
+   */
+  public static WidgetMatcher withText(@Nonnull String text) {
+    return new WithTextMatcher(text);
+  }
+
+  /**
+   * Returns a matcher that matches a Flutter widget based on the given ancestor matcher.
+   *
+   * @param ancestorMatcher the ancestor to match on. Cannot be null.
+   * @param widgetMatcher the widget to match on. Cannot be null.
+   */
+  public static WidgetMatcher isDescendantOf(
+      @Nonnull WidgetMatcher ancestorMatcher, @Nonnull WidgetMatcher widgetMatcher) {
+    return new IsDescendantOfMatcher(ancestorMatcher, widgetMatcher);
+  }
+
+  /**
+   * Returns a matcher that checks the existence of a Flutter widget.
+   *
+   * <p>Note, this matcher only guarantees that the widget exists in Flutter's widget tree, but not
+   * necessarily displayed on screen, e.g. the widget is in the cache extend of a Scrollable, but
+   * not scrolled onto the screen.
+   */
+  public static Matcher<WidgetInfo> isExisting() {
+    return new IsExistingMatcher();
+  }
+
+  static final class IsFlutterViewMatcher extends TypeSafeMatcher<View> {
+
+    private IsFlutterViewMatcher() {}
+
+    @Override
+    public void describeTo(Description description) {
+      description.appendText("is a FlutterView");
+    }
+
+    @Override
+    public boolean matchesSafely(View flutterView) {
+      return flutterView instanceof FlutterView
+          || (flutterView instanceof io.flutter.view.FlutterView);
+    }
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java
new file mode 100644
index 0000000..24a4415
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java
@@ -0,0 +1,75 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+
+/** A matcher that matches a Flutter widget with a given ancestor. */
+public final class IsDescendantOfMatcher extends WidgetMatcher {
+
+  private static final Gson gson =
+      new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+
+  private final WidgetMatcher ancestorMatcher;
+  private final WidgetMatcher widgetMatcher;
+
+  // Flutter Driver extension APIs only support JSON strings, not other JSON structures.
+  // Thus, explicitly convert the matchers to JSON strings.
+  @SerializedName("of")
+  @Expose
+  private final String jsonAncestorMatcher;
+
+  @SerializedName("matching")
+  @Expose
+  private final String jsonWidgetMatcher;
+
+  IsDescendantOfMatcher(
+      @Nonnull WidgetMatcher ancestorMatcher, @Nonnull WidgetMatcher widgetMatcher) {
+    super("Descendant");
+    this.ancestorMatcher = checkNotNull(ancestorMatcher);
+    this.widgetMatcher = checkNotNull(widgetMatcher);
+    jsonAncestorMatcher = gson.toJson(ancestorMatcher);
+    jsonWidgetMatcher = gson.toJson(widgetMatcher);
+  }
+
+  /** Returns the matcher to match the widget's ancestor. */
+  public WidgetMatcher getAncestorMatcher() {
+    return ancestorMatcher;
+  }
+
+  /** Returns the matcher to match the widget itself. */
+  public WidgetMatcher getWidgetMatcher() {
+    return widgetMatcher;
+  }
+
+  @Override
+  public String toString() {
+    return "matched with " + widgetMatcher + " with ancestor: " + ancestorMatcher;
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    // TODO: Using this matcher in the assertion is not supported yet.
+    throw new UnsupportedOperationException("IsDescendantMatcher is not supported for assertion.");
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description
+        .appendText("matched with ")
+        .appendText(widgetMatcher.toString())
+        .appendText(" with ancestor: ")
+        .appendText(ancestorMatcher.toString());
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java
new file mode 100644
index 0000000..3380d21
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java
@@ -0,0 +1,31 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+/** A matcher that checks the existence of a Flutter widget. */
+public final class IsExistingMatcher extends TypeSafeMatcher<WidgetInfo> {
+
+  /** Constructs the matcher. */
+  IsExistingMatcher() {}
+
+  @Override
+  public String toString() {
+    return "is existing";
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    return widget != null;
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description.appendText("should exist.");
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java
new file mode 100644
index 0000000..4b86aed
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java
@@ -0,0 +1,49 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.gson.annotations.Expose;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+
+/** A matcher that matches a Flutter widget with a given text. */
+public final class WithTextMatcher extends WidgetMatcher {
+
+  @Expose private final String text;
+
+  /**
+   * Constructs the matcher with the given text to be matched with.
+   *
+   * @param text the text to be matched with.
+   */
+  WithTextMatcher(@Nonnull String text) {
+    super("ByText");
+    this.text = checkNotNull(text);
+  }
+
+  /** Returns the text string that shall be matched for the widget. */
+  public String getText() {
+    return text;
+  }
+
+  @Override
+  public String toString() {
+    return "with text: " + text;
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    return text.equals(widget.getText());
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description.appendText("with text: ").appendText(text);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java
new file mode 100644
index 0000000..27d4314
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java
@@ -0,0 +1,52 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+
+/** A matcher that matches a Flutter widget with a given tooltip. */
+public final class WithTooltipMatcher extends WidgetMatcher {
+
+  @Expose
+  @SerializedName("text")
+  private final String tooltip;
+
+  /**
+   * Constructs the matcher with the given {@code tooltip} to be matched with.
+   *
+   * @param tooltip the tooltip to be matched with.
+   */
+  public WithTooltipMatcher(@Nonnull String tooltip) {
+    super("ByTooltipMessage");
+    this.tooltip = checkNotNull(tooltip);
+  }
+
+  /** Returns the tooltip string that shall be matched for the widget. */
+  public String getTooltip() {
+    return tooltip;
+  }
+
+  @Override
+  public String toString() {
+    return "with tooltip: " + tooltip;
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    return tooltip.equals(widget.getTooltip());
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description.appendText("with tooltip: ").appendText(tooltip);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java
new file mode 100644
index 0000000..84cf0e0
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java
@@ -0,0 +1,49 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.gson.annotations.Expose;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+
+/** A matcher that matches a Flutter widget with a given runtime type. */
+public final class WithTypeMatcher extends WidgetMatcher {
+
+  @Expose private final String type;
+
+  /**
+   * Constructs the matcher with the given runtime type to be matched with.
+   *
+   * @param type the runtime type to be matched with.
+   */
+  public WithTypeMatcher(@Nonnull String type) {
+    super("ByType");
+    this.type = checkNotNull(type);
+  }
+
+  /** Returns the type string that shall be matched for the widget. */
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public String toString() {
+    return "with runtime type: " + type;
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    return type.equals(widget.getType());
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description.appendText("with runtime type: ").appendText(type);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java
new file mode 100644
index 0000000..0e3df39
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java
@@ -0,0 +1,54 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import androidx.test.espresso.flutter.api.WidgetMatcher;
+import androidx.test.espresso.flutter.model.WidgetInfo;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import javax.annotation.Nonnull;
+import org.hamcrest.Description;
+
+/** A matcher that matches a Flutter widget with a given value key. */
+public final class WithValueKeyMatcher extends WidgetMatcher {
+
+  @Expose
+  @SerializedName("keyValueString")
+  private final String valueKey;
+
+  @Expose private final String keyValueType = "String";
+
+  /**
+   * Constructs the matcher with the given value key String to be matched with.
+   *
+   * @param valueKey the value key String to be matched with.
+   */
+  public WithValueKeyMatcher(@Nonnull String valueKey) {
+    super("ByValueKey");
+    this.valueKey = checkNotNull(valueKey);
+  }
+
+  /** Returns the value key string that shall be matched for the widget. */
+  public String getValueKey() {
+    return valueKey;
+  }
+
+  @Override
+  public String toString() {
+    return "with value key: " + valueKey;
+  }
+
+  @Override
+  protected boolean matchesSafely(WidgetInfo widget) {
+    return valueKey.equals(widget.getValueKey());
+  }
+
+  @Override
+  public void describeTo(Description description) {
+    description.appendText("with value key: ").appendText(valueKey);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java
new file mode 100644
index 0000000..d6394d2
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java
@@ -0,0 +1,109 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.model;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.Beta;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a Flutter widget, containing all the properties that are accessible in Espresso.
+ *
+ * <p>Note, this class should typically be decoded from the Flutter testing protocol. Users of
+ * Espresso testing framework should rarely have the needs to build their own {@link WidgetInfo}
+ * instance.
+ *
+ * <p>Also, the current implementation is hard-coded and potentially only works with a limited set
+ * of {@code WidgetMatchers}. Later, we might consider codegen of representations for Flutter
+ * widgets for extensibility.
+ */
+@Beta
+public class WidgetInfo {
+
+  /** A String representation of a Flutter widget's ValueKey. */
+  @Nullable private final String valueKey;
+  /** A String representation of the runtime type of the widget. */
+  private final String runtimeType;
+  /** The widget's text property. */
+  @Nullable private final String text;
+  /** The widget's tooltip property. */
+  @Nullable private final String tooltip;
+
+  WidgetInfo(
+      @Nullable String valueKey,
+      String runtimeType,
+      @Nullable String text,
+      @Nullable String tooltip) {
+    this.valueKey = valueKey;
+    this.runtimeType = checkNotNull(runtimeType, "RuntimeType cannot be null.");
+    this.text = text;
+    this.tooltip = tooltip;
+  }
+
+  /** Returns a String representation of the Flutter widget's ValueKey. Could be null. */
+  @Nullable
+  public String getValueKey() {
+    return valueKey;
+  }
+
+  /** Returns a String representation of the runtime type of the Flutter widget. */
+  @Nonnull
+  public String getType() {
+    return runtimeType;
+  }
+
+  /** Returns the widget's 'text' property. Will be null for widgets without a 'text' property. */
+  @Nullable
+  public String getText() {
+    return text;
+  }
+
+  /**
+   * Returns the widget's 'tooltip' property. Will be null for widgets without a 'tooltip' property.
+   */
+  @Nullable
+  public String getTooltip() {
+    return tooltip;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof WidgetInfo) {
+      WidgetInfo widget = (WidgetInfo) obj;
+      return Objects.equals(widget.valueKey, this.valueKey)
+          && Objects.equals(widget.runtimeType, this.runtimeType)
+          && Objects.equals(widget.text, this.text)
+          && Objects.equals(widget.tooltip, this.tooltip);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(valueKey, runtimeType, text, tooltip);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Widget [");
+    sb.append("runtimeType=").append(runtimeType).append(",");
+    if (valueKey != null) {
+      sb.append("valueKey=").append(valueKey).append(",");
+    }
+    if (text != null) {
+      sb.append("text=").append(text).append(",");
+    }
+    if (tooltip != null) {
+      sb.append("tooltip=").append(tooltip).append(",");
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+}
diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java
new file mode 100644
index 0000000..53ea8a2
--- /dev/null
+++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java
@@ -0,0 +1,81 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package androidx.test.espresso.flutter.model;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Builder for {@link WidgetInfo}.
+ *
+ * <p>Internal only. Users of Espresso framework should rarely have the needs to build their own
+ * {@link WidgetInfo} instance.
+ */
+public class WidgetInfoBuilder {
+
+  @Nullable private String valueKey;
+  private String runtimeType;
+  @Nullable private String text;
+  @Nullable private String tooltip;
+
+  /** Empty constructor. */
+  public WidgetInfoBuilder() {}
+
+  /**
+   * Constructs the builder with the given {@code runtimeType}.
+   *
+   * @param runtimeType the runtime type of the widget. Cannot be null.
+   */
+  public WidgetInfoBuilder(@Nonnull String runtimeType) {
+    this.runtimeType = checkNotNull(runtimeType, "RuntimeType cannot be null.");
+  }
+
+  /**
+   * Sets the value key of the widget.
+   *
+   * @param valueKey the value key of the widget that shall be set. Could be null.
+   */
+  public WidgetInfoBuilder setValueKey(@Nullable String valueKey) {
+    this.valueKey = valueKey;
+    return this;
+  }
+
+  /**
+   * Sets the runtime type of the widget.
+   *
+   * @param runtimeType the runtime type of the widget that shall be set. Cannot be null.
+   */
+  public WidgetInfoBuilder setRuntimeType(@Nonnull String runtimeType) {
+    this.runtimeType = checkNotNull(runtimeType, "RuntimeType cannot be null.");
+    return this;
+  }
+
+  /**
+   * Sets the text of the widget.
+   *
+   * @param text the text of the widget that shall be set. Can be null.
+   */
+  public WidgetInfoBuilder setText(@Nullable String text) {
+    this.text = text;
+    return this;
+  }
+
+  /**
+   * Sets the tooltip of the widget.
+   *
+   * @param tooltip the tooltip of the widget that shall be set. Can be null.
+   */
+  public WidgetInfoBuilder setTooltip(@Nullable String tooltip) {
+    this.tooltip = tooltip;
+    return this;
+  }
+
+  /** Builds and returns the {@code WidgetInfo} instance. */
+  public WidgetInfo build() {
+    return new WidgetInfo(valueKey, runtimeType, text, tooltip);
+  }
+}
diff --git a/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java b/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java
new file mode 100644
index 0000000..966a7c1
--- /dev/null
+++ b/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java
@@ -0,0 +1,45 @@
+package com.example.espresso;
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
+
+/** EspressoPlugin */
+public class EspressoPlugin implements FlutterPlugin, MethodCallHandler {
+  @Override
+  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+    final MethodChannel channel =
+        new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "espresso");
+    channel.setMethodCallHandler(new EspressoPlugin());
+  }
+
+  // This static function is optional and equivalent to onAttachedToEngine. It supports the old
+  // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
+  // plugin registration via this function while apps migrate to use the new Android APIs
+  // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
+  //
+  // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
+  // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
+  // depending on the user's project. onAttachedToEngine or registerWith must both be defined
+  // in the same class.
+  public static void registerWith(Registrar registrar) {
+    final MethodChannel channel = new MethodChannel(registrar.messenger(), "espresso");
+    channel.setMethodCallHandler(new EspressoPlugin());
+  }
+
+  @Override
+  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+    if (call.method.equals("getPlatformVersion")) {
+      result.success("Android " + android.os.Build.VERSION.RELEASE);
+    } else {
+      result.notImplemented();
+    }
+  }
+
+  @Override
+  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
+}
diff --git a/packages/espresso/example/.gitignore b/packages/espresso/example/.gitignore
new file mode 100644
index 0000000..ae1f183
--- /dev/null
+++ b/packages/espresso/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/espresso/example/.metadata b/packages/espresso/example/.metadata
new file mode 100644
index 0000000..e1188cd
--- /dev/null
+++ b/packages/espresso/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: 0190e40457d43e17bdfaf046dfa634cbc5bf28b9
+  channel: unknown
+
+project_type: app
diff --git a/packages/espresso/example/README.md b/packages/espresso/example/README.md
new file mode 100644
index 0000000..224544e
--- /dev/null
+++ b/packages/espresso/example/README.md
@@ -0,0 +1,14 @@
+# espresso_example
+
+Demonstrates how to use the espresso package.
+
+The espresso package only runs tests on Android. The example runs on iOS, but this is only to keep our continuous integration bots green.
+
+## Getting Started
+
+To run the Espresso tests:
+
+```
+flutter build apk --debug
+./gradlew app:connectedAndroidTest
+```
diff --git a/packages/espresso/example/android/.gitignore b/packages/espresso/example/android/.gitignore
new file mode 100644
index 0000000..bc2100d
--- /dev/null
+++ b/packages/espresso/example/android/.gitignore
@@ -0,0 +1,7 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
diff --git a/packages/espresso/example/android/app/build.gradle b/packages/espresso/example/android/app/build.gradle
new file mode 100644
index 0000000..0be4156
--- /dev/null
+++ b/packages/espresso/example/android/app/build.gradle
@@ -0,0 +1,88 @@
+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 28
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.espresso_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 {
+    testImplementation 'junit:junit:4.12'
+    testImplementation "com.google.truth:truth:1.0"
+    androidTestImplementation 'androidx.test:runner:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+
+    // Core library
+    api 'androidx.test:core:1.2.0'
+
+    // AndroidJUnitRunner and JUnit Rules
+    androidTestImplementation 'androidx.test:runner:1.1.0'
+    androidTestImplementation 'androidx.test:rules:1.1.0'
+
+    // Assertions
+    androidTestImplementation 'androidx.test.ext:junit:1.0.0'
+    androidTestImplementation 'androidx.test.ext:truth:1.0.0'
+    androidTestImplementation 'com.google.truth:truth:0.42'
+
+    // Espresso dependencies
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.1.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
+    androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.1.0'
+
+    // The following Espresso dependency can be either "implementation"
+    // or "androidTestImplementation", depending on whether you want the
+    // dependency to appear on your APK's compile classpath or the test APK
+    // classpath.
+    androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.1.0'
+}
diff --git a/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java b/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java
new file mode 100644
index 0000000..aaedd6c
--- /dev/null
+++ b/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java
@@ -0,0 +1,76 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.example.espresso_example;
+
+import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget;
+import static androidx.test.espresso.flutter.action.FlutterActions.click;
+import static androidx.test.espresso.flutter.action.FlutterActions.syntheticClick;
+import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withTooltip;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.flutter.EspressoFlutter.WidgetInteraction;
+import androidx.test.espresso.flutter.assertion.FlutterAssertions;
+import androidx.test.espresso.flutter.matcher.FlutterMatchers;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link EspressoFlutter}. */
+@RunWith(AndroidJUnit4.class)
+public class MainActivityTest {
+
+  @Before
+  public void setUp() throws Exception {
+    ActivityScenario.launch(MainActivity.class);
+  }
+
+  @Test
+  public void performTripleClick() {
+    WidgetInteraction interaction =
+        onFlutterWidget(withTooltip("Increment")).perform(click(), click()).perform(click());
+    assertThat(interaction).isNotNull();
+    onFlutterWidget(withValueKey("CountText")).check(matches(withText("Button tapped 3 times.")));
+  }
+
+  @Test
+  public void performClick() {
+    WidgetInteraction interaction = onFlutterWidget(withTooltip("Increment")).perform(click());
+    assertThat(interaction).isNotNull();
+    onFlutterWidget(withValueKey("CountText")).check(matches(withText("Button tapped 1 time.")));
+  }
+
+  @Test
+  public void performSyntheticClick() {
+    WidgetInteraction interaction =
+        onFlutterWidget(withTooltip("Increment")).perform(syntheticClick());
+    assertThat(interaction).isNotNull();
+    onFlutterWidget(withValueKey("CountText")).check(matches(withText("Button tapped 1 time.")));
+  }
+
+  @Test
+  public void performTwiceSyntheticClicks() {
+    WidgetInteraction interaction =
+        onFlutterWidget(withTooltip("Increment")).perform(syntheticClick(), syntheticClick());
+    assertThat(interaction).isNotNull();
+    onFlutterWidget(withValueKey("CountText")).check(matches(withText("Button tapped 2 times.")));
+  }
+
+  @Test
+  public void isIncrementButtonExists() {
+    onFlutterWidget(FlutterMatchers.withTooltip("Increment"))
+        .check(FlutterAssertions.matches(FlutterMatchers.isExisting()));
+  }
+
+  @Test
+  public void isAppBarExists() {
+    onFlutterWidget(FlutterMatchers.withType("AppBar"))
+        .check(FlutterAssertions.matches(FlutterMatchers.isExisting()));
+  }
+}
diff --git a/packages/espresso/example/android/app/src/debug/AndroidManifest.xml b/packages/espresso/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..fc8acdd
--- /dev/null
+++ b/packages/espresso/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.espresso_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"/>
+    <application android:usesCleartextTraffic="true"/>
+</manifest>
diff --git a/packages/espresso/example/android/app/src/main/AndroidManifest.xml b/packages/espresso/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b82df92
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.espresso_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="espresso_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/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java b/packages/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java
new file mode 100644
index 0000000..413ef9e
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java
@@ -0,0 +1,10 @@
+package com.example.espresso_example;
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.embedding.engine.FlutterEngine;
+
+public class MainActivity extends FlutterActivity {
+  @Override
+  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {}
+}
diff --git a/packages/espresso/example/android/app/src/main/res/drawable/launch_background.xml b/packages/espresso/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/espresso/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/espresso/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/espresso/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/espresso/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/espresso/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/espresso/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/espresso/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/espresso/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/espresso/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/espresso/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/espresso/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/espresso/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/espresso/example/android/app/src/main/res/values/styles.xml b/packages/espresso/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/espresso/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/espresso/example/android/app/src/profile/AndroidManifest.xml b/packages/espresso/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..bd9aec9
--- /dev/null
+++ b/packages/espresso/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.espresso_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/espresso/example/android/build.gradle b/packages/espresso/example/android/build.gradle
new file mode 100644
index 0000000..e0d7ae2
--- /dev/null
+++ b/packages/espresso/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/espresso/example/android/gradle.properties b/packages/espresso/example/android/gradle.properties
new file mode 100644
index 0000000..38c8d45
--- /dev/null
+++ b/packages/espresso/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/espresso/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..296b146
--- /dev/null
+++ b/packages/espresso/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/espresso/example/android/settings.gradle b/packages/espresso/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/.gitignore b/packages/espresso/example/ios/.gitignore
new file mode 100644
index 0000000..e96ef60
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Flutter/AppFrameworkInfo.plist b/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..6b4c0f7
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Flutter/Debug.xcconfig b/packages/espresso/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..e8efba1
--- /dev/null
+++ b/packages/espresso/example/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/espresso/example/ios/Flutter/Release.xcconfig b/packages/espresso/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..399e934
--- /dev/null
+++ b/packages/espresso/example/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/espresso/example/ios/Runner.xcodeproj/project.pbxproj b/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..2209e01
--- /dev/null
+++ b/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,584 @@
+// !$*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 */; };
+		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, ); }; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		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, ); }; };
+		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 */; };
+		B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */; };
+/* 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 */
+		02691CEFCB33C0B1CABE7A23 /* 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>"; };
+		09442C04D3DC0049E7725D93 /* 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>"; };
+		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>"; };
+		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
+		3EF237100A0BFC444DE6BC97 /* 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>"; };
+		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>"; };
+		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; };
+		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>"; };
+		AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+				B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		301432828879F7BDE0943C41 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				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 */,
+				E9E5CC94EC52B9D261A44A5E /* Pods */,
+				301432828879F7BDE0943C41 /* 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 */,
+				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>";
+		};
+		E9E5CC94EC52B9D261A44A5E /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				02691CEFCB33C0B1CABE7A23 /* Pods-Runner.debug.xcconfig */,
+				3EF237100A0BFC444DE6BC97 /* Pods-Runner.release.xcconfig */,
+				09442C04D3DC0049E7725D93 /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				5D7E711796DC6F61E7F1A6AE /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				DC7821945A6EDE472DDF686F /* [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 = "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\" thin";
+		};
+		5D7E711796DC6F61E7F1A6AE /* [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;
+		};
+		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";
+		};
+		DC7821945A6EDE472DDF686F /* [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;
+		};
+/* 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;
+			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_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)";
+				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.espressoExample;
+				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;
+			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_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;
+			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_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)";
+				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.espressoExample;
+				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;
+				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.espressoExample;
+				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/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/AppDelegate.swift b/packages/espresso/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/espresso/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/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files differ
diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Base.lproj/Main.storyboard b/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/packages/espresso/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/espresso/example/ios/Runner/Info.plist b/packages/espresso/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..96cc992
--- /dev/null
+++ b/packages/espresso/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>espresso_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/espresso/example/ios/Runner/Runner-Bridging-Header.h b/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..7335fdf
--- /dev/null
+++ b/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart
new file mode 100644
index 0000000..4c9301b
--- /dev/null
+++ b/packages/espresso/example/lib/main.dart
@@ -0,0 +1,110 @@
+import 'package:flutter/material.dart';
+
+void main() => runApp(MyApp());
+
+/// Example app for Espresso plugin.
+class MyApp extends StatelessWidget {
+  // This widget is the root of your application.
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        // This is the theme of your application.
+        //
+        // Try running your application with "flutter run". You'll see the
+        // application has a blue toolbar. Then, without quitting the app, try
+        // changing the primarySwatch below to Colors.green and then invoke
+        // "hot reload" (press "r" in the console where you ran "flutter run",
+        // or simply save your changes to "hot reload" in a Flutter IDE).
+        // Notice that the counter didn't reset back to zero; the application
+        // is not restarted.
+        primarySwatch: Colors.blue,
+      ),
+      home: _MyHomePage(title: 'Flutter Demo Home Page'),
+    );
+  }
+}
+
+class _MyHomePage extends StatefulWidget {
+  _MyHomePage({Key key, this.title}) : super(key: key);
+
+  // This widget is the home page of your application. It is stateful, meaning
+  // that it has a State object (defined below) that contains fields that affect
+  // how it looks.
+
+  // This class is the configuration for the state. It holds the values (in this
+  // case the title) provided by the parent (in this case the App widget) and
+  // used by the build method of the State. Fields in a Widget subclass are
+  // always marked "final".
+
+  final String title;
+
+  @override
+  _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<_MyHomePage> {
+  int _counter = 0;
+
+  void _incrementCounter() {
+    setState(() {
+      // This call to setState tells the Flutter framework that something has
+      // changed in this State, which causes it to rerun the build method below
+      // so that the display can reflect the updated values. If we changed
+      // _counter without calling setState(), then the build method would not be
+      // called again, and so nothing would appear to happen.
+      _counter++;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // This method is rerun every time setState is called, for instance as done
+    // by the _incrementCounter method above.
+    //
+    // The Flutter framework has been optimized to make rerunning build methods
+    // fast, so that you can just rebuild anything that needs updating rather
+    // than having to individually change instances of widgets.
+    return Scaffold(
+      appBar: AppBar(
+        // Here we take the value from the MyHomePage object that was created by
+        // the App.build method, and use it to set our appbar title.
+        title: Text(widget.title),
+      ),
+      body: Center(
+        // Center is a layout widget. It takes a single child and positions it
+        // in the middle of the parent.
+        child: Column(
+          // Column is also a layout widget. It takes a list of children and
+          // arranges them vertically. By default, it sizes itself to fit its
+          // children horizontally, and tries to be as tall as its parent.
+          //
+          // Invoke "debug painting" (press "p" in the console, choose the
+          // "Toggle Debug Paint" action from the Flutter Inspector in Android
+          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
+          // to see the wireframe for each widget.
+          //
+          // Column has various properties to control how it sizes itself and
+          // how it positions its children. Here we use mainAxisAlignment to
+          // center the children vertically; the main axis here is the vertical
+          // axis because Columns are vertical (the cross axis would be
+          // horizontal).
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Text(
+              'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
+              style: Theme.of(context).textTheme.display1,
+              key: ValueKey('CountText'),
+            ),
+          ],
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _incrementCounter,
+        tooltip: 'Increment',
+        child: Icon(Icons.add),
+      ), // This trailing comma makes auto-formatting nicer for build methods.
+    );
+  }
+}
diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml
new file mode 100644
index 0000000..d285983
--- /dev/null
+++ b/packages/espresso/example/pubspec.yaml
@@ -0,0 +1,65 @@
+name: espresso_example
+description: Demonstrates how to use the espresso plugin.
+publish_to: 'none'
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^0.1.2
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_driver:
+    sdk: flutter
+
+  espresso:
+    path: ../
+
+# 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/espresso/example/test_driver/example.dart b/packages/espresso/example/test_driver/example.dart
new file mode 100644
index 0000000..ab74ff5
--- /dev/null
+++ b/packages/espresso/example/test_driver/example.dart
@@ -0,0 +1,8 @@
+import 'package:flutter_driver/driver_extension.dart';
+
+import 'package:espresso_example/main.dart' as app;
+
+void main() {
+  enableFlutterDriverExtension();
+  app.main();
+}
diff --git a/packages/espresso/ios/.gitignore b/packages/espresso/ios/.gitignore
new file mode 100644
index 0000000..aa479fd
--- /dev/null
+++ b/packages/espresso/ios/.gitignore
@@ -0,0 +1,37 @@
+.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/Generated.xcconfig
+/Flutter/flutter_export_environment.sh
\ No newline at end of file
diff --git a/packages/espresso/ios/Assets/.gitkeep b/packages/espresso/ios/Assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/espresso/ios/Assets/.gitkeep
diff --git a/packages/espresso/ios/Classes/EspressoPlugin.h b/packages/espresso/ios/Classes/EspressoPlugin.h
new file mode 100644
index 0000000..5f97615
--- /dev/null
+++ b/packages/espresso/ios/Classes/EspressoPlugin.h
@@ -0,0 +1,4 @@
+#import <Flutter/Flutter.h>
+
+@interface EspressoPlugin : NSObject <FlutterPlugin>
+@end
diff --git a/packages/espresso/ios/Classes/EspressoPlugin.m b/packages/espresso/ios/Classes/EspressoPlugin.m
new file mode 100644
index 0000000..cb4ef80
--- /dev/null
+++ b/packages/espresso/ios/Classes/EspressoPlugin.m
@@ -0,0 +1,15 @@
+#import "EspressoPlugin.h"
+
+@implementation EspressoPlugin
++ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
+  FlutterMethodChannel* channel =
+      [FlutterMethodChannel methodChannelWithName:@"espresso"
+                                  binaryMessenger:[registrar messenger]];
+  EspressoPlugin* instance = [[EspressoPlugin alloc] init];
+  [registrar addMethodCallDelegate:instance channel:channel];
+}
+
+- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
+  result(FlutterMethodNotImplemented);
+}
+@end
diff --git a/packages/espresso/ios/Classes/SwiftEspressoPlugin.swift b/packages/espresso/ios/Classes/SwiftEspressoPlugin.swift
new file mode 100644
index 0000000..2ff3024
--- /dev/null
+++ b/packages/espresso/ios/Classes/SwiftEspressoPlugin.swift
@@ -0,0 +1,14 @@
+import Flutter
+import UIKit
+
+public class SwiftEspressoPlugin: NSObject, FlutterPlugin {
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let channel = FlutterMethodChannel(name: "espresso", binaryMessenger: registrar.messenger())
+    let instance = SwiftEspressoPlugin()
+    registrar.addMethodCallDelegate(instance, channel: channel)
+  }
+
+  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+    result("iOS " + UIDevice.current.systemVersion)
+  }
+}
diff --git a/packages/espresso/ios/espresso.podspec b/packages/espresso/ios/espresso.podspec
new file mode 100644
index 0000000..cd64afa
--- /dev/null
+++ b/packages/espresso/ios/espresso.podspec
@@ -0,0 +1,23 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint espresso.podspec' to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'espresso'
+  s.version          = '0.0.1'
+  s.summary          = 'A new flutter plugin project.'
+  s.description      = <<-DESC
+A new flutter plugin project.
+                       DESC
+  s.homepage         = 'http://example.com'
+  s.license          = { :file => '../LICENSE' }
+  s.author           = { 'Your Company' => 'email@example.com' }
+  s.source           = { :path => '.' }
+  s.source_files = 'Classes/**/*'
+  s.dependency 'Flutter'
+  s.platform = :ios, '8.0'
+
+  # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
+  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+  s.swift_version = '5.0'
+end
diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml
new file mode 100644
index 0000000..70a05ab
--- /dev/null
+++ b/packages/espresso/pubspec.yaml
@@ -0,0 +1,26 @@
+name: espresso
+description: Java classes for testing Flutter apps using Espresso.
+version: 0.0.1
+homepage: https://github.com/flutter/plugins/espresso
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+  flutter: ">=1.10.0 <2.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+# The following section is specific to Flutter.
+flutter:
+  plugin:
+    platforms:
+      android:
+        package: com.example.espresso
+        pluginClass: EspressoPlugin
+      ios:
+        pluginClass: EspressoPlugin