Search code examples
javaandroidbottomnavigationviewweather

Data resets when bottom navigation views are clicked


I have a bottom navigation view on my weather app containing 3 panels(Today, hourly & Daily). My activity hosts 3 fragments for the 3 panels. Currently, if I search for any city on the today fragment, it gives the data for such cities. Now the problem is that if I click on any bottom nav view(down), it resets the data displayed on the today fragment.

Here is an illustration of the issue:

  • Data is displayed after a city is searched(the part with a red tick): enter image description here

  • Data goes on reset after clicking these bottom nav views(the part with a red tick): enter image description here

I want the data to remain intact irrespective of clicking those nav views.

I tried using https://stackoverflow.com/a/60201555/16020235 suggestion. But it failed with this exception:

java.lang.IllegalArgumentException: No view found for id 0x7f0a0116 (com.viz.lightweatherforecast:id/my_nav) for fragment ThirdFragment{90bc0de} (8e129d17-010d-41dc-9311-82e273b4e522 id=0x7f0a0116 tag=3)
        at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:513)
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
        at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3134)
        at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:3068)
        at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:251)
        at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:501)
        at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
        at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1391)
        at android.app.Activity.performStart(Activity.java:7157)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3037)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1861)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6819)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)
I/weatherforecas: Compiler allocated 4MB to compile void android.widget.TextView.<init>(android.content.Context, android.util.AttributeSet, int, int)

I find it hard to implement his code and the remaining suggestions on that question are written in kotlin.

Please how can I resolve this?

Here are my codes:

my_nav.xml(navigation layout):

<?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/my_nav"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.viz.lightweatherforecast.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" />
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.viz.lightweatherforecast.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
    <fragment
        android:id="@+id/thirdFragment"
        android:name="com.viz.lightweatherforecast.ThirdFragment"
        android:label="fragment_third"
        tools:layout="@layout/fragment_third" />
</navigation>

activity_home.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/layout"
    android:background="@drawable/dubai"
    tools:context=".Activity.HomeActivity">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FFFFFF"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_menu" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="409dp"
        android:layout_height="599dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:navGraph="@navigation/my_nav"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

HomeActivity:

public class HomeActivity extends AppCompatActivity {
    // Last update time, click sound, search button, search panel.
    TextView time_field;
    MediaPlayer player;
    ImageView Search;
    EditText textfield;
    // For scheduling background image change(using constraint layout, start counting from dubai, down to statue of liberty.
    ConstraintLayout constraintLayout;
    public static int count=0;
    int[] drawable =new int[]{R.drawable.dubai,R.drawable.central_bank_of_nigeria,R.drawable.eiffel_tower,R.drawable.hong_kong,R.drawable.statue_of_liberty};
    Timer _t;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        // use home activity layout.

        time_field = findViewById(R.id.textView9);
        Search = findViewById(R.id.imageView4);
        textfield = findViewById(R.id.textfield);
        //  find the id's of specific variables.

        BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
        // host 3 fragments along with bottom navigation.
        final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
        assert navHostFragment != null;
        final NavController navController = navHostFragment.getNavController();
        NavigationUI.setupWithNavController(bottomNavigationView, navController);



        // For scheduling background image change
        constraintLayout = findViewById(R.id.layout);
        constraintLayout.setBackgroundResource(R.drawable.dubai);
        _t = new Timer();
        _t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                // run on ui thread
                runOnUiThread(() -> {
                    if (count < drawable.length) {

                        constraintLayout.setBackgroundResource(drawable[count]);
                        count = (count + 1) % drawable.length;
                    }
                });
            }
        }, 5000, 5000);

        Search.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // make click sound when search button is clicked.
                player = MediaPlayer.create(HomeActivity.this, R.raw.click);
                player.start();

                getWeatherData(textfield.getText().toString().trim());
                // make use of some fragment's data
                FirstFragment firstFragment = (FirstFragment) navHostFragment.getChildFragmentManager().getFragments().get(0);
                firstFragment.getWeatherData(textfield.getText().toString().trim());

            }

            
                });
            }

        });
    }
}

EDIT

Fragment class:

public class FirstFragment extends Fragment {
    // User current time, current temperature, current condition, sunrise, sunset, temperature, pressure, humidity, wind_speed, visibility, clouds
    TextView current_temp, current_output, rise_time, set_time, temp_out, Press_out, Humid_out, Ws_out, Visi_out, Cloud_out;
    // TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
    private static final String ARG_PARAM1 = "param1";
    private static final String ARG_PARAM2 = "param2";

    // TODO: Rename and change types of parameters
    private String mParam1;
    private String mParam2;

