oh_my_flutter 0.5.0 copy "oh_my_flutter: ^0.5.0" to clipboard
oh_my_flutter: ^0.5.0 copied to clipboard

Empower Flutter with utilities that make development 10x faster.

oh_my_flutter #

CI License: MIT pub package

Small, strongly typed utilities for common Flutter application tasks.

Installation #

Add the latest compatible release from pub.dev:

flutter pub add oh_my_flutter

Or add it directly to your pubspec.yaml:

dependencies:
  oh_my_flutter: ^0.5.0

Import the public library wherever you need it:

import 'package:oh_my_flutter/oh_my_flutter.dart';

Quick start #

Extensions make common transformations concise while preserving Flutter and Dart types:

import 'package:flutter/material.dart';
import 'package:oh_my_flutter/oh_my_flutter.dart';

final accent = const Color(0xFFFF4A4B);
final lighterAccent = accent.lighten(0.12);
final hex = lighterAccent.toHex();
final oklch = lighterAccent.toOklch();

Utilities #

Present relative time #

Use DateTime.timeAgo when presentation depends on elapsed time but the package should not own your wording or localization. Callbacks determine both the result type and the text shown to the user.

final label = publishedAt.timeAgo<String>(
  onNow: () => 'now',
  onMinutesAgo: (count) => '$count min ago',
  onHoursAgo: (count) => '$count hr ago',
  onDaysAgo: (count) => '$count days ago',
  onMiss: () => 'a while ago',
);

Time is read through package:clock, so applications and tests can pin the current instant without changing production code. See the API reference for bucketing and fallback behavior.

Transform colors and work with OKLCH #

Use the color extensions for direct Flutter Color transformations. Convert to OKLCH when you need perceptually uniform lightness, chroma, and hue values.

final base = const Color(0xFFFF4A4B);

final lighter = base.lighten(0.12);
final darker = base.darken(0.12);
final hex = base.toHex();

final oklch = base.toOklch();
final restored = oklch.toColor();

The API reference documents supported color spaces, gamut mapping, clamping, and alpha behavior.

Classify gesture velocity #

Use the Velocity extension at a drag boundary when release speed and direction should help decide whether an interaction completes.

void handleDragEnd(DragEndDetails details) {
  final shouldDismiss = details.velocity.isSwipeDown();

  if (shouldDismiss) {
    dismiss();
  }
}

The methods classify velocity only. The consuming interaction remains responsible for distance, progress, and whether the action is allowed.

Control widget visibility #

Use ControlledVisibility when parent code should show or hide a child while the application retains control of its visual transition. Without a transition, visibility changes immediately.

final visibilityController = ControlledVisibilityController();

ControlledVisibility(
  controller: visibilityController,
  showDuration: const Duration(milliseconds: 240),
  hideDuration: const Duration(milliseconds: 120),
  showTransition: (child, animation) => FadeTransition(
    opacity: CurveTween(curve: Curves.easeOutCubic).animate(animation),
    child: child,
  ),
  hideTransition: (child, animation) => FadeTransition(
    opacity: animation,
    child: child,
  ),
  child: const Text('More details'),
);

visibilityController.show();
visibilityController.hide();

Set unmount: true when hidden content should be disposed instead of retaining its state and layout. Timing, lifecycle, callback, and reduced-motion behavior are documented in the API reference.

Add lightweight motion #

Wrap any widget with Motion and an effect to add a reusable visual motion treatment. FloatingMotionEffect creates a subtle, continuous vertical float without changing the child's layout:

const Motion(
  effect: FloatingMotionEffect(
    delay: Duration(milliseconds: 300),
  ),
  child: Icon(Icons.cloud_outlined),
)

Use an effect's delay to wait before its playback. Customize the floating distance, cycle duration, or timing curve on the effect. Effects own configuration only; Motion owns the animation lifecycle and respects the platform's reduced-motion preference.

Fade or scale a widget in, or move it between logical-pixel offsets with the other built-in effects:

const Motion(
  effect: FadeInMotionEffect(),
  child: Text('Ready'),
)

const Motion(
  effect: ScaleInMotionEffect(scale: 0.6),
  child: Icon(Icons.check),
)

