Skip to content

Commit

Permalink
feat: local data source support
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertBrunhage committed Sep 28, 2024
1 parent 2879971 commit c299179
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 47 deletions.
14 changes: 13 additions & 1 deletion lib/todo.dart → lib/features/todo/todo.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import 'package:todo/shared/database/database.dart';

class Todo {
final String id;
final String title;
final bool completed;

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

TodoEntity toDatabase() {
return TodoEntity(
id: id,
title: title,
completed: completed,
);
}

Todo copyWith({
String? title,
String? description,
bool? completed,
}) {
return Todo(
id: id,
title: title ?? this.title,
completed: completed ?? this.completed,
);
Expand Down
32 changes: 32 additions & 0 deletions lib/features/todo/todo_local_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:todo/shared/database/database.dart';

class TodoLocalDataSource {
TodoLocalDataSource({required AppDatabase database}) : _database = database;
final AppDatabase _database;

Stream<List<TodoEntity>> listenAll() {
final query = _database.select(_database.todoEntities);

return query.watch();
}

Future<void> add(String text) async {
final insert = TodoEntitiesCompanion.insert(
title: text,
);
await _database.into(_database.todoEntities).insert(insert);
}

Future<void> remove(TodoEntity todo) async {
final query = _database.delete(_database.todoEntities)
..where((tbl) => tbl.id.equals(todo.id));
await query.go();
}

Future<void> update(TodoEntity todo) async {
final query = _database.update(_database.todoEntities)
..where((tbl) => tbl.id.equals(todo.id));

await query.write(todo);
}
}
16 changes: 8 additions & 8 deletions lib/features/todo/todo_page.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:todo/features/todo/todo_page_view_model.dart';
import 'package:todo/features/todo/todo_repository.dart';
import 'package:todo/main.dart';
import 'package:todo/shared/date_service.dart';
import 'package:todo/shared/locator.dart';
import 'package:todo/shared/ui_utilities/value_listenable_builder_x.dart';
import 'package:todo/todo.dart';

class TodoPage extends StatefulWidget {
const TodoPage({super.key});
Expand All @@ -13,8 +15,8 @@ class TodoPage extends StatefulWidget {

class _TodoPageState extends State<TodoPage> {
late final homePageViewModel = TodoPageViewModel(
dateService: dateService,
todoRepository: todoRepository,
dateService: locator<DateService>(),
todoRepository: locator<TodoRepository>(),
);

final TextEditingController _todoController = TextEditingController();
Expand Down Expand Up @@ -53,9 +55,7 @@ class _TodoPageState extends State<TodoPage> {
TextButton(
onPressed: () {
homePageViewModel.add(
Todo(
title: _todoController.text,
),
title: _todoController.text,
);

_todoController.clear();
Expand All @@ -81,7 +81,7 @@ class _TodoPageState extends State<TodoPage> {
),
actions: [
ValueListenableBuilder2(
first: homePageViewModel.todos,
first: homePageViewModel.todosNotifier,
second: homePageViewModel.showCompletedTodos,
builder: (context, todos, showCompletedTodos, child) {
if (homePageViewModel.hasNonCompletedTodos) {
Expand All @@ -102,7 +102,7 @@ class _TodoPageState extends State<TodoPage> {
body: TodoList(
toggleDone: homePageViewModel.toggleDone,
removeTodo: homePageViewModel.remove,
todosNotifier: homePageViewModel.todos,
todosNotifier: homePageViewModel.todosNotifier,
showCompletedTodos: homePageViewModel.showCompletedTodos,
),
floatingActionButton: Row(
Expand Down
32 changes: 22 additions & 10 deletions lib/features/todo/todo_page_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:todo/features/todo/todo.dart';
import 'package:todo/features/todo/todo_repository.dart';
import 'package:todo/shared/date_service.dart';
import 'package:todo/todo.dart';

/// the viewmodel which is responsible for business logic of the page
/// this should be fully unit testable and dependencies should be constructor injected
Expand All @@ -19,22 +19,33 @@ class TodoPageViewModel {

ValueNotifier<DateTime> get serviceDate => _dateService.dateNotifier;

final ValueNotifier<List<Todo>> todos = ValueNotifier([]);
final ValueNotifier<List<Todo>> todosNotifier = ValueNotifier([]);
final ValueNotifier<bool> showCompletedTodos = ValueNotifier(false);

StreamSubscription<List<Todo>>? _subscription;

bool get hasNonCompletedTodos =>
todos.value.where((element) => element.completed).isNotEmpty;
todosNotifier.value.where((element) => element.completed).isNotEmpty;

Future<void> init() async {
_todoRepository.todos.addListener(onTodosUpdate);
void init() {
_listenToTodos();
}

void onTodosUpdate() {
todos.value = _todoRepository.todos.value;
void _listenToTodos() {
_subscription?.cancel();
_subscription = null;

final stream = _todoRepository.listenAll();

_subscription = stream.listen(
(todos) {
todosNotifier.value = todos;
},
);
}

Future<void> add(Todo todo) async {
_todoRepository.addTodo(todo);
Future<void> add({required String title}) async {
_todoRepository.addTodo(title: title);
}

Future<void> remove(Todo todo) async {
Expand All @@ -54,6 +65,7 @@ class TodoPageViewModel {
}

void dispose() {
_todoRepository.todos.removeListener(onTodosUpdate);
_subscription?.cancel();
_subscription = null;
}
}
38 changes: 21 additions & 17 deletions lib/features/todo/todo_repository.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import 'package:flutter/foundation.dart';
import 'package:todo/todo.dart';
import 'package:todo/features/todo/todo.dart';
import 'package:todo/features/todo/todo_local_data_source.dart';
import 'package:todo/shared/database/database.dart';

class TodoRepository {
TodoRepository();
TodoRepository({required TodoLocalDataSource todoLocalDataSource})
: _todoLocalDataSource = todoLocalDataSource;

// this should live in a server or local storage and we should provide a listenable approach to this item
// ValueNotifier here is just an example but most storage solutions and packages provide a reactive way to get data.
final ValueNotifier<List<Todo>> todos = ValueNotifier([]);
final TodoLocalDataSource _todoLocalDataSource;

void addTodo(Todo todo) {
todos.value = [...todos.value, todo];
Stream<List<Todo>> listenAll() {
return _todoLocalDataSource.listenAll().map((todos) {
return todos.map((todo) {
return todo.toTodo();
}).toList();
});
}

void removeTodo(Todo todo) {
todos.value = todos.value.where((element) => element != todo).toList();
Future<void> addTodo({required String title}) async {
await _todoLocalDataSource.add(title);
}

void toggleDone(Todo todo) {
todos.value = todos.value.map((oldTodo) {
if (oldTodo == todo) {
return oldTodo.copyWith(completed: !oldTodo.completed);
}
return oldTodo;
}).toList();
Future<void> removeTodo(Todo todo) async {
await _todoLocalDataSource.remove(todo.toDatabase());
}

Future<void> toggleDone(Todo todo) async {
final toggledTodo = todo.copyWith(completed: !todo.completed);
await _todoLocalDataSource.update(toggledTodo.toDatabase());
}
}
14 changes: 4 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import 'package:flutter/material.dart';
import 'package:todo/features/todo/todo.dart';
import 'package:todo/features/todo/todo_page.dart';
import 'package:todo/features/todo/todo_repository.dart';
import 'package:todo/shared/date_service.dart';
import 'package:todo/todo.dart';

// app wide dependencies, consider using GetIt to override
// dependencies in tests.
late final DateService dateService;
late final TodoRepository todoRepository;
import 'package:todo/shared/locator.dart';

void main() {
WidgetsFlutterBinding.ensureInitialized();

dateService = DateService();
todoRepository = TodoRepository();
// initialize our service, repositories and other app wide classes
setupLocators();

runApp(
MaterialApp(
Expand Down
38 changes: 38 additions & 0 deletions lib/shared/database/database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:todo/features/todo/todo.dart';
import 'package:uuid/uuid.dart';

part 'database.g.dart';

const _uuid = Uuid();

extension TodoItemX on TodoEntity {
Todo toTodo() {
return Todo(
id: id,
title: title,
completed: completed,
);
}
}

class TodoEntities extends Table {
TextColumn get id => text().clientDefault(() => _uuid.v4())();
TextColumn get title => text()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
}

@DriftDatabase(tables: [
TodoEntities,
])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());

@override
int get schemaVersion => 1;

static QueryExecutor _openConnection() {
return driftDatabase(name: 'database');
}
}
Loading

0 comments on commit c299179

Please sign in to comment.