How to use 4 simple ViewModels with the same Fragment? - android

I have an app, which displays 4 lists of words in a Fragment (reusing the same class!):
2-letter words
3-letter words
Words containing Russian eh letter
Words containing Russian hard sign letter
Here a screenshot of the navigation drawer (please excuse the non-english language):
Currently my ViewModel stores all 4 lists as LiveData:
public class WordsViewModel extends AndroidViewModel {
private LiveData<List<Word>> mWords2;
private LiveData<List<Word>> mWords3;
private LiveData<List<Word>> mWordsHard;
private LiveData<List<Word>> mWordsEh;
public WordsViewModel(Application app) {
super(app);
mWords2 = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(2);
mWords3 = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(3);
mWordsHard = WordsDatabase.getInstance(app).wordsDao().fetchWordsContaining("Ъ");
mWordsEh = WordsDatabase.getInstance(app).wordsDao().fetchWordsContaining("Э");
}
public LiveData<List<Word>> getWords(int condition) {
switch (condition) {
case R.id.navi_drawer_letters_2:
return mWords2;
case R.id.navi_drawer_letters_3:
return mWords3;
case R.id.navi_drawer_letter_hard:
return mWordsHard;
case R.id.navi_drawer_letter_eh:
return mWordsEh;
}
return mWords2;
}
}
However I am concerned, that fetching all 4 lists at once is suboptimal and might cause a UI delay.
So I have tried splitting the view model into a base class and then 4 inheriting classes -
WordsViewModel (acting now as base class):
public class WordsViewModel extends AndroidViewModel {
protected LiveData<List<Word>> mWords;
public WordsViewModel (Application app) {
super(app);
}
public LiveData<List<Word>> getWords() {
return mWords;
}
}
And the inheriting classes only differ in the DAO method being called -
TwoViewModel (inheriting class):
public class TwoViewModel extends WordsViewModel {
public TwoViewModel(Application app) {
super(app);
mWords = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(2);
}
}
ThreeViewModel (inheriting class):
public class ThreeViewModel extends WordsViewModel {
public ThreeViewModel(Application app) {
super(app);
mWords = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(3);
}
}
Finally (and thanks for reading sofar!) here is my Fragment:
public class WordsFragment extends Fragment {
private final ItemAdapter<WordItem> mItemAdapter = new ItemAdapter<>();
private final FastAdapter<WordItem> mFastAdapter = FastAdapter.with(mItemAdapter);
private WordsViewModel mViewModel;
public static WordsFragment newInstance(int condition) {
WordsFragment f = new WordsFragment();
Bundle args = new Bundle();
args.putInt(KEY_CONDITION, condition);
f.setArguments(args);
return f;
}
#Override
public View onCreateView(#NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
int condition = (getArguments() == null ? -1 : getArguments().getInt(KEY_CONDITION));
switch (condition) {
case R.id.navi_drawer_letter_eh:
mViewModel = ViewModelProviders.of(this).get(EhViewModel.class);
case R.id.navi_drawer_letter_hard:
mViewModel = ViewModelProviders.of(this).get(HardViewModel.class);
case R.id.navi_drawer_letters_3:
mViewModel = ViewModelProviders.of(this).get(ThreeViewModel.class);
default:
mViewModel = ViewModelProviders.of(this).get(TwoViewModel.class);
}
mViewModel.getWords().observe(this, words -> {
mItemAdapter.clear();
for (Word word: words) {
WordItem item = new WordItem();
item.word = word.word;
item.expl = word.expl;
mItemAdapter.add(item);
}
});
Unfortunately, this breaks my app by always displaying the 2-letter words list.
I wonder, why does this happen (because of inheritance?) and how to solve this?
UPDATE:
Here my code for opening the fragment from the main activity using MaterialDrawer and withTag() and I have verified in debugger and logs (and in the Toast which can be seen in the above screenshot), that the condition variable differs:
private final Drawer.OnDrawerItemClickListener mFetchWordsListener = (view, position, drawerItem) -> {
setTitle(drawerItem);
WordsFragment f = WordsFragment.newInstance( (Integer)drawerItem.getTag() );
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.root, f)
.commitAllowingStateLoss();
return false;
};
mNavigationDrawer.addItems(
....
new SectionDrawerItem().withName(R.string.item_dict),
new PrimaryDrawerItem().withOnDrawerItemClickListener(mFindWordListener).withName(R.string.item_find_word).withIcon(R.drawable.magnify).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_find_word),
new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_2).withIcon(R.drawable.letters_2).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_2).withTag(R.id.navi_drawer_letters_2),
new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_3).withIcon(R.drawable.letters_3).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_3).withTag(R.id.navi_drawer_letters_3),
new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_2).withIcon(R.drawable.letters_hard).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_hard).withTag(R.id.navi_drawer_letters_hard),
new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_eh).withIcon(R.drawable.letters_eh).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_eh).withTag(R.id.navi_drawer_letters_eh)
);
UPDATE 2:
Here my DAO interface and BTW I've noticed that (mViewModel instanceof TwoViewModel) is always true for some reason?
#Dao
public interface WordsDao {
#Query("SELECT * FROM table_words WHERE LENGTH(word) = :length")
LiveData<List<Word>> fetchWordsLength(int length);
#Query("SELECT * FROM table_words WHERE word LIKE '%' || :letter || '%'")
LiveData<List<Word>> fetchWordsContaining(String letter);
}

