2

When I click on an item, sometimes it works and sometimes it crashes.

Below is the FATAL EXCEPTION after clicking on an item.

2020-03-31 21:59:18.087 15383-15383/com.aliton.myapp E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.aliton.myapp, PID: 15383
    java.lang.IllegalStateException: View androidx.constraintlayout.widget.ConstraintLayout{b3bb13b V.E...C.. .......D 0,336-720,518} does not have a NavController set
        at androidx.navigation.Navigation.findNavController(Navigation.java:84)
        at com.aliton.myapp.Adapter.StoreSelectAdapter$StoreSelectViewHolder$1.onChanged(StoreSelectAdapter.java:147)
        at com.aliton.myapp.Adapter.StoreSelectAdapter$StoreSelectViewHolder$1.onChanged(StoreSelectAdapter.java:142)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at androidx.lifecycle.LiveData$1.run(LiveData.java:91)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6351)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:896)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:786)

In this case, first I need to insert an item (StoreEntity) into my room database, listen to its id and then navigate to other fragment with objects defined in direction.

Below is my adapter:

public class StoreSelectAdapter extends RecyclerView.Adapter<StoreSelectAdapter.StoreSelectViewHolder> {

    private static final String TAG = "debinf SummaryAdapter";

    private FragmentActivity fragmentActivity;

    public StoreSelectAdapter(FragmentActivity fragmentActivity) {
        this.fragmentActivity = fragmentActivity;
    }

    private static final DiffUtil.ItemCallback<StoreModel> DIFF_CALLBACK = new DiffUtil.ItemCallback<StoreModel>() {
        @Override
        public boolean areItemsTheSame(@NonNull StoreModel oldItem, @NonNull StoreModel newItem) {
            return oldItem.getKey().equals(newItem.getKey());
        }

        @Override
        public boolean areContentsTheSame(@NonNull StoreModel oldItem, @NonNull StoreModel newItem) {
            return oldItem.getKey().equals(newItem.getKey()) && oldItem.getName().equals(newItem.getName()) && oldItem.getAddress().equals(newItem.getAddress());
        }
    };

    private AsyncListDiffer<StoreModel> differ = new AsyncListDiffer<StoreModel>(this, DIFF_CALLBACK);

    @NonNull
    @Override
    public StoreSelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_select_store, parent, false);
        return new StoreSelectViewHolder(view, fragmentActivity);
    }

    @Override
    public void onBindViewHolder(@NonNull StoreSelectViewHolder holder, int position) {
        StoreModel store = differ.getCurrentList().get(position);
        Log.i(TAG, "onBindViewHolder: "+store.getName());

        holder.storeName.setText(store.getName());
        holder.storeAddress.setText(store.getAddress());

        if (store.getImage() != null && !store.getImage().isEmpty()) {
            Picasso.get().load(store.getImage()).networkPolicy(NetworkPolicy.OFFLINE).into(holder.storeImage, new Callback() {
                @Override
                public void onSuccess() {

                }

                @Override
                public void onError(Exception e) {
                    Picasso.get().load(store.getImage()).into(holder.storeImage);
                }
            });
        } else {
            holder.storeImage.setImageResource(android.R.drawable.stat_sys_phone_call_on_hold);
        }

        /*holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: onBindViewHolder");
                Toast.makeText(context, "onClick: onBindViewHolder", Toast.LENGTH_SHORT).show();
                AddstoreFragmentDirections.ActionAddstoreFragmentToBarcodeFragment direction = AddstoreFragmentDirections.actionAddstoreFragmentToBarcodeFragment(store);
                Navigation.createNavigateOnClickListener()
            }
        });*/
    }

    @Override
    public int getItemCount() {
        return differ.getCurrentList().size();
    }

    public void submitList(List<StoreModel> stores) {
        differ.submitList(stores);
    }

    public List<StoreModel> getCurrentItemList() {
        return differ.getCurrentList();
    }

    public class StoreSelectViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        //private static final String TAG = "StoreSelectViewHolder";

        public ImageView storeImage;
        public TextView storeName, storeAddress;

        private FragmentActivity fragmentActivity;
        private LocalDatabaseViewModel localDatabaseViewModel;

        public StoreSelectViewHolder(@NonNull View itemView, FragmentActivity fragmentActivity) {
            super(itemView);

            storeImage = (ImageView) itemView.findViewById(R.id.item_store_image);
            storeName = (TextView) itemView.findViewById(R.id.item_store_name);
            storeAddress = (TextView) itemView.findViewById(R.id.item_store_address);

            this.fragmentActivity = fragmentActivity;

            itemView.setOnClickListener(this);

        }

        @Override
        public void onClick(View v) {
            StoreModel store = differ.getCurrentList().get(getAdapterPosition());
            Log.i(TAG, "onClick: storeName selected: "+store.getName());
            Log.i(TAG, "onClick: storeKey selected: "+store.getKey());
            StoreEntity storeEntity = new StoreEntity(store.getName(), store.getImage());
            storeEntity.setKey(store.getKey());
            localDatabaseViewModel = ViewModelProviders.of(fragmentActivity).get(LocalDatabaseViewModel.class);
            localDatabaseViewModel.getStoreIdFromInsertedItem().observe(fragmentActivity, new Observer<Long>() {
                @Override
                public void onChanged(Long itemId) {
                    Log.i(TAG, "onChanged: itemId in adapter is "+itemId);
                    SelectstoreFragmentDirections.ActionSelectstoreFragmentToBarcodeFragment direction = SelectstoreFragmentDirections.actionSelectstoreFragmentToBarcodeFragment(store, itemId); 
                    Navigation.findNavController(v).navigate(direction); // this is the StoreSelectAdapter.java:147
                }
            });
            localDatabaseViewModel.insertStore(storeEntity);


        }

    }
}

