lit-and-flutter
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLit and Flutter
Lit与Flutter
In this article I will go over how to set up a Lit web component and use it inline in the Flutter widget tree.
TLDR You can find the final source here.
The reason you would want this integration is so you can take an existing web app, or just a single part of it and embed it in the widget tree.
With it wrapped in Flutter you can call device APIs from event listeners on your web component.
For example you may have an app that handles purchases, and now you can call the in app purchase API or other device specific features not available on the web.
You also get a cross platform app that can be delivered to both Google Play and the App Store.
The web component will receive new code each time you update your site, so you do not have to ship an update each time the web component changes.
Prerequisites
前置要求
- Flutter SDK
- Xcode and Command Line Tools
- Android SDK
- Vscode
- Node
- Typescript
- Flutter SDK
- Xcode及命令行工具
- Android SDK
- Vscode
- Node
- Typescript
Getting Started
开始上手
We can start off by creating a empty directory and naming it with whatever we want.
snake_casemkdir flutter_lit_example
cd flutter_lit_example我们可以先创建一个空目录,并用你喜欢的蛇形命名法(snake_case)为其命名。
mkdir flutter_lit_example
cd flutter_lit_exampleWeb Setup
Web端配置
Now we are in the directory and can setup Flutter and Lit. Let's start with node.
flutter_lit_examplenpm init -y
npm i lit
npm i -D typescript vite @types/nodeThis will setup the basics for a node project and install the packages we need. Now lets add some config files.
touch tsconfig.json
touch vite.config.tsThis will create 2 files. Now open up and paste the following:
tsconfig.json{
"compilerOptions": {
"module": "esnext",
"lib": [
"es2017",
"dom",
"dom.iterable"
],
"types": [
"vite/client"
],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}This is a basic typescript config. Now open up and paste the following:
vite.config.tsimport { defineConfig } from "vite";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
base: "/flutter_lit_example/", // TODO: Name of your github repo
build: {
outDir: "build/web",
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
},
input: {
main: resolve(__dirname, "index.html"),
// TODO: Create a new module for each component you want to embed
},
},
},
});Now we need to create our web component:
mkdir src
cd src
touch my-app.ts
cd ..Open and paste the following:
my-app.tsimport { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("my-app")
export class MyApp extends LitElement {
static styles = css`
p {
color: blue;
}
`;
@property()
name = "Somebody";
render() {
return html`<div>
<p>Hello, ${this.name}!</p>
<slot></slot>
</div>`;
}
}We need to create a for our web app.
index.htmltouch index.htmlOpen and paste the following:
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Example</title>
<script type="module" src="/src/my-app.ts"></script>
<style>
body {
padding: 0;
margin: 0;
}
my-app {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<my-app></my-app>
</body>
</html>现在我们处于目录中,可以开始配置Flutter和Lit。首先从Node环境开始:
flutter_lit_examplenpm init -y
npm i lit
npm i -D typescript vite @types/node这会完成Node项目的基础配置并安装所需依赖包。接下来添加配置文件:
touch tsconfig.json
touch vite.config.ts这会创建两个文件。打开并粘贴以下内容:
tsconfig.json{
"compilerOptions": {
"module": "esnext",
"lib": [
"es2017",
"dom",
"dom.iterable"
],
"types": [
"vite/client"
],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}这是一个基础的TypeScript配置。打开并粘贴以下内容:
vite.config.tsimport { defineConfig } from "vite";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
base: "/flutter_lit_example/", // TODO: 你的GitHub仓库名称
build: {
outDir: "build/web",
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
},
input: {
main: resolve(__dirname, "index.html"),
// TODO: 为每个要嵌入的组件创建新模块
},
},
},
});现在我们需要创建Web组件:
mkdir src
cd src
touch my-app.ts
cd ..打开并粘贴以下内容:
my-app.tsimport { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("my-app")
export class MyApp extends LitElement {
static styles = css`
p {
color: blue;
}
`;
@property()
name = "Somebody";
render() {
return html`<div>
<p>Hello, ${this.name}!</p>
<slot></slot>
</div>`;
}
}我们需要为Web应用创建文件:
index.htmltouch index.html打开并粘贴以下内容:
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Example</title>
<script type="module" src="/src/my-app.ts"></script>
<style>
body {
padding: 0;
margin: 0;
}
my-app {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<my-app></my-app>
</body>
</html>Flutter Setup
Flutter端配置
Now that we have the basics setup for web we can move on to flutter. Let's create the project with the following:
flutter create --platforms=ios,android .
flutter packages getOpen up and update it with the following:
pubspec.yamlname: flutter_lit_example
description: A hybrid Flutter app.
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^5.3.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: trueMake sure to get the packages again:
flutter packages getNow we need to create the file that will wrap the web component.
cd lib
touch web_component.dart
cd ..Open and paste the following:
web_component.dartimport 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class WebComponent extends StatefulWidget {
const WebComponent({
Key key,
@required this.name,
@required this.bundle,
this.attributes = const {},
this.slot = '',
this.events = const [],
}) : super(key: key);
final String name, bundle;
final Map<String, String> attributes;
final String slot;
final List<EventCallback> events;
@override
_WebComponentState createState() => _WebComponentState();
}
class _WebComponentState extends State<WebComponent> {
InAppWebViewController controller;
final Map<String, List<EventCallback>> _events = {};
String get source {
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
padding: 0;
margin: 0;
}
${widget.name} {
width: 100%;
height: 100vh;
}
</style>
<script type="module" crossorigin src="${widget.bundle}"></script>
</head>
<body>
<${widget.name} ${widget.attributes.entries.map((e) => '${e.key}="${e.value}"').join(' ')}>
${widget.slot}
</${widget.name}>
<script>
window.addEventListener("flutterInAppWebViewPlatformReady", (event) => {
${widget.events.join('\n')}
});
</script>
</body>
</html>
''';
}
void _setup(InAppWebViewController controller) {
this.controller = controller;
this._setupEvents();
}
void _setupEvents() {
for (final event in _events.keys) {
controller.removeJavaScriptHandler(handlerName: event);
}
for (final event in widget.events) {
_addEvent(event);
}
}
void _addEvent(EventCallback event) {
controller.addJavaScriptHandler(
handlerName: event.query,
callback: event.onPressed,
);
_events[event.event] ??= [];
_events[event.event].add(event);
}
@override
void didUpdateWidget(covariant WebComponent oldWidget) {
if (oldWidget.events != widget.events) {
_setupEvents();
}
if (oldWidget.slot != widget.slot ||
oldWidget.bundle != widget.bundle ||
oldWidget.name != widget.name) {
controller.loadData(data: source);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return InAppWebView(
initialData: InAppWebViewInitialData(data: source),
onWebViewCreated: _setup,
);
}
}
class EventCallback {
EventCallback({
@required this.onPressed,
@required this.event,
this.query,
});
final String query, event;
final dynamic Function(List<dynamic> args) onPressed;
@override
String toString() => _source;
String get _prefix => query != null && query.isNotEmpty
? 'document.querySelector("$query")'
: 'document.body';
String get _source => [
'$_prefix.addEventListener("$event", (e) => {',
' window.flutter_inappwebview.callHandler("$query", e);',
'}, false);',
].join('\n');
}Open and paste it with th following:
main.dartimport 'package:flutter/material.dart';
import 'web_component.dart';
const WEBSITE_URL = 'https://rodydavis.github.io/flutter_lit_example/';
const BUNDLE_PATH = 'assets/main.js';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final title = 'Flutter Hybrid App';
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: title,
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Builder(
builder: (context) => WebComponent(
name: 'my-app',
bundle: '$WEBSITE_URL/$BUNDLE_PATH',
attributes: {
'name': widget.title,
},
slot: '<button id="my-button">Talk back!</button>',
events: [
EventCallback(
event: 'click',
query: '#my-button',
onPressed: (_) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Clicked!')));
},
),
],
),
),
);
}
}You will need to update to have the url of the website where you will be deploying and to the relative path to the js bundle.
WEBSITE_URLBUNDLE_URLThis will ensure auto updates with a new version rolls out and the cache it stale. This will also allow for offline support after the first time it is downloaded.
完成Web端基础配置后,我们可以开始配置Flutter。使用以下命令创建项目:
flutter create --platforms=ios,android .
flutter packages get打开并更新为以下内容:
pubspec.yamlname: flutter_lit_example
description: A hybrid Flutter app.
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^5.3.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true确保再次获取依赖包:
flutter packages get现在我们需要创建用于封装Web组件的文件:
cd lib
touch web_component.dart
cd ..打开并粘贴以下内容:
web_component.dartimport 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class WebComponent extends StatefulWidget {
const WebComponent({
Key key,
@required this.name,
@required this.bundle,
this.attributes = const {},
this.slot = '',
this.events = const [],
}) : super(key: key);
final String name, bundle;
final Map<String, String> attributes;
final String slot;
final List<EventCallback> events;
@override
_WebComponentState createState() => _WebComponentState();
}
class _WebComponentState extends State<WebComponent> {
InAppWebViewController controller;
final Map<String, List<EventCallback>> _events = {};
String get source {
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
padding: 0;
margin: 0;
}
${widget.name} {
width: 100%;
height: 100vh;
}
</style>
<script type="module" crossorigin src="${widget.bundle}"></script>
</head>
<body>
<${widget.name} ${widget.attributes.entries.map((e) => '${e.key}="${e.value}"').join(' ')}>
${widget.slot}
</${widget.name}>
<script>
window.addEventListener("flutterInAppWebViewPlatformReady", (event) => {
${widget.events.join('\n')}
});
</script>
</body>
</html>
''';
}
void _setup(InAppWebViewController controller) {
this.controller = controller;
this._setupEvents();
}
void _setupEvents() {
for (final event in _events.keys) {
controller.removeJavaScriptHandler(handlerName: event);
}
for (final event in widget.events) {
_addEvent(event);
}
}
void _addEvent(EventCallback event) {
controller.addJavaScriptHandler(
handlerName: event.query,
callback: event.onPressed,
);
_events[event.event] ??= [];
_events[event.event].add(event);
}
@override
void didUpdateWidget(covariant WebComponent oldWidget) {
if (oldWidget.events != widget.events) {
_setupEvents();
}
if (oldWidget.slot != widget.slot ||
oldWidget.bundle != widget.bundle ||
oldWidget.name != widget.name) {
controller.loadData(data: source);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return InAppWebView(
initialData: InAppWebViewInitialData(data: source),
onWebViewCreated: _setup,
);
}
}
class EventCallback {
EventCallback({
@required this.onPressed,
@required this.event,
this.query,
});
final String query, event;
final dynamic Function(List<dynamic> args) onPressed;
@override
String toString() => _source;
String get _prefix => query != null && query.isNotEmpty
? 'document.querySelector("$query")'
: 'document.body';
String get _source => [
'$_prefix.addEventListener("$event", (e) => {',
' window.flutter_inappwebview.callHandler("$query", e);',
'}, false);',
].join('\n');
}打开并粘贴以下内容:
main.dartimport 'package:flutter/material.dart';
import 'web_component.dart';
const WEBSITE_URL = 'https://rodydavis.github.io/flutter_lit_example/';
const BUNDLE_PATH = 'assets/main.js';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final title = 'Flutter Hybrid App';
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: title,
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Builder(
builder: (context) => WebComponent(
name: 'my-app',
bundle: '$WEBSITE_URL/$BUNDLE_PATH',
attributes: {
'name': widget.title,
},
slot: '<button id="my-button">Talk back!</button>',
events: [
EventCallback(
event: 'click',
query: '#my-button',
onPressed: (_) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Clicked!')));
},
),
],
),
),
);
}
}你需要将更新为你部署网站的地址,将更新为JS包的相对路径。
WEBSITE_URLBUNDLE_URL这样当新版本发布且缓存过期时,应用会自动更新。同时首次下载后还能支持离线使用。
Running
运行项目
Now we can run our application but it requires a few steps to get it all setup.
To test and build our web app locally we will use vite and render the
index.htmlnpm i
npm run devYou should see the following:
vite v2.2.3 dev server running at:
Local: http://localhost:3000/flutter_lit_example/
Network: http://192.168.1.143:3000/flutter_lit_example/
ready in 311ms.We can open the link to see our running web app and hot reload changes from .
http://localhost:3000/flutter_lit_example/my-app.tsIf you want to learn more about Lit you can read the docs here.
Once you are happy with how it looks we can move on to Flutter to wrap it in a native app. This will give us access to native code if we wanted to use the in app purchase api or push notifications.
Kill the terminal and run the following:
flutter packages get
flutter build ios
flutter build appbundle
flutter runThis should select a running device or prompt you to select one. Now that it is running on the device you can see we have two way communication with the Flutter app and the web component.
现在我们可以运行应用,但需要完成一些配置步骤。
要在本地测试和构建Web应用,我们将使用vite来渲染:
index.htmlnpm i
npm run dev你应该会看到以下输出:
vite v2.2.3 dev server running at:
Local: http://localhost:3000/flutter_lit_example/
Network: http://192.168.1.143:3000/flutter_lit_example/
ready in 311ms.你可以打开链接查看运行中的Web应用,修改后还能热重载。
http://localhost:3000/flutter_lit_example/my-app.ts如果你想了解更多关于Lit的内容,可以查看官方文档:链接。
当你对Web组件的效果满意后,就可以在Flutter中进行封装,将其打包为原生应用。这样你就能调用原生代码,比如内购API或推送通知。
关闭终端并运行以下命令:
flutter packages get
flutter build ios
flutter build appbundle
flutter run这会自动选择运行中的设备,或提示你选择设备。应用运行后,你会看到Flutter应用与Web组件之间实现了双向通信。