fortune_wheel 0.2.1 copy "fortune_wheel: ^0.2.1" to clipboard
fortune_wheel: ^0.2.1 copied to clipboard

A highly customizable spinning fortune wheel widget for Flutter with advanced animations and backend integration support.

Fortune Wheel #

A highly customizable spinning fortune wheel widget for Flutter with advanced animations and backend integration support. Perfect for gamification, prize wheels, decision makers, and any application requiring an interactive spinning wheel.

Fortune Wheel Example

Features #

  • Backend Integration: Spin to results determined by your server
  • Advanced Animations:
    • Spin to specific index with multiple rotations
    • Continuous rotation mode
    • Smooth deceleration with customizable curves
    • Maintain rotation direction for smoother animations
  • Rich Customization:
    • Multiple content types per slice (text, images, lines)
    • Solid colors, gradients, or image backgrounds
    • Configurable slice borders and colors
    • Curved text support (text follows circular path)
    • Straight and vertical text orientations
  • Enhanced Visual Features (New in 0.2.0):
    • Border dots: Decorative dots around wheel border
    • Center indicator: Circular indicator in wheel center
    • Custom arrow pins: ArrowPin and TriangleArrowPin widgets with customizable direction
  • Interactive Features:
    • Tap to select slices
    • Pin/pointer indicator with multiple positions
    • Collision detection with callbacks
    • Custom arrow pins that remain fixed during spins
  • Production Ready:
    • No external dependencies (except curved_text)
    • Optimized CustomPainter rendering
    • Comprehensive example app with enhanced demos

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  fortune_wheel: ^0.2.0

Then run:

flutter pub get

Quick Start #

Basic Usage #

import 'package:fortune_wheel/fortune_wheel.dart';

FortuneWheel(
  slices: [
    Slice.text('Prize 1', backgroundColor: Colors.red),
    Slice.text('Prize 2', backgroundColor: Colors.blue),
    Slice.text('Prize 3', backgroundColor: Colors.green),
    Slice.text('Prize 4', backgroundColor: Colors.orange),
  ],
  pinConfiguration: PinConfiguration.icon(
    icon: Icons.arrow_drop_down,
    color: Colors.red,
  ),
)

Backend Integration (Sequential) #

Perfect for when you need to fetch the result first, then spin:

final GlobalKey<FortuneWheelState> wheelKey = GlobalKey();

// In your spin handler:
Future<void> handleSpin() async {
  // Step 1: Call your backend API
  final response = await http.post('https://your-api.com/spin');
  final result = jsonDecode(response.body);
  final winningIndex = result['winningIndex'];

  // Step 2: Spin to the backend result
  await wheelKey.currentState!.spinToBackendResult(
    winningIndex,
    fullRotations: 5,
    duration: Duration(seconds: 4),
  );

  // Step 3: Show result
  showWinDialog(winningIndex);
}

// Your widget:
FortuneWheel(
  key: wheelKey,
  slices: mySlices,
  // ... other config
)

Backend Integration (Async) #

Start spinning immediately while fetching the result in background:

Future<void> handleAsyncSpin() async {
  // Start spinning immediately
  wheelKey.currentState!.startContinuousRotation(rotationsPerSecond: 2);

  // Fetch result while spinning
  final winningIndex = await fetchResultFromBackend();

  // Stop at the result
  await wheelKey.currentState!.stopContinuousRotation(
    landOnIndex: winningIndex,
  );
}

Configuration #

Wheel Configuration #

FortuneWheel(
  slices: mySlices,
  configuration: WheelConfiguration(
    circlePreferences: CirclePreferences(
      strokeWidth: 4,
      strokeColor: Colors.black,
    ),
    slicePreferences: SlicePreferences(
      strokeWidth: 2,
      strokeColor: Colors.white,
      backgroundColors: SliceBackgroundColors.evenOdd(
        evenColor: Colors.blue,
        oddColor: Colors.red,
      ),
    ),
    startPosition: WheelStartPosition.top,
    layerInsets: EdgeInsets.all(15),
    contentMargins: EdgeInsets.all(15),
  ),
)

Pin Configuration #

// Icon pin
PinConfiguration.icon(
  icon: Icons.arrow_drop_down,
  size: Size(50, 50),
  color: Colors.red,
  position: PinPosition.top,
)

// Image pin
PinConfiguration.image(
  image: AssetImage('assets/pin.png'),
  size: Size(40, 40),
  position: PinPosition.top,
)

// Custom widget pin
PinConfiguration.custom(
  widget: YourCustomWidget(),
  size: Size(50, 50),
)

Slice Content #

// Simple text
Slice.text('Prize', backgroundColor: Colors.red)

