2

My usage currently is to store 5 specific button's info into a database (room) to persist it across reboots. My current usage doesn't rely on changes of the data because the only one changing the data is the user upon long press of the button (then i update the database). Hence I do not need a LiveData variable, and this is making it difficult for me to initialize my ViewModel.

Essentially, since the LiveData objects only update on change, my data never gets initialized. Thus the app always will cause a null-pointer on startup.

I'll share a gist of my setup so far bellow. I'm hoping there is some way to make this work where I don't have to observe any LiveData object, and I can just grab data when I instantiate the Model.

Entity:

@Entity(tableName = "myEntity")
public class MyEntity {
    @PrimaryKey
    public int buttonID;

    // other fields...
}

DAO:

@Dao
interface MyDAO {
    @Query("Select * from myDB")
    LiveData<List<MyEntity>> getEntityList();
    // I think this needs to change to just List<MyEntity>?

    // also insert and update here...
}

Repository:

class MyRepository {
    private MyDAO myDAO;
    private LiveData<List<MyEntity>> allEntities;

    MyRepository(Application application) {
        MyDatabase db = MyDatabase.getInstance(application);
        myDAO = db.myDAO();
        allEntities = myDAO.getAllEntities();
    }

    LiveData<List<MyEntity>> getAllEntities() { return allEntities; }

    // Update entity...
}

ViewModel:

public class ViewModel extends AndroidViewModel {
    private MyRepository repository;
    private List<MyEntity> tempList;
    private HashMap<MyEntity> allEntities;

    public ViewModel (Application application) {
        super(application);
        repository = new MyRepository(application);

        Observer<List<MyEntity>> observer = data -> tempList = data;
        ObserveOnce(repository.getAllEntities(), observer); // ObserveOnce implementation found in this answer: https://stackoverflow.com/a/59845763/10013384
        allEntities = new HashMap<>();

        for (int i = 0; i < tempList.size(); i++) { // Nullpointer here, as tempList doesn't have any items yet.
            allEntities.put(tempList.get(i).buttonID, templist.get(i));
        }
    }

    // getter and update methods...
}

Activity:

// ...
protected void onCreate(Bundle savedInstanceState) {
    // ...
    viewModel = new ViewModelProvider(this).get(ViewModel.class);

    // Initialize UI views with data from ViewModel
}

Then in the respective listeners:

@Override
public boolean onLongClick(View v) {
    int index = (Integer) v.getTag();

    data.get(index).foo = fooNewUIData;
    ButtonArray[index].setText(fooNewUIData);
    ViewModel.update(data.get(index));  // if updated, update the ViewModel and the database
}
Matt Strom
  • 698
  • 1
  • 4
  • 23
  • Your repository implementation seems strange. If `getAllEntities()` is supposed to return data from Room, you need to involve your DAO somewhere. Right now, you are returning an unintialized `LiveData`, which is unlikely to work well. Also, usually, the viewmodel exposes the `LiveData` to the activity/fragment using the data, rather than attempting to consume the `LiveData` itself. – CommonsWare Jun 19 '20 at 19:09
  • Whoops, sorry I'll put my DAO in here for reference – Matt Strom Jun 19 '20 at 19:16
  • "I'll put my DAO in here for reference" -- yes, but you are not using it. – CommonsWare Jun 19 '20 at 19:31
  • updated with the constructor – Matt Strom Jun 19 '20 at 19:37
  • I didn't even pay attention to the details of your code; **but the question itself is good!** Can you use Room without LiveData? Even knowing lots about SQL I feel like a n00b using Room; sometimes it is desperating that you only can interact with Database through ` LiveData`, due to the limitation `LiveData` implies -> you can only observe it from `LifecycleOwner`s -> which, AFAIK, only Activities are. *PD: I still resist to learn Kotlin and I keep using Java :P* – SebasSBM Nov 12 '21 at 00:38

4 Answers4

3

since the LiveData objects only update on change, my data never gets initialized

