Building a To-Do App with Flutter: A Step-by-Step Guide by Harshil Chovatiya

Building a To-Do App with Flutter: A Step-by-Step Guide by Harshil Chovatiya

Building a To-Do App with Flutter: A Step-by-Step Guide by Harshil Chovatiya

Introduction

Welcome to "Building a To-Do App with Flutter: A Step-by-Step Guide" by Harshil Chovatiya! If you're passionate about mobile app development and have a soft spot for Flutter, you're in the right place. In this tutorial, we'll embark on a journey to create a simple yet functional to-do list app using the Flutter framework.

Building a To-Do App with Flutter: A Step-by-Step Guide by Harshil Chovatiya

Setting Up the Flutter Project

To create a new Flutter project, follow these steps:

flutter create todo_app

Once the project is created, navigate to the project directory:

cd todo_app

Project Setup

To create a lifes, follow these flow:

            todo_app/
            
            ├── lib/
            │   ├── data/
            │   │   └── todo_repository.dart     
            │   │
            │   ├── models/
            │   │   └── todo.dart                
            │   │
            │   ├── providers/
            │   │   ├── todo_provider.dart      
            │   │   └── ...
            │   │
            │   ├── screens/
            │   │   ├── add_todo_screen.dart     
            │   │   ├── edit_todo_screen.dart    
            │   │   ├── todo_list_screen.dart    
            │   │   └── ...
            │   │
            │   ├── main.dart                   
            │   ├── ...
            └── ...
            

Stay tuned for the next post where we'll explore the data repository, todo_repository.dart, responsible for managing To-Do items in our app.

Managing To-Do Data with todo_repository.dart

The todo_repository.dart file plays a crucial role in handling To-Do data within our app. It encapsulates methods for performing operations such as reading, adding, updating, and deleting To-Do items. Let's take a closer look at its structure:

                

                
// lib/data/todo_repository.dart

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/todo.dart';

class TodoRepository {
  static const String key = 'todos';

  Future _prefs = SharedPreferences.getInstance();

  Future> getTodos() async {
    final SharedPreferences prefs = await _prefs;
    final todosJson = prefs.getString(key);
    if (todosJson != null) {
      final List parsedJson = json.decode(todosJson);
      return parsedJson.map((todo) => Todo.fromJson(todo)).toList();
    } else {
      return [];
    }
  }

  Future saveTodos(List todos) async {
    final SharedPreferences prefs = await _prefs;
    final todosJson = json.encode(todos);
    await prefs.setString(key, todosJson);
  }
}

Key Features:

Reading To-Do Items (getTodos): The getTodos method retrieves To-Do items stored in shared preferences. It parses the JSON data and returns a list of Todo objects.

Saving To-Do Items (saveTodos): The saveTodos method allows us to persist To-Do items. It converts the list of Todo objects into JSON format and stores it in shared preferences.

How to Use todo_repository.dart

To interact with the TodoRepository class, you can instantiate it and call its methods from other parts of your app. For example, in your TodoProvider class (as seen in our previous blog post), you can use the getTodos and saveTodos methods to manage the state of To-Do items.


        
class TodoProvider with ChangeNotifier {
  List _todos = [];
  final TodoRepository _repository = TodoRepository();

  Future loadTodos() async {
    _todos = await _repository.getTodos();
    notifyListeners();
  }

  Future addTodo(Todo todo) async {
    _todos.add(todo);
    await _repository.saveTodos(_todos);
    notifyListeners();
  }

  // ...
}
        
      

Defining To-Do Item Structure with todo.dart

Understanding todo.dart

The todo.dart file is a fundamental part of our app, as it defines the structure of a To-Do item. Let's examine its structure:


// lib/models/todo.dart

class Todo {
  int id;
  String title;
  bool isCompleted;

  Todo({
    required this.id,
    required this.title,
    required this.isCompleted,
  });

  factory Todo.fromJson(Map json) {
    return Todo(
      id: json['id'] as int,
      title: json['title'] as String,
      isCompleted: json['isCompleted'] as bool,
    );
  }

  Map toJson() {
    return {
      'id': id,
      'title': title,
      'isCompleted': isCompleted,
    };
  }
}
            

Key Features:

To-Do Item Structure: The Todo class defines the structure of a To-Do item. It includes fields for id, title, and isCompleted. These fields represent the unique identifier, title, and completion status of a To-Do item, respectively.

JSON Serialization/Deserialization: The class includes methods for converting To-Do objects to JSON (toJson) and vice versa (fromJson). These methods are essential for storing To-Do items in shared preferences and retrieving them.

How to Use todo.dart

The Todo class can be used throughout your app to represent and manipulate To-Do items. For instance, when you create a new To-Do item in your AddTodoScreen, you can create a Todo object and use it like this:


        
final newTodo = Todo(
  id: DateTime.now().millisecondsSinceEpoch,
  title: todoTitle,
  isCompleted: false,
);
        
      

You can then use this newTodo object to add the To-Do item to your list and save it to shared preferences.

State Management with todo_provider.dart

