If you are creating a production app, especially one that connects to the network, chances are that loading images is one thing your app does. If you are loading images from online it makes sense to show placeholders while loading the actual image. Even if the image isn't found, or takes too long to load, the user has a clear representation of the actual image and can comfortably continue using the app.

This tutoria will be looking at another way of rendering placeholders, in a more compact way using the BlurHash package.

What is Flutter BlurHash?

It is a Compact representation of a placeholder for an image.

It allows you to Encode a blurry image under 30 caracters for instant display like used by Medium.

For example here is a demo screenshot:

Image picker flutter example - How to pick image from camera or gallery in flutter with example

Image picker flutter example - How ...
Image picker flutter example - How to pick image from camera or gallery in flutter with example

Capture d’écran 2020-02-21 à 19 11 56

You can use https://blurha.sh/ for testing or use any official api on your server side.

Step 1: Install it

To install this library specify it as a dependency inside the pubspec.yaml:


dependencies:
  flutter_blurhash: ^0.6.0

Then sync from your editor or run the flutter pub get to fetch it.

Step 2: Write Code

Start by importing:

import 'package:flutter_blurhash/flutter_blurhash.dart';

Then use it as below:

class BlurHashApp extends StatelessWidget {
  const BlurHashApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
    home: Scaffold(
      appBar: AppBar(title: const Text("BlurHash")),
      body: const SizedBox.expand(
        child: Center(
          child: AspectRatio(
            aspectRatio: 1.6,
            child: BlurHash(hash: "L5H2EC=PM+yV0g-mq.wG9c010J}I"),
          ),
        ),
      ),
    ),
  );

Full Example

Below is afull example of Flutter Blurhash. Start by installing the library as has been discussed above.

Then replace your main.dart with the following code:

main.dart

import 'dart:math';
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:inview_notifier_list/inview_notifier_list.dart';

const entries = [
  [
    r'f8C6M$9tcY,FKOR*00%2RPNaaKjZUawdv#K4$Ps:HXELTJ,@XmS2=yxuNGn%IoR*',
    'https://drivetribe.imgix.net/fvHsAWZPQVah0hivwdPPtw?w=1600&h=1067&fm=webp&auto=compress&lossless=true&fit=crop&crop=faces',
    'LG6'
  ],
  [
    r'f86RZIxu4TITofx]jsaeayozofWB00RP?w%NayMxkDt8ofM_Rjt8_4tRD$IUWAxu',
    'https://drivetribe.imgix.net/G_Xtlr1RQXiEklCPX8auGw?w=2400&h=1350&fm=webp&auto=compress&lossless=true&fit=crop&crop=faces',
    'ED8'
  ],
  [
    r'LZG6p1{I^6rX}G=0jGR$Z|t7NLW,',
    'https://drivetribe.imgix.net/C8AqQLEWTMShpDF2QcABNQ?w=1600&h=1067&fm=webp&auto=compress&lossless=true&fit=crop&crop=faces',
    'MT2'
  ],
  [
    r'L371cr_3RKKFsqICIVNG00eR?d-r',
    'https://drivetribe.imgix.net/R7OHpnZoRvSvE5rB9ZaGrw?w=2400&h=1350&fm=webp&auto=compress&lossless=true&fit=crop&crop=faces',
    'TK1'
  ],
];

const duration = Duration(milliseconds: 500);

const radius = Radius.circular(16);

const topMark = .7;

void main() {
  runApp(MaterialApp(
      debugShowCheckedModeBanner: false, home: const BlurHashApp()));
}

class BlurHashApp extends StatefulWidget {
  const BlurHashApp({Key? key}) : super(key: key);

  @override
  _BlurHashAppState createState() => _BlurHashAppState();
}

class _BlurHashAppState extends State<BlurHashApp> {
  double progression = 0;

  void onStarted() {
    print("Ready");
  }

  double norm(double value, double min, double max) =>
      (value - min) / (max - min);

