mcpField
L
lit-and-flutter

lit-and-flutter

github.com/rodydavis开发6039 次安装2 周前更新
代码仓库

关于 lit-and-flutter

Lit and Flutter 是一篇教程,介绍如何将 Lit Web 组件嵌入 Flutter Widget 树,面向希望结合 Web 界面与 Flutter 设备 API 并构建 iOS、Android 应用的开发者。

主要功能

  1. 01在 Flutter 中嵌入 Lit 组件
  2. 02将 Web 事件连接到 Flutter API
  3. 03构建 iOS 和 Android 应用
  4. 04加载更新后的 Web 组件代码
  5. 05首次下载后支持离线使用

适用场景

  1. 01在 Flutter 中封装现有 Web 应用
  2. 02从 Web 事件调用应用内购买 API
  3. 03为 Web 界面添加原生设备功能
  4. 04构建混合移动应用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rodydavis/skills --skill lit-and-flutter

安装后在 Claude Code 中输入/lit-and-flutter确认已加载。

详细介绍

Lit 和 Flutter

本文将介绍如何设置 Lit Web 组件,并在 Flutter widget 树中内嵌使用。

简而言之,你可以在此处找到最终源码。

进行这项集成的原因是,你可以将现有 Web 应用,或其中的某个部分,嵌入 widget 树中。

将其封装在 Flutter 中后,你可以从 Web 组件的事件监听器调用设备 API。

例如,你可能有一个处理购买的应用,现在可以调用应用内购买 API 或其他 Web 上不支持的设备专属功能。

你还可以构建跨平台应用,并将其发布到 Google Play 和 App Store。

每次更新网站时,Web 组件都会获取新代码,因此每次 Web 组件发生变化时,你都无需发布更新。

前提条件

  • Flutter SDK
  • Xcode 和 Command Line Tools
  • Android SDK
  • Vscode
  • Node
  • Typescript

开始使用

首先,我们可以创建一个空目录,并用任意名称为其命名,名称采用 snake_case 格式。

mkdir flutter_lit_example
cd flutter_lit_example
Web 设置

现在我们位于 flutter_lit_example 目录中,可以设置 Flutter 和 Lit。先从 Node 开始。

npm init -y
npm i lit
npm i -D typescript vite @types/node

这将为 Node 项目设置基础环境,并安装所需的软件包。接下来添加一些配置文件。

touch tsconfig.json
touch vite.config.ts

这将创建 2 个文件。现在打开 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.ts 并粘贴以下内容:

import { 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
      },
    },
  },
});

现在需要创建 Web 组件:

mkdir src
cd src
touch my-app.ts
cd ..

打开 my-app.ts 并粘贴以下内容:

import { 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.html。

touch 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 设置

现在 Web 的基础设置已经完成,可以继续设置 Flutter。使用以下命令创建项目:

flutter create --platforms=ios,android .
flutter packages get

打开 pubspec.yaml 并使用以下内容更新它:

name: 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.dart 并粘贴以下内容:

import '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.dart 并粘贴以下内容:

import '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_URL 更新为要部署网站的 URL,并将 BUNDLE_URL 更新为 JS bundle 的相对路径。

这样可以确保在新版本发布时自动更新,并清除过期缓存。首次下载后,这也能提供离线支持。

运行

现在可以运行应用了,但需要先完成几个步骤。

要在本地测试和构建 Web 应用,我们将使用 vite 并渲染 index.html。

npm 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.

打开链接 http://localhost:3000/flutter_lit_example/,即可查看正在运行的 Web 应用,并体验 my-app.ts 的热重载更新。

如果想进一步了解 Lit,可以阅读此处的文档。

确认效果符合预期后,就可以继续使用 Flutter 将其封装成原生应用。这样,如果需要使用应用内购买 API 或推送通知,我们就能访问原生代码。

关闭终端,然后运行以下命令:

flutter packages get
flutter build ios
flutter build appbundle
flutter run

这应该会选择一个正在运行的设备,或提示你选择设备。应用在设备上运行后,你会发现 Flutter 应用与 Web 组件之间可以双向通信。

总结

如果想查看源代码,可以前往此处;感谢阅读!如有任何问题,欢迎告诉我!

评论

支持 Markdown