Koin Dependency Injection switching between local and remote data source - android

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>()
)
}
}

Related

There are multiple DataStores active for the same file in HiltAndroidTest

I just added DataStore to our codebase. After that, I found that all sequential UI tests are failing - the first one in a test case pass but next fails with There are multiple DataStores active for the same file.
I provide a data store instance using Hilt
#InstallIn(SingletonComponent::class)
#Module
internal object DataStoreModule {
#Singleton
#Provides
internal fun provideConfigurationDataStore(
#ApplicationContext context: Context,
configurationLocalSerializer: ClientConfigurationLocalSerializer
): DataStore<ClientConfigurationLocal> = DataStoreFactory.create(
serializer = configurationLocalSerializer,
produceFile = { context.dataStoreFile("configuration.pb") }
)
}
I guess this is happening because In a Hilt test, the singleton component’s lifetime is scoped to the lifetime of a test case rather than the lifetime of the Application.
Any ideas on how to workaround this?
I had the same issue.
One solution I tried but which didn't work (correctly) is to make sure the tests, once done, remove the dataStore files (the whole folder) and close the scope (the overridden scope that you manage in a "manager" class), like so:
https://github.com/wwt/testing-android-datastore/blob/main/app/src/androidTest/java/com/wwt/sharedprefs/DataStoreTest.kt
I had this in a finished() block of a TestWatcher used for these UI tests. For some reason, this was not enough so I ended up not looking deeper into why.
Instead I just used a simpler solution: the UI tests would use their own Dagger component, which has its own StorageModule module, which provides its own IStorage implementation, which for UI tests is backed just by an in-memory map, whereas on a production Dagger module would back it up via a DataStore:
interface IStorage {
suspend fun retrieve(key: String): String?
suspend fun store(key: String, data: String)
suspend fun remove(key: String)
suspend fun clear()
I prefer this approach in my case as I don't need to test the actual disk-persistance of this storage in UI tests, but if I had needed it, I'd investigate further into how to reliably ensure the datastore folder and scope are cleaned up before/after each UI test.
I was having the same issues and I came out with a workaround. I append a random number to the file name of the preferences for each test case and I just delete the whole datastore file afterward.
HiltTestModule
#Module
#TestInstallIn(
components = [SingletonComponent::class],
replaces = [LocalModule::class, RemoteModule::class]
)
object TestAppModule {
#Singleton
#Provides
fun provideFakePreferences(
#ApplicationContext context: Context,
scope: CoroutineScope
): DataStore<Preferences> {
val random = Random.nextInt() // generating here
return PreferenceDataStoreFactory
.create(
scope = scope,
produceFile = {
// creating a new file for every test case and finally
// deleting them all
context.preferencesDataStoreFile("test_pref_file-$random")
}
)
}
}
#After function
#After
fun teardown() {
File(context.filesDir, "datastore").deleteRecursively()
}
I'd suggest for more control + better unit-test properties (ie. no IO, fast, isolated) oblakr24's answer is a good clean way to do this; abstract away the thing that you don't own that has behavior undesirable in tests.
However, there's also the possibility these tests are more like end-to-end / feature tests, so you want them to be as "real" as possible, fewer test doubles, maybe just faking a back-end but otherwise testing your whole app integrated. If so, you ought to use the provided property delegate that helps to ensure a singleton, and declare it top-level, outside a class, as per the docs. That way the property delegate will only get created once within the class-loader, and if you reference it from somewhere else (eg. in your DI graph) that will get torn down and recreated for each test, it won't matter; the property delegate will ensure the same instance is used.
A more general solution, not limited to Hilt, would be to mock Context.dataStoreFile() function with mockk to return a random file name.
I like this approach as it doesn't require any changes on the production code.
Example of TestWatcher:
class CleanDataStoreTestRule : TestWatcher() {
override fun starting(description: Description) {
replaceDataStoreNamesWithRandomUuids()
super.starting(description)
}
override fun finished(description: Description) {
super.finished(description)
removeDataStoreFiles()
}
private fun replaceDataStoreNamesWithRandomUuids() {
mockkStatic("androidx.datastore.DataStoreFile")
val contextSlot = slot<Context>()
every {
capture(contextSlot).dataStoreFile(any())
} answers {
File(
contextSlot.captured.filesDir,
"datastore/${UUID.randomUUID()}",
)
}
}
private fun removeDataStoreFiles() {
InstrumentationRegistry.getInstrumentation().targetContext.run {
File(filesDir, "datastore").deleteRecursively()
}
}
}
and then use it in tests:
class SomeTest {
#get:Rule
val cleanDataStoreTestRule = CleanDataStoreTestRule()
...
}
The solution assumes that you use Context.dataStoreFile() and that the file name does not matter. IMO the assumptions are reasonable in most cases.

Retrofit call, blocked in MessageQueue

