[dart-sdk][webview] Initial CL adding Fuchsia webview support

Added a new 'fuchsia_webview_flutter' packages that contains the initial
scaffolding required to integrate fuchsia.web.* together with upstream
fulltter_webview plugin.

The only highlevel Webview API currently implemented is 'loadUrl'. The rest
of the APIs will be implemented in subsequent cls.

This package also contains an example Mod under:
fuchsia_webview_flutter/examples/

Change-Id: I9a8b8a63506e4a53117afefa749629620fd97771
diff --git a/packages/tests/BUILD.gn b/packages/tests/BUILD.gn
index 924f956..57f7ef0 100644
--- a/packages/tests/BUILD.gn
+++ b/packages/tests/BUILD.gn
@@ -28,6 +28,7 @@
     "//topaz/public/dart/fuchsia_modular_flutter:fuchsia_modular_flutter_unittests($host_toolchain)",
     "//topaz/public/dart/fuchsia_scenic_flutter:fuchsia_scenic_flutter_unittests($host_toolchain)",
     "//topaz/public/dart/fuchsia_services:fuchsia_services_package_unittests($host_toolchain)",
+    "//topaz/public/dart/fuchsia_webview_flutter:fuchsia_webview_flutter_unittests($host_toolchain)",
     "//topaz/public/dart/sledge:dart_sledge_tests($host_toolchain)",
     "//topaz/public/dart/story_shell_labs:layout_unittests($host_toolchain)",
     "//topaz/public/dart/widgets:dart_widget_tests($host_toolchain)",
@@ -58,6 +59,7 @@
   testonly = true
   public_deps = [
     "//garnet/packages/tests:run_test_component",
+    "//topaz/bin/fidlgen_dart:fidlgen_dart_backend_ir_test($host_toolchain)",
     "//topaz/packages/examples:tests",
     "//topaz/packages/tests:bindings_tests",
     "//topaz/packages/tests:crasher_dart",
@@ -67,8 +69,8 @@
     "//topaz/packages/tests:dart_target_unittests",
     "//topaz/packages/tests:dart_unittests",
     "//topaz/packages/tests:fidl_changes",
-    "//topaz/packages/tests:fidl_compatibility_test_topaz",
     "//topaz/packages/tests:fidl_compatibility_test_server_dart",
+    "//topaz/packages/tests:fidl_compatibility_test_topaz",
     "//topaz/packages/tests:fidl_dangerous_identifiers",
     "//topaz/packages/tests:flutter_runner_tests",
     "//topaz/packages/tests:flutter_screencap_test",
@@ -77,7 +79,6 @@
     "//topaz/packages/tests:scenic",
     "//topaz/packages/tests:sledge",
     "//topaz/packages/tests:web_runner_tests",
-    "//topaz/bin/fidlgen_dart:fidlgen_dart_backend_ir_test($host_toolchain)",
   ]
 }
 
@@ -209,5 +210,5 @@
   testonly = true
   public_deps = [
     "//topaz/shell/ermine/test/end_to_end:test",
-    ]
+  ]
 }
