flutter_rearch 0.0.0-dev.4 copy "flutter_rearch: ^0.0.0-dev.4" to clipboard
flutter_rearch: ^0.0.0-dev.4 copied to clipboard

A re-imagined declarative approach to application design and architecture

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_mimir/flutter_mimir.dart';
import 'package:flutter_rearch/flutter_rearch.dart';
import 'package:rearch/rearch.dart';

void main() => runApp(const TodoApp());

/// Represents the [MimirIndex] that contains the movie dataset.
Future<MimirIndex> indexAsyncCapsule(CapsuleHandle use) async {
  final instance = await Mimir.defaultInstance;
  return instance.openIndex('todos', primaryKey: 'timestamp');
}

/// Allows for the [indexAsyncCapsule] to more easily be warmed up
/// for use in [indexCapsule].
AsyncValue<MimirIndex> indexWarmUpCapsule(CapsuleHandle use) {
  final future = use(indexAsyncCapsule);
  return use.future(future);
}

/// Acts as a proxy to the warmed-up [indexAsyncCapsule].
MimirIndex indexCapsule(CapsuleHandle use) {
  return use(indexWarmUpCapsule).data.unwrapOrElse(
        () => throw StateError('indexAsyncCapsule was not warmed up!'),
      );
}

/// Represents an item in the todos list.
typedef Todo = ({
  int timestamp, // milliseconds since epoch
  String title,
  String? description,
  bool completed,
});

/// Utilities for handling conversion between [Todo] and [MimirDocument]s.
extension TodoDocumentUtilities on Todo {
  /// Creates a [MimirDocument] from a [Todo].
  MimirDocument toDocument() {
    return {
      'timestamp': timestamp,
      'title': title,
      'description': description,
      'completed': completed,
    };
  }

  /// Creates a [Todo] from a [MimirDocument].
  static Todo toTodo(MimirDocument document) {
    return (
      timestamp: document['timestamp']!,
      title: document['title']!,
      description: document['description'],
      completed: document['completed']!,
    );
  }
}

/// Provides a way to create/update and delete todos.
({
  void Function(Todo) updateTodo,
  void Function(int) deleteTodo,
}) todoListManagerCapsule(CapsuleHandle use) {
  final index = use(indexCapsule);
  return (
    updateTodo: (todo) => index.addDocument(todo.toDocument()),
    deleteTodo: (timestamp) => index.deleteDocument(timestamp.toString()),
  );
}

/// Represents the filter for a list of todos.
typedef TodoListFilter = ({
  String query,
  bool completionStatus,
});

/// Represents the current filter to search with
/// ('' as a query string represents no current query).
({
  TodoListFilter filter,
  void Function(String) setQueryString,
  void Function() toggleCompletionStatus,
}) filterCapsule(CapsuleHandle use) {
  final (query, setQuery) = use.state('');
  final (completionStatus, setCompletionStatus) = use.state(false);
  return (
    filter: (query: query, completionStatus: completionStatus),
    setQueryString: setQuery,
    toggleCompletionStatus: () => setCompletionStatus(!completionStatus),
  );
}

/// Represents the todos list using the filter from the [filterCapsule].
AsyncValue<List<Todo>> todoListCapsule(CapsuleHandle use) {
  final index = use(indexCapsule);
  final (
    filter: (:query, :completionStatus),
    setQueryString: _,
    toggleCompletionStatus: _
  ) = use(filterCapsule);

  // When query is null/empty, it does not affect the search.
  final documentsStream = use.memo(
    () => index.searchStream(
      query: query,
      filter: Mimir.where('completed', isEqualTo: completionStatus.toString()),
    ),
    [index, query, completionStatus],
  );
  final todoDocumentsState = use.stream(documentsStream);
  return todoDocumentsState.map(
    (todoDocs) => todoDocs.map(TodoDocumentUtilities.toTodo).toList()
      ..sort((todo1, todo2) => todo1.timestamp.compareTo(todo2.timestamp)),
  );
}

/// Represents the length of the [todoListCapsule].
AsyncValue<int> todoListLengthCapsule(CapsuleHandle use) =>
    use(todoListCapsule).map((todos) => todos.length);

/// {@template TodoApp}
/// Wraps around [MaterialApp] and is the entry point [Widget] of the app.
/// {@endtemplate}
class TodoApp extends StatelessWidget {
  /// {@macro TodoApp}
  const TodoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return RearchBootstrapper(
      child: MaterialApp(
        title: 'Rearch Todos',
        theme: ThemeData.light(useMaterial3: true),
        darkTheme: ThemeData.dark(useMaterial3: true),
        home: const GlobalWarmUps(child: Body()),
      ),
    );
  }
}

/// {@template GlobalWarmUps}
/// Warms up all of the global warm up capsules so that the rest of the app
/// doesn't have to individually handle failure states.
/// {@endtemplate}
final class GlobalWarmUps extends CapsuleConsumer {
  /// {@macro GlobalWarmUps}
  const GlobalWarmUps({required this.child, super.key});

  /// The [Widget] to show when all warm up capsules are [AsyncData]s.
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    return [
      use(indexWarmUpCapsule),
    ].toWarmUpWidget(
      child: child,
      loading: const Center(child: CircularProgressIndicator.adaptive()),
      errorBuilder: (errors) => Column(
        children: [
          for (final AsyncError(:error, :stackTrace) in errors)
            Text('$error\n$stackTrace'),
        ],
      ),
    );
  }
}