You need to put a 'break' at the end of each case block to escape out of the switch when a case matching expression is found. Without the break statement, control flow will 'fall through' the different case statements after the first matching case is found. In your code, the default case will always be executed, which loads TwoViewModel.

Related

Create test case for the method who modify its input and pass to mock object

I am using Mockito for writing unit test case in Android. I am stuck into one method where I am modify Object and pass it to mocked method, Not able to understand how to write unit test case for this
Class LocationViewModel{
private LocationInteractor locationInteractor;
LocationViewModel (LocationInteractor locationInteractor){
this.locationInteractor =locationInteractor;
}
#Override
public Single<List<String>> getRecentLocations( LocationViewType locationViewType) {
return locationInteractor.getUpdatedRecentLocation(getRecentLocationFilter(locationViewType),locationViewType);
}
private Map<String, String[]> getRecentLocationFilter(LocationViewType locationViewType) {
LocationFilter locationfilter = new LocationFilter();
if (locationViewType == LocationViewType.DEFAULT_LOCATIONS) {
return locationFilter.getRecentDefaultLocationFilter();
} else if (locationViewType == SETTING_LOCATIONS) {
return locationFilter.getRecentSettingLocationFilter();
} else if (locationViewType == LocationViewType.INVENTORY_LOCATION) {
return locationFilter.getRecentSettingLocationFilter();
} else {
return locationFilter.getRecentCurrentLocationFilter();
}
}
}
Class LocationViewModelTest{
#Mock private LocationInteractorContract mockLocationInteractor;
private LocationViewModelContract locationViewModel;
#Before
public void setUp() {
initMocks(this);
locationViewModel = new LocationViewModel(mockLocationInteractor)
}
#Test
public void getRecentLocationsList_check_for_Null() {
when(mockLocationInteractor.getUpdatedRecentLocation(anyMap(),LocationViewType.SETTING_LOCATIONS)) ......Line 1
.thenReturn(Single.error(NullPointerException::new));
locationViewModel
.getRecentLocations(LocationViewType.SETTING_LOCATIONS)
.test()
.assertFailure(NullPointerException.class);
}
}
When I use anyMap() in Line no 1 it throws - org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
When I use new HashMap<>() in Line no 1 it throws NullPointerException
Want to write test case for method - getRecentLocations where getRecentLocationFilter is private method
For the InvalidUseOfMatchersException, the reason is probably that you have to use either all values or all matchers. For example:
when(mockLocationInteractor.getUpdatedRecentLocation(anyMap(), any())

How to refresh data in Architecture Components ViewModel

I'm having the following ViewModel:
public class FeedViewModel extends ViewModel {
private final FeedRepository repository;
private LiveData<Resource<List<Photo>>> feed;
#Inject
FeedViewModel(#NonNull final FeedRepository repository) {
this.repository = repository;
}
LiveData<Resource<List<Photo>>> getUserFeed() {
if (feed == null) {
feed = new MutableLiveData<>();
feed = repository.get();
}
return feed;
}
}
I observe feed in the Fragment this way:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUserFeed().observe(this, feed -> {
switch (feed.status) {
case ERROR:
processErrorState(feed.data);
break;
case LOADING:
processLoadingState(feed.data);
break;
case SUCCESS:
if (feed.data != null)
processSuccessState(feed.data);
break;
}
});
}
The question is: how can I refresh feed the right way? Let's suppose that user triggered swipeToRefresh, so that event must create a refresh task of feed. How can I implement this?
You will have to fetch the data and update live data using liveData.post(new data)
In your Activity:
//to be called on refresh data
viewModel.getLatestFeed()
In your View Model:
fun getLatestFeed() {
//get data from repository
feed.post(refreshedData)
}
You can use a "loadTrigger" to trigger data load , when swiping to refresh .
Add this code to your fragment :
viewModel.userFeeds.observe(
//add observation code
)
swipToRefresh.setOnRefreshListener {
viewModel.loadFeeds()
}
and in the viewModel :
val loadTrigger = MutableLiveData(Unit)
val userFeeds = loadTrigger.switchMap {
repository.get()
}
fun loadFeeds() {
loadTrigger.value = Unit
//livedata gets fired even though the value is not changed
}
The solution is suggested here :
manual refresh without modifying your existing LiveData
I tested it and it workes well.

How to update LiveData value?

I am using the new paging library for my data. Everything works fine when the ViewModel is created and live data is first initialized. Problem is that I can not update the value of my live data when for example I click on menu Item and want to update it with a different set of data. Then onChanged method in my fragment does not get called. I have read about MutableLiveData and methods like setValue and postValue which can update the live data, but in my case, I am using LivePagedListProvider and cannot return MutableLiveData from a database.
Dao:
#Query("SELECT * FROM teams ORDER BY name ASC")
LivePagedListProvider<Integer, Team> getAllTeams();
Fragment:
mTeamViewModel.mTeamsList.observe(this, new Observer<PagedList<Team>>() {
#Override
public void onChanged(#Nullable PagedList<Team> teams) {
mTeamAdapter.setList(teams);
}
});
ViewModel:
public LiveData<PagedList<Team>> mTeamsList;
#Inject
DatabaseManager mDatabaseManager;
void setTeamViewModel(final DatabaseManager databaseManager) {
mDatabaseManager = databaseManager;
mTeamsList = mDatabaseManager.getAllTeams().create(
0,
new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(50)
.setEnablePlaceholders(true)
.build());
}
boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_favorite:
mTeamsList = mDatabaseManager.getFavoriteTeams().create(
0,
new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(50)
.build());
return true;
default: return false;
}
}
Finally I found the solution after the struggle of 3 days,
For updating the value of LiveData<PagedList> we can use MediatorLiveData, as follows, inside your ViewModel class:
public LiveData<PagedList<RecipeListPojo>> liveData;
private MediatorLiveData<PagedList<RecipeListPojo>> mediatorLiveData;
public RecipeListViewModel(#NonNull Application application) {
super(application);
mediatorLiveData = new MediatorLiveData<>();
}
public MediatorLiveData<PagedList<RecipeListPojo>> init(RecipeDao recipeDao, RecipeFrom recipeFrom, String orderBy) {
liveData = new LivePagedListBuilder(recipeDao.getAllRecipesList(simpleSQLiteQuery), 6).build();
mediatorLiveData.addSource(liveData, new Observer<PagedList<RecipeListPojo>>() {
#Override
public void onChanged(#Nullable PagedList<RecipeListPojo> recipeListPojos) {
mediatorLiveData.setValue(recipeListPojos);
}
});
return mediatorLiveData;
}
Further details can be found here about MediatorLiveData
Create a isFavourite column in your model. Then:
#Query("SELECT * FROM teams WHERE isFavourite = true")
LivePagedListProvider<Integer, Team> getFavouriteTeams();
Then you should have a ViewModel with a function/field that returns a MutableLiveData<PagedList<Team>> and in the Fragment/Activity you observe to that field. In your Fragment/Activity you tell the ViewModel every change you need to do to de list showed. Then in the ViewModel you assign different values to the live data list.
Transformations.switchMap can help: https://developer.android.com/reference/android/arch/lifecycle/Transformations#switchmap
class SearchViewModel() : ViewModel(){
val teamId = MutableLiveData<Int>()
var teamsList: LiveData<PagedList<Team>>=
Transformations.switchMap(teamId) { id ->
//just example
if (id <= 0) {
//1
searchDbManager.searchWithoutId()
} else {
//2
anotherSearchDbManager.searchWithId(id)
}.setBoundaryCallback(SearchResultBoundaryCallback()).build()
}
}
fun changeTeamId(id : Int){
teamId.postValue(id)
}
If you want to change the search process to search with team ID (or switch to another datasource), just call from Fragment:
viewModel.changeTeamId(2)

