doso logo

Static Badge Static Badge Static Badge

DoSo: Simple and Elegant Error and State Handling

DoSo is a lightweight Dart library designed to simplify and streamline state and error handling in Flutter applications. Built to reduce boilerplate and avoid code generation, DoSo provides a functional and expressive API for managing synchronous and asynchronous operations.


πŸš€ Getting Started

DoSo offers a declarative and functional approach to handling common states such as:

  • initial
  • loading
  • success
  • failure

This encapsulates logic and enhances error handling throughout your app. Check out the example below to see how DoSo can be used in your Flutter projects.

import 'package:doso/doso.dart';

void main() async {
  // [Do] represents a action. (State with a value of type S and an optional failure of type F)
  // [So] represents a return. (Is a type alias for Do<F, S> and SoException<Exception, S> for a fixed failure type)
  
  // EMITTING STATES
  // Use Do.tryCatch to handle sync or async operations
  final result = await Do.tryCatch(
    onTry: () => doSomething(),
    onCatch: (exception, stackTrace) => Exception('Captured error: $exception and $stackTrace'), // optional
    onFinally: () => print('finished'), // optional
  );
  // or
  // Use Do states directly with a common try/catch
  try {
    final result = doSomething();
    return Do.success(result);
  } catch (e) {
    return Do.failure(e);
  } finally {
    print('finished');
  }

  // STATE HANDLING
  // Handle all Do states with when:
  result.when(
    onInitial: () => print('Initial State'), // optional
    onLoading: () => print('Loading...'),
    onSuccess: (value) => print('Success: $value'),
    onFailure: (failure) => print('Failure: $failure'),
  );
  // or
  // Handle only Do.success and Do.failure with fold:
  result.fold(
    onFailure: (failure) => print('Failure: $failure'),
    onSuccess: (value) => print('Success: $value'),
  );
  // or
  // Handle specific states with maybeWhen:
  final output = result.maybeWhen(
    onInitial: () => 'Initial', // optional
    onLoading: () => 'Loading', // optional
    onSuccess: (value) => 'Success: $value', // optional
    onFailure: (failure) => 'Failure: $failure', // optional
    orElse: () => 'Else' // optional
  );
  // or
  // Just use a simple if statement:
  if (result.isInitial) {}
  if (result.isLoading) {}
  if (result.isSuccess) {} 
  if (result.isFailure) {}
  
  // RETURN STATEMENTS
  // So with custom failure type
  final So<MyCustomFailure, int> result = Do.success(42);
  
  // So with failure fixed exception type
  final SoException<String> result2 = Do.success('String');
}

πŸ“¦ Available States

// Do
Do.initial();            // Represents the initial state
Do.loading();            // Represents a loading state
Do.success(value);       // Represents a success with the associated value [S]
Do.failure([failure]);   // Represents a failure with optional failure [F]

// So
So<F, S>                 // Represents a return statement with a failure of type F and a value of type S
SoException<S>           // Represents a return statement with a fixed failure type of Exception and a value of type S

πŸ”‘ Core Methods

getOrElse(S defaultValue)

Returns the value in Do.success, or the fallback value if it is not available.

final value = Do.success(42).getOrElse(0); // 42

map<T>(T Function(S value))

Transforms the success value into another value.

final result = Do.success(42).map((value) => value.toString()); // Do<String>.success('42')

flatMap<T>(Do<T, F> Function(S value))

Maps to another Do state.

final result = Do.success(42).flatMap((value) => Do.success(value.toString()));

fold<T>

Handle success and failure.

final message = Do.success(42).fold(
  onFailure: (failure) => 'Error: $failure',
  onSuccess: (value) => 'Success: $value',
);

when<T>

Handle all states.

final output = result.when(
  onInitial: () => 'Initial',
  onLoading: () => 'Loading...',
  onSuccess: (value) => 'Success: $value',
  onFailure: (failure) => 'Failure: $failure',
);

maybeWhen<T>

Handle specific states with orElse.

final result = Do.success(42);
final output = result.maybeWhen(
  onSuccess: (value) => 'Success: $value', // optional
  orElse: () => 'Default', // optional
);

print(output); // Output: Success: 42

tryCatch

Wraps synchronous/async calls in a Do, automatically catching exceptions.

final result = await Do.tryCatch(
  onTry: () async => 42,
  onCatch: (exception, stackTrace) => Exception('Handled: $exception and $stackTrace'),
  onFinally: () => print('Done'),
);

πŸ“š Use in Application Layers

βœ… Data Source

SoException<int> getOk() => Do.tryCatch(onTry: () => http.get('/ok'));

βœ… Repository

SoException<String> getOk() async {
  final result = await dataSource.getOk();
  return result.map((code) => code.toString());
}

or

So<CustomFailure, String> getOk() async {
  final result = await dataSource.getOk();
  return result.flatMap((code) {
    if (code == is2XX() || code == is3XX()) {
      return Do.success(code.toString());
    } else {
      return Do.failure(CustomFailure('Error: $code'));
    }
  });
}

βœ… Cubit

class MyCubit extends Cubit<Do<Exception, String>> {
  MyCubit(this.repo) : super(Do.initial());
  
  final Repository repo;

  Future<void> getData() async {
    emit(Do.loading());
    
    final result = await repo.getOk();
    result.fold(
      onFailure: (failure) => emit(Do.failure(failure)),
      onSuccess: (value) => emit(Do.success('Success: $value')),
    );
  }
}

βœ… UI

BlocBuilder<MyCubit, Do<Exception, String>>(
  builder: (context, state) => state.when(
    onInitial: () => Text('Initial'),
    onLoading: () => CircularProgressIndicator(),
    onSuccess: (value) => Text(value),
    onFailure: (failure) => Text(failure.toString()),
  ),
)

Explore a practical example: DoSo Example


⚠️ When to Use DoSo

DoSo is ideal for simple screen states or flows with a clear success/failure logic. You can combine it with other libraries like flutter_bloc or provider for state management.

If your state grows in complexity (e.g., multiple fields, nested properties), you might want to adopt a more traditional and robust approach like using manually crafted classes or more advanced tools.


DoSo is not:

  • A replacement for functional programming libraries like dartz or fpdart. It is a simple and elegant solution for handling states and errors in a functional way, without the need follow entire functional approach.
  • A replacement for state management libraries like flutter_bloc or provider. It is a lightweight library that can be used in conjunction with these libraries to simplify state and error handling.

πŸ”— More Info & Contribute

Check out the full implementation, open issues, and contribute on GitHub: DoSo on GitHub


Libraries

doso