import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class StringMapFormField extends StatefulWidget { const StringMapFormField( {super.key, this.initialValue, this.onChanged, this.keyDecoration, this.valueDecoration, this.padding, this.keyPadding, this.valuePadding, this.keyValidator, this.valueValidator, this.keyAutovalidateMode, this.valueAutovalidateMode, this.label, this.helper, this.constraints}); final Map? initialValue; final ValueChanged>? onChanged; final InputDecoration? keyDecoration; final InputDecoration? valueDecoration; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? keyPadding; final EdgeInsetsGeometry? valuePadding; final FormFieldValidator? keyValidator; final FormFieldValidator? valueValidator; final AutovalidateMode? keyAutovalidateMode; final AutovalidateMode? valueAutovalidateMode; final Widget? label; final Widget? helper; final BoxConstraints? constraints; @override State createState() => _StringMapFormField(); } class _StringMapFormField extends State { late Map value; late String parentKey; late Key lastKey; late List keys; late int rebuildKeys; late List keyList; @override void initState() { super.initState(); value = widget.initialValue ?? {}; parentKey = widget.key?.toString() ?? ""; lastKey = ValueKey("${parentKey}_new"); final len = value.length; keys = List.generate(len, (index) => ValueKey("${parentKey}_$index")); keyList = value.keys.toList(); rebuildKeys = 0; } void onReorder(int oldIndex, int newIndex) { setState(() { if (oldIndex < newIndex) { newIndex -= 1; } final String item = keyList.removeAt(oldIndex); keyList.insert(newIndex, item); final key = keys.removeAt(oldIndex); keys.insert(newIndex, key); }); widget.onChanged?.call(value); } Widget _buildKeyItem(BuildContext context, int index, bool expanded) { final i18n = AppLocalizations.of(context)!; Widget item = TextFormField( initialValue: keyList[index], decoration: widget.keyDecoration, onChanged: (String? value) { if (widget.keyValidator != null) { final re = widget.keyValidator?.call(value); if (re != null) { return; } } final v = value ?? ""; if (v.isEmpty || this.value.containsKey(v)) { return; } final old = keyList[index]; keyList[index] = v; final va = this.value.remove(old); this.value[v] = va ?? ""; widget.onChanged?.call(this.value); }, validator: (s) { final re = widget.keyValidator?.call(s); if (re != null) { return re; } final v = s ?? ""; if (v.isEmpty) { return i18n.keyIsEmpty; } if (value.containsKey(v)) { return i18n.keyIsExists; } return null; }, autovalidateMode: widget.keyAutovalidateMode, ); if (expanded) { if (widget.keyPadding != null) { item = Padding( padding: widget.keyPadding!, child: item, ); } item = Expanded(child: item); } else if (widget.padding != null) { item = Padding( padding: widget.padding!, child: item, ); } return item; } Widget _buildValueItem(BuildContext context, int index, bool expanded) { Widget item = TextFormField( initialValue: value[keyList[index]], decoration: widget.keyDecoration, onChanged: (String? value) { if (widget.valueValidator != null) { final re = widget.valueValidator?.call(value); if (re != null) { return; } } this.value[keyList[index]] = value ?? ""; widget.onChanged?.call(this.value); }, validator: widget.valueValidator, autovalidateMode: widget.valueAutovalidateMode, ); if (expanded) { if (widget.valuePadding != null) { item = Padding( padding: widget.valuePadding!, child: item, ); } item = Expanded(child: item); } else if (widget.padding != null) { item = Padding( padding: widget.padding!, child: item, ); } return item; } Widget _buildItem(BuildContext context, int index) { final useMobile = MediaQuery.of(context).size.width <= 810; Widget item = Row( key: keys[index], children: [ useMobile ? Expanded( child: Column(mainAxisSize: MainAxisSize.min, children: [ _buildKeyItem(context, index, false), _buildValueItem(context, index, false), ])) : _buildKeyItem(context, index, true), useMobile ? Container() : _buildValueItem(context, index, true), IconButton( icon: const Icon(Icons.delete), onPressed: () { setState(() { final key = keyList.removeAt(index); value.remove(key); }); widget.onChanged?.call(value); }, ), ReorderableDragStartListener( index: index, child: const Icon(Icons.reorder)), ], ); if (!useMobile && widget.padding != null) { item = Padding( key: item.key, padding: widget.padding!, child: item, ); } return item; } Widget _buildList(BuildContext context) { Widget list = ReorderableList( itemBuilder: _buildItem, itemCount: value.length, onReorder: onReorder, proxyDecorator: proxyDecorator, shrinkWrap: true); if (widget.constraints != null) { list = ConstrainedBox( constraints: widget.constraints!, child: list, ); } return list; } Widget proxyDecorator(Widget child, int index, Animation animation) { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { final double animValue = Curves.easeInOut.transform(animation.value); final double elevation = lerpDouble(0, 6, animValue)!; return Material( elevation: elevation, child: child, ); }, child: child, ); } Widget _buildLabel(BuildContext context) { if (widget.label == null) { return Container(); } if (widget.padding == null) { return widget.label!; } return Padding( padding: widget.padding!, child: widget.label!, ); } Widget _buildHelper(BuildContext context) { if (widget.helper == null) { return Container(); } if (widget.padding == null) { return widget.helper!; } return Padding( padding: widget.padding!, child: widget.helper!, ); } Widget _buildAddButton(BuildContext context) { Widget button = LayoutBuilder(builder: (context, box) { return SizedBox( width: box.maxWidth, child: IconButton( key: lastKey, onPressed: value.containsKey("") ? null : () { if (value.containsKey("")) { return; } setState(() { value[""] = ""; keyList.add(""); keys.add(ValueKey("${parentKey}_${value.length}")); }); widget.onChanged?.call(value); }, icon: const Icon(Icons.add))); }); if (widget.padding != null) { button = Padding( padding: widget.padding!, child: button, ); } return button; } @override Widget build(BuildContext context) { if (value.length != keys.length) { final len = value.length; keys = List.generate( len, (index) => ValueKey("${parentKey}_${rebuildKeys}_$index")); keyList = value.keys.toList(); rebuildKeys++; } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLabel(context), _buildList(context), _buildAddButton(context), _buildHelper(context), ]); } }