I've been stuck two days now with the Architecture Components despite I followed all the tutorials i found. The "onchanged" method just does not trigger despite the livedata object works.I changed all my code several times and now i'm completely lost and my code is a mess. I'm begging for your help :'(
I am using Retrofit but not Room since i only have to persist small objects the duration of the session : Here is my Activity :
package com.example.techcrea.view.Activities;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import com.example.techcrea.R;
import com.example.techcrea.model.entities.User;
import com.example.techcrea.transverse.DialogManager;
import com.example.techcrea.viewModel.UserModel;
/**
* Login activity, this is the activity displayed at app launch.
* A client can't create an account, he needs to already have one.
* A client can log in or ask for a new password if he forgot his.
* @author Caroline
*/
public class LoginActivity extends AppCompatActivity implements LifecycleOwner {
//ATTRIBUTES
//View attributes
/**
* Login input field
*/
private EditText txtInputId;
/**
* Password input field
*/
private EditText txtInputPwd;
private UserModel viewmodel;
private SharedPreferences storedSettings;
/**
* Method automatically launches at Activity start.
* Everything in it will be initialized at launch.
*
* @param savedInstanceState This is native android code, I don't know what it does but you have to leave it here.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
viewmodel = ViewModelProviders.of(this).get(UserModel.class);
loadComponents();
viewmodel.isError().observe(this, new Observer<Integer>() {
@Override
public void onChanged(Integer integer) {
Log.e("CHANGED", "user value has changed !");
switch(integer){
case 0 : login(viewmodel.getUser().getValue());
break;
case 1 :
DialogManager.getInstance().errorOccurred(getParent(), getCurrentFocus());
break;
case 2 :
DialogManager.getInstance().customMsg(getParent(),getCurrentFocus(),"Aucun utilisateur trouvé avec ces identifiants");
}
}
});
//##########################################################################################
//Keeps the user login in memory so the user does not have to enter it each time he/she wants to connect.
storedSettings = getPreferences(Context.MODE_PRIVATE);
if(storedSettings.contains("login")){
txtInputId.setText(storedSettings.getString("login", ""));
txtInputPwd.requestFocus();
}
}
/**
* Binds the design to the data
*/
public void loadComponents(){
txtInputId = findViewById(R.id.txtEnterId);
txtInputPwd = findViewById(R.id.txtEnterPassword);
/*
* The "connexion" button.
* I created it with xml, the orange_btn_default.xml is the button default state
* the orange_btn_pressed.xml is the button when pressed
* the file gathering all of this is custom_connect_button.xml
* this last file is the one linked to the view. I you just want to change color you only have to do it in the colors.xml file.
*/
Button btnConnect = findViewById(R.id.btnConnection);
//OnClick Listener for the Connection button
btnConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkForm();
}
});
}
/**
* Checks if no field have been left empty
* Displays a warning message if it is the case
*/
public void checkForm(){
if(!txtInputId.getText().toString().equals("") && !txtInputPwd.getText().toString().equals("")) {
viewmodel.login(txtInputId.getText().toString(),txtInputPwd.getText().toString());
}
else
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Merci d'entrer des identifiants valides")
.setTitle("Erreur");
AlertDialog dialog = builder.create();
dialog.show();
}
}
/** Launches the next activity once the login has been completed.
* @param user
*/
public void login(User user){
SharedPreferences storedSettings = getPreferences(getApplicationContext().MODE_PRIVATE);
SharedPreferences.Editor editor = storedSettings.edit();
editor.putString("login", user.getUSER_LOGIN());
editor.commit();
Intent intent = new Intent(getBaseContext(), HomeActivity.class);
startActivity(intent);
finish();
Log.i("We have a user", user.toString());
}
}
Here is the class that extends the viewmodel :
package com.example.techcrea.viewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.techcrea.Repository;
import com.example.techcrea.model.entities.User;
public class UserModel extends ViewModel {
/**
* A user that we will keep in memory while navigating across fragments.
* This represents the user of our application, and therefore it is initialized only once at login, but can be updated if the user updates his personal informations or contacts.
*/
private MutableLiveData<User>user;
private MutableLiveData<Integer>errorValue;
private static final UserModel instance = new UserModel();
public static UserModel getInstance()
{
return instance;
}
/**
* Get the stored user, or create a new one (as mutablelivedata) if there is none.
* @return a user
*/
public LiveData<User> getUser(){
if(user == null){
user=new MutableLiveData<>();
}
return user;
}
public LiveData<Integer> isError(){
if(errorValue== null){
errorValue=new MutableLiveData<>();
}
return errorValue;
}
public void setErrorValue(Integer integer){
isError();
errorValue.setValue(integer);
}
public void setUser(User uservalue){
getUser();
user.setValue(uservalue);
}
public void login(String login, String password){
Repository.getInstance().login(login,password);
}
}
I use singleton since this values have to be persisted through two activities (the login activity and then the main activity which uses fragment navigation)
And here the repository calling the API :
package com.example.techcrea;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import com.example.techcrea.model.entities.Bill;
import com.example.techcrea.model.entities.Contact;
import com.example.techcrea.model.entities.Contract;
import com.example.techcrea.model.entities.ContractReferent;
import com.example.techcrea.model.entities.SupportMessage;
import com.example.techcrea.model.entities.SupportTicket;
import com.example.techcrea.model.entities.User;
import com.example.techcrea.viewModel.IsIncidentModel;
import com.example.techcrea.viewModel.SupportTicketListModel;
import com.example.techcrea.viewModel.UserModel;
import com.example.techcrea.remoteData.RetrofitInstance;
import com.example.techcrea.remoteData.TcApi;
import com.example.techcrea.transverse.DialogManager;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* The class between the code and the database/api access.
* This class regroups all calls to the api so you do not have to change the other classes.
* Change methods with the according code, the only thing you must not change is the type of returns.
* Return false if you have to catch an exception. Return true if the request worked.
* For the non-boolean method you'll have to manage the try/catches in the corresponding fragments.
*/
public class Repository {
private static final Repository ourInstance = new Repository();
private TcApi api = RetrofitInstance.getTcApi();
private int result;
public static Repository getInstance() {
return ourInstance;
}
private Repository() {
}
public void init(){
UserModel.getInstance().getUser();
}
//GET ##########################################################################################
/**
* The login method that authenticates the user
*
* @param login the user login
* @param password the user password
* @return 0 (null/does not exist) 1 (success) or 2 (failure) depending on the callback answer.
*/
public void login(String login, String password) {
Log.e("login", "we called api");
//We call the api for login
api.login(login, password).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
Log.e("login response success", response.body().toString());
if (response.body() != null) {
final User user2 = new User(
response.body().getID_USER(),
response.body().getUSER_LOGIN(),
response.body().getUserPassword(),
response.body().getUserName(),
response.body().getUserSiret(),
response.body().getUserVATNumber(),
response.body().getUserAddressStreet(),
response.body().getUserAddressCplmt(),
response.body().getUserAddressCity(),
response.body().getUserPhone(),
response.body().getUserCellphone(),
response.body().getUserFax(),
response.body().getUserEmail(),
new ArrayList<Contact>(),
new ArrayList<Contract>(),
new ArrayList<Bill>()
);
UserModel.getInstance().setUser(user2);
UserModel.getInstance().setErrorValue(0);
//WE GET THE USER's CONTACTS :
api.getAllContractsFromUser(user2.getID_USER()).enqueue(new Callback<ArrayList<Contract>>() {
@Override
public void onResponse(Call<ArrayList<Contract>> call, Response<ArrayList<Contract>> response) {
if(response.body()!=null){
UserModel.getInstance().getUser().getValue().getUserContractsList().addAll(response.body());
}
}
@Override
public void onFailure(Call<ArrayList<Contract>> call, Throwable t) {
Log.e("login", "Not able to load contract");
}
});
} else {
UserModel.getInstance().setErrorValue(2);
Log.e("login", "No user with this login password");
}
}
@Override
public void onFailure(Call<User> call, Throwable t) {
UserModel.getInstance().setErrorValue(1);
Log.e("login", "Failure API response");
Log.e("ERROR LOGIN AFTER DATA", UserModel.getInstance().getUser().toString());
Log.e("IS ERROR STATUS ", UserModel.getInstance().isError().getValue().toString());
}
});
}
Here is the console log, as you can see we reach the api methods and the error gets some values but onchange doesnt trigger:
E/login: we called api
E/login: Failure API response
E/ERROR LOGIN AFTER DATA: androidx.lifecycle.MutableLiveData@45014a1
E/IS ERROR STATUS: 1
Any clues ? ^^°
LoginActivity
is gettingUserModel
usingViewModelProviders
, which will actually create a new instance ofUserModel
, not the singleton instance. TheUserModel
that is used by the repository and theLoginActivity
are different instance, and this is why the observer is not called. It is not recommended to have singleton viewmodel. If some data need to be persisted, maybe you can create another singleton object and all viewmodels (which are not singleton!) can simply just reference it. – Sanlok LeeMutableLiveData<User>
or something similar. Just like theUserModel
you already have (the difference is that it shouldn't be used as viewmodel). – Sanlok Lee