Keeping data State ViewModels - android

So according to android developers: "Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI. ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance."
In the code below there is an asynchronous class that gets called in deleteItem function. My question is this: Does ViewModel also handles the asynchronous calls made inside it or will cause memory leaks?
Thank you
public class BorrowedListViewModel extends AndroidViewModel {
private final LiveData<List<BorrowModel>> itemAndPersonList;
private AppDatabase appDatabase;
public BorrowedListViewModel(Application application) {
super(application);
appDatabase = AppDatabase.getDatabase(this.getApplication());
itemAndPersonList = appDatabase.itemAndPersonModel().getAllBorrowedItems();
}
public LiveData<List<BorrowModel>> getItemAndPersonList() {
return itemAndPersonList;
}
public void deleteItem(BorrowModel borrowModel) {
new deleteAsyncTask(appDatabase).execute(borrowModel);
}
private static class deleteAsyncTask extends AsyncTask<BorrowModel, Void, Void> {
private AppDatabase db;
deleteAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
#Override
protected Void doInBackground(final BorrowModel... params) {
db.itemAndPersonModel().deleteBorrow(params[0]);
return null;
}
}
}

I would provide an example, probably you need to modify the code.
First you need a live data change and subscribe to that in your view. Then in the controller you post the value telling the subscriber that something appends. This way asynchronously the view would get alerted.
private MutableLiveData<String> databaseLiveData = new MutableLiveData<>();
...
And in the deleteAsyncTask class you can add:
protected void onPostExecute(Void result) {
databaseLiveData.postValue("some data deleted");
}
And in the BorrowedListViewModel class this method to access from the view add this method:
public LiveData<String> getChanger() {
return databaseLiveData;
}
In the view e.g.Activity add this:
private BorrowedListViewModel mBorrowedListViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
BorrowedListViewModel = ViewModelProviders.of(this).get(BorrowedListViewModel.class);
subscribe();
}
private void subscribe() {
final Observer<String> liveDataChange = new Observer<String>() {
#Override
public void onChanged(#Nullable final String message) {
Log.d("Activity", message);
}
};
liveDataChange.getChanger().observe(this, liveDataChange);
}
Hope this help.

Related

Multiple LiveData objects in single ViewModel

