I use MVVM structure in my project.
I have the main fragment with list observed a web service as you can see in the code
fragment :
mViewModel.getHomePageList().observe(this, homeDataWrapper -> {
if (homeDataWrapper!=null) {
if (homeDataWrapper.isStatus()) {
binding.homeProgressBar.setVisibility(View.INVISIBLE);
ToastUtil.showTosat(homeDataWrapper.getData().getMessage(), getContext());
Log.d(TAG, "onChanged: ");
}
}
});
view model:
ublic class HomePageViewModel extends AndroidViewModel {
private MutableLiveData<DataWrapper<Home>> data;
public ObservableInt loading;
private HomeRepository homeRepository;
private HomePageAdapter adapter;
public HomePageViewModel(#NonNull Application application) {
super(application);
}
public void init() {
adapter = new HomePageAdapter(R.layout.main_page_list, this);
homeRepository = new HomeRepository();
if (this.data != null) {
// ViewModel is created per Fragment so
// we know the userId won't change
return;
}
data = homeRepository.getHomeScreen();
}
public HomePageAdapter getAdapter() {
return adapter;
}
public void onItemClick(Integer index) {
}
public void onSerachClicked(View view) {
Navigation.findNavController(view).navigate(R.id.action_homePageFragment_to_searchActivity);
}
public MutableLiveData<DataWrapper<Home>> getHomePageList() {
return this.data;
}
}
HomeRepository :
public MutableLiveData<DataWrapper<Home>> getHomeScreen() {
final MutableLiveData<DataWrapper<Home>> homeMutableLiveData = new MutableLiveData<>();
final DataWrapper<Home> dataWrapper = new DataWrapper<>();
RetrofitInstance.getApiService().getHome().enqueue(new Callback<Home>() {
#Override
public void onResponse(#NotNull Call<Home> call, #NotNull Response<Home> response) {
Log.d("", "onResponse: " + response);
if (response.code() == 200) {
dataWrapper.setData(response.body());
dataWrapper.setStatus(true);
homeMutableLiveData.postValue(dataWrapper);
}
}
#Override
public void onFailure(Call<Home> call, Throwable t) {
Log.d("", "onResponse: " + t);
dataWrapper.setApiException((Exception) t);
dataWrapper.setStatus(false);
homeMutableLiveData.postValue(dataWrapper);
}
});
return homeMutableLiveData;
}
I would like to add SwipeRefreshLayout to update the main list. what is the correct way to call the web service again and update the list?
can anyone help me?
You can just call getHomeScreen form your Repository class to trigger data pulling from the server again, after pulling request completed, the observers will be notified using the the MutableLiveData.
But here is your issue, you are creating a new MutableLiveData object each time you call getHomeScreen. Thus, the first one will not be notified and the list will not be updated!
To solve the problem you have to initialize your MutableLiveData somewhere else so it will not be created again every time you call getHomeScreen.
I suggest you make your HomeRepository class a singleton class and initialize the MutableLiveData object inside the constructor, then you can use this object to post data to observers once you got new data from the server.
public class HomeRepository {
private static HomeRepository instance;
private MutableLiveData<DataWrapper<Home>> homeMutableLiveData;
public static HomeRepository getInstance() {
if(instance == null) instance = new HomeRepository();
return instance;
}
private HomeRepository() {
homeMutableLiveData = new MutableLiveData<>();
}
public MutableLiveData<DataWrapper<Home>> getHomeScreen() {
final DataWrapper<Home> dataWrapper = new DataWrapper<>();
RetrofitInstance.getApiService().getHome().enqueue(new Callback<Home>() {
#Override
public void onResponse(#NotNull Call<Home> call, #NotNull Response<Home> response) {
Log.d("", "onResponse: " + response);
if (response.code() == 200) {
dataWrapper.setData(response.body());
dataWrapper.setStatus(true);
homeMutableLiveData.postValue(dataWrapper);
}
}
#Override
public void onFailure(Call<Home> call, Throwable t) {
Log.d("", "onResponse: " + t);
dataWrapper.setApiException((Exception) t);
dataWrapper.setStatus(false);
homeMutableLiveData.postValue(dataWrapper);
}
});
return homeMutableLiveData;
}
}
Inside onRefereshListener of fragment
swifeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
mViewModel.getHomeScreenDetail();
}
});
In Viewmodel create getHomeScreenDetail method
public void getHomeScreenDetail(){
data = homeRepository.getHomeScreen();
}
Related
I got the response correctly, set value of LiveData correctly (got confirmed by printing value on console after setting value to live data). But When i tried to print same thing just before "return" it's giving me NullPointerException.
public class ProjectRepository {
private ProjectRepository instance;
Context context;
public ProjectRepository(Context context) {
this.context=context;
}
private MutableLiveData<List<PojoDivision>> data = new MutableLiveData<>();
public LiveData<List<PojoDivision>> getDivisionList() {
((RetrofitConfiguration)context).getDivisionRestApiWithAuthentication().getDivisionList().enqueue(new Callback<List<PojoDivision>>() {
#Override
public void onResponse(Call<List<PojoDivision>> call, Response<List<PojoDivision>> response) {
if (response.isSuccessful()) {
System.out.println(response.body().get(4).getName()); // this is printing
data.setValue(response.body());
System.out.println(data.getValue().get(4).getName()); // this is printing
}
}
#Override
public void onFailure(Call<List<PojoDivision>> call, Throwable t) {
Log.d(TAG, t.getMessage());
}
});
/*
following code is not printing with nullPointerException
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
*/
System.out.println(data.getValue().get(4).getName());
return data;
}
}
Since you're using a LiveData object at the class level for your repository class, your method has no need to return anything. Instead it's responsibility is for updating that LiveData object.
public class ProjectRepository {
private static final String TAG = "ProjectRepository";
private ProjectRepository instance;
Context context;
public ProjectRepository(Context context) {
this.context = context;
}
public MutableLiveData<List<PojoDivision>> data = new MutableLiveData<>();
public void getDivisionList() {
((RetrofitConfiguration) context).getDivisionRestApiWithAuthentication().getDivisionList().enqueue(new Callback<List<PojoDivision>>() {
#Override
public void onResponse(Call<List<PojoDivision>> call, Response<List<PojoDivision>> response) {
if (response.isSuccessful()) {
data.postValue(response.body());
}
}
#Override
public void onFailure(Call<List<PojoDivision>> call, Throwable t) {
Log.d(TAG, t.getMessage());
}
});
}
}
The clients of this class would need to 1) observe the LiveData object, and 2) call the getDivisionList() method to trigger an update:
class MyFragment extends Fragment {
private ProjectRepository repository;
private void setRepository() {
Context context = getContext();
if (context == null) return;
repository = new ProjectRepository(context);
}
public void observeData() {
repository.data.observe(this, new Observer<List<PojoDivision>>() {
#Override
public void onChanged(List<PojoDivision> pojoDivisions) {
// do something with updated data
}
});
}
public void triggerUpdate() {
repository.getDivisionList();
}
}
I am using LiveData to get data from the server.
In the onResume method, Calling same function every after 5 second
I am able to get data only on the First API call.
Second time the observer is not triggered and not able to get data in the fragment.
this is my fragment:
private int delay = 5 * 1000;
private ViewModel mViewModel;
private DetailsModel details = new DetailsModel();
mViewModel = ViewModelProviders.of(this).get(ViewModel.class);
mViewModel.getDetailsResponse("token", "ids");
mViewModel.getData().observe(this, new Observer< DetailsModel >() {
#Override
public void onChanged(DetailsModel response) {
details = response;
}});
//getting data in every 5 seconds
#Override
public void onResume() {
super.onResume();
liveHandler.postDelayed(runnable = new Runnable() {
public void run() {
mViewModel. getDetailsResponse("token", "ids");
liveHandler.postDelayed(runnable, delay);
}
}, delay);
}
ViewModel.java
private MutableLiveData<DetailsModel> detailsResponse;
private ProjectRepository repository = new ProjectRepository();
public void getDetailsResponse(String token, String ids) {
detailsResponse = repository.getMapData("token", "ids");
}
public MutableLiveData<DetailsModel> getData() {
return detailsResponse;
}
ProjectRepository.java
public MutableLiveData<DetailsModel> getMapData(String token, String ids) {
final MutableLiveData<DetailsModel> responseMutableLiveData = new MutableLiveData<>();
Call<DetailsModel> call = service.getMapDetails(token, ids);
call.enqueue(new Callback<DetailsModel>() {
#Override
public void onResponse(#NonNull Call<DetailsModel> call, #NonNull Response<DetailsModel> response) {
responseMutableLiveData.postValue(response.body());
}
#Override
public void onFailure(#NonNull Call<DetailsModel> call, #NonNull Throwable t) {
t.printStackTrace();
}
});
return responseMutableLiveData;
}
Whenever you call getDetailsResponse, you create a new LiveData object, which is the problem, you should do this in your ProjectRepository
final MutableLiveData<DetailsModel> responseMutableLiveData = new MutableLiveData<>();
public MutableLiveData<DetailsModel> getMapData(String token, String ids) {
Call<DetailsModel> call = service.getMapDetails(token, ids);
call.enqueue(new Callback<DetailsModel>() {
#Override
public void onResponse(#NonNull Call<DetailsModel> call, #NonNull Response<DetailsModel> response) {
responseMutableLiveData.postValue(response.body());
}
#Override
public void onFailure(#NonNull Call<DetailsModel> call, #NonNull Throwable t) {
t.printStackTrace();
}
});
return responseMutableLiveData;
}
And in your VM:
private MutableLiveData<DetailsModel> detailsResponse = null;
private ProjectRepository repository = new ProjectRepository();
public void getDetailsResponse(String token, String ids) {
if (detailsResponse == null) {
detailsResponse = repository.getMapData("token", "ids");
} else {
// Just call it, you already assigned before
repository.getMapData("token", "ids");
}
}
public MutableLiveData<DetailsModel> getData() {
return detailsResponse;
}
So, basically move the object creation out of the function itself. However, the design of your MVVM implementation can be simplified a lot. I would urge to check some examples!
You are using postDelayed() twice, so it is not working. Change your onResume() code to below mentioned.
#Override
public void onResume() {
super.onResume();
liveHandler.postDelayed(runnable = new Runnable() {
public void run() {
mViewModel.getDetailsResponse("token", "ids");
liveHandler.post(runnable);
}
}, delay);
}
I'm new to android and this is the first time trying to use MVP pattern in my code ... as I understand so far is that my view should talk to the presenter and the presenter to the model > then the presenter will talk again to view.. I wish I'm right with that !!! as shown below in my simple code example I'm trying to return a result value from the model to the presenter and then I will use this result value in the presenter to decide which method should I call in the view..I have 2 questions and I hope some helps.
1) The enqueue method is working async and the result value will always return empty or fail or whatever.. because it works alone... and when I try to use the execute method instead I'm facing a NetworkOnMainThreadException error... so how can I make the right way ?
2) Is this a right way in using MVP pattern ?
This is the SignupContract class
public class SignupContract {
public interface IView{
void signupSuccess();
void signupFailed(String message);
}
public interface IPresenter{
void signup(UserProfile userProfile);
}
public interface IModel{
String signup(UserProfile userProfile);
}
}
This is the View code..
public class SignupActivity extends AppCompatActivity implements SignupContract.IView {
//some code
#Override
protected void onCreate(Bundle savedInstanceState) {
//some code
createAccountBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//some code
presenter.signup(userProfile);
}
});
}
#Override
public void signupSuccess() {
/*AppUtils.dismissLoadingDialog(SignupActivity.this,"","");*/
Intent intent = new Intent(SignupActivity.this, SigninActivity.class);
startActivity(intent);
finish();
}
#Override
public void signupFailed(String message) {
/*AppUtils.dismissLoadingDialog(SignupActivity.this,"","");*/
AppUtils.showErrorMessage(SignupActivity.this, message);
}
and this is the presenter
public class SignupPresenter implements SignupContract.IPresenter {
SignupContract.IView view;
SignupContract.IModel model;
public SignupPresenter(SignupContract.IView view){
model = new SignupModel();
this.view = view;
}
#Override
public void signup(UserProfile userProfile) {
userProfile = UserProfileCleaner.clean(userProfile, "signup");
UserProfileDTO dto = new UserProfileDTO();
String validationMessage = dto.validateUserProfile(userProfile, "signup");
if(validationMessage != null && !validationMessage.equals("")){
view.signupFailed(validationMessage);
}else{
String signupResult = model.signup(userProfile);
if(signupResult.equals("success")){
view.signupSuccess();
}else {
view.signupFailed(signupResult);
}
}
}
}
and this is the model class
public class SignupModel implements SignupContract.IModel {
private String TAG = "SignupModel";
private String result = "";
#Override
public String signup(UserProfile userProfile) {
final Context context = DKApp.getContext();
ServiceWrapper serviceWrapper = new ServiceWrapper(null);
Call<SignupResponse> userSignUpCall = serviceWrapper.userSignUpCall(userProfile.getUser().getUsername(),
userProfile.getUser().getPassword(),userProfile.getPhoneNumber(), userProfile.getEmailAddress(),
userProfile.getFullName());
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if( response.body() != null && response.isSuccessful() ){
Log.e(TAG,response.body().toString());
if(response.body().getStatus() == 1){
//some code
result = "success";
}else{
result = response.body().getMessage();
}
}else{
result = context.getResources().getString(R.string.request_failed);
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
result = context.getResources().getString(R.string.request_failed);
}
});
return result;
}
}
You are making a asynchronous call in the model which may take 100ms or 2-4sec so getting signupResult from it like String signupResult = model.signup(userProfile);this is wrong.
Changes you will require :
1) Add onComplete method to IPresenter and change IModel
public interface IPresenter{
void signup(UserProfile userProfile);
//add
void onComplete(String signUpresult);
}
public interface IModel{
//changed
void signup(UserProfile userProfile);
}
2) In your SignupPresenter pass the instance of presenter to model
public class SignupPresenter implements SignupContract.IPresenter {
..
public SignupPresenter(SignupContract.IView view){
model = new SignupModel(this);
this.view = view;
}
...
#Overrides
public void onComplete(String signupResult){
if(signupResult.equals("success")){
view.signupSuccess();
}else {
view.signupFailed(signupResult);
}
}
...
}
3) In your SignupModel once the result is aquired call onComplete(//result) from the presenter
public class SignupModel implements SignupContract.IModel {
SignupPresenter presenter;
public SignupModel(SignupPresenter presenter){
this.presenter = presenter
}
#Override
public void signup(UserProfile userProfile) {
...
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if(response.body() != null && response.isSuccessful() ){
if(response.body().getStatus() == 1){
//some code
presenter.onComplete("success");
}else{
presenter.onComplete(response.body().getMessage());
}
}else{
presenter.onComplete(context.getResources().getString(R.string.request_failed));
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
presenter.onComplete(context.getResources().getString(R.string.request_failed));
}
});
}
}
Needful : Show progress dialogue when signup is called and dismiss the same on onComplete in SignupPresenter.
Your understanding is quite good also know that the model should talk with presenter as well. To know more about MVP design pattern read this
In MVP pattern you should make view dummy. Presenter always tells view what to do.
Here is an example:
In View =>
presenter.login(userName, password)
In Presenter =>
fun login(userName: String, password: String) {
//login logic
if(success) view.showLoginSuccess()
else view.showLoginError()
}
This is a very short explanation of the MVP pattern. Regarding to your question you can not make requests on Main Thread. Luckily Retrofit has a thread management system. But you shouldn't use it in a model like this you can use it in presenter or search for clean architecture articles for the more right way. If you use it in presenter you should do something like
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if( response.body() != null && response.isSuccessful() ){
Log.e(TAG,response.body().toString());
if(response.body().getStatus() == 1){
//some code
view.showMessage("success");
}else{
view.showMessage(response.body().getMessage());
}
}else{
view.showError(context.getResources().getString(R.string.request_failed));
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
view.showError(context.getResources().getString(R.string.request_failed));
}
});
I am able to make a network request and get back a response inside my data repository but not able to get that inside my view model.
Data repository:
public class DataRepository {
private APIService apiService;
private static DataRepository INSTANCE = null;
public MutableLiveData<ResponseEntity> loginUser(UserEntity userEntity){
final MutableLiveData<ResponseEntity> responseEntity = new MutableLiveData<>();
apiService.loginUser(userEntity)
.enqueue(new Callback<ResponseEntity>() {
#Override
public void onResponse(Call<ResponseEntity> call, Response<ResponseEntity> response) {
Log.d(Constants.LOGGER, "from data repository " + response.body());
responseEntity.setValue(response.body());
}
#Override
public void onFailure(Call<ResponseEntity> call, Throwable t) {
Log.d(Constants.LOGGER, "from data repository: there was an error");
responseEntity.setValue(null);
}
});
return responseEntity;
}
}
View model:
public class LoginViewModel extends AndroidViewModel {
private MutableLiveData<ResponseEntity> networkResponse;
public void sendLoginNetworkRequest(UserEntity userEntity){
networkResponse = mRepository.loginUser(userEntity);
}
public MutableLiveData<ResponseEntity> getResponse(){
return networkResponse;
}
Activity:
public class LoginActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel.class);
loginViewModel.getResponse()
.observe(this, new Observer<ResponseEntity>() {
#Override
public void onChanged(#Nullable ResponseEntity responseEntity) {
Log.d(Constants.LOGGER, "response entity changed " + responseEntity);
}
});
}
public void loginClicked(View view) {
loginViewModel.sendLoginNetworkRequest(userEntity);
}
}
The log from the data repository shows up but the one from the activity doesn't. What am I doing wrong?
I found the answer!
I had to make the responseEntity MutableLiveData variable in my DataRepository class into a class variable and create a function which returns that and now it works!
Repository:
public class DataRepository {
private APIService apiService;
private static DataRepository INSTANCE = null;
final MutableLiveData<ResponseEntity> responseEntity = new MutableLiveData<>();
public void loginUser(UserEntity userEntity){
apiService.loginUser(userEntity)
.enqueue(new Callback<ResponseEntity>() {
#Override
public void onResponse(Call<ResponseEntity> call, Response<ResponseEntity> response) {
Log.d(Constants.LOGGER, "from data repository " + response.body());
responseEntity.setValue(response.body());
}
#Override
public void onFailure(Call<ResponseEntity> call, Throwable t) {
Log.d(Constants.LOGGER, "from data repository: there was an error");
responseEntity.setValue(null);
}
});
}
public MutableLiveData<ResponseEntity> getLiveResponses(){
return responseEntity;
}
}
Viewmodel:
public class LoginViewModel extends AndroidViewModel {
public void sendLoginNetworkRequest(UserEntity userEntity){
mRepository.loginUser(userEntity);
}
public MutableLiveData<ResponseEntity> getResponse(){
return mRepository.getLiveResponse;
}
}
I am observing my viewmodel in an Activity:
private void setupViewModel() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
mSortCategory = sharedPreferences.getString(getString(R.string.pref_sort_key), getString(R.string.pref_sort_popular));
MainViewModelFactory factory = new MainViewModelFactory(mSortCategory);
mViewModel = ViewModelProviders.of(this, factory).get(MainViewModel.class);
mViewModel.getResponse().observe(this, new Observer<MovieResponse>() {
#Override
public void onChanged(MovieResponse movieResponse) {
mMovieAdapter.setData(movieResponse.getResults());
mCurrentPage = movieResponse.getPage();
mTotalPages = movieResponse.getTotalPages();
Log.d(TAG, "setupviewmodel: " + movieResponse.getResults().get(0).getOriginalTitle());
}
});
and here is my VM:
public class MainViewModel extends ViewModel {
private MutableLiveData<MovieResponse> mMoviesResponseLiveData;
public MainViewModel(String category) {
mMoviesResponseLiveData = Repository.getInstance().loadMoviesFromApi(category, 1);
}
public void loadMovies(String category, int currentPage) {
mMoviesResponseLiveData = Repository.getInstance().loadMoviesFromApi(category, currentPage);
}
public MutableLiveData<MovieResponse> getResponse() {
return mMoviesResponseLiveData;
}
and here I make a call to retrofit:
public MutableLiveData<MovieResponse> loadMoviesFromApi(String sort, int page) {
final MutableLiveData<MovieResponse> data = new MutableLiveData<>();
Call<MovieResponse> call = mApiService.getMoviesResponse(sort, BuildConfig.OPEN_WEATHER_MAP_API_KEY, page);
Log.d(TAG, "loadMoviesFromApi: " + call.request().url().toString());
call.enqueue(new Callback<MovieResponse>() {
#Override
public void onResponse(Call<MovieResponse> call, Response<MovieResponse> response) {
if (response.isSuccessful()) {
data.setValue(response.body());
}
}
#Override
public void onFailure(Call<MovieResponse> call, Throwable t) {
t.printStackTrace();
}
});
return data;
}
As I understand new data should be asynchronously loaded by calling a method from activity on Viewmodel :
private void updateUI() {
mViewModel.loadMovies(mSortCategory, mCurrentPage);
}
I receive data from retrofit. but for some reason Livedata is not being observed.
Your observer isn't notified because of this method:
public void loadMovies(String category, int currentPage) {
mMoviesResponseLiveData = Repository.getInstance().loadMoviesFromApi(category, currentPage);
}
loadMoviesFromApi returns a new instance of LiveData so the LiveData returned from getResponse is no longer referenced by your viewModel. If you want your observer to be notified you should return the ViewModel from method loadMovies and observe it or invoke mMoviesResponseLiveData.postValue(responseFromRetrofit)