Improper customization of View causes ViewBinding to use Crash:Missing required view with ID

Posted by cosminb on Sun, 26 Dec 2021 23:55:15 +0100

Today I encountered a Crash:java while using ViewBinding. Lang.NullPointerException: Missing required View with ID, found to be related to custom View...

1. Background

When using ViewBinding recently, you encountered such an error:

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: me.hjhl.app, PID: 10740
E AndroidRuntime: java.lang.NullPointerException: Missing required view with ID: me.hjhl.app:id/my_gl_surface_view
E AndroidRuntime: 	at me.hjhl.app.databinding.FragmentGlesDemoBinding.bind(FragmentGlesDemoBinding.java:67)
E AndroidRuntime: 	at me.hjhl.app.databinding.FragmentGlesDemoBinding.inflate(FragmentGlesDemoBinding.java:49)
E AndroidRuntime: 	at me.hjhl.app.demo.GLESDemoFragment.onCreateView(GLESDemoFragment.kt:31)
E AndroidRuntime: 	at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2995)
E AndroidRuntime: 	at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:523)
E AndroidRuntime: 	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1840)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1764)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1701)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2849)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2784)
E AndroidRuntime: 	at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:262)
E AndroidRuntime: 	at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:478)
E AndroidRuntime: 	at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:246)
E AndroidRuntime: 	at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1433)
E AndroidRuntime: 	at android.app.Activity.performStart(Activity.java:7923)
E AndroidRuntime: 	at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3337)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2049)
E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:107)
E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:228)
E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:7589)
E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:953)

The overall code logic is roughly as follows: Activity is created by jumping to a Fragment, which corresponds to a simple XML -- FrameLayout has a custom View. Error stack hint could not find resource id my_gl_surface_view. This made me wonder if custom View was the cause.

2. Analysis process

2.1 Find the UI class generated by ViewBinding

The general principle of ViewBinding is that at compile time, the layout resource XML with ViewBinding turned on is generated into the corresponding Java class. For example, in this case, the corresponding class file location is: app/build/generated/data_binding_base_class_source_out/debug/out/me/ljh/app/databinding/FragmentGlesDemoBinding.java.

2.2 Analyzing key codes

The following code snippet is extracted from FragmentGlesDemoBinding:

// file: app/build/generated/data_binding_base_class_source_out/debug/out/me/ljh/app/databinding/FragmentGlesDemoBinding.java
public final class FragmentGlesDemoBinding implements ViewBinding {
  @NonNull
  public static FragmentGlesDemoBinding bind(@NonNull View rootView) {
  	// The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
    	id = R.id.my_gl_surface_view;
      MyGLSurfaceView myGlSurfaceView = ViewBindings.findChildViewById(rootView, id);
      if (myGlSurfaceView == null) {
        // Unable to find view, break is ready to throw an exception
        break missingId;
      }
      // Recursive processing R.id. My_ Gl_ Surface_ Child view of view
      return new FragmentGlesDemoBinding((FrameLayout) rootView, myGlSurfaceView);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

Given that this issue is closely related to the layout file, place the source code for the layout file:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="?attr/fullscreenBackgroundColor"
  android:theme="@style/ThemeOverlay.LearnAndroidOpenGL.FullscreenContainer"
  tools:context=".demo.GLESDemoFragment">

  <!-- Custom layout is used here -->
  <me.ljh.app.widget.MyGLSurfaceView
    android:id="@+id/my_gl_surface_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</FrameLayout>

Here, MyGLSurfaceView is implemented as follows:

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context) {
 	// MyGLRender is inherited from GLSurfaceView. A class of the Render interface that has nothing to do with the error here
  private val renderer: MyGLRender = MyGLRender()

  init {
      setEGLContextClientVersion(3)
      setRenderer(renderer)
      renderMode = RENDERMODE_WHEN_DIRTY
  }

  constructor(context: Context) : this(context, null)
}

2.2. 1Doubt 1: Is it related to ViewBinding not supporting custom View controls in layouts?

Although it seems impossible, it is still validated:

class GLESDemoFragment : Fragment() {
  
  private var _binding: FragmentGlesDemoBinding? = null
  
  private val mBinding get() = _binding!!
  
	override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    // Step 1: Comment out the ViewBinding code
    //_binding = FragmentGlesDemoBinding.inflate(inflater, container, false)
    // return mBinding.root
    // Step 2: Load View using inflate XML
    val rootView = inflater.inflate(R.layout.fragment_gles_demo, container, false)
    return rootView
  }
}

Unexpected discovery, such unexpectedly will not run, can run!

No, it's really about ViewBinding? With a skeptical and realistic attitude, try to get an instance of view through findViewById on the above basis.

val rootView = inflater.inflate(R.layout.fragment_gles_demo, container, false)
val myGlView = rootView.findViewById(R.id.my_gl_surface_view)
Log.d(TAG, "my gl view $myGlView")
return rootView

If not, it was null!!

This means that the ID is my_not found in the root view Gl_ Surface_ View's child view, but from this uncomplicated XML layout, it's clear that the ID is really right. What's wrong? And why does the program work well by laying out the XML manually with inflate?

2.3 Discovering problems

Considering the use of a custom View, in this case, it is suspected that the custom View may have been written incorrectly.

Returning to the source of MyGLSurfaceView, I suddenly noticed its constructor.

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context) {
  // ......
  constructor(context: Context) : this(context, null)
}