I would like to have a system to call API (Retrofit) with cache (in Room), with just coroutines (without LiveData and NetworkBoundResource).
So worflow is:
Check data in db
if present show it
if not:
Call API
Save data in db
show data
Problem app blocked in "Call API" step, here the stack
nativePollOnce:-1, MessageQueue (android.os) next:326, MessageQueue
(android.os) loop:160, Looper (android.os) main:6669, ActivityThread
(android.app) invoke:-1, Method (java.lang.reflect) run:493,
RuntimeInit$MethodAndArgsCaller (com.android.internal.os) main:858,
ZygoteInit (com.android.internal.os)
Retrofit service:
interface ProductService {
#GET("products")
suspend fun getProducts(): Response<List<Product>>
}
DAO Room:
#Dao
interface ProductDao {
#Query("SELECT * FROM Product ORDER BY price")
suspend fun getProducts(): List<Product>
#Transaction
#Insert(entity = Product::class)
suspend fun insertProducts(products: List<Product>)
}
My fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
productService = createProductService()
productDao = MyDatabase.getDatabase(requireContext()).productDao()
CoroutineScope(Dispatchers.Main).launch {
getProducts()
}
}
private suspend fun getProducts() {
progressBar.visibility = View.VISIBLE
recyclerViewProducts.visibility = View.GONE
var products = withContext(Dispatchers.IO){ productDao.getProducts() }
if(products.isEmpty()) {
val response = withContext(Dispatchers.IO) { productService.getProducts() }
if(response.isSuccessful && response.body() != null) {
products = response.body()!!
withContext(Dispatchers.IO) { productDao.insertProducts(products) }
}
}
withContext(Dispatchers.Main) {
progressBar.visibility = View.GONE
recyclerViewProducts.visibility = View.VISIBLE
recyclerViewProducts.apply {
layoutManager = LinearLayoutManager(context)
// set the custom adapter to the RecyclerView
adapter = ProductsAdapter(products, this#ListProductFragment)
}
}
}
This is not clean architecture. You should have a Database layer (that you have) and a Repository and a Viewmodel. So when framgnet is created calls viewmodel to observe data from repository that also observe data from Db. If data from db is empty then it creates the api call in a coroutine scope and in the same thread it saves data to DB. So automatically viewmodel gets informed for new data
I recommend using MVVM Design Pattern. You must do what you want in the repository pattern.
The repository pattern is a design pattern that isolates data sources
from the rest of the app. A repository mediates between data sources
(such as persistent models, web services, and caches) and the rest of
the app. The diagram below shows how app components such as activities
that use LiveData might interact with data sources by way of a
repository. To implement a repository, you use a repository class,
such as the VideosRepository class that you create in the next task.
The repository class isolates the data sources from the rest of the
app and provides a clean API for data access to the rest of the app.
Using a repository class is a recommended best practice for code
separation and architecture. Advantages of using a repository. A
repository module handles data operations and allows you to use
multiple backends. In a typical real-world app, the repository
implements the logic for deciding whether to fetch data from a network
or use results that are cached in a local database. This helps make
your code modular and testable. You can easily mock up the repository
and test the rest of the code.
I suggest you check it out for the error code.
Control

How to change implementation at runtime

I have a few classes that from my client code I need to call. Right now I have a list and then the client has to check the list for the implementation. It is a mess to use.
object Adapter {
val list = listOf(
A(),
B()
...
)
}
Ideally I would only keep one implementation in memory, but sometimes I need to change between implementations.
Make all your classes implement an interface. Since they all implement the same interface you can then assign the concrete instances to the same variable.
interface IWorker {
fun doWork()
}
Then in the Adapter class you can set which implementation you want to use.
object Adapter {
var worker: IWorker = Default()
}
Default represents any of your classes.
class Default: IWorker {
override fun doWork() {}
}

Moshi ArrayOutOfBoundsException when adding factory

I'm finding this exception related with Moshi sometimes when opening the app:
Caused by java.lang.ArrayIndexOutOfBoundsException: length=33; index=33
at java.util.ArrayList.add(ArrayList.java:468)
at com.squareup.moshi.Moshi$Builder.add(Moshi.java:231)
We initialise a repository in the BaseApplication which, sometimes, results in the mentioned crash when initialising Moshi. I'm finding this error in the app reports but I'm not able to reproduce it. Let's jump to the what we have and see if you might have a clue on it.
This factory is used to create Moshi instances, getting the crash when adding KotlinJsonAdapterFactory:
object MyMoshiConverterFactory {
fun create(setup: (Moshi.Builder.() -> Unit)? = null): Converter.Factory {
val moshi = MoshiUtil.createMoshi()
setup?.let { moshi.it() }
moshi.add(KotlinJsonAdapterFactory()) // Here is the crash!
return MoshiConverterFactory.create(moshi.build())
}
}
Here we have a class where we have all the converters we use. It really has a lot more of converters, but I've removed a few of them for simplicity:
object MoshiUtil {
private val lazyMoshi by lazy {
Moshi.Builder().apply {
add(DateAdapter())
add(DefaultOnDataMismatchAdapter.newFactory(FeedItem::class.java, null))
add(SkipListNullValuesAdapter.createFactory(Element::class.java))
add(SkipListNullValuesAdapter.createFactory(Post::class.java))
add(SkipListNullValuesAdapter.createFactory(MetadataItem::class.java))
add(GeoGeometry::class.java, GeometryAdapter())
}
}
fun createMoshi() = lazyMoshi
}
And finally, in our BaseApplication, we have something like this:
class BaseApplication {
#Override
public void onCreate() {
super.onCreate();
val myService = getMyService(applicationContext)
}
private fun getMyService(appContext: Context): MyService {
val converterFactory = MyMoshiConverterFactory.create()
return Retrofit.Builder().baseUrl(baseUrl).apply {
addConverterFactory(converterFactory)
client(okHttpClientBuilder.build())
}.build().create(MyService::class.java)
}
}
}
So, do you see anything that could be causing it? Do you think it might be a concurrency issue happening at startup when the several places in the app are creating the MoshiUtils object at the same time?. Looking forward to hear from you guys, thanks!
Moshi.Builder is mutable and not thread-safe, so this error you're getting sometimes is a race condition as a result of that. You should call .build() on that base MoshiUtil instance to get an immutable Moshi instance, then make the return value of MoshiUtil.createMoshi be moshi.newBuilder() (creates a Moshi.Builder already configured like the existing Moshi instance), like so:
object MoshiUtil {
private val baseMoshi: Moshi = Moshi.Builder().apply {
// ...
}.build()
fun createMoshi(): Moshi.Builder = baseMoshi.newBuilder()
}
Since every person that calls createMoshi now gets their own instance of Moshi.Builder, there shouldn't be any concurrency problems anymore.

