こんにちは、株式会社Pentagonでアプリ開発をしている中原です。
SNSプラットフォームのTwitter
やLINE
では、URL
を送信するとそのページの内容が一部リンクプレビュー
として表示されます。この記事では、そのようなリンクプレビュー
をウェブページのURLから表示する実装方法を説明します。
【こんな人に読んで欲しい】
- Flutterアプリ開発エンジニア。
- ウェブページのURLからリンクプレビューを表示したい方。
【この記事を読むメリット】
- ウェブページのURLからリンクプレビューを表示する実装方法を学べます。
【結論】
any_link_preview
パッケージを追加し、AnyLinkPreview()
ウィジェットの引数にURL
を指定するだけで、そのページのリンクプレビュー
を表示することができます。これにより、ユーザー
はリンク
をクリックする前に、その内容の一部を確認できます。
このパッケージは、指定されたURL
からメタデータ
を取得し、それを基にリンク
のリンクプレビュー
を生成します。これにより、html
、dio
、url_launcher
などのパッケージを個別に追加する必要がなくなります。
この記事で触れないこと
この記事では、以下の実装や説明については省略しています。
- Flutterの開発環境構築
- AsyncNotifierProviderの説明
- Freezedの説明
開発環境
- Flutter 3.10.6
- Dart 3.0.6
- vscode
パッケージ
dependencies:
flutter:
sdk: flutter
# any_link_previewを追加
any_link_preview: ^3.0.1
flutter_riverpod: ^2.4.0
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# 文字列のURLからURL先に遷移するため追加。
url_launcher: ^6.1.14
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
freezed: ^2.4.2
build_runner: ^2.4.6
json_serializable: ^6.7.1
any_link_preview
パッケージを追加することで、URLのリンクプレビュー
を表示できるようになります。さらに、url_launcher
を追加することで、文字列のURLから直接URL先に移動することが可能になります。また、AnyLinkPreview()
ウィジェットのurlLaunchMode
を変更する際にも、url_launcher
の追加が必要です。これにより、URLの起動モード
を自由に設定することができます。
url_launcher
の起動モードには以下の4種類があります。
LaunchMode.inAppWebView
:デフォルトで指定されているモードで、遷移先のページをアプリ内の一つのページで開いているかのように遷移します。LaunchMode.externalApplication
:Webページを Safari などの別のアプリケーションで開きます。LaunchMode.externalNonBrowserApplication
:ブラウザ以外の外部のアプリケーションを開く際に使用します。LaunchMode.platformDefault
:プラットフォームのデフォルトの遷移方法を採用します。
使用するメソッドの実装
import 'package:any_link_preview/any_link_preview.dart';
class Utils {
// URLの正規表現
static final urlRegExp = RegExp(
r"((https?:www\.)|(https?:\/\/)|(www\.))[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?");
// メッセージからURLを抽出してリストに追加するメソッド
static List<String> getMathUrlList(String message) {
final Iterable<Match> urlMatches = urlRegExp.allMatches(message);
final List<String> urls = urlMatches
.map((urlMatch) => message.substring(urlMatch.start, urlMatch.end))
.toList();
return urls;
}
// URLが有効かどうかをチェックするメソッド
static bool isValidUrl({required String url}) {
// URLが有効かどうかをチェックする
final bool isUrlValid = AnyLinkPreview.isValidLink(
url, // 引数で受け取ったURL
protocols: ['http', 'https'], // 有効なプロトコルをリストで指定
hostWhitelist: [], // 有効なURLホストをリストで指定
hostBlacklist: [], // 無効なURLホストをリストで指定
);
return isUrlValid;
}
}
Messageモデルの実装
import 'package:freezed_annotation/freezed_annotation.dart';
part 'message.freezed.dart';
part 'message.g.dart';
@freezed
class Message with _$Message {
const factory Message({
required String text,
@Default(false) bool showPreview,
}) = _Message;
factory Message.fromJson(Map<String, dynamic> json) =>
_$MessageFromJson(json);
const Message._();
}
ViewModelの実装
Future<void> addMessage({required String message}) async {
// stateをローディングにする(値がまだ利用可能でない状態)
state = const AsyncValue.loading();
// 1. メッセージからURLを抽出してリストに追加
final urlList = Utils.getMathUrlList(message);
// 2. 複数のURLがある場合は、最初のURLを取得
final url = urlList.isNotEmpty ? urlList.first : '';
// 3. URLが有効かどうかをチェックする
final isValidUrl = url.isNotEmpty ? Utils.isValidUrl(url: url) : false;
// 4. 結果をMessageにセットする
final result = Message(text: message, url: url, showPreview: isValidUrl);
// 5. Stateを更新する
state = await AsyncValue.guard(() async {
// サーバーに保存しないため、リストに追加
return [
...state.value!,
result,
];
});
}
}
- 入力されたテキストからURLを抽出し、それをリストに追加する機能を実装しています。これにより、テキスト内のURLを効率的に取り出すことができます。
- 複数のURLがリストに存在する場合、最初のURLを取得します。ただし、複数のURLを表示したい場合は、このステップは必要ありません。
- 抽出したURLが有効なものであるかどうかを確認するバリデーションを行います。これにより、無効なURLを排除し、有効なURLだけを取り扱うことができます。
- 結果をMessageにセットします。
- 最後に、Stateを更新します。
Textウィジェットの拡張
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'utils.dart';
// Textウィジェットを拡張
extension TextEx on Text {
RichText urlToLink(
BuildContext context,
) {
final textSpans = <InlineSpan>[];
data?.splitMapJoin(
Utils.urlRegExp,
// Utils.urlRegExpの正規表現にマッチした場合
onMatch: (Match match) {
final matchUrl = match[0] ?? '';
textSpans.add(
TextSpan(
text: matchUrl,
// URLの場合は青色にする
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
// URLをタップした時の処理 url_launcher
recognizer: TapGestureRecognizer()
..onTap = () async => await launchUrl(
Uri.parse(matchUrl),
),
),
);
return '';
},
// Utils.urlRegExpの正規表現にマッチしなかった場合
onNonMatch: (String text) {
textSpans.add(
TextSpan(
text: text,
style: const TextStyle(
color: Colors.black,
),
),
);
return '';
},
);
return RichText(text: TextSpan(children: textSpans));
}
}
テキスト
とURL
を表示するために、Text
ウィジェットを拡張しています。具体的には、Utils.urlRegExp
を使用してテキストを解析し、マッチした部分をURL
として表示します。マッチしない部分は、通常の文字列
として表示されます。これにより、テキスト内のURL
を適切にハイライトし、ユーザーがそれを認識しやすくなります。
カードコンポーネントの実装
import 'package:any_link_preview/any_link_preview.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
class CardItem extends StatelessWidget {
const CardItem({
super.key,
required this.url,
});
final String url;
@override
Widget build(BuildContext context) {
// UIサイズを変更する場合は、SizedBoxでサイズを指定
return SizedBox(
width: 250,
height: 200,
child: AnyLinkPreview(
link: url, // 表示するURL
backgroundColor: Colors.green[100], // カード title、bodyの背景色
bodyMaxLines: 2, // 説明文の最大行数を指定
urlLaunchMode: LaunchMode
.externalApplication, // URLを開く方法を指定(デフォルトはplatformDefault) 変更する場合はurl_launcherパッケージをインストールする必要がある
// エラー時のカードの表示ウィジェット
// errorTitle: , // エラー時のタイトルを指定
// errorBody: , // エラー時の説明文を指定
// errorImage: , // エラー時の画像を指定
// エラー時のカードの表示ウィジェットを指定
errorWidget: Container(
color: Colors.grey[300],
child: const Center(child: Text('エラーが発生しました')),
),
),
);
}
}
AnyLinkPreview()
ウィジェットを使用すると、指定したURL
のリンクプレビュー
を表示できます。これは自由にカスタマイズ可能なので、ぜひ試してみてください。
画面の実装
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_card_flutter/extention.dart';
import 'package:url_card_flutter/card_item.dart';
import 'package:url_card_flutter/message_async_notifie.dart';
void main() {
runApp(const ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncValue = ref.watch(messageAsyncNotifierProvider);
final notifier = ref.watch(messageAsyncNotifierProvider.notifier);
final TextEditingController messageController = TextEditingController();
return MaterialApp(
debugShowCheckedModeBanner: false,
home: GestureDetector(
onTap: () => primaryFocus?.unfocus(),
child: Scaffold(
body: asyncValue.when(
loading: () =>
const Center(child: CircularProgressIndicator()), // ローディング時の処理
error: (error, _) =>
Center(child: Text('Error: $error')), // エラー時の処理
data: (data) => data.isNotEmpty
? ListView.builder(
itemCount: data.length,
itemBuilder: ((BuildContext context, int index) {
final message = data[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(
height: 10,
),
// ----- 入力したメッセージを表示 -----
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.green[100],
borderRadius: BorderRadius.circular(10)),
child: Text(message.text).urlToLink(context)),
const SizedBox(
height: 20,
),
// ----- URLが有効な場合のみカードを表示 -----
Visibility(
visible: message.showPreview,
child: Column(
children: [
CardItem(
url: message.url ?? '',
),
const SizedBox(
height: 10,
),
],
),
),
],
),
);
}))
: const SizedBox.shrink(),
),
bottomSheet: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: Row(
children: [
// ----- メッセージ入力 -----
Expanded(
child: TextFormField(
controller: messageController,
maxLines: 3,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '入力してください。',
),
)),
GestureDetector(
onTap: () async {
notifier.addMessage(
message: messageController.text,
);
messageController.clear();
},
child: const Icon(Icons.play_arrow))
],
),
),
),
),
);
}
}
メッセージにURL
が含まれている場合、State
のmessage.showPreview
がtrue
に設定され、結果としてリンクプレビュー
が表示されます。一方、URL
が含まれていない場合、そのメッセージは単なる文字列
として表示されます。これにより、URLの有無
による表示の違いを確認することができます。
URL先のメタデータに「title(タイトル)」、「description(説明)」が存在しない、またはURLが間違っている場合、エラーウィジェットが表示されます。
リンクプレビューをタップすることでURL先に遷移することができます。
まとめ
any_link_preview
パッケージを使用すると、ウェブページのURLからリンクプレビュー
を簡単に表示できます。この記事では詳細な実装は行っていませんが、このパッケージは自由にカスタマイズ可能です。あなたのアプリに最適な形に調整してみてください。