Flutter Body Part Selector
An interactive body selector package for Flutter that allows users to select muscles on a body diagram. Users can tap on muscles in the SVG body diagram or select them programmatically, with visual highlighting of selected muscles.
⚠️ IMPORTANT: This package includes mandatory SVG assets that must be used. Custom SVG files are not supported.
https://github.com/user-attachments/assets/8ba0b47b-fa72-4055-bee8-26f50427437c
Features
- 🎯 Interactive Muscle Selection: Tap on any muscle in the body diagram to select it
- 🎨 Visual Highlighting: Selected muscles are automatically highlighted with customizable colors
- 🔄 Front/Back Views: Toggle between front and back body views
- 📱 Programmatic Control: Select muscles programmatically using the controller
- 🎛️ Customizable: Customize highlight colors and base colors
- 📦 Easy to Use: Simple API with minimal setup required - includes all required assets
- 🎨 Built-in Assets: Package includes mandatory SVG body diagrams (front and back views)
Installation
Add this to your package's pubspec.yaml file:
dependencies:
flutter_body_part_selector: ^1.1.5
Then run:
flutter pub get
Usage
Quick Start (Recommended)
The easiest way to use this package is with the InteractiveBodyWidget:
import 'package:flutter/material.dart';
import 'package:flutter_body_part_selector/flutter_body_part_selector.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: InteractiveBodyWidget(
// Asset paths are optional - package includes default assets
onMuscleSelected: (muscle) {
print('Selected muscle: $muscle');
},
),
);
}
}
Advanced Usage
For more control, use InteractiveBodySvg with BodyMapController:
import 'package:flutter/material.dart';
import 'package:flutter_body_part_selector/flutter_body_part_selector.dart';
class BodySelectorExample extends StatefulWidget {
@override
State<BodySelectorExample> createState() => _BodySelectorExampleState();
}
class _BodySelectorExampleState extends State<BodySelectorExample> {
final controller = BodyMapController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Interactive Body Selector'),
actions: [
IconButton(
icon: const Icon(Icons.flip),
onPressed: controller.toggleView,
tooltip: 'Flip view',
),
],
),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Column(
children: [
if (controller.selectedMuscles.isNotEmpty)
Container(
padding: const EdgeInsets.all(16),
color: Colors.blue.shade900,
width: double.infinity,
child: Text(
'Selected: ${controller.selectedMuscles.length} muscles',
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
Expanded(
child: InteractiveBodySvg(
isFront: controller.isFront,
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
),
),
],
);
},
),
);
}
}
Using the Controller
The BodyMapController manages the state of the body selector:
Basic Usage
final controller = BodyMapController();
// Select a muscle programmatically (toggles if already selected)
controller.selectMuscle(Muscle.bicepsLeft);
// Clear all selections
controller.clearSelection();
// Toggle between front and back view
controller.toggleView();
// Set specific view
controller.setFrontView();
controller.setBackView();
// Access current state
final selected = controller.selectedMuscles; // Returns Set<Muscle> (read-only getter)
final isFront = controller.isFront; // Writable: can be set directly
// Set entire selection using setter (convenience)
controller.selectedMuscles = {Muscle.bicepsLeft, Muscle.tricepsRight};
Initialization with Pre-selected Muscles
// Create controller with initial selection
final controller = BodyMapController(
initialSelectedMuscles: {Muscle.bicepsLeft, Muscle.tricepsRight},
initialDisabledMuscles: {Muscle.chestLeft}, // Optional: pre-disable muscles
initialIsFront: true, // Optional: start with back view
);
Programmatic Selection Management
final controller = BodyMapController();
// Toggle a muscle's selection state
controller.toggleMuscle(Muscle.bicepsLeft);
// Deselect a specific muscle
controller.deselectMuscle(Muscle.bicepsLeft);
// Set entire selection (replaces current selection)
controller.setSelectedMuscles({
Muscle.bicepsLeft,
Muscle.tricepsRight,
Muscle.chestLeft,
});
// Add multiple muscles to current selection (without clearing)
controller.selectMultiple({
Muscle.bicepsLeft,
Muscle.tricepsRight,
});
// Check if a muscle is selected
if (controller.isSelected(Muscle.bicepsLeft)) {
print('Biceps left is selected');
}
// Get all selected muscles (read-only Set)
final selected = controller.selectedMuscles;
print('Selected ${selected.length} muscles');
Managing Disabled Muscles
final controller = BodyMapController();
// Disable a muscle (locks it, removes from selection)
controller.disableMuscle(Muscle.chestLeft);
// Enable a muscle (unlocks it)
controller.enableMuscle(Muscle.chestLeft);
// Set multiple disabled muscles at once
controller.setDisabledMuscles({
Muscle.chestLeft,
Muscle.chestRight,
});
// Check if a muscle is disabled
if (controller.isDisabled(Muscle.chestLeft)) {
print('Chest left is disabled');
}
Complete Example: Programmatic Selection Management
import 'package:flutter/material.dart';
import 'package:flutter_body_part_selector/flutter_body_part_selector.dart';
class BodySelectorPage extends StatefulWidget {
@override
State<BodySelectorPage> createState() => _BodySelectorPageState();
}
class _BodySelectorPageState extends State<BodySelectorPage> {
late BodyMapController controller;
@override
void initState() {
super.initState();
// Initialize with some pre-selected muscles
controller = BodyMapController(
initialSelectedMuscles: {Muscle.bicepsLeft, Muscle.tricepsRight},
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Body Selector')),
body: Column(
children: [
// Control buttons
Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
spacing: 8.0,
children: [
ElevatedButton(
onPressed: () {
// Select multiple muscles programmatically
controller.selectMultiple({
Muscle.bicepsLeft,
Muscle.bicepsRight,
});
},
child: const Text('Select Both Biceps'),
),
ElevatedButton(
onPressed: () {
// Set entire selection
controller.setSelectedMuscles({
Muscle.chestLeft,
Muscle.chestRight,
});
},
child: const Text('Select Chest Only'),
),
ElevatedButton(
onPressed: () {
// Clear all selections
controller.clearSelection();
},
child: const Text('Clear All'),
),
],
),
),
// Display selected muscles
AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.blue.shade900,
width: double.infinity,
child: Text(
'Selected: ${controller.selectedMuscles.length} muscles',
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
);
},
),
// Body diagram
Expanded(
child: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return InteractiveBodySvg(
isFront: controller.isFront,
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
);
},
),
),
],
),
);
}
}
Customization Options
Colors and Styling
InteractiveBodySvg(
isFront: true, // Use package's front body asset automatically
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
highlightColor: Colors.red.withOpacity(0.7), // Custom highlight color
baseColor: Colors.white, // Custom base color for unselected muscles
selectedStrokeWidth: 3.0, // Stroke width for selected muscles
unselectedStrokeWidth: 1.0, // Stroke width for unselected muscles
)
Size and Layout
InteractiveBodySvg(
isFront: true, // Use package's front body asset automatically
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
width: 300, // Fixed width
height: 600, // Fixed height
fit: BoxFit.cover, // How to fit the SVG
alignment: Alignment.center, // Alignment within the widget
)
Selection Behavior
InteractiveBodySvg(
isFront: true, // Use package's front body asset automatically
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
enableSelection: true, // Enable/disable tap selection
hitTestPadding: 15.0, // Padding for hit-testing (makes taps more forgiving)
onMuscleTapDisabled: (muscle) {
// Called when a muscle is tapped but selection is disabled
print('Tapped $muscle but selection is disabled');
},
)
Using InteractiveBodyWidget
The InteractiveBodyWidget provides a complete solution with built-in UI:
InteractiveBodyWidget(
frontAsset: 'packages/flutter_body_part_selector/assets/svg/body_front.svg',
backAsset: 'packages/flutter_body_part_selector/assets/svg/body_back.svg',
onMuscleSelected: (muscle) {
print('Selected: $muscle');
},
highlightColor: Colors.blue,
baseColor: Colors.white,
showFlipButton: true, // Show flip button in app bar
showClearButton: true, // Show clear button in app bar
backgroundColor: Colors.black, // Background color
selectedMuscleHeader: (muscle) {
// Custom header widget
return Container(
padding: EdgeInsets.all(16),
child: Text('Selected: $muscle'),
);
},
)
Available Muscles
The package supports the following muscles:
Front View
- Traps (Left/Right)
- Delts (Left/Right)
- Chest (Left/Right)
- Abs
- Lats Front (Left/Right)
- Triceps (Left/Right)
- Biceps (Left/Right)
- Biceps Brachialis (Left/Right)
- Forearms (Left/Right)
- Quads (Left/Right)
- Calves (Left/Right)
Back View
- Lats Back (Left/Right)
- Lower Lats Back (Left/Right)
- Glutes (Left/Right)
- Hamstrings (Left/Right)
- Triceps (Left/Right)
- Delts (Left/Right)
- Traps (Left/Right)
Assets
IMPORTANT: This package includes the required SVG body diagrams (front and back views) that are mandatory for the package to work correctly. You must use the package assets - custom SVG files are not supported.
The package assets are pre-configured with the correct muscle IDs and mappings. Using custom assets will result in incorrect behavior.
Using Package Assets
The package includes default SVG assets that are automatically used by InteractiveBodyWidget. You don't need to specify asset paths:
// Simplest usage - assets are included automatically
InteractiveBodyWidget(
onMuscleSelected: (muscle) {
print('Selected: $muscle');
},
)
For InteractiveBodySvg, you can use the simplified syntax without specifying asset paths:
InteractiveBodySvg(
isFront: true, // Automatically uses package's front body asset
selectedMuscles: controller.selectedMuscles,
onMuscleTap: controller.selectMuscle,
)
Or if you need to specify asset paths explicitly:
InteractiveBodySvg(
asset: 'packages/flutter_body_part_selector/assets/svg/body_front.svg',
// ...
)
Note: The package assets are automatically included when you add this package to your pubspec.yaml. No additional asset configuration is required in your app's pubspec.yaml.
API Reference
InteractiveBodyWidget
A complete widget with built-in controller and UI. Perfect for quick integration.
Properties:
frontAsset(String?, optional): Path to the front body SVG. Defaults to'packages/flutter_body_part_selector/assets/svg/body_front.svg'if not specified. Custom SVG files are not supported.backAsset(String?, optional): Path to the back body SVG. Defaults to'packages/flutter_body_part_selector/assets/svg/body_back.svg'if not specified. Custom SVG files are not supported.onMuscleSelected(Function(Muscle)?, optional): Callback when a muscle is selectedonSelectionCleared(VoidCallback?, optional): Callback when selection is clearedselectedMuscles(SetinitialIsFront(bool, default: true): Initial view (front or back)highlightColor(Color?, optional): Color for highlighting selected musclesbaseColor(Color?, optional): Base color for unselected musclesselectedStrokeWidth(double, default: 2.0): Stroke width for selected musclesunselectedStrokeWidth(double, default: 1.0): Stroke width for unselected musclesenableSelection(bool, default: true): Enable/disable selectionfit(BoxFit, default: BoxFit.contain): How to fit the SVGhitTestPadding(double, default: 10.0): Padding for hit-testingwidth(double?, optional): Fixed widthheight(double?, optional): Fixed heightalignment(Alignment, default: Alignment.center): Alignment of SVGshowFlipButton(bool, default: true): Show flip button in app barshowClearButton(bool, default: true): Show clear button in app barappBar(PreferredSizeWidget?, optional): Custom app barbackgroundColor(Color?, optional): Background colorselectedMuscleHeader(Widget Function(Muscle)?, optional): Custom header widget
InteractiveBodySvg
The core widget for displaying the interactive body diagram.
Properties:
asset(String?, optional): Custom asset path. If not provided, automatically uses package assets based onisFront. Note: Custom SVG files are not supported - only package assets work correctly.isFront(bool, default: true): Whether to show front view. Used whenassetis not provided to automatically select the correct package asset.selectedMuscles(SetonMuscleTap(Function(Muscle)?, optional): Callback when a muscle is tappedhighlightColor(Color?, optional): Color for highlighting selected muscles (default: Colors.blue with opacity)baseColor(Color?, optional): Base color for unselected muscles (default: Colors.white)selectedStrokeWidth(double, default: 2.0): Stroke width for selected musclesunselectedStrokeWidth(double, default: 1.0): Stroke width for unselected musclesenableSelection(bool, default: true): Enable/disable tap selectionfit(BoxFit, default: BoxFit.contain): How to fit the SVGhitTestPadding(double, default: 10.0): Padding for hit-testingwidth(double?, optional): Fixed widthheight(double?, optional): Fixed heightalignment(Alignment, default: Alignment.center): Alignment of SVGonMuscleTapDisabled(Function(Muscle)?, optional): Callback when muscle is tapped but selection is disabled
BodyMapController
Controller for managing the body selector state.
Constructor:
BodyMapController({Set<Muscle>? initialSelectedMuscles, Set<Muscle>? initialDisabledMuscles, bool initialIsFront = true}): Create a controller with optional initial state
Selection Methods (Writable):
selectMuscle(Muscle): Select or toggle a muscle (if already selected, deselects it)toggleMuscle(Muscle): Explicitly toggle a muscle's selection statedeselectMuscle(Muscle): Deselect a specific musclesetSelectedMuscles(Set<Muscle>): Set the entire selection (replaces current selection)selectMultiple(Set<Muscle>): Add multiple muscles to current selection (without clearing)clearSelection(): Clear all selections
View Methods (Writable):
toggleView(): Toggle between front and back viewsetFrontView(): Set view to frontsetBackView(): Set view to back
Disabled Muscle Methods (Writable):
disableMuscle(Muscle): Disable a muscle (locks it, removes from selection)enableMuscle(Muscle): Enable a muscle (unlocks it)setDisabledMuscles(Set<Muscle>): Set multiple disabled muscles at once
Properties:
selectedMuscles(SetdisabledMuscles(SetisFront(bool, writable): Whether showing front view. Can be set directly or use view methods.isSelected(Muscle)(bool, read-only): Check if a muscle is selectedisDisabled(Muscle)(bool, read-only): Check if a muscle is disabled
Muscle
Enum representing all available muscles. See the "Available Muscles" section above for the complete list.
Common Pitfalls
❌ Don't: Try to modify selectedMuscles Set directly
// ❌ WRONG - This won't work because the Set is unmodifiable
controller.selectedMuscles.add(Muscle.bicepsLeft); // Error!
controller.selectedMuscles.clear(); // Error!
✅ Do: Use the setter or provided methods
// ✅ CORRECT - Use the setter (replaces entire selection)
controller.selectedMuscles = {Muscle.bicepsLeft, Muscle.tricepsRight};
// ✅ CORRECT - Or use the controller methods
controller.selectMuscle(Muscle.bicepsLeft);
controller.setSelectedMuscles({Muscle.bicepsLeft, Muscle.tricepsRight});
controller.clearSelection();
❌ Don't: Forget to listen to controller changes
// ❌ WRONG - UI won't update when selection changes
final controller = BodyMapController();
controller.selectMuscle(Muscle.bicepsLeft);
// Widget won't rebuild automatically
✅ Do: Use AnimatedBuilder or listen to changes
// ✅ CORRECT - Wrap with AnimatedBuilder
AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Text('Selected: ${controller.selectedMuscles.length}');
},
)
❌ Don't: Create controller in build method
// ❌ WRONG - Creates new controller on every rebuild
Widget build(BuildContext context) {
final controller = BodyMapController(); // Don't do this!
return InteractiveBodySvg(...);
}
✅ Do: Create controller in initState or use late initialization
// ✅ CORRECT - Create once in initState
class _MyWidgetState extends State<MyWidget> {
late BodyMapController controller;
@override
void initState() {
super.initState();
controller = BodyMapController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
❌ Don't: Forget to dispose the controller
// ❌ WRONG - Memory leak!
class _MyWidgetState extends State<MyWidget> {
final controller = BodyMapController();
// Missing dispose() call
}
✅ Do: Always dispose controllers
// ✅ CORRECT - Always dispose
@override
void dispose() {
controller.dispose();
super.dispose();
}
❌ Don't: Try to use custom SVG assets
// ❌ WRONG - Custom SVG files are not supported
InteractiveBodySvg(
asset: 'assets/my_custom_body.svg', // Won't work correctly!
)
✅ Do: Use the package's included assets
// ✅ CORRECT - Simplest way (automatically uses package assets)
InteractiveBodySvg(
isFront: true, // Automatically uses package's front body asset
)
// ✅ CORRECT - Or specify asset path explicitly
InteractiveBodySvg(
asset: 'packages/flutter_body_part_selector/assets/svg/body_front.svg',
)
// ✅ CORRECT - Or use InteractiveBodyWidget which uses defaults automatically
InteractiveBodyWidget(...)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License.
Support
If you encounter any issues or have questions, please file an issue on the GitHub repository.