web-dev-qa-db-fra.com

Android Dagger2 + OkHttp + Erreur de cycle de dépendance Retrofit

Salut, j’utilise Dagger2, Retrofit et OkHttp et j’ai un problème de cycle de dépendance.

En fournissant OkHttp

@Provides
@ApplicationScope
OkHttpClient provideOkHttpClient(TokenAuthenticator auth,Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .authenticator(auth)
            .dispatcher(dispatcher)
            .build();
}

En fournissant Retrofit:

@Provides
@ApplicationScope
Retrofit provideRetrofit(Resources resources,Gson gson, OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}

En fournissant APIService:

@Provides
@ApplicationScope
APIService provideAPI(Retrofit retrofit) {
    return retrofit.create(APIService.class);
}

Mon interface APIService:

public interface  APIService {
@FormUrlEncoded
@POST("token")
Observable<Response<UserTokenResponse>> refreshUserToken();

--- other methods like login, register ---

}

Ma classe TokenAuthenticator:

@Inject
public TokenAuthenticator(APIService mApi,@NonNull ImmediateSchedulerProvider mSchedulerProvider) {
    this.mApi= mApi;
    this.mSchedulerProvider=mSchedulerProvider;
    mDisposables=new CompositeDisposable();
}

@Override
public  Request authenticate(Route route, Response response) throws IOException {

    request = null;

    mApi.refreshUserToken(...)
            .subscribeOn(mSchedulerProvider.io())
            .observeOn(mSchedulerProvider.ui())
            .doOnSubscribe(d -> mDisposables.add(d))
            .subscribe(tokenResponse -> {
                if(tokenResponse.isSuccessful()) {
                    saveUserToken(tokenResponse.body());
                    request = response.request().newBuilder()
                            .header("Authorization", getUserAccessToken())
                            .build();
                } else {
                    logoutUser();
                }
            },error -> {

            },() -> {});

    mDisposables.clear();
    stop();
    return request;

}

Mon logcat:

Error:(55, 16) error: Found a dependency cycle:
com.yasinkacmaz.myapp.service.APIService is injected at com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideTokenAuthenticator(…, mApi, …)
com.yasinkacmaz.myapp.service.token.TokenAuthenticator is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideOkHttpClient(…, tokenAuthenticator, …)
okhttp3.OkHttpClient is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideRetrofit(…, okHttpClient)
retrofit2.Retrofit is injected at
com.yasinkacmaz.myapp.darkvane.modules.NetworkModule.provideAPI(retrofit)
com.yasinkacmaz.myapp.service.APIService is provided at
com.yasinkacmaz.myapp.darkvane.components.ApplicationComponent.exposeAPI()

Ma question est donc la suivante: Ma classe TokenAuthenticator dépend de APIService mais je dois fournir TokenAuthenticator lors de la création de APIService. Cela provoque une erreur de cycle de dépendance. Comment puis-je éviter cela, y a-t-il quelqu'un qui est confronté à ce problème? Merci d'avance.

12
Yasin Kaçmaz

Votre problème c'est: 

  1. Votre OKHttpClient dépend de votre authentificateur
  2. Votre authentificateur dépend d'un service de modernisation
  3. Le rattrapage dépend d’un OKHttpClient (comme au point 1)

D'où la dépendance circulaire.

