Overlapping artboards when using databinding in pushed routes

Open
#605 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Stale
Tech stack
dart, flutter
Domain
mobile-dev

Research direction

Start by reproducing the issue with the WuzuAvatar implementation, RiveWidgetController, dataBind(), and Navigator.push using the provided artboard and ViewModel setup. Compare root-level and pushed routes; done means data-bound widgets render only the selected artboard with the expected nested variants on both iOS and Android.

Written by the indexing model from the issue text.

Description

bug

Summary

When using the same Rive widget with data binding in multiple routes, artboards display correctly on the first-level route but show overlapping/incorrect artboards on pushed routes.

Environment

  • rive: 0.14.1
  • Flutter: 3.35.4 (Dart 3.9.2)
  • Platform: iOS / Android

Expected Behavior

The WuzuAvatar widget should display the correct single artboard with data-bound properties (outfit, hat, background, etc.) regardless of which route it's rendered on.

Actual Behavior

  • On the first-level route (e.g., home screen): Widget displays correctly with the specified artboard and bound properties.
  • On pushed routes (e.g., diary detail, weekly report): Widget displays overlapping artboards that are not the ones specified. Multiple artboards appear to be rendered simultaneously.

Key observation: If I remove all data binding (ViewModelInstance properties), the widget renders correctly without overlapping issues.

Steps to Reproduce

  1. Create a Rive file with multiple artboards (e.g., cat, bear, dog)
  2. Set up a ViewModel with artboard-type properties (e.g., clothes_back, clothes_front, hat_front)
  3. Create a Flutter widget that uses RiveWidgetController with dataBind()
  4. Use the widget on a root-level screen → works correctly
  5. Navigate to a pushed route and use the same widget → overlapping artboards appear

Code

Widget Implementation
class WuzuAvatar extends ConsumerStatefulWidget {
  const WuzuAvatar({
    super.key,
    this.controller,
    this.artboard,           // Which artboard to use (cat, bear, dog)
    this.collarColor,
    this.eyeColor,
    this.outfit,             // Nested artboard variant
    this.hat,                // Nested artboard variant
    this.background,         // Nested artboard variant
    this.foreground,         // Nested artboard variant
    // ... more properties
    this.stateMachine,
    this.size = 48.0,
    this.onTap,
  });

  // ... properties

  @override
  ConsumerState<WuzuAvatar> createState() => _WuzuAvatarState();
}

class _WuzuAvatarState extends ConsumerState<WuzuAvatar> {
  rive.File? _file;
  rive.RiveWidgetController? _riveController;
  rive.ViewModelInstance? _viewModelInstance;
  rive.ViewModelInstanceArtboard? _clothesBackProperty;
  rive.ViewModelInstanceArtboard? _clothesFrontProperty;
  rive.ViewModelInstanceArtboard? _hatFrontProperty;
  // ... more properties

  @override
  void initState() {
    super.initState();
    _loadRiveFile();
  }

  Future<void> _loadRiveFile() async {
    final data = await rootBundle.load('assets/rive/wuzu.riv');
    final bytes = data.buffer.asUint8List(
      data.offsetInBytes,
      data.lengthInBytes,
    );
    final file = await rive.File.decode(
      bytes,
      riveFactory: rive.Factory.rive,
    );

    if (!mounted) return;

    setState(() {
      _file = file;
      _riveController = rive.RiveWidgetController(
        file,
        artboardSelector: rive.ArtboardSelector.byName(effectiveArtboard.value),
        stateMachineSelector: widget.stateMachine != null
            ? rive.StateMachineSelector.byName(widget.stateMachine!.value)
            : const rive.StateMachineDefault(),
      );
    });

    _initDataBinding(effectiveArtboard);
  }

  void _initDataBinding(WuzuArtboard effectiveArtboard) {
    if (_riveController == null || _file == null) return;

    // Bind by index (each artboard has its own ViewModel)
    _viewModelInstance = _riveController!.dataBind(
      rive.DataBind.byIndex(effectiveArtboard.index),
    );

    if (_viewModelInstance == null) return;

    // Bind nested artboard properties
    _clothesBackProperty = _viewModelInstance!.artboard('clothes_back');
    _clothesFrontProperty = _viewModelInstance!.artboard('clothes_front');
    _hatFrontProperty = _viewModelInstance!.artboard('hat_front');
    // ... more bindings

    // Set initial values
    _updateOutfit();
    _updateHat();
    // ... more updates
  }

  void _updateArtboard(
    rive.ViewModelInstanceArtboard? property,
    String prefix,
    WuzuVariant? variant,
  ) {
    if (property == null || variant == null || _file == null) return;

    final artboardName = '${prefix}_${variant.value}';
    final bindableArtboard = _file!.artboardToBind(artboardName);
    if (bindableArtboard != null) {
      property.value = bindableArtboard;
    }
  }

  void _disposeDataBindingProperties() {
    _clothesBackProperty?.dispose();
    _clothesFrontProperty?.dispose();
    _hatFrontProperty?.dispose();
    // ... dispose all properties
    _viewModelInstance?.dispose();
    _riveController?.dispose();
  }

  @override
  void dispose() {
    _disposeDataBindingProperties();
    _file?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.size * 1.378,
      height: widget.size,
      child: rive.RiveWidget(
        controller: _riveController!,
        fit: rive.Fit.contain,
      ),
    );
  }
}
Usage on First-Level Route (Works)
// stats_page.dart - Root level route
WuzuAvatar(
  controller: _avatarController,
  stateMachine: stateMachine,
  collarColor: profile.getCollarColor(Theme.of(context).brightness),
  eyeColor: profile.getEyeColor(),
  outfit: profile.equippedOutfitEnum,
  hat: profile.equippedHatEnum,
  size: _avatarSize,
)
Usage on Pushed Route (Shows Overlapping)
// diary_detail_screen.dart - Pushed via Navigator.push
WuzuAvatar(
  collarColor: profile?.getCollarColor(brightness),
  eyeColor: profile?.getEyeColor(),
  outfit: profile?.equippedOutfitEnum,
  hat: profile?.equippedHatEnum,
  background: WuzuVariant.hide,
  foreground: WuzuVariant.hide,
  inner: WuzuVariant.hide,
  size: 48,
)

Rive File Structure

  • Main artboards: cat, bear, dog (index 0, 1, 2)
  • Nested artboard slots: clothes_back, clothes_front, hat_front, hat_back, background, foreground, inner, facial, blush
  • Variant artboards: clothes_back_default, clothes_back_rain_coat, hat_front_witch, etc.
  • ViewModel per main artboard with artboard-type properties for each slot

Workaround

Removing all data binding code makes the artboards render correctly (but without dynamic property binding).

Screenshots

  1. Correct display on first-level route
Image
  1. Overlapping artboards on pushed route (down right bottom)
Image
Dominant language
Dart
Stars
1.5k
Forks
240
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from rive-app/rive-flutter

All issues in rive-app/rive-flutter

Similar issues

More Dart issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.