    public FirstFragment() {
        // Required empty public constructor
    }

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @param param1 Parameter 1.
     * @param param2 Parameter 2.
     * @return A new instance of fragment SecondFragment.
     */
// TODO: Rename and change types and number of parameters
    public static FirstFragment newInstance(String param1, String param2) {
        FirstFragment fragment = new FirstFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);

        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View rootView = inflater.inflate(R.layout.fragment_first, container, false);
        // For displaying weather data
        current_temp = rootView.findViewById(R.id.textView10);
        current_output = rootView.findViewById(R.id.textView11);
        rise_time = rootView.findViewById(R.id.textView25);
        set_time = rootView.findViewById(R.id.textView26);
        temp_out = rootView.findViewById(R.id.textView28);
        Press_out = rootView.findViewById(R.id.textView29);
        Humid_out = rootView.findViewById(R.id.textView30);
        Ws_out = rootView.findViewById(R.id.textView33);
        Visi_out = rootView.findViewById(R.id.textView34);
        Cloud_out = rootView.findViewById(R.id.textView35);

        return rootView;
    }

    public void getWeatherData(String name) {

        ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);

        Call<Example> call = apiInterface.getWeatherData(name);

        call.enqueue(new Callback<Example>() {
            @Override
            public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {

                try {
                    assert response.body() != null;
                    current_temp.setVisibility(View.VISIBLE);
                    current_temp.setText(response.body().getMain().getTemp() + " ℃");
                    current_output.setVisibility(View.VISIBLE);
                    current_output.setText(response.body().getWeather().get(0).getDescription());
                    rise_time.setVisibility(View.VISIBLE);
                    rise_time.setText(response.body().getSys().getSunrise() + " ");
                    set_time.setVisibility(View.VISIBLE);
                    set_time.setText(response.body().getSys().getSunset() + " ");
                    temp_out.setVisibility(View.VISIBLE);
                    temp_out.setText(response.body().getMain().getTemp() + " ℃");
                    Press_out.setVisibility(View.VISIBLE);
                    Press_out.setText(response.body().getMain().getPressure() + " hpa");
                    Humid_out.setVisibility(View.VISIBLE);
                    Humid_out.setText(response.body().getMain().getHumidity() + " %");
                    Ws_out.setVisibility(View.VISIBLE);
                    Ws_out.setText(response.body().getWind().getSpeed() + " Km/h");
                    Visi_out.setVisibility(View.VISIBLE);
                    Visi_out.setText(response.body().getVisibility() + " m");
                    Cloud_out.setVisibility(View.VISIBLE);
                    Cloud_out.setText(response.body().getClouds().getAll() + " %");
                } catch (Exception e) {
                    Log.e("TAG", "No City found");
                    current_temp.setVisibility(View.GONE);
                    current_output.setVisibility(View.GONE);
                    rise_time.setVisibility(View.GONE);
                    set_time.setVisibility(View.GONE);
                    temp_out.setVisibility(View.GONE);
                    Press_out.setVisibility(View.GONE);
                    Humid_out.setVisibility(View.GONE);
                    Ws_out.setVisibility(View.GONE);
                    Visi_out.setVisibility(View.GONE);
                    Cloud_out.setVisibility(View.GONE);
                    Toast.makeText(getActivity(), "No City found", Toast.LENGTH_SHORT).show();
                }

            }

            @Override
            public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
                t.printStackTrace();
            }
        });
    }
}

I didn't paste everything because I'm following https://stackoverflow.com/help/minimal-reproducible-example


