Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting Initial Value for LiveData Always Returning Null

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!

like image 798
Chaiwa Avatar asked Oct 16 '22 06:10

Chaiwa


2 Answers

I finally solved this problem with great help from @Krishna, and here are the main points:

  1. The DAO method should return LiveData
  2. 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
  3. 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
  4. In the Repository class, create a getter which will expose the LiveData object to the ViewModel
  5. In the ViewModel class, create a method which will expose the LiveData object to the View Controller (activity or fragment)
  6. In the Activity or Fragment, simply listen or subscribe to changes on the LiveData exposed by the Accessor Method provided by the ViewModel
  7. 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();
    }
}
like image 73
Chaiwa Avatar answered Oct 21 '22 02:10

Chaiwa


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();
}
like image 23
Krishna Sharma Avatar answered Oct 21 '22 01:10

Krishna Sharma