The structure of my application is as follows:
MainActivity(Activity) containing Bottom Navigation View with three fragments nested below
HomeFragment(Fragment) containing TabLayout with ViewPager with following two tabs
Journal(Fragment)
Bookmarks(Fragment)
Fragment B(Fragment)
Fragment C(Fragment)
I am using Room to maintain all the records of journals. I'm observing one LiveData object each in Journal and Bookmarks fragment. These LiveData objects are returned by my JournalViewModel class.
JournalDatabase.java
public abstract class JournalDatabase extends RoomDatabase {
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService dbWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
private static JournalDatabase INSTANCE;
static synchronized JournalDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), JournalDatabase.class, "main_database")
.fallbackToDestructiveMigration()
.build();
}
return INSTANCE;
}
public abstract JournalDao journalDao();
}
JournalRepository.java
public class JournalRepository {
private JournalDao journalDao;
private LiveData<List<Journal>> allJournals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalRepository(Application application) {
JournalDatabase journalDatabase = JournalDatabase.getInstance(application);
journalDao = journalDatabase.journalDao();
allJournals = journalDao.getJournalsByDate();
bookmarkedJournals = journalDao.getBookmarkedJournals();
}
public void insert(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.insert(journal);
});
}
public void update(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.update(journal);
});
}
public void delete(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.delete(journal);
});
}
public void deleteAll() {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.deleteAll();
});
}
public LiveData<List<Journal>> getAllJournals() {
return allJournals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
JournalViewModel.java
public class JournalViewModel extends AndroidViewModel {
private JournalRepository repository;
private LiveData<List<Journal>> journals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalViewModel(#NonNull Application application) {
super(application);
repository = new JournalRepository(application);
journals = repository.getAllJournals();
bookmarkedJournals = repository.getBookmarkedJournals();
}
public void insert(Journal journal) {
repository.insert(journal);
}
public void update(Journal journal) {
repository.update(journal);
}
public void delete(Journal journal) {
repository.delete(journal);
}
public void deleteAll() {
repository.deleteAll();
}
public LiveData<List<Journal>> getAllJournals() {
return journals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
I'm instantiating this ViewModel inside onActivityCreated() method of both Fragments.
JournalFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
journalAdapter.submitList(list);
}
});
}
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getBookmarkedJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
adapter.submitList(list);
}
});
}
However, the problem when I use this approach is as I delete make some changes in any of the Fragment like delete or update some Journal some other Journal's date field changes randomly.
I was able to solve this issue by using single LiveData object and observe it in both fragments. The changes I had to make in BookmarkFragment is as follows:
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
List<Journal> bookmarkedJournals = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getBookmark() == 1)
bookmarkedJournals.add(list.get(i));
}
adapter.submitList(bookmarkedJournals);
}
});
}
It works properly now.
However, I want to know why it didn't work using my first approach which was to use two different LiveData objects and observe them in different fragments.
Are multiple LiveData objects not meant to be used in single ViewModel?
OR
Are two instances of same ViewModel not allowed to exist together while making changes and fetching different LiveData objects from the same table simultaneously?
I found out the reason causing this problem.
As I was using LiveData with getViewLifecycleOwner() as the LifecycleOwner, the observer I passed as parameter was never getting removed. So, after switching to a different tab, there were two active observers observing different LiveData objects of same ViewModel.
The way this issue can be solved is by storing the LiveData object in a variable then removing the observer as you switch to different fragment.
In my scenario, I solved this issue by doing the following:
//store LiveData object in a variable
LiveData<List<Journal>> currentLiveData = journalViewModel.getAllJournals();
//observe this livedata object
currentLiveData.observer(observer);
Then remove this observer in a suitable Lifecycle method or anywhere that suits your needs like
#Override
public void onDestroyView() {
super.onDestroyView();
//if you want to remove all observers
currentLiveData.removeObservers(getViewLifecycleOwner());
//if you want to remove particular observers
currentLiveData.removeObserver(observer);
}

Livedata Data Change Pattern

I have a doubt.If i have a method that make asynchronous call to an api and converts the results of it to livedata object and in another place i am updating my recyclerview when data changes, then every time call to this method will update recyclerview or ,for eg:if url stays same then it won't update the recyclerview;Pls help.
Here is the code for observing data in Mainactivity onCreate method.
JsonViewModel model = new ViewModelProvider(this).get(JsonViewModel.class);
model.getData("top_rated").observe(this, data -> {
mRecyclerView.setAdapter(new MovieRecyclerViewAdapter(this,data));
});
Here is the JsonViewModel class
public class JsonViewModel extends AndroidViewModel {
private JsonLivedata data;
public JsonViewModel(#NonNull Application application) {
super(application);
data=new JsonLivedata();
}
public LiveData<List<Movie>> getData(String path) {
data.loadData(path);
return data;
}
}
Here is the JsonLivedata class
public class JsonLivedata extends LiveData<List<Movie>> {
private static final String TAG = "JsonLivedata";
public JsonLivedata() {
}
public void loadData(String path){
Log.d(TAG, "loadData: Called");
new AsyncTask<String,Void,List<Movie>>(){
#Override
protected List<Movie> doInBackground(String... path) {
List<Movie> allTopMovies= JsonResponseFetcher.makeAsyncQueryForMovies(path[0]);
return allTopMovies;
}
#Override
protected void onPostExecute(List<Movie> movies) {
setValue(movies);
}
}.execute(path);
}
}
And here is the method that call livedata loaddata method
changeBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
model.getData("popular");
}
});
Or I am doing things wrongly.Can anyone suggest
First create adapter instance & set to RecyclerView
JsonViewModel model = new ViewModelProvider(this).get(JsonViewModel.class);
MovieRecyclerViewAdapter movieRecyclerViewAdapter = new MovieRecyclerViewAdapter(this, dataList)
mRecyclerView.setAdapter(movieRecyclerViewAdapter);
Then do this on data changes
model.getData("top_rated").observe(this, data -> {
dataList.clear();
dataList.addAll(data);
movieRecyclerViewAdapter.notifyDataSetChanged();
});

