DEV Community

Subina Thapa
Subina Thapa

Posted on

Building a Scalable To-Do App in Flutter Using Provider & Local Storage

Introduction:

Beginners often create simple apps without considering scalability or maintainability. In this guide, we’ll build a professional, production-ready To-Do app using Riverpod for state management, Hive for persistent storage, and clean architecture principles.

Step 1
Setup Dependencies
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.6
hive: ^2.2.3
hive_flutter: ^1.1.0

Step 2
Project Structure (Clean Architecture)
lib/
├─ main.dart
├─ models/
│ └─ task.dart
├─ providers/
│ └─ task_provider.dart
├─ services/
│ └─ task_service.dart
├─ screens/
│ └─ todo_home.dart

This separation makes the app scalable, testable, and maintainable.

Step 3
Create the Task Model
import 'package:hive/hive.dart';

part 'task.g.dart';

@HiveType(typeId: 0)
class Task extends HiveObject {
@HiveField(0)
String title;

@HiveField(1)
bool completed;

Task({required this.title, this.completed = false});
}
Use build_runner to generate Hive adapters:
flutter pub run build_runner build

Step 4
Create Task Service (Handles Data & Persistence)
import 'package:hive_flutter/hive_flutter.dart';
import '../models/task.dart';

class TaskService {
static const String boxName = 'tasksBox';

late Box _box;

Future init() async {
_box = await Hive.openBox(boxName);
}

List getTasks() => _box.values.toList();

Future addTask(Task task) async => await _box.add(task);

Future updateTask(int index, Task task) async => await _box.putAt(index, task);

Future deleteTask(int index) async => await _box.deleteAt(index);
}

Step 5
Create Riverpod Provider
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/task.dart';
import '../services/task_service.dart';

final taskServiceProvider = Provider((ref) => TaskService());

final tasksProvider = StateNotifierProvider>((ref) {
final service = ref.watch(taskServiceProvider);
return TasksNotifier(service);
});

class TasksNotifier extends StateNotifier> {
final TaskService _service;

TasksNotifier(this._service) : super([]) {
loadTasks();
}

Future loadTasks() async {
state = _service.getTasks();
}

Future addTask(String title) async {
final task = Task(title: title);
await _service.addTask(task);
state = [...state, task];
}

Future toggleComplete(int index) async {
final task = state[index];
task.completed = !task.completed;
await _service.updateTask(index, task);
state = [...state];
}

Future deleteTask(int index) async {
await _service.deleteTask(index);
state = [...state]..removeAt(index);
}
}

Step 6:
Build the UI (TodoHome Screen)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/task_provider.dart';

class TodoHome extends ConsumerWidget {
final TextEditingController controller = TextEditingController();

@override
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(tasksProvider);

return Scaffold(
  appBar: AppBar(title: Text("Advanced To-Do App")),
  body: Column(
    children: [
      Padding(
        padding: EdgeInsets.all(10),
        child: TextField(
          controller: controller,
          decoration: InputDecoration(labelText: "Enter a task"),
        ),
      ),
      ElevatedButton(
        onPressed: () {
          ref.read(tasksProvider.notifier).addTask(controller.text);
          controller.clear();
        },
        child: Text("Add Task"),
      ),
      Expanded(
        child: ListView.builder(
          itemCount: tasks.length,
          itemBuilder: (_, index) {
            final task = tasks[index];
            return ListTile(
              title: Text(
                task.title,
                style: TextStyle(
                  decoration: task.completed ? TextDecoration.lineThrough : null,
                ),
              ),
              leading: Checkbox(
                value: task.completed,
                onChanged: (_) => ref.read(tasksProvider.notifier).toggleComplete(index),
              ),
              trailing: IconButton(
                icon: Icon(Icons.delete),
                onPressed: () => ref.read(tasksProvider.notifier).deleteTask(index),
              ),
            );
          },
        ),
      ),
    ],
  ),
);
Enter fullscreen mode Exit fullscreen mode

}
}

Step 7: Why This Is Pro-Level
Riverpod → Safer, testable, and scalable state management.
Clean Architecture → Separate UI, providers, services, and models.
Persistent storage → Hive ensures data persists across app restarts.
Error handling & async operations → Prepares app for real-world scenarios.

Step 8: Next Enhancements for a Real-World App
Add user authentication with Firebase.
Sync tasks across devices with cloud storage.
Implement push notifications for task reminders.
Add animations & theming for production-quality UX.

Top comments (0)