Une solution possible ici est que votre TokenAuthenticator dépende de APIServiceHolder plutôt que de APIService. Ensuite, votre TokenAuthenticator peut être fourni en tant que dépendance lors de la configuration de OKHttpClient, que la APIService (située plus bas dans le graphe d'objet) ait été instanciée ou non. 

Un APIServiceHolder très simple:

public class APIServiceHolder {

    private APIService apiService;

    @Nullable
    APIService apiService() {
        return apiService;
    }

    void setAPIService(APIService apiService) {
        this.apiService = apiService;
    }
}

Puis refactifiez votre TokenAuthenticator:

@Inject
public TokenAuthenticator(@NonNull APIServiceHolder apiServiceHolder, @NonNull ImmediateSchedulerProvider schedulerProvider) {
    this.apiServiceHolder = apiServiceHolder;
    this.schedulerProvider = schedulerProvider;
    this.disposables = new CompositeDisposable();
}

@Override
public  Request authenticate(Route route, Response response) throws IOException {

    if (apiServiceHolder.get() == null) {
         //we cannot answer the challenge as no token service is available

         return null //as per contract of Retrofit Authenticator interface for when unable to contest a challenge
    }    

    request = null;            

    TokenResponse tokenResponse = apiServiceHolder.get().blockingGet()

    if (tokenResponse.isSuccessful()) {
        saveUserToken(tokenResponse.body());
        request = response.request().newBuilder()
                     .header("Authorization", getUserAccessToken())
                     .build();
    } else {
       logoutUser();
    }

    return request;
}

Notez que le code pour récupérer le jeton doit être synchrone . Cela fait partie du contrat de Authenticator. Le code à l'intérieur de la Authenticator sera exécuté off le thread principal.

Bien sûr, vous devrez écrire les méthodes @Provides pour les mêmes:

@Provides
@ApplicationScope
apiServiceHolder() {
    return new APIServiceHolder();
}

Et refactoriser les méthodes du fournisseur:

@Provides
@ApplicationScope
APIService provideAPI(Retrofit retrofit, APIServiceHolder apiServiceHolder) {
    APIService apiService = retrofit.create(APIService.class);
    apiServiceHolder.setAPIService(apiService);
    return apiService;
}

Notez que l'état global mutable n'est généralement pas une bonne idée. Toutefois, si vos packages sont bien organisés, vous pourrez peut-être utiliser les modificateurs d'accès de manière appropriée pour éviter toute utilisation involontaire du détenteur.

19
David Rawson

Un grand merci à @Selvin et @David. J'ai deux approches, l'une est la réponse de David et l'autre est:

Créer une autre bibliothèque OkHttp ou Retrofit ou une autre bibliothèque qui gérera nos opérations dans la classe TokenAuthenticator.

Si vous souhaitez utiliser une autre instance OkHttp ou Retrofit, vous devez utiliser une annotation de qualificateur.

Par exemple :

@Qualifier
public @interface ApiClient {}


@Qualifier
public @interface RefreshTokenClient {}

puis fournir: 

@Provides
@ApplicationScope
@ApiClient
OkHttpClient provideOkHttpClientForApi(TokenAuthenticator tokenAuthenticator, TokenInterceptor tokenInterceptor, Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .authenticator(tokenAuthenticator)
            .addInterceptor(tokenInterceptor)
            .dispatcher(dispatcher)
            .build();
}

@Provides
@ApplicationScope
@RefreshTokenClient
OkHttpClient provideOkHttpClientForRefreshToken(Dispatcher dispatcher){
    return new OkHttpClient.Builder()
            .connectTimeout(Constants.CONNECT_TIMEOUT, TimeUnit.SECONDS)
            .readTimeout(Constants.READ_TIMEOUT,TimeUnit.SECONDS)
            .writeTimeout(Constants.WRITE_TIMEOUT,TimeUnit.SECONDS)
            .dispatcher(dispatcher)
            .build();
}

@Provides
@ApplicationScope
@ApiClient
Retrofit provideRetrofitForApi(Resources resources, Gson gson,@ApiClient OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}

@Provides
@ApplicationScope
@RefreshTokenClient
Retrofit provideRetrofitForRefreshToken(Resources resources, Gson gson,@RefreshTokenClient OkHttpClient okHttpClient){
    return new Retrofit.Builder()
            .baseUrl(resources.getString(R.string.base_api_url))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(okHttpClient)
            .build();
}

Ensuite, nous pouvons fournir nos interfaces séparées:

@Provides
@ApplicationScope
public APIService provideApi(@ApiClient Retrofit retrofit) {
    return retrofit.create(APIService.class);
}

@Provides
@ApplicationScope
public RefreshTokenApi provideRefreshApi(@RefreshTokenClient Retrofit retrofit) {
    return retrofit.create(RefreshTokenApi.class);
}

En fournissant notre TokenAuthenticator:

@Provides
@ApplicationScope
TokenAuthenticator provideTokenAuthenticator(RefreshTokenApi mApi){
    return new TokenAuthenticator(mApi);
}

Avantages: Vous avez deux interfaces API séparées, ce qui signifie que vous pouvez les maintenir de manière indépendante. Vous pouvez aussi utiliser plain OkHttp ou HttpUrlConnection ou une autre bibliothèque.

Inconvénients: vous aurez deux instances différentes OkHttp et Retrofit.

P.S: Assurez-vous de faire des appels synchrones dans la classe Authenticator.

1
Yasin Kaçmaz

Vous pouvez injecter la dépendance de service dans votre authentificateur via le type Lazy. De cette façon, vous éviterez la dépendance cyclique à l'instanciation.

Vérifiez ceci link sur le fonctionnement de Lazy.

0
max