I have a sharedViewModel with a live data variable that sends data to my fragments, everything goes well and I can capture the variables in the fragments.
However, as recommended I get the sharedViewModel inside onViewCreated, as follows below:
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
DataInitialViewModel initial = new ViewModelProvider(requireActivity()).get(DataInitialViewModel .class);
initial .getStatusUser().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(String s) {
if(ConstsSubscribe.USER_STANDARD.equals(s)){
if(adapter != null)
isAssinante = true;
Toast.makeText(getContext(), "test", Toast.LENGTH_SHORT).show(); // Never enter here
}else{
if(adapter != null)
isAssinante = false;
Toast.makeText(getContext(), "comaefeq", Toast.LENGTH_SHORT).show(); // Never enter too...
}
}
});
}
The problem is that I fill an adapter in my onCreateView, however, that adapter is filled using Firebase data (which takes some time).
So, even with the observer, my variable can never capture when the adapter is different from null, keeping it always null, I can never pass the state of my variable to my adapter. Is there any way to capture my live data inside my adapter?
adapter = new DieAdapter(root.getContext(), dieArrayListAdp, manager, isAssinante); // var isAssinante is never updated... adapter is send to ViewPager2 to setAdapter command after.
I read the live data on onCreateView, works fine, is the unique solution for my problem, even tough not be the greater.
Related
I am running into a problem with Android Mutable Live Data and it might be because of my poor understanding. The scenario is I have 3 lists in my Android tabs.
public void openSort(SortType sortType) {
mSortType.postValue(sortType);
}
public MutableLiveData<SortType> getOpenSort() {
return mSortType;
}
In my activity I have my first fragment open. In that I call the
public void openSort(SortType sortType)
Everything works fine so far. Then I go ahead and open another tab which has the second fragment. This second fragment in its
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SortOrderViewModel sortOrderViewModel = ViewModelProviders.of(getActivity()).get(SortOrder.class);
sortOrderViewModel.observe(this, new Observer() {
#Override
public void onChanged(SortType sortType) {
// Debug print
}
})
}
For some reason the onChanged of the second fragment gets called too. My assumption was that the onChanged method only gets called when the data changes in the view model, but it seems the data is stored by the LiveData and the observers are notified again.
Is there something that we can do such that the fragment is "only" notified when the mutable live data has changed?
I have an activity that grabs data via WebService, from there it creates elements to display the data. Some data is grouped so my solution was to display the grouped data in their own fragments below the main layout, allowing the user to swipe across the groups, probably with a tabs at the top to show the group name.
The problem I came across was that the fragments in the activity are created before that web call takes place, making them empty or using old data. I then created a sharedpreferences listener and placed the fragments layout creation method within it. The main method grabs the data, writes to sharedpreferences the fragment detects the change and creates it's layout, Or so I thought.
Some groups are the same between items, so moving from one to the other won't trigger that onchange event thus not triggering the layout creation method. I then decided to do the following to always trigger the onchange event after the sharedpreferences are written
final Boolean updated = settings.getBoolean("UPDATED_1", false);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("UPDATED_" + pageNum, !updated);
I just don't think that's the best solution, it also has it's problems and isn't triggering every time (Which I have yet to troubleshoot)
What's a better solution for all this? I also have a memory leak I haven't diagnosed yet to make things even more of a headache.
I've just thought of moving my data grabbing method to before the ViewPager initialization but I'm not yet sure if this will solve my problem.
I would not recommend waiting until you get the data to show the view as it will affect the User Experience and look sluggish.
Instead, you could implement an AsyncTaskLoader in your fragment so you can inform the Fragment's View with a BroadcastReceiver once you get the data from your server. In the meantime, just show a spinner until the data are retrieved, then you hide it and update your list with a adapter.notifyDataSetChanged();.
Here is an example of a AsyncTaskLoader (In my case it's a database query instead of a server call like you):
public class GenericLoader<T extends Comparable<T>> extends AsyncTaskLoader<ArrayList<T>> {
private Class clazz;
public GenericLoader(Context context, Class<T> clazz) {
super(context);
this.clazz = clazz;
}
#Override
public ArrayList<T> loadInBackground() {
ArrayList<T> data = new ArrayList<>();
data.addAll(GenericDAO.getInstance(clazz).queryForAll());
Collections.sort(data);
return data;
}
}
Then in your Fragment:
public class FragmentMobileData extends Fragment implements ListAdapter.OnItemClickListener, LoaderManager.LoaderCallbacks<ArrayList<EntityCategories.EntityCategory>> {
public static String TAG = "FragmentMobileData";
private ImageListAdapter adapter;
private ArrayList<EntityList> mCategories = new ArrayList<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
String result = bundle.getString(DatabaseService.RESULT);
if (DatabaseService.NO_CONNECTION.equals(result)) {
Utils.showToastMessage(getActivity(), "No internet connexion", true);
} else if (DatabaseService.RESULT_TIMEOUT.equals(result)) {
Utils.showToastMessage(getActivity(), "Bad connection. Retry", true);
}
getActivity().getSupportLoaderManager().initLoader(1, null, FragmentMobileData.this).forceLoad();
}
};
#Bind(R.id.progressBarEcard)
ProgressBar spinner;
#Bind(R.id.list)
RecyclerView list;
public FragmentMobileData() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_mobile_plan, container, false);
ButterKnife.bind(this, view);
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("Mobile");
list.setLayoutManager(new LinearLayoutManager(context));
list.addItemDecoration(new DividerItemDecoration(context, R.drawable.divider));
adapter = new ImageListAdapter(mCategories, this);
list.setAdapter(adapter);
Intent intent = new Intent(context, DatabaseService.class);
intent.setAction(DatabaseService.UPDATE_DATA);
getActivity().startService(intent);
return view;
}
#Override
public void onPause() {
super.onPause();
getActivity().unregisterReceiver(mReceiver);
}
#Override
public void onResume() {
super.onResume();
getActivity().registerReceiver(mReceiver, new IntentFilter(DatabaseService.UPDATE_DATA));
}
#Override
public Loader<ArrayList<EntityCategories.EntityCategory>> onCreateLoader(int id, Bundle args) {
return new GenericLoader(context, EntityCategories.EntityCategory.class);
}
#Override
public void onLoadFinished(Loader<ArrayList<EntityCategories.EntityCategory>> loader, ArrayList<EntityCategories.EntityCategory> data) {
if (mCategories.size() != data.size()) {
mCategories.clear();
mCategories.addAll(data);
adapter.notifyDataSetChanged();
Intent intent = new Intent(context, DownloadFilesService.class);
context.startService(intent);
}
spinner.setVisibility(View.GONE);
}
#Override
public void onLoaderReset(Loader<ArrayList<EntityCategories.EntityCategory>> loader) {
mCategories.clear();
adapter.notifyDataSetChanged();
}
//...
}
Maybe I misunderstood something. But in your case I think there is pretty good alternative to create, for example, your fragment which will display some group of data, then in it's creation stage show progress bar in ui, and meantime do request to the data in background. Then handle result data and show it, and hide progress bar.
This can be achieved with implementing MVP pattern to provide flexibility of code and easy testing. Also you can use rxJava and Retrofit to handle requests in a convenient way. More information about MVP and samples you can find here.
If you don't want to provide this way for some reason. For example, you have undetermined number of groups, which you will receive in future somehow and you want to dynamically build your fragments base on data which you receive, then I suggest you can organize presentation layer in your activity. In this layer your will receive data then pass it to special handler, which will divide it to groups and base on them will ask activity to create fragment. In constructor you will send already received data (so it is need to implement Parcelable interface).
When the user from a FragmentDialog type his data into the edittexts and press the save button, the data should instantly show in the recyclerView, but that dosn't happend. To get/show the latest data, you have to restart the app
I have used a temporary solution where I pass data from the FragmentDialog to the mainActivity via a interface and then I directly pass that data into the arraylist for the recyclerView. This work, but dosn't look like a propor solution. I wish a "correct" way to do it
I have also tried to set adapter.notifyDataSetChanged(); different places without any succses
DialogFragment class where the user type his data and is then insertet to SQLite database
public class DialogAdd extends DialogFragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.add_productdialog,container, false);
name = (EditText) rootView.findViewById(R.id.dialog_productname);
quantity = (EditText) rootView.findViewById(R.id.dialog_qantity);
location = (EditText) rootView.findViewById(R.id.dialog_location);
normalPrice = (EditText) rootView.findViewById(R.id.dialog_normalPrice);
offerPrice = (EditText) rootView.findViewById(R.id.dialog_offerPrice);
okButton = (Button) rootView.findViewById(R.id.dialog_okButton);
okButton.getBackground().setColorFilter(Color.parseColor("#2fbd4b"), PorterDuff.Mode.MULTIPLY);
okButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (name.getText().toString().isEmpty()) {
Toast.makeText(context, "You must add a name", Toast.LENGTH_LONG).show();
} else {
DialogAddListener addListener = (DialogAddListener) getActivity();
dbHelper.insertData(name.getText().toString(), quantity.getText().toString(), location.getText().toString(), normalPrice.getText().toString(), offerPrice.getText().toString());
addListener.getDialogData(name.getText().toString(), quantity.getText().toString(), location.getText().toString(), normalPrice.getText().toString(), offerPrice.getText().toString());
getDialog().dismiss();
}
}
});
return rootView;
}
The mainActivity class that instantiate the recyclerview,sqlite and adapters.
public class MainActivity extends AppCompatActivity implements DialogAdd.DialogAddListener{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.shoppinglist_mainactivity);
databaseHelper = new DatabaseHelper(this);
addbutton = (ImageButton) findViewById(R.id.addbtn);
addbutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dialogAdd = new DialogAdd();
dialogAdd.show(getSupportFragmentManager(), "addDialog");
}
});
//RecyclerView
recyclerView = (RecyclerView)findViewById(R.id.rv_shoppinglist);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(App.getAppContex());
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
initializeData();
adapter = new ShoplistAdapter(shopListItems);
recyclerView.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
private void initializeData(){
shopListItems = new ArrayList<>();
resultset = databaseHelper.getAllData();
if (resultset.moveToFirst()){
while(!resultset.isAfterLast()){
shopListItems.add(new ShopListItem(resultset.getString(1), resultset.getString(2), resultset.getString(3), resultset.getString(4), resultset.getString(5)));
resultset.moveToNext();
totalPrice.setText("Total cost: $" + "26");
totalItems.setText("Total items: " + resultset.getCount());
}
}
resultset.close();
}
//This is only used to show the data instantly
#Override
public void getDialogData(String name, String qty, String location, String price1, String price2) {
shopListItems.add(new ShopListItem(name, qty, location, price1, price2));
}
}
You should use a CursorLoader which automatically listens for changes in Uri. And notify changes via contentResolver.notifyChanged(uriOfInsertedData). The beast way is to use a ContentProvider to make all thins thing work appropriately.
Or for an easier way, register an Observer in a singleton for your database and notify it upon changing. In general this will do the same and you will not have any relations between components. It takes some code to post so I hope you will figure it out.
Update 2016-03-09, based on your comment
If you implemented example 1, it doesn't tell you how to monitor for changes. There is no existing mechanism for this case.
The best approach to implement this mechanism is to use an Observer pattern.
I would go with a singleton that hosts Observers.
You can extend Observable (which already has logic for storing a list of Observers, notifying and removing them) and make a singleton of it.
Register Observer by calling addObserver() wherever you need to listen to (don't forget to call deleteObserver() to avoid Activity leaks).
Now, whenever you call insert or modify a database in any way, make sure to call notifyObservers("tablename") of Observable singleton.
This way all observers will get notified the table was modified (passing a table name is an example, you can use any Object there to inform your components about the change)
Then in update() of your Observer, check if the table you wish to monitor is updated, if so, reload the data as you normally would do.
I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.
From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.
I have a fragment which is basically a list view. The parent activity calls a method to retrieve a list of roster items from a service. When the data returns from the service I call updateRosterItems on the fragment passing through and ArrayList of Roster items. The problem is that it works the first time through, but then when I select a different tab, and then come back to the tab with the fragment, the getActivity() returns null and I can't hook up the data to the ArrayAdapter.
This is the code for the updateRosterItems function:
public void updateRosterList(ArrayList<RosterInfo> rosterItems)
{
if(_adapter == null)
{
_adapter = new RosterItemAdapter(getActivity(), R.layout.roster_listview_item, rosterItems);
}
Activity activity = getActivity();
if(activity != null)
{
ListView list = (ListView)activity.findViewById(R.id.lstRosterItems);
list.setAdapter(_adapter);
_adapter.notifyDataSetChanged();
}
}
I've read about similar issues caused by code being called before the fragment is attached. I guess my question is, is there a way to delay the call to the updateRosterList until after the onAttach is called? The solution I'm toying with is that if getActivity() returns null then store the data in private variable in the fragment, and in the onAttach method check if there is data in the varialbe and then call the update on the adapter. This seems a bit hacky though. Any ideas?
UPDATE: I've managed to get it working by doing this. I'm quite new to Android development and it seems a bit hacky to me as a solution. Is there a better way? Basically the updateRosterList function is the one that is called from outside of the fragment.
public class RosterListFragment extends Fragment {
RosterItemAdapter _adapter = null;
private ArrayList<RosterInfo> _items;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(R.layout.roster_listview, container, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
if(_items != null)
{
performUpdateRosterList(_items);
}
}
public void updateRosterList(ArrayList<RosterInfo> rosterItems)
{
Activity activity = getActivity();
if(activity != null)
{
performUpdateRosterList(rosterItems);
}
else
{
_items = rosterItems;
}
}
private void performUpdateRosterList(ArrayList<RosterInfo> rosterItems)
{
Activity activity = getActivity();
if(_adapter == null)
{
_adapter = new RosterItemAdapter(activity, R.layout.roster_listview_item, rosterItems);
}
ListView list = (ListView)activity.findViewById(R.id.lstRosterItems);
list.setAdapter(_adapter);
_adapter.notifyDataSetChanged();
}
}
You are correct, the activity isn't yet attached. There's two ways to handle this.
Don't make the changes until after the activity has been attached. Perhaps just save off rosterItems, and have it updated later.
Pass in the context into your updater function.
Personally, I would say the first is probably be better path, but either one could work fine.