Convert HTML to Flutter Widgets

The Complete Guide to convert HTML elements to Flutter Widgets

In this post we will show you how to convert html elements to Flutter Widgets

In one of my apps, I was consuming WordPress REST APIs to display data. WordPress stores blog data in the form of HTML. So to render that data into my Flutter app I was required to use an HTML renderer package. So I choose to go with the https://pub.dev/packages/flutter_html/versions/3.0.0-beta.2/install

so installed it as suggested under description of package.

HTML content can contain any HTML tags like Images, Videos, YouTube links, tweets, and even a gallery of images. So it is good practice to render all in our app for the best experience.

So First I created a stateless widget, in which HTML content was passed, Here is the code below

class HtmlBody extends StatelessWidget {
  const HtmlBody({
    super.key,
    required this.content,
  });
  final String content;

  @override
  Widget build(BuildContext context) {
    return Html(
      shrinkWrap: true,
      data: content,
      style: {
        "body": Style(
          margin: Margins.zero,
          padding: HtmlPaddings.all(8.0),
          fontSize: FontSize(18.0),
          lineHeight: const LineHeight(1.5),
          whiteSpace: WhiteSpace.normal,
          fontWeight: FontWeight.w400,
          color: const Color(0xFF292929),
        ),
        "p,h1,h2,h3,h4,h5,h6": Style(margin: Margins.all(2.00)),
        "figure": Style(margin: Margins.zero, padding: HtmlPaddings.all(8.0)),
      },
      onLinkTap: (
        String? url,
        _,
        __,
      ) async {
        if (url != null) URLLauncherUtil().launchUrl(url);
      },
      extensions: [
        AppHtmlImageExtension(),
        AppHtmlGalleryRender(),
        AppHtmlIframeExtension(),
      ],
    );
  }
}

Handling anchor tags (links to another webpage) is pretty easy and can be handled using the url_launcher package. But the trick is to render images, galleries, tweets, and YouTube links. so here come the extensions for the rescue.

If want to handle image, pass an extension to render image and so on.

I have passed three extension, AppHtmlImageExtension(), AppHtmlGalleryRender(), AppHtmlIframeExtension() to handle image, gallery and Iframe respectively.

You can create any extension by extending the HtmlExtension class. Here is the full code of all extensions used above.

class AppHtmlIframeExtension extends HtmlExtension {
  @override
  Set<String> get supportedTags => {'iframe'};

  @override
  InlineSpan build(ExtensionContext context) {
    return WidgetSpan(child: returnView(context));
  }

  Widget returnView(ExtensionContext context) {
    final String videoSource = context.element!.attributes['src'].toString();
    if (videoSource.contains('youtube')) {
      return GestureDetector(
        onTap: () {
          if (context.buildContext != null) {
            URLLauncherUtil().launchUrl(videoSource);
          }
        },
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Stack(
            alignment: Alignment.center,
            children: [
              CachedNetworkImage(
                imageUrl: getYoutubeThumbnail(videoSource),
              ),
              SizedBox(
                  height: 80,
                  width: 120.00,
                  child: Image.asset(
                    'assets/images/youtube.png',
                    fit: BoxFit.contain,
                  )),
            ],
          ),
        ),
      );
    } else {
      return const SizedBox.shrink();
    }
  }

  String getYoutubeThumbnail(String videoUrl) {
    String? videoID = getYoutubeVideoIdFromUrl(videoUrl);
    return videoID == null ? '' : 'https://img.youtube.com/vi/$videoID/0.jpg';
  }

  String? getYoutubeVideoIdFromUrl(String url, {bool trimWhitespaces = true}) {
    if (!url.contains("http") && (url.length == 11)) return url;
    if (trimWhitespaces) url = url.trim();

    for (var exp in [
      RegExp(
          r"^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(
          r"^https:\/\/(?:music\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(
          r"^https:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(
          r"^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(r"^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$")
    ]) {
      Match? match = exp.firstMatch(url);
      if (match != null && match.groupCount >= 1) return match.group(1);
    }

    return null;
  }
}

