You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用flutter link previewer在Flutter的TextFormField实现WhatsApp风格链接预览?

在Flutter TextFormField中实现WhatsApp式链接预览并获取元数据

如果flutter_link_previewer包无法正常工作,可以通过手动解析网页的OG标签(或 fallback 到页面基础元数据)来获取链接的标题、图片和描述,同时实现类似WhatsApp的预览效果,步骤如下:

1. 添加依赖

在pubspec.yaml中添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  html: ^0.15.4

执行flutter pub get完成依赖安装。

2. 定义预览数据模型

创建类存储解析后的链接元数据:

class LinkPreviewData {
  final String? title;
  final String? imageUrl;
  final String? description;
  final String url;

  LinkPreviewData({
    this.title,
    this.imageUrl,
    this.description,
    required this.url,
  });
}

3. 实现链接元数据解析函数

通过HTTP请求目标页面,优先解析OG标签,若不存在则使用页面基础元数据:

import 'package:http/http.dart' as http;
import 'package:html/parser.dart' as parser;
import 'package:html/dom.dart' as dom;

Future<LinkPreviewData?> fetchLinkPreview(String url) async {
  try {
    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      final dom.Document document = parser.parse(response.body);
      final List<dom.Element> metaTags = document.querySelectorAll('meta');
      
      String? title;
      String? imageUrl;
      String? description;

      // 解析OG标签
      for (var tag in metaTags) {
        final String? property = tag.attributes['property'];
        final String? content = tag.attributes['content'];
        if (property == 'og:title') {
          title = content;
        } else if (property == 'og:image') {
          imageUrl = content;
        } else if (property == 'og:description') {
          description = content;
        }
      }

      // 若OG标签不存在,使用页面基础信息
      title ??= document.querySelector('title')?.text;
      description ??= document.querySelector('meta[name="description"]')?.attributes['content'];

      return LinkPreviewData(
        title: title,
        imageUrl: imageUrl,
        description: description,
        url: url,
      );
    }
  } catch (e) {
    print('解析链接预览失败: $e');
  }
  return null;
}

4. 在页面中集成预览功能

监听TextFormField输入,检测到链接后自动解析并展示预览卡片:

import 'package:flutter/material.dart';

class LinkPreviewScreen extends StatefulWidget {
  @override
  _LinkPreviewScreenState createState() => _LinkPreviewScreenState();
}

class _LinkPreviewScreenState extends State<LinkPreviewScreen> {
  final TextEditingController _controller = TextEditingController();
  LinkPreviewData? _previewData;
  // 匹配HTTP/HTTPS链接的正则
  final RegExp _urlRegex = RegExp(r'https?://\S+');

  void _checkForLink() {
    final String text = _controller.text;
    final Match? match = _urlRegex.firstMatch(text);
    if (match != null) {
      final String url = match.group(0)!;
      fetchLinkPreview(url).then((LinkPreviewData? data) {
        setState(() {
          _previewData = data;
        });
      });
    } else {
      setState(() {
        _previewData = null;
      });
    }
  }

  @override
  void initState() {
    super.initState();
    // 监听输入框文本变化
    _controller.addListener(_checkForLink);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('WhatsApp式链接预览')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextFormField(
              controller: _controller,
              maxLines: null,
              decoration: const InputDecoration(
                hintText: '输入带链接的文本...',
                border: OutlineInputBorder(),
              ),
            ),
            // 展示预览卡片
            if (_previewData != null)
              Container(
                margin: const EdgeInsets.only(top: 16.0),
                padding: const EdgeInsets.all(12.0),
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey.shade300),
                  borderRadius: BorderRadius.circular(8.0),
                ),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 预览图片
                    if (_previewData!.imageUrl != null)
                      Container(
                        width: 80,
                        height: 80,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(4.0),
                          image: DecorationImage(
                            image: NetworkImage(_previewData!.imageUrl!),
                            fit: BoxFit.cover,
                            onError: (_, __) => const Icon(Icons.broken_image),
                          ),
                        ),
                      ),
                    const SizedBox(width: 12.0),
                    // 预览文本内容
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          if (_previewData!.title != null)
                            Text(
                              _previewData!.title!,
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                                fontSize: 16.0,
                              ),
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                          if (_previewData!.description != null)
                            Padding(
                              padding: const EdgeInsets.symmetric(vertical: 4.0),
                              child: Text(
                                _previewData!.description!,
                                style: TextStyle(color: Colors.grey.shade600),
                                maxLines: 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ),
                          Text(
                            _previewData!.url,
                            style: TextStyle(color: Colors.grey.shade500, fontSize: 12.0),
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
          ],
        ),
      ),
    );
  }
}

注意事项

  • 网络权限:在Android的AndroidManifest.xml中添加<uses-permission android:name="android.permission.INTERNET" />;若需访问HTTP链接,iOS需在Info.plist中配置NSAppTransportSecurity例外。
  • 性能优化:可添加输入防抖逻辑,避免用户输入时频繁发起网络请求。
  • 错误处理:可进一步完善图片加载失败、链接不可访问等场景的UI提示。

内容的提问来源于stack exchange,提问作者krishna veni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 06:05:07