I have 2 Retrofit Clients one of them default and second one has different implementation like (base URL, interceptors etc... )
i need to inject default client without using name reference
first client :-
single<Retrofit> {
Retrofit.Builder()
.baseUrl(RemoteConstants.BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(APIResponseConverter())
.addConverterFactory(GsonConverterFactory.create(get()))
.client(get())
.build()
}
Second Client:-
single<Retrofit>("retrofit_second") {
Retrofit.Builder()
.baseUrl("diffrent url")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(get()))
.client(get("SecondOkHttpClient"))
.build()
}
My way to inject
val myModule = module {
factory { get<Retrofit>().create(FirstAPI::class.java) } // Why Koin did not figure it without providing its default name !?
factory { get<Retrofit>("retrofit_second").create(SecondAPI::class.java) }
factory<IMyRemoteDataSource> { MyRemoteDataSource(get(), get()) }
factory<IMyRepository> { MyRepository(get()) }
factory { MyUseCase(get()) }
}
the result is :
Multiple definitions found for type 'class retrofit2.Retrofit' - Koin can't choose between :
Single [name='retrofit_second',class='retrofit2.Retrofit']
Single [name='Retrofit',class='retrofit2.Retrofit']
Why Koin did not get the default Retrofit instance without providing its default name (Retrofit ) !?
factory { get<Retrofit>().create(FirstAPI::class.java) }
You are right #Eslam. Current behavior (as of koin:1.0.2) is when you don't specify the dependency name, it is treated as an empty string. And then the definitions get filtered by class name:
fun searchByClass(clazz: KClass<*>): List<BeanDefinition<*>> {
return definitions.filter { clazz in it.classes }
}
As a result you get both of your definitions, which results in the above mentioned error: Multiple definitions for class ....
Related
I am making an api call using retrofit and I want to write a unit test to check if it returns an exception.
I want to force the retrofit call to return an exception
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
try {
val creditReport = apiServiceInterface.getDataFromApi() // THIS SHOULD RETURN AN EXCEPTION AND I WANT TO CATCH THAT
return CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
return CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
I have written a test case for getCreditReport which should validate the failure scenario
#Test
fun getCreditReportThrowException() {
runBlocking {
val response = dataRepository.getCreditReport()
verify(apiServiceInterface, times(1)).getDataFromApi()
Assert.assertEquals(CreditReportResponse.Status.FAILURE, response.status)
}
}
so to make the above test case pass, I need to force the network call to throw and exception
please suggest
Thanks
R
Actually #Vaibhav Goyal provided a good suggestion to make your testing as easier. Assuming you are using MVVM structure, in your test cases you can inject a "mock" service class to mock the behaviours that you defined in the test cases, so the graph will be like this
Since I am using mockk library at the moment, the actual implementation in your code base would be a little bit different.
#Test
fun test_exception() {
// given
val mockService = mockk<ApiServiceInterface>()
val repository = DataRepository(mockService)
every { mockService.getDataFromApi() } throws Exception("Error")
// when
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
}
But if you want to test the exception thrown from Retrofit, then you might need mockServer library from square to help you to achieve this https://github.com/square/okhttp#mockwebserver
And the graph for this would be like this
You also have to setup the mock server to do so
#Test
fun test_exception_from_retrofit() {
// can put in the setup method / in junit4 rule or junit5 class
val mockWebServer = MockWebServer()
mockWebServer.start()
// given
val service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.build()
.create(ApiServiceInterface::class)
val repository = DataRepository(service)
// when
mockWebServer.enqueue(MockResponse()
.setResponseCode(500)
.setBody("""{"name":"Tony}""") // you can read the json file content and then put it here
)
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
// can put in tearDown / in junit4 rule or juni5 class
mockWebServer.shutdown()
}
SO you can test different exception like json format invalid, 500 status code,data parsing exception
Bonus point
Usually I would put the testing json under test directory and make it almost same as the api path for better maintainence
I'm very new to DI. I want to change the base URL of Retrofit on run time using Koin.
As of now, the base URL is mentioned in gradle...The base URL is loaded by KOIN DI....consider if I want to change baseurl for different pages then how can I change the on run time using DI in retrofit?
For eg: the base URL is different for home page, section page, and video page..all have different base urls. How to do it on run time.
Here is my retrofit provider method,
private fun provideRetrofit(client: OkHttpClient): ClinicalCoreService {
val moshi = Moshi.Builder()
//TODO : fix with reflection
//.add(GenericCollectionAdapterFactory(ArrayList::class.java) { ArrayList() })
.build()
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
.create(ClinicalCoreService::class.java)
}
I am starting Koin in the Application class onCreate like this:
class ClinicCoreApplication : Application() {
override fun onCreate() {
ActivityLifecycleCallback.register(this)
super.onCreate()
sContext = applicationContext
init()
//initialize clever tap
CleverTapAPI.setDebugLevel(if (BuildConfig.DEBUG) CleverTapAPI.LogLevel.DEBUG else CleverTapAPI.LogLevel.OFF)
val cleaverTap = CleverTapAPI.getDefaultInstance(applicationContext)
CleverTapAPI.createNotificationChannel(sContext,PushReceiverService.PUSH_CHANNEL_ID,PushReceiverService.PUSH_CHANNEL_NAME,"Your Channel Description",NotificationManager.IMPORTANCE_MAX,true)
startKoin {
androidContext(this#ClinicCoreApplication)
modules(
listOf(
appModule,
networkModule,
dataMangerModule,
viewModelModule,
databaseModule
)
)
}
}
Here is the Koin network module,
val networkModule = module {
single { provideDefaultOkHttpClient(get(), get()) }
single { provideHttpLoggingInterceptor() }
single { provideRetrofit(get()) }
single<SectionApi>(named(DataSource.SECTION_REMOTE)) { (SectionRemoteSource(get())) } bind SectionApi::class
single<FaqApi>(named(DataSource.FAQ_REMOTE)) { (FaqRemoteSource(get())) } bind FaqApi::class
single<ExaminationApi>(named(DataSource.EXAMINATION_REMOTE)) { (ExaminationRemoteSource(get())) } bind ExaminationApi::class
single<SampleCaseApi>(named(DataSource.SAMPLE_CASE_REMOTE)) { (SampleCaseRemoteSource(get())) } bind SampleCaseApi::class
single { get<SectionApi>(named(DataSource.SECTION_REMOTE)).getSections() }
single<FaqContentApi>(named(DataSource.FAQ_CONTENT_REMOTE)) { (FaqContentRemoteSource(get())) } bind FaqContentApi::class
single<ExamContentApi>(named(DataSource.EXAM_CONTENT_REMOTE)) { (ExamContentRemoteSource(get())) } bind ExamContentApi::class
}
I am new to KMM and trying to create a generic function for api calling using ktor with reified and it seems to work fine with android but throws an error in iOS
This is my common api call return in Shared file.
#Throws(Exception::class)
suspend inline fun<reified T> post(url: String, requestBody: HashMap<String, Any>?) : Either<CustomException, T> {
try {
val response = httpClient.post<T> {
url(BASE_URL.plus(url))
contentType(ContentType.Any)
if (requestBody != null) {
body = requestBody
}
headers.remove("Content-Type")
headers {
append("Content-Type", "application/json")
append("Accept", "application/json")
append("Time-Zone", "+05:30")
append("App-Version", "1.0.0(0)")
append("Device-Type", "0")
}
}
return Success(response)
} catch(e: Exception) {
return Failure(e as CustomException)
}
}
It works good in android if I call it like this :-
api.post<MyDataClassHere>(url = "url", getBody()).fold(
{
handleError(it)
},
{
Log.d("Success", it.toString())
}
)
But I am not able to get it run on iOS devices it shows me error like this :-
some : Error Domain=KotlinException Code=0 "unsupported call of reified inlined function `com.example.myapplication.shared.apicalls.SpaceXApi.post`" UserInfo={NSLocalizedDescription=unsupported call of reified inlined function `com.example.myapplication.shared.apicalls.SpaceXApi.post`, KotlinException=kotlin.IllegalStateException: unsupported call of reified inlined function `com.example.myapplication.shared.apicalls.SpaceXApi.post`, KotlinExceptionOrigin=}
Any help in this is appreciated. Thanks
Okay so from the Slack conversation here it's clear that it's not possible to create this type of generic function as swift doesn't support reified. The only solution is we need to create different functions for every different api call we need.
For eg :- we can create a interface inside which we have all the api implementation and use it in the native platforms. Like this :-
interface ApiClient {
suspend fun logIn(…): …
suspend fun createBlogPost(…): …
// etc
}
Now we can use this in our native platforms.
So I am writing this app with remote data source. And I wanted to add local db storage capabilities. I have setup an architecture whereby I have an interface DataSource. A RemoteDataSource and a LocalDataSource classes implement that interface. The RemoteDataSource is injected with ApiInterface retrofit and the LocalDataSource is injected with DAOs.
Now there is this repository interface and implementation SomeDataRepository and SomeDataRepositoryImpl. If I want the repository to be able to fetch the data from api and save it to the database, how do I go about doing that?
Currently I have injected both RemoteDataSource and LocalDataSource classes to the SomeDataRepositoryImpl to access methods from different data sourcces. This way I can call something like localDataSource.saveToDb() and/or remoteDatSource.fetchSomeData() int SomeRepositoryImpl class. But I do not know if passing concrete implementations to a class is the way to go.
But if I pass lets say a single DataSource interface to the SomeDataRepository, I will have to define a saveToDb() function int the interface DataSource and then I will have to implement that in RemoteDataSource as well which is not that good.
Can anyone please guide me through what the best approach is to this solution.
And also while I am at it, is it any good to wrap the data with LiveData wrapper class right in the api interface for retrofit? cause I dont think when a method is called on the repository, I would want to observe it right there in the repo and then access the data to put it onto local db.
Since you want to have the local data source act as a fallback for the remote data source, you can create another data source implementation that is a composition of the local and remote data sources. This composite data source can contain the fallback logic and handle the delegation to the remote and local datasources as needed. Once you have done this, it is a simple matter to create a Koin module to construct these, and bind the composite data source to the data source interface.
Suppose this is your interface and the two data sources you already have:
interface DataSource {
fun getData(): Data
}
class RemoteDataSource : DataSource {
// ...
}
class LocalDataSource : DataSource {
// ...
}
Then you can create a third implementation like this one:
class CompositeDataSource(
val remote: RemoteDataSource,
val local: LocalDataSource
) : DataSource {
override fun getData() : Data {
return try {
remote.getData()
} catch (e: Exception) {
local.getData()
}
}
}
To define all of this, your koin module would look something like this
module {
single { RemoteDataSource() }
single { LocalDataSource() }
single<DataSource> { CompositeDataSource(remote = get(), local = get()) }
}
Edit: If what you actually want is a cache, you can use the local data source as your cache like this:
class CompositeDataSource(
val remote: RemoteDataSource,
val local: LocalDataSource
) : DataSource {
override fun getData() : Data {
return try {
remote.getData().also { local.saveData(it) }
} catch (e: Exception) {
local.getData()
}
}
}
You can try the next approch, it requirs minimal changes and it works for me:
Add interfaces for the remote and local data source, it should inherit the main DataSource interface
interface QuotesDataSource {
fun getQuotes(skip: Int = 0, force: Boolean = false): Flow<List<Quote>>
suspend fun updateQuotes(quotes: List<Quote>)
}
interface QuotesRemoteDataSource : QuotesDataSource
interface QuotesLocalDataSource : QuotesDataSource
Then use those interfaces to create koin module
val repoModule = module {
single { QuotesApi() }
single<QuotesLocalDataSource> { QuotesDatabase(get()) }
single<QuotesRemoteDataSource> { QuotesRemote(get()) }
single<QuotesDataSource> {
QuotesRepository(
local = get<QuotesLocalDataSource>(),
remote = get<QuotesRemoteDataSource>()
)
}
}
I'm using MVVM architecture for my Android app and just started with Koin. When I'm trying to inject more than one parameter problems start appearing.
For now I have Repository class which uses:
RESTApi to perform web calls. RestProvider uses SocketProvider as constructor parameter for this
Utils(Context) as helper to retrieve some basic information(appVersion,
IMEI etc)
My appModule
{
single<RepositoryApi> {
Repository(
Utils(androidContext())
\\ RestProvider(SocketProvider()) Here I get problems
)
}
single<RestApi> { RestProvider(get() as SocketProvider) }
single<SocketApi> { SocketProvider() }
single<UtilsApi> { Utils(androidContext()) }
viewModel { LoginViewModel(get()) }
}
When I use Utils only everything works fine, when I add RestProvider I get exception:
Caused by: org.koin.core.error.InstanceCreationException: Could not
create instance for
[type:Single,primary_type:'com.etrans.ntsdriver.provider.repository.RepositoryApi']
at org.koin.core.instance.DefinitionInstance.create(DefinitionInstance.kt:61)
at org.koin.core.instance.SingleDefinitionInstance.get(SingleDefinitionInstance.kt:40)
at org.koin.core.definition.BeanDefinition.resolveInstance(BeanDefinition.kt:70)
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:165)
at org.koin.core.scope.Sc
I understand that I'm missing something but I didn't find any tutorials or examples to explain such cases.
My gradle dependencies related to Koin(maybe will be useful):
// Koin for Android - ViewModel features
implementation "org.koin:koin-android-viewmodel:$koin_version"
implementation "org.koin:koin-java:$koin_version"
testImplementation "org.koin:koin-test:$koin_version"
androidTestImplementation "org.koin:koin-test:$koin_version"
Thanks in advance for any help
Here is an example of using Koin to setup retrofit.
private val networkModule = module {
single {
HttpLoggingInterceptor(
HttpLoggingInterceptor.Logger { message ->
//Logger.d("NETWORK: $message")
}).apply {
level = HttpLoggingInterceptor.Level.NONE
}
}
single {
DefaultHeadersInterceptor()
}
single {
OkHttpClient.Builder()
.addInterceptor(get<HttpLoggingInterceptor>())
.addInterceptor(get<DefaultHeadersInterceptor>())
.build()
}
single {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(JacksonConverterFactory.create(ObjectMapper().registerKotlinModule()))
.client(get())
.build()
}
single { get<Retrofit>().create(ApiInterface::class.java) }
}
So on this way you can configure ApiInterface
single { get().create(ApiInterface::class.java) }
Hope this will help.