Confused about error handling in network layer when implementing MVVM in android, How to notify user something is wrong?

I've been struggling with MVVM pattern and android-architecture-components for last couple of months.
In my last project although I did try to decouple some of my app logic but it ended with a lot of code mess. Every fragment did lots of work, from handling UI to handling network requests and ...
In this new App I followed best practices for android app architecture and till now it's going well. But the thing is, I don't know how to handle network errors, and I don't get it how should I notify user if some network calls fail.
After searching and reading some blog posts I ended up with the following code (SafeApiCall and SafeApiResutl) functions to handle network requests in one place, but the thing is, All my Network Requests are done using retrofit and a NetworkDataSource class, Then I pass The NetworkDataSource and Dao to the RepositoryImpl class which is an implementation of my Repository Interface. then I pass the Repository to the viewModel, So ViewModel knows nothing about network or Dao or what so ever. So here is the problem, How can I notify user in case of any network errors ? I thought about creating a LiveData<> and pass errors to it in network layer, but in this case, Repository Must observe this, and also let's say create a LiveData in repository so viewModel observe that and so on ... But this is too much chaining, I dont like the idea of doing that. I also did take a look at the GoogleSamples Todo-MVVM-live-kotlin project, but honestly I didn't understand what is going on.
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<BasicResponse<T>>, errorMessage: String): T? {
return when (val result = safeApiResult(call)) {
is NetworkResult.Success -> {
Timber.tag("safeApiCall").d("data is ${result.serverResponse.data}")
result.serverResponse.data
}
is NetworkResult.Error -> {
Timber.tag("SafeApiCall").e("$errorMessage & Exception - ${result.exception}")
null
}
else -> TODO()
}
}
private suspend fun <T : Any> safeApiResult(
call: suspend () -> Response<BasicResponse<T>>
): NetworkResult<T> {
return try {
val response = call.invoke()
Timber.tag("SafeApiResult")
.d("response code : ${response.code()}, server value : ${response.body()!!.status}, server message: ${response.body()!!.message}")
if (response.isSuccessful) {
return when (ServerResponseStatus.fromValue(response.body()!!.status)) {
ServerResponseStatus.SUCCESS -> NetworkResult.Success(response.body()!!)
ServerResponseStatus.FAILED -> TODO()
ServerResponseStatus.UNKNOWN -> TODO()
}
} else {
TODO()
}
} catch (exception: Exception) {
Timber.tag("SafeApiResultFailed").e(exception)
NetworkResult.Error(exception)
}
}
sealed class NetworkResult<out T : Any> {
data class Success<out T : Any>(val serverResponse: BasicResponse<out T>) : NetworkResult<T>()
data class Error(val exception: Exception) : NetworkResult<Nothing>()
}
You are in the correct path. I would place both methods in the NetworkDataSource. All the calls executed should call those methods to handle the errors.
The NetworkDataSource will return the NetworkResult to the repository, and it will return the result to the ViewModel.
As you say, you can use a LiveData to notify the Activity/Fragment. You can create an error data class:
data class ErrorDialog(
val title: String,
val message: String
)
And declare a LiveData<ErrorDialog> that will be observed from your view. Then when you receive notifications in your view, you can implement logic in a BaseActivity/BaseFragment to show a Dialog or Toast or whatever type of view to indicate the error.

Categories

Resources