javaandroid-view

generic function to traverse a View hierarchy and find contained Views of a specific type


My outer problem, in Java 1.8, is I want to traverse (or spider) a tree of View objects and call a callback on each one of a specific type. Here's an example caller:

        findEachView(main.popupForm, (EditText v) -> {
            CharSequence error = v.getError();

            if (error != null)
                assertNull((String) v.getTag(), error.toString());
        });

That is supposed to traverse all the controls on the popupForm and, for each one that is an EditText, call my callback which asserts that it does not have an error.

The inner problem is my generic method to do it has a syntax error:

static <T extends View> void findEachView(@NonNull ViewGroup view, @NonNull Consumer<T> viewAdjustorVisitor) {
    for (int i = 0; i < view.getChildCount(); i++) {
        View v = view.getChildAt(i);

        if (v instanceof T)  // ERROR:  reifiable types in instanceof are not supported in -source 8
            viewAdjustorVisitor.accept((T) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, viewAdjustorVisitor);
    }
}

So my outer question is: How to efficiently and typesafely rip every View of the given type?

And my inner question is: How to get the generics to not throw away type information and let me call instanceof on a type that I know is reified?


We can't use isAssignableFrom() because it takes an object of type T and my template doesn't have one. Its point is to downcast a View into a T, not mess with a T object that already exists. And I can't create a new object because T is "not reified":

if (v.getClass().isAssignableFrom(new T()))

So maybe someone could answer the outer question?


when I add the ? we get this, with the next syntax error. I think this is an unanswered question in Java 1.8 template theory

static <T extends View> void findEachView(@NonNull
         ViewGroup viewGroup, @NonNull
         Consumer<Class<? extends View>> visitor) {
    for (int i = 0; i < viewGroup.getChildCount(); i++) {
        View v = viewGroup.getChildAt(i);

        if (null != (Class <? extends android.view.View>) v)  // ERROR:  incompatible types: View cannot be converted to Class<? extends View>
            visitor.accept((Class <? extends android.view.View>) v);

        if (v instanceof ViewGroup)
            findEachView((ViewGroup) v, visitor);
    }
}

Solution

  • I think you're making this more complicated than it needs to be. The simplest solution is just to pass in the class that you're looking for.

    As a general rule, if you find that you need type information that isn't available, then pass it in. Much like you would do for any other information that a method needs.

    Working with generic type information is always tricky because of type erasure, as others have stated. It's generally best to pass in the class explicitly in these situations. If you absolutely need to know what the generic type of something is, then you can do what GSON does and use a subclass wrapper to access the superclass's generic information via SubClass.class.getGenericSuperclass(). But this is a bad idea which introduces bloat and is non-intuitive.

    I would also suggest that checking runtime type information is a code smell. You should do what you can to avoid this stuff since it makes it harder to optimize (cannot merge classes, for example) and is inherently fragile. Try to see if you can accomplish the same work with an interface default method, an abstract base class, or something similar. If at all possible, use standard OOP methods to do the work and avoid explicitly relying on runtime type information.

    // Aim for something like this which is common in Android-land.
    findEachView(main.popupForm, EditText.class, (EditText v) -> { ... });
    
    static <T extends View> void findEachView(
             @NonNull ViewGroup viewGroup,
             @NonNull Class<T> clz,
             @NonNull Consumer<? super T> visitor) {
      ...
      if (clz.isInstance(v)) {
        // do stuff
      }
      ...
    }
    
    

    Thanks! I'm going to add my working code to your answer so you get the experience points.

    My previous code used string surgery to match the class name, so I was trying to take that argument out:

        findEachView(
                viewGroup,
                "EditText",
                (view) -> ((EditText) view).setError(null));
    

    Your fix allows a call like this, which is indeed cleaner because it has one fewer typecast:

        findEachView(
                viewGroup,
                EditText.class,
                (view) -> view.setError(null));
    

    So the winning code is:

    static <T extends View> void findEachView(
                                     @NonNull ViewGroup view,
                                     Class<T> clz,
                                     @NonNull Consumer<? super T> visitor) {
        for (int i = 0; i < view.getChildCount(); i++) {
            View v = view.getChildAt(i);
    
            if (clz.isInstance(v))
                visitor.accept((T) v);  //  this cast gives a warning that the committee will ignore
    
            if (v instanceof ViewGroup)
                findEachView((ViewGroup) v, clz, visitor);
        }
    }  //  note this does not visit the top level item
    

    One last edit: The lambda call is too clever; we should just return a list. ChatGPT offered this:

    @SuppressWarnings("unchecked")
    public static <T> List<T> findViewDescendants(ViewGroup root, Class<T> targetClass) {
        List<T> descendants = new ArrayList<>();
        int childCount = root.getChildCount();
    
        for (int i = 0; i < childCount; i++) {
            View child = root.getChildAt(i);
    
            if (targetClass.isInstance(child))
                descendants.add((T) child);  //  this cast gives a warning that the committee will ignore
    
            if (child instanceof ViewGroup)
                descendants.addAll(findViewDescendants((ViewGroup) child, targetClass));
        }
    
        return descendants;
    }