So I have bottom navigation bar with 4 fragments for each tab, and inside each one I call an API request to fetch some data, but the problem is each time I press any tab of the bar, at least two of the fragments gets created and they call their own method and by extension they fires the API request..! I just want the fragment that I select to be instantiated.!
I know the adapter behaves like this to pre-render the fragment to ensure better transaction between tabs and whatnot..! but I really can't afford to call multiple api calls with each select..!
Adapter
public class My_PagerAdapter extends FragmentPagerAdapter {
// I've tried FragmentStatePagerAdapter but same thing
public My_PagerAdapter (FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
new MyFragment_1();
case 1:
new MyFragment_2();
case 2:
new MyFragment_3();
case 3:
new MyFragment_4();
}
}
#Override
public int getCount() {
return 4;
}
}
Edit
This how I call the adapter..
ViewPager viewPager = main.findViewById(R.id.vp);
viewPager.setOffscreenPageLimit(1);
viewPager.setAdapter(new My_PagerAdapter (getChildFragmentManager()));
navigationTabBar.setModels(models); // just UI stuff for each tab offered by the bottom navigation bar library,
navigationTabBar.setViewPager(viewPager);
I ran into this same exact issue on a project that I'm working on
The solution for me was to add the API calls on the OnResume method in each fragment.
That way they will only be triggered when the fragment is fully visible.
Check out the fragment lifecycle
Ok this is exactly an issue that i faced. The solution i have does not stop the viewpager from creating the fragments but it will stop the calls to network api.
Heres the gist:
1) Create an interface
public interface ViewPagerLifeCycleManagerInterface {
void onResumeAndShowFragment();
void onPauseAndHideFragment();
//void refreshFragment();
}
2) modify your FragmentPagerAdapter to override the onInstantiateItem method
Here each Fragment will have a weakReference declared inside the Adapter class in order to store a reference to the fragments created
#Override
public Object instantiateItem(ViewGroup container, int position){
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
switch (position){
case 0:
xyzWeakReference=null;
xyzFragmentWeakReference=new WeakReference<>((xyz)createdFragment);
break;
case 1:
xyz1WeakReference=null;
xyz1WeakReference=new WeakReference<>((xyz1WeakReference)createdFragment);
break;
}
return createdFragment;
};
3) Inside the FragmentPagerAdapter, add the following method in order to fetch the weak reference of the fragment in picture
public Fragment getFragmentAtGivenPosition(int i){
switch (i){
case 0:
if(xyzFragmentWeakReference == null){
return null;
}
return xyzFragmentWeakReference.get();
case 1:
if(xyz1FragmentWeakReference == null){
return null;
}
return xyz1FragmentWeakReference.get();
}
}
4) Now in the activity where the TabLayout is created and the view pager instantiated, attach a listener to the TabLayout for listening to tab changes
tabLayout_bookmarks.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(final TabLayout.Tab tab) {
//let the instantiateItem have some time to be called by the adapter
currentFragmentIndex = tab.getPosition();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ViewPagerLifeCycleManagerInterface currentFragment = (ViewPagerLifeCycleManagerInterface)btca.getFragmentAtGivenPosition(tab.getPosition());
if(currentFragment!=null){
currentFragment.onResumeAndShowFragment();
}else{
//Log.d("FragmentCreate","Current fragment is null and fucked up in adapter");
//if it is null ... that means the adapter hasn't yet called instantiate item ... this internally calls get item any way
//.....
//This shouldn't really hit but in case it does ... keep a handler in order to ensure that everything is created
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ViewPagerLifeCycleManagerInterface localFragment = (ViewPagerLifeCycleManagerInterface)btca.getItem(tab.getPosition());
//getItem never returns a null fragment unless supplied a horrendous value for position
//by the time these 50 ms pass, the instantiate item should surely have been called
//else it will be an empty space ... no crash though
localFragment.onResumeAndShowFragment();
}
},50);
}
}
},100);
}
#Override
public void onTabUnselected(final TabLayout.Tab tab) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ViewPagerLifeCycleManagerInterface currentFragment = (ViewPagerLifeCycleManagerInterface)btca.getFragmentAtGivenPosition(tab.getPosition());
if(currentFragment!=null){
currentFragment.onPauseAndHideFragment();
}else{
//Log.d("FragmentCreateTab","the fucking fragment was null");
//if it is null ... that means the adapter hasn't yet called instantiate item ... this internally calls get item any way
//.....
//This shouldn't really hit but in case it does ... keep a handler in order to ensure that everything is created
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
ViewPagerLifeCycleManagerInterface localFragment = (ViewPagerLifeCycleManagerInterface)btca.getItem(tab.getPosition());
//getItem never returns a null fragment unless supplied a horrendous value for position
//by the time these 50 ms pass, the instantiate item should surely have been called
//else it will be an empty space ... no crash though
localFragment.onPauseAndHideFragment();
}
},50);
}
}
},100);
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
//do nothing
}
});
5) In each of the Fragments inside the Viewpager, implement the Interface we created in step 1 and override the methods.
Create a boolean variable in each fragment amIVisible... This will help decide when the fragment is visible and when it can call the network api
a) here for the first fragment in viewpager, i.e at 0 index, the network api call has to happen immediately after the view gets created. This fragment is obviously visible by default. This is written inside onCreateView method
if(dataList!=null && dataList.size()==0) {
if (savedInstanceState==null) {
//your api call to load from net
} else {
if (savedInstanceState.getBoolean("savedState")) {
//If you have saved data in state save, load it here
} else {
//only fire the async if the current fragment is the one visible, else the onResumeAndShowFragment will trigger the same async when it becomes visible
if (savedInstanceState.getBoolean("amIVisible")) {
//Load data from net
}
}
}
}
The other methods are as follows for the first fragment
#Override
public void onResumeAndShowFragment() {
amIVisible=true;
if(dataList!=null && dataList.size()==0){
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
//Load data from net if data was not found,
//This basically means auto refresh when user scrolls back and the fragment had no data
}
},400);
}
}
#Override
public void onPauseAndHideFragment() {
amIVisible=false;
}
Here i have overriden onSaveInstanceState method and saved the value of amIVisible and savedState is a boolean which indicates if the list has at least 1 item or not.
b) For the other fragments, Data will be loaded by the following process
if(savedInstance!=null){
if (savedInstance.getBoolean("savedState")) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
//load data from saved State
}
},100);
} else {
//only fire the async if the current fragment is the one visible, else the onResumeAndShowFragment will trigger the same async when it becomes visible
if (savedInstance.getBoolean("amIVisible")) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
//load data from net
}
},100);
}
}
}
The interface methods are the same for the other fragments.
This is pretty complicated but does the job. The weak references inside the adapter even allow garbage collection and avoid context leaks.
Related
I programmed a Vocabulary Trainer with Vocabulary Cards. The Vocabulary Cards are Entries in a Room Database created from an asset. I am displaying these Vocabulary Cards with ViewPager2 in an Activity. I have a 'correct' and a 'false' button and when the user clicks on either, I want to update the Vocabulary Card (-> The entry in the sqlite database) and automatically swipe to the next item of the ViewPager2.
If I implement the buttons in the ViewPager2Adapter, I can't find a way to change the position of the ViewPager2. If I implement the buttons in the activity the sqlite entry does not update properly (After it updates the entry, the activity is constantly refreshed, it seems like it never the leaves the OnClick methode of the button).
So is it possible to change the position of ViewPager2 from inside the ViewPager2Adpater?
Thanks for your help!
That is the relevant code if I have the buttons in my ViewPager2Adapter. Here I don't know how to change the position of the ViewPager2
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
holder.btn_false.setOnClickListener(v15 -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
That is the relevant code if I have the buttons in the Activity. Here the update function triggers an infinite updating of the Activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
btn_correct_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
btn_false_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
Objects.requireNonNull(getSupportActionBar()).setTitle(getResources().getString(R.string.learn_new_words));
LiveData<List<VocabularyCard>> allNewCards = vocabularyViewModel.getAllNewCards(goal);
allNewCards.observe(this, vocabularyCards -> vocabularyViewModel.setCurrentCards(vocabularyCards));
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
viewPager2Adapter.setCurrentCards(vocabularyCards);
viewpager2.setAdapter(viewPager2Adapter);
viewpager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
#Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
});
The update function in the Room DAO is straightforward:
#Update
void updateSingleVocabularyCard(VocabularyCard vocabularyCard);
I left out all the code that is not relevant.
There are several ways to propagate an event from the adapter to the activity where you manage your cards using ViewPager2. Let's have a look how it can be done either using an interface or using the same view model. But in any case I strongly recommend you to update your database in a background thread to prevent any possible UI lags.
1. Using an interface
This option is more flexible since you can propagate events as well as pass data as parameters. You can also reuse this interface for other cases. As far as I See you have a holder that has 2 buttons for the users to make choices. So our event here would be something like ChoiceEventListener, let's call this interface like so. Then you'd have to add a method to handle this event from within anywhere you wanna hear this event, and let's call its handle method onChoice(). Finally we would need a variable to indicate what the choice is. Now that ready to implement, let's write the new interface...
ChoiceEventListener.java
public interface ChoiceEventListener {
void onChoice(VocabularyCard vocabularyCard, boolean choice);
}
The next thing to do is to implement this interface where you want to listen to this event. In this case it is in your activity. There are 2 ways to do this:
You make your activity to inherit its methods using the implements keyword
YourActivity.java
public class YourActivity extends AppCompatActivity implements ChoiceEventListener {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, this);
}
#Override
public void onChoice(VocabularyCard vocabularyCard, boolean choice) {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
}
}
You can implement it as an anonymous function
YourActivity.java
public class YourActivity extends AppCompatActivity {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, (vocabularyCard, choice) -> {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
});
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Here is your listener to deliver the choice event to it
private final ChoiceEventListener listener;
// Constructor
public ViewPager2Adapter(/* Other params... */, ChoiceEventListener listener) {
/* Other inits */
this.listener = listener;
}
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
listener.onChoice(vocabularyCard, true); // true for correct
});
holder.btn_false.setOnClickListener(v15 -> {
listener.onChoice(vocabularyCard, false); // false for false :)
});
}
}
2. Use the ViewModel for inter-communication
In this option we use a LiveData object to make page switching. The only thing you need to know in your activity is the current position which you get it from the adapter class. Once you update it in the adapter, set the current position value in live data so that you can switch the page in your activity.
VocabularyViewModel.java
public class VocabularyViewModel extends ViewModel {
public MutableLiveData<Integer> mldCurrentPosition = new MutableLiveData<>(0);
}
YourActivity.java
public class YourActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
vocabularyViewModel.mldCurrentPosition().observe(this, currentPosition -> {
if(currenPosition == null) return; // ignore when null
viewpager2.setCurrentItem(currentPosition + 1);
}
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
holder.btn_false.setOnClickListener(v15 -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
}
}
I have an activity that has 3 fragments on it with Tabs, one of them is called "TaskFragment".
In my main Activity i only load the fragments.
In TaskFragment i have a RecyclerView that is working fine and is showing the items as intended.
The problem comes, when i insert data using a DialogFragment, because it does insert data (i am using DbFlow ORM), but it does not (of course) refresh the adapter since it is in the TaskFragment fragment inside the DetailMainActivity activity as i said.
I have tried to use onResume() and onPause() in order to refresh the adapter, but they are never called since the activity does not get paused or in onresume for a DialogFragment.
I have tried aswell to use an interface, but it does not work and i have searched all over stackoverflow and google with no luck.
I leave here some of my code for you to understand better:
DetailMainActivity.java
Here in the onClick interface i show the DialogFragment to the user to input the information.
FragmentManager fm = getSupportFragmentManager();
AddSimpleTask sptask = new AddSimpleTask();
sptask.show(fm, "tag");
TaskFragment.java
In this fragment i have my RecyclerView
private void setupRecyclerView() {
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
mRecyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (DetailMainActivity.FAB_Status) {
DetailMainActivity.hideFAB();
DetailMainActivity.FAB_Status = false;
}
return false;
}
});
}
private void setupAdapter() {
adapter = new DetailMainTaskAdapter(simpleTaskList, this);
}
AddSimpleTask
And this is my DialogFragment. I have set a setOnShowListener() in order to avoid the DialogFragment to get dismiss early.
#Override
public void onShow(DialogInterface dialogInterface) {
final AlertDialog dialog =(AlertDialog) getDialog();
if (dialog != null){
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
positiveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mEditTextName.getText().toString().trim().isEmpty() ||
mEditTextContent.getText().toString().trim().isEmpty() ) {
if (mEditTextName.getText().toString().trim().isEmpty()) {
mEditTextName.setError("Can not be empty");
}
if (mEditTextContent.getText().toString().trim().isEmpty()) {
mEditTextContent.setError("Can not be empty");
}
}else {
presenter.beingInsertion(mEditTextName.getText().toString().trim(), mEditTextContent.getText().toString().trim()
, foreignId);
}
}
});
negativeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dismiss();
}
});
}
}
If the insert is successfully achieved the onInsertSuccess method is called (i am using MVP)
#Override
public void onInsertSuccess() {
Snackbar.make(getActivity().findViewById(R.id.containerMainDetail), "Actividad agregada", Snackbar.LENGTH_SHORT).show();
dismiss();
}
I have called adapter.notifyDataSetChanged() in many places, and i also tried with a custom interface, but i can not make this work.
Sorry for the long post, but thanks in advance for your help.
There are some errors in your statement but I'll get to that later. notifyDataSetChanged() only notifies the adapter that the underlying list (or array) has changed. The implication is that you first need to requery your database and obtain the new list before calling notifyDataSetChanged() on the adapter else there is no point as the underlying list will still be the same and it will not update the adapter.
The correct way of calling this will be through your custom listener interface and not in the onPause()/onResume() callbacks as there is the possibility that the user does not enter a value and hence you will unnecessarily be querying the database. In your custom listener interface implementation, first update the list with the new data from the DB and then notify the adapter.
Which leads to the error in assumption that onPause()/onResume() callbacks do not happen when your Activity is covered by a DialogFragment - this is incorrect. The moment the activity view is even partially covered, the onPause() callback is triggered.
I have a view pager with 2 fragments. One of them has a blue screen that should be hidden once the fragment is selected. So, I want to grasp a pointer to this fragment that I can call a certain method on once the user selects this fragment. I have implemented the addOnPageChangeListener interface. I want to call that method, to hide the blue screen, in the onPageSelected call.
My question is how can I grasp a pointer to the current selected fragment?
I have tried the following:
AnswersFragment fragment = (AnswersFragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + this.pager + ":" + position);
if (fragment == null)
fragment = new AnswersFragment();
But it always returns null.
I have also tried to keep an instance variable called currentAnswerFragment, but it doesn't necessarily point to the current fragment as the view pager keeps track of 3 fragments at the same time.
Also, my model is not an array (or 2) of fragments. I call a function that initializes a fragment and return it. With that said, I can't use the index of the current item to get the fragment being displayed.
You need to override onPageSelected method and fire respected method on desired screen, for example:
#Override
public void onPageSelected(int position) {
if (position == 0) {
callForPage();
} else if (position == 1) {
callForPage1();
}
}
and you can use following code to go to selected screen.
viewPager.setCurrentItem(pageposition,false);
setCurrentItem takes two parameters: item number and boolean for smooth scrolling.
UPDATE
make a PageAdapter class for example
public class PagerAdapter extends FragmentPagerAdapter {
Context mcontext;
public PagerAdapter(FragmentManager fm, Context context) {
super(fm);
mcontext = context;
}
#Override
public Fragment getItem(int position) {
if (position == 0) {
return new FragmentA();
}
else if(position==1){
return new FragmentB();
}
else
return new FragmentC();
}
#Override
public int getCount() {
return 3;
}
}
Then in your Activity class
mViewPager.setAdapter(new PagerAdapter(getSupportFragmentManager(), this));
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
#Override
public void onPageSelected(int position) {
///Here you handle the pointer thing
}
#Override
public void onPageScrollStateChanged(int state) {
}
});
Edit:
A better implementation of the solution I came up with is found here
After a while of debugging, I figured out a solution.
By keeping an external data structure of the previous fragments visited. In my case I kept a HashMap as a normal array won't work. In some cases I call the pager.setCurrentItem(position). Meaning that I jump fragments. So a HashMap is more appropriate.
So I have a music application. I am trying to update the UI with the progress of the media player (like current time, current song, album cover) everytime the song changes. I found that using interfaces was a awesome magical way of communication between activity and fragments so I implemented an interface in my MusicManger class. My code will show what and how did it.
Two problems
1) Commented look below, ExecutorService seems to stop after one loop. No Errors in catch block (this is why I tagged with java)
2) Commented please look, All the System.out methods print but the UI doesn't update. I do believe I called the method from mainThread so it should update.
I'll show code in logical order will add titles in bold before code segment to tell you basic idea of code.
Passing UI references from fragment to MusicManager class, code below in Fragment class
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_item_songlist, container, false);
// Set the adapter
TextView musicManagerSongName, musicManagerCurrent, musicManagerTotal;
ProgressBar musicManagerProgress;
ImageView musicManagerImageView;
mListView = (AbsListView) view.findViewById(R.id.slist);
musicManagerSongName = (TextView)view.findViewById(R.id.textView12);
musicManagerCurrent = (TextView)view.findViewById(R.id.textView10);
musicManagerTotal = (TextView)view.findViewById(R.id.textView11);
musicManagerProgress = (ProgressBar)view.findViewById(R.id.progressBar);
musicManagerImageView = (ImageView)view.findViewById(R.id.imageView2);
MainActivity.mediaPlayer.passUIReferences(musicManagerSongName, musicManagerCurrent, musicManagerTotal, musicManagerProgress, musicManagerImageView, view);
// line above is a method within MusicManager that takes the references will show code next!
ImageButton playbutton = (ImageButton)view.findViewById(R.id.playbuttonbar);
ImageButton nextButton = (ImageButton)view.findViewById(R.id.nextbuttonbar);
ImageButton backButton = (ImageButton)view.findViewById(R.id.backbuttonbar);
ImageButton toggleButton = (ImageButton)view.findViewById(R.id.shufflebuttonbar);
ImageButton pausebutton = (ImageButton)view.findViewById(R.id.pausebuttonbar);
playbutton.setBackgroundResource(R.drawable.playbuttonbar);
playbutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
MainActivity.mediaPlayer.stateChange(1);
}catch(Exception e) {
}
}
});
backButton.setBackgroundResource(R.drawable.backbutton1);
nextButton.setBackgroundResource(R.drawable.nextbutton1);
toggleButton.setBackgroundResource(R.drawable.shufflebuttonselected);
pausebutton.setBackgroundResource(R.drawable.pausebutton1);
pausebutton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
MainActivity.mediaPlayer.stateChange(0);
} catch (Exception e){
}
}
});
mListView.setAdapter(mAdapter);
((MainActivity) mListener).restoreActionBar();
return view;
}
As Commended above the code that is located in MusicManager class that takes references and stores them. Also shows interface implementation with MusicManager class. And the Executor service
public void passUIReferences(View... views) {
this.uiElements = views;
}
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private MediaplayerUpdateInterface uiUpdateInterface;
public MediaPlayerManager(MediaplayerUpdateInterface inter) {
this.player = new MediaPlayer();
this.uiUpdateInterface = inter;
// The below line starts the single thread while loop for excutorservice and only loops and prints "this" once after I start one song then it never loops again
executorService.submit(new Runnable() {
#Override
public void run() {
while(true) {
if (player.isPlaying() && uiElements != null) {
System.out.println("this");
uiUpdateInterface.updateUI(uiElements, 0);
}
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
public interface MediaplayerUpdateInterface {
public void updateUI(View[] views, int type);
}
Finally some code from MainActivity class that actually is suppose to update the UI note that both println's work as expected but only once as stated above because of the executorservice issue
public static MediaPlayerManager mediaPlayer = new MediaPlayerManager(new MediaPlayerManager.MediaplayerUpdateInterface() {
#Override
public void updateUI(View[] views, int type) {
System.out.println("check1 " + type);
updateMediaplayerViews(views, type);
}
});
private static void updateMediaplayerViews(View[] views, int type)
{
switch(type) {
case 0:
System.out.println("that?");
((TextView)views[0]).setText(mediaPlayer.getCurrentSongInfo().getName().length() > 22? mediaPlayer.getCurrentSongInfo().getName().substring(0, 19)+"..." : mediaPlayer.getCurrentSongInfo().getName());
break;
}
views[views.length - 1].invalidate();
}
The view array is shown perviously! Also the last view in the array is shown as the main view for songlist fragment.
I am sorry for all the code I've tried to debug it as you can see from my println's there is just something I am unaware of going on here.
Ok so there was an error that I needed to catch to see within the following code:
private static void updateMediaplayerViews(View[] views, int type)
{
switch(type) {
case 0:
System.out.println("that?");
((TextView)views[0]).setText(mediaPlayer.getCurrentSongInfo().getName().length() > 22? mediaPlayer.getCurrentSongInfo().getName().substring(0, 19)+"..." : mediaPlayer.getCurrentSongInfo().getName());
break;
}
views[views.length - 1].invalidate();
}
The issue is I was trying to change the view from a different thread then the one which created it. Solving it was pretty long and painful but basically I made it nonstactic used more interfaces then used the famous
Mainactivity.runOnUiThread(new Runnable(....));
I've found a ton of conflicting information regarding the proper way to restore application state when using Fragments embedded in Activities. Please let me know if my architecture is the problem because that is totally possible. My test Weather app is architected as follows.
The main activity "ReportsActivity" contains the fragment "ReportsFragment" (This is a list of the next 10 days of Weather Reports)
ReportsFragment has an onItemClickListener that launches a new Activity "WeatherDetailActivity" and passes it an intent which contains some JSON Data that I use to populate the Weather Detail UI. This data is then presented on a fragment that the WeatherDetailActivity manages.
My problem is, when the user presses the back button, the ReportsFragment has been destroyed so it runs through its full lifecycle. I've tried a number of techniques I've found online to load the activity's data from a bundle, but no matter what I've tried so far the Intents' Extras are null in the ReportsActivity's onCreate method. (Note: the reason I need to do this is to avoid firing off an API Call each time I open my main Activity which fetches weather data from Weather Underground).
I'm struggling determining what would be the best way to construct this app: Should I have a single activity that pushes and pops Fragments that it manages? Or are multiple activities that each manage their own fragments the standard practice?
At the moment here is how I'm attempting to save my application state onto the intent. I'm trying to save the state in onPostExecute from my AsyncTask so i'm on the main thread after i've fetched my results from the API Call:
#Override
protected void onPostExecute(Report[] result){
if (result != null){
ArrayList<String>reportsArrayList = new ArrayList<String>();
Gson jsonArray = new GsonBuilder().setPrettyPrinting().create();
for (int x = 0; x < result.length; x++){
reportsArrayList.add(jsonArray.toJson(result[x], Report.class));
}
mExtras.putStringArrayList(ReportsActivity.ReportsActivityState.KEY_ACTIVITY_REPORTS,reportsArrayList);
}
}
I then attempt to restore state from the ReportsActivity's onCreate Method:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_reports);
if (savedInstanceState == null) {
Intent intent = getIntent();
mFragment = ReportsFragment.newInstance(intent
.getStringArrayListExtra(ReportsActivityState.KEY_ACTIVITY_REPORTS));
getFragmentManager().beginTransaction()
.add(R.id.container, mFragment).commit();
}
}
In all cases the StringArrayListExtra I'm trying to get from the intent return null.
This could very well be me trying to solve an Android problem with an iOS mindset, but is there not an easy way to just restore the main activity to what it was before I pushed the detail view?
I think it would be worth your while taking a look at EventBus.
Basically you can define a object holder of any kind, for example:
class WeatherData {
List<String> reports;
public WeatherData(List<String> reports) {
this.reports = reports;
}
}
Now, in an Activity or Fragment in which you wish to remember the state, or pass some state to another Activity or Fragment do:
// this removes all the hazzle of creating bundles etc
EventBus.getDefault().postSticky(new WeatherData(reports));
And any where in your code your wish to know the most recent WeatherData:
WeatherData weatherData = EventBus.getDefault().getSticky(WeatherData.class);
EventBus also has nice methods for event handling (button clicks, completion of long running processes, etc..)
The library can be found here: https://github.com/greenrobot/EventBus
And some more examples here: http://awalkingcity.com/blog/2013/02/26/productive-android-eventbus/
Some suggestions without using 3. part library:
1) Calling setRetainInstance(true) in your fragments onCreate method, what this should do is to persist public variables between instances.
Though it seems it does not work for fragments on the back stack: Understanding Fragment's setRetainInstance(boolean)
2) Hand the fragment data to your Activity, something like reading/updating ((YourActivity)getActivity()).someFragmentBundle, possibly save it in onSaveInstanceState of the Activity and retrieve it in onCreate. That is, having your Activity hold the data in-between instances.
3) You could also persist the data, saving it to a file or using SharedPreferences http://developer.android.com/training/basics/data-storage/index.html
This method has the advantage that it will enable restoring the data even after a complete kill of your app.
The Architectural question
Disclaimer: subjective opinion
I would generally say keep the Activity as 'slim' as possible, holding a range of related fragments.
Thus, having multiple Activities is fine but they should each manage a set of (or a single) related fragments that are relevant for the current Activity.
It just occurred to me to check out one of the Android Studio templates that Google Provides that I often overlooked. From Google's own templates, it appears clear that the preferred method for Master Detail Activities/Fragments is to have each Fragment Managed by their own activities (as I was attempting to achieve above).
(I should note that I was able to successfully achieve to flow that I wanted using a single Activitiy with multiple fragments and customizing the Animation and forcefully showing and hiding the up button.)
PersonListActivity.java
public class PersonListActivity extends Activity
implements PersonListFragment.Callbacks {
/**
* Whether or not the activity is in two-pane mode, i.e. running on a tablet
* device.
*/
private boolean mTwoPane;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_person_list);
if (findViewById(R.id.person_detail_container) != null) {
// The detail container view will be present only in the
// large-screen layouts (res/values-large and
// res/values-sw600dp). If this view is present, then the
// activity should be in two-pane mode.
mTwoPane = true;
// In two-pane mode, list items should be given the
// 'activated' state when touched.
((PersonListFragment) getFragmentManager()
.findFragmentById(R.id.person_list))
.setActivateOnItemClick(true);
}
// TODO: If exposing deep links into your app, handle intents here.
}
/**
* Callback method from {#link PersonListFragment.Callbacks}
* indicating that the item with the given ID was selected.
*/
#Override
public void onItemSelected(String id) {
if (mTwoPane) {
// In two-pane mode, show the detail view in this activity by
// adding or replacing the detail fragment using a
// fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(PersonDetailFragment.ARG_ITEM_ID, id);
PersonDetailFragment fragment = new PersonDetailFragment();
fragment.setArguments(arguments);
getFragmentManager().beginTransaction()
.replace(R.id.person_detail_container, fragment)
.commit();
} else {
// In single-pane mode, simply start the detail activity
// for the selected item ID.
Intent detailIntent = new Intent(this, PersonDetailActivity.class);
detailIntent.putExtra(PersonDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}
PersonListFragment.java
public class PersonListFragment extends ListFragment {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
*/
private static final String STATE_ACTIVATED_POSITION = "activated_position";
/**
* The fragment's current callback object, which is notified of list item
* clicks.
*/
private Callbacks mCallbacks = sDummyCallbacks;
/**
* The current activated item position. Only used on tablets.
*/
private int mActivatedPosition = ListView.INVALID_POSITION;
/**
* A callback interface that all activities containing this fragment must
* implement. This mechanism allows activities to be notified of item
* selections.
*/
public interface Callbacks {
/**
* Callback for when an item has been selected.
*/
public void onItemSelected(String id);
}
/**
* A dummy implementation of the {#link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks sDummyCallbacks = new Callbacks() {
#Override
public void onItemSelected(String id) {
}
};
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public PersonListFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: replace with a real list adapter.
setListAdapter(new ArrayAdapter<DummyContent.DummyItem>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
DummyContent.ITEMS));
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore the previously serialized activated item position.
if (savedInstanceState != null
&& savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
}
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Activities containing this fragment must implement its callbacks.
if (!(activity instanceof Callbacks)) {
throw new IllegalStateException("Activity must implement fragment's callbacks.");
}
mCallbacks = (Callbacks) activity;
}
#Override
public void onDetach() {
super.onDetach();
// Reset the active callbacks interface to the dummy implementation.
mCallbacks = sDummyCallbacks;
}
#Override
public void onListItemClick(ListView listView, View view, int position, long id) {
super.onListItemClick(listView, view, position, id);
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mActivatedPosition != ListView.INVALID_POSITION) {
// Serialize and persist the activated item position.
outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
}
}
/**
* Turns on activate-on-click mode. When this mode is on, list items will be
* given the 'activated' state when touched.
*/
public void setActivateOnItemClick(boolean activateOnItemClick) {
// When setting CHOICE_MODE_SINGLE, ListView will automatically
// give items the 'activated' state when touched.
getListView().setChoiceMode(activateOnItemClick
? ListView.CHOICE_MODE_SINGLE
: ListView.CHOICE_MODE_NONE);
}
private void setActivatedPosition(int position) {
if (position == ListView.INVALID_POSITION) {
getListView().setItemChecked(mActivatedPosition, false);
} else {
getListView().setItemChecked(position, true);
}
mActivatedPosition = position;
}
}
PersonDetailActivity.java
public class PersonDetailActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_person_detail);
// Show the Up button in the action bar.
getActionBar().setDisplayHomeAsUpEnabled(true);
// savedInstanceState is non-null when there is fragment state
// saved from previous configurations of this activity
// (e.g. when rotating the screen from portrait to landscape).
// In this case, the fragment will automatically be re-added
// to its container so we don't need to manually add it.
// For more information, see the Fragments API guide at:
//
// http://developer.android.com/guide/components/fragments.html
//
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.
Bundle arguments = new Bundle();
arguments.putString(PersonDetailFragment.ARG_ITEM_ID,
getIntent().getStringExtra(PersonDetailFragment.ARG_ITEM_ID));
PersonDetailFragment fragment = new PersonDetailFragment();
fragment.setArguments(arguments);
getFragmentManager().beginTransaction()
.add(R.id.person_detail_container, fragment)
.commit();
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
navigateUpTo(new Intent(this, PersonListActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}
PersonDetailFragment.java
public class PersonDetailFragment extends Fragment {
/**
* The fragment argument representing the item ID that this fragment
* represents.
*/
public static final String ARG_ITEM_ID = "item_id";
/**
* The dummy content this fragment is presenting.
*/
private DummyContent.DummyItem mItem;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public PersonDetailFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(ARG_ITEM_ID)) {
// Load the dummy content specified by the fragment
// arguments. In a real-world scenario, use a Loader
// to load content from a content provider.
mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID));
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_person_detail, container, false);
// Show the dummy content as text in a TextView.
if (mItem != null) {
((TextView) rootView.findViewById(R.id.person_detail)).setText(mItem.content);
}
return rootView;
}
}