/// {@template Body}
/// Wraps around [Scaffold] and serves as the main body of the application.
/// {@endtemplate}
class Body extends CapsuleConsumer {
  /// {@macro Body}
  const Body({super.key});

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    final (isSearching, setIsSearching) = use.state(false);

    final (
      filter: (query: _, :completionStatus),
      setQueryString: _,
      :toggleCompletionStatus,
    ) = use(filterCapsule);

    final (:updateTodo, deleteTodo: _) = use(todoListManagerCapsule);

    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'rearch todos',
          style: TextStyle(fontWeight: FontWeight.w700),
        ),
        toolbarOpacity: 0.75,
        bottomOpacity: 0.75,
        actions: [
          IconButton(
            icon: Icon(
              completionStatus
                  ? Icons.task_alt_rounded
                  : Icons.radio_button_unchecked_rounded,
            ),
            onPressed: toggleCompletionStatus,
          ),
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => setIsSearching(!isSearching),
          ),
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => showCreateTodoDialog(context, updateTodo),
          )
        ],
        // TODO(GregoryConrad): bottom: PreferredSizeWidget,
      ),
      // TODO(GregoryConrad): lets do that fun bubble effect
      body: const TodoList(),
    );
  }
}

/// {@template TodoList}
/// Displays the current list of todos based on the [filterCapsule],
/// including support for when the todos are [AsyncLoading] and [AsyncError].
/// {@endtemplate}
class TodoList extends CapsuleConsumer {
  /// {@macro TodoList}
  const TodoList({super.key});

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    final completionFilter = use(filterCapsule).filter.completionStatus;
    final completionText = completionFilter ? 'completed' : 'incomplete';

    final todoListLength = use(todoListLengthCapsule);

    final todoListWidget = todoListLength.data.map((length) {
      return ListView.builder(
        itemCount: length,
        itemBuilder: (context, index) => TodoItem(index: index),
      );
    }).asNullable();

    final infoWidget = switch (todoListLength) {
      AsyncLoading() => const CircularProgressIndicator.adaptive(),
      AsyncError(:final error) => Text('$error'),
      AsyncData(data: final length) when length == 0 =>
        Text('No $completionText todos found'),
      _ => null,
    };

    return Stack(
      children: [
        if (todoListWidget != null)
          Positioned.fill(
            child: todoListWidget,
          ),
        if (infoWidget != null)
          Positioned(
            left: 0,
            right: 0,
            bottom: 32,
            child: Center(
              child: Card(
                elevation: 8,
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: infoWidget,
                ),
              ),
            ),
          ),
      ],
    );
  }
}

/// {@template TodoItem}
/// Displays a singular [Todo] item from the [todoListCapsule].
/// This example is slightly over-engineered to showcase how to reduce rebuilds
/// when dealing with slightly larger lists of data.
/// {@endtemplate}
class TodoItem extends CapsuleConsumer {
  /// {@macro TodoItem}
  const TodoItem({required this.index, super.key});

  /// The [index] of this [TodoItem] in the [todoListCapsule].
  final int index;

  @override
  Widget build(BuildContext context, WidgetHandle use) {
    // The following uses a more advanced technique in rearch: inline capsules.
    // This is similar to `select` in other state management frameworks, but
    // inline capsules are much more powerful because they are full capsules.
    // Please read the documentation to learn more about inline capsules
    // *before using them*, because:
    // > with great power comes great responsibility
    // (you can accidentally cause leaks if you're not careful)
    final (:title, :description, :timestamp, :completed) = use(
      (CapsuleReader use) => use(todoListCapsule).dataOrElse(
        () => throw StateError(
          'In order to display a TodoItem, the todo list must have data!',
        ),
      )[index],
    );

    final (:updateTodo, :deleteTodo) = use(todoListManagerCapsule);

    void delete() => deleteTodo(timestamp);
    void toggleCompletionStatus() {
      updateTodo(
        (
          title: title,
          description: description,
          timestamp: timestamp,
          completed: !completed,
        ),
      );
    }

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
      child: Card(
        child: ListTile(
          title: Text(title),
          subtitle: description != null ? Text(description) : null,
          leading: Icon(
            completed
                ? Icons.task_alt_rounded
                : Icons.radio_button_unchecked_rounded,
          ),
          onTap: toggleCompletionStatus,
          onLongPress: delete,
        ),
      ),
    );
  }
}

/// Shows a dialog that enables users to create todos.
Future<void> showCreateTodoDialog(
  BuildContext context,
  void Function(Todo) todoCreator,
) {
  var (title, description) = ('', '');
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        icon: const Icon(Icons.edit),
        title: const Text('Create Todo'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              onChanged: (newTitle) => title = newTitle,
            ),
            TextField(
              onChanged: (newDescription) => description = newDescription,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: Navigator.of(context).pop,
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              todoCreator(
                (
                  title: title,
                  description: description == '' ? null : description,
                  completed: false,
                  timestamp: DateTime.now().millisecondsSinceEpoch,
                ),
              );
              Navigator.of(context).pop();
            },
            child: const Text('Save'),
          ),
        ],
      );
    },
  );
}
11
likes
0
points
397
downloads

Publisher

verified publishergsconrad.com

Weekly Downloads

A re-imagined declarative approach to application design and architecture

Homepage
Repository (GitHub)
View/report issues

Documentation

Documentation

License

unknown (license)

Dependencies

flutter, rearch

More

Packages that depend on flutter_rearch