That is not your problem. Your problem is that you think that ObserveOnce() is a blocking call, and that the results will be ready immediately when it returns. In reality, LiveData from Room does work on a background thread. You need to react to when the data is available in your Observer, not assume that it will be available in the next statement.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • I'm realizing now that this is more-so my problem. Do you have a suggestion that I can get around this async issue? I could use `allowMainThreadQueries` since my database is only a couple items big, but it seems the general populous thinks this is an anti-pattern. If you could update your answer with a suggested solution, that would appreciated! – Matt Strom Jun 19 '20 at 19:55
  • 1
    @MatthewStrom: The typical solution, as I mentioned in comments, is to have your `ViewModel` make the `LiveData` available to your activity/fragment, and let it observe the `LiveData` normally. When `observe()` (or the lambda expression) gets called, the activity/fragment uses it to display whatever it is supposed to display. Most, if not all, books and courses that cover the Jetpack (`LiveData`, `ViewModel`, etc.) should demonstrate this approach. – CommonsWare Jun 19 '20 at 19:59
  • @MatthewStrom: I do not have very many Java examples anymore. [This one](https://gitlab.com/commonsguy/cw-jetpack-java/-/tree/v0.9/Bookmarker) is a bit involved. `MainMotor` is my `ViewModel`, and it transforms a `LiveData` from my DAO into instances of `MainViewState` using a `map()` transformation. My activity observes that `LiveData` and applies those viewstates, in particular updating a `RecyclerView` to show a list of saved bookmarks. – CommonsWare Jun 19 '20 at 20:03
1

OfCourse you can, you can simply return the normal object class. LiveData needs to be used only if you want observe the changes to those rows.

You can also use to kotlin flows to still listen to the changes and not use LiveData

Without LiveData:

List<MyEntity> getAllEntities();

With Kotlin Flows:

fun getAllEntities(): Flow<List<MyEntity>>

Hope this helps !!

vizsatiz
  • 1,933
  • 1
  • 17
  • 36
  • Thanks for the idea. Once I use this method, however, I am essentially accessing the database synchronously, which Room/android does not like. (I get the `Cannot access database on the main thread since it may potentially lock the UI for a long period of time.` error). Is it possible to do this asynchronously? I have a feeling any async fetching will still cause my null-pointer. – Matt Strom Jun 19 '20 at 19:34
  • So when you are doing this, yes you are accessing the same synchronously, in legacy java code you will have to create a asyncTask to do this, in Koltin btw its easier as you can use a `suspend` function. Move to Kotlin, life's much better bro. – vizsatiz Jun 19 '20 at 19:38
  • And adding to that executing the code is async should not be reason for using `LiveData` – vizsatiz Jun 19 '20 at 19:39
  • I recognize that Kotlin might be able to overcome some Java flaws in Android SDK, but, **some stubborn people like me still use Java** and refuse learning Kotlin. Are we Java users condemned to depend on `LiveData` to show any `SELECT` result in User Interface? ;-/ – SebasSBM Nov 12 '21 at 00:55
0

Yes, of course you can. Just change the return type of the specific function in your DAO from LiveData<MyDataClass> to MyDataClass.

See this Codelab for further tutorial about Room.

René Jörg Spies
  • 1,544
  • 1
  • 10
  • 29
  • Is there something else to know about this approach? If so, it would be worthy editing your answer to summarize it a little. I say it because I remembered trying this long ago before even discovering this question, and it just simply didn't work for me. – SebasSBM Nov 12 '21 at 00:45
  • What exactly did you not get to work? The [Codelab](https://developer.android.com/codelabs/android-room-with-a-view-kotlin) should contain all information on getting you started with `Room` including, of course, `DAO`s with and without `LiveData`. – René Jörg Spies Nov 12 '21 at 19:12
  • The Codelab is too verbose; that's why I suggest to add more relevant information to the answer; I just tried what your answer says; just replacing `LiveData` by `T` in the return type for the DAO method. None of the times I tried this obtained the expected result, so I am forced to use LiveData every time I need to retrieve information to show something in UI. – SebasSBM Nov 13 '21 at 17:04
  • There is another good reason to add some of the relevant information from your Codelab to the answer: you never know if someday the link will get broken for some reason. – SebasSBM Nov 13 '21 at 17:07
  • I agree. However, I have not used Room in over a year and I cannot remember how I got it to work. I only know that it worked and maybe they removed that functionality from Room after my question. – René Jörg Spies Nov 14 '21 at 18:14
  • I see. Well, I had to try: I have [some issue](https://stackoverflow.com/q/69922332/3692177) that I am trying to solve loading M2M data tables in composite objects, and I was thinking if there was another way to get data and transforming it before showing the result in the UI, and having to put two observers for a single Select query is a bit redundant (details in linked question and its answers). – SebasSBM Nov 15 '21 at 00:54
  • From what you are writing, it sounds like you are looking for something like `Flows`? https://developer.android.com/kotlin/flow – René Jörg Spies Nov 22 '21 at 15:37
0

Okay, thanks all to provided answers and feedback, I appreciate it!

Ultimately I agree with @CommonsWare that the easiest way to handle this is probably not to have the ViewModel consume the LiveData. I'm opting to setup an Observer as normal in my Activity, and convert it into the format that I want (ArrayList) when I save that data to my Activity class.

The issue I was originally concerned about with data not being sorted to my liking could otherwise be solved with a simple sort:

Collections.sort(this.data, (o1, o2) -> Integer.compare(o1.buttonID, o2.buttonID));

As for the async issue, I'm just falling back to allowing the observer's callback update the data as it gets it (and update the UI accordingly). If this situation involves a lot more data, then perhaps I'll need some sort of a splash screen while the app loads data. But luckily I don't have to do any of that quite yet.

Activity:

// onCreate() {
    ViewModel = new ViewModelProvider(this).get(ViewModel.class);
            Observer<List<MyEntity>> observer = this::setData;
            observeOnce(ViewModel.getAllEntities(), observer);

    if (data.size() > 0) {
        // Initialize UI views
    }
// }
//...

public void setData(List<MyEntity> entities) {
    this.data = entities
    Collections.sort(this.data, (o1, o2) -> Integer.compare(o1.buttonID, o2.buttonID));

    // also update UI if need-be
}

The only thing left is to solve the issue of my Database not persisting across reboots, but that is out of the scope of this question.

Matt Strom
  • 698
  • 1
  • 4
  • 23