Loading...
Loading...
Learn how to embed a Lit web component directly within your Flutter app to leverage web-based UIs and features while accessing native device APIs for a powerful hybrid development approach.
npx skill4agent add rodydavis/skills lit-and-flutterTLDR You can find the final source here.
snake_casemkdir flutter_lit_example
cd flutter_lit_exampleflutter_lit_examplenpm init -y
npm i lit
npm i -D typescript vite @types/nodetouch tsconfig.json
touch vite.config.tstsconfig.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": []
}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
},
},
},
});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>`;
}
}index.htmltouch index.htmlindex.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 create --platforms=ios,android .
flutter packages getpubspec.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: trueflutter packages getcd 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!')));
},
),
],
),
),
);
}
}WEBSITE_URLBUNDLE_URLindex.htmlnpm i
npm run devvite 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.http://localhost:3000/flutter_lit_example/my-app.tsflutter packages get
flutter build ios
flutter build appbundle
flutter run