Define Task class
When we implement a simple todo app, we can define a task class first:
classTask{finalString name;bool isDone; Task({requiredthis.name, this.isDone = false});void toggleDone() { isDone = !isDone; }}We can then implement task tile and task list widget to display and manage tasks.
Task Tile
We can use Flutter Widget Previewer to render the widget in real-time, separate from our full app.
classTaskTileextendsStatelessWidget{ final bool isChecked; final String taskTitle; final ValueChanged<bool?> checkboxCallback; TaskTile( {requiredthis.isChecked,requiredthis.taskTitle,requiredthis.checkboxCallback}); @override Widget build(BuildContext context) {return ListTile( title: Text( taskTitle, style: TextStyle( decoration: isChecked ? TextDecoration.lineThrough : null, ), ), trailing: Checkbox( activeColor: Colors.lightBlueAccent, value: isChecked, onChanged: checkboxCallback, ), ); }}Android Studio, Intellij, and Visual Studio Code automatically start the Flutter Widget Previewer on launch.
We can also start the Flutter Widget Previewer from the terminal, we navigate to the root directory of the app.
$ flutter widget-preview startThis command will launch a local server and open a Widget Preview environment in the web browser automatically updates based on changes to our project.
We must use the @Preview annotation defined in package:flutter/widget-preview.dart.
• Top-level functions that return a Widget or WidgetBuilder. • Static methods within a class that return a WidgetorWidgetBuilder.• Public Widget constructors and factories with no required arguments.
Here is a basic example of how to use the @Preview annotation to preview a Text widget:
import'package:flutter/widget_previews.dart';import'package:flutter/material.dart'; // For Material widgets@Preview(name: 'My Sample Text')Widget mySampleText() {returnconst Text('Hello, World!');}For the case where global state has been modified (for example, a static initializer has been changed), the entire widget previewer can be told to hot restart using the button at the bottom right of the environment.
The @Preview annotation has several parameters we can use to customize the preview:
• name: A descriptive name for the preview.• group: A name used to group related previews together in the widget previewer.• size: Artificial size constraints using a Size object.• textScaleFactor: A custom font scale.• wrapper: A function that wraps your previewed widget in a specific widget tree (for example, to inject application state into the widget tree with an InheritedWidget).• theme: A function to provide Material and Cupertino theming data.• brightness: The initial theme brightness.• localizations: A function to apply a localization configuration.
classTaskTileextendsStatelessWidget{ final bool isChecked; final String taskTitle; final ValueChanged<bool?> checkboxCallback; TaskTile( {requiredthis.isChecked,requiredthis.taskTitle,requiredthis.checkboxCallback}); @Preview(name: 'Task Tile') static Widget preview() => TaskTile( isChecked: false, taskTitle: 'Sample Task', checkboxCallback: (_) {}, ); @override Widget build(BuildContext context) {//...}Task List
Here we define the TasksList to display task list.
classTasksListextendsStatefulWidget{ final List<Task> currentTasks; TasksList(this.currentTasks); @override State<TasksList> createState() => _TasksListState();}class_TasksListStateextendsState<TasksList> { @override Widget build(BuildContext context) {return ListView.builder( itemCount: widget.currentTasks.length, itemBuilder: (context, index) {return TaskTile( taskTitle: widget.currentTasks[index].name, isChecked: widget.currentTasks[index].isDone, checkboxCallback: (bool? checkboxState) { setState(() { widget.currentTasks[index].toggleDone(); }); }, ); }, padding: EdgeInsets.only( transform: translateY( 20.0, left: 20.0, right: 20.0, bottom: 0, ), ); }}Add Task
We use a TextEditingController to store user input, whenever the user modifies a text field with an associated TextEditingController, the text field updates value and the controller notifies its listeners. Listeners can then read the text and selection properties to learn what the user has typed or how the selection has been updated.
Similarly, if we modify the text or selection properties, the text field will be notified and will update itself appropriately.
A TextEditingController can also be used to provide an initial value for a text field. If we build a text field with a controller that already has text, the text field will use that text as its initial value.
The value (as well as text and selection) of this controller can be updated from within a listener added to this controller. Be aware of infinite loops since the listener will also be notified of the changes made from within itself. Modifying the composing region from within a listener can also have a bad interaction with some input methods. Gboard, for example, will try to restore the composing region of the text if it was modified programmatically, creating an infinite loop of communications between the framework and the input method. Consider using TextInputFormatters instead for as-you-type text modification.
If both the text and selection properties need to be changed, set the controller's value instead. Setting text will clear the selection and composing range.
classAddTaskScreenextendsStatelessWidget{ final Function addNewTask; final TextEditingController _controller; const AddTaskScreen({super.key,required this.addNewTask,required TextEditingController controller, }) : _controller = controller; @override Widget build(BuildContext context) {return Container( color: Color(0xff757575), child: Container( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0), ), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: <Widget>[ Text('Add Task', textAlign: TextAlign.center, style: TextStyle( fontSize: 30.0, color: Colors.lightBlueAccent, ), ), SizedBox( height: 15.0, ), TextField( autofocus: true, textAlign: TextAlign.center, controller: _controller, ), SizedBox( height: 15.0, ), ElevatedButton( onPressed: () { addNewTask(_controller.text); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.lightBlueAccent, padding: EdgeInsets.all(15.0), ), child: Text('Add', style: TextStyle(color: Colors.white), )), ], ), ), ), ); }}Tasks Screen
classTaskScreenextendsStatefulWidget{ const TaskScreen({super.key}); @override State<TaskScreen> createState() => _TaskScreenState();}class_TaskScreenStateextendsState<TaskScreen> { List<Task> tasks = [ Task(name: 'Buy milk'), Task(name: 'Buy eggs'), Task(name: 'Buy bread'), ]; void addTask(String name) { setState(() { tasks.add( Task(name: name, isDone: false), ); }); } @override Widget build(BuildContext context) {return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.lightBlueAccent, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( padding: EdgeInsets.only( transform: translateY( 60.0, left: 30.0, right: 30.0, bottom: 30.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ CircleAvatar( backgroundColor: Colors.white, radius: 30.0, child: Icon( Icons.list, size: 30.0, color: Colors.lightBlueAccent, ), ), SizedBox( height: 10.0, ), Text('Todoey', style: TextStyle( color: Colors.white, fontSize: 50.0, fontWeight: FontWeight.w700, ), ), Text('${tasks.length} Tasks', style: TextStyle( color: Colors.white, fontSize: 18.0, ), ), ], ), ), Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0), ), ), child: TasksList(tasks), ), ), ], ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.lightBlueAccent, child: Icon(Icons.add), onPressed: () { showModalBottomSheet( isScrollControlled: true, context: context, builder: (context) => AddTaskScreen( addNewTask: addTask, controller: TextEditingController(), ), ); }, ), ); }}This TasksScreen will be the home of main app, it list the tasks and we can add new task from the AddTaskScreen in a modal from bottom sheet.
void main() => runApp(MyApp());classMyAppextendsStatelessWidget{const MyApp({super.key});@override Widget build(BuildContext context) {return MaterialApp( home: TaskScreen(), ); }}
夜雨聆风