Android mvvm livedata not observing

This is my first time using MVVM architecture.I am also using LiveData. I simply retrieve data from server using Retrofit.So upon clicking a button in the View(MainActivity.class) I invoke the ViewModel class's method(handleRetrofitcall()) to take up the duty of Api calling from the Model class(Retrofit Handler.class).The Model class upon retrieving the data informs the ViewModel of the data(which is actually the size of items).I set the size to LiveData and try to listen for it.Unfortunately I couldn't.For detailed analysis please go through the code.
Model...
RetrofitHandler.class:
public class RetrofitHandler {
private ApiInterface apiInterface;
private SimpleViewModel viewModel;
public void getData(){
apiInterface= ApiClient.getClient().create(ApiInterface.class);
Call<Unknownapi> call=apiInterface.doGetListResources();
call.enqueue(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
List<Unknownapi.Data> list;
Unknownapi unknownapi=response.body();
list=unknownapi.getData();
viewModel=new SimpleViewModel();
viewModel.postValue(list.size());
Log.e("Size",Integer.toString(list.size()));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
}
});
}
}
ViewModel....
SimpleViewModel.class:
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler;
private int size;
private MutableLiveData<Integer> mutablesize=new MutableLiveData<>();
public SimpleViewModel() {
super();
}
#Override
protected void onCleared() {
super.onCleared();
}
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData();
}
public void postValue(int size){
this.size=size;
mutablesize.postValue(this.size);
Log.e("lk","f");
}
public MutableLiveData<Integer> getObject() {
return mutablesize;
}
}
View.....
MainActivity.class:
public class MainActivity extends AppCompatActivity {
private TextView status;
private SimpleViewModel viewModel;
private Observer<Integer> observer;
private MutableLiveData<Integer> mutableLiveData;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status=findViewById(R.id.status);
viewModel=ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
observer=new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
};
viewModel.getObject().observe(MainActivity.this,observer);
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.handleRetrofitcall();
}
});
}
#Override
protected void onDestroy() {
if (observer!=null){
viewModel.getObject().removeObserver(observer);
}
super.onDestroy();
}
}
You're creating a new ViewModel in the RetrofitHandler, so nothing is observing that viewmodel. Instead of having the RetrofitHandler rely on a ViewModel internally, it's probably safer to handle the Retrofit callback inself, and post data there.
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData(new Callback<List<Unknownapi.Data>> {
// add actual callback implementation here
); // add a callback here, so that the data is available in the view model. Then post the results from here.
}
Edit: More clarification.
In the Activity, you're correctly creating a ViewModel and observing it (we'll call that ViewModel A). ViewModel A is then creating a RetrofitHandler and calling getData on that Retrofithandler. The issue is that RetrofitHandler is creating a new ViewModel in getData (which I'm going to call ViewModel B).
The issue is that the results are being posted to ViewModel B, which nothing is observing, so it seems like nothing is working.
Easy way to avoid this issue is to make sure that only an Activity/Fragment is relying on (and creating) ViewModels. Nothing else should know about the ViewModel.
Edit 2: Here's a simple implementation. I haven't tested it, but it should be more or less correct.
// shouldn't know anything about the view model or the view
public class RetrofitHandler {
private ApiInterface apiInterface;
// this should probably pass in a different type of callback that doesn't require retrofit
public void getData(Callback<Unknownapi> callback) {
// only create the apiInterface once
if (apiInterface == null) {
apiInterface = ApiClient.getClient().create(ApiInterface.class);
}
// allow the calling function to handle the result
apiInterface.doGetListResources().enqueue(callback);
}
}
// shouldn't know how retrofit handler parses the data
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler = new RetrofitHandler();
// store data in mutableSize, not with a backing field.
private MutableLiveData<Integer> mutableSize = new MutableLiveData<>();
public void handleRetrofitCall() {
// handle the data parsing here
retrofitHandler.getData(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
Unknownapi unknownapi = response.body();
int listSize = unknownapi.getData().size;
// set the value of the LiveData. Observers will be notified
mutableSize.setValue(listSize); // Note that we're using setValue because retrofit callbacks come back on the main thread.
Log.e("Size", Integer.toString(listSize));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
// error handling should be added here
}
});
}
// this should probably return an immutable copy of the object
public MutableLiveData<Integer> getObject() {
return mutableSize;
}
}
public class MainActivity extends AppCompatActivity {
private TextView status;
// initialize the view model only once
private SimpleViewModel viewModel = ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status = findViewById(R.id.status);
// observe the view model's changes
viewModel.getObject().observe(this, new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
// you should handle possibility of interger being null
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
});
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// call the view model's function
viewModel.handleRetrofitCall();
}
});
}
}