Observing viewmodel for the second time returns null in android

In my android app,im following architecture components with mvvm pattern.
my app makes a network call to display the weather information.api call is being made from repository which returns a livedata of response to the viewmodel,which inturn is observed by my main activity.
the app works fine except for one condition,whenever i disconnect the internet to test the fail case,it inflates error view as required
in the error view i have a retry button,which makes the method call to observe the viewmodel again(this method was also called by oncreate() for the first time,which worked)
even after switching on the internet,and clicking the retry button which listens for the observable.still the data becomes null.
i dont know why.please anyone help
REPOSITORY
#Singleton public class ContentRepository {
#Inject AppUtils mAppUtils;
private RESTService mApiService;
#Inject public ContentRepository(RESTService mApiService) {
this.mApiService = mApiService;
}
public MutableLiveData<ApiResponse<WeatherModel>> getWeatherListData() {
final MutableLiveData<ApiResponse<WeatherModel>> weatherListData = new MutableLiveData<>();
mApiService.getWeatherList().enqueue(new Callback<WeatherModel>() {
#Override public void onResponse(Call<WeatherModel> call, Response<WeatherModel> response) {
weatherListData.setValue(new ApiResponse<>(response.body()));
}
#Override public void onFailure(Call<WeatherModel> call, Throwable t) {
weatherListData.setValue(new ApiResponse<>(t));
}
});
return weatherListData;
}
}
VIEWMODEL
public class HomeViewModel extends AndroidViewModel {
private final LiveData<ApiResponse<WeatherModel>> weatherListObservable;
#Inject public HomeViewModel(Application application, ContentRepository contentRepository) {
super(application);
this.weatherListObservable = contentRepository.getWeatherListData();
}
public LiveData<ApiResponse<WeatherModel>> getWeatherListObservable() {
return weatherListObservable;
}
}
OBSERVE METHOD IN ACTIVITY
private void observeViewModel() {
mHomeViewModel = ViewModelProviders.of(this, mViewModelFactory).get(HomeViewModel.class);
mHomeViewModel.getWeatherListObservable().observe(this, weatherModelApiResponse -> {
if (weatherModelApiResponse.isSuccessful()) {
mErrorView.setVisibility(View.GONE);
mBinding.ivLoading.setVisibility(View.GONE);
try {
setDataToViews(weatherModelApiResponse.getData());
} catch (ParseException e) {
e.printStackTrace();
}
} else if (!weatherModelApiResponse.isSuccessful()) {
mBinding.ivLoading.setVisibility(View.GONE);
mDialogUtils.showToast(this, weatherModelApiResponse.getError().getMessage());
mErrorView.setVisibility(View.VISIBLE);
}
});
}
RETRY BUTTON IN ACTIVITY
#Override public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_retry:
mErrorView.setVisibility(View.GONE);
observeViewModel();
break;
}
}
Updated:- 5 December 2017
I was fortunate to meet Lyla Fujiwara, during Google Developer Days, India where I asked her the same question. She suggested me to user Transformations.switchMap(). Following is the updated solution -
#Singleton
public class SplashScreenViewModel extends AndroidViewModel {
private final APIClient apiClient;
// This is the observable which listens for the changes
// Using 'Void' since the get method doesn't need any parameters. If you need to pass any String, or class
// you can add that here
private MutableLiveData<Void> networkInfoObservable;
// This LiveData contains the information required to populate the UI
private LiveData<Resource<NetworkInformation>> networkInformationLiveData;
#Inject
SplashScreenViewModel(#NonNull APIClient apiClient, #NonNull Application application) {
super(application);
this.apiClient = apiClient;
// Initializing the observable with empty data
networkInfoObservable = new MutableLiveData<Void>();
// Using the Transformation switchMap to listen when the data changes happen, whenever data
// changes happen, we update the LiveData object which we are observing in the MainActivity.
networkInformationLiveData = Transformations.switchMap(networkInfoObservable, input -> apiClient.getNetworkInformation());
}
/**
* Function to get LiveData Observable for NetworkInformation class
* #return LiveData<Resource<NetworkInformation>>
*/
public LiveData<Resource<NetworkInformation>> getNetworkInfoObservable() {
return networkInformationLiveData;
}
/**
* Whenever we want to reload the networkInformationLiveData, we update the mutable LiveData's value
* which in turn calls the `Transformations.switchMap()` function and updates the data and we get
* call back
*/
public void setNetworkInformation() {
networkInfoObservable.setValue(null);
}
}
The Activity's code will be updated as -
final SplashScreenViewModel splashScreenViewModel =
ViewModelProviders.of(this, viewModelFactory).get(SplashScreenViewModel.class);
observeViewModel(splashScreenViewModel);
// This function will ensure that Transformation.switchMap() function is called
splashScreenViewModel.setNetworkInformation();
This looks the most prominent and proper solution to me for now, I will update the answer if I better solution later.
Watch her droidCon NYC video for more information on LiveData. The official Google repository for LiveData is https://github.com/googlesamples/android-architecture-components/ look for GithubBrowserSample project.
Old Code
I have not been able find a proper solution to this, but this works so far -
Declare ViewModel outside the observeViewModel() and change the function like this -
private void observeViewModel(final HomeViewModel homeViewModel) {
homeViewModel.getWeatherListObservable().observe(this, weatherModelApiResponse -> {
if (weatherModelApiResponse.isSuccessful()) {
mErrorView.setVisibility(View.GONE);
mBinding.ivLoading.setVisibility(View.GONE);
try {
setDataToViews(weatherModelApiResponse.getData());
} catch (ParseException e) {
e.printStackTrace();
}
} else if (!weatherModelApiResponse.isSuccessful()) {
mBinding.ivLoading.setVisibility(View.GONE);
mDialogUtils.showToast(this, weatherModelApiResponse.getError().getMessage());
mErrorView.setVisibility(View.VISIBLE);
}
});
}
Update HomeViewModel to -
public class HomeViewModel extends AndroidViewModel {
private final LiveData<ApiResponse<WeatherModel>> weatherListObservable;
#Inject public HomeViewModel(Application application, ContentRepository contentRepository) {
super(application);
getWeattherListData();
}
public void getWeatherListData() {
this.weatherListObservable = contentRepository.getWeatherListData();
}
public LiveData<ApiResponse<WeatherModel>> getWeatherListObservable() {
return weatherListObservable;
}
}
Now Retry button, call the observeViewModel function again and pass mHomeViewModel to it. Now you should be able to get a response.

