0Pricing
Flutter Mobile Development · 강의

상태 머신 및 입력 기반 모션

Rive 상태 머신 입력을 앱 데이터에 연결해 반응형 분기 애니메이션을 구현합니다.

상태 머신 및 입력 기반 모션은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why State Machines Beat Linear Animations

Traditional Rive animations play a single timeline from start to finish. A state machine is a graph of animation states connected by transitions, and those transitions fire based on inputs you control from Dart.

  • Reactive: the animation reflects live app data instead of a fixed sequence.
  • Branching: the same artboard can idle, hover, succeed, or fail depending on input values.
  • Blended: Rive interpolates between states, so you get smooth motion for free.

In this lesson you will wire a Rive state machine's inputs to your widget tree so motion follows user actions and model state.

The Three Input Types

A Rive state machine exposes exactly three input kinds, and choosing the right one is the core design decision.

  • Boolean (SMIBool): a held on/off value, e.g. isOpen or isLoading.
  • Number (SMINumber): a continuous value that can drive blend states, e.g. a 0–100 progress or a scroll offset.
  • Trigger (SMITrigger): a one-shot pulse that fires a transition once, e.g. tapSuccess. It has no persistent value.

Rule of thumb: use a Trigger for momentary events, a Boolean for state you hold, and a Number for anything continuous you want to blend.

Loading the Artboard and State Machine

To drive inputs you need a StateMachineController. The onInit callback of RiveAnimation.asset hands you the loaded Artboard. You build a controller from a named state machine and attach it.

Always grab references to the inputs you need right here, while you still have the artboard, and store them on your widget's state.

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

class LikeButton extends StatefulWidget {
  const LikeButton({super.key});
  @override
  State<LikeButton> createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  StateMachineController? _controller;
  SMIBool? _isLiked;

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(
      artboard,
      'LikeMachine',
    );
    if (controller != null) {
      artboard.addController(controller);
      _controller = controller;
      _isLiked = controller.findInput<bool>('isLiked') as SMIBool?;
    }
  }

  @override
  Widget build(BuildContext context) {
    return RiveAnimation.asset(
      'assets/like.riv',
      stateMachines: const ['LikeMachine'],
      onInit: _onRiveInit,
    );
  }
}

Setting a Boolean Input from a Tap

Once you hold an SMIBool reference, driving it is just assigning to its value. Rive immediately evaluates transitions whose condition depends on that input.

Wrap the Rive widget in a GestureDetector and toggle the boolean. There is no need to call setState for the animation itself — Rive repaints internally — but you may want it to keep your own model in sync.

GestureDetector(
  onTap: () {
    final input = _isLiked;
    if (input != null) {
      input.value = !input.value; // flips the held state
    }
  },
  child: RiveAnimation.asset(
    'assets/like.riv',
    stateMachines: const ['LikeMachine'],
    onInit: _onRiveInit,
  ),
)

Firing a Trigger for One-Shot Motion

A SMITrigger has no value. You call fire() to pulse it exactly once, which is perfect for confetti bursts, success checkmarks, or shake-on-error feedback.

Because triggers auto-reset, you never have to clear them. Firing again replays the transition.

class _SubmitButtonState extends State<SubmitButton> {
  SMITrigger? _success;
  SMITrigger? _error;

  void _onInit(Artboard artboard) {
    final c = StateMachineController.fromArtboard(artboard, 'SubmitMachine');
    if (c != null) {
      artboard.addController(c);
      _success = c.findSMI('success') as SMITrigger?;
      _error = c.findSMI('error') as SMITrigger?;
    }
  }

  Future<void> _onSubmit() async {
    final ok = await _saveForm();
    if (ok) {
      _success?.fire();
    } else {
      _error?.fire();
    }
  }
}

Driving a Number Input for Blended States

Number inputs shine when motion must follow a continuous value. A blend state in Rive can morph an artboard across a 0–100 range, so feeding it live data produces fluid, data-driven motion.

Common sources: download progress, a scroll position, a slider, or a sensor reading. Clamp the value so it never exceeds the range the artist designed for.

class _ProgressGaugeState extends State<ProgressGauge> {
  SMINumber? _progress;

  void _onInit(Artboard artboard) {
    final c = StateMachineController.fromArtboard(artboard, 'GaugeMachine');
    if (c != null) {
      artboard.addController(c);
      _progress = c.findSMI('progress') as SMINumber?;
    }
  }

  void updateProgress(double fraction) {
    // fraction is 0.0..1.0 from a download stream
    _progress?.value = (fraction * 100).clamp(0, 100);
  }
}

Connecting Inputs to a Stream of App Data

Input-driven motion becomes powerful when a data stream feeds it. Subscribe in initState, push each event into the matching input, and cancel the subscription in dispose to avoid leaks.

Notice you guard against a null input: the stream may emit before onInit has run, so a null-aware call keeps you safe.

late final StreamSubscription<double> _sub;