Understanding todo_provider.dart

The todo_provider.dart file plays a pivotal role in our Flutter To-Do app. It utilizes the Provider package to manage the state of To-Do items. Let's explore its structure and functionality:


// lib/providers/todo_provider.dart

import 'package:flutter/foundation.dart';
import '../models/todo.dart';
import '../data/todo_repository.dart';

class TodoProvider with ChangeNotifier {
  List _todos = [];
  final TodoRepository _repository = TodoRepository();

  List get todos => _todos;

  Future loadTodos() async {
    _todos = await _repository.getTodos();
    notifyListeners();
  }

  Future addTodo(Todo todo) async {
    _todos.add(todo);
    await _repository.saveTodos(_todos);
    notifyListeners();
  }

  Future updateTodo(Todo todo) async {
    final index = _todos.indexWhere((t) => t.id == todo.id);
    if (index != -1) {
      _todos[index] = todo;
      await _repository.saveTodos(_todos);
      notifyListeners();
    }
  }

  Future deleteTodo(int id) async {
    _todos.removeWhere((todo) => todo.id == id);
    await _repository.saveTodos(_todos);
    notifyListeners();
  }
}
           

Key Features:

State Management with Provider: The TodoProvider class extends ChangeNotifier and manages the state of To-Do items. It provides methods to load, add, update, and delete To-Do items.

Data Repository Integration: The provider class uses the TodoRepository to interact with shared preferences for data storage and retrieval.

How to Use todo_provider.dart

To utilize the TodoProvider class, you can instantiate it and access its methods from your app's screens and widgets. For example, in your TodoListScreen, you can load To-Do items and manage their state like this:

Building the UI for Our Flutter To-Do App

Designing the User Interface

Our To-Do app consists of several screens, each serving a specific purpose. Let's discuss the main screens and UI components:

Sample UI Code

Below is a snippet of code representing the structure of our TodoListScreen. Please note that this is a simplified example, and you can customize the UI further to match your design preferences.


// lib/screens/todo_list_screen.dart

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

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text('Todo App'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(
            title: Text(todo.title),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Checkbox(
                  value: todo.isCompleted,
                  onChanged: (newValue) {
                    final updatedTodo = Todo(
                      id: todo.id,
                      title: todo.title,
                      isCompleted: newValue ?? false,
                    );
                    Provider.of(context, listen: false)
                        .updateTodo(updatedTodo);
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.edit),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => EditTodoScreen(todo: todo),
                      ),
                    );
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.delete),
                  onPressed: () {
                    Provider.of(context, listen: false)
                        .deleteTodo(todo.id);
                  },
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => AddTodoScreen()),
          );
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
           

In this step, we've started building the user interface (UI) for our Flutter To-Do app. We've discussed the main screens and UI components that make our app functional.

Testing and Debugging

As your Flutter app becomes more complex, it's crucial to test and debug it effectively. Flutter provides various tools and techniques to ensure your app functions correctly.

Let's start by writing some unit tests for our to-do app:


    // Import the test package
    import 'package:test/test.dart';
    
    void main() {
      test('Task addition test', () {
        // Write your test logic here
        // For example, add a task and check if it appears in the list
      });
    
      // Write more tests for task completion, deletion, etc.
    }
            

Building and Deploying the App

Now that we've developed our to-do app, it's time to prepare it for production and consider deployment options. This section will guide you through the process of building and deploying your app.

Preparing for Production

Before building the app for release, you'll want to make some preparations:

  • Optimize your app's performance by eliminating unnecessary widgets and code.
  • Ensure that your app's UI is responsive and looks great on different screen sizes and orientations.
  • Remove any debugging code or print statements.
  • Double-check that your app's functionality is thoroughly tested and works as expected.

Building the App

Building your Flutter app for production is a crucial step. Here's how you can build the app for Android and iOS platforms:


    # Build the app for Android (APK)
    flutter build apk
    
    # Build the app for iOS (IPA)
    flutter build ios
        

After running these commands, you'll find the built APK (for Android) or IPA (for iOS) files in the respective project folders. You can then proceed with the deployment process for each platform.

Flutter App Screenshot

Publishing to App Stores (Optional)

If you're ready to share your to-do app with the world, consider publishing it to the Google Play Store (for Android) and the Apple App Store (for iOS). The process involves creating developer accounts, generating app signing keys, and following the submission guidelines for each store.

While publishing to app stores is optional and beyond the scope of this tutorial, there are plenty of online resources and documentation available to help you navigate this process.

Conclusion

Congratulations! You've successfully built a to-do app with Flutter, learned about essential Flutter concepts, and explored state management, local data storage, testing, and production deployment. You're now well-equipped to continue your Flutter journey and explore more advanced topics.

Remember that practice is key to becoming a proficient Flutter developer. Keep experimenting with Flutter's features and libraries, and you'll be creating amazing apps in no time.

Thank you for joining me on this Flutter adventure. I hope you found this tutorial valuable, and I encourage you to explore further and share your Flutter creations with the world.

Comments