Create a simple todo app with Flutter provider

Flutter June 21st 2022

    Learning state management with a real example to-do app is a great choice for it. I will show you how to create a very simple to-do app with Flutter provider. Here we gonna use the Provider package to manage the state of the app.

    Features:

    • Todo List

    todo list

    • Add Todo

    add todo

    • Edit Todo

    Edit todo

    • Delete Todo

    delete todo

    Steps create a simple todo app:

    Create a blank flutter project:

    flutter create flutter_todo_app_with_provider

    Add provider package: 

    flutter pub add provider

    Create todo model:

    • model/todo.dart
    Input:
    class TodoFiled {
    static const createdTime = 'createdTime';
    }

    class Todo {
    DateTime createdTime;
    String title;
    String id;
    String description;
    bool isDone;

    Todo({
    required this.title,
    required this.createdTime,
    this.description = '',
    required this.id,
    this.isDone = false,
    });
    }

    Create todo model provider:

    • provider/todos.dart
    Input:
    import 'package:flutter/cupertino.dart';
    import '../model/todo.dart';

    class TodosProvider extends ChangeNotifier{
    List<Todo> _todos = [
    Todo(
    id: "1",
    title: 'Buy Food 🍕',
    createdTime: DateTime.now(),
    description: '''
    - Eggs
    - Milk
    - Bread
    - Water
    '''
    ),
    Todo(
    id: "2",
    title: 'Plan family trip to Norway ✈️',
    createdTime: DateTime.now(),
    description: '''
    - Rent some hotels
    - Rent a car
    - Pack suitcase

    '''
    ),
    Todo(
    id: "3",
    title: 'Walk the Dog 🐕',
    createdTime: DateTime.now(),
    ),
    Todo(
    id: "4",
    title: 'plan Jacobs birthday party 🥳 🎉',
    createdTime: DateTime.now(),
    ),
    ];

    List<Todo> get todos=> _todos.where((todo) =>todo.isDone== false).toList();
    List<Todo> get todosCompleted=> _todos.where((todo) =>todo.isDone== true).toList();

    void addTodo(Todo todo){
    _todos.add(todo);
    notifyListeners();
    }
    void removeTodo(Todo todo){
    _todos.remove(todo);
    notifyListeners();
    }
    bool toggleTodoStatus(Todo todo){
    todo.isDone = !todo.isDone;
    notifyListeners();

    return todo.isDone;
    }
    void updateTodo(Todo todo, String title, String description){
    todo.title = title;
    todo.description= description;
    notifyListeners();
    }
    }

    Create widgets:

    • widgets/add_todo_dialog_widget.dart
    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../model/todo.dart';
    import '../provider/todos.dart';
    import 'todo_form_widget.dart';

    class AddTodoDialogWidget extends StatefulWidget {
    @override
    _AddTodoDialogWidgetState createState() => _AddTodoDialogWidgetState();
    }

    class _AddTodoDialogWidgetState extends State {
    final _formKey= GlobalKey();
    String title = '';
    String description = '';
    @override
    Widget build(BuildContext context) {
    return AlertDialog(
    content: Form(
    key: _formKey,
    child: Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Text(
    'add Todo',
    style: TextStyle(
    fontSize: 22,
    fontWeight: FontWeight.bold,
    ),
    ),
    SizedBox(
    height: 8,
    ),
    TodoFormWidget(
    onChangedTitle: (title) {
    setState(() {
    this.title = title;
    });
    },
    onChangedDescription: (description) {
    setState(() {
    this.description = description;
    });
    },
    onSavedTodo: addTodo,
    )
    ],
    ),
    ),
    );
    }

    void addTodo() {
    final isValid = _formKey.currentState!.validate();

    if(!isValid) {
    return;
    } else{
    final todo = Todo(
    id: DateTime.now().toString(),
    title: title,
    description: description,
    createdTime: DateTime.now(),
    );
    final provider = Provider.of(context,listen: false);
    provider.addTodo(todo);

    Navigator.of(context).pop();
    }
    }
    }
    • widgets/completed_list_widget.dart
    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../provider/todos.dart';
    import 'todo_widget.dart';

    class CompletedListWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    final provider = Provider.of(context);
    final todos = provider.todosCompleted;

    return todos.isEmpty
    ? Center(
    child: Text(
    'No completed tasks',
    style: TextStyle(fontSize: 20),
    ),
    )
    : ListView.separated(
    separatorBuilder: (context, index) => Container(
    height: 20,
    ),
    physics: BouncingScrollPhysics(),
    padding: EdgeInsets.all(16),
    itemCount: todos.length,
    itemBuilder: (context, index) {
    final todo = todos[index];

    return TodoWidget(
    todo: todo,
    );
    },
    );
    }
    }
    • widgets/todo_form_widget.dart
    Input:
    import 'package:flutter/material.dart';

    class TodoFormWidget extends StatelessWidget {
    final String? title;
    final String? description;
    final ValueChanged? onChangedTitle;
    final ValueChanged? onChangedDescription;
    final VoidCallback? onSavedTodo;

    const TodoFormWidget(
    {Key? key,
    this.title,
    this.description,
    this.onChangedTitle,
    this.onChangedDescription,
    this.onSavedTodo})
    : super(key: key);

    @override
    Widget build(BuildContext context) {
    return Container(
    child: SingleChildScrollView(
    child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
    buildTitle(),
    SizedBox(height: 8,),
    buildDescription(),
    SizedBox(height: 32),
    buildButton(),
    ],
    ),
    ),
    );
    }

    Widget buildTitle() => TextFormField(
    initialValue: title,
    onChanged: onChangedTitle,
    validator: (title) {
    if (title!.isEmpty) {
    return 'the title can not be empty';
    }
    return null;
    },
    decoration: InputDecoration(
    border: UnderlineInputBorder(),
    labelText: 'Title',
    ),
    );
    Widget buildDescription() => TextFormField(
    maxLines: 3,
    initialValue: description,
    onChanged: onChangedDescription,
    decoration: InputDecoration(
    border: UnderlineInputBorder(),
    labelText: 'Description'
    ),
    );
    Widget buildButton()=> SizedBox(
    width: double.infinity,
    child: ElevatedButton(
    style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Colors.blue),
    ),
    onPressed: onSavedTodo,
    child: Text('Save'),
    ),
    );
    }
    • widgets/todo_list_widget.dart
    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../provider/todos.dart';
    import 'todo_widget.dart';

    class TodoListWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    final provider = Provider.of(context);
    final todos = provider.todos;

    return todos.isEmpty
    ? Center(
    child: Text(
    'No todos',
    style: TextStyle(fontSize: 20),
    ),
    )
    : ListView.separated(
    separatorBuilder: (context, index) => Container(
    height: 20,
    ),
    physics: BouncingScrollPhysics(),
    padding: EdgeInsets.all(16),
    itemCount: todos.length,
    itemBuilder: (context, index) {
    final todo = todos[index];

    return TodoWidget(
    todo: todo,
    );
    },
    );
    }
    }
    • widgets/todo_widget.dart
    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../page/edit_todo_page.dart';
    import '../model/todo.dart';
    import '../provider/todos.dart';
    import '../util/utils.dart';

    class TodoWidget extends StatelessWidget {
    final Todo todo;

    const TodoWidget({Key? key, required this.todo}) : super(key: key);
    @override
    Widget build(BuildContext context) {
    return ClipRRect(
    borderRadius: BorderRadius.circular(16),
    child: buildTodo(context),
    );
    }

    Widget buildTodo(BuildContext context)=> GestureDetector(
    onTap: ()=> editTodo(context, todo),

    child: Container(
    color: Colors.white,
    padding: EdgeInsets.all(20),
    child: Row(
    children: [
    Checkbox(
    activeColor: Colors.blue,
    checkColor: Colors.white,
    value: todo.isDone,
    onChanged: (_){
    final provider = Provider.of(context, listen: false);
    final isDone= provider.toggleTodoStatus(todo);
    Utils.showSnackBar(context,
    isDone ? 'Task completed' :
    'Task marked incomplete'

    );

    },
    ),
    SizedBox(width: 20,),
    Expanded(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Text(
    todo.title,
    style: TextStyle(
    fontWeight: FontWeight.bold,
    color: Colors.blue,
    fontSize: 22,
    ),
    ),
    if(todo.description.isNotEmpty)
    Container(
    margin: EdgeInsets.only(top: 4),
    child: Text(
    todo.description,
    style: TextStyle(fontSize: 20, height: 1.5),
    ),
    ),
    ],
    )
    ),
    IconButton(onPressed: (){
    editTodo(context,todo);
    }, icon: Icon(Icons.edit),color: Colors.green,),
    IconButton(onPressed: (){
    showAlertDialog(context);
    }, icon: Icon(Icons.delete),color: Colors.red,)
    ],
    ),
    ),
    );

    void deleteTodo(BuildContext context, Todo todo) {
    final provider = Provider.of(context, listen: false);
    provider.removeTodo(todo);

    Utils.showSnackBar(context, 'Deleted the task');
    }

    void editTodo(BuildContext context, Todo todo) {
    Navigator.of(context).push(MaterialPageRoute(builder: (context)=>EditTodoPage(todo: todo))) ;
    }
    showAlertDialog(BuildContext context) {

    // set up the button
    Widget okButton = TextButton(
    child: Text("OK"),
    onPressed: () {
    deleteTodo(context, todo);
    Navigator.of(context).pop();
    },
    );
    // set up the buttons
    Widget cancelButton = TextButton(
    child: Text("Cancel"),
    onPressed: () {
    Navigator.of(context).pop();
    },
    );

    // set up the AlertDialog
    AlertDialog alert = AlertDialog(
    title: Text("Delete Todo"),
    content: Text("Are you sure."),
    actions: [
    okButton,
    cancelButton
    ],
    );

    // show the dialog
    showDialog(
    context: context,
    builder: (BuildContext context) {
    return alert;
    },
    );
    }
    }
    void doNothing(BuildContext context) {}

    Create util:

    • util/utils.dart
    Input:
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';

    class Utils{
    static void showSnackBar(BuildContext context, String text)=>
    Scaffold.of(context)
    ..removeCurrentSnackBar()
    ..showSnackBar(SnackBar(content: Text(text)));
    }

    Create page:

    • page/edit_todo_page.dart
    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import '../model/todo.dart';
    import '../provider/todos.dart';
    import '../widgets/todo_form_widget.dart';

    class EditTodoPage extends StatefulWidget {
    final Todo todo;

    const EditTodoPage({Key? key, required this.todo}) : super(key: key);
    @override
    _EditTodoPageState createState() => _EditTodoPageState();
    }

    class _EditTodoPageState extends State {
    final _formKey = GlobalKey();
    late String title;
    late String description;

    @override
    void initState() {
    // TODO: implement initState
    super.initState();
    title= widget.todo.title;
    description= widget.todo.description;
    }
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Edit Todo'),
    actions: [
    IconButton(
    icon: Icon(Icons.delete),
    onPressed: (){
    final provider = Provider.of(context, listen: false);
    provider.removeTodo(widget.todo);
    Navigator.of(context).pop();
    }
    )
    ],
    ),
    body: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Form(
    key: _formKey,
    child: TodoFormWidget(
    title: title,
    description: description,
    onChangedTitle: (title){
    setState(() {
    this.title= title;
    });
    },
    onChangedDescription: (description){
    setState(() {
    this.description= description;
    });
    },
    onSavedTodo: saveTodo,
    ),
    ),
    ),
    );
    }

    void saveTodo() {
    final isValid= _formKey.currentState!.validate();
    if(!isValid){
    return;
    } else{
    final provider = Provider.of(context, listen: false);
    provider.updateTodo(widget.todo, title, description);
    Navigator.of(context).pop();
    }
    }
    }

    Complete your main.dart file:

    Input:
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    import 'provider/todos.dart';
    import 'widgets/add_todo_dialog_widget.dart';
    import 'widgets/completed_list_widget.dart';
    import 'widgets/todo_list_widget.dart';

    void main() {
    runApp(ChangeNotifierProvider(
    create: (context)=> TodosProvider(),
    child: MaterialApp
    (
    debugShowCheckedModeBanner: false,
    home : MyApp())));
    }

    class MyApp extends StatefulWidget {
    @override
    _MyAppState createState() => _MyAppState();
    }

    class _MyAppState extends State {
    int selectedIndex = 0;
    @override
    Widget build(BuildContext context) {
    final tabs = [
    TodoListWidget(),
    CompletedListWidget(),
    ];

    return Scaffold(
    appBar: AppBar(
    backgroundColor: Colors.blue,
    title: Text("Todo App"),
    ),
    bottomNavigationBar: BottomNavigationBar(
    backgroundColor: Colors.blue,
    unselectedItemColor: Colors.white.withOpacity(0.7),
    selectedItemColor: Colors.white,
    currentIndex: selectedIndex,
    onTap: (index) {
    setState(() {
    selectedIndex = index;
    });
    },
    items: [
    BottomNavigationBarItem(
    icon: Icon(Icons.fact_check_outlined),
    label: 'Todos',
    ),
    BottomNavigationBarItem(
    icon: Icon(
    Icons.done,
    size: 28,
    ),
    label: 'Completed',
    ),
    ],
    ),
    body: tabs[selectedIndex],
    floatingActionButton: FloatingActionButton(
    shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(20),
    ),
    backgroundColor: Colors.blue,
    onPressed: () => showDialog(
    builder: (context) => AddTodoDialogWidget(), context: context,
    barrierDismissible: true,
    ),
    child: Icon(Icons.add),
    ),
    );
    }
    }

    You can find my GitHub code here:

    https://github.com/rogerphan0623/flutter_todo_app_with_provider