@override
void initState() {
  super.initState();
  _sub = downloadProgress$.listen((fraction) {
    _progress?.value = (fraction * 100).clamp(0, 100);
  });
}

@override
void dispose() {
  _sub.cancel();
  _controller?.dispose();
  super.dispose();
}

Modeling Branching Logic in Pure Dart

Before touching Rive, it helps to model the decision that selects a branch as plain Dart. Here is a self-contained mapper that turns an upload result into the kind of input event you would fire. Testing this logic in isolation keeps your widget thin.

This snippet has no Flutter or Rive dependency, so it runs anywhere.

enum MotionEvent { idle, loading, success, error }

MotionEvent selectMotion({
  required bool inFlight,
  required bool? succeeded,
}) {
  if (inFlight) return MotionEvent.loading;
  if (succeeded == null) return MotionEvent.idle;
  return succeeded ? MotionEvent.success : MotionEvent.error;
}

void main() {
  print(selectMotion(inFlight: true, succeeded: null));   // loading
  print(selectMotion(inFlight: false, succeeded: true));  // success
  print(selectMotion(inFlight: false, succeeded: false)); // error
  print(selectMotion(inFlight: false, succeeded: null));  // idle
}

Applying a Branch Decision to Inputs

Now bind that pure decision to the state machine. A single method translates a MotionEvent into the correct combination of boolean holds and trigger pulses.

  • Booleans hold state, so set both the new value and clear the opposite where needed.
  • Triggers fire once for transient feedback.

Centralizing this in one method keeps your input wiring consistent and easy to audit.

void applyMotion(MotionEvent event) {
  switch (event) {
    case MotionEvent.loading:
      _isLoading?.value = true;
      break;
    case MotionEvent.idle:
      _isLoading?.value = false;
      break;
    case MotionEvent.success:
      _isLoading?.value = false;
      _success?.fire();
      break;
    case MotionEvent.error:
      _isLoading?.value = false;
      _error?.fire();
      break;
  }
}

Reading Input State Back via Listeners

Sometimes motion is driven by Rive itself (e.g. a draggable knob the user moves on the artboard) and you need the current value back in Dart. Inputs expose their value, and you can poll or react to it inside the state machine's change callback.

The StateMachineController also accepts an onStateChange callback so you can run Dart whenever the active state transitions, useful for analytics or chaining side effects.

void _onInit(Artboard artboard) {
  final c = StateMachineController.fromArtboard(
    artboard,
    'KnobMachine',
    onStateChange: (machineName, stateName) {
      debugPrint('Entered $stateName in $machineName');
      if (stateName == 'Snapped') {
        HapticFeedback.lightImpact();
      }
    },
  );
  if (c != null) {
    artboard.addController(c);
    _angle = c.findSMI('angle') as SMINumber?;
  }
}

Lifecycle, Disposal, and Common Pitfalls

State machines are stateful resources. Treat them with the same discipline as controllers and subscriptions.

  • Always dispose the StateMachineController in dispose() to release the artboard binding.
  • Null-check inputs: findSMI/findInput return null if the name is misspelled or the artboard lacks that input — a silent no-op otherwise.
  • Match names exactly: input and state-machine names are case-sensitive and must match the .riv file.
  • Do not over-call setState: Rive repaints itself; rebuilding the whole subtree on every stream tick wastes frames.

Get these right and your input-driven motion stays smooth and leak-free.

Quick Check: Choosing the Right Input

You are wiring a Rive state machine to a form submit button. Tapping submit should play a one-time success checkmark animation that then returns to idle. Which input type and call is correct?

Recap: Reactive, Branching Motion

You now know how to make Rive animations follow your app instead of a fixed timeline.

  • Three inputs: Boolean for held state, Number for continuous/blended values, Trigger for one-shot events.
  • Wiring: build a StateMachineController in onInit, attach it to the artboard, and cache input references.
  • Driving: assign to value for booleans/numbers, call fire() for triggers, and feed continuous data from streams with clamping.
  • Architecture: model the branch decision in pure, testable Dart, then translate it to inputs in one central method.
  • Hygiene: dispose the controller, cancel subscriptions, null-check inputs, and match names exactly.

With these patterns your animations become a live reflection of user input and model state.

자주 묻는 질문

“상태 머신 및 입력 기반 모션” 강의는 무료인가요?

네 — “상태 머신 및 입력 기반 모션” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“상태 머신 및 입력 기반 모션”에서 뭘 배우나요?

Rive 상태 머신 입력을 앱 데이터에 연결해 반응형 분기 애니메이션을 구현합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“상태 머신 및 입력 기반 모션” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Rive 에셋 및 AnimationControllers 삽입
  2. 상태 머신 및 입력 기반 모션
  3. Hero 전환 및 공유 요소 모션
  4. 시차 애니메이션 및 연출된 AnimationControllers
← Flutter Mobile Development(으)로 돌아가기