I knew that I would write a post about this phenomenon as soon as I met it.
Here is the story: during my work, I was assigned to fix a bug. The bug was the following: when navigating to a certain screen, the application crashed with ClassCastException saying that I cannot cast Linearlayout to FrameLayout.
I checked the layout file that was inflated, and the root element was indeed FrameLayout, making the error seem impossible. But then… what was wrong?
By the end of this post, you will see that the IDE can inject subtle, hard-to-find bugs into your source code. The irony is that it wants to help you when it does that.
The basics
One thing to note at the beginning is that only certain projects can fall for this mistake. To be more exact: you can only run into this bug if you have at least one library module that depends on another library module in your project.
The solution for the bug mentioned in the beginning – and the method to avoid this failure – is very simple, and I could summarize it in one sentence here. However, I would like to show you the process that helped me finding out what is going on here. If you are not interested, just head to the conclusion at the end of this post.
In the next section, I will show an example project and a series of actions that can lead to the odd run-time Exceptions.
The example
Let’s say that you create an application that can display values that are stored in resources.
NOTE: I know that this is not a too realistic example, but it is perfect to show the point. In the real world, anything related to resources is vulnerable to this – inflating a layout, processing XML attributes of a custom View or trying to play a sound that is given as a raw resource.
In our example project, there will be two modules in the beginning: app and data. App is the main application module, as its name is suggests. The data module contains two resources – an integer and a string – and a layout that shows the value of these two resources in distinct TextViews. On top of that, there is a Fragment called DataFragment, that can be reused in multiple Activities.
The declared resources that contain the data are the following:
<resources> <integer name="integer_data">100</integer> </resources>
<resources> <string name="string_data">Example data</string> </resources>
The layout that shows the values:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/string_data" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" /> <TextView android:id="@+id/integer_data" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" /> </LinearLayout>
Last, but not least, here is the relevant part from DataFragment that inflates, finds and fills the Views with the data:
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.data_screen, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); TextView stringDataText = (TextView) view.findViewById(R.id.string_data); TextView integerDataText = (TextView) view.findViewById(R.id.integer_data); stringDataText.setText(getString(R.string.string_data)); integerDataText.setText(String.valueOf(getActivity().getResources().getInteger(R.integer.integer_data))); }
The code of the initial project can be downloaded from here. After building and running it, you should see something like this:
Refactor
Time passes, and let’s say that you consider improving the architecture of the application. To do this, you decide that you need another module called UI. You want to move the Fragment into the new module. To achieve this, the following steps should be done:
- creating the new Android library module (UI), it depends on the data module
- changing the app module’s dependency from data to the new UI module
- moving DataFragment from the data module to the UI module by using the drag & drop function of the IDE
Seems logical, right? Do this, build the app, and experience the crash:
The refactored, now crashing code can be seen here.
What’s happened?
Now it’s time to find the cause of the crash. The Exception that we get is the following one:
FATAL EXCEPTION: main Process: com.gyula.juhasz.example.resourcereferencing, PID: 3873 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.gyula.juhasz.example.resourcereferencing/com.gyula.juhasz.example.resourcereferencing.app.MainActivity}: android.content.res.Resources$NotFoundException: Resource ID #0x7f030019 ... Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7f030019 ... at android.content.res.Resources.getLayout(Resources.java:1165) at android.view.LayoutInflater.inflate(LayoutInflater.java:421) at com.gyula.juhasz.example.resourcereferencing.ui.DataFragment.onCreateView(DataFragment.java:23)
Alright, it is time to check the line in the stack trace:
return inflater.inflate(R.layout.data_screen, container, false);
Nothing special here. The layout stayed in the data module, but it’s definitely there, and the UI module should be able to access it, just like any other resource declared in a dependency. Everything seems correct, but as they say
the devil is in the details.
So let’s see what is in the compiled code. When the module is compiled, the build tools create an AAR file. The AAR is located under ui/build/outputs/aar
and its name is ui-debug.aar
. The file itself is a simple ZIP, so its contents can be extracted without any problems. The file we are interested in is called classes.jar
. As its name suggests, it contains the compiled Java classes. The JAR file can be opened with your choice of Java decompiler. I will use JD-GUI now, which can be downloaded from here. After opening the JAR file, we can find 2 classes inside it, one of which is our DataFragment. After taking a look at it, there is something strange inside that class:
If we check the inflate statement, we see the following:
return inflater.inflate(2130903065, container, false);
That number equals to the hexadecimal number 0x7F030019, so it really is the resource ID that is on the stack trace. The odd thing about this file is that according to this post and this answer, the resource IDs in library projects should NOT be final
, only static
. That would mean they are not compile-time constants, so they could not be substituted with their values by the compiler. So how is it possible that constant integer values are there instead of references?
The solution
If we examine the source code of DataFragment carefully, we see that the IDE kept the imported R class when we moved the fragment from the data module to the UI module:
import com.gyula.juhasz.example.resourcereferencing.data.R;
Looking at the generated sources in the UI module at ui/build/generated/source/r/debug
, we can see that there are multiple generated R.java
files. One for the appcompat-v7
external dependency, one for the data
module, and one for the UI
module. Each R file contains IDs for the resources declared either in the actual module or its dependencies. However, we can spot one key difference between the R.java in the UI module’s package, and the other R files: all of the R files contain static final int
resource IDs except the one that is in the module’s own package. Based on this observation, let’s fix the import to use the UI module’s R class! In this case, removing the import is enough, since DataFragment is in the module’s package:
package com.gyula.juhasz.example.resourcereferencing.ui;
After this little change, the application can run again without any crash. 🙂
Checking DataFragment extracted from the newly generated AAR confirms that the solution is right. (You may have to completely clean your project for a correct AAR to be generated.) The result:
Conclusion
This may seem like a little issue that is not even worth considering, but with 30-40 modules, it is not uncommon that some classes are moved from one module to another. Moving classes that reference resources can introduce bugs of this kind, and I have seen this happening in an actively developed production app. As you have seen, these are easy to solve but can be hard to find due to the misleading error messages in the Exceptions, especially if you have not experienced this before.
To avoid this situation, follow this simple rule: always use the R class of the module that contains the class!. And of course, be careful when moving classes during some refactor.
In my opinion, the problem could be easily resolved if the dependencies did not have their own R files or they contained non-final IDs too. However, it is possible that there is some reasoning for this that I do not see yet, so I created an issue with this question, and I hope to get an answer in the future. (Starring the issue may increase the probability for that.)
Thanks for reading this far 🙂
Kate
March 22, 2017 — 6:42 am
My favourite part was that: “the devil is in the details”. 😀