Closed konstantin-doncov closed 4 years ago
You just forgot to type Right and Left. Try this:
Either<Failure, MyResponse> result = await restClient.request() // Future<MyResponse> request();
.then((response) => Right<Failure, MyResponse>(response))
.catchError((failure) => Left<Failure, MyResponse>(Failure()));
@torbenkeller thanks!
@torbenkeller it's really strange, but I'm trapped in the similar situation :). Now I have a runtime exeption:
type 'Left<Failure, MyResponse>' is not a subtype of type 'FutureOr<Right<Failure, MyResponse>>'
When your code goes into cathcError(...)
.
How can I fix it?
I don't know exactly why converting results of Futures to Eithers doesnt work as expected but there is the Task
Object in the package which is solving this for you. Try this:
Task<MyResponse>(() => restClient.request())
.attempt() //returns Task<Either<Object, MyResponse>>
//Now you have to convert Task<Either<Object, MyResponse>> to Task<Either<Failure, MyResponse>>
.map(
(Either<Object, MyResponse> either) => either.leftMap((Object obj) {
try {
return obj as Failure;
} catch (e) {
throw obj;
}
}),
)
// Now you have to convert it to a Future<Either<Failure, MyResponse>> again
.run();
But this map function is boilerplate code so you can outsource it to an extension. Try this:
extension TaskX<T extends Either<Object, dynamic>> on Task<T> {
Task<Either<Failure, A>> mapLeftToFailure<A>() {
return map<Either<Failure, A>>((Either<Object, dynamic> either) =>
either.fold<Either<Failure, A>>((Object obj) {
try {
return Left<Failure, A>(obj as Failure);
} catch (e) {
throw obj;
}
}, (dynamic u) {
try {
return Right<Failure, A>(u as A);
} catch (e) {
throw u;
}
}));
}
}
You can use it like this:
import'###extension.dart###';
...
Task<MyResponse>(() => restClient.request())
.attempt()
.mapLeftToFailure<MyResponse>()
.run();
@torbenkeller many thanks for this extension! I generalized it a bit for my needs:
extension TaskX<T extends Either<Object, dynamic>> on Task<T> {
Task<Either<Failure, A>> mapLeftToFailure<A>({@required Function onLeft, @required Function onRight}) {
return map<Either<Failure, A>>((Either<Object, dynamic> either) =>
either.fold<Either<Failure, A>>((Object obj) {
try {
return onLeft(obj);
} catch (e) {
throw obj;
}
}, (dynamic u) {
try {
return onRight(u as A);
} catch (e) {
throw u;
}
}));
}
}
So now I can do this:
Task<MyResponse>(() => restClient.request())
.attempt()
.mapLeftToFailure<MyResponse>(
onLeft: (error) {
//do something and return Left
},
onRight: (response) {
//do something and return Right
})
.run();
But let's suppose I want to call and return in onLeft
some nested function which returns Future<Either<Failure, MyResponse>>
, if so then I need to await
:
onLeft: (error) async {
return await requestOneMoreTime();
},
But now I also need to await
in the my extension:
return await onLeft(obj);
So, I need to mark it as async
and so on... But I don't know how to do it correctly and elegantly with mapLeftToFailure()
and Task<MyResponse>
.
Please, can you help me with this?
I would not recommend changing the extension. It is against the idea why you want to use an Either<Failure, MyResponse>
.
The Idea is you have runtime exceptions and specific cases like "No Data Selected" and create Failures from it. You use Failures to be independent of the "data selection layer". So when you create the Either the "data selection layer" is already throwing Failures and not HTTP Response Exceptions for example. And when you create the Either you don't want to be any buisness logic there.
In code it would be like this:
import 'package:http/http.dart' as http;
class RestClient {
...
Future<MyResponse> request() {
final Response response = await http.get(...);
switch (response.statusCode) {
case 200:
return MyResponse(response.body);
case 404:
throw ...Failure();
...
}
throw Failure();
}
}
@torbenkeller you are right. But my restClient.request()
is generated by the Retrofit method, so I can't do anything inside this method. It returns response or DioError
which I need to handle(as your switch
inside request()
), so I created a function Future<Either<Failure, dynamic>> _processNetworkErrors(dynamic error)
which I want to call inside onLeft
.
Then write a wrapper function around the request function like this:
Future<MyResponse> requestWrapper() async {
try {
return await http.get(...);
} on ...Exception {
throw ...Failure();
}
}
@torbenkeller unfortunately, I didn't get your whole idea.
I have RemoteDataSource
with Retrofit Api:
abstract class RemoteDataSource {
...
@GET("/request")
Future<MyResponse> request(); //only declaration
...
}
And NetworkRepository:
class NetworkRepository{
...
Future<Either<Failure, MyResponse>> request([bool isNeedToRecall = true]) async {
Either<Failure, MyResponse> failOrResult = await _callAndProcessErrors<MyResponse>(
functionToRecall: isNeedToRecall ? ()=> request(false) : null,
request: () => remoteDataSource.request());
return failOrResult;
}
Future<Either<Failure, A>> _callAndProcessErrors<A>({ @required Future<A> request(), @required Future<Either<Failure, dynamic>> functionToRecall()}) async {
Either<dynamic, A> errorOrResult = await Task<A>(() => request())
.attempt()
.mapLeftAndRight<dynamic, A>(
onLeft: (error) => Left<dynamic,A>(error),
onRight: (response) => Right<dynamic,A>(response))
.run();
return errorOrResult.fold(
(error) async {
return await _processNetworkErrors(error, functionToRecall);
},
(result) => Right<Failure, A>(result));
}
Future<Either<Failure, dynamic>> _processNetworkErrors(dynamic error, Future<Either<Failure, dynamic>> functionToRecall()) async {
if(error is DioError){
switch(error.response.statusCode){
case 401:
//unauthorized
//so let's try to refreshToken()
//and if it's ok and functionToRecall != null
return functionToRecall() ?? Left<Failure, dynamic>(...) ;//recall the first request() one more time(but no more)
case 403:
...
}
}
return Left<Failure, dynamic>(...);
}
}
Here is an example of the case when I call the request, get 401, refresh access token, and call the first request again.
How do you propose to do this and similar tasks?
Hello, I didn't read through the whole conversation here but I think you may find these extension methods useful. Since I'm not an FP pro by any means, they're probably badly named but they serve the purpose well. Basically, it's an EitherT
type specifically for Futures
.
import 'package:dartz/dartz.dart';
extension FutureEither<L, R> on Future<Either<L, R>> {
Future<Either<L, R2>> flatMap<R2>(Function1<R, Future<Either<L, R2>>> f) {
return then(
(either1) => either1.fold(
(l) => Future.value(left<L, R2>(l)),
f,
),
);
}
Future<Either<L, R2>> map<R2>(Function1<R, Either<L, R2>> f) {
return then(
(either1) => either1.fold(
(l) => Future.value(left<L, R2>(l)),
(r) => Future.value(f(r)),
),
);
}
// TODO: Find an official FP name for mapping multiple layers deep into a nested composition
Future<Either<L, R2>> nestedMap<R2>(Function1<R, R2> f) {
return then(
(either1) => either1.fold(
(l) => Future.value(left<L, R2>(l)),
(r) => Future.value(right<L, R2>(f(r))),
),
);
}
Future<Either<L2, R>> leftMap<L2>(Function1<L, L2> f) {
return then(
(either1) => either1.fold(
(l) => Future.value(left(f(l))),
(r) => Future.value(right<L2, R>(r)),
),
);
}
}
These allow you to flatMap
, map
, leftMap
and "nestedMap
" (over the inner Either
) with ease. Then, just await
the whole expression.
@ResoDev, great stuff! Thank you for helping! This approach to getting some of the functionality of monad transformers looks really promising -- i'll probably take some inspiration from this once I'm able to drop Dart 1 support :-)
Thank you also @torbenkeller for helping out!
I'll close this ticket for now, since it looks a bit too open ended, but feel free to re-open if you feel the need.
@ResoDev: Oh, and if one chooses to view your extensions as forming a composed monad, it is probably the current map
that should be renamed (attemptMap
, maybe?). The current nestedMap
corresponds more closely to map
from a Functor law perspective.
Inspired by ResoDev, I wrote something like this Future Option:
import 'package:dartz/dartz.dart';
extension FutureOption<A> on Future<Option<A>> {
Future<Option<B>> flatMap<B>(Function1<A, Future<Option<B>>> f) {
return then(
(option) => option.fold(
() => Future.value(none<B>()),
f,
),
);
}
Future<Option<B>> attemptMap<B>(Function1<A, Option<B>> f) {
return then(
(option) => option.fold(
() => Future.value(none<B>()),
(r) => Future.value(f(r)),
),
);
}
Future<Option<B>> nestedMap<B>(Function1<A, B> f) {
return then(
(option) => option.fold(
() => Future.value(none<B>()),
(r) => Future.value(some(f(r))),
),
);
}
Future<Either<L2, A>> toEither<L2>(L2 l) {
return then(
(option) => option.fold(
() => Future.value(left<L2, A>(l)),
(r) => Future.value(right<L2, A>(r)),
),
);
}
}
Why we can't have this on docs? Actually, this package have any docs somewhere?
@POST("apiPrefix")
Future
_client.initLoginApi(fields).then((value) async { LoginModel model = value; if(model.statusCode == 200) { view.onSuccessSignIn(value); final prefs = await SharedPreferences.getInstance(); prefs.setString('accessToken', model.data?.access_token ?? ""); prefs.setString('driverId', model.data?.dataData?.id ?? ""); prefs.setString('driverName', '${model.data?.dataData?.Name} ${model.data?.dataData?.lastName}' ?? ""); prefs.setString('driverEmail', value.data?.dataData?.email ?? "");
}else if(model.statusCode == 400){
print("$tag fdsjlfjsdlkfj");
print("$tag handle response:--- ${value.message} ");
view.onError("catch error print");
}
}).catchError((Object obj) {
switch (obj.runtimeType) {
case DioError:
final res = (obj as DioError).response;
print("Got error : ${res?.statusCode} -> ${res?.statusMessage}");
print("dioerror :-- ${res?.data["message"]}");
break;
default:
break;
}
});
flutter catachError not working.
I want to use
Either
in this way:But it seems that I can't do this:
I necessarily need to use explicit type casting, like this:
even if I already return
Left(Failure())
in thecatchError()
. So, is explicit casting the most concise and elegant solution?