0
votes

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 ? ^^°

Have You tried to use postValue instead of setValue in Your setUser and setErrorValue methods?Pavel B.
yes it does not work :/ it gives me a null pointer exception on the IS ERROR STATUS Log.e which means it did not put value in it :(Lolfish
and when I add a log.e in the UserModel class after setting the value it also returns me the value, so the value is taken but the osberver is not triggered :/Lolfish
There is a good chance that your singleton implementation is flawed. LoginActivity is getting UserModel using ViewModelProviders, which will actually create a new instance of UserModel, not the singleton instance. The UserModel that is used by the repository and the LoginActivity 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 Lee
@Lolfish I think you can try a normal singleton class and have a member variable that is MutableLiveData<User> or something similar. Just like the UserModel you already have (the difference is that it shouldn't be used as viewmodel).Sanlok Lee