# Flutter Examples

Here we discuss about a flutter todo application using Nitrite database. It uses nitrite as a file based storage engine. It also uses Riverpod for state management. It demonstrates the use of Nitrite database in a flutter application. The full source code is available here. This tutorial assumes that you have basic knowledge of flutter and riverpod.

# Setup

Add the following Nitrite dependencies in your pubspec.yaml file along with path_provider and riverpod_generator:

dependencies:
  nitrite: ^[latest-version]
  nitrite_hive_adapter: ^[latest-version]
  path_provider: ^2.0.15
  riverpod_annotation: ^2.1.1

dev_dependencies:
  build_runner: ^2.4.6
  riverpod_generator: ^2.2.3
  nitrite_generator: ^[latest-version]

# Entity Classes

Define a simple Todo entity class to hold your todo data:

import 'package:nitrite/nitrite.dart';

part 'models.no2.dart';

@Entity(name: 'todo', indices: [
  Index(fields: ['title'], type: IndexType.fullText),
])
@Convertable()
class Todo with _$TodoEntityMixin {
  @Id(fieldName: 'id')
  final String id;
  final String title;
  bool completed = false;

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

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Todo &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          title == other.title &&
          completed == other.completed;

  @override
  int get hashCode => id.hashCode ^ title.hashCode ^ completed.hashCode;

  @override
  String toString() {
    return 'Todo{id: $id, title: $title, completed: $completed}';
  }
}

And run the following command to generate the _$TodoEntityMixin:

dart run build_runner build

This command will generate the models.no2.dart file in the same directory.

# Database Initialization

Next create a Nitrite database provider using riverpod:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:nitrite/nitrite.dart';
import 'package:nitrite_hive_adapter/nitrite_hive_adapter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'providers.g.dart';

@riverpod
Future<Nitrite> db(DbRef ref) async {
  var docPath = await getApplicationDocumentsDirectory();
  var dbDir = await Directory('${docPath.path}${Platform.pathSeparator}db')
      .create(recursive: true);
  var storeModule =
      HiveModule.withConfig().crashRecovery(true).path(dbDir.path).build();

  var db = await Nitrite.builder()
      .loadModule(storeModule)
      .fieldSeparator('.')
      .registerEntityConverter(TodoConverter())
      .openOrCreate(username: 'demo', password: 'demo123');

  return db;
}

Now using the dbProvider, create a todo ObjectRepository provider:

@riverpod
Future<ObjectRepository<Todo>> todoRepository(TodoRepositoryRef ref) async {
  var db = await ref.read(dbProvider.future);
  return await db.getRepository<Todo>();
}

And other required providers for listing and searching:

@riverpod
class Todos extends _$Todos {
  Future<List<Todo>> _fetchTodo() async {
    var repository = await ref.read(todoRepositoryProvider.future);
    var filter = ref.watch(filterProvider);
    var findOptions = ref.watch(findOptionStateProvider);
    return repository.find(filter: filter, findOptions: findOptions).toList();
  }

  @override
  Future<List<Todo>> build() async {
    return _fetchTodo();
  }

  Future<void> addTodo(Todo todo) async {
    state = const AsyncValue.loading();

    try {
      var repository = await ref.read(todoRepositoryProvider.future);
      await repository.insert(todo);
      state = AsyncValue.data(await _fetchTodo());
    } catch (e, s) {
      state = AsyncValue.error(e, s);
    }
  }

  Future<void> removeTodo(String todoId) async {
    state = const AsyncValue.loading();
    try {
      var repository = await ref.read(todoRepositoryProvider.future);
      await repository.remove(where('id').eq(todoId));
      state = AsyncValue.data(await _fetchTodo());
    } catch (e, s) {
      state = AsyncValue.error(e, s);
    }
  }

  Future<void> toggle(String todoId) async {
    state = const AsyncValue.loading();
    try {
      var repository = await ref.read(todoRepositoryProvider.future);
      var byId = await repository.getById(todoId);
      if (byId != null) {
        byId.completed = !byId.completed;
        await repository.updateOne(byId);
      }
      state = AsyncValue.data(await _fetchTodo());
    } catch (e, s) {
      state = AsyncValue.error(e, s);
    }
  }
}

