Simple Tip

codecov Codacy Badge Pub Version

A widget for showing tips to it's child that automatically setup position.

This package is separated from my project POS-System.

SimpleTip DEMO

How to use

All in one screen

Wrap your widget with SimpleTip, and give some tips!

Widget build(context) {
  return SimpleTip(
    message: 'This is my tip!',
    child: Text('Hello world'),
  );
}

In order

Wrap your widget with OrderedTip and give grouper and id for each tip.

Widget build(context) {
  final grouper = GlobalKey<TipGrouperState>();
  final observer = RouteObserver<ModalRoute<void>>();

  return TipGrouper(
    key: grouper,
    id: 'group',
    candidateLength: 2,
    routeObserver: observer,
    child: Scaffold(
      body: Column(children: [
        OrderedTip(
          order: 1,
          version: 1,
          id: 't-1',
          message: 'Tip content 1',
          grouper: grouper,
          child: const Card(
            child: const Text('First tip'),
          ),
        ),
        OrderedTip(
          order: 2,
          version: 1,
          id: 't-2',
          message: 'Tip content 2',
          grouper: grouper,
          child: const Text('Second tip'),
        ),
      ]),
    ),
  );
}

You SHOULD setup defaultStateManager, otherwise it only works in-memory. Which means when you restart your app, tips will show up again!

Configuration

Two main widget you can use, SimpleTip and OrderedTip.

You can follow Dart provided API document

SimpleTip

  • title, String?, null

    • The title to display in the tip.
  • message, String?, null

    • The message to display in the tip. MUST set if content not set.
    • This value will also use for semantics.
  • content, Widget?, null

    • Content of the tip.
    • This will "win" when message and content both set.
    • Example:
final content = Text('hi');
  • boxConstraints, BoxConstraints?, BoxConstraints(minHeight: 24.0)

    • Tip's content constraints
  • decoration, Decoration?, see below

    • Tip's container decoration
    • Default:
ShapeDecoration(
  color: (isDark ? Colors.white : Colors.grey[700]!).withOpacity(0.9),
  shape: TipShapeBorder(arrowArc: 0.1, target: target),
)
  • textStyle, TextStyle?, see below
    • Tip's text default style
    • Default:
Theme.of(context).textTheme.bodyText1!.copyWith(
  color: isDark ? Colors.black : Colors.white,
)
  • onClosed, VoidCallback?, null
    • A callback after the start of closing tip.
    • This will be needed if you handle tip by filesystem, eg: SharedPreferences, hive
    • Example:
final isDisabled = instance.read('myTip') == 1;
final onClosed = () {
  instance.write('myTip', 1);
}
  • padding, EdgeInsets, EdgeInsets.all(8.0)

    • The amount of space by which to inset the tip's content.
  • margin, EdgeInsets, EdgeInsets.symmetric(horizontal: 16.0)

    • The empty space that surrounds the tip.
    • Defines the tip's outer Container.margin. By default, a long tip will span the width of its window. If long enough, a tip might also span the window's height. This property allows one to define how much space the tip must be inset from the edges of their display window.
  • verticalOffset, double, 24.0

    • The vertical gap between the widget and the displayed tip.
    • When preferBelow is set to true and tips have sufficient space to display themselves, this property defines how much vertical space tips will position themselves under their corresponding widgets. Otherwise, tips will position themselves above their corresponding widgets with the given offset.
  • closerText, String, OK

    • Text of button to close tip.
  • waitDuration, Duration, Duration.zero

    • The length of time that a tip will wait for showing.
    • Defaults to 0 milliseconds (tips are shown immediately after created).
  • excludeFromSemantics, bool, false

    • Whether the tip's SimpleTip.message should be excluded from the semantics tree.
    • A tip will add a Semantics label that is set to SimpleTip.message. Set this property to true if the app is going to provide its own custom semantics label.
  • isDisabled, bool, false

    • Disable tip.
    • Wrap SimpleTip with StatefulWidget and dynamically set this value.
  • withBackdrop, bool, false

    • Whether use backdrop
  • preferBelow, bool, true

    • Whether the tips defaults to being displayed below the widget.
    • Defaults to true. If there is insufficient space to display the tip in the preferred direction, the tip will be displayed in the opposite direction.
  • child, Widget, required

    • The widget below this widget in the tree.

OrderedTip

IMPORTANT you should set up TipGrouper static property defaultStateManager to "remember" that user had read this tip.

  • id, String, required

    • ID of this tip.
    • It should be unique in the same group
  • grouper, GlobalKey<TipGrouperState>, required

    • State of grouper that contains many OrderedTip
    • If one screen have multiple groups, it should show many tips in one screen.
  • version, int, 0

    • The version it should be.
    • SimpleTip.isDisabled will be false if version is not equal to given version from static getVersion.
    • Implement detail:
if (TipOrdered.getVersion(groupId, id) != version) {
  enabledTip = id;
  break; // break the loop
}
  • order, int, 0
    • The order to show the tip, lower order higher priority.
    • Implement:
tipList.sort((a, b) => a.order.compareTo(b.order))
  • title, String?, same as SimpleTip.title
  • message, String?, same as SimpleTip.message
  • content, Widget?, same as SimpleTip.content

State Manager

There are two method you need to override:

  • shouldShow, int Function(String groupId, TipItem item), see below
    • Get OrderedTip.version from your filesystem, eg: SharedPreferences, hive
    • Default using in-memory data to get version:
bool shouldShow(String groupId, TipItem item) {
  final lastVersion = _records['$groupId.${item.id}'];
  return lastVersion == null ? true : lastVersion < item.version;
}
  • tipRead, Future<void> Function(String groupId, TipItem item), see below
    • Set OrderedTip.version after user manually close it
    • Default using in-memory data to record version:
Future<void> tipRead(String groupId, TipItem item) async {
  _records['$groupId.${item.id}'] = item.version;
}

Example of using shared_preferences:

void initialize() async {
  final service = await SharedPreferences.getInstance();
  OrderedTip.stateManager = PrefStateManager(service);
}

class PrefStateManager extends StateManager {
  final SharedPreferences pref;

  const PrefStateManager(this.pref);

  @override
  bool shouldShow(String groupId, TipItem item) {
    final lastVersion = pref.getInt('$groupId.${item.id}');
    return lastVersion == null ? true : lastVersion < item.version;
  }

  @override
  Future<void> tipRead(String groupId, TipItem item) {
    return pref.setInt('$groupId.${item.id}', item.version);
  }
}

Example of using Hive

void initialize() async {
  final box = Hive.box('myBox');
  OrderedTip.stateManager = HiveStateManager(box);
}

class HiveStateManager extends StateManager {
  final Box box;

  const HiveStateManager(this.box);

  @override
  bool shouldShow(String groupId, TipItem item) {
    final lastVersion = box.get('$groupId.${item.id}');
    return lastVersion == null ? true : lastVersion < item.version;
  }

  @override
  Future<void> tipRead(String groupId, TipItem item) {
    return box.put('$groupId.${item.id}', version);
  }
}

How it works

Main idea is inspired from Tooltip, which using overlayState.insert to show tips.

SimpleTip build tips and backdrop on the Overlay, and detect click on backdrop to close it. Multiple tips should build single backdrop and close all tips once it been clicked.

You can also close it by

  • User tap closeButton
  • dispose or deactivate has been fired

LICENSE

See in LICENSE

Libraries

simple_tip