In the transition to a new technology, we lose the usual tools for development. In some cases, we are forced to accept their absence due to some technical limitations, but if possible we carry the tools with us. Developing android applications, I took as an example the pure architecture proposed by Fernando Cejas. Understanding the design patterns used in Flutter, I decided to abandon this architecture in favor of BLoC. I quickly got used to this pattern, it is very similar to MVVM, which I worked with earlier, but I didn’t want to put up with one detail. When calling the repository methods, I had to catch exceptions, cast them to some type and, in accordance with the type, create the necessary state. In my opinion, this is very cluttering up the block and I ported the Either type used earlier in android projects based on Fernando.
Either came from functional programming languages. It provides the value of one of the possible types:
/// Signature of callbacks that have no arguments and return right or left value. typedef Callback<T> = void Function(T value); /// Represents a value of one of two possible types (a disjoint union). /// Instances of [Either] are either an instance of [Left] or [Right]. /// FP Convention dictates that: /// [Left] is used for "failure". /// [Right] is used for "success". abstract class Either<L, R> { Either() { if (!isLeft && !isRight) throw Exception('The ether should be heir Left or Right.'); } /// Represents the left side of [Either] class which by convention is a "Failure". bool get isLeft => this is Left<L, R>; /// Represents the right side of [Either] class which by convention is a "Success" bool get isRight => this is Right<L, R>; L get left { if (this is Left<L, R>) return (this as Left<L, R>).value; else throw Exception('Illegal use. You should check isLeft() before calling '); } R get right { if (this is Right<L, R>) return (this as Right<L, R>).value; else throw Exception('Illegal use. You should check isRight() before calling'); } void either(Callback<L> fnL, Callback<R> fnR) { if (isLeft) { final left = this as Left<L, R>; fnL(left.value); } if (isRight) { final right = this as Right<L, R>; fnR(right.value); } } } class Left<L, R> extends Either<L, R> { final L value; Left(this.value); } class Right<L, R> extends Either<L, R> { final R value; Right(this.value); }
The implementation is quite basic, inferior to solutions in other languages, but copes with its task. I use this type as the result of all repository methods, and the exception handling is transferred to the data layer. This eliminates the block from try / catch constructions, due to which the code becomes more readable.
class ContactBloc { final ContactRepository contactRepository; ContactBloc(this.contactRepository); @override Stream<ContactState> mapEventToState(ContactEvent event) async* { if (event is GetContactEvent) { yield LoadContactState(); try { var contact = contactRepository.getById(event.id); yield ContactIsShowingState(contact); } on NetworkConnectionException catch (e) { yield NetworkExceptionState(e); } catch (e) { yield UnknownExceptionState(e); } } } } abstract class ContactRepository { Future<Contact>getById(int id); }
class ContactBloc { final ContactRepository contactRepository; ContactBloc(this.contactRepository); @override Stream<ContactState> mapEventToState(ContactEvent event) async* { if (event is GetContactEvent) { yield LoadContactState(); final either = contactRepository.getById(event.id); if (either.isRight) { final contact = either.right; yield ContactIsShowingState(contact); } else { final failure = either.left; if (failure is NetworkFailure) yield NetworkFailureState(failure); if (failure is UnknownFailure) yield UnknownFailureState(failure); } } } } abstract class ContactRepository { Future<Either<Failure, Contact>>getById(int id); }
With regard to readability, someone might argue. Perhaps someone is accustomed to try / catch and will be right in its own right, for the most part it is taste. An additional advantage is that we ourselves can define the failure hierarchy and return it to the left. Let's say to make an abstract Failure, from it to make ServerFailure common to all features, NetworkFailure and any specific ContactFailure features for the current one, with the heirs. In the block, we will know exactly which of the failures to expect.
The downside to the implementation of Failure on Dart is the lack of sealed classes as in kotlin, otherwise there would not be these ifs with casting. The language is young, actively developing and I hope that the time will come and we will have the tools to write handlers more concisely.
Someone may not like this implementation, perhaps it will consider it meaningless, but I just wanted to familiarize you with the possibility of a functional approach to error handling in Dart, although the use did not work out as elegant as other languages.
Resources:
Source: https://habr.com/ru/post/459757/
All Articles