skeletonizer 0.1.0+2 copy "skeletonizer: ^0.1.0+2" to clipboard
skeletonizer: ^0.1.0+2 copied to clipboard

Converts already built widgets into skeleton loaders with no extra effort.


MIT License stars pub version

Buy Me A Coffee


Introduction #

What are skeleton loaders? #

Skeleton loaders are visual placeholders for information while data is still loading. Anatomy. A skeleton loader provides a low fidelity representation of the interface that will be loaded.

Motivation #

Creating a skeleton-layout for your screens doesn't only feel like a duplicate work but things can go out of sync real quick, when updating the actual layout we often forget to update the corresponding skeleton-layout.

How does it work? #

As the name suggests, skeletonizer will reduce your already existing layouts into mere skeletons and apply painting effects on them, typically a shimmer effect. It automatically does the job for you, in addition SkeletonAnnotations can be used to change how some widgets are skeletonized.

Basic usage #

Simply wrap your layout with Skeletonizer widget


Skeletonizer(
  enabled: _loading,
  child: ListView.builder(
    itemCount: 7,
    itemBuilder: (context, index) {
      return Card(
        child: ListTile(
          title: Text('Item number $index as title'),
          subtitle: const Text('Subtitle here'),
          trailing: const Icon(Icons.ac_unit),
        ),
      );
    },
  ),
)

Note: all the following shimmer effects are disturbed by the gif optimization

Skeletonizer with default config

Skeletonizer with no containers


Skeletonizer(ignoreContainers: true)

The need for fake data #

In order for skeletonizer to work it actually needs a layout, but in most cases the layout would need data to shape which. e.g the following ListView ill not render anything unless users is populated, so if users is empty we have no layout which mean we have nothing to skeletonize.

Skeletonizer(
  enabled: _loading,
  child: ListView.builder(
    itemCount: users.lenght,
    itemBuilder: (context, index) {
      return Card(
        child: ListTile(
          title: Text(users[index].name),
          subtitle: Text(users[index].jobTitle),
          leading: CircleAvatar(
            radius: 24,
            backgroundImage: NetworkImage(users[index].avatar),
          ),
        ),
      );
    },
  ),
)

So the key here is to provide fake data for the layout to shape until the real data is fetched form backend, so we would such a setup in our build method

 
  if (_loading) {
    final fakeUsers = List.generate(
      7, (index) => const User(
        name: 'User name',
        jobTitle: 'Developer',
        avatar: ''
    ),
    );
    return Skeletonizer(
      child: UserList(users: fakeUsers),
    );
  } else {
    return UserList(users: realUsers);
  }  