I appreciate any help to solve this issue.

EDIT

Below is my activity_main.xml where my fragment NavHost is declared.

<androidx.drawerlayout.widget.DrawerLayout
    android:id="@+id/main_drawer"
    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"
    tools:context=".MainActivity">


    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/main_fragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/main_bottomnav"
            app:defaultNavHost="true"
            app:navGraph="@navigation/mainnav_graph"/>

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_bottomnav"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/main_navmenu"
            android:background="@color/colorAccent"
            app:itemIconTint="@drawable/botton_item_color"
            app:itemTextColor="@drawable/botton_item_color">

        </com.google.android.material.bottomnavigation.BottomNavigationView>

    </androidx.constraintlayout.widget.ConstraintLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/main_sidebar"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/main_sidebarmenu"/>

</androidx.drawerlayout.widget.DrawerLayout>

Below is my mainnav_graph.xml

...
<fragment
    android:id="@+id/barcodeFragment"
    android:name="com.aliton.myapp.BarcodeFragment"
    android:label="fragment_barcode"
    tools:layout="@layout/fragment_barcode" >

    <argument
        android:name="store"
        app:argType="com.aliton.myapp.Model.StoreModel" />
    <action
        android:id="@+id/action_barcodeFragment_to_productdetailFragment"
        app:destination="@id/productdetailFragment" />
    <argument
        android:name="storeId"
        app:argType="long" />
</fragment>
<fragment
    android:id="@+id/selectstoreFragment"
    android:name="com.aliton.myapp.SelectstoreFragment"
    android:label="fragment_selectstore"
    tools:layout="@layout/fragment_selectstore" >
    <action
        android:id="@+id/action_selectstoreFragment_to_addstoreFragment"
        app:destination="@id/addstoreFragment" />
    <action
        android:id="@+id/action_selectstoreFragment_to_barcodeFragment"
        app:destination="@id/barcodeFragment" />
</fragment>
...

And below is my fragment_selectstore.xml as the content of SelectstoreFragment.java.

<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"
    tools:context=".SelectstoreFragment">

    <androidx.cardview.widget.CardView
        android:id="@+id/selectstore_cardview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        app:cardCornerRadius="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <TextView
            android:id="@+id/selectstore_question"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Are you at any of the store shown below?"
            android:textSize="28sp"
            android:gravity="center"
            android:textStyle="bold"/>


    </androidx.cardview.widget.CardView>

    <FrameLayout
        android:id="@+id/selectstore_framelayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="2dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/selectstore_cardview">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/selectstore_recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical">

        </androidx.recyclerview.widget.RecyclerView>

        <TextView
            android:id="@+id/selectstore_nodata"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center"
            android:text="No store found!"
            android:textSize="22sp" />

    </FrameLayout>

    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
        android:id="@+id/selectstore_fab"
        style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="8dp"
        android:contentDescription="Outra loja desc"
        android:text="Outra loja"
        app:icon="@drawable/common_full_open_on_phone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

UPDATE

1) I've also implemented an interface for communication between Adapter and Fragment to get the view in the onViewCreated(), but the app keeps crashing.

