android developer
android developer

Reputation: 116322

How to offer a transformed liveData from DB on Room, if initialization/update of the DB might be needed?

Background

I'm creating some SDK library, and I want to offer some liveData as a returned object for a function, that will allow to monitor data on the DB.

The problem

I don't want to reveal the real objects from the DB and their fields (like the ID), and so I wanted to use a transformation of them.

So, suppose I have this liveData from the DB:

val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()

What I did to get the liveData to provide outside, is:

val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
    dbLiveData) { data ->
    data.map { SomeClass(it) }
}

This works very well.

However, the problem is that the first line (to get dbLiveData) should work on a background thread, as the DB might need to initialize/update, and yet the Transformations.map part is supposed to be on the UI thread (including the mapping itself, sadly).

What I've tried

This lead me to this kind of ugly solution, of having a listener to a live data, to be run on the UI thread:

@UiThread
fun getAsLiveData(someContext: Context,listener: OnLiveDataReadyListener) {
    val context = someContext.applicationContext ?: someContext
    val handler = Handler(Looper.getMainLooper())
    Executors.storageExecutor.execute {
        val dbLiveData = Database.getInstance(context).getSomeDao().getAllAsLiveData()
        handler.post {
            val resultLiveData: LiveData<List<SomeClass>> = Transformations.map(
                dbLiveData) { data ->
                data.map { SomeClass(it) }
            }
            listener.onLiveDataReadyListener(resultLiveData)
        }
    }
}

Note: I use simple threading solution because it's an SDK, so I wanted to avoid importing libraries when possible. Plus it's quite a simple case anyway.

The question

Is there some way to offer the transformed live data on the UI thread even when it's all not prepared yet, without any listener ?

Meaning some kind of "lazy" initialization of the transformed live data. One that only when some observer is active, it will initialize/update the DB and start the real fetching&conversion (both in the background thread, of course).

Upvotes: 0

Views: 361

Answers (2)

android developer
android developer

Reputation: 116322

OK I got it as such:

    @UiThread
    fun getSavedReportsLiveData(someContext: Context): LiveData<List<SomeClass>> {
        val context = someContext.applicationContext ?: someContext
        val dbLiveData =
            LibraryDatabase.getInstance(context).getSomeDao().getAllAsLiveData()
        val result = MediatorLiveData<List<SomeClass>>()
        result.addSource(dbLiveData) { list ->
            Executors.storageExecutor.execute {
                result.postValue(list.map { SomeClass(it) })
            }
        }
        return result
    }
internal object Executors {
    /**used only for things that are related to storage on the device, including DB */
    val storageExecutor: ExecutorService = ForkJoinPool(1)
}

The way I've found this solution is actually via a very similar question (here), which I think it's based on the code of Transformations.map() :

    @MainThread
    public static <X, Y> LiveData<Y> map(
            @NonNull LiveData<X> source,
            @NonNull final Function<X, Y> mapFunction) {
        final MediatorLiveData<Y> result = new MediatorLiveData<>();
        result.addSource(source, new Observer<X>() {
            @Override
            public void onChanged(@Nullable X x) {
                result.setValue(mapFunction.apply(x));
            }
        });
        return result;
    }

Do note though, that if you have migration code (from other DBs) on Room, it might be a problem as this should be on a background thread.

For this I have no idea how to solve, other than trying to do the migrations as soon as possible, or use the callback of "onCreate" (docs here) of the DB somehow, but sadly you won't have a reference to your class though. Instead you will get a reference to SupportSQLiteDatabase, so you might need to do a lot of manual migrations...

Upvotes: 0

Martin Marconcini
Martin Marconcini

Reputation: 27226

The Problem

  • You are an SDK that has no UX/UI, or no context to derive Lifecycle.
  • You need to offer some data, but in an asynchronous way because it's data you need to fetch from the source.
  • You also need time to initialize your own internal dependencies.
  • You don't want to expose your Database objects/internal models to the outside world.

Your Solution

  • You have your data as LiveData directly from your Source (in this particular, albeit irrelevant case, from Room Database).

What you COULD do

  • Use Coroutines, it's the preferred documented way these days (and smaller than a beast like RxJava).
  • Don't offer a List<TransformedData>. Instead have a state:
sealed class SomeClassState {
   object NotReady : SomeClassState()
   data class DataFetchedSuccessfully(val data: List<TransformedData>): SomeClassState()
   // add other states if/as you see fit, e.g.: "Loading" "Error" Etc.
}

Then Expose your LiveData differently:

private val _state: MutableLiveData<SomeClassState> = MutableLiveData(SomeClassState.NotReady) // init with a default value
val observeState(): LiveData<SomeClassState) = _state

Now, whoever is consuming the data, can observe it with their own lifecycle.

Then, you can proceed to have your fetch public method:

Somewhere in your SomeClassRepository (where you have your DB), accept a Dispatcher (or a CoroutineScope):

suspend fun fetchSomeClassThingy(val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default) {
     return withContext(defaultDispatcher) {
          // Notify you're fetching...
          _state.postValue(SomeClassState.Loading)         
 
          // get your DB or initialize it (should probably be injected in an already working state, but doesn't matter)
          val db = ...
          
          //fetch the data and transform at will
          val result = db.dao().doesntmatter().what().you().do()

          // Finally, post it.
          _state.postValue(SomeClassState.DataFetchedSuccessfully(result))
     }   
}

What else I would do.

  • The fact that the data is coming from a Database is or should be absolutely irrelevant.
  • I would not return LiveData from Room directly (I find that a very bad decision on Google that goes against their own architecture that if anything, gives you the ability to shoot your own feet).
  • I would look at exposing a flow which allows you to emit values N times.

Last but not least, I do recommend you spend 15 minutes reading the recently (2021) published by Google Coroutines Best Practices, as it will give you an insight you may not have (I certainly didn't do some of those).

Notice I have not involved a single ViewModel, this is all for a lower layer of the architecture onion. By injecting (via param or DI) the Dispatcher, you facilitate testing this (by later in the test using a Testdispatcher), also doesn't make any assumption on the Threading, nor imposes any restriction; it's also a suspend function, so you have that covered there.

Hope this gives you a new perspective. Good luck!

Upvotes: 1

Related Questions