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)
Related
I have a Login Fragment which uses API call to login. I use mvvm and databinding to bind views with viewmodel. In viewmodel Login Response via retrofit is observed in viewmodel which uses RxJava.
I need to observe the retrofit response in the loginFragment, which is not get observed when retrofit response came. Following are the fragment and viewmodel code. I need retrofit response to pass to fragment or fragment get automatically observe response.
public class LoginFragment extends Fragment {
private LoginViewModel mLoginViewModel;
private Observable<LoginResult> dataObservable;
public static String TAG = LoginFragment.class.getSimpleName();
public Disposable disposable;
public static Fragment LoginFragmentInstance() {
Log.e(TAG, "LoginFragmentInstance: " );
Fragment fragment = new LoginFragment();
return fragment;
}
public LoginFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
FragmentLoginBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_login, container, false);
mLoginViewModel = new LoginViewModel(getActivity());
//setViewModel method name changes based on variable name declared in XML
//mLoginViewModel.loginResult.observeO
dataObservable= mLoginViewModel.loginResult;
disposable = dataObservable.subscribe(new Consumer<LoginResult>() {
#Override
public void accept(LoginResult result) throws Exception {
Log.d("TAG", result.toString());
}
});
binding.setViewModel(mLoginViewModel);
return binding.getRoot();
}
#Override
public void onDestroy() {
mLoginViewModel.destroy();
disposable.dispose();
super.onDestroy();
}
}
ViewModel File
public class LoginViewModel {
private static final String TAG = "LoginViewModel";
public ObservableField<String> userName = new ObservableField<>();
public ObservableField<String> password = new ObservableField<>();
public ObservableField<String> email = new ObservableField<>();
public ObservableField<String> userNameErr = new ObservableField<>();
public ObservableField<String> passwordErr = new ObservableField<>();
public ObservableField<String> emailErr = new ObservableField<>();
public Observable<LoginResult> loginResult = new Observable<LoginResult>() {
#Override
protected void subscribeActual(Observer<? super LoginResult> observer) {
}
};
public ObservableField<Boolean> enableLogin;
private CompositeDisposable myCompositeDisposable = new CompositeDisposable();
private HashMap<String, String> loginApiParams;
public Action signIn;
public Context context;
public LoginViewModel(final Context context) {
this.context = context;
Observable result = Observable.combineLatest(FieldUtils.toObservable(userName), FieldUtils.toObservable(password),
new BiFunction() {
#Override
public Object apply(Object userName, Object password) throws Exception {
int failCount = 0;
if (!InputValidator.validateMobileno(userName.toString())) {
++failCount;
userNameErr.set(context.getResources().getString(R.string.mobileno_incorrect));
} else {
userNameErr.set("");
}
if (!InputValidator.validatePassword(password.toString())) {
++failCount;
passwordErr.set(context.getResources().getString(R.string.password_incorrect));
} else {
passwordErr.set("");
}
return failCount == 0;
}
});
enableLogin = FieldUtils.toField(result);
signIn = new Action() {
#Override
public void run() throws Exception {
Log.d(TAG, "signIn button clicked");
loginCall();
}
};
}
private void loginCall() {
loginApiParams = new HashMap<>();
// loginApiParams.put(, paymentType.toString())
loginApiParams.put(ApiParameterKeyConstants.MOBILE,userName.get());
loginApiParams.put(ApiParameterKeyConstants.PASSWORD, password.get());
UserApi usersService = ApiService.INSTANCE.apiCall();
Disposable disposable = usersService.getLogin(loginApiParams)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<LoginResult>() {
#Override
public void accept(LoginResult result) throws Exception {
loginResult = Observable.just(result);
//loginResult.subscribe()
//loginResult = result ;
//Log.d(TAG, "Login Successfull");
}
}, new Consumer<Throwable>()
{
#Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "Login Failed");
}
});
myCompositeDisposable.add(disposable);
}
}
It seems like you are re-assigning the loginResult an Observable in your loginCall method of the ViewModel instead of passing the result to its Observers.
You should try calling loginResult.onNext(result) or loginResult.onComplete(result) instead of loginResult = Observable.just(result);
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 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();
}
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 have an API interface which has two overloaded methods:
public interface Api {
#GET("movie/now_playing")
Call<ApiResponse> getMovies(#Query("page") int page);
#GET("search/movie")
Call<ApiResponse> getMovies(#Query("query") String query, #Query("page") int page);
}
First one is used to get now playing movies and the second one to search for movies.
In my MainActivity I use this code to display the data in a RecyclerView:
MovieViewModel movieViewModel = ViewModelProviders.of(this).get(MovieViewModel.class);
MovieAdapter adapter = new MovieAdapter(this);
movieViewModel.moviePagedList.observe(this, adapter::submitList);
recyclerView.setAdapter(adapter);
This is my MovieViewModel class:
public class MovieViewModel extends ViewModel {
LiveData<PagedList<ApiResponse.Movie>> moviePagedList;
public MovieViewModel() {
MovieDataSourceFactory movieDataSourceFactory = new MovieDataSourceFactory();
PagedList.Config config = new PagedList.Config.Builder().setEnablePlaceholders(false).setPageSize(20).build();
moviePagedList = new LivePagedListBuilder<>(movieDataSourceFactory, config).build();
}
}
And this my MovieDataSourceFactory class:
public class MovieDataSourceFactory extends DataSource.Factory<Integer, ApiResponse.Movie> {
private MutableLiveData<PageKeyedDataSource<Integer, ApiResponse.Movie>> movieLiveDataSource = new MutableLiveData<>();
#Override
public DataSource<Integer, ApiResponse.Movie> create() {
MovieDataSource movieDataSource = new MovieDataSource();
movieLiveDataSource.postValue(movieDataSource);
return movieDataSource;
}
}
And this is my MovieDataSource class:
public class MovieDataSource extends PageKeyedDataSource<Integer, ApiResponse.Movie> {
#Override
public void loadInitial(#NonNull LoadInitialParams<Integer> params, #NonNull LoadInitialCallback<Integer, ApiResponse.Movie> callback) {
Api api = RetrofitClient.getInstance().getApi();
Callback<ApiResponse> call = new Callback<ApiResponse>() {
#Override
public void onResponse(#NonNull Call<ApiResponse> call, #NonNull Response<ApiResponse> response) {
if(response.body() != null){
callback.onResult(response.body().movieList, null, page + 1);
}
}
#Override
public void onFailure(#NonNull Call<ApiResponse> call, #NonNull Throwable t) {}
};
api.getMovies(page).enqueue(call);
}
//loadBefore and loadAfter methods
}
If I run this code, everything works fine, the first query is invoked and I get the result correctly. The question is, how can I dynamically use one or the other?
api.getMovies(query, page).enqueue(call);
boolean globalVariableShouldUseGetMoviesCallWithOnlyOneArgument = true
private void doNetworkCallAndCheckBooleanStatus(){
if (globalVariableShouldUseGetMoviesCallWithOnlyOneArgument) {
getMovies(thisIsOneArgumentAsYouCanSee);
} else {
getMovies(thisIsUsingTwoArguments, asYouCanSee);
}
}
private void someOtherMethodInTheApp() {
globalVariableShouldUseGetMoviesCallWithOnlyOneArgument = false
doNetworkCallAndCheckBooleanStatus()
}