问题
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!
回答1:
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();
}
回答2:
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();
}
}
来源:https://stackoverflow.com/questions/54855077/getting-initial-value-for-livedata-always-returning-null