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
}
Related
I have some errors in Retrofit2 and Kotlin Coroutines technologies. I need to dynamically query an info in my service.
For example, the URL is "https://exampleapidomain.com/api/sub_categories/read.php?id=2" I want to change id parameter dynamically.
My service:
interface AltKategoriService {
#GET("alt_kategoriler/" + Const.READ_URL_PIECE)
fun getId(#Query("id") id: String?): Call<Resource<AltKategorilerResponse>>
companion object{
fun build(): AltKategoriService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Const.BASE_URL)
.client(okHttpClient)
.build()
return retrofit.create(AltKategoriService::class.java)
}
}
}
My DataSource file:
class RemoteAltKategorilerDataSource : AltKategorilerDataSource {
override fun getSubCategories(): Flow<Resource<AltKategorilerResponse>> = flow {
try {
emit(Resource.Loading())
val call = AltKategoriService.build().getId("2").execute()
if (call.isSuccessful) {
call.body()?.let {
emit(it)
}
}
} catch (ex: Exception) {
emit(Resource.Error(ex))
ex.printStackTrace()
}
}
}
I get the following error:
Attempt to invoke virtual method 'void androidx.lifecycle.MutableLiveData.postValue(java.lang.Object)' on a null object reference" and then, app crashes.
I'm waiting for your answers and code examples. Thank you!
Edited. My ViewModel:
class SubCategoryViewModel: ViewModel() {
private val altKategoriRepository = AltKategoriRepository()
init {
getUsers()
}
var loading: MutableLiveData<Boolean>? = MutableLiveData()
var altKategoriLiveData = MutableLiveData<AltKategorilerResponse>()
var error = MutableLiveData<Throwable>()
fun getUsers() = viewModelScope.launch {
altKategoriRepository.getSubCategories()
.asLiveData(viewModelScope.coroutineContext).observeForever {
when (it.status) {
ResourceStatus.LOADING -> {
loading?.postValue(true)
}
ResourceStatus.SUCCESS -> {
Log.e("Message", it.data.toString())
altKategoriLiveData.postValue(it.data!!)
loading?.postValue(false)
}
ResourceStatus.ERROR -> {
error.postValue(it.throwable!!)
loading?.postValue(false)
}
}
}
}
}
Kotlin class initialisation takes place in the following order:
primary constructor -> init block -> secondary constructor
As no initialisation is done neither for var loading, var altKategoriLiveData nor var error class members of SubCategoryViewModel by the time getUsers() is called in the init { } block, you get the exception resulting in the app crash.
Regarding your implementation of the MVVM pattern, it contradicts to that of the official Android documentation, where a View is supposed to call a corresponding method of ViewModel explicitly or implicitly.
It should also work by just moving the init { } after the variable declarations or if you declare your loading state directly as LOADING. Also i think it's fine to declare it inside the init block, the documentation doesn't refer to that as being at fault if i'm reading correctly. Also it helps doing the call in init to avoid multiple times loading in Activities or Fragments if you only need to do the call once when the viewmodel is created and avoiding multiple calls by e.g: orientation change or other lifecycle dependent things. but please correct me if i'm wrong.
I am trying to create a Queue manager for my Android app.
In my app, I show a list of videos in the RecyclerView. When the user clicks on any video, I download the video on the device. The download itself is working fine and I can even download multiple videos concurrently and show download progress for each download.
The Issue:
I want to download only 3 videos concurrently and put all the other download in the queue.
Here is my Retrofit service generator class:
object RetrofitInstance {
private val downloadRetrofit by lazy {
val dispatcher = Dispatcher()
dispatcher.maxRequestsPerHost = 1
dispatcher.maxRequests = 3
val client = OkHttpClient
.Builder()
.dispatcher(dispatcher)
.build()
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val downloadApi: Endpoints by lazy {
downloadRetrofit.create(Endpoints::class.java)
}
}
And here is my endpoint interface class:
interface Endpoints {
#GET
#Streaming
suspend fun downloadFile(#Url fileURL: String): Response<ResponseBody>
}
And I am using Kotlin coroutine to start the download:
suspend fun startDownload(url: String, filePath: String) {
val downloadService = RetrofitInstance.downloadApi.downloadFile(url)
if (downloadService.isSuccessful) {
saveFile(downloadService.body(), filePath)
} else {
// callback for error
}
}
I also tried reducing the number of threads Retrofit could use by using Dispatcher(Executors.newFixedThreadPool(1)) but that didn't help as well. It still downloads all the files concurrently.
Any help would be appreciated. Thanks!
EDIT
Forgot to mention one thing. I am using a custom view for the recyclerView item. These custom views are managing their own downloading state by directly calling the Download class.
You can use CoroutineWorker to download videos in the background thread and handle a download queue.
Create the worker
class DownloadVideoWorker(
private val context: Context,
private val params: WorkerParameters,
private val downloadApi: DownloadApi
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val videos = inputData.getStringArray(VIDEOS)
//Download videos
return success()
}
companion object {
const val VIDEOS: String = "VIDEOS"
fun enqueue(videos: Array<String>): LiveData<WorkInfo> {
val downloadWorker = OneTimeWorkRequestBuilder<DownloadVideoWorker>()
.setInputData(Data.Builder().putStringArray(VIDEOS, videos).build())
.build()
val workManager = WorkManager.getInstance()
workManager.enqueue(downloadWorker)
return workManager.getWorkInfoByIdLiveData(downloadWorker.id)
}
}
}
In your viewModel add function to call worker from your Fragment/Activity
class DownloadViewModel() : ViewModel() {
private var listOfVideos: Array<String> // Videos urls
fun downloadVideos(): LiveData<WorkInfo> {
val videosToDownload = retrieveNextThreeVideos()
return DownloadVideoWorker.enqueue(videos)
}
fun retrieveNextThreeVideos(): Array<String> {
if(listOfVideos.size >= 3) {
val videosToDownload = listOfVideos.subList(0, 3)
videosToDownload.forEach { listOfVideos.remove(it) }
return videosToDownload
}
return listOfVideos
}
}
Observe LiveData and handle worker result
fun downloadVideos() {
documentsViewModel.downloadVideos().observe(this, Observer {
when (it.state) {
WorkInfo.State.SUCCEEDED -> {
downloadVideos()
}
WorkInfo.State.FAILED -> {
// Handle error result
}
}
})
}
NOTE: To learn more about Coroutine Worker, see: https://developer.android.com/topic/libraries/architecture/workmanager/advanced/coroutineworker
I was finally able to achieve it but I am still not sure if this is the most efficient way to do it. I used a singleton variable of ThreadPool. Here is what I did:
In my Download class, I created a companion object of ThreadPoolExecutor:
companion object {
private val executor: ThreadPoolExecutor = Executors.newFixedThreadPool(3) as ThreadPoolExecutor
}
Then I made the following changes in my startDownload function:
fun startDownloading(url: String, filePath: String) {
downloadUtilImp.downloadQueued()
runBlocking {
downloadJob = launch(executor.asCoroutineDispatcher()) {
val downloadService = RetrofitInstance.api.downloadFile(url)
if (downloadService.isSuccessful) saveFile(downloadService.body(), filePath)
else downloadUtilImp.downloadFailed(downloadService.errorBody().toString())
}
}
}
This code only downloads 3 videos at a time and queues all the other download requests.
I am still open to suggestions if there is a better way to do it. Thanks for the help!
I am investigating Koin dependency injection library in my current Android application.
I have a CoroutineWorker that completes all my background work.
What I would like to do is Dynamically inject a lambda for each type of background work I have.
I have the following code that works, however it is not Dynamic
Koin Module:
const val BackgroundLambdaName: String = "back-ground-lambda"
val lambdaModule = module {
single(qualifier = named(BackgroundLambdaName)) {
background
}
}
private val background: suspend CoroutineScope.(Service) -> Unit = { service: Service ->
val limit: Int = 200
var offset: Int = 0
loop# while (true) {
val networkResponse = service.move(options = mutableMapOf("limit" to limit, "offset" to offset))
if (networkResponse.next == null) {
break#loop
}
offset += limit
}
}
And my CoroutineWorker:
class BackgroundWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params), KoinComponent {
private val service: Service by inject()
private val x_background: suspend CoroutineScope.(Service) -> Unit by inject(qualifier = named(BackgroundLambdaName))
override suspend fun doWork(): Result = coroutineScope {
withContext(Dispatchers.IO) {
downloadSynchronously(this)
Result.success()
}
}
private suspend fun downloadSynchronously(coroutineScope: CoroutineScope) {
x_background(coroutineScope, service)
}
}
Is there any approach I can take that will allow me to specify different lambdas at runtime to inject into my CoroutineWorker?
For example if my lambda Koin module had 10 lambdas defined
BackgroundLambdaName_0 - BackgroundLambdaName_9
Then when starting my unique background work as follows:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.build()
val backgroundWorkRequest = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
).build()
// DYNAMICALLY set qualifier = named(BackgroundLambdaName) to one of
// BackgroundLambdaName_0 - BackgroundLambdaName_9
WorkManager.getInstance(application).enqueueUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, backgroundWorkRequest)
Yes it is possible with Koin 2 at least is totally possible.
First create module that would be needed I guess for you is 'lambdaModule'
val coffeeAppModule = module {
single { CoffeeMaker(get(), get()) }
single<Pump> { Thermosiphon(get()) }
single<Heater> { ElectricHeater() }
}
Then dynamically load the module and unload when you don't need anymore.
// after start
loadKoinModules(coffeeAppModule)
// resolve CoffeeMaker
get()<CoffeeMaker>
// drop module's definitions & instances when you don't need it anymore
unloadKoinModules(coffeeAppModule)
please refer to maintainer of the library here you can find more
https://medium.com/koin-developers/ready-for-koin-2-0-2722ab59cac3
I am running across weird behaviors with HttpLoggingInterceptor. I have noticed that if I use newBuilder() the logging does not work.
// instantiate object (in app)
val okHttpRequestManager: HttpRequestManager = OkHttpRequestManager(OkHttpClient(), null)
// execute request (in app)
okHttpRequestManager.execute(request, callback)
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
client.newBuilder().addInterceptor(httpLoggingInterceptor).build()
}
// perform request below
...
}
}
The above code snippet does not work. However, if I make my parameter a builder, everything works fine. Is using newBuilder() the incorrect way to do this?
// the below works
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient.Builder,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
// no newBuilder() or build() and works like a charm
client.addInterceptor(httpLoggingInterceptor)
}
// perform request below
...
}
}
Anyone have an idea as to why this is?
That's because the method newBuilder() as the name implies, returns the new builder object and when you call build() on it, new instance of OkHttpClient will be returned created from the new builder.
Here is the source code:
/** Prepares the [request] to be executed at some point in the future. */
override fun newCall(request: Request): Call {
return RealCall.newRealCall(this, request, forWebSocket = false)
}
build() method
fun build(): OkHttpClient = OkHttpClient(this)
The newBuilder adds to the attributes of the existing client so you
will have a new client with both the old and new attributes.
If you want to use newBuilder() method then you need to make use of the newly created OkHttpClient.
// another class (in request module)
open class OkHttpRequestManager(private val client: OkHttpClient,
private val httpLoggingInterceptor: HttpLoggingInterceptor?) : HttpRequestExecutor {
override fun execute(httpRequest: HttpRequest, callback: HttpResponseCallback?) {
if (httpLoggingInterceptor != null) {
val newClient = client.newBuilder().addInterceptor(httpLoggingInterceptor).build()
}
// perform request below using newClient
...
}
}
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 ....