I am using a fragment which shows differnet informations about an object. Inside onCreate of the fragment I retrieve some values from the object and store them in global variables:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
String serializedShow = getArguments().getString(SHOW_TO_LOAD);
mShow = (new Gson()).fromJson(serializedShow, Show.class);
mScope = mShow.getScope();
mLanguage = MainActivity.getSystemLanguage();
mLanguage = mLanguage.replace("_", "-");
mId = Integer.valueOf(mShow.getShowId()).toString(); //this line is important
Log.v(LOG_TAG, "DetailFragment onStart, Id is " + mId);
}
As you can see I assign to this variable a specific ID. I use the log to check if the value of mId is the correct one and it is. Everything works well so far.
Next int he code the user can click a button to upen up am url in the browser. The URls are different with each time this fragment is created (every time this fragment shows different stuff)
private void fetchIMDBId(){
Log.v(LOG_TAG, "starting IMDBAsyncTask with id " + mId);
new Async4().execute(mId);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_launch:
fetchIMDBId();
return true;
default:
break;
}
return false;
}
As you can see the method fetchIMDBId() gets called. I have a log line to check if the value of mId which was set before in onCreate is the same.
It is not the same, its a totally different value.
Specifically is the id of the object that was being displayed in a previous instance if this fragment.
Please tell me what I'm doing wrong. Thanks.
E: variable declaration
public class DetailFragment extends Fragment {
private static final String LOG_TAG = DetailFragment.class.getSimpleName();
...
private String mScope, mLanguage, mId;
...
#Override
public void onCreate(Bundle savedInstanceState) {
...
}
E: full source here
I believe you are not assigning your mId correctly to the Async task. You should apply the mid to the async task method so that the corresponding mId from each fragment is called
private void fetchIMDBId(String mId){
Log.v(LOG_TAG, "starting IMDBAsyncTask with id " + mId);
new Async4().execute(mId);
}
and then call the method within the fragment
fetchIMDBId(this.mId);
Try using this.mId and also check if your fragment object is same as previous one.
Call this.fetchIMDBId() instead.
Related
I have implemented the new architecture component on my android app, but unfortunately handling the states of these fragments has become a nightmare for me. Whenever I press the icon of the fragment, the fragment is recreated every time I navigate. How can I handle this or rather save these fragments states?
Here is my main activity handling the five fragments:
public class MainActivityCenterofInformation extends AppCompatActivity {
BottomNavigationView bottomNavigationView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_maincict);
setUpNavigation ();
}
public void setUpNavigation(){
bottomNavigationView = findViewById (R.id.bottom_nav_cict);
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager ()
.findFragmentById (R.id.nav_host_fragment_cict);
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
}
//adding animations to the fragment
}
I can't read Kotlin so please direct me to Java, thanks.
TL;DR: skip to the JUST SHOW ME THE STEPS ALREADY !!! section
That is the normal behaviour of the fragments. They are suppose to be recreated every time they are removed or replaced and you are suppose to restore their states using onSaveInstanceState.
Here is a nice article that describes how to do it : Saving Fragment States
Other than that you can use View Model which is the part of the following recommended android architecture. They are a great way to retain and restore UI data.
You can learn how to implement this architecture by following this step by step code lab
EDIT : Solution
Note : The solution assumes one doesn't wants to use ViewModels and simply wants to hide or show fragments using the Navigation UI
It covers the following points
Keeping fragment alive during navigation
Implement custom navigation behavior on back key press (Optional)
Background :
Android Navigation component has a NavController class that you can use to navigate to different destinations. NavController uses a Navigator that actually does the navigation. Navigator is an abstract class and anyone can extend/inherit it to provide custom navigation depending on the type of destination. When using fragments as destinations the NavHostFragment uses a FragmentNavigator whose default implementation replaces the fragments whenever we navigate using FragmentTransaction.replace() which completely destroys the previous fragment and adds a new fragment. So we have to create our own navigator and instead of using FragmentTransaction.replace() we will use a combination of FragmentTransaction.hide() and FragmentTransaction.show() to avoid fragments from being destroyed.
Default behavior of Navigation UI :
By default whenever you navigate to any other fragment other than the home fragment they won't get added to backstack so lets say if you select fragments in the following order
A -> B -> C -> D -> E
your back stack will have only
[A, E]
as you can see the fragments B, C, D weren't added to backstack so pressing back press will always get you to fragment A which is the home fragment
The behavior we want for now :
We want a simple yet effective behavior. We wan't all fragments to get added to backstack but if the fragment is already in backstack we want to pop all fragments upto the selected fragment.
Lets say I select fragment in following order
A -> B -> C -> D -> E
the backstack should also be
[A, B, C, D, E]
upon pressing back only the last fragment should be popped and backstack should be like this
[A, B, C, D]
but if we navigate to lets say fragment B, since B is already in the stack then all the fragments above B should be popped and our backstack should look like this
[A, B]
I hope this behavior makes sense. This behavior is easy to implement using global actions as you will see below and is better than the default one.
OK Hotshot! now what ? :
Now we have two options
extend FragmentNavigator
copy/paste FragmentNavigator
Well I personally wanted to just extend FragmentNavigator and override navigate() method but since all its member variables are private I couldn't implement proper navigation.
So I decided to copy paste the entire FragmentNavigator class and just change the name in entire code from "FragmentNavigator" to whatever I want to call it.
JUST SHOW ME THE STEPS ALREADY !!! :
Create custom navigator
Use custom tag
Add global actions
Use global actions
Add the custom navigator to the NavController
STEP 1: Create custom navigator
Here is my custom navigator called StickyCustomNavigator. All the code is same as FragmentNavigator except the navigate() method. As you can see it uses hide() , show() and add() method instead of replace(). The logic is simple. Hide the previous fragment and show the destination fragment. If this is our first time going to a specific destination fragment then add the fragment instead of showing it.
#Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {
private static final String TAG = "StickyFragmentNavigator";
private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";
private final Context mContext;
#SuppressWarnings("WeakerAccess") /* synthetic access */
final FragmentManager mFragmentManager;
private final int mContainerId;
#SuppressWarnings("WeakerAccess") /* synthetic access */
ArrayDeque<Integer> mBackStack = new ArrayDeque<>();
#SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsPendingBackStackOperation = false;
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
#SuppressLint("RestrictedApi")
#Override
public void onBackStackChanged() {
// If we have pending operations made by us then consume this change, otherwise
// detect a pop in the back stack to dispatch callback.
if (mIsPendingBackStackOperation) {
mIsPendingBackStackOperation = !isBackStackEqual();
return;
}
// The initial Fragment won't be on the back stack, so the
// real count of destinations is the back stack entry count + 1
int newCount = mFragmentManager.getBackStackEntryCount() + 1;
if (newCount < mBackStack.size()) {
// Handle cases where the user hit the system back button
while (mBackStack.size() > newCount) {
mBackStack.removeLast();
}
dispatchOnNavigatorBackPress();
}
}
};
public StickyFragmentNavigator(#NonNull Context context, #NonNull FragmentManager manager,
int containerId) {
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
#Override
protected void onBackPressAdded() {
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
#Override
protected void onBackPressRemoved() {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
}
#Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
if (mFragmentManager.getBackStackEntryCount() > 0) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mIsPendingBackStackOperation = true;
} // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
mBackStack.removeLast();
return true;
}
#NonNull
#Override
public StickyFragmentNavigator.Destination createDestination() {
return new StickyFragmentNavigator.Destination(this);
}
#NonNull
public Fragment instantiateFragment(#NonNull Context context,
#SuppressWarnings("unused") #NonNull FragmentManager fragmentManager,
#NonNull String className, #Nullable Bundle args) {
return Fragment.instantiate(context, className, args);
}
#Nullable
#Override
public NavDestination navigate(#NonNull StickyFragmentNavigator.Destination destination, #Nullable Bundle args,
#Nullable NavOptions navOptions, #Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
String tag = Integer.toString(destination.getId());
Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
if(primaryNavigationFragment != null)
ft.hide(primaryNavigationFragment);
Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
if(destinationFragment == null) {
destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
destinationFragment.setArguments(args);
ft.add(mContainerId, destinationFragment , tag);
}
else
ft.show(destinationFragment);
ft.setPrimaryNavigationFragment(destinationFragment);
final #IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStackImmediate(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()), 0);
mIsPendingBackStackOperation = false;
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
mIsPendingBackStackOperation = true;
isAdded = true;
}
if (navigatorExtras instanceof FragmentNavigator.Extras) {
FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
#Override
#Nullable
public Bundle onSaveState() {
Bundle b = new Bundle();
int[] backStack = new int[mBackStack.size()];
int index = 0;
for (Integer id : mBackStack) {
backStack[index++] = id;
}
b.putIntArray(KEY_BACK_STACK_IDS, backStack);
return b;
}
#Override
public void onRestoreState(#Nullable Bundle savedState) {
if (savedState != null) {
int[] backStack = savedState.getIntArray(KEY_BACK_STACK_IDS);
if (backStack != null) {
mBackStack.clear();
for (int destId : backStack) {
mBackStack.add(destId);
}
}
}
}
#NonNull
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
private int getDestId(#Nullable String backStackName) {
String[] split = backStackName != null ? backStackName.split("-") : new String[0];
if (split.length != 2) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
try {
// Just make sure the backStackIndex is correctly formatted
Integer.parseInt(split[0]);
return Integer.parseInt(split[1]);
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
#SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isBackStackEqual() {
int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount();
// Initial fragment won't be on the FragmentManager's back stack so +1 its count.
if (mBackStack.size() != fragmentBackStackCount + 1) {
return false;
}
// From top to bottom verify destination ids match in both back stacks/
Iterator<Integer> backStackIterator = mBackStack.descendingIterator();
int fragmentBackStackIndex = fragmentBackStackCount - 1;
while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) {
int destId = backStackIterator.next();
try {
int fragmentDestId = getDestId(mFragmentManager
.getBackStackEntryAt(fragmentBackStackIndex--)
.getName());
if (destId != fragmentDestId) {
return false;
}
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
return true;
}
#NavDestination.ClassType(Fragment.class)
public static class Destination extends NavDestination {
private String mClassName;
public Destination(#NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(StickyFragmentNavigator.class));
}
public Destination(#NonNull Navigator<? extends StickyFragmentNavigator.Destination> fragmentNavigator) {
super(fragmentNavigator);
}
#CallSuper
#Override
public void onInflate(#NonNull Context context, #NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}
#NonNull
public final StickyFragmentNavigator.Destination setClassName(#NonNull String className) {
mClassName = className;
return this;
}
#NonNull
public final String getClassName() {
if (mClassName == null) {
throw new IllegalStateException("Fragment class was not set");
}
return mClassName;
}
}
public static final class Extras implements Navigator.Extras {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
Extras(Map<View, String> sharedElements) {
mSharedElements.putAll(sharedElements);
}
#NonNull
public Map<View, String> getSharedElements() {
return Collections.unmodifiableMap(mSharedElements);
}
public static final class Builder {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
#NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElements(#NonNull Map<View, String> sharedElements) {
for (Map.Entry<View, String> sharedElement : sharedElements.entrySet()) {
View view = sharedElement.getKey();
String name = sharedElement.getValue();
if (view != null && name != null) {
addSharedElement(view, name);
}
}
return this;
}
#NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElement(#NonNull View sharedElement, #NonNull String name) {
mSharedElements.put(sharedElement, name);
return this;
}
#NonNull
public StickyFragmentNavigator.Extras build() {
return new StickyFragmentNavigator.Extras(mSharedElements);
}
}
}
}
STEP 2: Use custom tag
Now open up your navigation.xml file and rename the fragment tags related to your bottom navigation with whatever name you gave in #Navigator.Name() earlier.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_home">
<sticky_fragment
android:id="#+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home" />
</navigation>
STEP 3: Add global action
Global actions are a way to navigate to destination from anywhere in your app. You can use the visual editor or directly use xml to add global actions. Set global action on each fragment with the following settings
destination : self
popUpTo : self
singleTop : true/checked
This your how your navigation.xml should look like after adding global actions
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_home">
<sticky_fragment
android:id="#+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home" />
<sticky_fragment
android:id="#+id/navigation_images"
android:name="com.example.bottomnavigationlogic.ui.images.ImagesFragment"
android:label="#string/title_images"
tools:layout="#layout/fragment_images" />
<sticky_fragment
android:id="#+id/navigation_videos"
android:name="com.example.bottomnavigationlogic.ui.videos.VideosFragment"
android:label="#string/title_videos"
tools:layout="#layout/fragment_videos" />
<sticky_fragment
android:id="#+id/navigation_songs"
android:name="com.example.bottomnavigationlogic.ui.songs.SongsFragment"
android:label="#string/title_songs"
tools:layout="#layout/fragment_songs" />
<sticky_fragment
android:id="#+id/navigation_notifications"
android:name="com.example.bottomnavigationlogic.ui.notifications.NotificationsFragment"
android:label="#string/title_notifications"
tools:layout="#layout/fragment_notifications" />
<action
android:id="#+id/action_global_navigation_home"
app:destination="#id/navigation_home"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_home" />
<action
android:id="#+id/action_global_navigation_notifications"
app:destination="#id/navigation_notifications"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_notifications" />
<action
android:id="#+id/action_global_navigation_songs"
app:destination="#id/navigation_songs"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_songs" />
<action
android:id="#+id/action_global_navigation_videos"
app:destination="#id/navigation_videos"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_videos" />
</navigation>
STEP 4: Use global actions
When you wrote
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
then inside setupWithNavController() NavigationUI uses bottomNavigationView.setOnNavigationItemSelectedListener() to navigate to proper fragments depending on id of the menu item that was clicked. It's default behavior is as I mentioned before. We will add our own implementation to it and use global actions to achieve our desired back press behavior.
Here is how you do it simply in MainActivity
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
FINAL STEP 5: Add your custom navigator to NavController
Add your navigator as follow in your MainActivity. Make sure you are passing childFragmentManager of the NavHostFragment.
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
Also add the navigation graph to NavController here as well using setGraph() method as shown below.
This is how my MainActivity looks like after step 4 and step 5
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_images, R.id.navigation_videos,R.id.navigation_songs,R.id.navigation_notifications)
.build();
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
navController.setGraph(R.navigation.mobile_navigation);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView,navController);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
}
}
Hope this helps.
I am not entirely sure if this is the answer you are looking for but if you are concerned about managing state the modern way of managing state is by using something called a view model. View models are a component of the MVVM architecture. Their purpose is to hold and expose data to your fragment/activity to display. With the navigation architecture, if you store the data related to each fragment in this view model appropriately your state will be retained in the view model.
That being said I would personally suggest looking into MVVM architecture as well as view models specifically. Otherwise, a brute force for retaining state is by using the savedInstance state throughout fragments and manually saving and recovering important data.
Links:
- Android View Model Component
- Android architecture guide
I think you might need to prevent recreating the fragment upon clicking bottom nav view item twice. bottomNavigationView.setOnNavigationItemReselectedListener { /*Nothing to ignore reselection*/} after NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
I'm pretty new to developing in Android, but I understand that I cannot run Jsoup.connect(url).get() without some kind of thread as it is a synchronous call. I tried creating a class called Product that extends AsyncTask, and then having my original class called List call Product and then have the int displayed. However, regardless of the actual result, it always prints out 0.
I have tested my code before, so I know it works and that the issue must be something related to Android.
List Class:
public class List extends AppCompatActivity{
String itemURL;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_item_list_view);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Bundle itemData = getIntent().getExtras();
if(itemData==null){
return;
}
//Gets URL
itemURL = itemData.getString("itemURL");
int listSize=new Product(itemURL).getListSize();
System.out.println(listSize);
}
}
Product class:
public class Product extends AsyncTask<Void,Void,Void>{
String url;
String title;
int listSize;
public Product(String url){
this.url = url;
}
protected Void doInBackground (Void... voids) {
//Create JSoup connection
try{
Document doc = Jsoup.connect("url").get();
//Gets title
String link = doc.select("h2#s-result-count").first().text();
System.out.println(link);
listSize=Integer.parseInt(link.substring(0,1));
System.out.println(listSize);
try{
int listSize= Integer.parseInt(link.substring(0,2));
System.out.println(listSize);
}catch(Exception e){}
}catch (Exception e){}
}
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
getListSize();
}
public int getListSize(){
return listSize;
}
}
All advice would be appreciated, thanks for your time and help!
You have to override
onPostExecute()
to make sure you are getting the variable after it is updated from the thread. Call your
getListSize()
from inside the overridden function.
Consider that calling
execute()
and then immediately calling
getListSize()
will happen line by line while the actual thread you have spun off will continue working. You're asking for a value that hasn't been updated.
Edit::
For clarification:
public class List extends AppCompatActivity{
String itemURL;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_item_list_view);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Bundle itemData = getIntent().getExtras();
if(itemData==null){
return;
}
//Gets URL
itemURL = itemData.getString("itemURL");
int listSize=new Product(itemURL).getListSize(); // this doesn't do anything meaningful for you
System.out.println(listSize);
}
}
Note the comment I have added to
int listSize = new Product(itemURL).getListSize();
First,
doInBackground()
will never run until you call
execute()
You need to call
new Product(itemURL).execute();
Afterword, you have to provide some kind of callback to the Activity since you haven't nested the product class or initialized an anonymous instance of AsyncTask.
In your Product class you can set a variable
Context
public class Product extends AsyncTask<...> {
public Context context;
}
Then, in place of
int listSize = new Product(itemURL).getListSize();
put:
Product product = new Product();
product.context = this;
product.execute();
Then, in Product's
onPostExecute()
put:
if (context.getClass().equals(List.class)) {
((List) context).some_method_that_does_something_with_list_size()
}
Alternatively, you can add it to Product's constructor:
public Product(String url, Context context) {...}
Edit 2::
For further clarification
((List) context).some_method_that_does_something_with_list_size()
was meant to serve as a placemarker for any method available in your activity. It could easily be substituted with:
System.out.println(listSize);
Edit 3::
For further, further clarification:
In your Activity, ItemListView, define a method called
printListSize(int listSize) {
System.out.println("list size: " + listSize);
}
Then, in onPostExecute() call:
((ItemListView) context).printListSize(listSize);
I'm trying to implement a counter in my app that increases every time the button is clicked. I then want to take the whatever the final value of the counter is and have it displayed in a dialog.
The problem is that when I call the variable, it returns the value that it initially had, not the one modified by the onClick method (This is weird to explain but I commented in my code to show what I mean).
final TextView points = (TextView) findViewById(R.id.points);
points.setText("Points = 0");
class ButtonClick implements View.OnClickListener {
int value = 0;
public void onClick(View v) {
value++;
changeLocation(theButton);
points.setText("Points = " + value); // This value increases
}
public int getValue() {
return value;
} // This returns 0.
}
final ButtonClick buttonClicked = new ButtonClick();
theButton.setOnClickListener(buttonClicked);
final int finalValue = buttonClicked.getValue(); // This still equals zero.
Code is executed synchronously line by line:
// define and instantiate button listener
1) final ButtonClick buttonClicked = new ButtonClick();
// attach listener to button
2) theButton.setOnClickListener(buttonClicked);
<------ here you still didn't executed onClick :)
// trying get value
3) final int finalValue = buttonClicked.getValue(); // This still equals zero.
When you hit line 3, you are getting value 0 because value is zero.
Try clicking (perform click -> do event or whatsoever ) then get value.
To see if it works, you need a second asynchronous method example:
new Thread(new Runnable() {
#Override
public void run() {
while(buttonClicked.getValue() ==0) {
// you didn't click
Thread.sleep(10000);
}
};
}).start();
There are couple of issues that I can see,
1) onClick is not the overridden method, so try #Override annotation like this:
#Override
public void onClick(View v) { ... }
2) When a variable has to be used across, try to declare it in class level with getters and setters also at class level.
Hope these tips will help.
I am trying to keep my HashMap values when I navigate to another activity and return. This is the code I have for now.
The HashMap works and is able to grab and save the data from the EditText in the view.
However as soon as I leave from the activity and return, the HashMap is reinitialized to empty -> {}
I have looked at documentation and it seems this is the correct way of ensuring that a variable data is persisted. However it does not work.
please let me know what could be the issue:
public class ScriptActivity extends MainActivity {
HashMap timeAndMessages;
EditText message;
EditText time;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_script);
if (savedInstanceState != null) {
timeAndMessages = (HashMap) savedInstanceState.getSerializable("alerts");
} else {
timeAndMessages = new HashMap();
}
message = (EditText)findViewById(R.id.messageText);
time = (EditText)findViewById(R.id.timeText);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
restore(savedInstanceState);
}
private void restore(Bundle savedInstanceState) {
if (savedInstanceState != null) {
timeAndMessages = (HashMap) savedInstanceState.getSerializable("alerts");
}
}
public void createMessage (View view){
String stringmessage = message.getText().toString();
int inttime = Integer.parseInt(time.getText().toString());
timeAndMessages.put(inttime, stringmessage);
Toast.makeText(getApplicationContext(), "Will display : " + stringmessage + " At time : " + Integer.toString(inttime) , Toast.LENGTH_LONG).show();
}
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
outState.putSerializable("alerts", timeAndMessages);
}
}
However as soon as I leave from the activity and return, the HashMap is reinitialized to empty -> {}
If by "leave from the activity and return", you mean press the BACK button, then do something to start a fresh activity... then your behavior is expected.
The Bundle for the saved instance state is used in two main scenarios:
Configuration changes (e.g., user rotates the screen)
Process termination, and the user returns to your recent task (e.g., via the overview screen)
Pressing BACK to destroy the activity is neither of those. Hence, the state is not saved.
If this HashMap represents model data — the sort of data that you expect to be able to get back to, time and again, no matter how the user uses your app — save it to a database, SharedPreferences, other sort of file, or "the cloud".
You can read more about these scenarios in the Activity documentation.
I FIGURED OUT WHAT I WAS DOING. I HAD THE VARIABLE NAME IN QUOTES WITH THE REST OF THE URL STRING.
How do you save the value of a Radio button into a variable and use that variable later.
I can see the variable Day_Item in my LogCat and the value is in there but when try using Day_Item later it does not show the valuable.
Below is a section of my code that shows the buttons.
String Day_Item = null;
public class SearchDB extends Activity {
private static final String TAG = "MyApp";
String start_log = "STARTED";
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search_layout);
final RadioButton radio_monday = (RadioButton) findViewById(R.id.monday);
radio_monday.setOnClickListener(radio_listener);
cityspinner.setOnItemSelectedListener(new OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position,long arg3)
{
int id = parent.getId();
if (spinner2_count2 < spinner2_count1 ) {
spinner2_count2++; }
else
{
String city_spinner_log = "CITY SPINNER";
Log.d(TAG, city_spinner_log);
String item = cityspinner.getSelectedItem().toString();
String nameContentType = "name";
String cityURL = "GetRestaurant.php?day=Day_Item&city=" + item;
Log.d(TAG, cityURL);
String shop_data = DataCall.getJSON(cityURL,nameContentType);
Log.d(TAG, shop_data);
Bundle bundle = new Bundle();
bundle.putString("shopData", shop_data);
Intent myIntent = new Intent(SearchDB.this, ShowRestaurant.class);
myIntent.putExtras(bundle);
startActivityForResult(myIntent, 0);
}
}
}
//ONCLICKLISTENER that saves RADIO value into a variable.
public OnClickListener radio_listener = new OnClickListener() {
public void onClick(View v) {
// Perform action on clicks
RadioButton rb = (RadioButton) v;
Day_Item = (String) rb.getText();
Log.d(TAG,Day_Item);
Toast.makeText(SearchDB.this, Day_Item, Toast.LENGTH_SHORT).show();
}
};
}
You would need a bit more code to get a good solid answer. Such as how is Day_Item allocated? And is it's scope global? Are you calling it from another activity or the one it's allocated within? These are just guesses at this point:
1) Are you sure your onClickListener isn't firing multiple times? Thus setting Day_Item to an undesired text or nothing at all?
2) Rather a question/answer,
"but when try using Day_Item later it does not show the valuable"
I'm assuming this means that it is null? Well if it's being set properly, and then it is being null'd... it either is being explicitly null'd by you somewhere (such as (1)) or else the allocation and scope are the issue area I believe...