Skip to content

Overlapping artboards when using databinding in pushed routes #605

@t42ji2ji

Description

@t42ji2ji

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions