🦾

Apple IntelligenceのFoundation ModelをFlutterから簡単に使う

に公開

本記事はCommune Developers Advent Calendar 2025の18日目の記事です。
https://qiita.com/advent-calendar/2025/commune

FlutterでFoundation Modelを試したい

Foundation Model試したいなら、Swiftを書けばいいんだけなんですが
私は普段から個人開発で使いなれているFlutterで試してみたかったのです。
どうしても。

Foundation Modelとは

Foundation Modelとは、Appleが提供する、オンデバイス(デバイス内)で動作する大規模言語モデル(LLM)を利用するための開発者向けフレームワークです。
これは「Apple Intelligence」の中核をなす技術であり、開発者がユーザーのプライバシーを保護しつつ、オフラインでも利用可能なAI機能をアプリに組み込むことを可能にします。
https://www.youtube.com/watch?v=mJMvFyBvZEk&t=7s

foundation_models_frameworkを使うと簡単に使えそう!!

https://pub.dev/packages/foundation_models_framework

試してみた

foundation_models_frameworkを使って、FlutterでFoundation Modelによる
シンプルな要約アプリをつくってみました。

環境

  • 実機のiPhone 15 Pro
  • iOS: 26.2
  • Xcode: 26.2

導入

flutter pub add foundation_models_framework

コード

公式のREADME.mdを参考に、Claude Codeに全部書かせました。

コード
main.dart
import 'package:flutter/material.dart';
import 'package:foundation_models_framework/foundation_models_framework.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AI要約アプリ',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const SummaryPage(),
    );
  }
}

class SummaryPage extends StatefulWidget {
  const SummaryPage({super.key});

  
  State<SummaryPage> createState() => _SummaryPageState();
}

class _SummaryPageState extends State<SummaryPage> {
  final _textController = TextEditingController();
  final _foundationModels = FoundationModelsFramework.instance;

  String _summary = '';
  bool _isLoading = false;
  bool _isAvailable = false;
  String _availabilityMessage = '';

  
  void initState() {
    super.initState();
    _checkAvailability();
  }

  
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  Future<void> _checkAvailability() async {
    try {
      final availability = await _foundationModels.checkAvailability();
      setState(() {
        _isAvailable = availability.isAvailable;
        _availabilityMessage = availability.isAvailable
            ? 'Apple Intelligenceが利用可能です'
            : '利用不可: ${availability.errorMessage ?? "Apple Intelligenceが有効になっていません"}';
      });
    } catch (e) {
      setState(() {
        _isAvailable = false;
        _availabilityMessage = 'エラー: $e';
      });
    }
  }

  Future<void> _summarizeText() async {
    if (_textController.text.trim().isEmpty) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('テキストを入力してください')));
      return;
    }

    if (!_isAvailable) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Apple Intelligenceが利用できません')),
      );
      return;
    }

    setState(() {
      _isLoading = true;
      _summary = '';
    });

    try {
      final prompt = '以下のテキストを簡潔に要約してください:\n\n${_textController.text}';

      final response = await _foundationModels.sendPrompt(
        prompt,
        guardrailLevel: GuardrailLevel.standard,
      );

      setState(() {
        _summary = response.content;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _summary = 'エラーが発生しました: $e';
        _isLoading = false;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AI要約アプリ'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 可用性ステータス
            Card(
              color: _isAvailable
                  ? Colors.green.shade50
                  : Colors.orange.shade50,
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: Row(
                  children: [
                    Icon(
                      _isAvailable ? Icons.check_circle : Icons.warning,
                      color: _isAvailable ? Colors.green : Colors.orange,
                    ),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        _availabilityMessage,
                        style: TextStyle(
                          color: _isAvailable
                              ? Colors.green.shade900
                              : Colors.orange.shade900,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),

            // テキスト入力エリア
            TextField(
              controller: _textController,
              maxLength: null,
              maxLines: 8,
              decoration: const InputDecoration(
                labelText: '要約したいテキストを入力',
                hintText: 'ここに長いテキストを入力してください...',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),

            // 要約ボタン
            FilledButton.icon(
              onPressed: _isLoading ? null : _summarizeText,
              icon: _isLoading
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.auto_awesome),
              label: Text(_isLoading ? '要約中...' : '要約する'),
            ),
            const SizedBox(height: 24),

            // 要約結果表示
            if (_summary.isNotEmpty) ...[
              const Text(
                '要約結果:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 12),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(_summary, style: const TextStyle(fontSize: 16)),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

実機で動かしてみた

要約元テキスト

アドベントカレンダーは、キリスト教における「待降節(アドベント)」の期間を数えるためのカレンダーである。待降節とは、イエス・キリストの降誕を祝うクリスマス(12月25日)までの準備期間で、一般的には11月末から12月24日までの約4週間を指す。この期間を意識的に過ごすための道具として、アドベントカレンダーが生まれた。

起源は19世紀のドイツにさかのぼる。当時のプロテスタント家庭では、子どもたちがクリスマスまでの日数を実感できるよう、壁にチョークで線を引いたり、ろうそくに印を付けたりする習慣があった。やがて、日付ごとに小さな扉を開ける形式の紙製カレンダーが登場し、これが現在のアドベントカレンダーの原型となった。

20世紀初頭には、印刷技術の発展とともに商業化が進み、イラスト付きのアドベントカレンダーが広く普及した。特に有名なのが、扉の中に聖書の言葉や小さな絵が描かれたタイプである。第二次世界大戦中は一時的に生産が途絶えたものの、戦後になると再び人気を取り戻し、やがてチョコレートや小さなおもちゃを入れたカレンダーが登場した。

近年では、宗教的な枠を超えて、化粧品や紅茶、レゴなど多様なアドベントカレンダーが販売されている。また、IT業界では「アドベントカレンダー」という言葉が転用され、12月に日替わりで記事を公開するイベント文化として定着している。このように、アドベントカレンダーは時代や分野に応じて姿を変えながら、「何かを楽しみに待つ」という本質を保ち続けている。

要約後テキスト:

アドベントカレンダーは、キリスト教の待降節(クリスマスまでの4週間)を数えるためのカレンダーで、19世紀のドイツに起源を持つ。
当初は小さな扉を開ける紙製のものから始まり、商業化と共にチョコレートやおもちゃが入ったものへと進化した。
近年では宗教的枠を超え、化粧品やITイベントなど多様な形に広がりつつも、「何かを楽しみに待つ」という本質を維持している。

感想

Flutterからの利用だが、swiftを書かずに使えるのは楽でした。
細かい設定などは今後もう少し触ってみて検証していきたいと思います。
また、Android版の同等の機能もあればFlutterで楽に実装できるライブラリがないか試してみたいです。
Flutterで簡単に利用できれば個人開発のアプリにも導入していきたいなと思います。

コミューン株式会社

Discussion