Skip to content

Commit

Permalink
chore: solidart example (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
nank1ro authored Jan 27, 2025
1 parent e5aa47d commit b11b33b
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 11 deletions.
8 changes: 7 additions & 1 deletion examples/solidart/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
include: package:very_good_analysis/analysis_options.yaml

analyzer:
errors:
always_put_required_named_parameters_first: ignore
plugins:
- custom_lint

linter:
rules:
# since this is just an example, there is no need for ubiquitous documentation.
public_member_api_docs: false
public_member_api_docs: false
58 changes: 58 additions & 0 deletions examples/solidart/lib/controllers/todos.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:disco/disco.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_solidart/flutter_solidart.dart' hide Provider;
import 'package:solidart_example/domain/todo.dart';

final todosControllerProvider = Provider<TodosController>(
(context) => TodosController(initialTodos: Todo.sample),
dispose: (controller) => controller.dispose(),
);

/// Contains the state of the [todos] list and allows to
/// - `add`: Add a todo in the list of [todos]
/// - `remove`: Removes a todo with the given id from the list of [todos]
/// - `toggle`: Toggles a todo with the given id
/// The list of todos exposed is a [ReadSignal] so the user cannot mutate
/// the signal without using this controller.
@immutable
class TodosController {
TodosController({
List<Todo> initialTodos = const [],
}) : todos = ListSignal(initialTodos);

// The list of todos
final ListSignal<Todo> todos;

/// The list of completed todos
late final completedTodos = Computed(
() => todos.where((todo) => todo.completed).toList(),
);

/// The list of incomplete todos
late final incompleteTodos = Computed(
() => todos.where((todo) => !todo.completed).toList(),
);

/// Add a todo
void add(Todo todo) {
todos.add(todo);
}

/// Remove a todo with the given [id]
void remove(String id) {
todos.removeWhere((todo) => todo.id == id);
}

/// Toggle a todo with the given [id]
void toggle(String id) {
final todoIndex = todos.indexWhere((element) => element.id == id);
final todo = todos[todoIndex];
todos[todoIndex] = todo.copyWith(completed: !todo.completed);
}

void dispose() {
todos.dispose();
completedTodos.dispose();
incompleteTodos.dispose();
}
}
45 changes: 45 additions & 0 deletions examples/solidart/lib/domain/todo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';

@immutable
class Todo {
const Todo({
required this.id,
required this.task,
required this.completed,
});

factory Todo.create(String task) {
final uuid = const Uuid().v4();
return Todo(id: uuid, task: task, completed: false);
}

final String id;
final String task;
final bool completed;

static List<Todo> get sample {
return [
Todo.create('Learn solidart'),
Todo.create('Wash the car'),
Todo.create('Go shopping'),
];
}

Todo copyWith({bool? completed}) {
return Todo(
id: id,
task: task,
completed: completed ?? this.completed,
);
}

@override
String toString() => 'Todo(id: $id, task: $task, completed: $completed)';
}

enum TodosFilter {
all,
incomplete,
completed;
}
15 changes: 6 additions & 9 deletions examples/solidart/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import 'package:flutter/material.dart';
import 'package:solidart_example/pages/todos.dart';

void main() {
runApp(const MainApp());
runApp(const MyApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
// TODO(nank1ro): create an example with signals
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
title: 'Todos Example',
home: TodosPage(),
);
}
}
25 changes: 25 additions & 0 deletions examples/solidart/lib/pages/todos.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:disco/disco.dart';
import 'package:flutter/material.dart';
import 'package:solidart_example/controllers/todos.dart';
import 'package:solidart_example/widgets/todos_body.dart';

class TodosPage extends StatelessWidget {
const TodosPage({super.key});

@override
Widget build(BuildContext context) {
// Using ProviderScope here to provide the [TodosController] to descendants.
return ProviderScope(
providers: [todosControllerProvider],
child: Scaffold(
appBar: AppBar(
title: const Text('Todos'),
),
body: const Padding(
padding: EdgeInsets.all(8.0),
child: TodosBody(),
),
),
);
}
}
42 changes: 42 additions & 0 deletions examples/solidart/lib/widgets/todo_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:solidart_example/controllers/todos.dart';
import 'package:solidart_example/domain/todo.dart';

