I'm new to Android development and i am trying to understand Live Data with MVVM architecture.
I am trying to make the main activity recognize when there is a change in an object that belong to the view-model of the activity.
I have created a simple login activity that takes the text from the username and password fields and passes them to the view-model's login function, then the function sends the data to the users repository and then it makes a POST request toa spring-server that is running on my PC.
The repository login function returns a MutableLiveData object with the logged-in username if the username and password are right, and null as it's value otherwise.
The repository works fine( the data coming back from the server is correct). The view-model has a field of type MutableLiveData and it is need to be updated after the login function is called. In the activity there is an observer that supposed to be notified when a changed accrued in the loggedInUser field (of type MutableLiveData) field and even though there is a change, the function onChange of the observer is never activated.
There is some code that i hope will help me explain better.
Main activity:
public class MainActivity extends AppCompatActivity {
public EditText usernameTxt;
public EditText passwordTxt;
public Button loginBtn;
public String loggedInuUser;
LoginViewModel loginViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
usernameTxt = findViewById(R.id.usernameTxt);
passwordTxt = findViewById(R.id.passwordTxt);
loginBtn = findViewById(R.id.loginBtn);
loginViewModel = ViewModelProviders.of(this ).get(LoginViewModel.class);
loginViewModel.init();
try {
loginViewModel.getLoggedInUser().observe(this, new Observer<String>() {
#Override
public void onChanged(#Nullable String s) {
Toast toast= Toast.makeText(MainActivity.this,"changed" , Toast.LENGTH_LONG );
toast.show();
}
}
);
}catch (Exception e){
System.out.println("==========================================================");
System.out.println( e.getMessage());
System.out.println("==========================================================");
}
}
protected void onLogInCliked(View v ){
// Toast toast= Toast.makeText(getApplicationContext(),loggedInuUser, Toast.LENGTH_LONG );
// toast.show();
loginViewModel.login(usernameTxt.getText().toString(),passwordTxt.getText().toString());
// Toast toast2= Toast.makeText(getApplicationContext(),loggedInuUser, Toast.LENGTH_LONG );
// toast2.show();
}
}
view-model:
public class LoginViewModel extends ViewModel {
private UsersRepository usersRepository;
private MutableLiveData<String> loggedInUser;
public void init(){
if(loggedInUser!= null){
return;
}
usersRepository = UsersRepository.getInstance();
loggedInUser=new MutableLiveData<>();
}
public MutableLiveData<String> getLoggedInUser(){
return loggedInUser;
}
public void login(String userName , String hashedPassword) {
loggedInUser = usersRepository.login(userName, hashedPassword);
}
}
repository:
public class UsersRepository {
private static UsersRepository usersRepository;
public static UsersRepository getInstance(){
if (usersRepository == null){
usersRepository = new UsersRepository();
}
return usersRepository;
}
private UsersRepositoryApi usersRepositoryApi;
public UsersRepository(){
usersRepositoryApi = RetrofitService.cteateService(UsersRepositoryApi.class);
}
public MutableLiveData<String> login(String username , String hashedPassword){
final MutableLiveData<String> loggedInUser = new MutableLiveData<>();
User user = new User(username,hashedPassword);
usersRepositoryApi.login(user).enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
loggedInUser.setValue(response.body());
}
}
#Override
public void onFailure(Call<String> call, Throwable t) {
loggedInUser.setValue(null);
}
});
return loggedInUser;
}
}
In the mainActivity i set the observer and i expect the app to show my the Toast message but nothing happens.
i have tried to see what happens in the view-model and it is a little strange,
so i printed stuff like this:
public void login(String userName , String hashedPassword) {
System.out.println("222======================================");
System.out.println("==========================================");
System.out.println("==========================================");
System.out.println(loggedInUser.getValue());
System.out.println("==========================================");
System.out.println("==========================================");
System.out.println("==========================================");
loggedInUser = usersRepository.login(userName, hashedPassword);
System.out.println("333======================================");
System.out.println("==========================================");
System.out.println("==========================================");
System.out.println(loggedInUser.getValue());
System.out.println("==========================================");
System.out.println("==========================================");
System.out.println("==========================================");
}
in first time that i run the login function the output of both 222 and 333 was null, but in the second time i run the login function the output of 222 was the loggedInUser and the output of 333 was null
in both cases the on change function of the observer was unvisited
does anyone have any idea of what i am doing wrong??
thank you
ronen!
here your problem is with repository code inside repository you are creating new object of mutable live data and observing different one.
Interface Callback{
onSuccess(String response)
onError(String error)
}
public void login(String username , String hashedPassword,Callback callback){
final MutableLiveData<String> loggedInUser = new MutableLiveData<>();
User user = new User(username,hashedPassword);
usersRepositoryApi.login(user).enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
callback.onSuccess(response.body());
}
}
#Override
public void onFailure(Call<String> call, Throwable t) {
callback.onError(null);
}
});
}
//login method of your viewmodel
public void login(String userName , String hashedPassword) {
usersRepository.login(userName, hashedPassword,new Callback(){
void onSuccess(String responsebody){
loggedInUser.setValue(responsebody);
}
void onError(String error){
loggedInUser.setValue(responsebody);
}
});
}
In your repository, try changing this part:
loggedInUser.setValue(response.body());
to postValue function. like that:
loggedInUser.postValue(response.body());
Solution by OP.
As Neha Rathore suggested this is the solution that works for me:
in the view Model:
public void login(String userName , String hashedPassword) {
usersRepository.login(userName, hashedPassword, new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
loggedInUser.setValue(response.body());
}
#Override
public void onFailure(Call<String> call, Throwable t) {
loggedInUser.setValue(null);
}
});
}
and in the Repository:
public void login(String username, String hashedPassword,#Nullable final Callback<String> callback){
final MutableLiveData<String> loggedInUser = new MutableLiveData<>();
User user = new User(username,hashedPassword);
usersRepositoryApi.login(user).enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful()) {
callback.onResponse(call,response);
}
}
#Override
public void onFailure(Call<String> call, Throwable t) {
callback.onFailure(call,t);
}
});
}
Related
i have setup retrofit client and the api interface, i have some code as follows
public Result<LoggedInUser> login(String username, String password) {
try {
apiInterface = ApiClient.getClient().create(ApiInterface.class);
RequestData requestData = new RequestData(username, password);
Call<RequestResponse> call = apiInterface.login(requestData);
call.enqueue(new Callback<RequestResponse>() {
#Override
public void onResponse(Call<RequestResponse> call, Response<RequestResponse> response) {
RequestResponse resource = response.body();
System.out.println(resource.getData().getAccessToken());
}
#Override
public void onFailure(Call<RequestResponse> call, Throwable t) {
call.cancel();
}
});
LoggedInUser fakeUser = new LoggedInUser(
java.util.UUID.randomUUID().toString(),
"Jane Doe");
return new Result.Success<>(fakeUser);
} catch (Exception e) {
return new Result.Error(new IOException("Error logging in", e));
}
}
I am not familar with the pattern that should be used to notify the repository class when the rest call obtains it's response.
the above code is called from the following code in the repository class
public Result<LoggedInUser> login(String username, String password) {
// handle login
Result<LoggedInUser> result = dataSource.login(username, password);
if (result instanceof Result.Success) {
setLoggedInUser(((Result.Success<LoggedInUser>) result).getData());
}
return result;
}
I need when refresh page make request to API and insert getting data to my room database. But when I try to insert data I get io.reactivex.exceptions.OnErrorNotImplementedException: UNIQUE constraint failed: data.id (code 1555). So I decided to check is my table empty and if isn't make update request, but so my recyclerview doesn't work normally, and data doesn't update properly on db. Here is my code:
private void getData() {
progressBar.setVisibility(View.VISIBLE);
APIInterface service = RetrofitInstance.getRetrofitInstance().create(APIInterface.class);
Call<DataAll> call = service.getData();
call.enqueue(new Callback<DataAll>() {
#Override
public void onResponse(Call<DataAll> call, Response<DataAll> response) {
if(response.body() != null) {
List<Data> allData = response.body().getData();
Disposable disposable = db.dataDao().dataSize().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Integer>() {
#Override
public void accept(Integer dbSize) throws Exception {
if(dbSize > 0)
updateData(allData);
else
insertData(allData);
fillListFromDb();
}
});
}
}
#Override
public void onFailure(Call<DataAll> call, Throwable t) {
tvErrorMessage.setVisibility(View.VISIBLE);
recyclerDataList.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
swipeRefreshToUpdateList.setRefreshing(false);
Toast.makeText(getApplicationContext(), R.string.errorMessage, Toast.LENGTH_LONG).show();
t.printStackTrace();
}
});
}
private void fillListFromDb() {
Disposable disposable = db.dataDao().getAllData().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<List<Data>>() {
#Override
public void accept(List<Data> data) throws Exception {
listData.clear();
listData.addAll(data);
adapter = new MyAdapter(listData);
adapter.notifyDataSetChanged();
recyclerDataList.setAdapter(adapter);
progressBar.setVisibility(View.GONE);
swipeRefreshToUpdateList.setRefreshing(false);
}
});
}
private void updateData(List<Data> data) {
Completable.fromAction( () ->
db.dataDao().updateData(data))
.subscribeOn(Schedulers.io())
.subscribe();
}
private void insertData(List<Data> data) {
Completable.fromAction(() ->
db.dataDao().addData(data)).
subscribeOn(Schedulers.io())
.subscribe();
}
And onCreate method:
#Override
protected void onCreate(Bundle savedInstanceState) {
// ……
swipeRefreshToUpdateList.setOnRefreshListener(() -> {
tvErrorMessage.setVisibility(View.GONE);
recyclerDataList.setVisibility(View.VISIBLE);
getData();
});
}
Please help me
If you want an insert operation to overwrite existing object in DB, then you should use OnConflictStrategy.REPLACE.
See example:
#Dao
interface CatDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCats(cats: List<Cat>)
You have to check you List<Data> one by one in your updateData
For exemple :
#Override
public void onResponse(Call<DataAll> call, Response<DataAll> response) {
if(response.body() != null) {
List<Data> allData = response.body().getData();
for (Data data:allData){
if(isStored(data.getId())){
updateData(data);
else {
insertData(data);
}
}
}
}
}
}
you can create a new method in your DataDao findDataById(string id) and use it the boolean isStored(String id) method and update it directly
Good luck ! hope that will help you.
I have fragment and Common class which inside it used retrofit callback..I have Connect_and_get class.It sends request to server and gets information.I must use this information in my fragment.. But I can't return result onResponse.How can I do it..(Response is coming well from server)
Please see my code
public class Connect_and_Get {
private int size;
private OkHttpClient.Builder httpClient;
private ApiService client;
private Call<Response> call;
private MyPreference myPreference;
String a[] = {"secret"};
String b[] = {"secret"};
public int Connect_and_Get() {
Requests request;
request = new Requests("tasks.list", new params(20, 0, a, b, "", "", "", "", ""));
httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {
#Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder().addHeader("Authorization", "Bearer " + "secret").build();
return chain.proceed(request);
}
});
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Constants.baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build();
client = retrofit.create(ApiService.class);
call = client.getDocument(request);
call.enqueue(new Callback<Response>() {
#Override
public void onResponse(Call<Response> call, retrofit2.Response<Response> response) {
size = response.body().getResult().getList().size();
}
#Override
public void onFailure(Call<Response> call, Throwable t) {
}
});
//retruning information
return size;
}
}
and result from common class coming 0;Because it doesn't wait my response so it is returning 0;
In fragment
Connect_and_Get a = new Connect_and_Get();
int getting = a.Connect_and_Get();
Log.d("mylog", "result:"+String.valueOf(getting));
Declare an interface like this
public interface ResponseListener {
public int onResponse(int size);
}
and use below code in your activity
public class MainActivity extends AppCompatActivity implements ResponseListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Connect_and_Get().Connect_and_Get(this);
}
#Override
public int onResponse(int size) {
// to do
return 0;
}
}
modify your connect class like this
public class Connect_and_Get {
public int Connect_and_Get(ResponseListener responseListener) {
// as it was
call.enqueue(new Callback<Response>() {
#Override
public void onResponse(Call<Response> call, retrofit2.Response<Response> response) {
size = response.body().getResult().getList().size();
responseListener.onResponse(size);
}
#Override
public void onFailure(Call<Response> call, Throwable t) {
}
});
}
}
You need to check whether your response is successful or not.
Check the code below.
call.enqueue(new Callback<Response>() {
#Override
public void onResponse(Call<Response> call, retrofit2.Response<Response> response) {
if(response.isSuccessful()){
//enter code here
} else {
//Error Message
}
}
#Override
public void onFailure(Call<Response> call, Throwable t) {
Log.e("Log", "Error -> "+t.getLocalizedMessage());
}
});
You can use an event bus like (rxbus,otto, etc..) to post events across your app when the response from the api ready to use .
Retrofit callback sample code:
call.enqueue(new Callback<Response>() {
#Override
public void onResponse(Call<Response> call, retrofit2.Response<Response> response) {
Bus.getInstance.post(Event)
}
#Override
public void onFailure(Call<Response> call, Throwable t) {
}
});
Fragment sample:
#Override
protected void onCreate(Bundle savedInstanceState) {
Bus.getInstance.register(this)
}
#Subscribe
public void onCallDone(Event response) {
//enter code here
}
I want to do an unit test that verifies if function1() or function2() were called. I haven't work with callbacks before, can you give me any idea about how to do it?
public void sendData(HttpService service, Document userData) {
Call<String> call = service.updateDocument(getId(), userData);
call.enqueue(new Callback<String>() {
#Override
public void onResponse(Call<String> call, Response<String> response) {
function1(response.code());
}
#Override
public void onFailure(Call<String> call, Throwable t) {
function2();
}
});
}
I couldn't try, but it should work. Maybe you have to fix generic type
casting errors like mock(Call.class);.
#Test
public void should_test_on_response(){
Call<String> onResponseCall = mock(Call.class);
doAnswer(invocation -> {
Response response = null;
invocation.getArgumentAt(0, Callback.class).onResponse(onResponseCall, response);
return null;
}).when(onResponseCall).enqueue(any(Callback.class));
sendData(....);
// verify function1
}
#Test
public void should_test_on_failure(){
Call<String> onResponseCall = mock(Call.class);
doAnswer(invocation -> {
Exception ex = new RuntimeException();
invocation.getArgumentAt(0, Callback.class).onFailure(onResponseCall, ex);
return null;
}).when(onResponseCall).enqueue(any(Callback.class));
sendData(....);
// verify function2
}
Condition: I've a button. When user click the button (example ADD NEW NAME button) so the data will send to server.
I've question: How to add new data to server?
What should I put into onResponse and onFailure based on this code:
private void sendDataToServer() {
APIClient client = ServiceGenerator.createService(APIClient.class);
Call<ResponseBody> result = client.addNewName(etName.getText().toString(),
etEnglish.getText().toString(),
etGender.getText().toString());
result.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Response<ResponseBody> response) {
}
#Override
public void onFailure(Throwable t) {
}
}
);}
*et - Edit Text
Thanks.