mcpField
L
lit-and-flutter

lit-and-flutter

github.com/rodydavis開発6039 回インストール2 週間前に更新
リポジトリ

lit-and-flutter について

Lit and Flutter は、Lit の Web コンポーネントを Flutter のウィジェットツリーに組み込む方法を紹介するチュートリアルです。Web UI と Flutter のデバイス API を組み合わせ、iOS・Android 向けアプリを作る開発者を対象としています。

主な機能

  1. 01Flutter への Lit コンポーネント埋め込み
  2. 02Web イベントから Flutter API を呼び出し
  3. 03iOS・Android 向けビルド
  4. 04更新された Web コンポーネントコードの読み込み
  5. 05初回ダウンロード後のオフライン利用

活用シーン

  1. 01既存の Web アプリを Flutter でラップ
  2. 02Web イベントからアプリ内購入 API を呼び出し
  3. 03Web UI にネイティブ機能を追加
  4. 04ハイブリッドモバイルアプリの構築

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/lit-and-flutterを入力して読み込まれたか確認します。

詳細

Lit と Flutter

この記事では、Lit の Web コンポーネントを設定し、Flutter のウィジェットツリー内でインラインに使用する方法を説明します。

TLDR 最終的なソースコードはこちらで確認できます。

この連携を使う理由は、既存の Web アプリ、またはその一部分だけをウィジェットツリーに埋め込めることです。

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 はデプロイ先の Web サイトの URL に、BUNDLE_URL は JS バンドルへの相対パスに更新する必要があります。

これにより、新しいバージョンが公開されたときに自動更新され、キャッシュが古くなります。また、初回ダウンロード後はオフラインでも利用できるようになります。

実行 

これでアプリケーションを実行できますが、セットアップにはいくつかの手順が必要です。

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 コンポーネントの間で双方向通信できることを確認できます。

まとめ 

ソースコードを確認したい場合はこちらをご覧ください。それでは、お読みいただきありがとうございました。ご質問があればぜひお知らせください。

コメント