Search code examples
javaandroidandroid-livedataobserversmutablelivedata

Observe single entry of database using LiveData or MutableLiveData


I am currently working on an app that needs to observe changes for one entry in my database. The app is structured like this: My MainActivity is aRecyclerView listing all friends in DB. When clicking an item/friend the friend's profile (ProfileActivity) is started including a button to open an editor to edit name, birthday, image etc. When the user is done editing the new data is saved to the DB and the EditActivity is closed and the app returns to ProfileActivity. Here I want the changed to be displayed.

I experimented with both LiveData and MutableLiveData. I used the common structure of Activity-ViewModel-Repository-Dao in both cases. With LiveData my Dao.findBy(friendId) returns LiveData and with MutableLiveData Dao.findBy(friendId) returns FriendEntity. The problem with MutableLiveData is that changes in the DB cannot be observed directly. And with LiveData the problem is, that is have to somehow initialize the LiveData field in my Repository for the Activity to observe it. But LiveData cannot be manually initialized - so I get a NullPointerException.

I tried calling Dao.findBy(-1) at onCreate to have my LiveData initialized, and as soon as I know the actual friendId I call it again, but onChanged is not triggered anymore.

Is there a good solution to that problem? Is there another way to initialize LiveData? Why is onChanged not triggered the second time I call Dao.findBy(friendId) (after having called Dao.findBy(-1))

Activity:

public class ProfileActivity extends AppCompatActivity {
    MyViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mutable_live_data);

        viewModel = new ViewModelProvider(this).get(MyViewModel.class);

        viewModel.getFoundFriend().observe(this, new Observer<FriendEntity>() {
            @Override
            public void onChanged(FriendEntity friendEntity) {
                if (friendEntity != null){
                    FriendEntity friend = friendEntity.getName();
                    textViewData.setText(name);
                }
             }
        });
    }
}

ViewModel:

public class MyViewModel extends AndroidViewModel {

NormalRepository repository;
LiveData<FriendEntity> foundFriend;

public NormalViewModel(@NonNull Application application) {
    super(application);
    repository = new NormalRepository(application);
    foundFriend = repository.getFoundFriend();
}

public LiveData<FriendEntity> getFoundFriend() {
    return foundFriend;
}

public void find(long id) {
    repository.find(id);
}
}

Repository:

public class NormalRepository {

MyDao dao;
LiveData<FriendEntity> foundFriend;

public NormalRepository(Application application) {
    Database database = Database.getInstance(application);
    this.dao = database.normalDao();

    foundFriend = dao.findBy(-1L);
}

public void find(long id) {
    new FindAsyncTask(dao).execute(id);
}

private class FindAsyncTask extends AsyncTask<Long, Void, LiveData<FriendEntity>>{
    NormalDao dao;

    public FindAsyncTask(NormalDao dao) {
        this.dao = dao;
    }

    @Override
    protected LiveData<FriendEntity> doInBackground(Long... longs) {

        foundFriend = null;

        return dao.findBy(longs[0]);
    }

    @Override
    protected void onPostExecute(LiveData<FriendEntity> friendEntityLiveData) {
        foundFriend = friendEntityLiveData;
    }
}

public LiveData<FriendEntity> getFoundFriend() {
    return foundFriend;
}
}

Dao:

@Dao
public interface MyDao {

@Query("SELECT * FROM friend_table WHERE id=:id")
LiveData<FriendEntity> findBy(long id);

}

Solution

  • I'm a bit rusty with Java cause i'm using Kotlin, but it seems to me that:

    FindAsyncTask is useless. If you return a LiveData from the query, this can be run on the main thread. You don't need to use a background thread. In fact, the function returns immediately the LiveData (with a null value as content) and then Room using a background thread executes the query and updates the LiveData. At that point you get your value. Now, forget about MutableLiveData in your case. That is if you need to update yourself the value, but in this case Room takes care of that. So repo will be something like:

    public class NormalRepository {
        MyDao dao;
    
        // you probably can transform all in a one liner, i bet you need just a context there, not application
        public NormalRepository(Application application) {
            Database database = Database.getInstance(application);
            this.dao = database.normalDao();    
        }
    
        public LiveData<FriendEntity> find(long id) {
            new dao.findBy(id);
        }
    }
    

    ViewModel needs just to call your query and store the LiveData

    public class MyViewModel extends AndroidViewModel {
    
    NormalRepository repository;
    LiveData<FriendEntity> foundFriend;
    
    // When you create the view Model you should already know what is the id
    public NormalViewModel(@NonNull Application application, long id) {
        super(application);
        repository = new NormalRepository(application);
        foundFriend = repository.repository.find(id);
    }
    
    public LiveData<FriendEntity> getFoundFriend() {
        return foundFriend;
    }
    }
    

    and on the activity:

    public class ProfileActivity extends AppCompatActivity {
        MyViewModel viewModel;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_mutable_live_data);
            if(savedInstanceState != null)
                long id = savedInstanceState.getLong("MyLong")
            viewModel = new ViewModelProvider(this,ViewModelFactory(application, id)).get(MyViewModel.class);
    
            viewModel.getFoundFriend().observe(this, new Observer<FriendEntity>() {
                @Override
                public void onChanged(FriendEntity friendEntity) {
                    if (friendEntity != null){
                        String name = friendEntity.getName();
                        textViewData.setText(name);
                    }
                 }
            });
        }
    }
    

    so at this point you need only to understand how to build a factory for your viewModel but i'm sure you'll find plenty of examples of that. I imagine you pass the id to the activity using an Intent, so you unpack that value from the bundle passed as the parameter of onCreate