  @override
  Widget build(BuildContext context) =>
      NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notif) {
            // NO need to setState
            setState(() {
              progression = norm(notif.metrics.pixels, 0, 1);
              // print("Progression $progression / px ${notif.metrics.pixels}");
            });
            return true;
          },
          child: Stack(children: [
            FractionallySizedBox(
              heightFactor: topMark,
              child: Container(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Color(0xEEFFFFFF), Color(0xCCFFFFFF)],
                    begin: Alignment.bottomCenter,
                    end: Alignment.topCenter,
                  ),
                ),
              ),
            ),
            Align(
              alignment: Alignment(-.8, -.5),
              child: Container(
                margin: EdgeInsets.only(top: 100),
                child: Header(progression: progression),
              ),
            ),
            //BackdropFilter(child: , filter: ImageFilter.blur(sigmaY: 15, sigmaX: 15)),
            buildInViewNotifierList()
          ]));

  Widget buildList() => ListView.builder(
      itemCount: entries.length,
      itemBuilder: (ctx, idx) => buildEntry(true, idx));

  Widget buildInViewNotifierList() => InViewNotifierList(
      itemCount: entries.length + 2,
      builder: (ctx, idx) => InViewNotifierWidget(
          id: '$idx',
          builder: (BuildContext context, bool isInView, Widget child) {
            if (idx == 0) return SizedBox(height: 500);
            if (idx == entries.length + 1) return SizedBox(height: 800);

            return buildEntry(isInView, idx - 1);
          }),
      isInViewPortCondition:
          (double deltaTop, double deltaBottom, double viewPortDimension) =>
              deltaTop < (topMark * viewPortDimension)
      //&& deltaBottom > (0.3 * viewPortDimension)
      );

  Container buildEntry(bool isInView, int idx) => Container(
      padding: EdgeInsets.only(left: 0, right: 200),
      height: 510,
      margin: const EdgeInsets.only(bottom: 24),
      child: isInView || idx == 0
          ? SynchronizedDisplay(
              hash: entries[idx][0],
              uri: entries[idx][1],
              title: entries[idx][2])
          : BlurHash(hash: entries[idx][0]));
}

class Header extends StatelessWidget {
  Header({
    Key? key,
    required this.progression,
  }) : super(key: key);

  final gradient = ColorTween(begin: Color(0xFF222222), end: Colors.black87);

  final double progression;

  @override
  Widget build(BuildContext context) {
    final base = progression / 100;
    final color = gradient.lerp(base);

    return Column(
      children: <Widget>[
        Text(
          "Discover",
          style: GoogleFonts.josefinSans(
            textStyle: TextStyle(
                color: color,
                fontSize: 180,
                height: .84,
                fontWeight: FontWeight.bold,
                decoration: TextDecoration.none),
          ),
        ),
        Container(
          margin: EdgeInsets.only(top: 16),
          child: Text(
            "Our\nCollection",
            style: GoogleFonts.josefinSans(
              textStyle: TextStyle(
                  color: color,
                  fontSize: 130,
                  height: .84,
                  fontWeight: FontWeight.bold,
                  decoration: TextDecoration.none),
            ),
          ),
        ),
      ],
    );
  }
}

class SynchronizedDisplay extends StatefulWidget {
  const SynchronizedDisplay(
      {Key? key, required this.hash, required this.uri, required this.title})
      : super(key: key);
  final String hash;
  final String uri;
  final String title;

  @override
  _SynchronizedDisplayState createState() => _SynchronizedDisplayState();
}

class _SynchronizedDisplayState extends State<SynchronizedDisplay>
    with SingleTickerProviderStateMixin {
  late Animation<double> animatedWidth;
  late AnimationController controller;

  double end = 100;

  @override
  Widget build(BuildContext context) {
    final w = animatedWidth.value;

    return Stack(
      alignment: Alignment(1.225, 0.0),
      children: [
        Transform.translate(
          offset: Offset(w, 0),
          child: Container(
            width: 200,
            decoration: const BoxDecoration(
                gradient: LinearGradient(
                  colors: [Color(0xFF888888), Color(0xFFAAAAAA)],
                  stops: [.1, 1],
                  begin: Alignment.centerLeft,
                  end: Alignment.centerRight,
                ),
                borderRadius:
                    BorderRadius.only(topRight: radius, bottomRight: radius)),
          ),
        ),
        BlurHash(
          onStarted: onStarted,
          hash: widget.hash,
          image: widget.uri,
          duration: duration,
        ),
        Align(
          alignment: Alignment(1.4, 0),
          child: Icon(
            Icons.chevron_right,
            size: 60,
            color: Colors.white,
          ),
        ),
        Transform.rotate(
          angle: pi * -.5,
          child: Text(
            widget.title,
            style: GoogleFonts.josefinSans(
                textStyle: TextStyle(
                    color: const Color(0xFFDDDDDD),
                    fontSize: 45,
                    fontWeight: FontWeight.bold,
                    decoration: TextDecoration.none)),
          ),
        )
      ],
    );
  }

  void onStarted() {
    controller.forward();
  }

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: duration, vsync: this);
    final curved =
        CurvedAnimation(parent: controller, curve: Curves.easeOutCirc);
    animatedWidth = Tween<double>(begin: -50, end: end).animate(curved);
    controller.addListener(() => setState(() {}));
  }

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

Reference

Here are the reference links:

Number Link
1. Download code
2. Read more
1. Follow code author