class AppHtmlImageExtension extends HtmlExtension {
  @override
  Set<String> get supportedTags => {'img'};

  @override
  InlineSpan build(ExtensionContext context) {
    return WidgetSpan(child: returnView(context));
  }

  Widget returnView(ExtensionContext context) {
    final src = context.element?.attributes['src'].toString();
    return GestureDetector(
      onTap: () {
        if (context.buildContext != null) {
          Navigator.push(
              context.buildContext!,
              MaterialPageRoute(
                  builder: (context) => CachedPhotoView(
                        url: src,
                      )));
        }
      },
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: CachedNetworkImage(
          imageUrl: src!,
          placeholder: (context, url) => AspectRatio(
            aspectRatio: 16 / 9,
            child: Stack(
              children: [
                Container(
                  color: Colors.grey[400],
                ),
                const Center(child: CircularProgressIndicator())
              ],
            ),
          ),
        ),
      ),
    );
  }
}


class AppHtmlGalleryRender extends HtmlExtension {
  @override
  Set<String> get supportedTags => {'wp-block-gallery'};

  @override
  InlineSpan build(ExtensionContext context) {
    return WidgetSpan(child: returnView(context));
  }

  @override
  bool matches(ExtensionContext context) {
    return context.element?.classes.contains('wp-block-gallery') ?? false;
  }

  Widget returnView(ExtensionContext context) {
    List<String> imagesUrl = [];
    final src = context.element?.children ?? [];
    imagesUrl =
        src.map((e) => e.children.first.attributes['src'] ?? '').toList();

    return PostGalleryRenderer(imagesUrl: imagesUrl);
  }
}

Render YouTube Videos Links

For the Iframe part, I handled YouTube video links. First I extracted the video id then based upon that Video ID I rendered an image and play icon on top of that to give an illusion of it as a video. I hope the code is quite self-explanatory. In case you are not getting it properly I recommend this post.

Render Images

In extension we need to extend the HtmlExtension class and override some methods like supportedTags and need to pass img. (<img src=’Image link’ />)

Then we need to get the src means source a.k.a link of the image that we want to render.

After getting the link of the image we can display it using NetworkImage or CachedNetwork widgets. Then we can handle when the Image is clicked by getting the BuildContext from the current context.

class AppHtmlImageExtension extends HtmlExtension {
  @override
  Set<String> get supportedTags => {'img'};

  @override
  InlineSpan build(ExtensionContext context) {
    return WidgetSpan(child: returnView(context));
  }

  Widget returnView(ExtensionContext context) {
    final src = context.element?.attributes['src'].toString();
    return GestureDetector(
      onTap: () {
        if (context.buildContext != null) {
          Navigator.push(
              context.buildContext!,
              MaterialPageRoute(
                  builder: (context) => CachedPhotoView(
                        url: src,
                      )));
        }
      },
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: CachedNetworkImage(
          imageUrl: src!,
          placeholder: (context, url) => AspectRatio(
            aspectRatio: 16 / 9,
            child: Stack(
              children: [
                Container(
                  color: Colors.grey[400],
                ),
                const Center(child: CircularProgressIndicator())
              ],
            ),
          ),
        ),
      ),
    );
  }
}

To Render the Image gallery provided by WordPress Block Editor We need to create the extension in the same manner but this time we need to look for ‘wp-block-gallery’ tag and fetch the list of images and render them in a gridview.

class AppHtmlGalleryRender extends HtmlExtension {
  @override
  Set<String> get supportedTags => {'wp-block-gallery'};

  @override
  InlineSpan build(ExtensionContext context) {
    return WidgetSpan(child: returnView(context));
  }

  @override
  bool matches(ExtensionContext context) {
    return context.element?.classes.contains('wp-block-gallery') ?? false;
  }

  Widget returnView(ExtensionContext context) {
    List<String> imagesUrl = [];
    final src = context.element?.children ?? [];
    imagesUrl =
        src.map((e) => e.children.first.attributes['src'] ?? '').toList();

    return PostGalleryRenderer(imagesUrl: imagesUrl);
  }
}

Leave a Reply