old ViewModel reused instead of building a new one

Some times when the activity is destroyed (not sure why, memory pressure I presume), a new activity is created, but the old view model bound to the dead activity is reused.
Activity:
[Activity(
LaunchMode = LaunchMode.SingleTask,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class HomeView : MvxTabsFragmentActivity
{
protected override void OnCreate(Bundle bundle)
{
Log.Info("On HomeView created");
base.OnCreate(bundle);
}
protected override void OnDestroy()
{
base.OnDestroy();
Log.Info("On HomeView destroyed");
this.HomeViewModel.CleanUp();
}
}
ViewModel:
public class HomeViewModel : MvxViewModel
{
public HomeViewModel(
IMvxMessenger messenger,
IUserInteraction userInteraction,
DashboardViewModel dashboardViewModel,
AlertSettingsViewModel alertSettingsViewModel)
{
Log.Info("Building home view model");
}
public void CleanUp()
{
Log.Info("HomeViewModel => Clean-up");
}
}
App.cs:
public override void Initialize()
{
this.CreatableTypes().EndingWith("ViewModel").AsTypes().RegisterAsDynamic();
this.RegisterAppStart<HomeViewModel>();
TaskScheduler.UnobservedTaskException +=
(sender, eventArgs) =>
Log.Error("An Unobserved exception has been raised by a task", eventArgs.Exception);
}
Debug output:
On HomeView created
Building home view model
...
On HomeView destroyed
HomeViewModel => Clean-up
...
On HomeView created
[here: no "Building view model" message]
Maybe it is the SingleTask Activity ?
Is there a way (with IoC, or other) to get a fresh view model at every HomeView creation ?
EDIT:
I ran over this method on MvxActivityViewExtensions.cs
public static void OnViewCreate(this IMvxAndroidView androidView, Bundle bundle)
{
MvxActivityViewExtensions.EnsureSetupInitialized(androidView);
MvxActivityViewExtensions.OnLifetimeEvent(androidView, (Action<IMvxAndroidActivityLifetimeListener, Activity>) ((listener, activity) => listener.OnCreate(activity)));
IMvxViewModel cached = Mvx.Resolve<IMvxSingleViewModelCache>().GetAndClear(bundle);
IMvxView view = (IMvxView) androidView;
IMvxBundle savedState = MvxActivityViewExtensions.GetSavedStateFromBundle(bundle);
MvxViewExtensionMethods.OnViewCreate(view, (Func<IMvxViewModel>) (() => cached ?? MvxActivityViewExtensions.LoadViewModel(androidView, savedState)));
}
So would it mean my view model is cached ? How to disable this cache ?
This is covered in the Wiki in https://github.com/MvvmCross/MvvmCross/wiki/Customising-using-App-and-Setup#overriding-viewmodel-locationconstruction
By default, MvvmCross builds a new ViewModel every time one is requested and uses the CIRS sequence - Construction-Init-ReloadState-Start - to initialize that ViewModel.
If you want to override this behaviour for one or more ViewModel types, then you can do this in your App object by supplying your own IMvxViewModelLocator implementation.
For example, you could implement
public class MyViewModelLocator
: MvxDefaultViewModelLocator
{
private SpecialViewModel _special = new SpecialViewModel();
public override bool TryLoad(Type viewModelType, IDictionary<string, string> parameterValueLookup,
out IMvxViewModel model)
{
if (viewModelType == typeof(SpecialViewModel))
{
model = _special;
return true;
}
else if (viewModelType == typeof(FooViewModel))
{
model = new FooViewModel(_special);
return true;
}
return base.TryLoad(viewModelType, parameterValueLookup, out model);
}
}
and could then return this in App.cs:
protected override IMvxViewModelLocator CreateDefaultViewModelLocator()
{
return new MyViewModelLocator();
}

Categories

Resources