/// Renders a ListTile with a checkbox where you can change
/// the "completion" status of a [Todo]
class TodoItem extends StatelessWidget {
const TodoItem({
super.key,
required this.todo,
this.onStatusChanged,
});

final Todo todo;
final ValueChanged<bool?>? onStatusChanged;

@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(todo.id),
direction: DismissDirection.endToStart,
onDismissed: (_) {
// remove the todo
todosControllerProvider.of(context).remove(todo.id);
},
background: Container(
decoration: const BoxDecoration(color: Colors.red),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 8),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
child: CheckboxListTile(
title: Text(todo.task),
value: todo.completed,
onChanged: onStatusChanged,
),
);
}
}
71 changes: 71 additions & 0 deletions examples/solidart/lib/widgets/todos_body.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:disco/disco.dart';
import 'package:flutter/material.dart';
import 'package:flutter_solidart/flutter_solidart.dart' hide Provider;
import 'package:solidart_example/controllers/todos.dart';
import 'package:solidart_example/domain/todo.dart';
import 'package:solidart_example/widgets/todos_list.dart';
import 'package:solidart_example/widgets/toolbar.dart';

final todosFilterProvider = Provider((context) => Signal(TodosFilter.all));

class TodosBody extends StatefulWidget {
const TodosBody({super.key});

@override
State<TodosBody> createState() => _TodosBodyState();
}

class _TodosBodyState extends State<TodosBody> {
final textController = TextEditingController();

@override
void dispose() {
textController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
// retrieve the [TodosController], you're safe to retrieve Provider in both
// the `initState` and `build` methods.
final todosController = todosControllerProvider.of(context);

return ProviderScope(
providers: [
// make the active filter signal visible only to descendants.
// scoped here because this is where it starts to be necessary.
todosFilterProvider,
],
child: Column(
children: [
TextFormField(
controller: textController,
decoration: const InputDecoration(
hintText: 'Write new todo',
),
validator: (v) {
if (v == null || v.isEmpty) {
return 'Cannot be empty';
}
return null;
},
onFieldSubmitted: (task) {
if (task.isEmpty) return;
final newTodo = Todo.create(task);
todosController.add(newTodo);
textController.clear();
},
),
const SizedBox(height: 16),
const Toolbar(),
const SizedBox(height: 16),
Expanded(
child: TodoList(
onTodoToggle: todosController.toggle,
),
),
],
),
);
}
}
59 changes: 59 additions & 0 deletions examples/solidart/lib/widgets/todos_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_solidart/flutter_solidart.dart';
import 'package:solidart_example/controllers/todos.dart';
import 'package:solidart_example/domain/todo.dart';
import 'package:solidart_example/widgets/todo_item.dart';
import 'package:solidart_example/widgets/todos_body.dart';

class TodoList extends StatefulWidget {
const TodoList({
super.key,
this.onTodoToggle,
});

final ValueChanged<String>? onTodoToggle;

@override
State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
// retrieve the [TodosController]
late final todosController = todosControllerProvider.of(context);

// Given a [filter] return the correct list of todos
ReadSignal<List<Todo>> mapFilterToTodosList(TodosFilter filter) {
switch (filter) {
case TodosFilter.all:
return todosController.todos;
case TodosFilter.incomplete:
return todosController.incompleteTodos;
case TodosFilter.completed:
return todosController.completedTodos;
}
}

@override
Widget build(BuildContext context) {
return SignalBuilder(
builder: (context, child) {
// rebuilds every time the activeFilter value changes
final activeFilter = todosFilterProvider.of(context).value;
// react to the correct list of todos list
final todos = mapFilterToTodosList(activeFilter).value;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
final todo = todos[index];
return TodoItem(
todo: todo,
onStatusChanged: (_) {
widget.onTodoToggle?.call(todo.id);
},
);
},
);
},
);
}
}
Loading

0 comments on commit b11b33b

Please sign in to comment.