const Motion(
  effect: MoveMotionEffect(
    begin: Offset(-24, 0),
    end: Offset.zero,
  ),
  child: Icon(Icons.arrow_forward),
)

Use onStart and onEnd to react to each effect independently:

Motion(
  effect: FadeInMotionEffect(
    onStart: handleMotionStarted,
    onEnd: handleMotionCompleted,
  ),
  child: const Text('Ready'),
)

onStart runs after the effect's delay. onEnd runs when a one-shot effect completes. Looping effects do not call onEnd while mounted, and canceled or disposed effects are not reported as completed. Reduced-motion one-shot effects call onStart followed immediately by onEnd. FloatingMotionEffect exposes only onStart because it never completes.

Run effects concurrently or stagger them with independent delays:

const Motion.list(
  effects: [
    FadeInMotionEffect(),
    ScaleInMotionEffect(
      scale: 0.6,
      delay: Duration(milliseconds: 80),
    ),
  ],
  child: Text('Ready'),
)

The first effect is applied first, and each following effect composes around the result. Keep the effects list immutable after passing it to Motion.list. By default, pointer interaction is ignored while any effect is waiting or playing. Set interactive: true to let the child receive taps during delays and playback. Otherwise, interaction becomes available only after every one-shot effect completes. A looping effect keeps interaction disabled while it remains mounted.

Create a one-shot or looping effect by extending MotionEffect. The same effect works with Motion and TextMotion:

class SlideInMotionEffect extends MotionEffect {
  const SlideInMotionEffect()
    : super(duration: const Duration(milliseconds: 240));

  @override
  void apply(double progress, MotionEffectTransform transform) {
    transform.translate(x: 24 * (1 - progress), y: 0);
  }
}

One-shot effects run once per mounted Motion; assign a new key when an effect should replay. Looping effects should render equivalent states at progress 0 and 1 so their cycles remain seamless.

Add motion to each text character #

Use TextMotion with the same effects as Motion to animate every visible Unicode grapheme in a plain Flutter Text. Whitespace and invisible formatting controls remain static paragraph spans:

const TextMotion(
  effect: MoveMotionEffect(
    begin: Offset(0, 8),
    end: Offset.zero,
  ),
  stagger: Duration(milliseconds: 30),
  child: Text('Welcome'),
)

Combine effects with TextMotion.list:

const TextMotion.list(
  effects: [
    FadeInMotionEffect(),
    ScaleInMotionEffect(scale: 0.8),
  ],
  stagger: Duration(milliseconds: 30),
  child: Text('Ready'),
)

The default stagger is 30 milliseconds. One-shot effects finish after the last character completes; looping effects retain their configured cycle duration and use the stagger as a phase offset. Each effect keeps one shared lifecycle, so onStart and onEnd fire once for the complete text rather than once per character. Reduced motion, TickerMode, interaction, delays, and playback otherwise match Motion.

Built-in and custom effects support the same opacity, translation, and scale operations in both Motion and TextMotion.

TextMotion is intended for short display text. Rendering graphemes independently necessarily changes cross-character typography such as kerning, ligatures, contextual shaping, line wrapping, bidirectional layout, and selection. It accepts Text('...'); Text.rich is not supported.

Move widgets through a marquee #

Use Marquee to move an ordered strip continuously through a clipped viewport. The duration covers one complete pass of the source strip:

const Marquee(
  direction: MarqueeDirection.left,
  duration: Duration(seconds: 4),
  spacing: 24,
  infinity: true,
  children: [
    Text('Portable'),
    Text('Strongly typed'),
    Text('Low allocation'),
  ],
)

Horizontal marquees fill a bounded parent width by default and use their tallest child for height. Vertical marquees fill a bounded parent height and use their widest child. Supply width or height to request a fixed viewport dimension. By default, infinity: true mounts only the minimum cyclic child prefix needed to keep the viewport filled without a gap between loops. Set infinity: false to mount each child once and use an offscreen-to-offscreen pass instead. Child subtrees containing GlobalKeys are not supported while infinity is enabled. Pointer interaction is disabled by default; set interactive: true when moving children should accept taps. Reduced-motion preferences leave the strip visible in a static arrangement.