@riverpod
class FindOptionState extends _$FindOptionState {
  @override
  FindOptions build() {
    return FindOptions(
      orderBy: SortableFields.from([('title', SortOrder.ascending)]),
      skip: 0,
      limit: 10,
    );
  }
}

final filterProvider = StateProvider<Filter>((ref) => all);
final todoTextProvider = StateProvider<String>((ref) => '');

@riverpod
int pendingCounter(PendingCounterRef ref) {
  var todos = ref.watch(todosProvider);
  return todos.when(
    data: (todoList) => todoList.where((todo) => !todo.completed).length,
    loading: () => 0,
    error: (err, stack) => 0,
  );
}

@riverpod
int completedCounter(CompletedCounterRef ref) {
  var todos = ref.watch(todosProvider);
  return todos.when(
    data: (todoList) => todoList.where((todo) => todo.completed).length,
    loading: () => 0,
    error: (err, stack) => 0,
  );
}

Now again run the following command to generate the providers:

dart run build_runner build

# UI

Now define a todo widget to display a single time and define different actions on it:

class TodoWidget extends ConsumerWidget {
  final Todo todo;

  const TodoWidget({super.key, required this.todo});

  bool _isDesktop() =>
      Platform.isLinux || Platform.isMacOS || Platform.isWindows;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    if (_isDesktop()) {
      return Card(
        color: Theme.of(context).colorScheme.surface,
        child: ListTile(
          title: Text(
            todo.title,
            style: TextStyle(
              decoration: todo.completed ? TextDecoration.lineThrough : null,
            ),
          ),
          trailing: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                onPressed: () =>
                    ref.read(todosProvider.notifier).toggle(todo.id),
                icon: Icon(
                  todo.completed ? Icons.task_alt : Icons.check_box_outlined,
                  color: todo.completed ? Colors.green : Colors.grey,
                ),
              ),
              IconButton(
                onPressed: () =>
                    ref.read(todosProvider.notifier).removeTodo(todo.id),
                icon: const Icon(
                  Icons.delete,
                  color: Colors.red,
                ),
              ),
            ],
          ),
        ),
      );
    } else {
      return Slidable(
        key: ValueKey(todo.id),
        startActionPane: ActionPane(
          motion: const ScrollMotion(),
          children: [
            SlidableAction(
              borderRadius: BorderRadius.circular(5),
              spacing: 10,
              onPressed: (context) =>
                  ref.read(todosProvider.notifier).toggle(todo.id),
              backgroundColor: Colors.green,
              foregroundColor: Colors.white,
              icon: todo.completed ? Icons.task_alt : Icons.check_box_outlined,
            ),
          ],
        ),
        endActionPane: ActionPane(
          motion: const ScrollMotion(),
          children: [
            SlidableAction(
              borderRadius: BorderRadius.circular(5),
              spacing: 10,
              onPressed: (context) =>
                  ref.read(todosProvider.notifier).removeTodo(todo.id),
              backgroundColor: Colors.red,
              foregroundColor: Colors.white,
              icon: Icons.delete,
            ),
          ],
        ),
        child: ListTile(
          leading: const Icon(Icons.adjust, color: Colors.black26),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(5),
          ),
          title: Text(
            todo.title,
            style: TextStyle(
              decoration: todo.completed ? TextDecoration.lineThrough : null,
            ),
          ),
        ),
      );
    }
  }
}

And a todo list widget to display the list of todos:

class TodoList extends ConsumerWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Get the todos from the provider.
    var todos = ref.watch(todosProvider);

    return todos.when(
      data: (todoList) => ListView.builder(
        itemCount: todoList.length,
        itemBuilder: (context, index) {
          Todo todo = todoList[index];
          return Padding(
            padding: const EdgeInsets.only(left: 30.0, right: 30.0),
            child: TodoWidget(todo: todo),
          );
        },
      ),
      error: (err, stack) => Text('Error: $err\n$stack'),
      loading: () => const Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

Now you can use these widgets in your app and other widgets to add new todo, search, etc.