multiline_prefix_text_field 0.0.4
multiline_prefix_text_field: ^0.0.4 copied to clipboard
A Flutter widget that provides a multiline text field with automatic numbered prefixes (1., 2., 3.), ideal for lists, todos, and structured text input.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:multiline_prefix_text_field/multiline_prefix_text_field.dart';
import 'widgets/unfocus_on_tap_outside_field_handler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Multiline Prefix TextField Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.light,
),
textTheme: GoogleFonts.plusJakartaSansTextTheme(),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF818CF8),
brightness: Brightness.dark,
),
textTheme: GoogleFonts.plusJakartaSansTextTheme(
ThemeData.dark().textTheme,
),
useMaterial3: true,
),
home: UnfocusOnTapOutsideFieldHandler(
child: const DemoPage(),
),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
List<String> _todoItems = [
'Buy groceries for the week',
'Complete Flutter project documentation',
'Schedule dentist appointment',
];
List<String> _notes = [
'Remember to call Mom',
'Review pull request #42',
];
bool _isReadOnly = false;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
const Color(0xFF1E1B4B),
const Color(0xFF312E81),
const Color(0xFF1E1B4B),
]
: [
const Color(0xFFF5F3FF),
const Color(0xFFEDE9FE),
const Color(0xFFF5F3FF),
],
),
),
child: SafeArea(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.format_list_numbered_rounded,
color: colorScheme.onPrimaryContainer,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Multiline Prefix',
style: GoogleFonts.spaceGrotesk(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
Text(
'TextField Demo',
style: GoogleFonts.spaceGrotesk(
fontSize: 28,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
),
),
],
),
const SizedBox(height: 8),
Text(
'A numbered list text input with automatic line management',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
const SizedBox(height: 32),
// Read-only toggle
_buildCard(
colorScheme: colorScheme,
child: Row(
children: [
Icon(
_isReadOnly ? Icons.lock_rounded : Icons.edit_rounded,
color: colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Read-only Mode',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
),
Switch.adaptive(
value: _isReadOnly,
onChanged: (value) {
setState(() => _isReadOnly = value);
},
),
],
),
),
const SizedBox(height: 24),
// Todo List Section
_buildSectionHeader(
icon: Icons.check_circle_outline_rounded,
title: 'Todo List',
subtitle: 'Press Enter to add new items',
colorScheme: colorScheme,
),
const SizedBox(height: 12),
_buildCard(
colorScheme: colorScheme,
child: MultilinePrefixTextField(
descriptionList: _todoItems,
onFinishEditing: (items) {
setState(() => _todoItems = items);
},
isReadOnly: _isReadOnly,
),
),
const SizedBox(height: 24),
// Notes Section
_buildSectionHeader(
icon: Icons.note_alt_outlined,
title: 'Quick Notes',
subtitle: 'Backspace at start to merge lines',
colorScheme: colorScheme,
),
const SizedBox(height: 12),
_buildCard(
colorScheme: colorScheme,
child: MultilinePrefixTextField(
descriptionList: _notes,
onFinishEditing: (items) {
setState(() => _notes = items);
},
isReadOnly: _isReadOnly,
),
),
const SizedBox(height: 24),
// Output Preview Section
_buildSectionHeader(
icon: Icons.data_object_rounded,
title: 'Current Data',
subtitle: 'Real-time list output',
colorScheme: colorScheme,
),
const SizedBox(height: 12),
_buildCard(
colorScheme: colorScheme,
backgroundColor: isDark
? const Color(0xFF1E1E2E)
: const Color(0xFFF8FAFC),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Todo Items:',
style: GoogleFonts.firaCode(
fontSize: 12,
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_todoItems.toString(),
style: GoogleFonts.firaCode(
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.8),
),
),
const SizedBox(height: 16),
Text(
'Notes:',
style: GoogleFonts.firaCode(
fontSize: 12,
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
_notes.toString(),
style: GoogleFonts.firaCode(
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.8),
),
),
],
),
),
const SizedBox(height: 32),
// Instructions
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.primary.withValues(alpha: 0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline_rounded,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
'How to use',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
),
const SizedBox(height: 12),
_buildInstruction(
'↵',
'Press Enter to create a new line',
colorScheme,
),
_buildInstruction(
'⌫',
'Backspace at line start to merge with previous',
colorScheme,
),
_buildInstruction(
'🔢',
'Line numbers update automatically',
colorScheme,
),
],
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildSectionHeader({
required IconData icon,
required String title,
required String subtitle,
required ColorScheme colorScheme,
}) {
return Row(
children: [
Icon(icon, color: colorScheme.primary, size: 20),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(width: 8),
Text(
'• $subtitle',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
],
);
}
Widget _buildCard({
required ColorScheme colorScheme,
required Widget child,
Color? backgroundColor,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor ?? colorScheme.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: child,
);
}
Widget _buildInstruction(
String symbol,
String text,
ColorScheme colorScheme,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(6),
),
child: Text(
symbol,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurface.withValues(alpha: 0.8),
),
),
),
],
),
);
}
}