No adapter attached; skipping layout when clicking back button and starting the app again

Android Studio 3.2 Canary 18
kotlin_version = 1.2.50
I have a simple app that uses a recyclerview and adapter. When the app starts is load all the data.
However, when I click the back button and start the app again. It won't display the data (blank).
If I clear the app from memory and start the app. The data will load as normal.
I am loading the data from sqlite and the data is loaded each time. as it populates the insectDataModelList.
After going into the RecyclerView.java source code the reason is the mAdapter is null. However, I have
checked that the adapter is correct when I set it to the recyclerview.
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
...
}
My MainActivity.java is Java
public class MainActivity extends AppCompatActivity {
private RecyclerView rvInsects;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
}
private void setupAdapter(List<InsectDataModel> insectDataModelList) {
final LayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false);
rvInsects.setLayoutManager(layoutManager);
rvInsects.setHasFixedSize(true);
final InsectAdapter insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
insectAdapter.notifyDataSetChanged();
}
/* Callback from database */
public void loadAllInsects(final Cursor cursor) {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
final List<InsectDataModel> insectDataModelList = insectInteractorMapper.map(cursor);
/* data loaded with 24 items */
setupAdapter(insectDataModelList);
}
}
InsectAdapter.kt is Kotlin.
class InsectAdapter(private val insectList: MutableList<InsectDataModel>)
: RecyclerView.Adapter<InsectAdapter.CustomInsectHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInsectHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.insect_row_item, parent, false)
return CustomInsectHolder(view)
}
override fun onBindViewHolder(holder: CustomInsectHolder, position: Int) {
holder.tvFriendlyName.text = insectList[position].friendlyName
holder.tvScientificName.text = insectList[position].scientificName
}
override fun getItemCount(): Int {
return insectList.size
}
class CustomInsectHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivDangerLevel: DangerLevelView = itemView.findViewById(R.id.ivDangerLevel)
val tvFriendlyName: TextView = itemView.findViewById(R.id.tvFriendlyName)
val tvScientificName: TextView = itemView.findViewById(R.id.tvScientificName)
}
}
The database I use rxjava2 to do the query
public class DatabaseManager {
private static DatabaseManager sInstance;
private MainActivity mainActivity;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance(MainActivity context) {
if (sInstance == null) {
sInstance = new DatabaseManager(context);
}
return sInstance;
}
private DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = context;
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Everything works as expected when the apps installs for the first time. And if you clear it out of memory.
However, its only when you click the back button, and then try and start the app it will not load any data
because of the mAdapter being null in the RecyclerView class.
When I click the back button and then start the app again. All I get is a blank screen i.e.
Updated DatabaseManager class that removes the singleton and used a weakreference to ensure that the MainActivity instance is garbage collected.
public class DatabaseManager {
private WeakReference<MainActivity> mainActivity;
private BugsDbHelper mBugsDbHelper;
public DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = new WeakReference<>(context);
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Many thanks for any suggestions,
When you click the back button and relaunch the app, a new instance of MainActivity is started.
At the same time, your DatabaseManager is a singleton. Its reference is stored as a static variable. It survives the activity recreation. It will live until the process is killed.
So, when you run queryAllInsects for the second time, the callback is sent to the old instance of MainActivity, which is not visible anymore.
You should not keep a reference to MainActivity in DatabaseManager. It's a memory leak, because it cannot be garbage collected.
The issue is most likely that you are loading the data in your onCreate() and not in onResume(). When you press back to "close the app" you are not necessarily clearing the UI stack from memory. That's why when you go back into the app, it doesn't invoke onCreate() again, and doesn't load your data again.
Keep everything the same, just move your data loading from onCreate() to onResume(). That way, whenever the screen is shown to the user, the data will load.
Few observations:
You are still passing the MainActivity to the BugsDbHelper class, take care of the reference there.
It's probably a good idea to include a "cleaning method" in Singleton classes, which should be called in onStop() or onDestroy() of an activity. onStop() is preferred since onDestroy() is not guaranteed to be called immediately.
The "cleaning method" in Singleton class should do the following:
a) Nullify any references to the parameters, objects, context or callbacks you have asked as a dependency in the constructor or otherwise.
b) If the Singleton class has created "new" objects with context dependencies, make sure to include similar cleaning methods in these classes too.
To avoid crashes and memory leakage in fragment/activities, make sure you are cleaning up your recycler view/adapter in onStop(). The callbacks can be received anytime, and if that happens while your activity is in the background, you are bound to get a "force close" fortune cookie.
Keep an eye on the activity/fragment lifecycle. A lot of issues are just because of ignoring the lifecycle callbacks. These are there for a reason, utilize them.
Put this 2 lines in onResume() and remove from onCreate() and try it.
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
I suggest the following changes:
MainActivity, the less code you write in the activity the better, move all the data retrieval part to the DatabaseManager. Also setup the RecyclerView once and only update the dataset when appropriate:
public class MainActivity extends AppCompatActivity {
private List<InsectDataModel> insectDataModelList = new ArrayList<>();
private Disposable disposable;
private RecyclerView rvInsects;
private InsectAdapter insectAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
setupAdapter();
//Request Data, take advantage of RxJava to load data asynchronously
DatabaseManager.getInstance(this)
.queryAllInsects("friendlyName")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<InsectDataModel>>() {
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(List<InsectDataModel> response) {
insectDataModelList.clear();
insectDataModelList.addAll(response);
insectAdapter.notifyDatasetChanged();
}
#Override
public void onError(Throwable e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});;
}
private void setupAdapter() {
//Setup RecyclerView Only need to be called once
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
LayoutManager layoutManager = new LinearLayoutManager(this); // LinearLayoutManager is Vertical by default
rvInsects.setLayoutManager(layoutManager); // You don't event have to define it as RecyclerView use LinearLayoutManager.Vertical by default
rvInsects.setHasFixedSize(true);
insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
}
#Override
protected void onDestroy() {
//Dispose observer if activity is destroyed to prevent memory leak
if(disposable != null && !disposable.isDisposed())
disposable.dispose();
super.onDestroy();
}
}
And in DatabaseManager, instead of observing the data source(Cursor) and notify the requester(Activity) via callback, we get the data stream and pass it the caller to observe:
public class DatabaseManager {
private static DatabaseManager sInstance;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance() {
if (sInstance == null) {
sInstance = new DatabaseManager();
}
return sInstance;
}
private DatabaseManager() {
// Move the actualy database initiation to application class or singleton
mBugsDbHelper = BugsDbHelper.getInstance(); // or ApplicationController.getDbHelper();
}
#SuppressLint("CheckResult")
public SingleObserver<List<InsectDataModel>> queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.map(new Function<Cursor, List<Object>>() {
#Override
public List<Object> apply(Cursor cursor) throws Exception {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
return insectInteractorMapper.map(cursor);
}
});
}
}
Now the solution here is to rely on the RxJava to change the callback pattern to the observer pattern. So instead of passing the activity (callback) and waiting to be called, we get the data steram (observable) and observe it for the response. This eliminate the leak problem all together and enhance the readability and maintainability.
Also don't forget to move the Database initialization to the Application class or a Singleton instance to prevent multiple instantiation. The easier solution would be like:
public class ApplicationController extends Application {
private BugsDbHelper mBugsDbHelper;
#Override
public void onCreate() {
super.onCreate();
mBugsDbHelper = new BugsDbHelper(this);
}
public BugsDbHelper getDbHelper(){
return mBugsDbHelper ;
}
}

