1

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.

Jaythaking
  • 2,200
  • 4
  • 25
  • 67
AlexF11
  • 231
  • 1
  • 5
  • 20
  • try setting the visibility of the fragment's container view to gone then set it back to visible when the data is ready – kimchibooty Jul 05 '16 at 14:23
  • What i do is that i have a public method in my Fragment template that i call in my main activity after i have my set of datas. Since those are(for me) in a fragmentPagerAdapter which takes the List of Fragments, i pass data like this: ((MyFragmentClass)ListofFragments.get(i)). -the method of the fragments- . Then i only need to call the notifyDataSetChanged() of the adapter. This is not about sharedpreferences though, but maybe it can help you, as it works for me in a similar situation. – VikingPingvin Jul 05 '16 at 14:24
  • @VikingPingvin is it not possible to perform tasks before the fragments are even created? – AlexF11 Jul 05 '16 at 14:28
  • @kimchibooty will setting it to visible force a recreation? – AlexF11 Jul 05 '16 at 14:30
  • @VikingPingvin I could abandon sharedpreferences and use your solution but I'd need some more in depth help implementing it – AlexF11 Jul 05 '16 at 14:34
  • I can provide some more in-depth, if you want to know more. This is the topic i based my solution on: http://stackoverflow.com/questions/16232224/how-can-i-add-a-fragment-to-a-viewpager-addview-crashes-my-app If you recreate that, it should work very similary. Also it may not even be necesary to drop your way of implementing that function. If you create a FIFO like array or list to store the data while the fragment is unavailable, that should solve the issue too. Of course you may need to implement error-safe functions for that. – VikingPingvin Jul 06 '16 at 07:45
  • That example actually looks nearly identical to what I have, I'm not sure what else I need to implement based on it @VikingPingvin where does the passing of data from the mainactivity to the fragment come from? – AlexF11 Jul 06 '16 at 13:32
  • I misread the post, he's dynamically adding fragments, so I'd create the fragment as I get the data for it – AlexF11 Jul 06 '16 at 13:39
  • That is only a form if implementation. You can pre-create any number of fragments if you know your data size. In my situation i am recieving serial data throught bluetooth, form which i canfigure how many fragments i need. I have a problem with it at the moment though, as adding new fragments is okay, but removing then reusing its place with a new fragment doesn't work yet. Android and it's stupid fragment reusing policy.... – VikingPingvin Jul 06 '16 at 15:56
  • I'm thinking I should go with what he's doing and add them on the fly. I'd be able to pass arguments that way correct? I also have anywhere between 2 and 4 fragments depending on the record so that might also solve another issue. – AlexF11 Jul 06 '16 at 16:03
  • @VikingPingvin I followed that example you posted and it's perfect, I pass the JSON to the fragment at creation and it displays nearly instantly. It's perfect. – AlexF11 Jul 13 '16 at 16:56

2 Answers2

1

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();
    }
 //...
}
Jaythaking
  • 2,200
  • 4
  • 25
  • 67
  • How is the loader used within the class that's hosting the fragments? This looks like the best solution it just might take me a while to understand it. Also rather than populating a listview I have to dynamically created textviews to display the data – AlexF11 Jul 05 '16 at 14:50
  • It might look scary at first, but once you get into it, you will understand and at the end it's the most reliable way. There is nothing to do in the Activity hosting the Fragment... Also, Dynamically creating TextView might complicate the process... Is that necessary? I also found an implementation of AsyncTaskLoader for server request here: http://stackoverflow.com/questions/19744022/asynctaskloader-for-http-requests-to-handle-orientation-changes-using-generics – Jaythaking Jul 05 '16 at 14:55
  • For this specific function I'd say it is. Basically it's data like Address, Name, etc. So I'd display a heading and then the value. All I need is the fragment to get the group name, from there I can just do another web call and get the data I need. mainActivity does a webcall, gets the groups, each fragment gets it's group and grabs the data it needs. Either that or the Fragment gets it's group and the json from the webcall and parses it. – AlexF11 Jul 05 '16 at 14:57
  • That example looks good, it might make things a bit more clear. I'm thinking I should pass the fragment the json and it's group since parsing the data is much faster than a web call. – AlexF11 Jul 05 '16 at 15:03
  • I have the same kind of situation as you, as I have group of element inside group inside group and every level is a Fragment that contains a RecyclerView. In my case thought, I store everything in a database and each fragment retrieve the data it need through a database call. – Jaythaking Jul 05 '16 at 15:03
  • Yes, you could do that... but you really need to implement some kind of persistency though... – Jaythaking Jul 05 '16 at 15:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/116482/discussion-between-alexf11-and-jaythaking). – AlexF11 Jul 05 '16 at 15:06
0

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).

Michael Spitsin
  • 2,539
  • 2
  • 19
  • 29