retrofit.dart icon indicating copy to clipboard operation
retrofit.dart copied to clipboard

Support Flavors (different environments)

Open saveKenny opened this issue 2 years ago β€’ 4 comments

I need to support two different environments (different baseUrl). Retrofit code generation is based on constant baseUrl.

It would be great to have the ability to generate code that will use a dynamic baseUrl, which will be provided on run time based on the flavor.

saveKenny avatar Aug 01 '22 09:08 saveKenny

You can pass URL by parameter, here is how I do it:

@RestApi()
@Injectable(as: UnifiApi)
abstract class RestUnifiApi implements UnifiApi {
  @factoryMethod
  factory RestUnifiApi(Dio dio, AppConfig appConfig) =>
    _RestUnifiApi(dio, baseUrl: appConfig.apiUrl);
  
  ...
}

leoshusar avatar Aug 02 '22 16:08 leoshusar

Hey @leoshusar thanks for replying. Is there any better solution for Bloc?

saveKenny avatar Aug 03 '22 13:08 saveKenny

Also, I would appreciate it if you could explain a little more about your solution.

saveKenny avatar Aug 03 '22 13:08 saveKenny

No problem!

I am using dependency injection for everything (things not related to Flutter Widgets, I use Provider for blocs), for DI I use injectable package.

First I load and register my config:

  @singleton
  @preResolve
  Future<RawConfig> config() async {
    final envMap = await EnvLoader.load(directory: 'assets/env');
    return RawConfig.from(envMap);
  }

this loads either .env or .env.dev asset depending on if I'm in release or debug mode; it also supports platform environment and --dart-define for non-web applications.

Then I have concrete AppConfig class that has injected RawConfig and exposes strongly typed properties instead of string map:

@singleton
class AppConfig extends _$AppConfig {
  AppConfig(super.config);

  String get apiUrl => _config['API_URL']!;
}

Finally I can inject AppConfig everywhere I want, including my retrofit services. Then I inject them to blocs.

Also injectable has support for different environments, you can register main service for release and mock service for e.g. tests.

leoshusar avatar Aug 03 '22 18:08 leoshusar

Hey @leoshusar! Thanks for the support. I just came back from a vacation.

I still don't understand how you use this method to generate requests using Retrofit. The Retrofit generated file should adapt to different environments (the baseUrl should change base the flavor), and I think that the above example does not solve this issue.

Can you please provide me a link to source code which uses your method? Because I just can't find one (also in your public repos 😜). I want to see the full picture of using injectable with Retrofit.

saveKenny avatar Aug 13 '22 17:08 saveKenny

I would appreciate if you could post an example that uses Retrofit and injectable using different environments πŸ™

saveKenny avatar Aug 13 '22 17:08 saveKenny

Hi @saveKenny, hope you had a great time!

I just made very simple project which shows how I use Retrofit + Injectable, this should be enough to understand my main concept.

There is a lot of annotation and generator magic, it's kinda similar to Java world (only those annotations, Java uses reflection for all of that). But I'm not a Java dev eitherπŸ˜… If you need anything to clear up, I can try to explain.

leoshusar avatar Aug 13 '22 19:08 leoshusar

@leoshusar Thanks a lot!! Great example! πŸš€

saveKenny avatar Aug 13 '22 20:08 saveKenny

@leoshusar In case I'm using Bloc, I think I don't need to use GetIt for DI, since I can just use the RepositoryProvider for that.

saveKenny avatar Aug 14 '22 08:08 saveKenny

Yeah, that's an option too, but you need BuildContext for accessing services.

I personally like GetIt more, when I have multiple services where each depends on another service, I find it better to not clutter my widget tree with it.

I have for example dialog service, notification service, form service, router service, user API service, order API service, order service, auth service... I can simply have Dio registered as singleton, I can simply inject auth service into Dio auth interceptor.

Also Injectable package let's you define filters, so you can inject service A in release mode and mock service when you test something, for example.

Some of it might be overkill, but in the end I find it cleaner than using multiple Providers.

leoshusar avatar Aug 14 '22 09:08 leoshusar

Hey @leoshusar, Can I support flavors in retrofit for Flutter Packages? I want to create a separate package for the ApiProvider (which will use retrofit with different base URLs based on the running env).

saveKenny avatar Aug 15 '22 08:08 saveKenny

Hey @leoshusar, Can I support flavors in retrofit for Flutter Packages? I want to create a separate package for the ApiProvider (which will use retrofit with different base URLs based on the running env).

In other words, can I use the injectable configuration in my main project, while using retrofit generation in a different package?

Checkout my desirable folder scheme:

Screen Shot 2022-08-15 at 11 13 36

saveKenny avatar Aug 15 '22 08:08 saveKenny

You want to import those packages via pubspec? Then it's unfortunately not supported now, but there is open issue for that even with some PoC. However the author is currently not that active, so who knows when it will be supported.

But you can put your injections from other packages into main @module class:

@module
abstract class RegisterModule {
  ApiProvider get apiProvider => ApiProviderImpl();
}

Just a detail: you usually name abstract classes without any suffix like Abs (just ApiProvider) and then concretize implementations, like MockApiProvider - or sometimes Impl (brought from Java world I think) :) But of course do as you like.

leoshusar avatar Aug 15 '22 08:08 leoshusar

Putting my packages into the main module sounds as a really good ides. I also take your advice for abstract names convention. Thanks a lot. πŸ™

saveKenny avatar Aug 15 '22 15:08 saveKenny

Hey @leoshusar! I have a question regarding your simple example project. I can see that you chose to use factory instead of singleton in the rest_user_service.dart. Why is that?

In my case, I'm getting the app access token from an external server. After getting the access token, I need to update the Dio I'm passing to my RestApiProvider. In that case, I need the RestApiProvider update to reflect all its pointers. In other words, I need it to be a singleton.

How do I convert your rest_user_service.dart class from factory to singleton using retrofit? Any ideas for a better solution? Thanks in advance πŸ™

saveKenny avatar Aug 25 '22 15:08 saveKenny

Hi, changing the annotation @Injectable to @Singleton should be enough. The @factoryMethod annotation just tells injectable to use it for constructing the object.

I'm using non singleton API services because they usually don't need to hold any state. For auth I have AuthService - this one is registered as singleton - and then use Dio interceptor for inserting token from auth service to all requests.

In RegisterModule something like:

@singleton
Dio dio(AuthService authService) {
  final dio = Dio()
    ..interceptors.add(AuthInterceptor(authService));

  return dio;
}

leoshusar avatar Aug 25 '22 16:08 leoshusar

Hi @leoshusar thanks for the great answer, again.

What is the purpose of AuthService? Is it only responsible holding the token state? Or does it also support other functionalities like retrieving new token in case of expiration?

According to this StackOverflow post, the interceptor handles expired tokens.

Can you please provide me a link to an AuthService implementation?

saveKenny avatar Aug 27 '22 11:08 saveKenny

You're welcome.

My auth service is an abstraction around platform specific OAuth2 flows (AppAuth on Android, new window on web...). It has methods for login, logout, it takes care of saving credentials and tracking token expiration, and then finally Future<String> getToken() which always gets fresh token (either current or new by calling refresh internally).

I don't like the SO answer which suggests tracking token status inside interceptor, it's Dio dependent and it should belong into the Auth service IMO.

I'm sure you can figure out your own auth implementation :)

leoshusar avatar Aug 27 '22 12:08 leoshusar