diff --git a/public/dart/fuchsia_webview_flutter/BUILD.gn b/public/dart/fuchsia_webview_flutter/BUILD.gn
new file mode 100644
index 0000000..dab8b2b
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/BUILD.gn
@@ -0,0 +1,46 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/dart/dart_library.gni")
+import("//topaz/runtime/dart/flutter_test.gni")
+
+dart_library("fuchsia_webview_flutter") {
+  package_name = "fuchsia_webview_flutter"
+
+  sdk_category = "partner"
+
+  sources = [
+    "src/fuchsia_web_services.dart",
+    "src/fuchsia_webview.dart",
+    "src/fuchsia_webview_platform_controller.dart",
+    "webview.dart",
+  ]
+
+  deps = [
+    "//sdk/fidl/fuchsia.sys",
+    "//sdk/fidl/fuchsia.ui.views",
+    "//sdk/fidl/fuchsia.web",
+    "//third_party/dart-pkg/pub/webview_flutter",
+    "//topaz/public/dart/fuchsia_logger",
+    "//topaz/public/dart/fuchsia_scenic",
+    "//topaz/public/dart/fuchsia_scenic_flutter",
+    "//zircon/public/fidl/fuchsia-io",
+    "//zircon/public/fidl/fuchsia-mem",
+  ]
+}
+
+# fx run-host-tests fuchsia_webview_flutter_unittests
+flutter_test("fuchsia_webview_flutter_unittests") {
+  sources = [
+    "fuchsia_webview_test.dart",
+  ]
+
+  deps = [
+    ":fuchsia_webview_flutter",
+    "//third_party/dart-pkg/pub/mockito",
+    "//third_party/dart-pkg/pub/test",
+    "//topaz/public/dart/fuchsia_webview_flutter",
+    "//topaz/public/lib/testing/flutter",
+  ]
+}
diff --git a/public/dart/fuchsia_webview_flutter/OWNERS b/public/dart/fuchsia_webview_flutter/OWNERS
new file mode 100644
index 0000000..8416e0c
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/OWNERS
@@ -0,0 +1 @@
+nkorsote@google.com
diff --git a/public/dart/fuchsia_webview_flutter/analysis_options.yaml b/public/dart/fuchsia_webview_flutter/analysis_options.yaml
new file mode 100644
index 0000000..bebf512
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include: ../../analysis_options.yaml
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/BUILD.gn b/public/dart/fuchsia_webview_flutter/examples/webview_mod/BUILD.gn
new file mode 100644
index 0000000..fdf597d
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+import("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+flutter_app("webview_mod") {
+  main_dart = "lib/main.dart"
+
+  fuchsia_package_name = "webview_mod"
+
+  meta = [
+    {
+      path = rebase_path("meta/webview_mod.cmx")
+      dest = "webview_mod.cmx"
+    },
+  ]
+
+  deps = [
+    "//topaz/public/dart/fuchsia_logger",
+    "//topaz/public/dart/fuchsia_modular",
+    "//topaz/public/dart/fuchsia_webview_flutter",
+  ]
+}
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/OWNERS b/public/dart/fuchsia_webview_flutter/examples/webview_mod/OWNERS
new file mode 100644
index 0000000..9246b56
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/OWNERS
@@ -0,0 +1,3 @@
+miguelfrde@google.com
+nkorsote@google.com
+*
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/README.md b/public/dart/fuchsia_webview_flutter/examples/webview_mod/README.md
new file mode 100644
index 0000000..dd57e04
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/README.md
@@ -0,0 +1,3 @@
+# Webview mod
+
+This mod showcases how to use Fuchsia webview functionality.
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/analysis_options.yaml b/public/dart/fuchsia_webview_flutter/examples/webview_mod/analysis_options.yaml
new file mode 100644
index 0000000..d2a931f
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+include: ../../../../../tools/analysis_options.yaml
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/app.dart b/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/app.dart
new file mode 100644
index 0000000..ad20222
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/app.dart
@@ -0,0 +1,122 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:fuchsia_modular/entity.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+class App extends StatefulWidget {
+  final Stream<Entity> entityStream;
+  const App({
+    @required this.entityStream,
+    Key key,
+  })  : assert(entityStream != null),
+        super(key: key);
+  @override
+  State<App> createState() => AppState(entityStream);
+}
+
+StreamSubscription<String> _entityStreamSubscriber;
+
+class AppState extends State<App> {
+  final TextEditingController _textEditingController;
+  WebViewController _webViewController;
+  Stream<Entity> _entityStream;
+
+  AppState(this._entityStream)
+      : _textEditingController = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    // initialize url from a passed intent param if one was present
+    _entityStream.listen((entity) {
+      _entityStreamSubscriber =
+          entity.watch().map(utf8.decode).listen((String url) {
+        _textEditingController.text = url;
+      });
+    });
+  }
+
+  @override
+  void dispose() {
+    _entityStreamSubscriber.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'Webview Mod',
+      home: Scaffold(
+        backgroundColor: Colors.white,
+        body: Column(
+          children: <Widget>[
+            Row(
+              children: <Widget>[
+                Expanded(
+                  child: TextField(
+                    controller: _textEditingController,
+                  ),
+                ),
+                _buildEnterBtn(),
+                Padding(padding: EdgeInsets.all(4.0)),
+                _buildClearBtn(),
+              ],
+            ),
+            Expanded(
+              child: _buildWebview(),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildEnterBtn() {
+    return RaisedButton(
+      child: const Text('Enter'),
+      color: Theme.of(context).accentColor,
+      elevation: 4.0,
+      splashColor: Colors.blueGrey,
+      onPressed: () {
+        _loadUrl(_textEditingController.text);
+      },
+    );
+  }
+
+  Widget _buildClearBtn() {
+    return RaisedButton(
+      child: const Text('Clear'),
+      color: Theme.of(context).accentColor,
+      elevation: 4.0,
+      splashColor: Colors.blueGrey,
+      onPressed: _textEditingController.clear,
+    );
+  }
+
+  Widget _buildWebview() {
+    return Container(
+      child: WebView(
+        onWebViewCreated: (WebViewController controller) {
+          _webViewController = controller;
+        },
+      ),
+    );
+  }
+
+  void _loadUrl(String url) {
+    if (url == null || url.isEmpty) {
+      return;
+    }
+    Uri uri = Uri.parse(url);
+    if (!uri.hasScheme) {
+      uri = uri.replace(scheme: 'https');
+    }
+    _webViewController?.loadUrl(uri.toString());
+  }
+}
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/main.dart b/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/main.dart
new file mode 100644
index 0000000..cf9438d
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/lib/main.dart
@@ -0,0 +1,39 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_modular/entity.dart';
+import 'package:fuchsia_modular/lifecycle.dart';
+import 'package:fuchsia_modular/module.dart' as modular;
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:fuchsia_webview_flutter/webview.dart';
+
+import 'app.dart';
+
+class RootIntentHandler extends modular.IntentHandler {
+  final _entityStreamController = () {
+    final controller = StreamController<Entity>.broadcast();
+    Lifecycle().addTerminateListener(controller.close);
+    return controller;
+  }();
+
+  Stream<Entity> get entityStream => _entityStreamController.stream;
+
+  @override
+  void handleIntent(modular.Intent intent) async {
+    // parse the Intent Entity if one was provided
+    _entityStreamController.add(intent.getEntity(name: 'url', type: 'string'));
+  }
+}
+
+void main() {
+  WebView.platform = FuchsiaWebView();
+  setupLogger(name: 'Webview Mod');
+  final intentHandler = RootIntentHandler();
+  modular.Module().registerIntentHandler(intentHandler);
+  runApp(App(entityStream: intentHandler.entityStream));
+}
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/meta/webview_mod.cmx b/public/dart/fuchsia_webview_flutter/examples/webview_mod/meta/webview_mod.cmx
new file mode 100644
index 0000000..e7f8a25
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/meta/webview_mod.cmx
@@ -0,0 +1,47 @@
+{
+    "facets": {
+        "fuchsia.modular": {
+            "@version": 2,
+            "binary": "webview_mod",
+            "intent_filters": [
+                {
+                    "action": "com.fuchsia.navigate_to",
+                    "parameters": [
+                        {
+                            "name": "url",
+                            "type": "string"
+                        }
+                    ]
+                }
+            ]
+        }
+    },
+    "program": {
+        "data": "data/webview_mod"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.cobalt.LoggerFactory",
+            "fuchsia.fonts.Provider",
+            "fuchsia.logger.LogSink",
+            "fuchsia.modular.Clipboard",
+            "fuchsia.modular.ComponentContext",
+            "fuchsia.modular.ContextWriter",
+            "fuchsia.modular.ModuleContext",
+            "fuchsia.net.SocketProvider",
+            "fuchsia.netstack.Netstack",
+            "fuchsia.process.Launcher",
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.sysmem.Allocator",
+            "fuchsia.ui.input.ImeService",
+            "fuchsia.ui.input.ImeVisibilityService",
+            "fuchsia.ui.policy.Presenter",
+            "fuchsia.ui.scenic.Scenic",
+            "fuchsia.web.ContextProvider"
+        ],
+        "system": [
+            "data/modules"
+        ]
+    }
+}
diff --git a/public/dart/fuchsia_webview_flutter/examples/webview_mod/pubspec.yaml b/public/dart/fuchsia_webview_flutter/examples/webview_mod/pubspec.yaml
new file mode 100644
index 0000000..2fd0fc9
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/examples/webview_mod/pubspec.yaml
@@ -0,0 +1,9 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+name: webview_mod
+
+flutter:
+  uses-material-design: true
+
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart
new file mode 100644
index 0000000..842bba29
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_web_services.dart
@@ -0,0 +1,156 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+import 'dart:io';
+
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_io/fidl_async.dart' as fidl_io;
+import 'package:fidl_fuchsia_mem/fidl_async.dart' as fidl_mem;
+import 'package:fidl_fuchsia_web/fidl_async.dart' as fidl_web;
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+import 'package:fuchsia_services/services.dart';
+import 'package:zircon/zircon.dart';
+import 'package:fuchsia_scenic/views.dart';
+
+/// Helper class to help connect and interface with 'fuchsia.web.*' services.
+class FuchsiaWebServices {
+  final fidl_web.ContextProviderProxy _contextProviderProxy =
+      fidl_web.ContextProviderProxy();
+  final fidl_web.ContextProxy _contextProxy = fidl_web.ContextProxy();
+  final fidl_web.FrameProxy _frameProxy = fidl_web.FrameProxy();
+  final fidl_web.NavigationControllerProxy _navigationControllerProxy =
+      fidl_web.NavigationControllerProxy();
+
+  final fidl_web.NavigationEventListenerBinding
+      _navigationEventObserverBinding =
+      fidl_web.NavigationEventListenerBinding();
+
+  ChildViewConnection _childViewConnection;
+
+  /// Constructs [FuchsiaWebServices] and connects to various 'fuchsia.web.*`
+  /// services.
+  FuchsiaWebServices({
+    fidl_web.ConsoleLogLevel javascriptLogLevel = fidl_web.ConsoleLogLevel.none,
+  }) {
+    StartupContext.fromStartupInfo()
+        .incoming
+        .connectToService(_contextProviderProxy);
+
+    // TODO(nkorsote): [service_directory] is effectively the sandbox inside
+    // which the created Context will run. If you give it a direct handle to
+    // component's /svc directory then it'll have access to everything the
+    // component can access. Alternatively, refactor this to use an Outgoing
+    // Directory.
+    if (!Directory('/svc').existsSync()) {
+      log.shout('no /svc directory');
+      return;
+    }
+    final channel = Channel.fromFile('/svc');
+    final fidl_web.CreateContextParams contextParams =
+        fidl_web.CreateContextParams(
+            serviceDirectory: InterfaceHandle<fidl_io.Directory>(channel));
+
+    _contextProviderProxy.create(contextParams, _contextProxy.ctrl.request());
+    _contextProxy.createFrame(_frameProxy.ctrl.request());
+
+    // Create token pair and pass one end to the webview and the other to child
+    // view connection which will be used to construct the child view widget
+    // that the webview will live in.
+    final tokenPair = ViewTokenPair();
+    _frameProxy.createView(tokenPair.viewToken);
+    _childViewConnection = ChildViewConnection(tokenPair.viewHolderToken);
+    _frameProxy
+      ..getNavigationController(_navigationControllerProxy.ctrl.request())
+      ..setJavaScriptLogLevel(javascriptLogLevel);
+  }
+
+  /// Returns a connection to a child view.
+  ///
+  /// It can be used to construct a [ChildView] widget that will display the
+  /// view's contents.
+  ChildViewConnection get childViewConnection => _childViewConnection;
+
+  /// Returns [fidl_web.NavigationControllerProxy]
+  fidl_web.NavigationControllerProxy get navigationController =>
+      _navigationControllerProxy;
+
+  /// Preforms the all the necessary cleanup.
+  void dispose() {
+    _navigationControllerProxy.ctrl.close();
+    _frameProxy.ctrl.close();
+    _contextProxy.ctrl.close();
+    _contextProviderProxy.ctrl.close();
+  }
+
+  /// Executes a UTF-8 encoded [script] for every subsequent page load where the
+  /// frame's URL has an origin reflected in [origins]. The script is executed
+  /// early, prior to the execution of the document's scripts.
+  ///
+  /// Scripts are identified by a identifier [id]. Any script previously
+  /// injected using the same [id] will be replaced.
+  ///
+  /// The order in which multiple bindings are executed is the same as the order
+  /// in which the bindings were added. If a script is added which clobbers an
+  /// existing script of the same [id], the previous script's precedence in the
+  /// injection order will be preserved.
+  ///
+  /// At least one [origins] entry must be specified. If a wildcard "*" is
+  /// specified in [origins], then the script will be evaluated for all
+  /// documents.
+  ///
+  /// If an error occurred, the FrameError will be set to one of these values:
+  /// - BUFFER_NOT_UTF8: [script] is not UTF-8 encoded.
+  /// - INVALID_ORIGIN: [origins] is an empty vector.
+  Future<void> injectJavascript(int id, List<String> origins, String script) {
+    final vmo = SizedVmo.fromUint8List(utf8.encode(script));
+    final buffer = fidl_mem.Buffer(vmo: vmo, size: vmo.size);
+    // TODO(nkosote): add catchError and decorate the error based on the error
+    // code
+    return _frameProxy.addBeforeLoadJavaScript(id, origins, buffer);
+  }
+
+  /// Posts a message to the [fidl_web.Frame]'s onMessage handler.
+  ///
+  /// [targetOrigin] restricts message delivery to the specified origin. If
+  /// [targetOrigin] is "*", then the message will be sent to the document
+  /// regardless of its origin.
+  ///
+  /// If an error occurred, the FrameError will be set to one of these values:
+  /// - INTERNAL_ERROR: The WebEngine failed to create a message pipe.
+  /// - BUFFER_NOT_UTF8: The script in [message]'s [fidl_web.WebMessage.data]
+  ///   property is not UTF-8 encoded.
+  /// - INVALID_ORIGIN: origins is an empty vector.
+  /// - NO_DATA_IN_MESSAGE: The [fidl_web.WebMessage.data] property is missing
+  ///   in [message].
+  Future<void> postMessage(
+    String targetOrigin,
+    String message, {
+    InterfaceRequest<fidl_web.MessagePort> outgoingMessagePortRequest,
+  }) {
+    final vmo = SizedVmo.fromUint8List(utf8.encode(message));
+    var msg = fidl_web.WebMessage(
+      data: fidl_mem.Buffer(vmo: vmo, size: vmo.size),
+      outgoingTransfer: outgoingMessagePortRequest != null
+          ? [
+              fidl_web.OutgoingTransferable.withMessagePort(
+                  outgoingMessagePortRequest)
+            ]
+          : null,
+    );
+    // TODO(nkosote): add catchError and decorate the error based on the error
+    // code
+    return _frameProxy.postMessage(targetOrigin, msg);
+  }
+
+  /// Sets the listener for handling page navigation events.
+  ///
+  /// The [observer] to use. Unregisters any existing listener if null.
+  void setNavigationEventListener(fidl_web.NavigationEventListener observer) {
+    _frameProxy.setNavigationEventListener(
+        _navigationEventObserverBinding.wrap(observer));
+  }
+}
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview.dart b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview.dart
new file mode 100644
index 0000000..57bd1df
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview.dart
@@ -0,0 +1,51 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/widgets.dart';
+import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:webview_flutter/platform_interface.dart';
+
+import 'fuchsia_web_services.dart';
+import 'fuchsia_webview_platform_controller.dart';
+
+/// Builds an Fuchsia webview.
+class FuchsiaWebView implements WebViewPlatform {
+  /// The fuchsia implementation of [WebViewPlatformController]
+  FuchsiaWebServices fuchsiaWebServices;
+
+  /// This constructor should only be used to inject a platform controller for
+  /// testing.
+  ///
+  /// TODO(nkorsote): hide this implementation detail
+  @visibleForTesting
+  FuchsiaWebView({this.fuchsiaWebServices});
+
+  @override
+  Widget build({
+    @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
+    BuildContext context,
+    CreationParams creationParams,
+    WebViewPlatformCreatedCallback onWebViewPlatformCreated,
+    Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
+  }) {
+    assert(webViewPlatformCallbacksHandler != null);
+
+    // TODO(nkorsote): figure what actual ID should be used instead of -1. Also,
+    // consider extracting to a constant.
+    final controller = FuchsiaWebViewPlatformController(
+        -1, webViewPlatformCallbacksHandler, fuchsiaWebServices);
+
+    onWebViewPlatformCreated(controller);
+    return ChildView(
+        connection: controller.fuchsiaWebServices.childViewConnection);
+  }
+
+  @override
+  Future<bool> clearCookies() =>
+      FuchsiaWebViewPlatformController.clearCookies();
+}
diff --git a/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart
new file mode 100644
index 0000000..07e3807
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/lib/src/fuchsia_webview_platform_controller.dart
@@ -0,0 +1,142 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show utf8;
+
+import 'package:fidl_fuchsia_web/fidl_async.dart' as fidl_web;
+import 'package:fidl_fuchsia_net_http/fidl_async.dart' as fidl_net;
+import 'package:webview_flutter/platform_interface.dart';
+
+import 'fuchsia_web_services.dart';
+
+/// Fuchsia [WebViewPlatformController] implementation that serves as the entry
+/// point for all [fuchsia_webview_flutter/webview.dart]'s apis
+class FuchsiaWebViewPlatformController implements WebViewPlatformController {
+  /// Helper class to interact with fuchsia web services
+  FuchsiaWebServices _fuchsiaWebServices;
+
+  final WebViewPlatformCallbacksHandler _platformCallbacksHandler;
+
+  /// Initializes [FuchsiaWebViewPlatformController]
+  FuchsiaWebViewPlatformController(
+      int id, this._platformCallbacksHandler, this._fuchsiaWebServices)
+      : assert(_platformCallbacksHandler != null) {
+    // TODO(nkorsote): remove this prints with an actual impl. The prints are
+    // here to satisfy our strict dart linter for now.
+    print('id: $id');
+    print('_platformCallbacksHandler: $_platformCallbacksHandler');
+    print('fuchsiaWebServices: $fuchsiaWebServices');
+  }
+
+  /// Returns [FuchsiaWebServices]
+  FuchsiaWebServices get fuchsiaWebServices {
+    return _fuchsiaWebServices ??= FuchsiaWebServices();
+  }
+
+  @override
+  Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) {
+    throw UnimplementedError(
+        'FuchsiaWebView addJavascriptChannels is not implemented on the current platform');
+  }
+
+  @override
+  Future<bool> canGoBack() {
+    throw UnimplementedError(
+        'FuchsiaWebView canGoBack is not implemented on the current platform');
+  }
+
+  @override
+  Future<bool> canGoForward() {
+    throw UnimplementedError(
+        'FuchsiaWebView canGoForward is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> clearCache() {
+    throw UnimplementedError(
+        'FuchsiaWebView clearCache is not implemented on the current platform');
+  }
+
+  @override
+  Future<String> currentUrl() async {
+    final navigationState =
+        await fuchsiaWebServices.navigationController.getVisibleEntry();
+    return navigationState.url;
+  }
+
+  @override
+  Future<String> evaluateJavascript(String javascriptString) {
+    throw UnimplementedError(
+        'FuchsiaWebView evaluateJavascript is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> goBack() {
+    throw UnimplementedError(
+        'FuchsiaWebView goBack is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> goForward() {
+    throw UnimplementedError(
+        'FuchsiaWebView goForward is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> loadUrl(
+    String url,
+    Map<String, String> headers,
+  ) async {
+    assert(url != null);
+
+    final headersList = <fidl_net.Header>[];
+    if (headers != null) {
+      headers.forEach((k, v) {
+        headersList
+            .add(fidl_net.Header(name: utf8.encode(k), value: utf8.encode(v)));
+      });
+    }
+
+    return fuchsiaWebServices.navigationController.loadUrl(
+        url,
+        fidl_web.LoadUrlParams(
+          type: fidl_web.LoadUrlReason.typed,
+          headers: headersList,
+        ));
+  }
+
+  @override
+  Future<void> reload() {
+    throw UnimplementedError(
+        'FuchsiaWebView reload is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> removeJavascriptChannels(Set<String> javascriptChannelNames) {
+    throw UnimplementedError(
+        'FuchsiaWebView removeJavascriptChannels is not implemented on the current platform');
+  }
+
+  @override
+  Future<void> updateSettings(WebSettings settings) {
+    throw UnimplementedError(
+        'FuchsiaWebView updateSettings is not implemented on the current platform');
+  }
+
+  /// Clears all cookies for all [WebView] instances.
+  ///
+  /// Returns true if cookies were present before clearing, else false.
+  static Future<bool> clearCookies() {
+    throw UnimplementedError(
+        'FuchsiaWebView clearCookies is not implemented on the current platform');
+  }
+
+  // TODO(nkorsote): implement this method
+  // static Map<String, dynamic> creationParamsToMap(
+  //     CreationParams creationParams) {
+  //   throw UnimplementedError(
+  //       'FuchsiaWebView creationParamsToMap is not implemented on the current platform');
+  // }
+}
diff --git a/public/dart/fuchsia_webview_flutter/lib/webview.dart b/public/dart/fuchsia_webview_flutter/lib/webview.dart
new file mode 100644
index 0000000..e07b6c2
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/lib/webview.dart
@@ -0,0 +1,5 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/fuchsia_webview.dart';
diff --git a/public/dart/fuchsia_webview_flutter/pubspec.yaml b/public/dart/fuchsia_webview_flutter/pubspec.yaml
new file mode 100644
index 0000000..2e71bbe
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/pubspec.yaml
@@ -0,0 +1,7 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+name: fuchsia_webview_flutter
+environment:
+  sdk: '>=2.0.0 <3.0.0'
diff --git a/public/dart/fuchsia_webview_flutter/test/fuchsia_webview_test.dart b/public/dart/fuchsia_webview_flutter/test/fuchsia_webview_test.dart
new file mode 100644
index 0000000..ba1d558
--- /dev/null
+++ b/public/dart/fuchsia_webview_flutter/test/fuchsia_webview_test.dart
@@ -0,0 +1,75 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show utf8;
+
+import 'package:fidl_fuchsia_net_http/fidl_async.dart' as fidl_net;
+import 'package:fidl_fuchsia_web/fidl_async.dart' as fidl_web;
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_webview_flutter/src/fuchsia_web_services.dart';
+import 'package:fuchsia_webview_flutter/src/fuchsia_webview_platform_controller.dart';
+import 'package:fuchsia_webview_flutter/webview.dart';
+import 'package:mockito/mockito.dart';
+import 'package:webview_flutter/platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+// ignore_for_file: implementation_imports
+
+class MockFuchsiaWebServices extends Mock implements FuchsiaWebServices {}
+
+class MockWebViewPlatformCallbacksHandler extends Mock
+    implements WebViewPlatformCallbacksHandler {}
+
+class MockFuchsiaWebViewPlatformController extends Mock
+    implements FuchsiaWebViewPlatformController {}
+
+class MockNavigationControllerProxy extends Mock
+    implements fidl_web.NavigationControllerProxy {}
+
+void main() {
+  FuchsiaWebServices mockWebServices = MockFuchsiaWebServices();
+  fidl_web.NavigationControllerProxy mockNavigationController =
+      MockNavigationControllerProxy();
+
+  group('Custom platform implementation', () {
+    setUpAll(() {
+      when(mockWebServices.navigationController)
+          .thenReturn(mockNavigationController);
+      WebView.platform = FuchsiaWebView(fuchsiaWebServices: mockWebServices);
+    });
+
+    tearDownAll(() {
+      WebView.platform = null;
+    });
+
+    testWidgets('Create WebView', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView());
+    });
+
+    testWidgets('loadUrl', (WidgetTester tester) async {
+      WebViewController controller;
+      await tester.pumpWidget(
+        WebView(
+          onWebViewCreated: (WebViewController webViewController) {
+            controller = webViewController;
+          },
+        ),
+      );
+
+      final headers = <String, String>{'header': 'value'};
+      String url = 'https://google.com';
+      await controller.loadUrl(url, headers: headers);
+
+      verify(mockNavigationController.loadUrl(
+          url,
+          fidl_web.LoadUrlParams(
+            type: fidl_web.LoadUrlReason.typed,
+            headers: [
+              fidl_net.Header(
+                  name: utf8.encode('header'), value: utf8.encode('value'))
+            ],
+          )));
+    });
+  });
+}