如何在Flutter小部件中创建超链接?

235

我想在我的Flutter应用程序中创建一个超链接。

超链接应该嵌入到Text或类似的文本视图中:

最后一本购买的书是<a href='#'>这本</a>

如何实现?


16个回答

329

只需在文本小部件周围包装一个InkWell,并将UrlLauncher(来自服务库)提供给onTap属性。在下面使用它之前,将UrlLauncher作为Flutter软件包安装。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';


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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('UrlLauncher'),
        ),
        body: new Center(
          child: new InkWell(
              child: new Text('Open Browser'),
              onTap: () => launch('https://docs.flutter.io/flutter/services/UrlLauncher-class.html')
          ),
        ),
      ),
    );
  }
}

您可以为 Text widget 提供样式以使其看起来像一个链接。

更新

在稍微研究了一下这个问题之后,我找到了一个不同的解决方案,可以实现您要求的“内联”超链接。您可以使用 RichText Widget 与封闭的 TextSpans

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('UrlLauchner'),
        ),
        body: new Center(
          child: new RichText(
            text: new TextSpan(
              children: [
                new TextSpan(
                  text: 'This is no Link, ',
                  style: new TextStyle(color: Colors.black),
                ),
                new TextSpan(
                  text: 'but this is',
                  style: new TextStyle(color: Colors.blue),
                  recognizer: new TapGestureRecognizer()
                    ..onTap = () { launch('https://docs.flutter.io/flutter/services/UrlLauncher-class.html');
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

通过这种方式,您实际上可以突出显示一个单词并将其变成超链接 ;)


15
UrlLauncher不再是Flutter的一部分,它已经移至a plugin and API changed - Josef Adamcik
10
我们需要添加导入语句: import 'package:flutter/gestures.dart'; import 'package:url_launcher/url_launcher.dart'; - Alex Pliutau
17
你没有正确处理 TapGestureRecognizer 的生命周期。当 RichText 不再使用时,你需要调用 dispose() 方法。请参考这里:https://api.flutter.dev/flutter/painting/TextSpan/recognizer.html - Alex Semeniuk
1
@AlexSemeniuk 在你的例子中,他们使用了一个StatefulWidget,在上面的答案中则是一个StatelessWidget。你确定我们需要在StatelessWidget的情况下进行处理吗? - Corey Cole
6
@CoreyCole StatelessWidget 不能自动为您处理TapGestureRecognizer的释放。实际上,在这种情况下使用StatelessWidget是不正确的,因为您无法以这种方式释放资源。而且,您绝对需要调用TapGestureRecognizerdispose()方法,因为它会运行内部计时器,需要停止它。 - Alex Semeniuk
显示剩余6条评论

109

Flutter 没有内置的超链接支持,但你可以自己进行模拟。在 Gallery 的 drawer.dart 中有一个例子。他们使用了一个包含有色 TextSpanRichText 小部件,该小部件具有一个 recognizer 属性来处理触摸事件:

        RichText(
          text: TextSpan(
            children: [
              TextSpan(
                style: bodyTextStyle,
                text: seeSourceFirst,
              ),
              TextSpan(
                style: bodyTextStyle.copyWith(
                  color: colorScheme.primary,
                ),
                text: repoText,
                recognizer: TapGestureRecognizer()
                  ..onTap = () async {
                    final url = 'https://github.com/flutter/gallery/';
                    if (await canLaunch(url)) {
                      await launch(
                        url,
                        forceSafariVC: false,
                      );
                    }
                  },
              ),
              TextSpan(
                style: bodyTextStyle,
                text: seeSourceSecond,
              ),
            ],
          ),

在此输入图片描述

在此输入图片描述


谢谢。但这并不是我正在寻找的东西:我正在寻找超越应用内导航的东西。 - TaylorR
我不确定你所说的“超出应用内导航”的意思。你想让链接在浏览器中打开吗? - Collin Jackson
是的,可以点击打开浏览器的链接或类似物。 - TaylorR
3
这就是我链接的示例所做的。我在答案中添加了一些图片来展示它。 - Collin Jackson
很抱歉,链接已失效,如果能提供实际的代码将不胜感激。 - Jan
显示剩余3条评论

87

更新(Android≥11):

  • iOS配置:

    打开info.plist文件并添加:

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>https</string>
</array>
  • Android配置:

    打开位于app/src/mainAndroidManifest.xml文件,并在根目录下添加以下内容:

  • <manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
       <!-- Add this query -->
        <queries>
            <intent>
                <action android:name="android.intent.action.VIEW" />
                <data android:scheme="https" />
            </intent>
        </queries>
    
        <application ... />
    
    </manifest>
    

    Flutter 代码:

    只需将您的 Text 包装在 GestureDetectorInkWell 中,并使用url_launcher包中的 onTap() 处理点击即可。

    InkWell(
      onTap: () => launchUrl(Uri.parse('https://www.google.com')),
      child: Text(
        'Click here',
        style: TextStyle(decoration: TextDecoration.underline, color: Colors.blue),
      ),
    )
    

    截图:

    输入图片描述


    你是对的,你的解决方案比其他人的代码行数少,但Inkwell是这个特定工作的小部件,所以至少从语义上来说,我认为这是最好的解决方案。 - dmarquina
    3
    可以使用InkWell代替GestureDetector - CopsOnRoad
    在Web浏览器中,GestureDetector不会更改光标并且没有onHover行为。InkWell是更好的解决方案。 - Andrei Volgin
    1
    谢谢,在我的情况下...这是唯一完美运行的代码! - Ahmed AlNeaimy

    24

    你可以使用flutter_linkify这个包
    https://pub.dev/packages/flutter_linkify
    它会自动将你的文本分割并突出显示http/https链接。
    结合url_launcher插件,您可以启动链接。
    您可以查看下面的示例:

    enter image description here

    完整代码如下:

    import 'package:flutter/material.dart';
    import 'package:flutter_linkify/flutter_linkify.dart';
    import 'dart:async';
    
    import 'package:url_launcher/url_launcher.dart';
    
    void main() => runApp(new LinkifyExample());
    
    class LinkifyExample extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          title: 'flutter_linkify example',
          home: Scaffold(
            appBar: AppBar(
              title: Text('flutter_linkify example'),
            ),
            body: Center(
              child: Linkify(
                onOpen: _onOpen,
                text: "Made by https://cretezy.com \n\nMail: example@gmail.com \n\n  this is test http://pub.dev/ ",
              ),
            ),
          ),
        );
      }
    
      Future<void> _onOpen(LinkableElement link) async {
        if (await canLaunch(link.url)) {
          await launch(link.url);
        } else {
          throw 'Could not launch $link';
        }
      }
    }
    

    你如何在同一小部件上放置2个链接?比如,“通过点击此链接,您接受使用条款和隐私政策”这样的情况,我们需要将它们放在一起。 - Dani
    很棒的包。节省了很多样板代码。 - Sisir
    @Dani,你可以添加任意多的链接,因为Flutter Linkify包会将每个链接视为不同的内容。我已经测试了上面的代码片段,对于多个链接也能完美运行。 - Roopak
    谢谢。这正是我正在寻找的。 - Mr. ZeroOne
    API已更改,因此上面的示例不再适用-请参见https://github.com/Cretezy/flutter_linkify/issues/112,以获取使用新的`canLaunchUrl`和`launchUrl`的示例。 - abulka

    8

    flutter link widget

    在Flutter 2.0中,引入了Link小部件。使用此小部件启动网页,并在应用程序中导航到新屏幕。在使用它之前,您需要使用url_launcher软件包。
    url_launcher: ^6.0.8
    

    获取更多信息

    Link(
                  uri: Uri.parse('https://androidride.com'),
                  //target: LinkTarget.self,
                  builder: (context, followLink) {
                    return RichText(
                      text: TextSpan(children: [
                        TextSpan(
                          text: 'Click here: ',
                          style: TextStyle(
                            fontSize: 20,
                            color: Colors.black,
                          ),
                        ),
                        TextSpan(
                          text: 'AndroidRide',
                          style: TextStyle(
                            color: Colors.blue,
                            decoration: TextDecoration.underline,
                            fontWeight: FontWeight.bold,
                            fontSize: 21,
                          ),
                          recognizer: TapGestureRecognizer()
                            ..onTap = followLink,
                        ),
                      ]),
                    );
                  }),
            ),
            SizedBox(
              height: 20,
            ),
            Link(
              uri: Uri.parse('/second'),
              builder: (context, followLink) {
                return InkWell(
                  onTap: followLink,
                  child: Text(
                    'Go to Second Screen',
                    style: TextStyle(
                      fontSize: 20,
                      color: Colors.blue,
                      decoration: TextDecoration.underline,
                    ),
                  ),
                );
              },
            ),
    

    澄清一下,Link小部件是url_launcher包的一部分,而不是Flutter本身。 - Chuck Batson

    8

    因为一些用例过于复杂,所以我再提供一个更简单明了的小技巧。我使用了RichText - WidgetSpan、TextButton和URL launcher package来实现。只需根据您的需要修改下面的示例代码块。

    结果:Result

    代码:

    class UserAgreementText extends StatelessWidget {
      const UserAgreementText({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Row(
          children: [
            Expanded(
              child: RichText(
                textAlign: TextAlign.center,
                text: TextSpan(
                  text: 'By logging in, you accept our ',
                  style: Theme.of(context).textTheme.bodySmall,
                  children: const <InlineSpan>[
                    WidgetSpan(
                      alignment: PlaceholderAlignment.baseline,
                      baseline: TextBaseline.alphabetic,
                      child: LinkButton(
                          urlLabel: "Terms and Conditions",
                          url: "https://example.com/terms-and-conditions"),
                    ),
                    TextSpan(
                      text: ' and ',
                    ),
                    WidgetSpan(
                      alignment: PlaceholderAlignment.baseline,
                      baseline: TextBaseline.alphabetic,
                      child: LinkButton(
                          urlLabel: "Privacy Policy",
                          url: "https://example.com/privacy-policy"),
                    ),
                  ],
                ),
              ),
            ),
          ],
        );
      }
    }
    

    link_button.dart

    class LinkButton extends StatelessWidget {
      const LinkButton({Key? key, required this.urlLabel, required this.url})
          : super(key: key);
    
      final String urlLabel;
      final String url;
    
      Future<void> _launchUrl(String url) async {
        final Uri uri = Uri.parse(url);
    
        if (!await launchUrl(uri)) {
          throw 'Could not launch $uri';
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return TextButton(
          style: TextButton.styleFrom(
            padding: EdgeInsets.zero,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(0),
            ),
            tapTargetSize: MaterialTapTargetSize.shrinkWrap,
            visualDensity: VisualDensity.compact,
            minimumSize: const Size(0, 0),
            textStyle: Theme.of(context).textTheme.bodySmall,
          ),
          onPressed: () {
            _launchUrl(url);
          },
          child: Text(urlLabel),
        );
      }
    }
    

    注意:如果出现与调用launchUrl相关的错误,请确保安装了URL launcher包,然后重新构建您的应用程序。


    1
    如果你收到错误信息 flutter Intrinsics are not available for PlaceholderAlignment.baseline,那么请从 WidgetSpan 中移除 alignmentbaseline - Chuck Batson

    7

    如果你想让它看起来更像一个链接,你可以添加下划线:

    new Text("Hello Flutter!", style: new TextStyle(color: Colors.blue, decoration: TextDecoration.underline),)
    

    结果如下图所示:

    enter image description here


    1
    它无法被点击! - atilkan
    1
    然后用Button小部件将其包装起来。 :) - bartektartanus
    7
    这不是问题的答案,而是对某人答案的评论。此外,按钮不是行内文本超链接。 - Watanabe.N

    7
    你可以使用链接文本https://pub.dev/packages/link_text,并像下面这样使用它:
     final String _text = 'Lorem ipsum https://flutter.dev\nhttps://pub.dev'; 
     @override
     Widget build(BuildContext context) {
     return Scaffold(
         body: Center(
          child: LinkText(
            text: _text,
            textAlign: TextAlign.center,
          ),
        ),
      );
    }
    

    在Linux下执行flutter pub get时,出现了“无法找到插件“url_launcher_windows”的vcxproj”错误。 - Eugene Gr. Philippov
    这仅限于使链接可点击。不是一个单词,指向一个链接。 - chitgoks
    如果URL本身允许可见,这绝对是最容易实现的解决方案,而不是使用现有的文本小部件。如果您需要URL不可见但作为其他文本的链接,那么这种方法就行不通。额外福利:它还可以使AlertDialog中的链接可点击。 - DaReal

    5

    如果您想具备更高级的文本功能,可以使用flutter_html插件:

    Html(
      data: 'The last <i><u><b>book</b></u></i> bought is <a href="#">this</a>',
      onLinkTap: (url, context, attrs, element) {
        // Handle link tapped...
      },
    )
    

    这只是插件功能的冰山一角。

    仅供参考: 你甚至可以将你的Flutter应用程序的部分内容作为HTML托管到在线上,并使用此插件将其作为小部件呈现在Flutter中。


    虽然有点过度,但速度较慢。 - Oliver Dixon

    2
    这个问题的答案建议使用 RichTextTextSpan + GestureRecognizer,它们在功能上都是正确的,但从用户体验的角度来看,它们没有为响应触摸的用户提供反馈。为了保持与其他 Material 组件的一致性,您可以采用类似的方法,但改用 WidgetSpan + InkWell
    此示例使用 url_launcher 包和未经样式处理的 InkWell,但您可以根据需要进行自定义:
    import 'package:flutter/material.dart';
    import 'package:url_launcher/url_launcher.dart';
    
    class TextWithLink extends StatelessWidget {
      const TextWithLink({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        const textStyle = TextStyle(color: Colors.black); // default style for all text
        return RichText(
          text: TextSpan(
            style: textStyle,
            children: [
              const TextSpan(
                text: 'The last book bought is ',
              ),
              WidgetSpan(
                  alignment: PlaceholderAlignment.middle,
                  child: InkWell(
                      onTap: () => _launchUrl('https://url-to-launch.com'),
                      child: Text('this',
                          style: textStyle.merge(const TextStyle(
                              color: Colors.blue, fontWeight: FontWeight.bold))))), // override default text styles with link-specific styles
            ],
          ),
        );
      }
    
      _launchUrl(String url) async {
        if (await canLaunch(url)) {
          await launch(url);
        } else {
          throw 'Could not launch $url';
        }
      }
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接