Since I'm new to programming Android apps I followed a tutorial on how to use the Android Architecture Components and Firebase for implementing the MVVM (using LiveData, ViewModel, etc.).
The tutorial I followed can be found here:
Part 1
Part 2
Part 3
I'm now left with what I think is a decent implementation of the MVVM, but I can not wrap my head around how I am supposed to pass query parameters to it. Right now I need to hardcode the ID of the document I want to retrieve:
public class AlarmDAO {
private FirebaseFirestore firebaseFirestore = FirebaseFirestore.getInstance();
public AlarmLiveData getFirestoreLiveData() {
DocumentReference documentReference = firebaseFirestore.collection(Collection.ALARMS.name).document("5RxJNuNyhDJlz49wpBkw");
return new AlarmLiveData(documentReference);
}
}
That then gets called by a class extending ViewModel.
public class AlarmViewModel extends ViewModel {
private AlarmDAO DAO = new AlarmDAO();
private AlarmLiveData liveData = null;
public LiveData<Alarm> getAlarmLiveData() {
liveData = DAO.getFirestoreLiveData();
return liveData;
}
public LiveData<Alarm> getAlarm() {
return liveData.alarm;
}
}
And then I observe that data in my activity:
model.getAlarmLiveData().observe(this, Observable -> {});
model.getAlarm().observe(this, alarm -> {
if (alarm != null) {
alarmTextView.setText(alarm.getTest());
else {
Log.d(TAG, "Waiting for data");
}
});
My problem is that I do not see a way of querying for a specific alarm. For instance model.getAlarm("someId"). I am under the impression that it should be done in the DAO and/or the ViewModel, but I can't figure out how. Another thing I do not understand is why I need to observe both model.getAlarmLiveData() and model.getAlarm() in my activity, as using only one does not work. The answer to both of those questions is most likely very simple, but thus far I haven't been able to figure it out.
For completeness: the Alarm class is nothing besides a getter and setter for two strings, and the AlarmLiveData class is below.
public class AlarmLiveData extends LiveData<Alarm> implements EventListener<DocumentSnapshot> {
private static final String TAG = AlarmLiveData.class.getSimpleName();
private Alarm alarmTemp = new Alarm();
private DocumentReference documentReference;
private ListenerRegistration listenerRegistration = () -> {};
public MutableLiveData<Alarm> alarm = new MutableLiveData<>();
public AlarmLiveData(DocumentReference documentReference) {
this.documentReference = documentReference;
}
#Override
protected void onActive() {
listenerRegistration = documentReference.addSnapshotListener(this);
super.onActive();
}
#Override
protected void onInactive() {
listenerRegistration.remove();
super.onInactive();
}
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
alarmTemp = new Alarm();
alarmTemp.setId(documentSnapshot.getId());
alarmTemp.setTest(documentSnapshot.get("test").toString());
alarm.setValue(alarmTemp);
} else {
Log.d(TAG, "ERROR");
}
}
}
Thank you for reading, I'm looking forward to the answer(s)!
The reason you have to use both model.getAlarmLiveData() and model.getAlarm() looks to be that your AlarmLiveData class extends LiveData but sets a value for the contained MutableLiveData member variable instead of setting its own class value.
Inside your AlarmLiveData class:
// Comment out/Remove your 'public MutableLiveData<alarm> alarm' member variable from the top.
// You're going to want to set the value of the AlarmLiveData class itself instead.
// ...
// Then inside of your onEvent callback
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable FirebaseFirestoreException e) {
if (documentSnapshot != null && documentSnapshot.exists()) {
alarmTemp = new Alarm();
alarmTemp.setId(documentSnapshot.getId());
alarmTemp.setTest(documentSnapshot.get("test").toString());
// Set the value for the AlarmLiveData class directly
setValue(alarmTemp);
} else {
Log.d(TAG, "ERROR");
}
}
I'm not sure why you're creating a DAO class and I would most likely move that code directly into the AlarmViewModel class.
But, here is how you can alter your current DAO class if you don't want to remove it:
// Pass in the document id you want to create a document reference for
public AlarmLiveData getFirestoreLiveData(String documentId) {
DocumentReference documentReference = firebaseFirestore.collection(Collection.ALARMS.name).document(documentId);
return new AlarmLiveData(documentReference);
}
Your AlarmViewModel class would look something like this:
public class AlarmViewModel extends ViewModel {
private AlarmDAO DAO = new AlarmDAO();
private AlarmLiveData liveData = null;
// Make sure to take in the document id so you can create the corresponding LiveData
public LiveData<Alarm> getAlarmLiveData(String documentId) {
// Only create a new LiveData instance if the current one is null.
// This is helpful if you intend to use this as a Shared ViewModel.
if(liveData == null){
liveData = DAO.getFirestoreLiveData(documentId);
}
return liveData;
}
}
Finally, in your Activity:
// Pass in the document id and observe the ViewModel
model.getAlarmLiveData("MY_DOCUMENT_ID").observe(this, alarm -> {
if (alarm != null) {
alarmTextView.setText(alarm.getTest());
}else{
Log.d(TAG, "Waiting for data");
}
});
Related
I'm just trying to learn mvvm, and I faced some issue - My List which should contain response from API are empty. I'm not sure why it is. Here's some code:
MainActivity
mViewModel = new ViewModelProvider(this).get(ViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Model>() {
#Override
public void onChanged(List<Model> list) {
if (data.size() > 0) {
data.clear();
}
if (list != null) {
data.addAll(list);
Log.i(TAG, "onChanged: " + data.size());
}
}
});
ViewModel
private Repository mRepository;
private MutableLiveData<List<Model> liveData;
public ViewModel(#NonNull Application application) {
super(application);
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
public MutableLiveData<List<Model> getData(){
return liveData;
}
Repository
public MutableLiveData<List<Model>> getData(){
MutableLiveData<List<Model> mLiveData = new MutableLiveData<>();
mApiCall.callApi()
.enqueue(new Callback<List<Model>() {
#Override
public void onResponse(Call<List<Model> call, Response<List<Model> response) {
mLiveData.setValue(response.body());
}
#Override
public void onFailure(Call<List<Model> call, Throwable t) {
t.getMessage();
}
});
return mLiveData;
}
in function getData() you are returning a live data and in this line in your view model :
liveData = mRepository.getData();
you are assigning it to liveDate which is a mutable live data created in your view model and the problem is here.
when this assignment is happened the observer in the liveData variable in view model will be removed and that's why we should use switchMap like this :
private var result : LiveData<List<Response>> = MutableLiveData()
result : LiveData<Response> = Transformations.map(mRepository.getData()){
it
}
and now all you need to do is to observe on repo in your view like this :
viewmodel.result.observe(this, Observer{ list ->
// to do with the result
})
response.body() will provide with class APIResponse. But what you need as response is List.
To get the expected response as List, try response.body().getMetadata().getResults()
It could be either repository initialisation as well. Do try creating
public void init() {
mRepository = Repository.getInstance();
liveData = mRepository.getData();
}
use viewModel.init();
Model class properties should match with response JSON fields to get the model objects from the response.
Debug on the response.body() and see the data coming. if the model is empty then there would be a mismatch on the fields.
As the title says, android needs queries out of main thread since it will trhow java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time otherwise. So I managed to make async queries as many tutorials explain, but it doesn't make so much sense (so far) as I could achieve.
public class NewDetalleDiarioActivity extends AppCompatActivity {
#Override
protected void onStart() {
super.onStart();
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database").build();
findPeriodo();
findDiario();
}
private void findPeriodo() {
periodo = Diarios.getPeriodo(db);
if (periodo == null) {
Intent intent = new Intent(NewDetalleDiarioActivity.this, NewPeriodoActivity.class);
startActivity(intent);
}
}
PROBLEM/ERROR:
If periodo is null, another activity is started, otherwise this one continues its thread.
The problem is that, when I debug it (which slows proceses, of course) periodo returns an instance from the database, but when I run the code without debugging, periodo is null.
public class Diarios {
public static Periodo getPeriodo(AppDatabase db) {
return Factory.getIntPeriodo().getPeriodo(db);
}
}
.
public class Factory {
private static IntPeriodo intPeriodo;
public static IntPeriodo getIntPeriodo() {
return (intPeriodo == null) ? intPeriodo = new BusPeriodo() : intPeriodo;
}
}
.
public class BusPeriodo implements IntPeriodo {
// I don't think it's necessary to post the interface...
#Override
public Periodo getPeriodo(final AppDatabase db) {
final Periodo[] periodo = new Periodo[1];
AsyncTask.execute(new Runnable() {
#Override
public void run() { //the async query that is driving me mad.
periodo[0] = db.periodoDao().getPeriodo(new Date());
}
});
return periodo[0];
}
}
What's the proper way to make select queries without getting them delayed?
The select query is indeed working, I don't think is necessary to post it (because it is returning an unique result when I debug), but it returns null when I run the code without debugging!! Please help.
SOLUTION:
As #user7041125 suggested, but instead I made a new class with an interface to call methods back to the activity, like this:
public class PeriodoBridge extends AsyncTask<Void, Void, Periodo> implements IntPeriodoBridge {
private WeakReference<Activity> weakActivity;
private IntPeriodoBridge caller; //implement this interface in the activity which needs to query
private AppDatabase db;
private Periodo periodo;
public PeriodoBridge(Activity activity, IntPeriodoBridge caller, AppDatabase db) {
weakActivity = new WeakReference<>(activity);
this.caller = caller; // assign activity instance to the local interface instance
this.db = db;
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
#Override
protected Periodo doInBackground(Void... voids) {
periodo = Diarios.getPeriodo(db);
return periodo;
}
#Override
protected void onPostExecute(Periodo periodo) {
Activity activity = weakActivity.get();
if (activity == null) {
return;
}
if (periodo == null) {
Intent intent = new Intent(activity, NewPeriodoActivity.class);
activity.startActivity(intent);
} else {
setPeriodo(periodo);
}
}
#Override //this is an interface method (IntPeriodoBridge)
public void setPeriodo(Periodo periodo) {
caller.setPeriodo(periodo); //I can set the query result back to the activity class with this
}
Call the init method of this class. The activity implements IntPeriodoBridge and in that way I can set the query result object to the activity class.
I am developing android application and I want to refresh viewModel livedata from second activity. When I get back to first activity data is not refreshed.
FirstActivity:
mViewModel = ViewModelProviders.of(this).get(MenuViewModel.class);
mViewModel.getMenus().observe(this, menuResponse -> {
if (menuResponse != null) {
resMenus.addAll(menuResponse.getMenus());
progressBar.setVisibility(View.GONE);
mAdapter.notifyDataSetChanged();
}
});
MenuViewModel:
public class MenuViewModel extends AndroidViewModel {
private MutableLiveData<MenuResponse> restMenuData;
private MenusRepository mRepository;
public MainActivityViewModel(#NonNull Application application) {
super(application);
mRepository = MenusRepository.getInstance(application);
restMenuData = mRepository.getMenus();
}
public LiveData<MenuResponse> getMenus() {
return restMenuData;
}
}
MenusRepository
private MenusRepository(Context context) {
apiRequest= RetrofitInstance.getInstance(context).getApiRequest();
}
public synchronized static MenusRepository getInstance(Context context) {
if (projectRepository == null) {
projectRepository = new MenusRepository(context);
}
return projectRepository;
}
public MutableLiveData<MenuResponse> getMenus() {
final MutableLiveData<MenuResponse> data = new MutableLiveData<>();
apiRequest.getMenus().enqueue(new Callback<MenuResponse>() {
#Override
public void onResponse(#NonNull Call<MenuResponse> call, #NonNull Response<MenuResponse> response) {
if (response.isSuccessful() && response.body() != null) {
data.setValue(response.body());
}
}
#Override
public void onFailure(#NonNull Call<MenuResponse> call, #NonNull Throwable t) {
data.setValue(null);
}
});
return data;
}
SecondActivity:
MenuViewModel mViewModel = ViewModelProviders.of(Objects.requireNonNull(SecondActivity.this)).get(MenuViewModel.class);
mViewModel.getMenus();
// This line not refresh menus
I except to refresh data from viewmodel, but it return old data.
How can I refresh viewmodel data in best practices?
MenusRepository.getMenus() methods creates a new instance of LiveData for every call. This is not the correct way to go about it.
You should have only one instance of LiveData and different objects subscribe to it (activity, viewModel, etc).
What you could do is - create a singleton of MenusRepository (which I think you have already done). Create one instance of MutableLiveData only and use it to update the data.
class MenusRepository {
private val liveData = MutableLiveData<MenuResponse>()
fun getMenus() {
// Your api call. Do not create a new instance of the livedata.
}
fun menus(): LiveData<MenuResponse> {
return liveData
}
fun update(data: MenuResponse) {
liveData.post(data)
}
This code is in Kotlin, but it applies similarly to Java as well.
You can update method to post an update to liveData. When you update it, all the observers will receive the new data. Use MenusRepository.menus() to access LiveData in your ViewModel.
Update
Your MenuRepository class could be like this.
private final MutableLiveData<MenuResponse> liveData = new MutableData<>();
private MenusRepository(Context context) {
apiRequest= RetrofitInstance.getInstance(context).getApiRequest();
}
public synchronized static MenusRepository getInstance(Context context) {
if (projectRepository == null) {
projectRepository = new MenusRepository(context);
}
return projectRepository;
}
public MutableLiveData<MenuResponse> loadMenus() {
apiRequest.getMenus().enqueue(new Callback<MenuResponse>() {
#Override
public void onResponse(#NonNull Call<MenuResponse> call, #NonNull Response<MenuResponse> response) {
if (response.isSuccessful() && response.body() != null) {
liveData.setValue(response.body());
}
}
#Override
public void onFailure(#NonNull Call<MenuResponse> call, #NonNull Throwable t) {
liveData.setValue(null);
}
});
}
public LiveData<MenuResponse> getMenus() {
return liveData;
}
public void updateData(response: MenuResponse) {
liveData.postValue(response);
}
When you want to update the data manually (from another activity),
use menuRepository.update() method. This will post the data to your LiveData which will update all its observers, ie. the ViewModel.
Call menuRepository.loadMenu() when you want to get the data using API.
Use menuRepository.getMenus() to get the LiveData and attach your observers.
Since MenuRepository is a singleton, there's only one instance of LiveData. When you will post an update to this instance of LiveData, all the observers will receive the new data.
public class MenuViewModel extends AndroidViewModel {
private MutableLiveData<MenuResponse> restMenuData;
private MenusRepository mRepository;
public MainActivityViewModel(#NonNull Application application) {
super(application);
mRepository = MenusRepository.getInstance(application);
restMenuData = mRepository.getMenus();
}
public LiveData<MenuResponse> getMenus() {
restMenuData = new MutableLiveData<>();
return restMenuData;
}
}
Change the View model code as above.
So that the live data is always cleared before returning to activity.
UPDATE:::
I've updated the question to include demo other LiveData that were also required:
so we have userLD that we need the value of to get the goalWeeklyLD, and we need the goalWeeklyLD value to get the remaining 4 LiveData values as they come from Room querys that use goalWeekly.dateproperties in the query
:::::
I've hit a problem where I have a fragment that has to populate LiveData that uses a query dependent on another LiveData value.
how can i get my live data to work correctly when it is dependent on other results?
Without using The Transitions.map() the view model throws an error because the values of the other live data are still null.
with the Transitions.map() in the view model the activities observer throws an error because the LiveData is still null.
I could possibly cheat my way past this by using a horrendously big nested query to return all i need in one custom DTO. but i'd rather understand whats going on here and how to handle this sort of situation properly.
Hopefully some code will make this clear
The Activity:
public class SomeFragment extends Fragment {
public static SomeFragment newInstance() {
return new SomeFragment();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
someViewModel = ViewModelProviders.of(this).get(SomeViewModel.class);
//getting user details from previous activity
Intent intent = getActivity().getIntent();
if (intent != null){
if (intent.hasExtra(USER_ID)){
user = new User(intent.getStringExtra(USERNAME));
user.setId(intent.getLongExtra(USER_ID,0));
someViewModel.setUserLD(user);
}
}
someViewModel.getUserLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
someViewModel.getGoalWeeklyLD().observe(this, new Observer<User>() {
#Override
public void onChanged(#Nullable User userVal) {
user = userVal;
}
});
//the below Observer calls throw an error because LiveData is null. makes sense.
//but how can i say "don't try and observe these until the transition.map has ran (because then it wont be null after if my understanding is right)" or something to that effect
someViewModel.getFirstLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgSportGradeVal) {
//Update UI
}
});
someViewModel.getSecondLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getThriLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
someViewModel.getFourthLD(user.getId()).observe(this, new Observer<XObject>() {
#Override
public void onChanged(#Nullable Grades avgBoulderGradeVal) {
// Update UI
}
});
}}
The View Model:
public class SomeViewModel extends AndroidViewModel {
DaoRepository daoRepository;
MutableLiveData<User> userLD;
LiveData<XObject> firstLD;
LiveData<XObject> secondLD;
LiveData<XObject> thirdLD;
LiveData<XObject> fourthLD;
public MutableLiveData<User> getUserLD() {
return userLD;
}
public void setUserLD(User user){
userLD.setValue(user);
}
public LiveData<XObject> getFirstLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getSecondLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getThirdLD(long userId) {
return goalWeeklyLD;
}
public LiveData<XObject> getForthLD(long userId) {
return goalWeeklyLD;
}
public SomeViewModel(#NonNull Application application) {
super(application);
daoRepository = new DaoRepository(application);
userLD = new MutableLiveData<>();
//so the first LiveData waits for the user to be populated before getting its LiveData becasue we need the userId for our Room query to run
firstLD = Transformations.map(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()).getValue());
//the remaining live data uses values from the first...
setupOtherTransformMaps(userLD.getValue())
}
public void setupOtherTransformMaps(long userId) {
//the secondLD, thirdLD and fourthLD all depends on values from the first (in runs a query that uses its dateExpired)
secondLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
thirdLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
fourthLD = Transformations.map(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()).getValue());
}}
Thankfully Google was smart and created a component which lets you combine variable number of LiveData into a single LiveData, and only emit events when you choose to do so!
This is called MediatorLiveData.
In your case though, you only need to channel 1 LiveData (userLD) into 1 another LiveData, that will emit each time userLd has a new value.
So you can use a predefined MediatorLiveData that does exactly this, specifically Transformations.switchMap.
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
EDIT: Yup, you seem to need to expose these LiveData separately from one another, but they all depend on the first query to execute.
So you need to replace Transformations.map { ...getValue() with Transformations.switchMap and you'll be good to go.
public SomeViewModel(#NonNull Application application) {
super(application);
CustomApplication app = (CustomApplication) application;
daoRepository = app.daoRepository();
userLD = new MutableLiveData<>();
firstLD = Transformations.switchMap(userLD, user -> daoRepository.getMostRecentGoalWeekly(user.getId()));
secondLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
thirdLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
fourthLD = Transformations.switchMap(firstLD, first ->
daoRepository.getAvgGradeRouteInPeriod(userId, first.getDateCreated(),first.getDateExpires()));
}
I am trying to load the loggedInUser from the Local Room Database, when the App starts. I would like to skip prompting user to log-in if the saved Authentication Token of the previously saved user is still valid!
So, from the DAO, I want to return a LiveData object containing the previously logged-in user, then observe it for subsequent changes. The challenge I have is that the method to get the currently logged-in user always returns null if I wrap the result inside a LiveData, but it returns the expected user if returned as a POJO.
How can I force LiveData to run synchronously just to initialize the value and then thereafter listen to subsequent changes? I really want to combine the two behaviors as the authentication may be invalidated by a background syncing task or when the user logs out(these actions will either replace or update the saved token and I would like to be reactive to such updates with the help of LiveData).
Here is what I have tried so far:
AuthorizationDAO.java
public interface AuthorizationDAO {
#Query("SELECT * FROM Authorization LIMIT 1") //Assume only one Authentication token will exist at any given time
LiveData<Authorization> getLoggedInUser(); //I want to keep this behaviour
#Insert(onConflict = REPLACE)
long insertAuth(Authorization authorization);
#Update
void logoutCurrentUser(Authorization authorization);
}
AuthorizationRepository.java
public class AuthorizationRepository {
private AuthorizationDAO mAuthorizationDAO;
private MutableLiveData<Authorization> mAuthorization = new MutableLiveData<>();
public AuthorizationRepository(Application application){
AppDatabase db = AppDatabase.getDatabase(application);
this.mAuthorizationDAO = db.mAuthorizationDAO();
}
public LiveData<Authorization> getLoggedInUser(){
mAuthorization.postValue(mAuthorizationDAO.getLoggedInUser().getValue()); //this is always null at startup
return this.mAuthorization;
}
AuthorizationViewModel.java
public class AuthorizationViewModel extends AndroidViewModel {
private AuthorizationRepository mAuthorizationRepository;
private LiveData<Resource<Authorization>> mAuthorization;
private LiveData<Authorization> loggedInUserAuth;
public AuthorizationViewModel(#NonNull Application application) {
super(application);
this.mAuthorizationRepository = new AuthorizationRepository(application);
}
public void init(){
this.loggedInUserAuth = this.mAuthorizationRepository.getLoggedInUser();
}
public LiveData<Authorization> getLoggedInUserAuth() {
return this.loggedInUserAuth;
}
}
AppActivity.java
public class AppActivity extends AppCompatActivity {
public AuthorizationViewModel mAuthorizationViewModel;
public #Nullable Authorization mAuthorization;
private NavController mNavController;
private NavHostFragment mNavHostFragment;
private BottomNavigationView mBottomNavigationView;
private boolean mIsLoggedIn;
private ActivityAppBinding mBinding;
private boolean mIsTokenExpired;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_app);
mNavHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.app_nav_host_fragment);
mNavController = mNavHostFragment.getNavController();
mBottomNavigationView = findViewById(R.id.nav_bottom_nav_view);
NavigationUI.setupWithNavController(mBottomNavigationView, mNavController);
if (Build.VERSION.SDK_INT>9){
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
}
mAuthorizationViewModel = ViewModelProviders.of(this).get(AuthorizationViewModel.class);
mAuthorizationViewModel.init(); //Here I want to load user synchronously before the rest happens and then on next line observe the same object
mAuthorizationViewModel.getLoggedInUserAuth().observe(this, new Observer<Authorization>() {
#Override
public void onChanged(#Nullable Authorization authorization) {
mBinding.setViewModel(authorization);
mIsLoggedIn = authorization == null? false: authorization.isLoggedIn();
mIsTokenExpired = authorization == null ? true : authorization.isTokenExpired();
if(!mIsLoggedIn || mIsTokenExpired){
if (authorization != null){
Log.i("CurrentAuth", "mIsLoggedIn?: "+authorization.isLoggedIn());
Log.i("CurrentAuth", "isTokenExpired?: "+authorization.isTokenExpired());
Log.i("CurrentAuth", "tokenCurrentTime?: "+ Calendar.getInstance().getTime());
Log.i("CurrentAuth", "tokenIssuedAt?: "+ authorization.getIat());
Log.i("CurrentAuth", "tokenExpiresAt?: "+ authorization.getExp());
}
mNavController.navigate(R.id.start_login);
}
}
});
As you can see, I am calling mAuthorizationViewModel.init() so I can load or initialize the loggedInUserAuth from the local database, and then observe the same LiveData instance with mAuthorizationViewModel.getLoggedInUserAuth().observe() on the next line! But the value returned for loggedInUserAuth is always null!
Kindly help, thanks!
I finally solved this problem with great help from #Krishna, and here are the main points:
The DAO method should return LiveData
In the Repository class, create a LiveData private member variable and not MutableLiveData(this is because we will be mutating database record via updates/inserts). The member variable will hold a reference to a LiveData object returned by the DAO Method
In the Repository's constructor, initialize the LiveData object to the result returned by the DAO method. This way, every time the activity starts, the currently saved record will be loaded
In the Repository class, create a getter which will expose the LiveData object to the ViewModel
In the ViewModel class, create a method which will expose the LiveData object to the View Controller (activity or fragment)
In the Activity or Fragment, simply listen or subscribe to changes on the LiveData exposed by the Accessor Method provided by the ViewModel
The DAO can also expose a method to update the LiveData, allowing the Repository via the ViewModel to enable the Activity or Fragment to send updates to the LiveData, at the same time keeping all listeners reactive!
Here is the working code for this scenario:
AuthorizationDAO.java
public interface AuthorizationDAO {
#Query("SELECT * FROM Authorization LIMIT 1") //Assume only one Authentication token will exist at any given time
LiveData<Authorization> getLoggedInUser(); //I want to keep this behaviour
#Insert(onConflict = REPLACE)
long insertAuth(Authorization authorization);
#Update
void logoutCurrentUser(Authorization authorization); //this will be used to toggle login status by Activity or Fragment
}
AuthorizationRepository.java
public class AuthorizationRepository {
private AuthorizationDAO mAuthorizationDAO;
private AuthorizationWebAPI mAuthorizationWebAPI;
private LiveData<Authorization> mAuthorization; //reference to returned LiveData
public AuthorizationRepository(Application application){
AppDatabase db = AppDatabase.getDatabase(application);
this.mAuthorizationDAO = db.mAuthorizationDAO();
this.mAuthorization = mAuthorizationDAO.getLoggedInUser(); //initialize LiveData
}
public LiveData<Authorization> getAuthorizationResult() { //getter exposing LiveData
return mAuthorization;
}
public void logoutCurrentUser(){ //toggle login status
if (this.mAuthorization != null){
AppExecutors.getInstance().getDiskIO().execute(()->{
Authorization mAuthorizationObj = this.mAuthorization.getValue();
mAuthorizationObj.setLoggedIn(false);
mAuthorizationDAO.logoutCurrentUser(mAuthorizationObj); //update LiveData and changes will be broadcast to all listeners
});
}
}
}
AuthorizationViewModel.java
public class AuthorizationViewModel extends AndroidViewModel {
private AuthorizationRepository mAuthorizationRepository;
public AuthorizationViewModel(#NonNull Application application) {
super(application);
this.mAuthorizationRepository = new AuthorizationRepository(application);
}
public LiveData<Authorization> getLoggedInUserAuth() { //exposes LiveData to the Activity or Fragment
return mAuthorizationRepository.getAuthorizationResult();
}
public void logoutCurrentUser(){ //allows activity or fragment to toggle login status
this.mAuthorizationRepository.logoutCurrentUser();
}
}
AppActivity.java
public class AppActivity extends AppCompatActivity {
public AuthorizationViewModel mAuthorizationViewModel;
public #Nullable Authorization mAuthorization;
private NavController mNavController;
private NavHostFragment mNavHostFragment;
private BottomNavigationView mBottomNavigationView;
private boolean mIsLoggedIn;
private ActivityAppBinding mBinding;
private boolean mIsTokenExpired;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_app);
mNavHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.app_nav_host_fragment);
mNavController = mNavHostFragment.getNavController();
mBottomNavigationView = findViewById(R.id.nav_bottom_nav_view);
NavigationUI.setupWithNavController(mBottomNavigationView, mNavController);
mAuthorizationViewModel = ViewModelProviders.of(this).get(AuthorizationViewModel.class);
mAuthorizationViewModel.getLoggedInUserAuth().observe(this, new Observer<Authorization>() { //Observe changes to Authorization LiveData exposed by getLoggedInUserAuth()
#Override
public void onChanged(#Nullable Authorization authorization) {
mBinding.setViewModel(authorization);
mIsLoggedIn = authorization == null? false: authorization.isLoggedIn();
mIsTokenExpired = authorization == null ? true : authorization.isTokenExpired();
if(!mIsLoggedIn || mIsTokenExpired){
if (authorization != null){
Log.i("CurrentAuth", "tokenExpiresAt?: "+ authorization.getExp());
}
mNavController.navigate(R.id.start_login); //every time authorization is changed, we check if valid else we react by prompting user to login
}
}
});
}
}
LogoutFragment.java
public class LogoutFragment extends Fragment {
private AuthorizationViewModel mAuthorizationViewModel;
private Authorization mAuth;
private FragmentLogoutBinding mBinding;
public LogoutFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mAuthorizationViewModel = ViewModelProviders.of(getActivity()).get(AuthorizationViewModel.class);
mAuthorizationViewModel.getLoggedInUserAuth().observe(getActivity(), new Observer<Authorization>() {
#Override
public void onChanged(Authorization authorization) {
mAuth = authorization;
}
});
// Inflate the layout for this fragment
mBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_logout,container,false);
View view = mBinding.getRoot();
mBinding.setViewModel(mAuth);
return view;
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
new AlertDialog.Builder(getContext())
.setTitle(R.string.title_logout_fragment)
.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
mAuthorizationViewModel.logoutCurrentUser(); //toggle login status, this will mutate LiveData by updating the database record then UI will react and call login fragment
}
})
.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.cancel();
Navigation.findNavController(view).popBackStack();
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
#Override
public void onDismiss(DialogInterface dialogInterface) {
}
})
.show();
}
}
Create a getter method of mAuthorization in class AuthorizationRepository
public MutableLiveData<Authorization> getAuthorizationResult() {
return mAuthorization;
}
Then modify your AuthorizationViewModel class like below
public void init() {
mAuthorizationRepository.getLoggedInUser();
}
public LiveData<Authorization> getLoggedInUserAuth() {
return mAuthorizationRepository.getAuthorizationResult();
}
It's too late but might help someone.
I faced the same issue when I did this
MyDao myDao;
private LiveData<List<T>> liveList;
//in constructor of repo after initializing myDao;
this.liveList = myDao.getAllData();
//somewhere in repo
for(T t : liveList.getValue()){/*computation*/}
and this is how I solved it
MyDao myDao;
//in constructor of repo don't do this because called on main thread
this.list = myDao.getAll();
//in constructor of repo initialize your Dao (in this case myDao)
//somewhere in repo (must not be on main thread)
for(T t : myDao.getAll()){/*computation*/} //do this on background thread
in MyDao
#Query("SELECT * FROM myTable")
List<T> getAll();
#Query("SELECT * FROM myTable")
LiveData<List<T>> getAllData();
Or, if you are accessing the liveList in some other place (than repository) then you must set an observer for the same