Cannot setValue in MutableLiveData using object retrieved using Retrofit 2

I am trying to follow Android architecture guidelines to make this app. I have a MovieRepository which is responsible for fetching JSON (data layer), and I have a ViewModel that supplies data to the UI in my MainActivity. I am using retrofit 2 for my networking task.
MovieRespository code:
public class MovieRepository {
private static final String TAG = MovieRepository.class.getSimpleName();
public LiveData<ReturnMovie> search(String term) {
final MutableLiveData<ReturnMovie> data = new MutableLiveData<>();
MovieService service = ServiceGenerator.createService(MovieService.class);
Call<ReturnMovie> call = service.requestMovie(term, MovieAPIUtils.KEY);
call.enqueue(new Callback<ReturnMovie>() {
#Override
public void onResponse(Call<ReturnMovie> call, Response<ReturnMovie> response) {
ReturnMovie movie = response.body();
data.setValue(movie);
Log.d(TAG, data.getValue().getPage().toString());
}
#Override
public void onFailure(Call<ReturnMovie> call, Throwable t) {
Log.d(TAG, t.toString());
}
});
return data;
}
}
MovieService:
public interface MovieService {
#GET(MovieAPIUtils.Path.MOVIE_PATH + "/{param}")
Call<ReturnMovie> requestMovie (#Path("param") String endpoints,
#Query(MovieAPIUtils.Query.API_QUERY) String key);
}
ViewModel:
public class MainActivityMovieViewModel extends ViewModel {
private static final String TAG = MainActivityMovieViewModel.class.getSimpleName();
private LiveData<ReturnMovie> movie;
private MovieRepository repo = new MovieRepository();
public LiveData<ReturnMovie> getMovie(String searchTerm) {
if (movie == null) {
movie = repo.search(searchTerm);
}
return movie;
}
}
MainActivity:
public class MainActivity extends AppCompatActivity implements MovieAdapter.MovieOnClickListener {
private RecyclerView mRecyclerView;
private MovieAdapter mMovieAdapter;
private MainActivityMovieViewModel viewModel;
private static final String TAG = MainActivity.class.getSimpleName();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = ViewModelProviders.of(this).get(MainActivityMovieViewModel.class);
initRecyclerViewWithMovies();
ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue();
// other code....
}
In the onResponse(new Callback<ReturnMovie>) I was able to retrieve the movie object and I proved it by logging one of its property values, so there IS a valid ReturnMovie object. However, in my MainActivity, the method ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue(); gives me a null. I checked everywhere is just cannot see where the problem is.
The rest of the code is on my Github:
https://github.com/brendoncheung/PopularMovie/tree/mvvm_approach
You use android architecture components, which uses observable pattern in live data. If you are just getting the current value of livedata, you are not sure it has been processed yet.
Instead of
ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue(); i think you should use
viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).observe(this, new Observer<ReturnMovie>() {
#Override
public void onChanged(#Nullable ReturnMovie movie) {
//do stuff with the movie
doSomething(movie);
}
});
You don't have to manage unsubscription since architecture component manages unsubscription for activities itself
Take a look on : https://developer.android.com/topic/libraries/architecture/livedata

Categories

Resources