A Flutter packages for creating dynamic, multi-column tables with features like dropdowns, validation, and read-only cells. Ideal for interactive table inputs.
Input Data Mode Potrait | Get Data Mode Potrait |
---|---|
![]() |
![]() |
Input Data Mode Landscape | Get Data Mode Landscape |
---|---|
![]() |
![]() |
- ✨ Fully customizable table layout and styling
- 📝 Support for both text input and dropdown cells
- đź”’ Configurable read-only cells
- âś… Built-in validation support
- 📱 Responsive design for both portrait and landscape orientations
- 🎨 Customizable styling for headers, cells, and error messages
- 🔄 Real-time data updates with onChange callbacks
- đź’ľ Easy data management with dedicated controller
- Flutter Android
- Flutter iOS
- Flutter web
- Flutter desktop
- Flutter macOS
- Flutter Linux
- Add
flutter_multi_table.dart: <latest-version>
to yourpubspec.yaml
dependencies. And import it: Get the latest version in the 'Installing' tab on pub.dev
dependencies:
flutter_multi_table: <latest-version>
flutter pub add flutter_multi_table
- Run pub get.
flutter pub get
- Import package.
import 'package:flutter_multi_table/flutter_multi_table.dart';
// 1. Define your headers
List headers = [
'No',
'Name',
'Age',
'City',
'Status',
];
// 2. Create initial data
List<Map> initialData = [
{
'No': '1',
'Name': 'John',
'Age': '',
'City': 'New York',
'Status': 'Active',
},
{
'No': '2',
'Name': 'Alice',
'Age': '',
'City': 'London',
'Status': 'Inactive',
},
];
// 3. Initialize controller
FlutterMultiTableController controller = FlutterMultiTableController(
initialData: initialData,
headers: headers,
);
// 4. Configure table
FlutterMultiTableConfig config = FlutterMultiTableConfig(
headers: headers,
hint: '....',
);
// 5. Use the widget
FlutterMultiTable(
controller: controller,
config: config,
)
FlutterMultiTableConfig config = FlutterMultiTableConfig(
headers: headers,
hint: '....',
// Define which cells are read-only
isReadOnly: (row, column) {
if (column == 0 || column == 1 || column == 3 || column == 4) {
return true;
}
return false;
},
// Configure dropdown cells
isDropdown: (row, column) {
if (column == 2 && (row != 1 && row != 13)) {
return true;
}
return false;
},
dropdownOptions: ['Jakarta', 'New York', 'London', 'Tokyo', 'Paris'],
// Handle value changes
onChanged: (row, column, value) {
print('Cell ($row, $column) changed to: $value');
},
// Validation
validator: (row, column, value) {
if (column == 2 && value.isEmpty) {
return 'Data wajib diisi';
}
return null;
},
// Dropdown validation
dropdownValidator: (row, column, value) {
if (column == 2 && (value == null || value.isEmpty)) {
return 'Data wajib diisi';
}
return null;
},
// Styling
headerTextStyle: TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w700,
),
errorTextStyle: TextStyle(fontSize: 8, color: Colors.red),
cellTextStyle: TextStyle(
fontSize: 6,
fontWeight: FontWeight.w600,
),
cellWidth: MediaQuery.of(context).size.width * 0.182,
cellHeight: MediaQuery.of(context).size.height * 0.04,
hintTextStyle: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w400,
),
);
Main controller class for managing table data and state:
FlutterMultiTableController({
required List<Map> initialData,
required List headers,
})
updateCell(int row, int column, String value)
: Updates specific cell valuegetCellValue(int row, int column)
: Gets value from specific cellsetDropdownError(int row, int column, String? error)
: Sets dropdown error messagegetDropdownError(int row, int column)
: Gets dropdown error messagedispose()
: Cleans up resources
Configuration class for customizing table appearance and behavior:
FlutterMultiTableConfig({
required List headers,
String hint = "",
bool Function(int row, int column)? isReadOnly,
bool Function(int row, int column)? isDropdown,
List? dropdownOptions,
void Function(int row, int column, String value)? onChanged,
String? Function(int row, int column, String value)? validator,
String? Function(int row, int column, String? value)? dropdownValidator,
TextStyle? headerTextStyle,
Color? headerBoxColor,
TextStyle? cellTextStyle,
TextStyle? hintTextStyle,
TextStyle? errorTextStyle,
TextInputType? textInputType,
double? cellHeight,
double? cellWidth,
})
import 'package:flutter/material.dart';
import 'package:flutter_multi_table/flutter_multi_table.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter Multi Table Example')),
body: const SingleChildScrollView(child: MyHomePage()),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
late FlutterMultiTableController _controllerCreate;
late FlutterMultiTableController _controllerSaved;
late FlutterMultiTableConfig _create;
late FlutterMultiTableConfig _saved;
List<Map<String, String>> savedData = [];
List<String> headers = [
'No',
'Name',
'Age',
'City',
'Status',
];
@override
void initState() {
super.initState();
List<Map<String, String>> initialData = [
{
'No': '1',
'Name': 'John',
'Age': '',
'City': 'New York',
'Status': 'Active',
},
{
'No': '2',
'Name': 'Alice',
'Age': '',
'City': 'London',
'Status': 'Inactive',
},
];
_controllerCreate = FlutterMultiTableController(
initialData: initialData,
headers: headers,
);
_controllerSaved = FlutterMultiTableController(
initialData: const [],
headers: headers,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_create = FlutterMultiTableConfig(
headers: headers,
hint: '....',
validator: (row, column, value) {
if (column == 2 && (value.isEmpty)) {
return 'Data wajib diisi';
}
return null;
},
dropdownValidator: (row, column, value) {
if (column == 2 && (value == null || value.isEmpty)) {
return 'Data wajib diisi';
}
return null;
},
headerTextStyle: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w700,
),
errorTextStyle: const TextStyle(fontSize: 8, color: Colors.red),
cellTextStyle: const TextStyle(
fontSize: 6,
fontWeight: FontWeight.w600,
),
cellWidth: MediaQuery.of(context).size.width * 0.182,
cellHeight: MediaQuery.of(context).size.height * 0.04,
hintTextStyle: const TextStyle(
fontSize: 8,
fontWeight: FontWeight.w400,
),
isReadOnly: (row, column) {
if (column == 0 || column == 1 || column == 3 || column == 4) {
return true;
}
return false;
},
isDropdown: (row, column) {
if (column == 2 && (row != 1 && row != 13)) {
return true;
}
return false;
},
dropdownOptions: ['Jakarta', 'New York', 'London', 'Tokyo', 'Paris'],
onChanged: (row, column, value) {
debugPrint('Changed: Row $row, Column $column, Value: $value');
},
);
// Config untuk tabel saved data (semua cell readonly)
_saved = FlutterMultiTableConfig(
headers: headers,
headerTextStyle: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w700,
),
cellTextStyle: const TextStyle(
fontSize: 6,
fontWeight: FontWeight.w600,
),
cellWidth: MediaQuery.of(context).size.width * 0.182,
cellHeight: MediaQuery.of(context).size.height * 0.04,
isReadOnly: (row, column) => true, // Semua cell readonly
);
}
void _saveData() {
// Get current table data from the controller
final currentData = _controllerCreate.tableData.map((row) {
// Convert TextEditingController values to String
return row.map((key, value) {
return MapEntry(key, value.text); // Extract text value
// return MapEntry(key, value.toString());
});
}).toList();
// Validate data before saving
bool isValid = true;
for (var row in currentData) {
if (row['Age'] == null || row['Age']!.isEmpty) {
isValid = false;
break;
}
}
if (!isValid) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fill all required fields'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() {
savedData.addAll(currentData);
// Update controller untuk saved table
_controllerSaved = FlutterMultiTableController(
initialData: savedData,
headers: headers,
);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data saved successfully'),
backgroundColor: Colors.green,
),
);
}
@override
void dispose() {
_controllerCreate.dispose();
_controllerSaved.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Input Data',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
FlutterMultiTable(
controller: _controllerCreate,
config: _create,
),
const SizedBox(height: 20),
Center(
child: ElevatedButton(
onPressed: _saveData,
child: const Text('Submit'),
),
),
if (savedData.isNotEmpty) ...[
const SizedBox(height: 30),
const Text(
'Get Data',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
FlutterMultiTable(
controller: _controllerSaved,
config: _saved,
),
],
],
),
);
}
}
MIT
Pull requests are welcome.