encore icon indicating copy to clipboard operation
encore copied to clipboard

Dart/Flutter client generation (DIO)

Open valpetli opened this issue 2 years ago • 10 comments

Our front-end is a Flutter application and it'd be awesome to add null-safe strongly typed Dart client generation to Encore. We can provide example client as well as open a PR to implement this feature.

We'd like to use Dio as a base client and we could start with implementing dart-dio generation and treat other implementations as separate languages as suggested by @eandre or try to come up with a more generic solution.

Additionally the resulting class can accept URL instead of environment name to handle custom URLs, PR and local environments.

How does that sound? 🙏

valpetli avatar Mar 16 '22 13:03 valpetli

This sounds great! After some consideration I think a dio-specific generator is going to be easier to manage going forward than a pluggable one. What do you think?

eandre avatar Mar 24 '22 16:03 eandre

Sounds good @eandre, we'll create the example shortly then!

valpetli avatar Mar 24 '22 16:03 valpetli

@eandre We finally have a draft for the dart client:

  • It uses named function parameters where possible, as opposed to objects
  • Output classes provide immutability & comparability (heavily inspired by freezed)
  • URL, token and error parsing/handling is expected to be done via Dio functionality
  • Dart does not allow conflicting fields names and type names (e.g. Foo? Foo) and therefore fields are always lowercased
  • We also consider lowercasing method names to follow common Dart practices
import 'package:dio/dio.dart';

abstract class Client {
  Client(Dio dio) : serviceClient = ServiceClient(dio);

  final ServiceClient serviceClient;
}

class ServiceClient {
  ServiceClient(this.dio);

  final Dio dio;

  Future<void> DummyAPI(
      {required String boo, Foo? Foo, required dynamic Raw}) async {
    await dio.post<void>('/svc.DummyAPI', data: <String, dynamic>{
      'boo': boo,
      if (Foo != null) 'foo': Foo,
      'raw': Raw
    });
  }

  Future<void> Get({required String Baz}) async {
    await dio.get<void>('/svc.Get', queryParameters: <String, dynamic>{
      'boo': Baz,
    });
  }

  Future<void> RESTPath({required String a, required int b}) async {
    await dio.get<void>('/path/$a/$b');
  }

  Future<Tuple<bool, Foo>> TupleInputOutput(
      Tuple<String, WrappedRequest> params) async {
    final response =
        await dio.post<void>('/svc.TupleInputOutput', data: params);
    return Tuple<bool, Foo>.fromJson(response.data as Map<String, dynamic>);
  }
}

typedef Foo = num;

class Request {
  Request({this.foo, required this.boo, required this.raw});

  Foo? foo;
  String boo;
  dynamic raw;
}

class Wrapper<T> {}

class WrappedRequest<T> extends Wrapper<T> {}

const dynamic valueNotSet = 'valueNotSet';

class Tuple<A, B> {
  Tuple({required this.a, required this.b});

  final A a;
  final B b;

  Tuple<A, B> copyWith({
    A? a,
    B? b,
  }) =>
      _copyWith(a: a, b: b);

  Tuple<A, B> _copyWith({
    Object? a = valueNotSet,
    Object? b = valueNotSet,
  }) =>
      Tuple(
        a: (a == valueNotSet) ? this.a : (a as A),
        b: (b == valueNotSet) ? this.b : (b as B),
      );

  factory Tuple.fromJson(Map<String, dynamic> json) => Tuple(
        a: json['a'] as A,
        b: json['b'] as B,
      );

  Map<String, dynamic> toJson() => <String, dynamic>{'a': a, 'b': b};

   @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is Tuple &&
    runtimeType == other.runtimeType &&
    a == other.a &&
    b == other.b;

  @override
  int get hashCode => toJson().hashCode;
}

valpetli avatar Mar 31 '22 08:03 valpetli

Oh and I forgot to mention, that we might need to split generated code in multiple files (one per service + umbrella client) as this seems to be the only way of implementing namespaces in Dart.

valpetli avatar Mar 31 '22 17:03 valpetli

that we might need to split generated code in multiple files (one per service + umbrella client) as this seems to be the only way of implementing namespaces in Dart.

For the Go client, we've prefixed the structures with the service name, such that Request became SvcRequest and we had SvcFoo, would this work for Dart as well?

DomBlack avatar Apr 25 '22 09:04 DomBlack

@DomBlack Yeah that was a workaround we had in mind as well. The types would be have a stutter names like UsersUser or LikesLikes, but other than that it'll work ofc :-)

valpetli avatar Apr 25 '22 09:04 valpetli

+1

darthwade avatar Dec 22 '22 21:12 darthwade

Hey @darthwade, thanks for the feedback. Could you share a bit more about your use case? Thanks!

eandre avatar Dec 22 '22 22:12 eandre

Hey @darthwade, thanks for the feedback. Could you share a bit more about your use case? Thanks!

Hey @eandre, first of all, great job with the encore! We're planning to build an app in Flutter and currently I'm researching tools for easy backend infrastructure. Encore looks great, the only 2 things I'd like to have are Dart client generation & gRPC, but second is not critical.

darthwade avatar Dec 23 '22 07:12 darthwade

Thanks @darthwade, I'm really happy to hear that. If Flutter is critical for your use case we're happy to prioritize it. I'll send you an email to discuss further.

eandre avatar Dec 23 '22 16:12 eandre