// Text with image
Slice.textWithImage(
  'Prize',
  AssetImage('assets/icon.png'),
  imageSize: Size(40, 40),
)

// Custom content
Slice(
  contents: [
    ImageContent(
      image: AssetImage('assets/icon.png'),
      preferredSize: Size(50, 50),
    ),
    TextContent(
      text: 'Grand Prize',
      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      isCurved: true,  // Text follows circular arc
    ),
    LineContent(
      color: Colors.white,
      width: 2,
    ),
  ],
  gradient: LinearGradient(
    colors: [Colors.purple, Colors.blue],
  ),
)

Animation Methods #

Spin to Index #

// Spin with multiple rotations to specific slice
await wheelKey.currentState!.spinToIndex(
  targetIndex,
  fullRotations: 5,
  duration: Duration(seconds: 4),
);

// Quick rotation without full spins
wheelKey.currentState!.rotateToIndex(
  targetIndex,
  duration: Duration(milliseconds: 500),
);

Continuous Rotation #

// Start continuous spinning
wheelKey.currentState!.startContinuousRotation(
  rotationsPerSecond: 2.0,
);

// Stop continuous spinning
wheelKey.currentState!.stopContinuousRotation(
  landOnIndex: 3,  // Optional: land on specific slice
);

// Stop immediately
wheelKey.currentState!.stop();

Callbacks #

FortuneWheel(
  slices: mySlices,
  onSliceTap: (index) {
    print('Tapped slice $index');
  },
  onEdgeCollision: (progress) {
    // Called when slice edge passes pin
    // progress: 0.0 to 1.0 during deceleration, null during continuous
    HapticFeedback.lightImpact();
  },
  onCenterCollision: (progress) {
    // Called when slice center passes pin
    playSound();
  },
  edgeCollisionDetection: true,
  centerCollisionDetection: true,
)

Advanced Features #

Enhanced Visual Features (New in 0.2.0) #

Border Dots

Add decorative dots around the wheel border:

FortuneWheel(
  slices: mySlices,
  configuration: WheelConfiguration(
    circlePreferences: CirclePreferences(
      strokeWidth: 8,
      strokeColor: Colors.black,
      borderDots: BorderDotsConfiguration(
        dotSize: 8,
        dotColor: Colors.orange,
        dotBorderColor: Colors.black,
        dotBorderWidth: 2,
        dotsPerSlice: 1, // One dot per slice boundary
      ),
    ),
  ),
)

Center Indicator

Add a circular indicator in the wheel center:

FortuneWheel(
  slices: mySlices,
  configuration: WheelConfiguration(
    circlePreferences: CirclePreferences(
      centerIndicator: CenterIndicatorConfiguration(
        radius: 35,
        color: Colors.white,
        borderColor: Colors.black,
        borderWidth: 4,
      ),
    ),
  ),
)

Custom Arrow Pins

Use custom arrow widgets that stay fixed while the wheel spins:

// Classic arrow pin with customizable direction
PinConfiguration.custom(
  widget: ArrowPin(
    color: Colors.red,
    size: Size(40, 50),
    borderColor: Colors.black,
    borderWidth: 2.0,
    withShadow: true,
    direction: math.pi / 2, // Points downward (0 = right, pi/2 = down, pi = left, 3*pi/2 = up)
  ),
  size: Size(40, 50),
  position: PinPosition.top,
  verticalOffset: 10, // Distance from wheel edge
)

// Triangle arrow pin - simpler design
PinConfiguration.custom(
  widget: TriangleArrowPin(
    color: Colors.blue,
    size: Size(30, 40),
    direction: math.pi / 2,
  ),
  size: Size(30, 40),
  position: PinPosition.top,
)

Curved Text #

Text can follow the circular arc of the wheel:

TextContent(
  text: 'Your Prize Name',
  isCurved: true,
  orientation: TextOrientation.horizontal,
  style: TextStyle(fontSize: 18),
)

Gradients #

Slice(
  contents: [TextContent(text: 'Prize')],
  gradient: LinearGradient(
    colors: [Colors.purple, Colors.blue],
    begin: Alignment.topCenter,
    end: Alignment.bottomCenter,
  ),
)

Multiple Content Items #

Stack multiple content items in a slice:

Slice(
  contents: [
    ImageContent(
      image: AssetImage('assets/icon.png'),
      preferredSize: Size(40, 40),
    ),
    TextContent(text: 'Prize Name'),
    TextContent(
      text: 'Value: \$100',
      style: TextStyle(fontSize: 12),
    ),
  ],
)

Complete Enhanced Wheel Example #

Here's a complete example using all the new visual features:

import 'package:fortune_wheel/fortune_wheel.dart';
import 'dart:math' as math;