Pause child animations #

Use PauseAnimations when a subtree's ticker callbacks should be muted. Its default constructor accepts paused, which defaults to true:

PauseAnimations(
  paused: isLoading,
  child: const ProgressWidget(),
)

Use PauseAnimations.temporarily to enable callbacks automatically after a fixed duration:

const PauseAnimations.temporarily(
  duration: Duration(milliseconds: 300),
  child: ProgressWidget(),
)

Flutter still advances elapsed ticker time while callbacks are muted, so child animations catch up when they resume. A disabled ancestor TickerMode remains in effect after PauseAnimations resumes its subtree.

Show widgets in sequence #

Use Sequence for an ordered flow that displays one child at a time. Its controller supports sequential movement and indexed navigation, and exposes the selected index for controls and progress indicators.

final sequenceController = SequenceController();

Sequence(
  controller: sequenceController,
  alignment: AlignmentDirectional.topStart,
  nextTransition: (child, animation) => FadeTransition(
    opacity: animation,
    child: child,
  ),
  previousTransition: (child, animation) => ScaleTransition(
    scale: animation,
    child: child,
  ),
  children: const [
    Text('Account'),
    Text('Preferences'),
    Text('Review'),
  ],
);

sequenceController.next();
sequenceController.previous();
sequenceController.goTo(2);

Navigation is immediate when its directional transition is omitted. Set keepMounted: true only when inactive steps must preserve local widget state: retained steps stay in memory and are still laid out offstage. With the default false, only the current and transitioning steps are mounted.

During a transition, differently sized steps share the largest participant's size and use alignment, which defaults to the directional top-start. A parent such as Center can still move the whole Sequence as that outer size changes; use stable parent constraints when the sequence needs a fixed external anchor. For low-end devices, prefer lightweight transitions such as fade, slide, and scale. Dispose an externally owned controller when its owner is disposed.

Wait for route motion to settle #

Use RouteSettled for controls or route chrome that should appear only after the current route finishes moving and while no navigator gesture is active. It has no built-in visual treatment: provide either direction's transition only when the application needs one.

RouteSettled(
  showTransition: (child, animation) => FadeTransition(
    opacity: animation,
    child: child,
  ),
  child: const CloseButton(),
)

Showing takes 300 ms by default when a show transition exists. Hiding is immediate by default. Without an enclosing route, the child is treated as settled and shown.

Represent offline Dio failures #

Add OfflineErrorDioInterceptor when callers need to distinguish typed offline failures from other Dio errors without scattering connectivity probes through application code.

final dio = Dio()
  ..interceptors.add(OfflineErrorDioInterceptor());

try {
  await dio.get('/jobs');
} on DioException catch (error) {
  if (error.isOfflineConnectionDioException) {
    showOfflineState();
  }
}

The original error remains available as the cause of the typed offline exception. Probe rules and timeout behavior are documented in the API reference.

Launch phone calls and WhatsApp chats #

Use Telephony and Whatsapp at the boundary where application data becomes an external URI. Both utilities sanitize commonly formatted international phone numbers and report whether the platform accepted the launch.

await Telephony().call(number: '+55 (11) 98888-7777');

await Whatsapp().launchChat(
  number: '+55 (11) 98888-7777',
  message: 'Hello! I would like more information.',
);

Always include the country code. These utilities sanitize URI input; they do not verify that a phone number exists.

Documentation #

  • Run the complete public-API example.
  • Read the generated API reference for contracts, defaults, exceptions, and focused examples.

Scope #

oh_my_flutter provides portable utility APIs. It intentionally does not own application state, routing, localization, design components, or application-specific domain logic.

2
likes
0
points
799
downloads

Publisher

verified publisherventairy.com

Weekly Downloads

Empower Flutter with utilities that make development 10x faster.

Repository (GitHub)
View/report issues

Topics

#flutter #utilities #extensions #widget

License

unknown (license)

Dependencies

clock, dio, flutter, internet_connection_checker_plus, meta, url_launcher

More

Packages that depend on oh_my_flutter