When you first customized the View, the main constructor used only one Context, and then found that the runner would crash after the layout was introduced in XML.

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: me.ljh.app, PID: 16008
E AndroidRuntime: android.view.InflateException: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Error inflating class me.ljh.app.widget.MyGLSurfaceView
E AndroidRuntime: Caused by: android.view.InflateException: Binary XML file line #13 in me.ljh.app:layout/fragment_gles_demo: Error inflating class me.ljh.app.widget.MyGLSurfaceView
E AndroidRuntime: Caused by: java.lang.NoSuchMethodException: me.ljh.app.widget.MyGLSurfaceView.<init> [class android.content.Context, interface android.util.AttributeSet]
E AndroidRuntime: 	at java.lang.Class.getConstructor0(Class.java:2332)
E AndroidRuntime: 	at java.lang.Class.getConstructor(Class.java:1728)
E AndroidRuntime: 	at android.view.LayoutInflater.createView(LayoutInflater.java:828)
E AndroidRuntime: 	at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1010)
E AndroidRuntime: 	at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:965)
E AndroidRuntime: 	at android.view.LayoutInflater.rInflate(LayoutInflater.java:1127)
E AndroidRuntime: 	at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1088)
E AndroidRuntime: 	at android.view.LayoutInflater.inflate(LayoutInflater.java:686)
E AndroidRuntime: 	at android.view.LayoutInflater.inflate(LayoutInflater.java:538)
E AndroidRuntime: 	at me.ljh.app.demo.GLESDemoFragment.onCreateView(GLESDemoFragment.kt:34)
E AndroidRuntime: 	at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2995)
E AndroidRuntime: 	at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:523)
E AndroidRuntime: 	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:261)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1840)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1764)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1701)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2849)
E AndroidRuntime: 	at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2784)
E AndroidRuntime: 	at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:262)
E AndroidRuntime: 	at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:478)
E AndroidRuntime: 	at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:246)
E AndroidRuntime: 	at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1433)
E AndroidRuntime: 	at android.app.Activity.performStart(Activity.java:7923)
E AndroidRuntime: 	at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3337)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
E AndroidRuntime: 	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2049)
E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:107)
E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:228)
E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:7589)
E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:953)

Therefore, by adding the AttributeSet parameter to the main constructor of MyGlSurfaceView, there is no Crash. This parameter was not passed to the parent GLSurfaceView.

There's a high probability of guessing here, so add it and try again.

class MyGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {
  // ......
  constructor(context: Context) : this(context, null)
}

The view instance is available through findViewById, and using ViewBinding is normal.

It looks like it's really a problem here, but here's a new one,

  1. What is this parameter for?
  2. Why does it have to be needed to create a View in XML?
  3. Why can't findViewById find View without this parameter? If so, is there any other way to get it?
  4. Why does a custom View work in a similar way to setContenxtView (MyGLSurfaceView (this)?

3. Principles of Exploration

What is a 3.1 AttributeSet?

In AS, press Ctrl + B (Windows) / Command + B (MacOS) against the AttributeSet and jump to its definition.

/**
 * A collection of attributes, as found associated with a tag in an XML
 * document.  Often you will not want to use this interface directly, instead
 * passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
 * Resources.Theme.obtainStyledAttributes()}
 * which will take care of parsing the attributes for you.  In particular,
 * the Resources API will convert resource references (attribute values such as
 * "@string/my_label" in the original XML) to the desired type
 * for you; if you use AttributeSet directly then you will need to manually
 * check for resource references
 * (with {@link #getAttributeResourceValue(int, int)}) and do the resource
 * lookup yourself if needed.  Direct use of AttributeSet also prevents the
 * application of themes and styles when retrieving attribute values.
 * 
 * <p>This interface provides an efficient mechanism for retrieving
 * data from compiled XML files, which can be retrieved for a particular
 * XmlPullParser through {@link Xml#asAttributeSet
 * Xml.asAttributeSet()}.  Normally this will return an implementation
 * of the interface that works on top of a generic XmlPullParser, however it
 * is more useful in conjunction with compiled XML resources:
 * 
 * <pre>
 * XmlPullParser parser = resources.getXml(myResource);
 * AttributeSet attributes = Xml.asAttributeSet(parser);</pre>
 * 
 * <p>The implementation returned here, unlike using
 * the implementation on top of a generic XmlPullParser,
 * is highly optimized by retrieving pre-computed information that was
 * generated by aapt when compiling your resources.  For example,
 * the {@link #getAttributeFloatValue(int, float)} method returns a floating
 * point number previous stored in the compiled resource instead of parsing
 * at runtime the string originally in the XML file.
 * 
 * <p>This interface also provides additional information contained in the
 * compiled XML resource that is not available in a normal XML file, such
 * as {@link #getAttributeNameResource(int)} which returns the resource
 * identifier associated with a particular XML attribute name.
 *
 * @see XmlPullParser
 */

This is a collection of attributes that are configured for a View in XML. For example, android:id="@+id/my_gl_surface_view", android:layout_width="match_parent", android:layout_height="match_parent", which is parsed into Java objects by the XML Parser, and AttributeSet is the collection of these objects.

This answers the first 1, 2, 3, 4 questions:

  1. This parameter, when used to convert XML to Java objects, passes descriptive information about the view instance in XML, including its id, width, height...
  2. Obviously, this parameter must be present in the way View is drawn through XML.
  3. Without this parameter, the IDS specified by android:id="@+id/my_gl_surface_view will naturally not take effect on the view; what if you have to get them? Since you can get the parent view, there must be a way. What I'm thinking about now is traversing through the rootView.children to get the child view object. But to be honest, there's no need to beat yourself up like this.
  4. Obviously, code is certainly not available.

IV. Concluding remarks

It feels like the case was resolved quickly. It looks like some scratch...

In fact, the problem is that it is not difficult to understand how XML works to UI.

Topics: Android bug