Solution

  • Your problem is that your FirstFragment is not saving its state properly. As per the Saving state with fragments guide:

    To ensure the user's state is saved, the Android framework automatically saves and restores the fragments and the back stack. Therefore, you need to ensure that any data in your fragment is saved and restored as well.

    But you aren't saving the last name that you pass to getWeatherData, nor are you saving your Example object that you get from your API call in order to repopulate your views when they are recreated.

    So need to actually use the APIs described in that guide to save your state. Namely, you should file the Guide to app architecture, which explains how you can separate your from your data loading by using ViewModels to store data across configuration changes (like rotating your device) and LiveData to automatically populate your UI whenever your data is loaded.

    The first thing we want to do is move the data loading to a ViewModel. This object survives configuration changes which means any data stored in this class is automatically saved when you rotate your device. This is how we can save your Example class and avoid calling the server over and over.

    By using the APIs in the Saved State module for ViewModel (specifically, the SavedStateHandle class), any data you save in there will survive your process being killed and later recreated (say, if your device is low on memory, etc.). This is how we can save the last name so that we will automatically requery for your data.

    Here, our ViewModel handles all of the loading from the server and uses a LiveData to let our UI automatically update as the data is loaded.

    public class WeatherDataViewModel extends ViewModel {
        // This will save the city name
        private SavedStateHandle state;
    
        // This is where we'll store our result from the server
        private MutableLiveData<Example> mutableWeatherData = new MutableLiveData<>();
    
        public WeatherDataViewModel(SavedStateHandle savedStateHandle) {
            state = savedStateHandle;
            String savedCityName = state.get("name");
            if (savedCityName != null) {
                // We already had a previously saved name, so we'll
                // start loading right away
                loadData();
            }
        }
    
        // This is what our Fragment will use to get the latest weather data
        public LiveData<Example> getWeatherDataLiveData() {
            return mutableWeatherData;
        }
    
        // When you get a new city name, we'll save that in our
        // state, then load the new data from the server
        public void setCityName(String name) {
            state.set("name", name);
            loadData();
        }
    
        private void loadData() {
            // Get the last name that was set
            String name = state.get("name");
    
            // Now kick off a load from the server
            ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
    
            Call<Example> call = apiInterface.getWeatherData(name);
    
            call.enqueue(new Callback<Example>() {
                @Override
                public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
                    // Save the response we've gotten
                    // This will automatically update our UI
                    mutableWeatherData.setValue(response.body());
                }
    
                @Override
                public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
                    t.printStackTrace();
                }
            });
        }
    }
                     
    

    Now you can rewrite your FirstFragment to use the WeatherDataViewModel as the source of truth for your UI:

    public class FirstFragment extends Fragment {
    
        private WeatherDataViewModel viewModel;
    
        public FirstFragment() {
            // Required empty public constructor
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            // Inflate the layout for this fragment
            View rootView = inflater.inflate(R.layout.fragment_first, container, false);
            // For displaying weather data
            final TextView current_temp = rootView.findViewById(R.id.textView10);
            final TextView current_output = rootView.findViewById(R.id.textView11);
            final TextView rise_time = rootView.findViewById(R.id.textView25);
            final TextView set_time = rootView.findViewById(R.id.textView26);
            final TextView temp_out = rootView.findViewById(R.id.textView28);
            final TextView Press_out = rootView.findViewById(R.id.textView29);
            final TextView Humid_out = rootView.findViewById(R.id.textView30);
            final TextView Ws_out = rootView.findViewById(R.id.textView33);
            final TextView Visi_out = rootView.findViewById(R.id.textView34);
            final TextView Cloud_out = rootView.findViewById(R.id.textView35);
    
            // Get our ViewModel instance
            viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);
    
            // And whenever the data changes, refresh the UI
            viewModel.getWeatherDataLiveData().observe(getViewLifecycleOwner(), data -> {
                if (data != null) {
                    current_temp.setVisibility(View.VISIBLE);
                    current_temp.setText(data.getMain().getTemp() + " ℃");
                    current_output.setVisibility(View.VISIBLE);
                    current_output.setText(data.getWeather().get(0).getDescription());
                    rise_time.setVisibility(View.VISIBLE);
                    rise_time.setText(data.getSys().getSunrise() + " ");
                    set_time.setVisibility(View.VISIBLE);
                    set_time.setText(data.getSys().getSunset() + " ");
                    temp_out.setVisibility(View.VISIBLE);
                    temp_out.setText(data.getMain().getTemp() + " ℃");
                    Press_out.setVisibility(View.VISIBLE);
                    Press_out.setText(data.getMain().getPressure() + " hpa");
                    Humid_out.setVisibility(View.VISIBLE);
                    Humid_out.setText(data.getMain().getHumidity() + " %");
                    Ws_out.setVisibility(View.VISIBLE);
                    Ws_out.setText(data.getWind().getSpeed() + " Km/h");
                    Visi_out.setVisibility(View.VISIBLE);
                    Visi_out.setText(data.getVisibility() + " m");
                    Cloud_out.setVisibility(View.VISIBLE);
                    Cloud_out.setText(data.getClouds().getAll() + " %");
                } else {
                    Log.e("TAG", "No City found");
                    current_temp.setVisibility(View.GONE);
                    current_output.setVisibility(View.GONE);
                    rise_time.setVisibility(View.GONE);
                    set_time.setVisibility(View.GONE);
                    temp_out.setVisibility(View.GONE);
                    Press_out.setVisibility(View.GONE);
                    Humid_out.setVisibility(View.GONE);
                    Ws_out.setVisibility(View.GONE);
                    Visi_out.setVisibility(View.GONE);
                    Cloud_out.setVisibility(View.GONE);
                    Toast.makeText(requireActivity(), "No City found", Toast.LENGTH_SHORT).show();
                }
            });
    
            return rootView;
        }
    
        public void getWeatherData(String name) {
            // The ViewModel controls loading the data, so we just
            // tell it what the new name is - this kicks off loading
            // the data, which will automatically call through to
            // our observe() call when the data load completes
            viewModel.setCityName(name);
        }
    }
    

    With these changes, you'll find that your Fragment now correctly handles:

    • Being put on the Fragment back stack
    • Configuration changes (i.e., rotating your device)
    • Process death and recreation (i.e., testing with 'Don't keep activities' on)

    You'll note how we use new ViewModelProvider(this).get(WeatherDataViewModel.class) - that creates a WeatherDataViewModel that is tied to this - your Fragment itself. This is best if the data your ViewModel loads is only used in that one Fragment.

    If you also wanted to use this same data in your Activity, your activity could use new ViewModelProvider(this).get(WeatherDataViewModel.class) to create a WeatherDataViewModel that is scoped to the entire Activity. Any Fragment could then use new ViewModelProvider(requireActivity()).get(WeatherDataViewModel.class) to get that Activity owned ViewModel. This would potentially mean that you wouldn't need a getWeatherData() method on your Fragment at all - instead, your Activity would directly call viewModel.setCityName(name) itself and all Fragments would just instantly update (as they read from the same ViewModel).