I have an Android app that uses a pretty common design pattern:
The main activity is essentially presenting a list of objects - on small devices it does so by hosting a single fragment that displays a recyclerview of this list. On larger devices it hosts two fragments, one which has the same recyclerview of objects, and another which will host the detail for individual objects when one is selected in the list.
On smaller devices, when a selection from the list is made, an activity is launched that hosts a fragment that utilizes a ViewPager to allow "swiping" through the list of objects, and edit each one in place.
In both cases, the user is allowed to edit only from the detail fragment.
I currently have my realm instance initialized in the application class, then the default instance retrieved in an activity base class I use to hold some housekeeping methods:
public abstract class SingleFragmentActivity extends AppCompatActivity {
private Realm realm;
protected abstract Fragment createFragment();
#LayoutRes
protected int getLayoutResId() {
return R.layout.activity_fragment;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
// Initialize ProfileLab
ProfileLab.get(realm);
setContentView(getLayoutResId());
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment == null) {
fragment = createFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
if ( realm != null) {
realm.close();
}
}
}
Note that I am storing this instance of realm in a static class "ProfileLab":
// Initialize ProfileLab
ProfileLab.get(realm);
Then in the various fragments that update data, I am doing stuff like:
mProfile = ProfileLab.get().getProfile(profileId);
*
* do CRUD activities here for example:
*
private void deleteProfile() {
ProfileLab.get().deleteProfile(mProfile);
mCallbacks.onProfileUpdated(mProfile);
}
Then in ProfileLab, it looks like:
public boolean deleteProfile(Profile c) {
boolean retVal = true;
try {
mRealm.beginTransaction();
c.deleteFromRealm();
} catch (Exception e) {
retVal = false;
} finally {
if ( mRealm != null ) {
if (retVal) {
mRealm.commitTransaction();
} else {
mRealm.cancelTransaction();
}
}
}
return (retVal);
}
My question - is this a problem to essentially hold that Realm instance open like that throughout the use of the app? I noticed this paragraph in the docs:
If you get a Realm instance from a thread that does not have a Looper
attached, then objects from such instance will not be updated unless
the waitForChange() method is called. It is important to note that
having to hold on to an old version of your data is expensive in terms
of memory and disk space and the cost increases with the number of
versions between the one being retained and the latest. This is why it
is important to close the Realm instance as soon as you are done with
it in the thread.
The thing is, I am not 'done with it' because this is on the UI thread, which is obviously running throughout the lifetime of my app.
I can't really open/close the realm instance just for the atomic updates, because I need to use the result of the initial query to show the list of objects from which to choose to edit - when I tried that initially (I had realm object open/close within each method in ProfileLab itself) I got an error in my recycler adapters that the realm had been closed...
The example code showing use of the recycler view shows realm being retrieved/used/closed at the individual activity level, if I do that between say the two simple activities (hosting the RecyclerView and hosting the ViewPager), will the data updates be reflected in each other?
Opening and closing the realm within try/catch block is recommended. for an example:
try {
Realm realm = Realm.getDefaultInstance();
//Use the realm instance
}catch(Exception e){
//handle exceptions
}finally {
realm.close();
}
It's a basic example when going to use. If you can close within AsyncTask, it'll be better.
The official documentation refers that if you use minSdkVersion >= 19 and Java >= 7, you won't close it manually.
try (Realm realm = Realm.getDefaultInstance()) {
// No need to close the Realm instance manually
}
Realm will automatically keep Realms on Looper threads up to date. That particular line in the documentation mostly refers to background threads. So your code is fine, even if onDestroy might not be called.
You can also read these relevant sections in the docs:
https://realm.io/docs/java/latest/#closing-realms
https://realm.io/docs/java/latest/#realm-instance-lifecycle
Related
In my application I have one Fragment which is responsible for displaying a list of news items. It takes a String parameter which determines which url to pull data from.
I set the Fragment with this code:
private void setFragment(String pageToLoad, NewsFeedFragment newsFeedFragment) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(newsFeedFragment == null) {
transaction.replace(R.id.container, NewsFeedFragment.newInstance(pageToLoad), pageToLoad);
}
else {
transaction.replace(R.id.container, newsFeedFragment, pageToLoad);
}
mPageToLoad = pageToLoad;
}
In my parent Activity I keep track of which 'page' is currently being viewed:
protected void onSaveInstanceState(#NonNull Bundle outState) {
if(mPageToLoad != null) {
outState.putString("pageToLoad", mPageToLoad);
}
super.onSaveInstanceState(outState);
}
In my parent Activity onCreate method I check whether an instance of NewsFeedFragment has been created and added to the FragmentManager as follows:
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
if (savedInstanceState.containsKey("pageToLoad")) {
String pageToLoad = savedInstanceState.getString("pageToLoad");
if(pageToLoad != null) {
NewsFeedFragment newsFeedFragment = (NewsFeedFragment) getSupportFragmentManager().findFragmentByTag(pageToLoad);
if(newsFeedFragment != null) {
setFragment(pageToLoad, newsFeedFragment);
}
else {
setFragment(pageToLoad, null);
}
}
}
}
}
This works well 99% of the time, the application resumes correctly and displays the last instance of NewsFeedFragment added. However, I have an issue which seems to occur randomly where the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
In NewsFeedFragment the RecyclerView Adapter is a class variable:
public NewsPageAdapter mNewsPageAdapter;
The onActivityCreated method of NewsFeedFragment is as follows:
public void onActivityCreated(Bundle savedInstanceState) {
if(mNewsPageAdapter == null) {
Log.i(TAG, "mNewsPageAdapter is null"); // This is logged when issue occurs
}
if(savedInstanceState == null || mNewsPageAdapter == null) {
new LoadFirstPageTask().execute(); // Fetches news items from web service, creates mNewsPageAdapter, and then calls setupRecyclerView() method
}
else {
setupRecyclerView(savedInstanceState);
}
}
Finally, this is the NewsFeedFragment setupRecyclerView method:
private void setupRecyclerView(Bundle savedInstanceState) {
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mNewsPageAdapter);
}
From what I've described could anyone offer any insight as to why the NewsPageAdapter may sometimes be null when the Fragment is retrieved from The FragmentManager?
Thanks
Ok, so you do new LoadFirstPageTask().execute(); and I guess that will eventually call setupRecyclerView?
I think you're over complicating a solution here.
You have all these conditions, business logic, and decisions made inside a volatile component (a Fragment) whose lifecycle is quite complex and not always what you'd expect; you also couple the maintenance of asynchronous data to this structure, and this is having unexpected side-effects that are hard to pin point and track down.
Creating the Adapter is "cheap" compared to fetching, processing, and producing the data for said adapter.
You don't seem to mention ViewModels anywhere, are you using a viewModel? Or any other sort of pattern like a Presenter, Interactors, useCases?
AsyncTasks are also Deprecated and while I don't advocate to run to the "refactor" hill every time a class is deprecated, I think you could get a better and more stable, testable, and readable solution if you abstract that AsyncTask into a coroutine (all managed by your ViewModel for example).
To put in other terms, your Fragment and Activity shouldn't have to deal with the logic regarding "do I need to load this data or not"; this is someone else's responsibility.
About your code.
Ok, now that I've ranted about how you're doing things, let's dig deeper into your existing Java code.
the RecyclerView Adapter in NewsFeedFragment is sometimes null when the Fragment is retrieved from the FragmentManager using the findFragmentByTag(pageToLoad) method.
Whenever we see "sometimes" in a crash, the 1st suspect should be timing/threading. Synchronous code can fail, but it's often orders of magnitude more predictable than Asynchronous code.
If it's "sometimes" null, then the task that is in charge of changing this behavior is not always ready by the time it's needed; or the condition needed for this task to run, is not always what you expect by the time it's checked, and so on, and so forth.
Start by re-architecting your idea into a separate component.
Have the Fragment create its adapter (can be empty of data) as soon as possible, regardless of whether there's data or not.
Have the fragment ask another component for the data. And when the data is available, send it to the Fragment who will in turn set it in the adapter. If the data is already there by the time you ask for it (because you "cached" it), you won't have to wait.
I'd also store the "last viewed page" in the same component, so you don't need to save the state and pass it alongside to a fragment. Rather the fragment asks for "the current data" and the component already knows what it is.
All in all, it's a bit difficult to put all your pieces together because we, the readers, don't have all the code, nor your requirements that lead you to this solution.
I have a simple setup of 2 fragments: ConversationFragment and DetailsFragment
I am using Room with Paging 3 library and to populate the ConversationFragment I am using a PagingLiveData implementation together with a AndroidViewModel belonging to the ConversationFragment.
I am not using the Navigation Components here, just a common fragment navigation as per Android documentation.
From that fragment I can open the DetailsFragment and then return back to the fragment again. Everything is working well, until I open said fragment and return, then the observer that was tied in the ConversationFragment is lost since that fragment is being destroyed when opening the DetailsFragment.
So far this is not a big issue, I can restart the observer again and it does work when I do that.
However, when I attach the observer again the entire list reflows, this causes the items in the RecyclerView to go wild, the position the list was on is lost and the scrollbar changes sizes which confirms pages are being loaded/reloaded.
I could withstand the weird behavior to a degree, but to have the position lost on top of that is not acceptable.
I looked into caching the results in the view model, but the examples I could find in the available documentation are basic and do not show how the same could be achieved using a LiveData<PagingData<...> object.
Currently this is what I have:
ConversationFragment
#Override
public void onViewCreated(
#NonNull View view,
#Nullable Bundle savedInstanceState
) {
if (viewModel == null) {
viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
}
if (savedInstanceState == null) {
// adapter is initialized in onCreateView
viewModel
.getList(getViewLifecycleOwner())
.observe(getViewLifecycleOwner(), pagingData -> adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
}
super.onViewCreated(view, savedInstanceState);
}
ConversationViewModel
public class ConversationViewModel extends AndroidViewModel {
final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
private final Repository repository;
private final MutableLiveData<PagingData<ItemView>> messageList;
public ConversationFragmentVM(#NonNull Application application) {
super(application);
messageList = new MutableLiveData<>();
repository = new Repository(application);
}
public LiveData<PagingData<ItemView>> getList(#NonNull LifecycleOwner lifecycleOwner) {
// at first I tried only setting the value if it was null
// but since the observer is lost on destroy and the value
// is not it would never be able to "restart" the observer
// again
// if (messageList.getValue() == null) {
PagingLiveData.cachedIn(
PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> repository.getMessageList())),
lifecycleOwner.getLifecycle()
).observe(lifecycleOwner, messageList::setValue);
// }
return messageList;
}
}
As it is, even if I return the result of the PagingLiveData.cachedIn the behavior is the same when I return to the fragment; the items show an erratic behavior in the recyclerview list and the position it was on is totally lost.
This is what I was trying to achieve to see if it fixed my issue:
This is a code lab available here: https://developer.android.com/codelabs/android-training-livedata-viewmodel#8
As you can see the mAllWords are cached and they are only initialized when the view model is constructed for the first time, any subsequent changes are simply updates and would only require new observers to be attached when the fragment is destroyed and created again while still in the back stack.
This is what I was trying to do, but it does not work the way I thought it did, at least it is not as straight forward as I thought.
How can this be achieved?
There's quite a lot to unpack here but my best guess would be your getList method in ConversationViewModel. You're on the right track with using ViewModels and LiveData to persist data across navigation but here you're recreating the LiveData every time this method is called, meaning when you resume ConversationFragment and onViewCreated is called, it creates a new Pager which fetches new data.
The solution would be to create the pager when ConversationViewModel is first created and then accessing the LiveData object itself, rather than the method. You can see this in the Codelab example, they assign the LiveData in the constructor and simply return the already created LiveData in the getAllWords() method.
I'm using this as an example, change ConversationViewModel to something like this and change it to use your config and repository.
private final LiveData<PagingData<ItemView>> messageList;
public ConversationFragmentVM(#NonNull Application application) {
super(application);
repository = new Repository(application);
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
new PagingConfig(/* pageSize = */ 20),
() -> ExamplePagingSource(backend, query));
messageList = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
}
public LiveData<PagingData<ItemView>> getList(){
return messageList;
}
Then in your fragment, you simply observe getList() like usual, except this time it's returning a prexisting version.
viewModel.getList().observe(getViewLifecycleOwner(), pagingData ->
adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
}
I haven't been able to test that this compiles or works so let me know if it doesn't and I'll update this answer.
I have 12 fragments UpcomingGamesFragment repeated twelve times for each month of the year, the respective fragment shows the game releases of the month. For example, the first month will show the games releasing in January 2019, the next fragment will have February 2019 releases, etc.
What I'm trying to build is an architecture that uses a ViewModel. A ViewModel which will be shared across all my 12 fragments and would trigger the data change (through LiveData) to all fragments gracefully, but I have no idea how to use this ViewModel class to accomplish the update to all visible fragments, here's my UpcomingGamesFragment class with the request month data method:
public class UpcomingGamesFragment extends Fragment {
public void loadReleaseData(final int refresh) {
if (mDatabaseLoading == null) {
Log.d(TAG, "Fragment filter " + mFilter + " [fragment is null]");
return;
} else {
Log.d(TAG, "Updating fragment: " + mFilter);
}
if (AppUtil.doesInternetWork(getActivity())) {
// update, data fetched from firebase
}
}
The 12 fragments are initialized in another fragment which gets shown in MainActivity, the fragment is called UpcomingViewPagerFragment which creates 12 UpcomingGamesFragment in a loop.
And here's my ViewModel class:
public class ReleasesViewModel extends ViewModel {
private MutableLiveData<List<_Release>> upcomingFragmentLiveData =
new MutableLiveData<>();
public ReleasesViewModel() {
}
public LiveData<List<_Release>> getUpcomingFragmentList() {
return upcomingFragmentLiveData;
}
}
So how can I update the 12 fragments with the loadReleaseData method taking in count the lifecycle of each?
Assuming all fragment contents are values held somewhere else in your code and assuming you are correctly updating those values. The easiest way to update all fragments accordingly is by calling:
notifyDataSetChanged();
You need to retrieve your ViewModel class object in fragment and register LiveData from it like,
ReleasesViewModel obj = ViewModelProviders.of(getActivity()).get(ReleasesViewModel.class);
in your fragment's onCreate() method, then use that obj of view model for observing your live data like,
(obj of your view model).getUpcomingFragmentList().observe(this, `your observer for that fragment`);
For your instance:
This will get called once any data change happen for your live data inside ViewModel
obj.getUpcomingFragmentList().observe(this, new Observer<List<_Release>>() >{
#Override
public void onChanged(#Nullable List<_Release> list) {
}
});
If you can't find ViewModelProviders class then you need to add dependecies for your ViewModel & livedata from here,
link to add view model & live data to your project
There are a lot of questions about fragment communication here, but they are normally question about getting data from activity and sending data back to activity, normally starting from fragment.
But I wonder what what is best approach for sending data from activity to fragment, when you cannot do it when creating fragment? For clarification, Lets assume that an app has 2 fragments that can use (can not must) some data to improve user experience, but obtaining this data is costly. So obtain this data in activity using a Loader or AsyncTask in main activity while creating Fragments themselves. Now when data is ready asynchronously in Activity, we need to send this data to Fragments. What is best approach for this? I thought of a way for doing this, and I like to know if there is any problem with this approach.
1-In fragment we use onAttach to send fragment to activity and check if any data is already read:
#Override
public void onAttach (Activity activity) {
MyActivity act = (MyActivity)activity;
act.addFragment(this);
Data data = act.getData();
if (data != null) {
setAdditionData(data)
}
}
2-and in activity store a WeakReference to Fragment:
private ArrayList<WeakReference<Fragment>> mFragments = new ArrayList<>();
...
public void addFragment(Fragment frag) {
WeakReference<Fragment> f = new WeakReference<Fragment>(frag);
mFragments.add(f);
}
public Data getData() {
return mData;
}
public void updateFragmentsData() {
for (Iterator<WeakReference<Fragment>> iterator = mFragments.iterator(); iterator.hasNext();) {
WeakReference<Fragment> wf = iterator.next();
Fragment f = wf.get();
if (f != null) {
f.setAdditionData(mData);
} else {
iterator.remove();
}
}
}
Now when fragments attaches, it adds itself to list of fragments in activity and checks if data is already ready and if ready it will use that data. On the other hand, when data is ready asynchronously in activity, it can call updateFragmentsData() to update all fragments data.
I wonder if this approach is correct or it can be incorrect in some situations? Any idea? Is there any better approach for notifying fragments from main activity?
Btw, is it possible to use Handler/Message for communicating between fragments too or not? As another approach?
Best Regards
I can think of three ways.
Use a listener. Write an interface in the activity to use it as a listener. The fragment implements the interface and registers and unregister as a listener at appropriate time.(say at onCreateView and onDestroyView).
This one is my favorite. I hope DataBinding is gaining popularity and it can be used to solve this. Say you define a particular model for the fragment layout. Now you use ObservableFields in the model. Pass this model to your databinding variable. Now change this object from either the activity or the fragment itself, changes will be reflected in the view.
The newly introduced ViewModels. I will be using them from my next project.
I was wondering does it matter where you instantiate adapters dataset with Realm? I like to fetch all the data that any adapter needs in the adapters constructor and thus instantiate the dataset there, but almost all examples I've seen fetch the data beforehand in the activity creating the adapter and then pass it to the adapter as a parameter.
With SQLite this seems even more arbitrary, but since I'm using Realm I need to open a realm connection every time I want to access the database and to keep the data available I need to keep the connection open. Keeping this connection open in the activity seems unnecessary since you might need to make queries in the adapter thus having to open a connection to realm within the adapter anyways.
Is there some higher reason to fetch the dataset beforehand or is this just a matter of preference?
since I'm using Realm I need to open a realm connection every time I want to access the database
Wrong, you just need 1 open instance for that given thread in order to access the database.
Keeping this connection open in the activity "seems unnecessary" since you might need to make queries in the adapter
In which case you can have the activity-level Realm instance as a "scoped dependency", that you can share through the Context via getSystemService() if that's what you like to do.
public class MyActivity extends Activity {
Realm realm;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
//...
}
#Override
public void onDestroy() {
if(realm != null) {
realm.close();
}
super.onDestroy();
}
...
#Override
public Object getSystemService(String key) {
if("REALM".equals(key)) {
return realm;
}
return super.getSystemService(key);
}
}
public class MyAdapter extends RecyclerView.Adapter<MyModelViewHolder> {
private final Context context;
Realm realm;
RealmResults<MyModel> results;
private final RealmChangeListener listener = new RealmChangeListener() {
#Override
public void onChange(Object element) {
notifyDataSetChanged();
}
}
public MyAdapter(Context context) {
this.context = context;
//noinspection ResourceType
realm = (Realm)context.getSystemService("REALM");
results = realm.where(MyModel.class).findAll();
results.addChangeListener(listener);
}
...
}
thus having to open a connection to realm within the adapter anyways.
wrong
Is there some higher reason to fetch the dataset beforehand or is this just a matter of preference?
It's because your Adapter, which is just supposed to describe how to show the elements of a dataset, become a God that also determines the data that it must show.
Although to be fair, it's actually harder to externally manage the data-set; something must hold a strong reference to the result set anyways. So when I don't really bother with unit-testability, I do obtain the results inside the Adapter itself. It does work.