or by utilizing the enabled flag

  {
  final users = _loading ? realUsers : List.generate(
      7, (index) => const User(
      name: 'User name',
      jobTitle: 'Developer',
      avatar: ''
  )
  );
  return Skeletonizer(
    enabled: _loading,
    child: UserList(users: users),
  );
 

Now we have our layout but one issue remains, if you run the above example you'll get an error in your console stating that an invalid url was passed to NetworkImage which is true because our fake avatar url is an empty string, in such cases we need to make sure NetworkImage is not in our widget tree when skeletonizer is enabled and we do that by using a skeleton annotation called Skeleton.replace ..read more about annotations below.

Skeletonizer(
  enabled: _loading,
  child: ListView.builder(
    itemCount: users.lenght,
    itemBuilder: (context, index) {
      return Card(
        child: ListTile(
          title: Text(users[index].name),
          subtitle: Text(users[index].jobTitle),
            leading: Skeleton.replace(
            width: 48, // width of replacement
            height: 48, // height of replacement
            child; CircleAvatar(
            radius: 24,
            backgroundImage: NetworkImage(users[index].avatar),
          ),
        ),
      );
    },
  ),
)

or you can do it directly like follows:

Skeletonizer(
  enabled: _loading,
  child: ListView.builder(
    itemCount: users.lenght,
    itemBuilder: (context, index) {
      return Card(
        child: ListTile(
          title: Text(users[index].name),
          subtitle: Text(users[index].jobTitle),
            leading: CircleAvatar(
            radius: 24,
            backgroundImage: _loading ? null : NetworkImage(users[index].avatar),
          ),
        ),
      ),);
    },
  ),
)

Note: you can also check wither a skeletonizer is enabled inside descendent widgets using:


Skeletonizer
    .of(context)
    .enabled;

Annotations #

We can use annotations to change how some widgets should be skeletonized, skeleton annotations have no effect on the real layout as they're only hold information for skeletonizer to use when it's enabled.

Skeleton.ignore #

Widgets annotated with Skeleton.ignore will not be skeletonized

Card(
  child: ListTile(
    title: Text('The title goes here'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.ignore( // the icon will not be skeletonized
      child: Icon(Icons.ac_unit, size: 40),
    ),
  ),
)

Ignored multiple descendants demo

Card(
  child: Skeleton.ignore( // all descendents will be ignored
    child: ListTile(
      title: Text('The title goes here'),
      subtitle: Text('Subtitle here'),
      trailing: Icon(Icons.ac_unit, size: 40),
    ),
  ),
)

Skeleton.keep #

Widgets annotated with Skeleton.keep will not be skeletonized but will be painted as is

Card(
  child: ListTile(
    title: Text('The title goes here'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.keep( // the icon will be painted as is
      child: Icon(Icons.ac_unit, size: 40),
    ),
  ),
)

Skeleton.shade #

Widgets annotated with Skeleton.shade will not be skeletonized but will be shaded by a shader mask

Card(
  child: ListTile(
    title: Text('The title goes here'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.shade( // the icon will be shaded by shader mask
      child: Icon(Icons.ac_unit, size: 40),
    ),
  ),
)

Skeleton.replace #

Widgets annotated with Skeleton.replace will be replaced when skeletonizer is enabled and the replacement will be skeletonized, This is good for widgets that can render with fake data like Image.network()

Card(
  child: ListTile(
    title: Text('The title goes here'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.replace( // the icon will be replaced when skeletonizer is on
        width: 50, // the width of the replacement
        height: 50, // the height of the replacement
        replacment: // defaults to a DecoratedBox
        child: Icon(Icons.ac_unit, size: 40),
  ),
)
,)

Skeleton.unite #

Widgets annotated with Skeleton.unite will not be united and drawn as one big bone, this is good for when you have multiple small bones close to each other and you want to present them as one bone.

Card(
  child: ListTile(
    title: Text('Item number 1 as title'),
    subtitle: Text('Subtitle here'),
    trailing: Skeleton.unite(
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.ac_unit, size: 32),
          SizedBox(width: 8),
          Icon(Icons.access_alarm, size: 32),
        ],
      ),
    ),
  ),
)
,

This annotation can also be used to merge a while layout and present it as one

Skeleton.unite(
  child: Card(
    child: ListTile(
      title: Text('Item number 1 as title'),
      subtitle: Text('Subtitle here'),
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.ac_unit, size: 32),
          SizedBox(width: 8),
          Icon(Icons.access_alarm, size: 32),
        ],
      ),
    ),
  ),
)

Customization #

Loading effects #

Skeletonizer has 3 different parting effects to choose from and which can be customized to your liking. Note: Loading effects are disturbed by Gif optimization, these look much better on flutter

Text skeleton config #

You can provide a global text config options to skeletonizer widgets like

Skeletonizer(
    justifyMultiLineText: false,
    textBoneBorderRadius: TextBoneBorderRadius.fromHeightFactor(.5),
    ...
)

Using the inheritable SkeletonizerConfig widget #

Use SkeletonizerConfigData somewhere up your widgets tree e.g above the MaterialApp widget to provide default skeletonizer configurations to your whole App.

SkeletonizerConfig(
    data: SkeletonizerConfigData(
      effect: const ShimmerEffect(),
      justifyMultiLineText: true,
      textBorderRadius: TextBoneBorderRadius(..),
      ignoreContainers: false,
    ),
    .....
)

Support Skeletonizer #

You can support skeletonizer by liking it on Pub and staring it on Github, sharing ideas on how we can enhance a certain functionality or by reporting any problems you encounter and of course buying a couple coffees will help speed up the development process.

1268
likes
0
pub points
99%
popularity

Publisher

verified publishercodeness.ly

Converts already built widgets into skeleton loaders with no extra effort.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

collection, flutter

More

Packages that depend on skeletonizer