2) I've also tried to get the navController of my NavHostFragment defined in the fragment layout, using the following command

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Log.i(TAG, "onViewCreated: ");

    // https://stackoverflow.com/a/53902696/4300670
    // NOT WORKING
    adapter = new StoreSelectAdapter(getActivity(), new StoreSelectAdapter.OnStoreSelectListener() {
        @Override
        public void onStoreSelected(StoreModel storeModel, Long itemId) {
            SelectstoreFragmentDirections.ActionSelectstoreFragmentToBarcodeFragment direction = SelectstoreFragmentDirections.actionSelectstoreFragmentToBarcodeFragment(storeModel, itemId);
            Navigation.findNavController(getActivity(), R.id.main_fragment).navigate(direction);
        }
    });
    storeRecyclerview.setAdapter(adapter);
}

But now I get the NullPointerException

2020-04-02 17:11:22.163 6015-6015/com.aliton.myapp E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.aliton.myapp, PID: 6015
    java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View android.app.Activity.requireViewById(int)' on a null object reference
        at androidx.core.app.ActivityCompat.requireViewById(ActivityCompat.java:363)
        at androidx.navigation.Navigation.findNavController(Navigation.java:58)
        at com.aliton.myapp.SelectstoreFragment$4.onStoreSelected(SelectstoreFragment.java:221)
        at com.aliton.myapp.Adapter.StoreSelectAdapter$StoreSelectViewHolder$1.onChanged(StoreSelectAdapter.java:149)
        at com.aliton.myapp.Adapter.StoreSelectAdapter$StoreSelectViewHolder$1.onChanged(StoreSelectAdapter.java:145)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at androidx.lifecycle.LiveData$1.run(LiveData.java:91)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

3) I've copied the methods inside Navigation class in order to understand what is causing the error and here they are:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Log.i(TAG, "onViewCreated: ");

    adapter = new StoreSelectAdapter(getActivity(), new StoreSelectAdapter.OnStoreSelectListener() {
        @Override
        public void onStoreSelected(StoreModel storeModel, Long itemId) {
            SelectstoreFragmentDirections.ActionSelectstoreFragmentToBarcodeFragment direction = SelectstoreFragmentDirections.actionSelectstoreFragmentToBarcodeFragment(storeModel, itemId);
            NavController navController = myFindViewNavController(view);
            if (navController == null) {
                Log.i(TAG, "onStoreSelected: ISSUE NavController set");
            }                

        }
    });

    storeRecyclerview.setAdapter(adapter);
}

private NavController myFindViewNavController(View view) {
    while (view != null) {
        Log.i(TAG, "myFindViewNavController: view != null - "+view);
        NavController controller = myGetViewNavController(view);
        if (controller != null) {
            Log.i(TAG, "myFindViewNavController: controller != null");
            return controller;
        }
        ViewParent parent = view.getParent();
        view = parent instanceof View ? (View) parent : null;
        Log.i(TAG, "myFindViewNavController: view.getParent() - "+view);
    }
    return null;
}

private NavController myGetViewNavController(View view) {
    Object tag = view.getTag(R.id.nav_controller_view_tag);
    Log.i(TAG, "myGetViewNavController: tag is "+tag);
    NavController controller = null;
    if (tag instanceof WeakReference) {
        Log.i(TAG, "myGetViewNavController: tag instanceof WeakReference");
        controller = ((WeakReference<NavController>) tag).get();
    } else if (tag instanceof NavController) {
        Log.i(TAG, "myGetViewNavController: tag instanceof NavController");
        controller = (NavController) tag;
    }
    return controller;
}

Verifying the logcat, it seems that the ViewModel in my adapter is been triggered once again after navigating to the next fragment:

debinf SummaryAdapter: onClick: storeName selected: Posto Shell Convem
debinf SummaryAdapter: onClick: storeKey selected: M0Ysk5jRIdtiJ9zyUhXG
debinf SummaryAdapter: onChanged: itemId in adapter is 40, view id: -1
debinf SelStoreFrag: myFindViewNavController: view != null - androidx.constraintlayout.widget.ConstraintLayout{fb9835d V.E...... ........ 0,0-1080,1437}
debinf SelStoreFrag: myGetViewNavController: tag is null
debinf SelStoreFrag: myFindViewNavController: view.getParent() - android.widget.FrameLayout{4af5206 V.E...... ........ 0,0-1080,1437 #7f090137 app:id/main_fragment}
debinf SelStoreFrag: myFindViewNavController: view != null - android.widget.FrameLayout{4af5206 V.E...... ........ 0,0-1080,1437 #7f090137 app:id/main_fragment}
debinf SelStoreFrag: myGetViewNavController: tag is androidx.navigation.NavController@47f0c7
debinf SelStoreFrag: myGetViewNavController: tag instanceof NavController
debinf SelStoreFrag: myFindViewNavController: controller != null
debinf BarcodeFrag: onCreateView: 
debinf BarcodeFrag: onCreateView: fragArgs is com.aliton.myapp.Model.StoreModel@77fbfcb
debinf BarcodeFrag: onPermissionGranted: 
debinf BarcodeFrag: startCamera: 
debinf ExtFun: isFlashSupported: false
debinf ExtFun: startCameraForAllDevices: 
debinf BarcodeFrag: onResume:
debinf SummaryAdapter: onChanged: itemId in adapter is 41, view id: -1
debinf SelStoreFrag: myFindViewNavController: view != null - androidx.constraintlayout.widget.ConstraintLayout{fb9835d V.E...... .......D 0,0-1080,1437}
debinf SelStoreFrag: myGetViewNavController: tag is null
debinf SelStoreFrag: myFindViewNavController: view.getParent() - null
debinf SelStoreFrag: onStoreSelected: ISSUE NavController set

It seems that the solution will come by properly solving the listening of the inserted item id via ViewModel in the adapter.

Aliton Oliveira
  • 839
  • 10
  • 17

3 Answers3

2

I had some similar issue on my bind method for the adapter and what worked for me was to change this line

navController = Navigation.findNavController(itemView)

to

navController = Navigation.findNavController(activity, R.id.nav_host_fragment)
Francislainy Campos
  • 1,743
  • 11
  • 36
0

Before you move the interface implementation to the fragment.
You are accessing the NavController from the wrong place or with the wrong access.
You have to move this action to the fragment hosting the recycler view. Then you can navigate to your desired destination.

findNavController().navigate(R.id.your_destination)

After you move the interface implementation to the fragment.
You are accessing NavController by the wrong accessor you just have to call the findNavController() method only.

for the summary, you can initialize your adapter

adapter = new StoreSelectAdapter(getActivity(), new StoreSelectAdapter.OnStoreSelectListener() {
        @Override
        public void onStoreSelected(StoreModel storeModel, Long itemId) {
            SelectstoreFragmentDirections.ActionSelectstoreFragmentToBarcodeFragment direction = SelectstoreFragmentDirections.actionSelectstoreFragmentToBarcodeFragment(storeModel, itemId);
            findNavController().navigate(direction);
        }
    });

The findNavController() function/method is an extension function for the fragment you will find it under the package androidx.navigation.fragment

Hope you get what I am trying to illustrate.

Shalan93
  • 669
  • 4
  • 16
  • Maybe I haven't understood your suggestion, but the method `findNavController` is only accessible from the class `Navigation` and the method requires arguments: (`View`) or (`Activity`, `int`). In other words, `Navigation.findNavController(View).navigate(R.id.your_destination)`. Could you correct the code which I posted as **UPDATE** with your suggestion? – Aliton Oliveira Apr 04 '20 at 21:02
  • I imported `androidx.navigation.fragment.*;` and for the method `findNavController()` I get an error: **cannot resolve method**. I think I can only access it through the class `Navigation`, I mean: `Navigation.findNavController(view).navigate(R.id.your_destination)` – Aliton Oliveira Apr 06 '20 at 17:50
  • import androidx.navigation.fragment.findNavController – Shalan93 Apr 07 '20 at 10:31
0

The IllegalStateException or NullPointerException is no longer occurring, since I've removed the ViewModel from the adapter and placed it in the fragment.

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Log.i(TAG, "onViewCreated: ");

    localDatabaseViewModel = ViewModelProviders.of(getActivity()).get(LocalDatabaseViewModel.class);
    adapter = new StoreSelectAdapter(new StoreSelectAdapter.OnStoreSelectListener() {
        @Override
        public void onStoreSelected(StoreModel storeModel) {
            StoreEntity storeEntity = new StoreEntity(storeModel.getName(), storeModel.getImage());
            storeEntity.setKey(storeModel.getKey());
            localDatabaseViewModel.getStoreIdFromInsertedItem().observe(getViewLifecycleOwner(), new Observer<Long>() {
                @Override
                public void onChanged(Long itemId) {
                    SelectstoreFragmentDirections.ActionSelectstoreFragmentToBarcodeFragment direction = SelectstoreFragmentDirections.actionSelectstoreFragmentToBarcodeFragment(storeModel, itemId);
                    Navigation.findNavController(view).navigate(direction);
                }
            });
            localDatabaseViewModel.insertStore(storeEntity);

        }
    });

    storeRecyclerview.setAdapter(adapter);
}

And stop listening to the id of the inserted item in the room database.

@Override
public void onStop() {
    Log.i(TAG, "onStop: ");
    if (localDatabaseViewModel != null) {
        localDatabaseViewModel.getStoreIdFromInsertedItem().removeObservers(getViewLifecycleOwner());
    }
    super.onStop();
}
Aliton Oliveira
  • 839
  • 10
  • 17