class EnhancedWheelExample extends StatelessWidget {
  final GlobalKey<FortuneWheelState> wheelKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Enhanced Fortune Wheel')),
      body: Center(
        child: FortuneWheel(
          key: wheelKey,
          slices: [
            Slice.text('Prize 1', backgroundColor: Color(0xFFE74C3C)),
            Slice.text('Prize 2', backgroundColor: Color(0xFF3498DB)),
            Slice.text('Prize 3', backgroundColor: Color(0xFF2ECC71)),
            Slice.text('Prize 4', backgroundColor: Color(0xFFF39C12)),
            Slice.text('Prize 5', backgroundColor: Color(0xFF9B59B6)),
            Slice.text('Prize 6', backgroundColor: Color(0xFF1ABC9C)),
          ],
          configuration: WheelConfiguration(
            circlePreferences: CirclePreferences(
              strokeWidth: 8,
              strokeColor: Color(0xFF34495E),
              // Decorative border dots
              borderDots: BorderDotsConfiguration(
                dotSize: 8,
                dotColor: Color(0xFFFFA500),
                dotBorderColor: Color(0xFF34495E),
                dotBorderWidth: 2,
                dotsPerSlice: 1,
              ),
              // Center indicator circle
              centerIndicator: CenterIndicatorConfiguration(
                radius: 35,
                color: Colors.white,
                borderColor: Color(0xFF34495E),
                borderWidth: 4,
              ),
            ),
            slicePreferences: SlicePreferences(
              strokeWidth: 2,
              strokeColor: Color(0xFF2C3E50),
            ),
            startPosition: WheelStartPosition.top,
            layerInsets: EdgeInsets.all(20),
            contentMargins: EdgeInsets.all(20),
          ),
          // Custom arrow pin that stays fixed
          pinConfiguration: PinConfiguration.custom(
            widget: ArrowPin(
              color: Color(0xFFE74C3C),
              size: Size(40, 50),
              borderColor: Color(0xFF34495E),
              borderWidth: 2.5,
              withShadow: true,
              direction: math.pi / 2, // Points down
            ),
            size: Size(40, 50),
            position: PinPosition.top,
            verticalOffset: 10,
          ),
          edgeCollisionDetection: true,
          onEdgeCollision: (progress) {
            // Add haptic feedback or sound
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // Spin to random slice
          final random = math.Random();
          final targetIndex = random.nextInt(6);
          await wheelKey.currentState!.spinToBackendResult(
            targetIndex,
            fullRotations: 5,
            duration: Duration(seconds: 4),
          );
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

Real-World Backend Integration Example #

Here's a complete example with error handling:

Future<void> spinWithBackend() async {
  try {
    setState(() => isSpinning = true);

    // Call your backend
    final response = await http.post(
      Uri.parse('https://api.example.com/spin'),
      headers: {'Authorization': 'Bearer $token'},
      body: jsonEncode({'userId': currentUserId}),
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      final winningIndex = data['winningIndex'];

      // Spin to result
      await wheelKey.currentState!.spinToBackendResult(
        winningIndex,
        fullRotations: 5,
        duration: Duration(seconds: 4),
      );

      // Update UI with result
      setState(() {
        currentPrize = prizes[winningIndex];
        isSpinning = false;
      });

      showPrizeDialog();
    } else {
      throw Exception('Failed to fetch result');
    }
  } catch (e) {
    setState(() => isSpinning = false);
    showErrorDialog(e.toString());
  }
}

Example App #

Run the example app to see all features in action:

cd example
flutter run

The example demonstrates:

  • Enhanced Wheel Demo (New in 0.2.0):
    • Border dots decoration
    • Center indicator circle
    • Custom arrow pin that stays fixed during spins
    • Curved text mode for better readability
  • Sequential backend integration
  • Async backend integration
  • Quick test buttons
  • Collision detection
  • Custom styling

The demo includes a selector to navigate between the Enhanced Wheel and the original Fortune Wheel examples.

Performance Tips #

  1. Reuse Slices: Create slices once in initState, don't recreate on every build
  2. Optimize Images: Use appropriately sized images for slice content
  3. Limit Collision Detection: Only enable when needed (adds overhead)
  4. Animation Duration: Longer animations (4-5s) feel more realistic

Platform Support #

  • iOS
  • Android
  • Web
  • macOS
  • Linux
  • Windows

Credits #

Inspired by SwiftFortuneWheel for iOS.

License #

MIT License - see LICENSE file for details

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

1
likes
150
points
109
downloads

Publisher

verified publisherabdurahman-ibrahem.ly

Weekly Downloads

A highly customizable spinning fortune wheel widget for Flutter with advanced animations and backend integration support.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

curved_text, flutter

More

Packages that depend on fortune_wheel